You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12564 lines
484 KiB

-- *********************************************************
-- ** Deadly Boss Mods - Core **
-- ** http://www.deadlybossmods.com **
-- ** https://www.patreon.com/deadlybossmods **
-- *********************************************************
--
-- This addon is written and copyrighted by:
-- * Paul Emmerich (Tandanu @ EU-Aegwynn) (DBM-Core)
-- * Martin Verges (Nitram @ EU-Azshara) (DBM-GUI)
-- * Adam Williams (Omegal @ US-Whisperwind) (Primary boss mod author & DBM maintainer)
--
-- The localizations are written by:
-- * enGB/enUS: Omegal Twitter @MysticalOS
-- * deDE: Ebmor http://www.deadlybossmods.com/forum/memberlist.php?mode=viewprofile&u=79
-- * ruRU: TOM_RUS http://www.curseforge.com/profiles/TOM_RUS/
-- * zhTW: Whyv ultrashining@gmail.com
-- * koKR: Elnarfim ---
-- * zhCN: Mini Dragon projecteurs@gmail.com
--
--
-- Special thanks to:
-- * Arta
-- * Tennberg (a lot of fixes in the enGB/enUS localization)
-- * nBlueWiz (a lot of previous fixes in the koKR localization as well as boss mod work) Contact: everfinale@gmail.com
--
--
-- The code of this addon is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License. (see license.txt)
-- 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.
--
--
-- You are free:
-- * to Share - to copy, distribute, display, and perform the work
-- * to Remix - to make derivative works
-- Under the following conditions:
-- * 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)
-- * Noncommercial. You may not use this work for commercial purposes.
-- * 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.
--
local L = DBM_CORE_L
-------------------------------
-- Globals/Default Options --
-------------------------------
local function releaseDate(year, month, day, hour, minute, second)
hour = hour or 0
minute = minute or 0
second = second or 0
return second + minute * 10^2 + hour * 10^4 + day * 10^6 + month * 10^8 + year * 10^10
end
local function parseCurseDate(date)
date = tostring(date)
if #date == 13 then
-- support for broken curse timestamps: leading 0 in hours is missing...
date = date:sub(1, 8) .. "0" .. date:sub(9, #date)
end
local year, month, day, hour, minute, second = tonumber(date:sub(1, 4)), tonumber(date:sub(5, 6)), tonumber(date:sub(7, 8)), tonumber(date:sub(9, 10)), tonumber(date:sub(11, 12)), tonumber(date:sub(13, 14))
if year and month and day and hour and minute and second then
return releaseDate(year, month, day, hour, minute, second)
end
end
local function showRealDate(curseDate)
curseDate = tostring(curseDate)
local year, month, day, hour, minute, second = curseDate:sub(1, 4), curseDate:sub(5, 6), curseDate:sub(7, 8), curseDate:sub(9, 10), curseDate:sub(11, 12), curseDate:sub(13, 14)
if year and month and day and hour and minute and second then
return year.."/"..month.."/"..day.." "..hour..":"..minute..":"..second
end
end
DBM = {
Revision = parseCurseDate("20210701004539"),
DisplayVersion = "9.1.1", -- the string that is shown as version
ReleaseRevision = releaseDate(2021, 6, 30) -- the date of the latest stable version that is available, optionally pass hours, minutes, and seconds for multiple releases in one day
}
DBM.HighestRelease = DBM.ReleaseRevision --Updated if newer version is detected, used by update nags to reflect critical fixes user is missing on boss pulls
-- support for github downloads, which doesn't support curse keyword expansion
-- just use the latest release revision
if not DBM.Revision then
DBM.Revision = DBM.ReleaseRevision
end
function DBM:ShowRealDate(curseDate)
return showRealDate(curseDate)
end
function DBM:ReleaseDate(year, month, day, hour, minute, second)
return releaseDate(year, month, day, hour, minute, second)
end
local wowVersionString, wowBuild, _, wowTOC = GetBuildInfo()
local testBuild = false
if IsTestBuild() then
testBuild = true
end
function DBM:GetTOC()
return wowTOC, testBuild, wowVersionString, wowBuild
end
do
local isShadowlandsClient = BackdropTemplateMixin and true or false
function DBM:IsShadowlands()
return isShadowlandsClient
end
end
-- dual profile setup
local _, playerClass = UnitClass("player")
DBM_UseDualProfile = true
if playerClass == "MAGE" or playerClass == "WARLOCK" or playerClass == "ROGUE" then
DBM_UseDualProfile = false
end
DBM_CharSavedRevision = 2
--Hard code STANDARD_TEXT_FONT since skinning mods like to taint it (or worse, set it to nil, wtf?)
local standardFont
if LOCALE_koKR then
standardFont = "Fonts\\2002.TTF"
elseif LOCALE_zhCN then
standardFont = "Fonts\\ARKai_T.ttf"
elseif LOCALE_zhTW then
standardFont = "Fonts\\blei00d.TTF"
elseif LOCALE_ruRU then
standardFont = "Fonts\\FRIZQT___CYR.TTF"
else
standardFont = "Fonts\\FRIZQT__.TTF"
end
DBM.DefaultOptions = {
WarningColors = {
{r = 0.41, g = 0.80, b = 0.94}, -- Color 1 - #69CCF0 - Turqoise
{r = 0.95, g = 0.95, b = 0.00}, -- Color 2 - #F2F200 - Yellow
{r = 1.00, g = 0.50, b = 0.00}, -- Color 3 - #FF8000 - Orange
{r = 1.00, g = 0.10, b = 0.10}, -- Color 4 - #FF1A1A - Red
},
RaidWarningSound = 11742,--"Sound\\Doodad\\BellTollNightElf.ogg"
SpecialWarningSound = 8174,--"Sound\\Spells\\PVPFlagTaken.ogg"
SpecialWarningSound2 = 15391,--"Sound\\Creature\\AlgalonTheObserver\\UR_Algalon_BHole01.ogg"
SpecialWarningSound3 = "Interface\\AddOns\\DBM-Core\\sounds\\AirHorn.ogg",
SpecialWarningSound4 = 9278,--"Sound\\Creature\\HoodWolf\\HoodWolfTransformPlayer01.ogg"
SpecialWarningSound5 = 128466,--"Sound\\Creature\\Loathstare\\Loa_Naxx_Aggro02.ogg"
ModelSoundValue = "Short",
CountdownVoice = "Corsica",
CountdownVoice2 = "Kolt",
CountdownVoice3 = "Smooth",
ChosenVoicePack = "None",
VoiceOverSpecW2 = "DefaultOnly",
AlwaysPlayVoice = false,
EventSoundVictory2 = "Interface\\AddOns\\DBM-Core\\sounds\\Victory\\SmoothMcGroove_Fanfare.ogg",
EventSoundWipe = "None",
EventSoundEngage2 = "None",
EventSoundMusic = "None",
EventSoundDungeonBGM = "None",
EventSoundMusicCombined = false,
EventDungMusicMythicFilter = true,
EventMusicMythicFilter = true,
Enabled = true,
ShowWarningsInChat = true,
ShowSWarningsInChat = true,
WarningIconLeft = true,
WarningIconRight = true,
WarningIconChat = true,
WarningAlphabetical = true,
WarningShortText = true,
StripServerName = true,
ShowAllVersions = true,
ShowReminders = true,
ShowPizzaMessage = true,
ShowEngageMessage = true,
ShowDefeatMessage = true,
ShowGuildMessages = true,
ShowGuildMessagesPlus = false,
AutoRespond = true,
EnableWBSharing = true,
WhisperStats = false,
DisableStatusWhisper = false,
DisableGuildStatus = false,
HideBossEmoteFrame2 = true,
SWarningAlphabetical = true,
SWarnNameInNote = true,
CustomSounds = 0,
FilterTankSpec = true,
FilterInterrupt2 = "TandFandBossCooldown",
FilterInterruptNoteName = false,
FilterDispel = true,
FilterTrashWarnings2 = true,
FilterVoidFormSay = true,
--FilterSelfHud = true,
AutologBosses = false,
AdvancedAutologBosses = false,
RecordOnlyBosses = false,
LogOnlyNonTrivial = true,
UseSoundChannel = "Master",
LFDEnhance = true,
WorldBossNearAlert = false,
RLReadyCheckSound = true,
AFKHealthWarning = false,
AutoReplySound = true,
HideObjectivesFrame = true,
HideGarrisonToasts = true,
HideGuildChallengeUpdates = true,
HideTooltips = false,
DisableSFX = false,
EnableModels = true,
GUIWidth = 800,
GUIHeight = 600,
RangeFrameFrames = "radar",
RangeFrameUpdates = "Average",
RangeFramePoint = "CENTER",
RangeFrameX = 50,
RangeFrameY = -50,
RangeFrameSound1 = "none",
RangeFrameSound2 = "none",
RangeFrameLocked = false,
RangeFrameRadarPoint = "CENTER",
RangeFrameRadarX = 100,
RangeFrameRadarY = -100,
InfoFramePoint = "CENTER",
InfoFrameX = 75,
InfoFrameY = -75,
InfoFrameShowSelf = false,
InfoFrameLines = 0,
InfoFrameCols = 0,
InfoFrameFont = "standardFont",
InfoFrameFontSize = 12,
InfoFrameFontStyle = "None",
WarningDuration2 = 1.5,
WarningPoint = "CENTER",
WarningX = 0,
WarningY = 260,
WarningFont = "standardFont",
WarningFontSize = 20,
WarningFontStyle = "None",
WarningFontShadow = true,
SpecialWarningDuration2 = 1.5,
SpecialWarningPoint = "CENTER",
SpecialWarningX = 0,
SpecialWarningY = 75,
SpecialWarningFont = "standardFont",
SpecialWarningFontSize2 = 35,
SpecialWarningFontStyle = "THICKOUTLINE",
SpecialWarningFontShadow = false,
SpecialWarningIcon = true,
SpecialWarningShortText = true,
SpecialWarningFontCol = {1.0, 0.7, 0.0},--Yellow, with a tint of orange
SpecialWarningFlashCol1 = {1.0, 1.0, 0.0},--Yellow
SpecialWarningFlashCol2 = {1.0, 0.5, 0.0},--Orange
SpecialWarningFlashCol3 = {1.0, 0.0, 0.0},--Red
SpecialWarningFlashCol4 = {1.0, 0.0, 1.0},--Purple
SpecialWarningFlashCol5 = {0.2, 1.0, 1.0},--Tealish
SpecialWarningFlashDura1 = 0.4,
SpecialWarningFlashDura2 = 0.4,
SpecialWarningFlashDura3 = 1,
SpecialWarningFlashDura4 = 0.7,
SpecialWarningFlashDura5 = 1,
SpecialWarningFlashAlph1 = 0.3,
SpecialWarningFlashAlph2 = 0.3,
SpecialWarningFlashAlph3 = 0.4,
SpecialWarningFlashAlph4 = 0.4,
SpecialWarningFlashAlph5 = 0.5,
SpecialWarningFlash1 = true,
SpecialWarningFlash2 = true,
SpecialWarningFlash3 = true,
SpecialWarningFlash4 = true,
SpecialWarningFlash5 = true,
SpecialWarningFlashCount1 = 1,
SpecialWarningFlashCount2 = 1,
SpecialWarningFlashCount3 = 3,
SpecialWarningFlashCount4 = 2,
SpecialWarningFlashCount5 = 3,
SWarnClassColor = true,
ArrowPosX = 0,
ArrowPosY = -150,
ArrowPoint = "TOP",
-- global boss mod settings (overrides mod-specific settings for some options)
DontShowBossAnnounces = false,
DontShowTargetAnnouncements = true,
DontShowSpecialWarningText = false,
DontShowSpecialWarningFlash = false,
DontPlaySpecialWarningSound = false,
DontPlayTrivialSpecialWarningSound = true,
DontShowBossTimers = false,
DontShowUserTimers = false,
DontShowFarWarnings = true,
DontSetIcons = false,
DontRestoreIcons = false,
DontShowRangeFrame = false,
DontRestoreRange = false,
DontShowInfoFrame = false,
DontShowHudMap2 = false,
DontShowNameplateIcons = false,
UseNameplateHandoff = true,
NPAuraSize = 40,
DontPlayCountdowns = false,
DontSendYells = false,
BlockNoteShare = false,
DontShowPT2 = false,
DontShowPTCountdownText = false,
DontPlayPTCountdown = false,
DontShowPTText = false,
DontShowPTNoID = false,
PTCountThreshold2 = 5,
LatencyThreshold = 250,
oRA3AnnounceConsumables = false,
SettingsMessageShown = false,
ForumsMessageShown = false,
AlwaysShowSpeedKillTimer2 = false,
ShowRespawn = true,
ShowQueuePop = true,
HelpMessageVersion = 3,
MoviesSeen = {},
MovieFilter2 = "OnlyFight",
LastRevision = 0,
DebugMode = false,
DebugLevel = 1,
RoleSpecAlert = true,
CheckGear = true,
WorldBossAlert = false,
BadTimerAlert = false,
BadIDAlert = false,
AutoAcceptFriendInvite = false,
AutoAcceptGuildInvite = false,
FakeBWVersion = false,
AITimer = true,
ShortTimerText = true,
ChatFrame = "DEFAULT_CHAT_FRAME",
CoreSavedRevision = 1,
SilentMode = false,
}
DBM.Mods = {}
DBM.ModLists = {}
DBM.Counts = {
{ text = "Corsica",value = "Corsica", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\", max = 10},
{ text = "Koltrane",value = "Kolt", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\", max = 10},
{ text = "Smooth",value = "Smooth", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Smooth\\", max = 10},
{ text = "Smooth (Reverb)",value = "SmoothR", path = "Interface\\AddOns\\DBM-Core\\Sounds\\SmoothReverb\\", max = 10},
{ text = "Pewsey",value = "Pewsey", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Pewsey\\", max = 10},
{ text = "Bear (Child)",value = "Bear", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Bear\\", max = 10},
{ text = "Moshne", value = "Mosh", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Mosh\\", max = 5},
{ text = "Anshlun (ptBR)",value = "Anshlun", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Anshlun\\", max = 10},
{ text = "Neryssa (ptBR)",value = "Neryssa", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Neryssa\\", max = 10},
}
--Sounds use SoundKit Ids (not file data ids)
DBM.Victory = {
{text = L.NONE,value = "None"},
{text = L.RANDOM,value = "Random"},
{text = "Blakbyrd: FF Fanfare",value = "Interface\\AddOns\\DBM-Core\\sounds\\Victory\\bbvictory.ogg", length=4},
{text = "SMG: FF Fanfare",value = "Interface\\AddOns\\DBM-Core\\sounds\\Victory\\SmoothMcGroove_Fanfare.ogg", length=4},
}
DBM.Defeat = {
{text = L.NONE,value = "None"},
{text = L.RANDOM,value = "Random"},
{text = "Alizabal: Incompetent Raiders",value = 25780, length=4},--"Sound\\Creature\\ALIZABAL\\VO_BH_ALIZABAL_RESET_01.ogg"
{text = "Bwonsamdi: Over Your Head",value = 109293, length=4},--"Sound\\Creature\\bwonsamdi\\vo_801_bwonsamdi_35_m.ogg"
{text = "Bwonsamdi: Pour Little Thing",value = 109295, length=4},--"Sound\\Creature\\bwonsamdi\\vo_801_bwonsamdi_37_m.ogg"
{text = "Bwonsamdi: Impressive Death",value = 109296, length=4},--"Sound\\Creature\\bwonsamdi\\vo_801_bwonsamdi_38_m.ogg"
{text = "Bwonsamdi: All That Armor",value = 109308, length=4},--"Sound\\Creature\\bwonsamdi\\vo_801_bwonsamdi_50_m.ogg"
{text = "Kologarn: You Fail",value = 15588, length=4},--"Sound\\Creature\\Kologarn\\UR_Kologarn_Slay02.ogg"
{text = "Hodir: Tragic",value = 15553, length=4},--"Sound\\Creature\\Hodir\\UR_Hodir_Slay01.ogg"
{text = "Scrollsage Nola: Cycle",value = 109069, length=4},--"sound/creature/scrollsage_nola/vo_801_scrollsage_nola_34_f.ogg"
{text = "Thorim: Failures",value = 15742, length=4},--"Sound\\Creature\\Thorim\\UR_Thorim_P1Wipe01.ogg"
{text = "Valithria: Failures",value = 17067, length=4},--"Sound\\Creature\\ValithriaDreamwalker\\IC_Valithria_Berserk01.ogg"
{text = "Yogg-Saron: Laugh",value = 15757, length=4},--"Sound\\Creature\\YoggSaron\\UR_YoggSaron_Slay01.ogg"
}
DBM.DungeonMusic = {--Filtered list of media assigned to dungeon/raid background music catagory
{text = L.NONE,value = "None"},
{text = L.RANDOM,value = "Random"},
{text = "Anduin Part 1 B",value = 1417242, length=140},--"sound\\music\\Legion\\MUS_70_AnduinPt1_B.mp3" Soundkit: 68230
{text = "Nightsong",value = 441705, length=160},--"Sound\\Music\\cataclysm\\MUS_NightElves_GU01.mp3" Soundkit: 71181
{text = "Ulduar: Titan Orchestra",value = 298910, length=102},--"Sound\\Music\\ZoneMusic\\UlduarRaidInt\\UR_TitanOrchestraIntro.mp3" Soundkit: 15873
}
DBM.BattleMusic = {--Filtered list of media assigned to boss/encounter background music catagory
{text = L.NONE,value = "None"},
{text = L.RANDOM,value = "Random"},
{text = "Anduin Part 2 B",value = 1417248, length=111},--"sound\\music\\Legion\\MUS_70_AnduinPt2_B.mp3" Soundkit: 68230
{text = "Bronze Jam",value = 350021, length=116},--"Sound\\Music\\ZoneMusic\\IcecrownRaid\\IR_BronzeJam.mp3" Soundkit: 118800
{text = "Invincible",value = 1100052, length=197},--"Sound\\Music\\Draenor\\MUS_Invincible.mp3" Soundkit: 49536
}
--Music uses file data IDs
DBM.Music = {--Contains all music media, period
{text = L.NONE,value = "None"},
{text = L.RANDOM,value = "Random"},
{text = "Anduin Part 1 B",value = 1417242, length=140},--"sound\\music\\Legion\\MUS_70_AnduinPt1_B.mp3" Soundkit: 68230
{text = "Anduin Part 2 B",value = 1417248, length=111},--"sound\\music\\Legion\\MUS_70_AnduinPt2_B.mp3" Soundkit: 68230
{text = "Bronze Jam",value = 350021, length=116},--"Sound\\Music\\ZoneMusic\\IcecrownRaid\\IR_BronzeJam.mp3" Soundkit: 118800
{text = "Invincible",value = 1100052, length=197},--"Sound\\Music\\Draenor\\MUS_Invincible.mp3" Soundkit: 49536
{text = "Nightsong",value = 441705, length=160},--"Sound\\Music\\cataclysm\\MUS_NightElves_GU01.mp3" Soundkit: 71181
{text = "Ulduar: Titan Orchestra",value = 298910, length=102},--"Sound\\Music\\ZoneMusic\\UlduarRaidInt\\UR_TitanOrchestraIntro.mp3" Soundkit: 15873
}
------------------------
-- Global Identifiers --
------------------------
DBM_DISABLE_ZONE_DETECTION = newproxy(false)
DBM_OPTION_SPACER = newproxy(false)
--------------
-- Locals --
--------------
local bossModPrototype = {}
local usedProfile = "Default"
local dbmIsEnabled = true
local lastCombatStarted = GetTime()
local loadcIds = {}
local inCombat = {}
local oocBWComms = {}
local combatInfo = {}
local bossIds = {}
local updateFunctions = {}
local raid = {}
local modSyncSpam = {}
local autoRespondSpam = {}
local chatPrefix = "<Deadly Boss Mods> "
local chatPrefixShort = "<DBM> "
--local ver = ("%s (%s)"):format(DBM.DisplayVersion, tostring(DBM.Revision))
local mainFrame = CreateFrame("Frame", "DBMMainFrame")
local newerVersionPerson = {}
local newerRevisionPerson = {}
local combatInitialized = false
local healthCombatInitialized = false
local pformat
local schedulerFrame = CreateFrame("Frame", "DBMScheduler")
schedulerFrame:Hide()
local startScheduler
local schedule
local unschedule
local unscheduleAll
local scheduleCountdown
local scheduleRepeat
local loadOptions
local checkWipe
local checkBossHealth
local checkCustomBossHealth
local fireEvent
local playerName = UnitName("player")
local playerLevel = UnitLevel("player")
local playerRealm = GetRealmName()
local LastInstanceMapID = -1
local LastGroupSize = 0
local LastInstanceType
local queuedBattlefield = {}
local watchFrameRestore = false
local bossHealth = {}
local bossHealthuIdCache = {}
local bossuIdCache = {}
local savedDifficulty, difficultyText, difficultyIndex, difficultyModifier = nil, nil, nil, 0
local lastBossEngage = {}
local lastBossDefeat = {}
local bossuIdFound = false
local timerRequestInProgress = false
local updateNotificationDisplayed = 0
local showConstantReminder = 0
local SWFilterDisabed = 11
local currentSpecID, currentSpecName, currentSpecGroup
local cSyncSender = {}
local cSyncReceived = 0
local eeSyncSender = {}
local eeSyncReceived = 0
local canSetIcons = {}
local iconSetRevision = {}
local iconSetPerson = {}
local addsGUIDs = {}
local targetEventsRegistered = false
local targetMonitor = {}
local statusWhisperDisabled = false
local statusGuildDisabled = false
local dbmToc = 0
local breakTimerStart
local AddMsg
local delayedFunction
local dataBroker
local voiceSessionDisabled = false
local handleSync
local fakeBWVersion, fakeBWHash = 199, "d06bd70"--199.1
local bwVersionResponseString = "V^%d^%s"
local enableIcons = true -- set to false when a raid leader or a promoted player has a newer version of DBM
local bannedMods = { -- a list of "banned" (meaning they are replaced by another mod or discontinued). These mods will not be loaded by DBM (and they wont show up in the GUI)
"DBM-Battlegrounds", --replaced by DBM-PvP
-- ZG and ZA are now part of the party mods for Cataclysm
"DBM-ZulAman",
"DBM-ZG",
"DBM-SiegeOfOrgrimmar",--Block legacy version. New version is "DBM-SiegeOfOrgrimmarV2"
"DBM-HighMail",
"DBM-ProvingGrounds-MoP",--Renamed to DBM-ProvingGrounds in 6.0 version since blizzard updated content for WoD
"DBM-ProvingGrounds",--Renamed to DBM-Challenges going forward to include proving grounds and any new single player challendges of similar design such as mage tower artifact quests
"DBM-VPKiwiBeta",--Renamed to DBM-VPKiwi in final version.
"DBM-Suramar",--Renamed to DBM-Nighthold
"DBM-KulTiras",--Merged to DBM-BfA
"DBM-Zandalar",--Merged to DBM-BfA
"DBM-Azeroth",--Merged into DBM-Core events mod.
"DBM-Argus",--Merged into DBM-BrokenIsles mod
"DBM-GarrisonInvasions",--Merged into DBM-Draenor mod
"DBM-Azeroth-BfA",--renamed to DBM-BfA
"DBM-BattlefieldBarrens",--Apparently people are still running this
}
--[InstanceID]={level,zoneType}
--zoneType: 1 = outdoor, 2 = dungeon, 3 = raid
local instanceDifficultyBylevel = {
--World
[0]={50,1},[1]={50, 1},--Eastern Kingdoms and Azeroth world events. These would be warfront and aniversery world bosses, so they'd be set to 50 for now. Likely 60 next year
[530]={30, 1},--Outlands World Bosses
[870]={30, 1},[1064]={30, 1},--MoP World Bosses
[1116]={40, 1},[1159]={40, 1},[1331]={40, 1},[1158]={40, 1},[1153]={40, 1},[1152]={40, 1},[1330]={40, 1},[1160]={40, 1},[1154]={40, 1},[1464]={40, 1},--Wod World and Garrison Bosses
[1220]={45, 1},[1779]={45, 1},--Legion World bosses
[1643]={50, 1},[1642]={50, 1},[1718]={50, 1},[1943]={50, 1},[1876]={50, 1},[2105]={50, 1},[2111]={50, 1},[2275]={50, 1},--Bfa World bosses and warfronts
--Raids
[509]={30, 3},[531]={30, 3},[469]={30, 3},[409]={30, 3},--Classic Raids
[564]={30, 3},[534]={30, 3},[532]={30, 3},[565]={30, 3},[544]={30, 3},[548]={30, 3},[580]={30, 3},[550]={30, 3},--BC Raids
[615]={30, 3},[724]={30, 3},[649]={30, 3},[616]={30, 3},[631]={30, 3},[533]={30, 3},[249]={30, 3},[603]={30, 3},[624]={30, 3},--Wrath Raids
[757]={35, 3},[671]={35, 3},[669]={35, 3},[967]={35, 3},[720]={35, 3},[951]={35, 3},[754]={35, 3},--Cata Raids
[1009]={35, 3},[1008]={35, 3},[1136]={35, 3},[996]={35, 3},[1098]={35, 3},--MoP Raids
[1205]={40, 3},[1448]={40, 3},[1228]={40, 3},--WoD Raids (yes, only 3 kekw)
[1712]={50, 3},[1520]={50, 3},[1530]={50, 3},[1676]={50, 3},[1648]={50, 3},--Legion Raids (Set to 50 because 45 tuning makes them difficult even at 50)
[1861]={50, 3},[2070]={50, 3},[2096]={50, 3},[2164]={50, 3},[2217]={50, 3},--BfA Raids
[2296]={60, 3},[2450]={60, 3},--Shadowlands Raids
--Dungeons
[48]={30, 2},[230]={30, 2},[429]={30, 2},[389]={30, 2},[34]={30, 2},--Classic Dungeons
[540]={30, 2},[558]={30, 2},[556]={30, 2},[555]={30, 2},[542]={30, 2},[546]={30, 2},[545]={30, 2},[547]={30, 2},[553]={30, 2},[554]={30, 2},[552]={30, 2},[557]={30, 2},[269]={30, 2},[560]={30, 2},[543]={30, 2},[585]={30, 2},--BC Dungeons
[619]={30, 2},[601]={30, 2},[595]={30, 2},[600]={30, 2},[604]={30, 2},[602]={30, 2},[599]={30, 2},[576]={30, 2},[578]={30, 2},[574]={30, 2},[575]={30, 2},[608]={30, 2},[658]={30, 2},[632]={30, 2},[668]={30, 2},[650]={30, 2},--Wrath Dungeons
[755]={35, 2},[645]={35, 2},[36]={35, 2},[670]={35, 2},[644]={35, 2},[33]={35, 2},[643]={35, 2},[725]={35, 2},[657]={35, 2},[309]={35, 2},[859]={35, 2},[568]={35, 2},[938]={35, 2},[940]={35, 2},[939]={35, 2},[646]={35, 2},--Cata Dungeons
[960]={35, 2},[961]={35, 2},[959]={35, 2},[962]={35, 2},[994]={35, 2},[1011]={35, 2},[1007]={35, 2},[1001]={35, 2},[1004]={35, 2},--MoP Dungeons
[1182]={40, 2},[1175]={40, 2},[1208]={40, 2},[1195]={40, 2},[1279]={40, 2},[1176]={40, 2},[1209]={40, 2},[1358]={40, 2},--WoD Dungeons
[1501]={45, 2},[1466]={45, 2},[1456]={45, 2},[1477]={45, 2},[1458]={45, 2},[1516]={45, 2},[1571]={45, 2},[1492]={45, 2},[1544]={45, 2},[1493]={45, 2},[1651]={45, 2},[1677]={45, 2},[1753]={45, 2},--Legion Dungeons
[1763]={50, 2},[1754]={50, 2},[1762]={50, 2},[1864]={50, 2},[1822]={50, 2},[1877]={50, 2},[1594]={50, 2},[1841]={50, 2},[1771]={50, 2},[1862]={50, 2},[2097]={50, 2},--Bfa Dungeons
[2286]={60, 2},[2289]={60, 2},[2290]={60, 2},[2287]={60, 2},[2285]={60, 2},[2293]={60, 2},[2291]={60, 2},[2284]={60, 2},[2441]={60, 2},--Shadowlands Dungeons
}
-----------------
-- Libraries --
-----------------
local LibStub = _G["LibStub"]
local LL
if LibStub("LibLatency", true) then
LL = LibStub("LibLatency")
end
local LD
if LibStub("LibDurability", true) then
LD = LibStub("LibDurability")
end
--------------------------------------------------------
-- Cache frequently used global variables in locals --
--------------------------------------------------------
local DBM = DBM
-- these global functions are accessed all the time by the event handler
-- so caching them is worth the effort
local ipairs, pairs, next = ipairs, pairs, next
local tonumber, tostring = tonumber, tostring
local tinsert, tremove, twipe, tsort, tconcat = table.insert, table.remove, table.wipe, table.sort, table.concat
local type, select = type, select
local GetTime = GetTime
local bband = bit.band
local floor, mhuge, mmin, mmax, mrandom = math.floor, math.huge, math.min, math.max, math.random
local GetNumGroupMembers, GetRaidRosterInfo = GetNumGroupMembers, GetRaidRosterInfo
local UnitName, GetUnitName = UnitName, GetUnitName
local IsInRaid, IsInGroup, IsInInstance = IsInRaid, IsInGroup, IsInInstance
local UnitAffectingCombat, InCombatLockdown, IsFalling, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty = UnitAffectingCombat, InCombatLockdown, IsFalling, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty
local UnitGUID, UnitHealth, UnitHealthMax, UnitBuff, UnitDebuff, UnitAura = UnitGUID, UnitHealth, UnitHealthMax, UnitBuff, UnitDebuff, UnitAura
local UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit = UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit
local GetSpellInfo, GetDungeonInfo, GetSpellTexture, GetSpellCooldown = GetSpellInfo, C_LFGInfo and C_LFGInfo.GetDungeonInfo or GetDungeonInfo, GetSpellTexture, GetSpellCooldown
local EJ_GetEncounterInfo, EJ_GetCreatureInfo = EJ_GetEncounterInfo, EJ_GetCreatureInfo
local EJ_GetSectionInfo, GetSectionIconFlags
if C_EncounterJournal then
EJ_GetSectionInfo, GetSectionIconFlags = C_EncounterJournal.GetSectionInfo, C_EncounterJournal.GetSectionIconFlags
end
local GetInstanceInfo = GetInstanceInfo
local GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID = GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID
local UnitDetailedThreatSituation = UnitDetailedThreatSituation
local UnitIsGroupLeader, UnitIsGroupAssistant = UnitIsGroupLeader, UnitIsGroupAssistant
local PlaySoundFile, PlaySound = PlaySoundFile, PlaySound
local Ambiguate = Ambiguate
local C_TimerNewTicker, C_TimerAfter = C_Timer.NewTicker, C_Timer.After
local IsQuestFlaggedCompleted = C_QuestLog.IsQuestFlaggedCompleted
local SendAddonMessage = C_ChatInfo.SendAddonMessage
local RAID_CLASS_COLORS = _G["CUSTOM_CLASS_COLORS"] or RAID_CLASS_COLORS-- for Phanx' Class Colors
---------------------------------
-- General (local) functions --
---------------------------------
-- checks if a given value is in an array
-- returns true if it finds the value, false otherwise
local function checkEntry(t, val)
for _, v in ipairs(t) do
if v == val then
return true
end
end
return false
end
local function findEntry(t, val)
for _, v in ipairs(t) do
if v and val and val:find(v) then
return true
end
end
return false
end
-- removes all occurrences of a value in an array
-- returns true if at least one occurrence was remove, false otherwise
local function removeEntry(t, val)
local existed = false
for i = #t, 1, -1 do
if t[i] == val then
tremove(t, i)
existed = true
end
end
return existed
end
--Whisper/Whisper Sync filter function
local function checkForSafeSender(sender, checkFriends, checkGuild, filterRaid, isRealIdMessage)
if checkFriends then
--Check Battle.net friends
if isRealIdMessage then
if filterRaid then
--Since filterRaid is true, we need to get tooninfo to see if they are in raid
local accountInfo = C_BattleNet.GetAccountInfoByID(sender)
if accountInfo and accountInfo.gameAccountInfo then--game account info means they are logged into a bnet game
local toonName, client = accountInfo.gameAccountInfo.characterName, accountInfo.gameAccountInfo.clientProgram or ""
if toonName and client == BNET_CLIENT_WOW and DBM:GetRaidUnitId(toonName) then--Check if toon name exists and if client is wow and if toonName is in raid.
return false--just set sender as unsafe
end
end
end
return true--Basically, if not trying to filter someone who's in raid with us, always return true. Non friends can't send realid/battle.net messages
else--Non battle.net message
--We still need to see if it's a bnet friend, even if it's not a realID message, just have to iterate over entire friendslist to find matching toonname
local _, numBNetOnline = BNGetNumFriends()
for i = 1, numBNetOnline do
local accountInfo = C_BattleNet.GetFriendAccountInfo(i)
if accountInfo and accountInfo.gameAccountInfo then
local toonName, client = accountInfo.gameAccountInfo.characterName, accountInfo.gameAccountInfo.clientProgram or ""
--Check if it's a bnet friend sending a non bnet whisper
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
if toonName == sender then--Now simply see if this is sender
if filterRaid and DBM:GetRaidUnitId(toonName) then--Person is in raid group and filter raid enabled
return false--just set sender as unsafe
else
return true
end
end
end
end
end
end
--Check if it's a non bnet friend
local friendInfo = C_FriendList.GetFriendInfo(sender)
if friendInfo then
if filterRaid and DBM:GetRaidUnitId(friendInfo.name) then--Person is in raid group and filter raid enabled
return false--just set sender as unsafe
else
return true
end
end
end
--Check Guildies (not used by whisper syncs, but used by status whispers)
if checkGuild then
local totalMembers, _, numOnlineAndMobileMembers = GetNumGuildMembers()
local scanTotal = GetGuildRosterShowOffline() and totalMembers or numOnlineAndMobileMembers--Attempt CPU saving, if "show offline" is unchecked, we can reliably scan only online members instead of whole roster
for i=1, scanTotal do
local name = GetGuildRosterInfo(i)
if not name then break end
name = Ambiguate(name, "none")
if name == sender then
if filterRaid and DBM:GetRaidUnitId(name) then--Person is in raid group and filter raid enabled
return false--just set sender as unsafe
else
return true
end
end
end
end
return false
end
-- automatically sends an addon message to the appropriate channel (INSTANCE_CHAT, RAID or PARTY)
local function sendSync(prefix, msg)
msg = msg or ""
if IsInGroup(2) and IsInInstance() then--For BGs, LFR and LFG (we also check IsInInstance() so if you're in queue but fighting something outside like a world boss, it'll sync in "RAID" instead)
SendAddonMessage("D4", prefix .. "\t" .. msg, "INSTANCE_CHAT")
else
if IsInRaid() then
SendAddonMessage("D4", prefix .. "\t" .. msg, "RAID")
elseif IsInGroup(1) then
SendAddonMessage("D4", prefix .. "\t" .. msg, "PARTY")
else--for solo raid
handleSync("SOLO", playerName, prefix, strsplit("\t", msg))
end
end
end
--Custom sync function that should only be used for user generated sync messages
local function sendLoggedSync(prefix, msg)
msg = msg or ""
if IsInGroup(2) and IsInInstance() then--For BGs, LFR and LFG (we also check IsInInstance() so if you're in queue but fighting something outside like a world boss, it'll sync in "RAID" instead)
C_ChatInfo.SendAddonMessageLogged("D4", prefix .. "\t" .. msg, "INSTANCE_CHAT")
else
if IsInRaid() then
C_ChatInfo.SendAddonMessageLogged("D4", prefix .. "\t" .. msg, "RAID")
elseif IsInGroup(1) then
C_ChatInfo.SendAddonMessageLogged("D4", prefix .. "\t" .. msg, "PARTY")
else--for solo raid
handleSync("SOLO", playerName, prefix, strsplit("\t", msg))
end
end
end
--Sync Object specifically for out in the world sync messages that have different rules than standard syncs
local function SendWorldSync(self, prefix, msg, noBNet)
DBM:Debug("SendWorldSync running for "..prefix)
if IsInRaid() then
SendAddonMessage("D4", prefix.."\t"..msg, "RAID")
elseif IsInGroup(1) then
SendAddonMessage("D4", prefix.."\t"..msg, "PARTY")
else--for solo raid
handleSync("SOLO", playerName, prefix, strsplit("\t", msg))
end
if IsInGuild() then
SendAddonMessage("D4", prefix.."\t"..msg, "GUILD")--Even guild syncs send realm so we can keep antispam the same across realid as well.
end
if not noBNet then
local _, numBNetOnline = BNGetNumFriends()
local connectedServers = GetAutoCompleteRealms()
for i = 1, numBNetOnline do
local sameRealm = false
local accountInfo = C_BattleNet.GetFriendAccountInfo(i)
if accountInfo then
local gameAccountID, isOnline, userRealm = accountInfo.gameAccountInfo.gameAccountID, accountInfo.gameAccountInfo.isOnline, accountInfo.gameAccountInfo.realmName
if gameAccountID and isOnline and userRealm then
--local gameAccountInfo = C_BattleNet.GetGameAccountInfoByID(presenceID)--Just in case required, if it can't actually be pulled from sub table of accountInfo above
--local userRealm = accountInfo.gameAccountInfo.realmName or L.UNKNOWN
if connectedServers then
for j = 1, #connectedServers do
if userRealm == connectedServers[j] then
sameRealm = true
break
end
end
else
if userRealm == playerRealm then
sameRealm = true
end
end
if sameRealm then
BNSendGameData(gameAccountID, "D4", prefix.."\t"..msg)--Just send users realm for pull, so we can eliminate connectedServers checks on sync handler
end
end
end
end
end
end
local function strFromTime(time)
if type(time) ~= "number" then time = 0 end
time = floor(time*100)/100
if time < 60 then
return L.TIMER_FORMAT_SECS:format(time)
elseif time % 60 == 0 then
return L.TIMER_FORMAT_MINS:format(time/60)
else
return L.TIMER_FORMAT:format(time/60, time % 60)
end
end
function DBM:strFromTime(time)
return strFromTime(time)
end
do
-- fail-safe format, replaces missing arguments with unknown
-- note: doesn't handle cases like %%%s correctly at the moment (should become %unknown, but becomes %%s)
-- also, the end of the format directive is not detected in all cases, but handles everything that occurs in our boss mods ;)
--> 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)
local function replace(cap1, cap2)
return cap1 == "%" and L.UNKNOWN
end
function pformat(fstr, ...)
local ok, str = pcall(format, fstr, ...)
return ok and str or fstr:gsub("(%%+)([^%%%s<]+)", replace):gsub("%%%%", "%%")
end
end
-- sends a whisper to a player by his or her character name or BNet presence id
-- returns true if the message was sent, nil otherwise
local function sendWhisper(target, msg)
if type(target) == "number" then
if not BNIsSelf(target) then -- never send BNet whispers to ourselves
BNSendWhisper(target, msg)
return true
end
elseif type(target) == "string" then
-- whispering to ourselves here is okay and somewhat useful for whisper-warnings
SendChatMessage(msg, "WHISPER", nil, target)
return true
end
end
local BNSendWhisper = sendWhisper
--Another custom server name strip function that first strips out the "><" DBM wraps around playernames
local function stripServerName(cap)
cap = cap:sub(2, -2)
cap = DBM:GetShortServerName(cap)
return cap
end
--------------
-- Events --
--------------
do
local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo
local registeredEvents = {}
local registeredSpellIds = {}
local unfilteredCLEUEvents = {}
local registeredUnitEventIds = {}
local argsMT = {__index = {}}
local args = setmetatable({}, argsMT)
function argsMT.__index:IsSpellID(...)
return tIndexOf({...}, args.spellId) ~= nil
end
function argsMT.__index:IsPlayer()
return bband(args.destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsPlayerSource()
return bband(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsNPC()
return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0
end
function argsMT.__index:IsPet()
return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
end
function argsMT.__index:IsPetSource()
return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
end
function argsMT.__index:IsSrcTypePlayer()
return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsDestTypePlayer()
return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsSrcTypeHostile()
return bband(args.sourceFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
end
function argsMT.__index:IsDestTypeHostile()
return bband(args.destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
end
function argsMT.__index:GetSrcCreatureID()
return DBM:GetCIDFromGUID(self.sourceGUID)
end
function argsMT.__index:GetDestCreatureID()
return DBM:GetCIDFromGUID(self.destGUID)
end
local function handleEvent(self, event, ...)
local isUnitEvent = event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED"
if self == mainFrame and isUnitEvent then
-- UNIT_* events that come from mainFrame are _UNFILTERED variants and need their suffix
event = event .. "_UNFILTERED"
isUnitEvent = false -- not actually a real unit id for this function...
end
if not registeredEvents[event] or not dbmIsEnabled then return end
for _, v in ipairs(registeredEvents[event]) do
local zones = v.zones
local handler = v[event]
local modEvents = v.registeredUnitEvents
if handler and (not isUnitEvent or not modEvents or modEvents[event .. ...]) and (not zones or zones[LastInstanceMapID]) and not (not v.isTrashModBossFightAllowed and v.isTrashMod and #inCombat > 0) then
handler(v, ...)
end
end
end
local registerUnitEvent, unregisterUnitEvent, registerSpellId, unregisterSpellId, registerCLEUEvent, unregisterCLEUEvent
do
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)
function registerUnitEvent(mod, event, ...)
mod.registeredUnitEvents = mod.registeredUnitEvents or {}
for i = 1, select("#", ...) do
local uId = select(i, ...)
if not uId then break end
local frame = frames[uId]
if not frame then
frame = CreateFrame("Frame")
if uId == "mouseover" then
-- work-around for mouse-over events (broken!)
frame:SetScript("OnEvent", function(self, event, uId, ...)
-- we registered mouseover events, so we only want mouseover events, thanks.
handleEvent(self, event, "mouseover", ...)
end)
else
frame:SetScript("OnEvent", handleEvent)
end
frames[uId] = frame
end
registeredUnitEventIds[event .. uId] = (registeredUnitEventIds[event .. uId] or 0) + 1
mod.registeredUnitEvents[event .. uId] = true
frame:RegisterUnitEvent(event, uId)
end
end
function unregisterUnitEvent(mod, event, ...)
for i = 1, select("#", ...) do
local uId = select(i, ...)
if not uId then break end
local frame = frames[uId]
local refs = (registeredUnitEventIds[event .. uId] or 1) - 1
registeredUnitEventIds[event .. uId] = refs
if refs <= 0 then
registeredUnitEventIds[event .. uId] = nil
if frame then
frame:UnregisterEvent(event)
end
end
if mod.registeredUnitEvents and mod.registeredUnitEvents[event .. uId] then
mod.registeredUnitEvents[event .. uId] = nil
end
end
for i = #registeredEvents[event], 1, -1 do
if registeredEvents[event][i] == mod then
tremove(registeredEvents[event], i)
end
end
if #registeredEvents[event] == 0 then
registeredEvents[event] = nil
end
end
function registerSpellId(event, spellId)
if type(spellId) == "string" then--Something is screwed up, like SPELL_AURA_APPLIED DOSE
DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." is not a number!")
return
end
if spellId and not DBM:GetSpellInfo(spellId) then
DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." spell id does not exist!")
return
end
if not registeredSpellIds[event] then
registeredSpellIds[event] = {}
end
registeredSpellIds[event][spellId] = (registeredSpellIds[event][spellId] or 0) + 1
end
function unregisterSpellId(event, spellId)
if not registeredSpellIds[event] then return end
if spellId and not DBM:GetSpellInfo(spellId) then
DBM:AddMsg("DBM unregisterSpellId Error: "..spellId.." spell id does not exist!")
return
end
local refs = (registeredSpellIds[event][spellId] or 1) - 1
registeredSpellIds[event][spellId] = refs
if refs <= 0 then
registeredSpellIds[event][spellId] = nil
end
end
--There are 2 tables. unfilteredCLEUEvents and registeredSpellIds table.
--unfilteredCLEUEvents saves UNFILTERED cleu event count. this is count table to prevent bad unregister.
--registeredSpellIds tables filtered table. this saves event and spell ids. works smiliar with unfilteredCLEUEvents table.
function registerCLEUEvent(mod, event)
local argTable = {strsplit(" ", event)}
-- filtered cleu event. save information in registeredSpellIds table.
if #argTable > 1 then
event = argTable[1]
for i = 2, #argTable do
registerSpellId(event, tonumber(argTable[i]))
end
-- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
else
unfilteredCLEUEvents[event] = (unfilteredCLEUEvents[event] or 0) + 1
end
registeredEvents[event] = registeredEvents[event] or {}
tinsert(registeredEvents[event], mod)
end
function unregisterCLEUEvent(mod, event)
local argTable = {strsplit(" ", event)}
local eventCleared = false
-- filtered cleu event. save information in registeredSpellIds table.
if #argTable > 1 then
event = argTable[1]
for i = 2, #argTable do
unregisterSpellId(event, tonumber(argTable[i]))
end
local remainingSpellIdCount = 0
if registeredSpellIds[event] then
for _, _ in pairs(registeredSpellIds[event]) do
remainingSpellIdCount = remainingSpellIdCount + 1
end
end
if remainingSpellIdCount == 0 then
registeredSpellIds[event] = nil
-- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
if not unfilteredCLEUEvents[event] then
eventCleared = true
end
end
-- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
else
local refs = (unfilteredCLEUEvents[event] or 1) - 1
unfilteredCLEUEvents[event] = refs
if refs <= 0 then
unfilteredCLEUEvents[event] = nil
-- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
if not registeredSpellIds[event] then
eventCleared = true
end
end
end
for i = #registeredEvents[event], 1, -1 do
if registeredEvents[event][i] == mod then
registeredEvents[event][i] = {}
break
end
end
if eventCleared then
registeredEvents[event] = nil
end
end
end
-- UNIT_* events are special: they can take 'parameters' like this: "UNIT_HEALTH boss1 boss2" which only trigger the event for the given unit ids
function DBM:RegisterEvents(...)
for i = 1, select("#", ...) do
local event = select(i, ...)
-- spell events with special care.
if event:sub(0, 6) == "SPELL_" and event ~= "SPELL_NAME_UPDATE" or event:sub(0, 6) == "RANGE_" or event:sub(0, 6) == "SWING_" or event == "UNIT_DIED" or event == "UNIT_DESTROYED" or event == "PARTY_KILL" then
registerCLEUEvent(self, event)
else
local eventWithArgs = event
-- unit events need special care
if event:sub(0, 5) == "UNIT_" then
-- 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))
local arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8
event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
if not arg1 and event:sub(event:len() - 10) ~= "_UNFILTERED" then -- no arguments given, support for legacy mods
eventWithArgs = event .. " boss1 boss2 boss3 boss4 boss5 target focus"
event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", eventWithArgs)
end
if event:sub(event:len() - 10) == "_UNFILTERED" then
-- we really want *all* unit ids
mainFrame:RegisterEvent(event:sub(0, -12))
else
registerUnitEvent(self, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
end
-- spell events with filter
else
-- normal events
mainFrame:RegisterEvent(event)
end
registeredEvents[eventWithArgs] = registeredEvents[eventWithArgs] or {}
tinsert(registeredEvents[eventWithArgs], self)
if event ~= eventWithArgs then
registeredEvents[event] = registeredEvents[event] or {}
tinsert(registeredEvents[event], self)
end
end
end
end
local function unregisterUEvent(mod, event)
if event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED" then
local eventName, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
if eventName:sub(eventName:len() - 10) == "_UNFILTERED" then
mainFrame:UnregisterEvent(eventName:sub(0, -12))
else
unregisterUnitEvent(mod, eventName, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
end
end
end
local function findRealEvent(t, val)
for _, v in ipairs(t) do
local event = strsplit(" ", v)
if event == val then
return v
end
end
end
function DBM:UnregisterInCombatEvents(srmOnly, srmIncluded)
for event, mods in pairs(registeredEvents) do
if srmOnly then
local i = 1
while mods[i] do
if mods[i] == self and event == "SPELL_AURA_REMOVED" then
local findEvent = findRealEvent(self.inCombatOnlyEvents, "SPELL_AURA_REMOVED")
if findEvent then
unregisterCLEUEvent(self, findEvent)
break
end
end
i = i +1
end
elseif (event:sub(0, 6) == "SPELL_"and event ~= "SPELL_NAME_UPDATE" or event:sub(0, 6) == "RANGE_") then
local i = 1
while mods[i] do
if mods[i] == self and (srmIncluded or event ~= "SPELL_AURA_REMOVED") then
local findEvent = findRealEvent(self.inCombatOnlyEvents, event)
if findEvent then
unregisterCLEUEvent(self, findEvent)
break
end
end
i = i +1
end
else
local match = false
for i = #mods, 1, -1 do
if mods[i] == self and checkEntry(self.inCombatOnlyEvents, event) then
tremove(mods, i)
match = true
end
end
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
unregisterUEvent(self, event)
end
if #mods == 0 then
registeredEvents[event] = nil
end
end
end
end
function DBM:RegisterShortTermEvents(...)
local _shortTermRegisterEvents = {...}
for k, v in pairs(_shortTermRegisterEvents) do
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
-- legacy event, oh noes
_shortTermRegisterEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
end
end
self.shortTermEventsRegistered = 1
self:RegisterEvents(unpack(_shortTermRegisterEvents))
-- Fix so we can register multiple short term events. Use at your own risk, as unsucribing will cause
-- all short term events to unregister.
if not self.shortTermRegisterEvents then
self.shortTermRegisterEvents = {}
end
for k, v in pairs(_shortTermRegisterEvents) do
self.shortTermRegisterEvents[k] = v
end
-- End fix
end
function DBM:UnregisterShortTermEvents()
if self.shortTermRegisterEvents then
for event, mods in pairs(registeredEvents) do
if event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_" then
local i = 1
while mods[i] do
if mods[i] == self then
local findEvent = findRealEvent(self.shortTermRegisterEvents, event)
if findEvent then
unregisterCLEUEvent(self, findEvent)
break
end
end
i = i +1
end
else
local match = false
for i = #mods, 1, -1 do
if mods[i] == self and checkEntry(self.shortTermRegisterEvents, event) then
tremove(mods, i)
match = true
end
end
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
unregisterUEvent(self, event)
end
if #mods == 0 then
registeredEvents[event] = nil
end
end
end
self.shortTermEventsRegistered = nil
self.shortTermRegisterEvents = nil
end
end
DBM:RegisterEvents("ADDON_LOADED")
function DBM:FilterRaidBossEmote(msg, ...)
return handleEvent(nil, "CHAT_MSG_RAID_BOSS_EMOTE_FILTERED", msg:gsub("\124c%x+(.-)\124r", "%1"), ...)
end
local noArgTableEvents = {
SWING_DAMAGE = true,
SWING_MISSED = true,
RANGE_DAMAGE = true,
RANGE_MISSED = true,
SPELL_DAMAGE = true,
SPELL_BUILDING_DAMAGE = true,
SPELL_MISSED = true,
SPELL_ABSORBED = true,
SPELL_HEAL = true,
SPELL_ENERGIZE = true,
SPELL_PERIODIC_ENERGIZE = true,
SPELL_PERIODIC_MISSED = true,
SPELL_PERIODIC_DAMAGE = true,
SPELL_PERIODIC_DRAIN = true,
SPELL_PERIODIC_LEECH = true,
SPELL_DRAIN = true,
SPELL_LEECH = true,
SPELL_CAST_FAILED = true
}
function DBM:COMBAT_LOG_EVENT_UNFILTERED()
local timestamp, event, _, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, extraArg1, extraArg2, extraArg3, extraArg4, extraArg5, extraArg6, extraArg7, extraArg8, extraArg9, extraArg10 = CombatLogGetCurrentEventInfo()
if not registeredEvents[event] then return end
local eventSub6 = event:sub(0, 6)
if (eventSub6 == "SPELL_" or eventSub6 == "RANGE_") and not unfilteredCLEUEvents[event] and registeredSpellIds[event] then
if not registeredSpellIds[event][extraArg1] then return end
end
-- process some high volume events without building the whole table which is somewhat faster
-- this prevents work-around with mods that used to have their own event handler to prevent this overhead
if noArgTableEvents[event] then
return handleEvent(nil, event, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, extraArg1, extraArg2, extraArg3, extraArg4, extraArg5, extraArg6, extraArg7, extraArg8, extraArg9, extraArg10)
else
twipe(args)
args.timestamp = timestamp
args.event = event
args.sourceGUID = sourceGUID
args.sourceName = sourceName
args.sourceFlags = sourceFlags
args.sourceRaidFlags = sourceRaidFlags
args.destGUID = destGUID
args.destName = destName
args.destFlags = destFlags
args.destRaidFlags = destRaidFlags
if eventSub6 == "SPELL_" then
args.spellId, args.spellName = extraArg1, extraArg2
if event == "SPELL_AURA_APPLIED" or event == "SPELL_AURA_REFRESH" or event == "SPELL_AURA_REMOVED" then
args.amount = extraArg5
if not args.sourceName then
args.sourceName = args.destName
args.sourceGUID = args.destGUID
args.sourceFlags = args.destFlags
end
elseif event == "SPELL_AURA_APPLIED_DOSE" or event == "SPELL_AURA_REMOVED_DOSE" then
args.amount = extraArg5
if not args.sourceName then
args.sourceName = args.destName
args.sourceGUID = args.destGUID
args.sourceFlags = args.destFlags
end
elseif event == "SPELL_INTERRUPT" or event == "SPELL_DISPEL" or event == "SPELL_DISPEL_FAILED" or event == "SPELL_AURA_STOLEN" then
args.extraSpellId, args.extraSpellName = extraArg4, extraArg5
end
elseif event == "UNIT_DIED" or event == "UNIT_DESTROYED" then
args.sourceName = args.destName
args.sourceGUID = args.destGUID
args.sourceFlags = args.destFlags
elseif event == "ENVIRONMENTAL_DAMAGE" then
args.environmentalType, args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = extraArg1, extraArg2, extraArg3, extraArg4, extraArg5, extraArg6, extraArg7, extraArg8, extraArg9, extraArg10
end
return handleEvent(nil, event, args)
end
end
mainFrame:SetScript("OnEvent", handleEvent)
end
--------------
-- OnLoad --
--------------
do
local isLoaded = false
local onLoadCallbacks = {}
local disabledMods = {}
local function infniteLoopNotice(self, message)
AddMsg(self, message)
self:Schedule(30, infniteLoopNotice, self, message)
end
local function runDelayedFunctions(self)
--Check if voice pack missing
local activeVP = self.Options.ChosenVoicePack
if activeVP ~= "None" then
if not self.VoiceVersions[activeVP] or (self.VoiceVersions[activeVP] and self.VoiceVersions[activeVP] == 0) then--A voice pack is selected that does not belong
voiceSessionDisabled = true
AddMsg(self, L.VOICE_MISSING)
end
else
if self.Options.ShowReminders and #self.Voices > 1 then
--At least one voice pack installed but activeVP set to "None"
AddMsg(self, L.VOICE_DISABLED)
end
end
--Check if any of countdown sounds are using missing voice pack
local found1, found2, found3 = false, false, false
for i = 1, #self.Counts do
local voice = self.Counts[i].value
if voice == self.Options.CountdownVoice then
found1 = true
end
if voice == self.Options.CountdownVoice2 then
found2 = true
end
if voice == self.Options.CountdownVoice3 then
found3 = true
end
end
if not found1 then
AddMsg(self, L.VOICE_COUNT_MISSING:format(1, self.DefaultOptions.CountdownVoice))
self.Options.CountdownVoice = self.DefaultOptions.CountdownVoice
end
if not found2 then
AddMsg(self, L.VOICE_COUNT_MISSING:format(2, self.DefaultOptions.CountdownVoice2))
self.Options.CountdownVoice2 = self.DefaultOptions.CountdownVoice2
end
if not found3 then
AddMsg(self, L.VOICE_COUNT_MISSING:format(3, self.DefaultOptions.CountdownVoice3))
self.Options.CountdownVoice3 = self.DefaultOptions.CountdownVoice3
end
self:BuildVoiceCountdownCache()
--Break timer recovery
--Try local settings
if self.Options.RestoreSettingBreakTimer then
local timer, startTime = string.split("/", self.Options.RestoreSettingBreakTimer)
local elapsed = time() - tonumber(startTime)
local remaining = timer - elapsed
if remaining > 0 then
breakTimerStart(DBM, remaining, playerName)
else--It must have ended while we were offline, kill variable.
self.Options.RestoreSettingBreakTimer = nil
end
end
if IsInGuild() then
SendAddonMessage("D4", "GH", "GUILD")
end
if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = self:GetCurrentInstanceDifficulty()
end
end
-- register a callback that will be executed once the addon is fully loaded (ADDON_LOADED fired, saved vars are available)
function DBM:RegisterOnLoadCallback(cb)
if isLoaded then
cb()
else
onLoadCallbacks[#onLoadCallbacks + 1] = cb
end
end
function DBM:ADDON_LOADED(modname)
if modname == "DBM-Core" and not isLoaded then
dbmToc = tonumber(GetAddOnMetadata("DBM-Core", "X-Min-Interface"))
isLoaded = true
for _, v in ipairs(onLoadCallbacks) do
xpcall(v, geterrorhandler())
end
onLoadCallbacks = nil
loadOptions(self)
if WOW_PROJECT_ID ~= (WOW_PROJECT_MAINLINE or 1) then
self:Disable(true)
self:Schedule(15, infniteLoopNotice, self, L.RETAIL_ONLY)
return
end
if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
self:Disable(true)
self:Schedule(15, infniteLoopNotice, self, L.VEM)
return
end
if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
self:Disable(true)
self:Schedule(15, infniteLoopNotice, self, L.OUTDATEDPROFILES)
return
end
if GetAddOnEnableState(playerName, "DBM-SpellTimers") >= 1 then
local version = GetAddOnMetadata("DBM-SpellTimers", "Version") or "r0"
version = tonumber(string.sub(version, 2, 4)) or 0
if version < 122 and not self.Options.DebugMode then
self:Disable(true)
self:Schedule(15, infniteLoopNotice, self, L.OUTDATEDSPELLTIMERS)
return
end
end
if GetAddOnEnableState(playerName, "DBM-RaidLeadTools") >= 1 and not self.Options.DebugMode then
self:Disable(true)
self:Schedule(15, infniteLoopNotice, self, L.OUTDATEDRLT)
return
end
if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
self:Disable(true)
self:Schedule(15, infniteLoopNotice, self, L.DPMCORE)
return
end
if GetAddOnEnableState(playerName, "DBM-VictorySound") >= 1 then
self:Disable(true)
C_TimerAfter(15, function() AddMsg(self, L.VICTORYSOUND) end)
return
end
if GetAddOnEnableState(playerName, "DBM-LDB") >= 1 then
C_TimerAfter(15, function() AddMsg(self, L.DBMLDB) end)
end
if GetAddOnEnableState(playerName, "DBM-LootReminder") >= 1 then
C_TimerAfter(15, function() AddMsg(self, L.DBMLOOTREMINDER) end)
end
DBT:LoadOptions("DBM")
self.Arrow:LoadPosition()
-- LibDBIcon setup
if type(DBM_MinimapIcon) ~= "table" then
DBM_MinimapIcon = {}
end
if LibStub("LibDBIcon-1.0", true) then
LibStub("LibDBIcon-1.0"):Register("DBM", dataBroker, DBM_MinimapIcon)
end
local soundChannels = tonumber(GetCVar("Sound_NumChannels")) or 24--if set to 24, may return nil, Defaults usually do
--If this messes with your fps, stop raiding with a toaster. It's only fix for addon sound ducking.
if soundChannels < 64 then
SetCVar("Sound_NumChannels", 64)
end
self.AddOns = {}
self.Voices = { {text = "None",value = "None"}, }--Create voice table, with default "None" value
self.VoiceVersions = {}
for i = 1, GetNumAddOns() do
local addonName = GetAddOnInfo(i)
local enabled = GetAddOnEnableState(playerName, i)
if GetAddOnMetadata(i, "X-DBM-Mod") then
if enabled ~= 0 then
if checkEntry(bannedMods, addonName) then
AddMsg(self, "The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
else
local mapIdTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-MapID") or "")}
tinsert(self.AddOns, {
sort = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Sort") or mhuge) or mhuge,
type = GetAddOnMetadata(i, "X-DBM-Mod-Type") or "OTHER",
category = GetAddOnMetadata(i, "X-DBM-Mod-Category") or "Other",
statTypes = GetAddOnMetadata(i, "X-DBM-StatTypes") or "",
name = GetAddOnMetadata(i, "X-DBM-Mod-Name") or GetRealZoneText(tonumber(mapIdTable[1])) or L.UNKNOWN,
mapId = mapIdTable,
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"))},
oneFormat = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Single-Format") or 0) == 1, -- Deprecated
hasLFR = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-LFR") or 0) == 1, -- Deprecated
hasChallenge = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Challenge") or 0) == 1, -- Deprecated
noHeroic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Heroic") or 0) == 1, -- Deprecated
hasMythic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Mythic") or 0) == 1, -- Deprecated
hasTimeWalker = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-TimeWalker") or 0) == 1, -- Deprecated
noStatistics = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Statistics") or 0) == 1,
isWorldBoss = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-World-Boss") or 0) == 1,
isExpedition = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Expedition") or 0) == 1,
minRevision = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-MinCoreRevision") or 0),
minExpansion = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-MinExpansion") or 0),
minToc = tonumber(GetAddOnMetadata(i, "X-Min-Interface") or 0),
modId = addonName,
})
for j = #self.AddOns[#self.AddOns].mapId, 1, -1 do
local id = tonumber(self.AddOns[#self.AddOns].mapId[j])
if id then
self.AddOns[#self.AddOns].mapId[j] = id
else
tremove(self.AddOns[#self.AddOns].mapId, j)
end
end
if self.AddOns[#self.AddOns].subTabs then
local subTabs = self.AddOns[#self.AddOns].subTabs
for k, _ in ipairs(subTabs) do
--Ugly hack to inject custom string text into auto localized zone name sub cats
if subTabs[k]:find("|") then
local id, nameModifier = strsplit("|", subTabs[k])
if id and nameModifier then
id = tonumber(id)
self.AddOns[#self.AddOns].subTabs[k] = (GetRealZoneText(id):trim() or id).." ("..nameModifier..")"
else
self.AddOns[#self.AddOns].subTabs[k] = (subTabs[k]):trim()
end
else
local id = tonumber(subTabs[k])
if id then
local name = GetRealZoneText(id):trim() or id
local subname = strsplit("-", name)--For handling zones like Warfront: Arathi - Alliance
self.AddOns[#self.AddOns].subTabs[k] = subname
else
self.AddOns[#self.AddOns].subTabs[k] = (subTabs[k]):trim()
end
end
end
end
if GetAddOnMetadata(i, "X-DBM-Mod-LoadCID") then
local idTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadCID"))}
for j = 1, #idTable do
loadcIds[tonumber(idTable[j]) or ""] = addonName
end
end
end
else
disabledMods[#disabledMods+1] = addonName
end
end
if GetAddOnMetadata(i, "X-DBM-Voice") and enabled ~= 0 then
if checkEntry(bannedMods, addonName) then
AddMsg(self, "The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
else
C_TimerAfter(0.01, function()
local voiceValue = GetAddOnMetadata(i, "X-DBM-Voice-ShortName")
local voiceVersion = tonumber(GetAddOnMetadata(i, "X-DBM-Voice-Version") or 0)
if voiceVersion > 0 then--Do not insert voice version 0 into THIS table. 0 should be used by voice packs that insert only countdown
tinsert(self.Voices, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = voiceValue })
end
self.VoiceVersions[voiceValue] = voiceVersion
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
if GetAddOnMetadata(i, "X-DBM-Voice-HasCount") then--Supports adding countdown options, insert new countdown into table
tinsert(self.Counts, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = "VP:"..voiceValue, path = "Interface\\AddOns\\DBM-VP"..voiceValue.."\\count\\", max = 10})
end
end)
end
end
if GetAddOnMetadata(i, "X-DBM-CountPack") and enabled ~= 0 then
if checkEntry(bannedMods, addonName) then
AddMsg(self, "The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
else
local loaded = LoadAddOn(addonName)
C_TimerAfter(0.01, function()
local voiceGlobal = GetAddOnMetadata(i, "X-DBM-CountPack-GlobalName")
local insertFunction = _G[voiceGlobal]
if loaded and insertFunction then
insertFunction()
else
self:Debug(addonName.." failed to load at time CountPack function "..voiceGlobal.."ran", 2)
end
end)
end
end
if GetAddOnMetadata(i, "X-DBM-VictoryPack") and enabled ~= 0 then
if checkEntry(bannedMods, addonName) then
AddMsg(self, "The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
else
local loaded = LoadAddOn(addonName)
C_TimerAfter(0.01, function()
local victoryGlobal = GetAddOnMetadata(i, "X-DBM-VictoryPack-GlobalName")
local insertFunction = _G[victoryGlobal]
if loaded and insertFunction then
insertFunction()
else
self:Debug(addonName.." failed to load at time VictoryPack function "..victoryGlobal.." ran", 2)
end
end)
end
end
if GetAddOnMetadata(i, "X-DBM-DefeatPack") and enabled ~= 0 then
if checkEntry(bannedMods, addonName) then
AddMsg(self, "The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
else
local loaded = LoadAddOn(addonName)
C_TimerAfter(0.01, function()
local defeatGlobal = GetAddOnMetadata(i, "X-DBM-DefeatPack-GlobalName")
local insertFunction = _G[defeatGlobal]
if loaded and insertFunction then
insertFunction()
else
self:Debug(addonName.." failed to load at time DefeatPack function "..defeatGlobal.." ran", 2)
end
end)
end
end
if GetAddOnMetadata(i, "X-DBM-MusicPack") and enabled ~= 0 then
if checkEntry(bannedMods, addonName) then
AddMsg(self, "The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
else
local loaded = LoadAddOn(addonName)
C_TimerAfter(0.01, function()
local musicGlobal = GetAddOnMetadata(i, "X-DBM-MusicPack-GlobalName")
local insertFunction = _G[musicGlobal]
if loaded and insertFunction then
insertFunction()
else
self:Debug(addonName.." failed to load at time MusicPack function "..musicGlobal.." ran", 2)
end
end)
end
end
end
tsort(self.AddOns, function(v1, v2) return v1.sort < v2.sort end)
self:RegisterEvents(
"COMBAT_LOG_EVENT_UNFILTERED",
"GROUP_ROSTER_UPDATE",
"INSTANCE_GROUP_SIZE_CHANGED",
"CHAT_MSG_ADDON",
"CHAT_MSG_ADDON_LOGGED",
"BN_CHAT_MSG_ADDON",
"PLAYER_REGEN_DISABLED",
"PLAYER_REGEN_ENABLED",
"INSTANCE_ENCOUNTER_ENGAGE_UNIT",
"UNIT_TARGETABLE_CHANGED",
"UNIT_SPELLCAST_SUCCEEDED boss1 boss2 boss3 boss4 boss5",
"UNIT_TARGET_UNFILTERED",
"ENCOUNTER_START",
"ENCOUNTER_END",
"BOSS_KILL",
"UNIT_DIED",
"UNIT_DESTROYED",
"UNIT_HEALTH mouseover target focus player",
"CHAT_MSG_WHISPER",
"CHAT_MSG_BN_WHISPER",
"CHAT_MSG_MONSTER_YELL",
"CHAT_MSG_MONSTER_EMOTE",
"CHAT_MSG_MONSTER_SAY",
"CHAT_MSG_RAID_BOSS_EMOTE",
"RAID_BOSS_EMOTE",
"RAID_BOSS_WHISPER",
"PLAYER_ENTERING_WORLD",
"LFG_ROLE_CHECK_SHOW",
"LFG_PROPOSAL_SHOW",
"LFG_PROPOSAL_FAILED",
"LFG_PROPOSAL_SUCCEEDED",
"READY_CHECK",
"UPDATE_BATTLEFIELD_STATUS",
"PLAY_MOVIE",
"CINEMATIC_START",
"CINEMATIC_STOP",
"PLAYER_LEVEL_CHANGED",
"PLAYER_SPECIALIZATION_CHANGED",
"PARTY_INVITE_REQUEST",
"LOADING_SCREEN_DISABLED",
"LOADING_SCREEN_ENABLED",
"SCENARIO_COMPLETED",
"CHALLENGE_MODE_RESET"
)
if RolePollPopup:IsEventRegistered("ROLE_POLL_BEGIN") then
RolePollPopup:UnregisterEvent("ROLE_POLL_BEGIN")
end
self:GROUP_ROSTER_UPDATE()
C_TimerAfter(1.5, function()
combatInitialized = true
end)
C_TimerAfter(20, function()--Delay UNIT_HEALTH combat start for 20 sec. (not to break Timer Recovery stuff)
healthCombatInitialized = true
end)
self:Schedule(10, runDelayedFunctions, self)
end
end
end
-----------------
-- Callbacks --
-----------------
do
local callbacks = {}
function fireEvent(event, ...)
if not callbacks[event] then return end
for _, v in ipairs(callbacks[event]) do
local ok, err = pcall(v, event, ...)
if not ok then DBM:AddMsg(("Error while executing callback %s for event %s: %s"):format(tostring(v), tostring(event), err)) end
end
end
function DBM:FireEvent(event, ...)
fireEvent(event, ...)
end
function DBM:IsCallbackRegistered(event, f)
if not event or type(f) ~= "function" then
error("Usage: IsCallbackRegistered(event, callbackFunc)", 2)
end
if not callbacks[event] then return end
for i = 1, #callbacks[event] do
if callbacks[event][i] == f then return true end
end
return false
end
function DBM:RegisterCallback(event, f)
if not event or type(f) ~= "function" then
error("Usage: DBM:RegisterCallback(event, callbackFunc)", 2)
end
callbacks[event] = callbacks[event] or {}
tinsert(callbacks[event], f)
return #callbacks[event]
end
function DBM:UnregisterCallback(event, f)
if not event or not callbacks[event] then return end
if f then
if type(f) ~= "function" then
error("Usage: UnregisterCallback(event, callbackFunc)", 2)
end
--> checking from the end to start and not stoping after found one result in case of a func being twice registered.
for i = #callbacks[event], 1, -1 do
if callbacks[event][i] == f then tremove (callbacks[event], i) end
end
else
callbacks[event] = nil
end
end
end
--------------------------
-- OnUpdate/Scheduler --
--------------------------
do
-- stack that stores a few tables (up to 8) which will be recycled
local popCachedTable, pushCachedTable
local numChachedTables = 0
do
local tableCache
-- gets a table from the stack, it will then be recycled.
function popCachedTable()
local t = tableCache
if t then
tableCache = t.next
numChachedTables = numChachedTables - 1
end
return t
end
-- tries to push a table on the stack
-- 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...
-- 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
-- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection)
-- 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
function pushCachedTable(t)
if numChachedTables < 8 and #t <= 4 then
twipe(t)
t.next = tableCache
tableCache = t
numChachedTables = numChachedTables + 1
end
end
end
-- priority queue (min-heap) that stores all scheduled tasks.
-- insert: O(log n)
-- deleteMin: O(log n)
-- getMin: O(1)
-- removeAllMatching: O(n)
local insert, removeAllMatching, getMin, deleteMin
do
local heap = {}
local firstFree = 1
-- gets the next task
function getMin()
return heap[1]
end
-- restores the heap invariant by moving an item up
local function siftUp(n)
local parent = floor(n / 2)
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
heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent
n = parent
parent = floor(n / 2)
end
end
-- restores the heap invariant by moving an item down
local function siftDown(n)
local m -- position of the smaller child
while 2 * n < firstFree do -- #children >= 1
-- swap the element with its smaller child
if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1
m = 2 * n -- left child
elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child
m = 2 * n -- left child
else -- #children = 2 and right child is smaller than the left one
m = 2 * n + 1 -- right
end
if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored
return
end
heap[n], heap[m] = heap[m], heap[n]
n = m
end
end
-- inserts a new element into the heap
function insert(ele)
heap[firstFree] = ele
siftUp(firstFree)
firstFree = firstFree + 1
end
-- deletes the min element
function deleteMin()
local min = heap[1]
firstFree = firstFree - 1
heap[1] = heap[firstFree]
heap[firstFree] = nil
siftDown(1)
return min
end
-- removes multiple scheduled tasks from the heap
-- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches
function removeAllMatching(f, mod, ...)
-- remove all elements that match the signature, this destroyes the heap and leaves a normal array
local v, match
local foundMatch = false
for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove
v = heap[i]
if (not f or v.func == f) and (not mod or v.mod == mod) then
match = true
for j = 1, select("#", ...) do
if select(j, ...) ~= v[j] then
match = false
break
end
end
if match then
tremove(heap, i)
firstFree = firstFree - 1
foundMatch = true
end
end
end
-- rebuild the heap from the array in O(n)
if foundMatch then
for i = floor((firstFree - 1) / 2), 1, -1 do
siftDown(i)
end
end
end
end
local wrappers = {}
local function range(max, cur, ...)
cur = cur or 1
if cur > max then
return ...
end
return cur, range(max, cur + 1, select(2, ...))
end
local function getWrapper(n)
wrappers[n] = wrappers[n] or loadstring(([[
return function(func, tbl)
return func(]] .. ("tbl[%s], "):rep(n):sub(0, -3) .. [[)
end
]]):format(range(n)))()
return wrappers[n]
end
local nextModSyncSpamUpdate = 0
--mainFrame:SetScript("OnUpdate", function(self, elapsed)
local function onUpdate(self, elapsed)
local time = GetTime()
-- execute scheduled tasks
local nextTask = getMin()
while nextTask and nextTask.func and nextTask.time <= time do
deleteMin()
local n = nextTask.n
if n == #nextTask then
nextTask.func(unpack(nextTask))
else
-- too many nil values (or a trailing nil)
-- this is bad because unpack will not work properly
-- TODO: is there a better solution?
getWrapper(n)(nextTask.func, nextTask)
end
pushCachedTable(nextTask)
nextTask = getMin()
end
-- execute OnUpdate handlers of all modules
local foundModFunctions = 0
for i, v in pairs(updateFunctions) do
foundModFunctions = foundModFunctions + 1
if i.Options.Enabled and (not i.zones or i.zones[LastInstanceMapID]) then
i.elapsed = (i.elapsed or 0) + elapsed
if i.elapsed >= (i.updateInterval or 0) then
v(i, i.elapsed)
i.elapsed = 0
end
end
end
-- clean up sync spam timers and auto respond spam blockers
if time > nextModSyncSpamUpdate then
nextModSyncSpamUpdate = time + 20
-- 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...
-- we now do this just every 20 seconds since the earlier assumption about modSyncSpam isn't true any longer
-- 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)
local k, v = next(modSyncSpam, nil)
if v and (time - v > 8) then
modSyncSpam[k] = nil
end
end
if not nextTask and foundModFunctions == 0 then--Nothing left, stop scheduler
schedulerFrame:SetScript("OnUpdate", nil)
schedulerFrame:Hide()
end
end
function startScheduler()
if not schedulerFrame:IsShown() then
schedulerFrame:Show()
schedulerFrame:SetScript("OnUpdate", onUpdate)
end
end
function schedule(t, f, mod, ...)
if type(f) ~= "function" then
error("usage: schedule(time, func, [mod, args...])", 2)
end
startScheduler()
local v
if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots
v = popCachedTable()
v.time = GetTime() + t
v.func = f
v.mod = mod
v.n = select("#", ...)
for i = 1, v.n do
v[i] = select(i, ...)
end
-- clear slots if necessary
for i = v.n + 1, 4 do
v[i] = nil
end
else -- create a new table
v = {time = GetTime() + t, func = f, mod = mod, n = select("#", ...), ...}
end
insert(v)
end
function scheduleCountdown(time, numAnnounces, func, mod, self, ...)
time = time or 5
numAnnounces = numAnnounces or 3
for i = 1, numAnnounces do
--In event time is < numbmer of announces (ie 2 second time, with 3 announces)
local validTime = time - i
if validTime >= 1 then
schedule(validTime, func, mod, self, i, ...)
end
end
end
function scheduleRepeat(time, spellId, func, mod, self, ...)
--Loops until debuff is gone
if DBM:UnitAura("player", spellId) then--GetPlayerAuraBySpellID
func(...)--Probably not going to work, this is going to need to get a lot more hacky
schedule(time or 2, scheduleRepeat, time, spellId, func, mod, self, ...)
end
end
function unschedule(f, mod, ...)
if not f and not mod then
-- you really want to kill the complete scheduler? call unscheduleAll
error("cannot unschedule everything, pass a function and/or a mod")
end
return removeAllMatching(f, mod, ...)
end
function unscheduleAll()
return removeAllMatching()
end
end
function DBM:Schedule(t, f, ...)
if type(f) ~= "function" then
error("usage: DBM:Schedule(time, func, [args...])", 2)
end
return schedule(t, f, nil, ...)
end
function DBM:Unschedule(f, ...)
return unschedule(f, nil, ...)
end
---------------
-- Profile --
---------------
function DBM:CreateProfile(name)
if not name or name == "" or name:find(" ") then
self:AddMsg(L.PROFILE_CREATE_ERROR)
return
end
if DBM_AllSavedOptions[name] then
self:AddMsg(L.PROFILE_CREATE_ERROR_D:format(name))
return
end
-- create profile
usedProfile = name
DBM_UsedProfile = usedProfile
DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[usedProfile] or {}
self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
self.Options = DBM_AllSavedOptions[usedProfile]
-- rearrange position
DBT:CreateProfile("DBM")
self:RepositionFrames()
self:AddMsg(L.PROFILE_CREATED:format(name))
end
function DBM:ApplyProfile(name)
if not name or not DBM_AllSavedOptions[name] then
self:AddMsg(L.PROFILE_APPLY_ERROR:format(name or L.UNKNOWN))
return
end
usedProfile = name
DBM_UsedProfile = usedProfile
self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
self.Options = DBM_AllSavedOptions[usedProfile]
-- rearrange position
DBT:ApplyProfile("DBM")
self:RepositionFrames()
self:AddMsg(L.PROFILE_APPLIED:format(name))
end
function DBM:CopyProfile(name)
if not name or not DBM_AllSavedOptions[name] then
self:AddMsg(L.PROFILE_COPY_ERROR:format(name or L.UNKNOWN))
return
elseif name == usedProfile then
self:AddMsg(L.PROFILE_COPY_ERROR_SELF)
return
end
DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[name]
self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
self.Options = DBM_AllSavedOptions[usedProfile]
-- rearrange position
DBT:CopyProfile(name, "DBM", true)
self:RepositionFrames()
self:AddMsg(L.PROFILE_COPIED:format(name))
end
function DBM:DeleteProfile(name)
if not name or not DBM_AllSavedOptions[name] then
self:AddMsg(L.PROFILE_DELETE_ERROR:format(name or L.UNKNOWN))
return
elseif name == "Default" then-- Default profile cannot be deleted.
self:AddMsg(L.PROFILE_CANNOT_DELETE)
return
end
--Delete
DBM_AllSavedOptions[name] = nil
usedProfile = "Default"--Restore to default
DBM_UsedProfile = usedProfile
self.Options = DBM_AllSavedOptions[usedProfile]
if not self.Options then
-- the default profile got lost somehow (maybe WoW crashed and the saved variables file got corrupted)
self:CreateProfile("Default")
end
-- rearrange position
DBT:DeleteProfile(name, "DBM")
self:RepositionFrames()
self:AddMsg(L.PROFILE_DELETED:format(name))
end
function DBM:RepositionFrames()
-- rearrange position
self:UpdateWarningOptions()
self:UpdateSpecialWarningOptions()
self.Arrow:LoadPosition()
local rangeCheck = _G["DBMRangeCheck"]
if rangeCheck then
rangeCheck:ClearAllPoints()
rangeCheck:SetPoint(self.Options.RangeFramePoint, UIParent, self.Options.RangeFramePoint, self.Options.RangeFrameX, self.Options.RangeFrameY)
end
local rangeCheckRadar = _G["DBMRangeCheckRadar"]
if rangeCheckRadar then
rangeCheckRadar:ClearAllPoints()
rangeCheckRadar:SetPoint(self.Options.RangeFrameRadarPoint, UIParent, self.Options.RangeFrameRadarPoint, self.Options.RangeFrameRadarX, self.Options.RangeFrameRadarY)
end
local infoFrame = _G["DBMInfoFrame"]
if infoFrame then
infoFrame:ClearAllPoints()
infoFrame:SetPoint(self.Options.InfoFramePoint, UIParent, self.Options.InfoFramePoint, self.Options.InfoFrameX, self.Options.InfoFrameY)
end
end
----------------------
-- Slash Commands --
----------------------
do
local trackedHudMarkers = {}
local function Pull(timer)
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.
if (DBM:GetRaidRank(playerName) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
return DBM:AddMsg(L.ERROR_NO_PERMISSION)
end
if timer > 0 and timer < 3 then
return DBM:AddMsg(L.TIME_TOO_SHORT)
end
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
if targetName then
sendSync("PT", timer.."\t"..LastInstanceMapID.."\t"..targetName)
else
sendSync("PT", timer.."\t"..LastInstanceMapID)
end
end
local function Break(timer)
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
DBM:AddMsg(L.ERROR_NO_PERMISSION)
return
end
if timer > 60 then
DBM:AddMsg(L.BREAK_USAGE)
return
end
timer = timer * 60
sendSync("BT", timer)
end
SLASH_DEADLYBOSSMODS1 = "/dbm"
SLASH_DEADLYBOSSMODSRPULL1 = "/rpull"
SLASH_DEADLYBOSSMODSDWAY1 = "/dway"--/way not used because DBM would load before TomTom and can't check
SlashCmdList["DEADLYBOSSMODSDWAY"] = function(msg)
if DBM:HasMapRestrictions() then
DBM:AddMsg(L.NO_ARROW)
return
end
local x, y = string.split(" ", msg:sub(1):trim())
local xNum, yNum = tonumber(x or ""), tonumber(y or "")
local success
if xNum and yNum then
DBM.Arrow:ShowRunTo(xNum, yNum, 1, nil, true)
success = true
else--Check if they used , instead of space.
x, y = string.split(",", msg:sub(1):trim())
xNum, yNum = tonumber(x or ""), tonumber(y or "")
if xNum and yNum then
DBM.Arrow:ShowRunTo(xNum, yNum, 1, nil, true)
success = true
end
end
if not success then
if DBM.Arrow:IsShown() then
DBM.Arrow:Hide()--Hide
else--error
DBM:AddMsg(L.ARROW_WAY_USAGE)
end
end
end
if not _G["BigWigs"] then
--Register pull and break slash commands for BW converts, if BW isn't loaded
--This shouldn't raise an issue since BW SHOULD load before DBM in any case they are both present.
SLASH_DEADLYBOSSMODSPULL1 = "/pull"
SLASH_DEADLYBOSSMODSBREAK1 = "/break"
SlashCmdList["DEADLYBOSSMODSPULL"] = function(msg)
Pull(tonumber(msg) or 10)
end
SlashCmdList["DEADLYBOSSMODSBREAK"] = function(msg)
Break(tonumber(msg) or 10)
end
end
SlashCmdList["DEADLYBOSSMODSRPULL"] = function(msg)
Pull(30)
end
SlashCmdList["DEADLYBOSSMODS"] = function(msg)
local cmd = msg:lower()
if cmd == "ver" or cmd == "version" then
DBM:ShowVersions(false)
elseif cmd == "ver2" or cmd == "version2" then
DBM:ShowVersions(true)
elseif cmd == "unlock" or cmd == "move" then
DBT:ShowMovableBar()
elseif cmd == "help2" then
for _, v in ipairs(L.SLASHCMD_HELP2) do DBM:AddMsg(v) end
elseif cmd == "help" then
for _, v in ipairs(L.SLASHCMD_HELP) do DBM:AddMsg(v) end
elseif cmd:sub(1, 13) == "timer endloop" then
DBM:CreatePizzaTimer(time, "", nil, nil, nil, true)
elseif cmd:sub(1, 5) == "timer" then
local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
if not (time and text) then
for _, v in ipairs(L.TIMER_USAGE) do DBM:AddMsg(v) end
return
end
local min, sec = string.split(":", time)
min = tonumber(min or "") or 0
sec = tonumber(sec or "")
if min and not sec then
sec = min
min = 0
end
time = min * 60 + sec
DBM:CreatePizzaTimer(time, text)
elseif cmd:sub(1, 6) == "ltimer" then
local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
if not (time and text) then
DBM:AddMsg(L.PIZZA_ERROR_USAGE)
return
end
local min, sec = string.split(":", time)
min = tonumber(min or "") or 0
sec = tonumber(sec or "")
if min and not sec then
sec = min
min = 0
end
time = min * 60 + sec
DBM:CreatePizzaTimer(time, text, nil, nil, true)
elseif cmd:sub(1, 15) == "broadcast timer" then--Standard Timer
local permission = true
if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
DBM:AddMsg(L.ERROR_NO_PERMISSION)
permission = false
end
local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
if not (time and text) then
DBM:AddMsg(L.PIZZA_ERROR_USAGE)
return
end
local min, sec = string.split(":", time)
min = tonumber(min or "") or 0
sec = tonumber(sec or "")
if min and not sec then
sec = min
min = 0
end
time = min * 60 + sec
DBM:CreatePizzaTimer(time, text, permission)
elseif cmd:sub(1, 16) == "broadcast ltimer" then
local permission = true
if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
DBM:AddMsg(L.ERROR_NO_PERMISSION)
permission = false
end
local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
if not (time and text) then
DBM:AddMsg(L.PIZZA_ERROR_USAGE)
return
end
local min, sec = string.split(":", time)
min = tonumber(min or "") or 0
sec = tonumber(sec or "")
if min and not sec then
sec = min
min = 0
end
time = min * 60 + sec
DBM:CreatePizzaTimer(time, text, permission, nil, true)
elseif cmd:sub(0,5) == "break" then
local timer = tonumber(cmd:sub(6)) or 5
Break(timer)
elseif cmd:sub(1, 4) == "pull" then
local timer = tonumber(cmd:sub(5)) or 10
Pull(timer)
elseif cmd:sub(1, 5) == "rpull" then
Pull(30)
elseif cmd:sub(1, 3) == "lag" then
if not LL then
DBM:AddMsg(L.UPDATE_REQUIRES_RELAUNCH)
return
end
LL:RequestLatency()
DBM:AddMsg(L.LAG_CHECKING)
C_TimerAfter(5, function() DBM:ShowLag() end)
elseif cmd:sub(1, 10) == "durability" then
if not LD then
DBM:AddMsg(L.UPDATE_REQUIRES_RELAUNCH)
return
end
LD:RequestDurability()
DBM:AddMsg(L.DUR_CHECKING)
C_TimerAfter(5, function() DBM:ShowDurability() end)
elseif cmd:sub(1, 3) == "hud" then
if DBM:HasMapRestrictions() then
DBM:AddMsg(L.NO_HUD)
return
end
local hudType, target, duration = string.split(" ", msg:sub(4):trim())
if hudType == "" then
for _, v in ipairs(L.HUD_USAGE) do
DBM:AddMsg(v)
end
return
end
local hudDuration = tonumber(duration) or 1200--if no duration defined. 20 minutes to cover even longest of fights
local success = false
if type(hudType) == "string" and hudType:trim() ~= "" then
if hudType:upper() == "HIDE" then
for name, _ in pairs(trackedHudMarkers) do
DBM.HudMap:FreeEncounterMarkerByTarget(12345, name)
trackedHudMarkers[name] = nil
end
return
end
if not target then
DBM:AddMsg(L.HUD_INVALID_TARGET)
return
end
local uId
if target:upper() == "TARGET" and UnitExists("target") then
uId = "target"
elseif target:upper() == "FOCUS" and UnitExists("focus") then
uId = "focus"
else--Try to use it as player name
uId = DBM:GetRaidUnitId(target)
end
if not uId then
DBM:AddMsg(L.HUD_INVALID_TARGET)
return
end
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
DBM:AddMsg(L.HUD_INVALID_SELF)
return
end
local targetName = UnitName(uId)
if hudType:upper() == "ARROW" then
local _, targetClass = UnitClass(uId)
local color2 = RAID_CLASS_COLORS[targetClass]
local m1 = DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, "party", playerName, 0.1, hudDuration, 0, 1, 0, 1, nil, false):Appear()
local m2 = DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, "party", targetName, 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
trackedHudMarkers[playerName] = true
trackedHudMarkers[targetName] = true
m2:EdgeTo(m1, nil, hudDuration, 0, 1, 0, 1)
success = true
elseif hudType:upper() == "DOT" then
local _, targetClass = UnitClass(uId)
local color2 = RAID_CLASS_COLORS[targetClass]
DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, "party", targetName, 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
trackedHudMarkers[targetName] = true
success = true
elseif hudType:upper() == "GREEN" then
DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", targetName, 3.5, hudDuration, 0, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
trackedHudMarkers[targetName] = true
success = true
elseif hudType:upper() == "RED" then
DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", targetName, 3.5, hudDuration, 1, 0, 0, 0.5, nil, false):Pulse(0.5, 0.5)
trackedHudMarkers[targetName] = true
success = true
elseif hudType:upper() == "YELLOW" then
DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", targetName, 3.5, hudDuration, 1, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
trackedHudMarkers[targetName] = true
success = true
elseif hudType:upper() == "BLUE" then
DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", targetName, 3.5, hudDuration, 0, 0, 1, 0.5, nil, false):Pulse(0.5, 0.5)
trackedHudMarkers[targetName] = true
success = true
elseif hudType:upper() == "ICON" then
local icon = GetRaidTargetIndex(uId)
if not icon then
DBM:AddMsg(L.HUD_INVALID_ICON)
return
end
local iconString = DBM:IconNumToString(icon):lower()
DBM.HudMap:RegisterRangeMarkerOnPartyMember(12345, iconString, targetName, 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
trackedHudMarkers[targetName] = true
success = true
else
DBM:AddMsg(L.HUD_INVALID_TYPE)
end
end
if success then
DBM:AddMsg(L.HUD_SUCCESS:format(strFromTime(hudDuration)))
end
elseif cmd:sub(1, 5) == "arrow" then
if DBM:HasMapRestrictions() then
DBM:AddMsg(L.NO_ARROW)
return
end
local x, y, z = string.split(" ", msg:sub(6):trim())
local xNum, yNum, zNum = tonumber(x or ""), tonumber(y or ""), tonumber(z or "")
local success
if xNum and yNum then
DBM.Arrow:ShowRunTo(xNum, yNum, 0)
success = true
elseif type(x) == "string" and x:trim() ~= "" then
local subCmd = x:trim()
if subCmd:upper() == "HIDE" then
DBM.Arrow:Hide()
success = true
elseif subCmd:upper() == "MOVE" then
DBM.Arrow:Move()
success = true
elseif subCmd:upper() == "TARGET" then
DBM.Arrow:ShowRunTo("target")
success = true
elseif subCmd:upper() == "FOCUS" then
DBM.Arrow:ShowRunTo("focus")
success = true
elseif subCmd:upper() == "MAP" then
DBM.Arrow:ShowRunTo(yNum, zNum, 0, nil, true)
success = true
elseif DBM:GetRaidUnitId(subCmd) then
DBM.Arrow:ShowRunTo(subCmd)
success = true
end
end
if not success then
for _, v in ipairs(L.ARROW_ERROR_USAGE) do
DBM:AddMsg(v)
end
end
elseif cmd:sub(1, 10) == "debuglevel" then
local level = tonumber(cmd:sub(11)) or 1
if level < 1 or level > 3 then
DBM:AddMsg("Invalid Value. Debug Level must be between 1 and 3.")
return
end
DBM.Options.DebugLevel = level
DBM:AddMsg("Debug Level is " .. level)
elseif cmd:sub(1, 5) == "debug" then
DBM.Options.DebugMode = DBM.Options.DebugMode == false and true or false
DBM:AddMsg("Debug Message is " .. (DBM.Options.DebugMode and "ON" or "OFF"))
elseif cmd:sub(1, 8) == "whereiam" or cmd:sub(1, 8) == "whereami" then
if DBM:HasMapRestrictions() then
local _, _, _, map = UnitPosition("player")
local mapID = C_Map.GetBestMapForUnit("player")
DBM:AddMsg(("Location Information\nYou are at zone %u (%s).\nLocal Map ID %u (%s)"):format(map, GetRealZoneText(map), mapID, GetZoneText()))
else
local x, y, _, map = UnitPosition("player")
local mapID, mapx, mapy
mapID = C_Map.GetBestMapForUnit("player")
local tempTable = C_Map.GetPlayerMapPosition(mapID, "player")
mapx, mapy = tempTable.x, tempTable.y
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))
end
elseif cmd:sub(1, 7) == "request" then
DBM:Unschedule(DBM.RequestTimers)
DBM:RequestTimers(1)
DBM:RequestTimers(2)
DBM:RequestTimers(3)
elseif cmd:sub(1, 6) == "silent" then
DBM.Options.SilentMode = DBM.Options.SilentMode == false and true or false
DBM:AddMsg(L.SILENTMODE_IS .. (DBM.Options.SilentMode and "ON" or "OFF"))
elseif cmd:sub(1, 10) == "musicstart" then
DBM:TransitionToDungeonBGM(true)
elseif cmd:sub(1, 9) == "musicstop" then
DBM:TransitionToDungeonBGM(false, true)
elseif cmd:sub(1, 9) == "infoframe" then
if DBM.InfoFrame:IsShown() then
DBM.InfoFrame:Hide()
else
DBM.InfoFrame:Show(5, "test")
end
elseif cmd:sub(1, 10) == "aggroframe" then
if DBM.InfoFrame:IsShown() then
DBM.InfoFrame:Hide()
else
DBM.InfoFrame:SetHeader(L.INFOFRAME_AGGRO)
DBM.InfoFrame:Show(7, "playeraggro", 1)
end
else
DBM:LoadGUI()
end
end
end
do
local function updateRangeFrame(r, reverse)
if DBM.RangeCheck:IsShown() then
DBM.RangeCheck:Hide(true)
else
if DBM:HasMapRestrictions() then
DBM:AddMsg(L.NO_RANGE)
end
if r and (r < 201) then
DBM.RangeCheck:Show(r, nil, true, nil, reverse)
else
DBM.RangeCheck:Show(10, nil, true, nil, reverse)
end
end
end
SLASH_DBMRANGE1 = "/range"
SLASH_DBMRANGE2 = "/distance"
SLASH_DBMHUDAR1 = "/hudar"
SLASH_DBMRRANGE1 = "/rrange"
SLASH_DBMRRANGE2 = "/rdistance"
SlashCmdList["DBMRANGE"] = function(msg)
local r = tonumber(msg) or 10
updateRangeFrame(r, false)
end
SlashCmdList["DBMHUDAR"] = function(msg)
local r = tonumber(msg) or 10
DBM.HudMap:ToggleHudar(r)
end
SlashCmdList["DBMRRANGE"] = function(msg)
local r = tonumber(msg) or 10
updateRangeFrame(r, true)
end
end
do
local sortMe = {}
local OutdatedUsers = {}
local function sort(v1, v2)
if v1.revision and not v2.revision then
return true
elseif v2.revision and not v1.revision then
return false
elseif v1.revision and v2.revision then
return v1.revision > v2.revision
else
return (v1.bwversion or 0) > (v2.bwversion or 0)
end
end
function DBM:ShowVersions(notify)
for _, v in pairs(raid) do
tinsert(sortMe, v)
end
tsort(sortMe, sort)
twipe(OutdatedUsers)
self:AddMsg(L.VERSIONCHECK_HEADER)
for _, v in ipairs(sortMe) do
local name = v.name
local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
if playerColor then
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)
end
if v.displayVersion and not v.bwversion then--DBM, no BigWigs
if self.Options.ShowAllVersions then
self:AddMsg(L.VERSIONCHECK_ENTRY:format(name, L.DBM.." "..v.displayVersion, showRealDate(v.revision), v.VPVersion or ""), false)--Only display VP version if not running two mods
end
if notify and v.revision < self.ReleaseRevision then
SendChatMessage(chatPrefixShort..L.YOUR_VERSION_OUTDATED, "WHISPER", nil, v.name)
end
elseif self.Options.ShowAllVersions and v.displayVersion and v.bwversion then--DBM & BigWigs
self:AddMsg(L.VERSIONCHECK_ENTRY_TWO:format(name, L.DBM.." "..v.displayVersion, showRealDate(v.revision), L.BIG_WIGS, bwVersionResponseString:format(v.bwversion, v.bwhash)), false)
elseif self.Options.ShowAllVersions and not v.displayVersion and v.bwversion then--BigWigs, No DBM
self:AddMsg(L.VERSIONCHECK_ENTRY:format(name, L.BIG_WIGS, bwVersionResponseString:format(v.bwversion, v.bwhash), ""), false)
else
if self.Options.ShowAllVersions then
self:AddMsg(L.VERSIONCHECK_ENTRY_NO_DBM:format(name), false)
end
end
end
local TotalUsers = #sortMe
local NoDBM = 0
local NoBigwigs = 0
local OldMod = 0
for i = #sortMe, 1, -1 do
if not sortMe[i].revision then
NoDBM = NoDBM + 1
end
if not (sortMe[i].bwversion) then
NoBigwigs = NoBigwigs + 1
end
--Table sorting sorts dbm to top, bigwigs underneath. Highest version dbm always at top. so sortMe[1]
--This check compares all dbm version to highest RELEASE version in raid.
if sortMe[i].revision and (sortMe[i].revision < sortMe[1].version) or sortMe[i].bwversion and (sortMe[i].bwversion < fakeBWVersion) then
OldMod = OldMod + 1
local name = sortMe[i].name
local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
if playerColor then
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)
end
tinsert(OutdatedUsers, name)
end
end
local TotalDBM = TotalUsers - NoDBM
local TotalBW = TotalUsers - NoBigwigs
self:AddMsg("---", false)
self:AddMsg(L.VERSIONCHECK_FOOTER:format(TotalDBM, TotalBW), false)
self:AddMsg(L.VERSIONCHECK_OUTDATED:format(OldMod, #OutdatedUsers > 0 and tconcat(OutdatedUsers, ", ") or NONE), false)
twipe(OutdatedUsers)
twipe(sortMe)
for i = #sortMe, 1, -1 do
sortMe[i] = nil
end
end
end
-- Lag checking
do
local sortLag = {}
local nolagResponse = {}
local function sortit(v1, v2)
return (v1.worldlag or 0) < (v2.worldlag or 0)
end
function DBM:ShowLag()
for _, v in pairs(raid) do
tinsert(sortLag, v)
end
tsort(sortLag, sortit)
self:AddMsg(L.LAG_HEADER)
for _, v in ipairs(sortLag) do
local name = v.name
local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
if playerColor then
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)
end
if v.worldlag then
self:AddMsg(L.LAG_ENTRY:format(name, v.worldlag, v.homelag), false)
else
tinsert(nolagResponse, v.name)
end
end
if #nolagResponse > 0 then
self:AddMsg(L.LAG_FOOTER:format(tconcat(nolagResponse, ", ")), false)
for i = #nolagResponse, 1, -1 do
nolagResponse[i] = nil
end
end
for i = #sortLag, 1, -1 do
sortLag[i] = nil
end
end
if LL then
LL:Register("DBM", function(homelag, worldlag, sender, channel)
if sender and raid[sender] then
raid[sender].homelag = homelag
raid[sender].worldlag = worldlag
end
end)
end
end
-- Durability checking
do
local sortDur = {}
local nodurResponse = {}
local function sortit(v1, v2)
return (v1.worldlag or 0) < (v2.worldlag or 0)
end
function DBM:ShowDurability()
for _, v in pairs(raid) do
tinsert(sortDur, v)
end
tsort(sortDur, sortit)
self:AddMsg(L.DUR_HEADER)
for _, v in ipairs(sortDur) do
local name = v.name
local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
if playerColor then
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)
end
if v.durpercent then
self:AddMsg(L.DUR_ENTRY:format(name, v.durpercent, v.durbroken), false)
else
tinsert(nodurResponse, v.name)
end
end
if #nodurResponse > 0 then
self:AddMsg(L.LAG_FOOTER:format(tconcat(nodurResponse, ", ")), false)
for i = #nodurResponse, 1, -1 do
nodurResponse[i] = nil
end
end
for i = #sortDur, 1, -1 do
sortDur[i] = nil
end
end
if LD then
LD:Register("DBM", function(percent, broken, sender, channel)
if sender and raid[sender] then
raid[sender].durpercent = percent
raid[sender].durbroken = broken
end
end)
end
end
-------------------
-- Pizza Timer --
-------------------
do
local function loopTimer(time, text, broadcast, sender)
DBM:CreatePizzaTimer(time, text, broadcast, sender, true)
end
local ignore = {}
--Standard Pizza Timer
function DBM:CreatePizzaTimer(time, text, broadcast, sender, loop, terminate, whisperTarget)
if terminate or time == 0 then
self:Unschedule(loopTimer)
DBT:CancelBar(text)
fireEvent("DBM_TimerStop", "DBMPizzaTimer")
-- Fire cancelation of pizza timer
if broadcast then
text = text:sub(1, 16)
text = text:gsub("%%t", UnitName("target") or "<no target>")
if whisperTarget then
C_ChatInfo.SendAddonMessageLogged("D4", ("UW\t0\t%s"):format(text), "WHISPER", whisperTarget)
else
sendLoggedSync("U", ("0\t%s"):format(text))
end
end
return
end
if sender and ignore[sender] then return end
text = text:sub(1, 16)
text = text:gsub("%%t", UnitName("target") or "<no target>")
if time < 3 then
self:AddMsg(L.PIZZA_ERROR_USAGE)
return
end
DBT:CreateBar(time, text, 237538)
fireEvent("DBM_TimerStart", "DBMPizzaTimer", text, time, "237538", "pizzatimer", nil, 0)
if broadcast then
if whisperTarget then
--no dbm function uses whisper for pizza timers
--this allows weak aura creators or other modders to use the pizza timer object unicast via whisper instead of spamming group sync channels
C_ChatInfo.SendAddonMessageLogged("D4", ("UW\t%s\t%s"):format(time, text), "WHISPER", whisperTarget)
else
sendLoggedSync("U", ("%s\t%s"):format(time, text))
end
end
if sender then self:ShowPizzaInfo(text, sender) end
if loop then
self:Unschedule(loopTimer)--Only one loop timer supported at once doing this, but much cleaner this way
self:Schedule(time, loopTimer, time, text, broadcast, sender)
end
end
function DBM:AddToPizzaIgnore(name)
ignore[name] = true
end
end
function DBM:ShowPizzaInfo(id, sender)
if self.Options.ShowPizzaMessage then
self:AddMsg(L.PIZZA_SYNC_INFO:format(sender, id))
end
end
------------------
-- Hyperlinks --
------------------
do
local ignore, cancel
local popuplevel = 0
local function showPopupConfirmIgnore(ignore, cancel)
local popup = CreateFrame("Frame", "DBMHyperLinks", UIParent, DBM:IsShadowlands() and "BackdropTemplate")
popup.backdropInfo = {
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark", -- 312922
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", -- 131072
tile = true,
tileSize = 16,
edgeSize = 16,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
}
if not DBM:IsShadowlands() then
popup:SetBackdrop(popup.backdropInfo)
else
popup:ApplyBackdrop()
end
popup:SetSize(500, 80)
popup:SetPoint("TOP", UIParent, "TOP", 0, -200)
popup:SetFrameStrata("DIALOG")
popup:SetFrameLevel(popuplevel)
popuplevel = popuplevel + 1
local text = popup:CreateFontString()
text:SetFontObject(ChatFontNormal)
text:SetWidth(470)
text:SetWordWrap(true)
text:SetPoint("TOP", popup, "TOP", 0, -15)
text:SetText(L.PIZZA_CONFIRM_IGNORE:format(ignore))
local accept = CreateFrame("Button", nil, popup)
accept:SetNormalTexture(130763)--"Interface\\Buttons\\UI-DialogBox-Button-Up"
accept:SetPushedTexture(130761)--"Interface\\Buttons\\UI-DialogBox-Button-Down"
accept:SetHighlightTexture(130762, "ADD")--"Interface\\Buttons\\UI-DialogBox-Button-Highlight"
accept:SetSize(128, 35)
accept:SetPoint("BOTTOM", popup, "BOTTOM", -75, 0)
accept:SetScript("OnClick", function(f) DBM:AddToPizzaIgnore(ignore) DBT:CancelBar(cancel) f:GetParent():Hide() end)
local atext = accept:CreateFontString()
atext:SetFontObject(ChatFontNormal)
atext:SetPoint("CENTER", accept, "CENTER", 0, 5)
atext:SetText(YES)
local decline = CreateFrame("Button", nil, popup)
decline:SetNormalTexture(130763)--"Interface\\Buttons\\UI-DialogBox-Button-Up"
decline:SetPushedTexture(130761)--"Interface\\Buttons\\UI-DialogBox-Button-Down"
decline:SetHighlightTexture(130762, "ADD")--"Interface\\Buttons\\UI-DialogBox-Button-Highlight"
decline:SetSize(128, 35)
decline:SetPoint("BOTTOM", popup, "BOTTOM", 75, 0)
decline:SetScript("OnClick", function(f) f:GetParent():Hide() end)
local dtext = decline:CreateFontString()
dtext:SetFontObject(ChatFontNormal)
dtext:SetPoint("CENTER", decline, "CENTER", 0, 5)
dtext:SetText(NO)
PlaySound(850)
end
local function linkHook(self, link, string, button, ...)
local linkType, arg1, arg2, arg3, arg4, arg5, arg6 = strsplit(":", link)
if linkType ~= "DBM" then
return
end
if arg1 == "cancel" then
DBT:CancelBar(link:match("DBM:cancel:(.+):nil$"))
elseif arg1 == "ignore" then
cancel = link:match("DBM:ignore:(.+):[^%s:]+$")
ignore = link:match(":([^:]+)$")
showPopupConfirmIgnore(ignore, cancel)
elseif arg1 == "update" then
DBM:ShowUpdateReminder(arg2, arg3) -- displayVersion, revision
-- elseif arg1 == "forumsnews" then
-- DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG_NEWS, "https://discord.gg/DF5mffk")
-- elseif arg1 == "forums" then
-- DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG)
elseif arg1 == "noteshare" then
local mod = DBM:GetModByName(arg2 or "")
if mod then
DBM:ShowNoteEditor(mod, arg3, arg4, arg5, arg6)--modvar, ability, text, sender
else--Should not happen, since mod was verified before getting this far, but just in case
DBM:Debug("Bad note share, mod not valid")
end
end
end
-- local frame = _G[tostring(DBM.Options.ChatFrame)]
-- frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
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)
local i = 1
while _G["ChatFrame" .. i] do
if _G["ChatFrame" .. i] ~= DEFAULT_CHAT_FRAME then
_G["ChatFrame" .. i]:HookScript("OnHyperlinkClick", linkHook)
end
i = i + 1
end
end
do
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
function ItemRefTooltip:SetHyperlink(link, ...)
if link and link:sub(0, 4) == "DBM:" then
return
end
return old(self, link, ...)
end
end
-----------------
-- GUI Stuff --
-----------------
do
local callOnLoad = {}
function DBM:LoadGUI()
if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
self:AddMsg(L.VEM)
return
end
if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
self:AddMsg(L.OUTDATEDPROFILES)
return
end
if GetAddOnEnableState(playerName, "DBM-SpellTimers") >= 1 then
local version = GetAddOnMetadata("DBM-SpellTimers", "Version") or "r0"
version = tonumber(string.sub(version, 2, 4)) or 0
if version < 122 and not self.Options.DebugMode then
self:AddMsg(L.OUTDATEDSPELLTIMERS)
return
end
end
if GetAddOnEnableState(playerName, "DBM-RaidLeadTools") >= 1 and not self.Options.DebugMode then
self:AddMsg(L.OUTDATEDRLT)
return
end
if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
self:AddMsg(L.DPMCORE)
return
end
if GetAddOnEnableState(playerName, "DBM-VictorySound") >= 1 then
self:AddMsg(L.VICTORYSOUND)
return
end
if not dbmIsEnabled then
self:AddMsg(L.UPDATEREMINDER_DISABLE)
return
end
if self.NewerVersion and showConstantReminder >= 1 then
AddMsg(self, L.UPDATEREMINDER_HEADER:format(self.NewerVersion, showRealDate(self.HighestRelease)))
end
local firstLoad = false
if not IsAddOnLoaded("DBM-GUI") then
local enabled = GetAddOnEnableState(playerName, "DBM-GUI")
if enabled == 0 then
EnableAddOn("DBM-GUI")
end
local loaded, reason = LoadAddOn("DBM-GUI")
if not loaded then
if reason then
self:AddMsg(L.LOAD_GUI_ERROR:format(tostring(_G["ADDON_"..reason or ""])))
else
self:AddMsg(L.LOAD_GUI_ERROR:format(L.UNKNOWN))
end
return false
end
if not InCombatLockdown() and not UnitAffectingCombat("player") and not IsFalling() then--We loaded in combat but still need to avoid garbage collect in combat
collectgarbage("collect")
end
firstLoad = true
end
DBM_GUI:ShowHide()
if firstLoad then
tsort(callOnLoad, function(v1, v2) return v1[2] < v2[2] end)
for _, v in ipairs(callOnLoad) do v[1]() end
end
end
function DBM:RegisterOnGuiLoadCallback(f, sort)
tinsert(callOnLoad, {f, sort or mhuge})
end
end
----------------------
-- Minimap Button --
----------------------
do
--Old LDB Functions
local frame = CreateFrame("Frame", "DBMLDBFrame")
--New LDB Object
if LibStub("LibDataBroker-1.1", true) then
dataBroker = LibStub("LibDataBroker-1.1"):NewDataObject("DBM",
{type = "launcher", label = "DBM", icon = "Interface\\AddOns\\DBM-Core\\textures\\dbm_airhorn"}
)
function dataBroker.OnClick(self, button)
if IsShiftKeyDown() then return end
-- if IsAltKeyDown() and button == "RightButton" then
-- DBM.Options.SilentMode = DBM.Options.SilentMode == false and true or false
-- DBM:AddMsg(L.SILENTMODE_IS .. (DBM.Options.SilentMode and "ON" or "OFF"))
-- else
DBM:LoadGUI()
-- end
end
function dataBroker.OnTooltipShow(GameTooltip)
GameTooltip:SetText(L.MINIMAP_TOOLTIP_HEADER, 1, 1, 1)
GameTooltip:AddLine(("%s (%s)"):format(DBM.DisplayVersion, showRealDate(DBM.Revision)), NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
GameTooltip:AddLine(" ")
GameTooltip:AddLine(L.MINIMAP_TOOLTIP_FOOTER, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b, 1)
GameTooltip:AddLine(L.LDB_TOOLTIP_HELP1, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b)
-- GameTooltip:AddLine(L.LDB_TOOLTIP_HELP2, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b)
end
end
function DBM:ToggleMinimapButton()
DBM_MinimapIcon.hide = not DBM_MinimapIcon.hide
if DBM_MinimapIcon.hide then
LibStub("LibDBIcon-1.0"):Hide("DBM")
else
LibStub("LibDBIcon-1.0"):Show("DBM")
end
end
end
-------------------------------------------------
-- Raid/Party Handling and Unit ID Utilities --
-------------------------------------------------
do
local bwVersionQueryString = "Q^%d^%s"--Only used here
local inRaid = false
local raidGuids = {}
local iconSeter = {}
-- save playerinfo into raid table on load. (for solo raid)
DBM:RegisterOnLoadCallback(function()
C_TimerAfter(6, function()
if not raid[playerName] then
raid[playerName] = {}
raid[playerName].name = playerName
raid[playerName].shortname = playerName
raid[playerName].guid = UnitGUID("player")
raid[playerName].rank = 0
raid[playerName].class = playerClass
raid[playerName].id = "player"
raid[playerName].groupId = 0
raid[playerName].revision = DBM.Revision
raid[playerName].version = DBM.ReleaseRevision
raid[playerName].displayVersion = DBM.DisplayVersion
raid[playerName].locale = GetLocale()
raid[playerName].enabledIcons = tostring(not DBM.Options.DontSetIcons)
raidGuids[UnitGUID("player") or ""] = playerName
end
end)
end)
local function updateAllRoster(self)
if IsInRaid() then
if not inRaid then
inRaid = true
sendSync("H")
SendAddonMessage("BigWigs", bwVersionQueryString:format(0, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "RAID")
self:Schedule(2, self.RoleCheck, false, self)
fireEvent("DBM_raidJoin", playerName)
local bigWigs = _G["BigWigs"]
if bigWigs and bigWigs.db.profile.raidicon and not self.Options.DontSetIcons and self:GetRaidRank() > 0 then--Both DBM and bigwigs have raid icon marking turned on.
self:AddMsg(L.BIGWIGS_ICON_CONFLICT)--Warn that one of them should be turned off to prevent conflict (which they turn off is obviously up to raid leaders preference, dbm accepts either or turned off to stop this alert)
end
end
for i = 1, GetNumGroupMembers() do
local name, rank, subgroup, _, _, className = GetRaidRosterInfo(i)
-- Maybe GetNumGroupMembers() bug? Seems that GetNumGroupMembers() rarely returns bad value, causing GetRaidRosterInfo() returns to nil.
-- Filter name = nil to prevent nil table error.
if name then
local id = "raid" .. i
local shortname = UnitName(id)
if (not raid[name]) and inRaid then
fireEvent("DBM_raidJoin", name)
end
raid[name] = raid[name] or {}
raid[name].name = name
raid[name].shortname = shortname
raid[name].rank = rank
raid[name].subgroup = subgroup
raid[name].class = className
raid[name].id = id
raid[name].groupId = i
raid[name].guid = UnitGUID(id) or ""
raid[name].updated = true
raidGuids[UnitGUID(id) or ""] = name
end
end
enableIcons = false
twipe(iconSeter)
for i, v in pairs(raid) do
if not v.updated then
raidGuids[v.guid] = nil
raid[i] = nil
removeEntry(newerVersionPerson, i)
fireEvent("DBM_raidLeave", i)
else
v.updated = nil
if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
iconSeter[#iconSeter + 1] = v.revision.." "..v.name
end
end
end
if #iconSeter > 0 then
tsort(iconSeter, function(a, b) return a > b end)
local elected = iconSeter[1]
if playerName == elected:sub(elected:find(" ") + 1) then--Highest revision in raid, auto allow, period, even if out of date, you're revision in raid that has assist
enableIcons = true
DBM:Debug("You have been elected as primary icon setter for raid for having newest revision in raid that has assist/lead", 2)
end
--Initiate backups that at least have latest version, in case the main elect doesn't have icons enabled
for i = 2, 3 do--Allow top 3 revisions in raid to set icons, instead of just top one
local electedBackup = iconSeter[i]
if updateNotificationDisplayed == 0 and electedBackup and playerName == electedBackup:sub(elected:find(" ") + 1) then
enableIcons = true
DBM:Debug("You have been elected as one of 2 backup icon setters in raid that have assist/lead", 2)
end
end
end
elseif IsInGroup() then
if not inRaid then
-- joined a new party
inRaid = true
sendSync("H")
SendAddonMessage("BigWigs", bwVersionQueryString:format(0, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "PARTY")
self:Schedule(2, self.RoleCheck, false, self)
fireEvent("DBM_partyJoin", playerName)
end
for i = 0, GetNumSubgroupMembers() do
local id
if (i == 0) then
id = "player"
else
id = "party"..i
end
local name = GetUnitName(id, true)
local shortname = UnitName(id)
local rank = UnitIsGroupLeader(id) and 2 or 0
local _, className = UnitClass(id)
if (not raid[name]) and inRaid then
fireEvent("DBM_partyJoin", name)
end
raid[name] = raid[name] or {}
raid[name].name = name
raid[name].shortname = shortname
raid[name].guid = UnitGUID(id) or ""
raid[name].rank = rank
raid[name].class = className
raid[name].id = id
raid[name].groupId = i
raid[name].updated = true
raidGuids[UnitGUID(id) or ""] = name
end
enableIcons = false
twipe(iconSeter)
for i, v in pairs(raid) do
if not v.updated then
raidGuids[v.guid] = nil
raid[i] = nil
removeEntry(newerVersionPerson, i)
fireEvent("DBM_partyLeave", i)
else
v.updated = nil
if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
iconSeter[#iconSeter + 1] = v.revision.." "..v.name
end
end
end
if #iconSeter > 0 then
tsort(iconSeter, function(a, b) return a > b end)
local elected = iconSeter[1]
if playerName == elected:sub(elected:find(" ") + 1) then
enableIcons = true
end
end
else
-- left the current group/raid
inRaid = false
enableIcons = true
fireEvent("DBM_raidLeave", playerName)
twipe(raid)
twipe(newerVersionPerson)
-- restore playerinfo into raid table on raidleave. (for solo raid)
raid[playerName] = {}
raid[playerName].name = playerName
raid[playerName].shortname = playerName
raid[playerName].guid = UnitGUID("player")
raid[playerName].rank = 0
raid[playerName].class = playerClass
raid[playerName].id = "player"
raid[playerName].groupId = 0
raid[playerName].revision = DBM.Revision
raid[playerName].version = DBM.ReleaseRevision
raid[playerName].displayVersion = DBM.DisplayVersion
raid[playerName].locale = GetLocale()
raidGuids[UnitGUID("player")] = playerName
end
end
function DBM:GROUP_ROSTER_UPDATE(force)
self:Unschedule(updateAllRoster)
if force then
updateAllRoster(self)
else
self:Schedule(1.5, updateAllRoster, self)
end
end
function DBM:INSTANCE_GROUP_SIZE_CHANGED()
local _, _, _, _, _, _, _, _, instanceGroupSize = GetInstanceInfo()
LastGroupSize = instanceGroupSize
end
--C_Map.GetMapGroupMembersInfo
function DBM:GetNumRealPlayersInZone()
if not IsInGroup() then return 1 end
local total = 0
local _, _, _, currentMapId = UnitPosition("player")
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
local _, _, _, targetMapId = UnitPosition("raid"..i)
if targetMapId == currentMapId then
total = total + 1
end
end
else
total = 1--add player/self for "party" count
for i = 1, GetNumSubgroupMembers() do
local _, _, _, targetMapId = UnitPosition("party"..i)
if targetMapId == currentMapId then
total = total + 1
end
end
end
return total
end
function DBM:GetRaidRank(name)
name = name or playerName
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.
return UnitIsGroupLeader("player") and 2 or UnitIsGroupAssistant("player") and 1 or 0
else
return (raid[name] and raid[name].rank) or 0
end
end
function DBM:GetRaidSubgroup(name)
return (raid[name] and raid[name].subgroup) or 0
end
function DBM:GetRaidClass(name)
if raid[name] then
return raid[name].class or "UNKNOWN", raid[name].id and GetRaidTargetIndex(raid[name].id) or 0
else
return "UNKNOWN", 0
end
end
function DBM:GetRaidUnitId(name)
for i = 1, 5 do
local unitId = "boss"..i
local bossName = UnitName(unitId)
if bossName and bossName == name then
return unitId
end
end
return raid[name] and raid[name].id
end
function DBM:GetEnemyUnitIdByGUID(guid)
for i = 1, 5 do
local unitId = "boss"..i
local guid2 = UnitGUID(unitId)
if guid == guid2 then
return unitId
end
end
local idType = (IsInRaid() and "raid") or "party"
for i = 0, GetNumGroupMembers() do
local unitId = ((i == 0) and "target") or idType..i.."target"
local guid2 = UnitGUID(unitId)
if guid == guid2 then
return unitId
end
end
return L.UNKNOWN
end
function DBM:GetPlayerGUIDByName(name)
return raid[name] and raid[name].guid
end
function DBM:GetMyPlayerInfo()
return playerName, playerLevel, playerRealm
end
--Intentionally grabs server name at all times, usually to make sure warning/infoframe target info can name match the combat log in the table
function DBM:GetUnitFullName(uId)
if not uId then return nil end
return GetUnitName(uId, true)
end
--Shortens name but custom so we add * to off realmers instead of stripping it entirely like Ambiguate does
--Technically GetUnitName without "true" can be used to shorten name to "name (*)" but "name*" is even shorter which is why we do this
function DBM:GetShortServerName(name)
if not self.Options.StripServerName then return name end--If strip is disabled, just return name
local shortName, serverName = string.split("-", name)
if serverName and serverName ~= playerRealm then
return shortName.."*"
else
return name
end
end
function DBM:GetFullPlayerNameByGUID(guid)
return raidGuids[guid]
end
function DBM:GetPlayerNameByGUID(guid)
return raidGuids[guid] and raidGuids[guid]:gsub("%-.*$", "")
end
function DBM:GetGroupId(name)
local raidMember = raid[name] or raid[GetUnitName(name, true) or ""]
return raidMember and raidMember.groupId or 0
end
end
do
-- yes, we still do avoid memory allocations during fights; so we don't use a closure around a counter here
-- this seems to be the easiest way to write an iterator that returns the unit id *string* as first argument without a memory allocation
local function raidIterator(groupMembers, uId)
local a, b = uId:byte(-2, -1)
local i = (a >= 0x30 and a <= 0x39 and (a - 0x30) * 10 or 0) + b - 0x30
if i < groupMembers then
return "raid" .. i + 1, i + 1
end
end
local function partyIterator(groupMembers, uId)
if not uId then
return "player", 0
elseif uId == "player" then
if groupMembers > 0 then
return "party1", 1
end
else
local i = uId:byte(-1) - 0x30
if i < groupMembers then
return "party" .. i + 1, i + 1
end
end
end
local function soloIterator(_, state)
if not state then -- no state == first call
return "player", 0
end
end
-- returns the unit ids of all raid or party members, including the player's own id
-- limitations: will break if there are ever raids with more than 99 players or partys with more than 10
function DBM:GetGroupMembers()
if IsInRaid() then
return raidIterator, GetNumGroupMembers(), "raid0"
elseif IsInGroup() then
return partyIterator, GetNumSubgroupMembers(), nil
else
-- solo!
return soloIterator, nil, nil
end
end
end
function DBM:GetNumGroupMembers()
return IsInGroup() and GetNumGroupMembers() or 1
end
--For returning the number of players actually in zone with us for status functions
--This is very touchy though and will fail if everyone isn't in same SUB zone (ie same room/area)
--It should work for pretty much any case but outdoor
function DBM:GetNumRealGroupMembers()
if not IsInInstance() then--Not accurate outside of instances (such as world bosses)
return IsInGroup() and GetNumGroupMembers() or 1--So just return regular group members.
end
local _, _, _, currentMapId = UnitPosition("player")
local realGroupMembers = 0
if IsInGroup() then
for uId in self:GetGroupMembers() do
local _, _, _, targetMapId = UnitPosition(uId)
if targetMapId == currentMapId then
realGroupMembers = realGroupMembers + 1
end
end
else
return 1
end
return realGroupMembers
end
function DBM:GetUnitCreatureId(uId)
local guid = UnitGUID(uId)
return self:GetCIDFromGUID(guid)
end
--Creature/Vehicle/Pet
----<type>:<subtype>:<realmID>:<mapID>:<serverID>:<dbID>:<creationbits>
--Player/Item
----<type>:<realmID>:<dbID>
function DBM:GetCIDFromGUID(guid)
local guidType, _, playerdbID, _, _, cid, _ = strsplit("-", guid or "")
if guidType and (guidType == "Creature" or guidType == "Vehicle" or guidType == "Pet") then
return tonumber(cid)
elseif type and (guidType == "Player" or guidType == "Item") then
return tonumber(playerdbID)
end
return 0
end
function DBM:IsNonPlayableGUID(guid)
if type(guid) == "number" then return false end
local guidType = strsplit("-", guid or "")
if guidType and (guidType == "Creature" or guidType == "Vehicle" or guidType == "NPC") then--To determine, add pet or not?
return true
end
return false
end
function DBM:IsCreatureGUID(guid)
local guidType = strsplit("-", guid or "")
if guidType and (guidType == "Creature" or guidType == "Vehicle") then--To determine, add pet or not?
return true
end
return false
end
function DBM:GetBossUnitId(name, bossOnly)--Deprecated, only old mods use this
local returnUnitID
for i = 1, 5 do
if UnitName("boss" .. i) == name then
returnUnitID = "boss"..i
end
end
if not returnUnitID and not bossOnly then
for uId in self:GetGroupMembers() do
if UnitName(uId .. "target") == name and not UnitIsPlayer(uId .. "target") then
returnUnitID = uId.."target"
end
end
end
return returnUnitID
end
function DBM:GetUnitIdFromGUID(cidOrGuid, bossOnly)
local returnUnitID
for i = 1, 5 do
local unitId = "boss"..i
local bossGUID = UnitGUID(unitId)
if type(cidOrGuid) == "number" then--CID passed
local cid = self:GetCIDFromGUID(bossGUID)
if cid == cidOrGuid then
returnUnitID = unitId
end
else--GUID passed
if bossGUID == cidOrGuid then
returnUnitID = unitId
end
end
end
--Didn't find valid unitID from boss units, scan raid targets
if not returnUnitID and not bossOnly then
for uId in DBM:GetGroupMembers() do--Do not use self on this function, because self might be bossModPrototype
local unitId = uId .. "target"
local bossGUID = UnitGUID(unitId)
local cid = self:GetCIDFromGUID(cidOrGuid)
if bossGUID == cidOrGuid or cid == cidOrGuid then
returnUnitID = unitId
end
end
end
return returnUnitID
end
function DBM:CheckNearby(range, targetname)
if not targetname and DBM.RangeCheck:GetDistanceAll(range) then--Do not use self on this function, because self might be bossModPrototype
return true--No target name means check if anyone is near self, period
else
local uId = DBM:GetRaidUnitId(targetname)--Do not use self on this function, because self might be bossModPrototype
if uId and not UnitIsUnit("player", uId) then
local inRange = DBM.RangeCheck:GetDistance(uId)--Do not use self on this function, because self might be bossModPrototype
if inRange and inRange < range+0.5 then
return true
end
end
end
return false
end
function DBM:IsTrivial(customLevel)
--if timewalking or chromie time, it's always non trivial content
if C_PlayerInfo.IsPlayerInChromieTime() or difficultyIndex == 24 or difficultyIndex == 33 then
return false
end
--if custom level passed, we always hard check that level for trivial vs non trivial
if customLevel then--Custom level parameter
if playerLevel >= customLevel then
return true
end
else
--First, auto bail and return non trivial if it's an instancce not in table to prevent nil error
if not instanceDifficultyBylevel[LastInstanceMapID] then return false end
--Content is trivial if player level is 10 higher than content involved
if playerLevel >= (instanceDifficultyBylevel[LastInstanceMapID][1]+10) then
return true
end
end
return false
end
---------------
-- Options --
---------------
function DBM:AddDefaultOptions(t1, t2)
for i, v in pairs(t2) do
if t1[i] == nil then
t1[i] = v
elseif type(v) == "table" and type(t1[i]) == "table" then
self:AddDefaultOptions(t1[i], v)
end
end
end
function DBM:LoadModOptions(modId, inCombat, first)
local oldSavedVarsName = modId:gsub("-", "").."_SavedVars"
local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
local savedStatsName = modId:gsub("-", "").."_SavedStats"
local fullname = playerName.."-"..playerRealm
if not currentSpecID or not currentSpecGroup then
self:SetCurrentSpecInfo()
end
local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
if not _G[savedVarsName] then _G[savedVarsName] = {} end
local savedOptions = _G[savedVarsName][fullname] or {}
local savedStats = _G[savedStatsName] or {}
local existId = {}
for _, id in ipairs(self.ModLists[modId]) do
existId[id] = true
-- init
if not savedOptions[id] then savedOptions[id] = {} end
local mod = self:GetModByName(id)
-- migrate old option
if _G[oldSavedVarsName] and _G[oldSavedVarsName][id] then
self:Debug("LoadModOptions: Found old options, importing", 2)
local oldTable = _G[oldSavedVarsName][id]
_G[oldSavedVarsName][id] = nil
savedOptions[id][profileNum] = oldTable
end
if not savedOptions[id][profileNum] and not first then--previous profile not found. load defaults
self:Debug("LoadModOptions: No saved options, creating defaults for profile "..profileNum, 2)
local defaultOptions = {}
for option, optionValue in pairs(mod.DefaultOptions) do
if type(optionValue) == "table" then
optionValue = optionValue.value
elseif type(optionValue) == "string" then
optionValue = mod:GetRoleFlagValue(optionValue)
end
defaultOptions[option] = optionValue
end
savedOptions[id][profileNum] = defaultOptions
else
savedOptions[id][profileNum] = savedOptions[id][profileNum] or mod.Options
--check new option
for option, optionValue in pairs(mod.DefaultOptions) do
if savedOptions[id][profileNum][option] == nil then
if type(optionValue) == "table" then
optionValue = optionValue.value
elseif type(optionValue) == "string" then
optionValue = mod:GetRoleFlagValue(optionValue)
end
savedOptions[id][profileNum][option] = optionValue
end
end
--clean unused saved variables (do not work on combat load)
--Why are saved options cleaned twice?
if not inCombat then
for option, _ in pairs(savedOptions[id][profileNum]) do
if type(option) == "number" then
self:Debug("|cffff0000Everybody knows shit's fucked: |r"..option)
end
if (mod.DefaultOptions[option] == nil) and not (option:find("talent") or option:find("FastestClear") or option:find("CVAR") or option:find("RestoreSetting")) then
savedOptions[id][profileNum][option] = nil
elseif mod.DefaultOptions[option] and (type(mod.DefaultOptions[option]) == "table") then--recover broken dropdown option
if savedOptions[id][profileNum][option] and (type(savedOptions[id][profileNum][option]) == "boolean") then
savedOptions[id][profileNum][option] = mod.DefaultOptions[option].value
end
--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.
elseif option:find("TColor") then
if savedOptions[id][profileNum][option] and savedOptions[id][profileNum][option] == 0 and mod.DefaultOptions[option] and mod.DefaultOptions[option] ~= 0 then
savedOptions[id][profileNum][option] = mod.DefaultOptions[option]
self:Debug("Migrated "..option.." to option defaults")
end
--Fix options for custom special warning sounds not in addons folder that are not using soundkit IDs
elseif option:find("SWSound") then
if savedOptions[id][profileNum][option] and (type(savedOptions[id][profileNum][option]) == "string") and (savedOptions[id][profileNum][option] ~= "") and (savedOptions[id][profileNum][option] ~= "None") then
local searchMsg = (savedOptions[id][profileNum][option]):lower()
if not searchMsg:find("addons") then
savedOptions[id][profileNum][option] = mod.DefaultOptions[option]
self:Debug("Migrated "..option.." to option defaults")
end
end
end
end
end
end
--apply saved option to actual option table
mod.Options = savedOptions[id][profileNum]
--stats init (only first load)
if first then
savedStats[id] = savedStats[id] or {}
local stats = savedStats[id]
stats.normalKills = stats.normalKills or 0
stats.normalPulls = stats.normalPulls or 0
stats.heroicKills = stats.heroicKills or 0
stats.heroicPulls = stats.heroicPulls or 0
stats.challengeKills = stats.challengeKills or 0
stats.challengePulls = stats.challengePulls or 0
stats.challengeBestRank = stats.challengeBestRank or 0
stats.mythicKills = stats.mythicKills or 0
stats.mythicPulls = stats.mythicPulls or 0
stats.normal25Kills = stats.normal25Kills or 0
stats.normal25Kills = stats.normal25Kills or 0
stats.normal25Pulls = stats.normal25Pulls or 0
stats.heroic25Kills = stats.heroic25Kills or 0
stats.heroic25Pulls = stats.heroic25Pulls or 0
stats.lfr25Kills = stats.lfr25Kills or 0
stats.lfr25Pulls = stats.lfr25Pulls or 0
stats.timewalkerKills = stats.timewalkerKills or 0
stats.timewalkerPulls = stats.timewalkerPulls or 0
mod.stats = stats
--run OnInitialize function
if mod.OnInitialize then mod:OnInitialize(mod) end
end
end
--clean unused saved variables (do not work on combat load)
--Why are saved options cleaned twice?
if not inCombat then
for id, _ in pairs(savedOptions) do
if not existId[id] and not (id:find("talent") or id:find("FastestClear") or id:find("CVAR") or id:find("RestoreSetting")) then
savedOptions[id] = nil
end
end
for id, _ in pairs(savedStats) do
if not existId[id] then
savedStats[id] = nil
end
end
end
_G[savedVarsName][fullname] = savedOptions
if profileNum > 0 then
_G[savedVarsName][fullname]["talent"..profileNum] = currentSpecName
self:Debug("LoadModOptions: Finished loading "..(_G[savedVarsName][fullname]["talent"..profileNum] or L.UNKNOWN))
end
_G[savedStatsName] = savedStats
local optionsFrame = _G["DBM_GUI_OptionsFrame"]
if not first and DBM_GUI and DBM_GUI.currentViewing and optionsFrame:IsShown() then
optionsFrame:DisplayFrame(DBM_GUI.currentViewing)
end
end
function DBM:SpecChanged(force)
if not force and not DBM_UseDualProfile then return end
--Load Options again.
self:Debug("SpecChanged fired", 2)
for modId, _ in pairs(self.ModLists) do
self:LoadModOptions(modId)
end
end
function DBM:PLAYER_LEVEL_CHANGED()
playerLevel = UnitLevel("player")
if playerLevel < 15 and playerLevel > 9 then
self:PLAYER_SPECIALIZATION_CHANGED()
end
end
function DBM:LoadAllModDefaultOption(modId)
-- modId is string like "DBM-Highmaul"
if not modId or not self.ModLists[modId] then return end
-- prevent error
if not currentSpecID or not currentSpecGroup then
self:SetCurrentSpecInfo()
end
-- variable init
local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
local fullname = playerName.."-"..playerRealm
local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
-- prevent nil table error
if not _G[savedVarsName] then _G[savedVarsName] = {} end
for _, id in ipairs(self.ModLists[modId]) do
-- prevent nil table error
if not _G[savedVarsName][fullname][id] then _G[savedVarsName][fullname][id] = {} end
-- actual do load default option
local mod = self:GetModByName(id)
local defaultOptions = {}
for option, optionValue in pairs(mod.DefaultOptions) do
if type(optionValue) == "table" then
optionValue = optionValue.value
elseif type(optionValue) == "string" then
optionValue = mod:GetRoleFlagValue(optionValue)
end
defaultOptions[option] = optionValue
end
mod.Options = {}
mod.Options = defaultOptions
_G[savedVarsName][fullname][id][profileNum] = {}
_G[savedVarsName][fullname][id][profileNum] = mod.Options
end
self:AddMsg(L.ALLMOD_DEFAULT_LOADED)
-- update gui if showing
local optionsFrame = _G["DBM_GUI_OptionsFrame"]
if DBM_GUI and DBM_GUI.currentViewing and optionsFrame:IsShown() then
optionsFrame:DisplayFrame(DBM_GUI.currentViewing)
end
end
function DBM:LoadModDefaultOption(mod)
-- mod must be table
if not mod then return end
-- prevent error
if not currentSpecID or not currentSpecGroup then
self:SetCurrentSpecInfo()
end
-- variable init
local savedVarsName = (mod.modId):gsub("-", "").."_AllSavedVars"
local fullname = playerName.."-"..playerRealm
local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
-- prevent nil table error
if not _G[savedVarsName] then _G[savedVarsName] = {} end
if not _G[savedVarsName][fullname] then _G[savedVarsName][fullname] = {} end
if not _G[savedVarsName][fullname][mod.id] then _G[savedVarsName][fullname][mod.id] = {} end
-- do load default
local defaultOptions = {}
for option, optionValue in pairs(mod.DefaultOptions) do
if type(optionValue) == "table" then
optionValue = optionValue.value
elseif type(optionValue) == "string" then
optionValue = mod:GetRoleFlagValue(optionValue)
end
defaultOptions[option] = optionValue
end
mod.Options = {}
mod.Options = defaultOptions
_G[savedVarsName][fullname][mod.id][profileNum] = {}
_G[savedVarsName][fullname][mod.id][profileNum] = mod.Options
self:AddMsg(L.MOD_DEFAULT_LOADED)
-- update gui if showing
local optionsFrame = _G["DBM_GUI_OptionsFrame"]
if DBM_GUI and DBM_GUI.currentViewing and optionsFrame:IsShown() then
optionsFrame:DisplayFrame(DBM_GUI.currentViewing)
end
end
function DBM:CopyAllModOption(modId, sourceName, sourceProfile)
-- modId is string like "DBM-Highmaul"
if not modId or not sourceName or not sourceProfile or not DBM.ModLists[modId] then return end
-- prevent error
if not currentSpecID or not currentSpecGroup then
self:SetCurrentSpecInfo()
end
-- variable init
local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
local targetName = playerName.."-"..playerRealm
local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
-- do not copy setting itself
if targetName == sourceName and targetProfile == sourceProfile then
self:AddMsg(L.MPROFILE_COPY_SELF_ERROR)
return
end
-- prevent nil table error
if not _G[savedVarsName] then _G[savedVarsName] = {} end
-- check source is exist
if not _G[savedVarsName][sourceName] then
self:AddMsg(L.MPROFILE_COPY_S_ERROR)
return
end
for _, id in ipairs(self.ModLists[modId]) do
-- check source is exist
if not _G[savedVarsName][sourceName][id] then
self:AddMsg(L.MPROFILE_COPY_S_ERROR)
return
end
if not _G[savedVarsName][sourceName][id][sourceProfile] then
self:AddMsg(L.MPROFILE_COPY_S_ERROR)
return
end
-- prevent nil table error
if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
-- copy table
_G[savedVarsName][targetName][id][targetProfile] = {}--clear before copy
_G[savedVarsName][targetName][id][targetProfile] = _G[savedVarsName][sourceName][id][sourceProfile]
--check new option
local mod = self:GetModByName(id)
for option, optionValue in pairs(mod.Options) do
if _G[savedVarsName][targetName][id][targetProfile][option] == nil then
_G[savedVarsName][targetName][id][targetProfile][option] = optionValue
end
end
-- apply to options table
mod.Options = {}
mod.Options = _G[savedVarsName][targetName][id][targetProfile]
end
if targetProfile > 0 then
_G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
end
self:AddMsg(L.MPROFILE_COPY_SUCCESS:format(sourceName, sourceProfile))
-- update gui if showing
local optionsFrame = _G["DBM_GUI_OptionsFrame"]
if DBM_GUI and DBM_GUI.currentViewing and optionsFrame:IsShown() then
optionsFrame:DisplayFrame(DBM_GUI.currentViewing)
end
end
function DBM:CopyAllModTypeOption(modId, sourceName, sourceProfile, Type)
-- modId is string like "DBM-Highmaul"
if not modId or not sourceName or not sourceProfile or not self.ModLists[modId] or not Type then return end
-- prevent error
if not currentSpecID or not currentSpecGroup then
self:SetCurrentSpecInfo()
end
-- variable init
local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
local targetName = playerName.."-"..playerRealm
local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
-- do not copy setting itself
if targetName == sourceName and targetProfile == sourceProfile then
self:AddMsg(L.MPROFILE_COPYS_SELF_ERROR)
return
end
-- prevent nil table error
if not _G[savedVarsName] then _G[savedVarsName] = {} end
-- check source is exist
if not _G[savedVarsName][sourceName] then
self:AddMsg(L.MPROFILE_COPYS_S_ERROR)
return
end
for _, id in ipairs(self.ModLists[modId]) do
-- check source is exist
if not _G[savedVarsName][sourceName][id] then
self:AddMsg(L.MPROFILE_COPYS_S_ERROR)
return
end
if not _G[savedVarsName][sourceName][id][sourceProfile] then
self:AddMsg(L.MPROFILE_COPYS_S_ERROR)
return
end
-- prevent nil table error
if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
-- copy table
for option, optionValue in pairs(_G[savedVarsName][sourceName][id][sourceProfile]) do
if option:find(Type) then
_G[savedVarsName][targetName][id][targetProfile][option] = optionValue
end
end
-- apply to options table
local mod = self:GetModByName(id)
mod.Options = {}
mod.Options = _G[savedVarsName][targetName][id][targetProfile]
end
if targetProfile > 0 then
_G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
end
self:AddMsg(L.MPROFILE_COPYS_SUCCESS:format(sourceName, sourceProfile))
-- update gui if showing
local optionsFrame = _G["DBM_GUI_OptionsFrame"]
if DBM_GUI and DBM_GUI.currentViewing and optionsFrame:IsShown() then
optionsFrame:DisplayFrame(DBM_GUI.currentViewing)
end
end
function DBM:DeleteAllModOption(modId, name, profile)
-- modId is string like "DBM-Highmaul"
if not modId or not name or not profile or not self.ModLists[modId] then return end
-- prevent error
if not currentSpecID or not currentSpecGroup then
self:SetCurrentSpecInfo()
end
-- variable init
local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
local fullname = playerName.."-"..playerRealm
local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
-- cannot delete current profile.
if fullname == name and profileNum == profile then
self:AddMsg(L.MPROFILE_DELETE_SELF_ERROR)
return
end
-- prevent nil table error
if not _G[savedVarsName] then _G[savedVarsName] = {} end
if not _G[savedVarsName][name] then
self:AddMsg(L.MPROFILE_DELETE_S_ERROR)
return
end
for _, id in ipairs(self.ModLists[modId]) do
-- prevent nil table error
if not _G[savedVarsName][name][id] then
self:AddMsg(L.MPROFILE_DELETE_S_ERROR)
return
end
-- delete
_G[savedVarsName][name][id][profile] = nil
end
_G[savedVarsName][name]["talent"..profile] = nil
self:AddMsg(L.MPROFILE_DELETE_SUCCESS:format(name, profile))
end
function DBM:ClearAllStats(modId)
-- modId is string like "DBM-Highmaul"
if not modId or not self.ModLists[modId] then return end
-- variable init
local savedStatsName = modId:gsub("-", "").."_SavedStats"
-- prevent nil table error
if not _G[savedStatsName] then _G[savedStatsName] = {} end
for _, id in ipairs(self.ModLists[modId]) do
local mod = self:GetModByName(id)
-- prevent nil table error
local defaultStats = {}
defaultStats.normalKills = 0
defaultStats.normalPulls = 0
defaultStats.heroicKills = 0
defaultStats.heroicPulls = 0
defaultStats.challengeKills = 0
defaultStats.challengePulls = 0
defaultStats.challengeBestRank = 0
defaultStats.mythicKills = 0
defaultStats.mythicPulls = 0
defaultStats.normal25Kills = 0
defaultStats.normal25Kills = 0
defaultStats.normal25Pulls = 0
defaultStats.heroic25Kills = 0
defaultStats.heroic25Pulls = 0
defaultStats.lfr25Kills = 0
defaultStats.lfr25Pulls = 0
defaultStats.timewalkerKills = 0
defaultStats.timewalkerPulls = 0
mod.stats = {}
mod.stats = defaultStats
_G[savedStatsName][id] = {}
_G[savedStatsName][id] = defaultStats
end
self:AddMsg(L.ALLMOD_STATS_RESETED)
DBM_GUI:UpdateModList()
end
do
function loadOptions(self)
--init
if not DBM_AllSavedOptions then DBM_AllSavedOptions = {} end
usedProfile = DBM_UsedProfile or usedProfile
if not usedProfile or (usedProfile ~= "Default" and not DBM_AllSavedOptions[usedProfile]) then
-- DBM.Option is not loaded. so use print function
print(L.PROFILE_NOT_FOUND)
usedProfile = "Default"
end
DBM_UsedProfile = usedProfile
self.Options = DBM_AllSavedOptions[usedProfile] or {}
dbmIsEnabled = true
self:AddDefaultOptions(self.Options, self.DefaultOptions)
DBM_AllSavedOptions[usedProfile] = self.Options
-- force enable dual profile (change default)
if DBM_CharSavedRevision < 12976 then
if playerClass ~= "MAGE" and playerClass ~= "WARLOCK" and playerClass ~= "ROGUE" then
DBM_UseDualProfile = true
end
end
DBM_CharSavedRevision = self.Revision
-- load special warning options
self:UpdateWarningOptions()
self:UpdateSpecialWarningOptions()
self.Options.CoreSavedRevision = self.Revision
--Fix fonts if they are nil or set to any of standard font values
if not self.Options.WarningFont or (self.Options.WarningFont == "Fonts\\2002.TTF" or self.Options.WarningFont == "Fonts\\ARKai_T.ttf" or self.Options.WarningFont == "Fonts\\blei00d.TTF" or self.Options.WarningFont == "Fonts\\FRIZQT___CYR.TTF" or self.Options.WarningFont == "Fonts\\FRIZQT__.TTF") then
self.Options.WarningFont = "standardFont"
end
if not self.Options.SpecialWarningFont or (self.Options.SpecialWarningFont == "Fonts\\2002.TTF" or self.Options.SpecialWarningFont == "Fonts\\ARKai_T.ttf" or self.Options.SpecialWarningFont == "Fonts\\blei00d.TTF" or self.Options.SpecialWarningFont == "Fonts\\FRIZQT___CYR.TTF" or self.Options.SpecialWarningFont == "Fonts\\FRIZQT__.TTF") then
self.Options.SpecialWarningFont = "standardFont"
end
if WOW_PROJECT_ID ~= (WOW_PROJECT_MAINLINE or 1) then return end--Don't do sound migration in a situation user is loading wrong DBM version, to avoid sound path corruption
--Migrate user sound options to soundkit Ids if selected media doesn't exist in Interface\\AddOns
--This will in the short term, screw with people trying to use LibSharedMedia sound files on 8.1.5 until LSM has migrated as well.
local migrated = false
if type(self.Options.RaidWarningSound) == "string" and self.Options.RaidWarningSound ~= "" then
local searchMsg = self.Options.RaidWarningSound:lower()
if not searchMsg:find("addons") or searchMsg:find("classicsupport") then
self.Options.RaidWarningSound = self.DefaultOptions.RaidWarningSound
migrated = true
end
end
if type(self.Options.SpecialWarningSound) == "string" and self.Options.SpecialWarningSound ~= "" then
local searchMsg = self.Options.SpecialWarningSound:lower()
if not searchMsg:find("addons") or searchMsg:find("classicsupport") then
self.Options.SpecialWarningSound = self.DefaultOptions.SpecialWarningSound
migrated = true
end
end
if type(self.Options.SpecialWarningSound2) == "string" and self.Options.SpecialWarningSound2 ~= "" then
local searchMsg = self.Options.SpecialWarningSound2:lower()
if not searchMsg:find("addons") or searchMsg:find("classicsupport") then
self.Options.SpecialWarningSound2 = self.DefaultOptions.SpecialWarningSound2
migrated = true
end
end
if type(self.Options.SpecialWarningSound3) == "string" and self.Options.SpecialWarningSound3 ~= "" then
local searchMsg = self.Options.SpecialWarningSound3:lower()
if not searchMsg:find("addons") or searchMsg:find("classicsupport") then
self.Options.SpecialWarningSound3 = self.DefaultOptions.SpecialWarningSound3
migrated = true
end
end
if type(self.Options.SpecialWarningSound4) == "string" and self.Options.SpecialWarningSound4 ~= "" then
local searchMsg = self.Options.SpecialWarningSound4:lower()
if not searchMsg:find("addons") or searchMsg:find("classicsupport") then
self.Options.SpecialWarningSound4 = self.DefaultOptions.SpecialWarningSound4
migrated = true
end
end
if type(self.Options.SpecialWarningSound5) == "string" and self.Options.SpecialWarningSound5 ~= "" then
local searchMsg = self.Options.SpecialWarningSound5:lower()
if not searchMsg:find("addons") or searchMsg:find("classicsupport") then
self.Options.SpecialWarningSound5 = self.DefaultOptions.SpecialWarningSound5
migrated = true
end
end
if migrated then
self:AddMsg(L.SOUNDKIT_MIGRATION)
end
--TODO, why doesn't DBM core garbage collection options like sub mods do?
--If deleted unused option check code does get added though it needs excemption from anything comtaining text "RestoreSetting"
end
end
do
local lastLFGAlert = 0
function DBM:LFG_ROLE_CHECK_SHOW()
if not UnitIsGroupLeader("player") and self.Options.LFDEnhance and GetTime() - lastLFGAlert > 5 then
self:FlashClientIcon()
self:PlaySound(8960, true)--Because regular sound uses SFX channel which is too low of volume most of time
lastLFGAlert = GetTime()
end
end
end
function DBM:LFG_PROPOSAL_SHOW()
if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
DBT:CreateBar(40, L.LFG_INVITE, 237538)
fireEvent("DBM_TimerStart", "DBMLFGTimer", L.LFG_INVITE, 40, "237538", "extratimer", nil, 0)
end
if self.Options.LFDEnhance then
self:FlashClientIcon()
self:PlaySound(8960, true)--Because regular sound uses SFX channel which is too low of volume most of time
end
end
function DBM:LFG_PROPOSAL_FAILED()
DBT:CancelBar(L.LFG_INVITE)
fireEvent("DBM_TimerStop", "DBMLFGTimer")
end
function DBM:LFG_PROPOSAL_SUCCEEDED()
DBT:CancelBar(L.LFG_INVITE)
fireEvent("DBM_TimerStop", "DBMLFGTimer")
end
function DBM:READY_CHECK()
if self.Options.RLReadyCheckSound then--readycheck sound, if ora3 not installed (bad to have 2 mods do it)
self:FlashClientIcon()
if not BINDING_HEADER_oRA3 then
DBM:PlaySound(8960, true)--Because regular sound uses SFX channel which is too low of volume most of time
end
end
self:TransitionToDungeonBGM(false, true)
self:Schedule(4, self.TransitionToDungeonBGM, self)
end
function DBM:PLAYER_SPECIALIZATION_CHANGED()
local lastSpecID = currentSpecID
self:SetCurrentSpecInfo()
if currentSpecID ~= lastSpecID then--Don't fire specchanged unless spec actually has changed.
self:SpecChanged()
if IsInGroup() then
self:RoleCheck(false)
end
end
end
do
local function AcceptPartyInvite()
AcceptGroup()
for i=1, STATICPOPUP_NUMDIALOGS do
local whichDialog = _G["StaticPopup"..i].which
if whichDialog == "PARTY_INVITE" or whichDialog == "PARTY_INVITE_XREALM" then
_G["StaticPopup"..i].inviteAccepted = 1
StaticPopup_Hide(whichDialog)
break
end
end
end
function DBM:PARTY_INVITE_REQUEST(sender)
--First off, if you are in queue for something, lets not allow guildies or friends boot you from it.
if IsInInstance() or GetLFGMode(1) or GetLFGMode(2) or GetLFGMode(3) or GetLFGMode(4) or GetLFGMode(5) then return end
--Checks friends and guildies
if self.Options.AutoAcceptFriendInvite then
if checkForSafeSender(sender, self.Options.AutoAcceptFriendInvite, self.Options.AutoAcceptGuildInvite, nil, type(sender) == "number") then
AcceptPartyInvite()
end
end
end
end
function DBM:UPDATE_BATTLEFIELD_STATUS(queueID)
for i = 1, 2 do
if GetBattlefieldStatus(i) == "confirm" then
if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
queuedBattlefield[i] = select(2, GetBattlefieldStatus(i))
local expiration = GetBattlefieldPortExpiration(queueID)
local timerIcon = GetPlayerFactionGroup("player") == "Alliance" and 132486 or 132485
DBT:CreateBar(expiration or 85, queuedBattlefield[i], timerIcon)
self:FlashClientIcon()
fireEvent("DBM_TimerStart", "DBMBFSTimer", queuedBattlefield[i], expiration or 85, tostring(timerIcon), "extratimer", nil, 0)
end
if self.Options.LFDEnhance then
self:PlaySound(8960, true)--Because regular sound uses SFX channel which is too low of volume most of time
end
elseif queuedBattlefield[i] then
DBT:CancelBar(queuedBattlefield[i])
fireEvent("DBM_TimerStop", "DBMBFSTimer")
queuedBattlefield[i] = nil
end
end
end
function DBM:SCENARIO_COMPLETED()
if #inCombat > 0 and C_Scenario.IsInScenario() then
for i = #inCombat, 1, -1 do
local v = inCombat[i]
if v.inScenario then
self:EndCombat(v)
end
end
end
end
--------------------------------
-- Load Boss Mods on Demand --
--------------------------------
do
local modAdvertisementShown = false
local classicZones = {[509]=true,[531]=true,[469]=true,[409]=true}
local bcZones = {[564]=true,[534]=true,[532]=true,[565]=true,[544]=true,[548]=true,[580]=true,[550]=true}
local wrathZones = {[615]=true,[724]=true,[649]=true,[616]=true,[631]=true,[533]=true,[249]=true,[603]=true,[624]=true}
local cataZones = {[757]=true,[671]=true,[669]=true,[967]=true,[720]=true,[951]=true,[754]=true}
local mopZones = {[1009]=true,[1008]=true,[1136]=true,[996]=true,[1098]=true}
local wodZones = {[1205]=true,[1448]=true,[1228]=true}
local legionZones = {[1712]=true,[1520]=true,[1530]=true,[1676]=true,[1648]=true}
local bfaZones = {[1861]=true,[2070]=true,[2096]=true,[2164]=true,[2217]=true}
local challengeScenarios = {[1148]=true,[1698]=true,[1710]=true,[1703]=true,[1702]=true,[1684]=true,[1673]=true,[1616]=true,[2215]=true}
local pvpZones = {[30]=true,[489]=true,[529]=true,[559]=true,[562]=true,[566]=true,[572]=true,[617]=true,[618]=true,[628]=true,[726]=true,[727]=true,[761]=true,[968]=true,[980]=true,[998]=true,[1105]=true,[1134]=true,[1681]=true,[1803]=true,[2107]=true,[2118]=true,[2177]=true,[2197]=true}
local oldDungeons = {
[48]=true,[230]=true,[429]=true,[389]=true,[34]=true,--Classic
[540]=true,[558]=true,[556]=true,[555]=true,[542]=true,[546]=true,[545]=true,[547]=true,[553]=true,[554]=true,[552]=true,[557]=true,[269]=true,[560]=true,[543]=true,[585]=true,--BC
[619]=true,[601]=true,[595]=true,[600]=true,[604]=true,[602]=true,[599]=true,[576]=true,[578]=true,[574]=true,[575]=true,[608]=true,[658]=true,[632]=true,[668]=true,[650]=true,--Wrath
[755]=true,[645]=true,[36]=true,[670]=true,[644]=true,[33]=true,[643]=true,[725]=true,[657]=true,[309]=true,[859]=true,[568]=true,[938]=true,[940]=true,[939]=true,[646]=true,--Cata
[960]=true,[961]=true,[959]=true,[962]=true,[994]=true,[1011]=true,[1007]=true,[1001]=true,[1004]=true,--MoP
[1182]=true,[1175]=true,[1208]=true,[1195]=true,[1279]=true,[1176]=true,[1209]=true,[1358]=true,--WoD
[1501]=true,[1466]=true,[1456]=true,[1477]=true,[1458]=true,[1516]=true,[1571]=true,[1492]=true,[1544]=true,[1493]=true,[1651]=true,[1677]=true,[1753]=true--Legion
--[1763]=true,[1754]=true,[1762]=true,[1864]=true,[1822]=true,[1877]=true,[1594]=true,[1841]=true,[1771]=true,[1862]=true,[2097]=true--BfA Dungeons
}
--This never wants to spam you to use mods for trivial content you don't need mods for.
--It's intended to suggest mods for content that's relevant to your level (TW, leveling up in dungeons, or even older raids you can't just shit on)
function DBM:CheckAvailableMods()
if _G["BigWigs"] or modAdvertisementShown then return end--If they are running two boss mods at once, lets assume they are only using DBM for a specific feature (such as brawlers) and not nag
local timeWalking = C_PlayerInfo.IsPlayerInChromieTime() and true or difficultyIndex == 24 or difficultyIndex == 33 or false
if oldDungeons[LastInstanceMapID] and (timeWalking or playerLevel < 50) and not GetAddOnInfo("DBM-Party-BC") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM Old Dungeon mods"))
modAdvertisementShown = true
elseif (classicZones[LastInstanceMapID] or bcZones[LastInstanceMapID]) and (timeWalking or playerLevel < 31) and not GetAddOnInfo("DBM-BlackTemple") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM BC/Vanilla mods"))
modAdvertisementShown = true
elseif wrathZones[LastInstanceMapID] and (timeWalking or playerLevel < 31) and not GetAddOnInfo("DBM-Ulduar") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM Wrath of the Lich King mods"))
modAdvertisementShown = true
elseif cataZones[LastInstanceMapID] and (timeWalking or playerLevel < 36) and not GetAddOnInfo("DBM-Firelands") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM Cataclysm mods"))
modAdvertisementShown = true
elseif mopZones[LastInstanceMapID] and (timeWalking or playerLevel < 36) and not GetAddOnInfo("DBM-SiegeOfOrgrimmarV2") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM Mists of Pandaria mods"))
modAdvertisementShown = true
elseif wodZones[LastInstanceMapID] and (timeWalking or playerLevel < 41) and not GetAddOnInfo("DBM-HellfireCitadel") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM Warlords of Draenor mods"))
modAdvertisementShown = true
elseif legionZones[LastInstanceMapID] and (timeWalking or playerLevel < 51) and not GetAddOnInfo("DBM-AntorusBurningThrone") then--Technically 45 level with quish, but because of tuning you need need mods even at 50
AddMsg(self, L.MOD_AVAILABLE:format("DBM Legion mods"))
modAdvertisementShown = true
elseif bfaZones[LastInstanceMapID] and (timeWalking or playerLevel < 61) and not GetAddOnInfo("DBM-Nyalotha") then--Technically 50, but tuning and huge loss of player power, zones are even HARDER at 60
AddMsg(self, L.MOD_AVAILABLE:format("DBM Battle for Azeroth mods"))
modAdvertisementShown = true
elseif challengeScenarios[LastInstanceMapID] and not GetAddOnInfo("DBM-Challenges") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM-Challenges"))
modAdvertisementShown = true
elseif pvpZones[LastInstanceMapID] and not GetAddOnInfo("DBM-PvP") then
AddMsg(self, L.MOD_AVAILABLE:format("DBM-PvP"))
modAdvertisementShown = true
end
end
function DBM:TransitionToDungeonBGM(force, cleanup)
if cleanup then--Runs on zone change/cinematic Start (first load delay) and combat end
self:Unschedule(self.TransitionToDungeonBGM)
if self.Options.RestoreSettingMusic then
SetCVar("Sound_EnableMusic", self.Options.RestoreSettingMusic)
self.Options.RestoreSettingMusic = nil
self:Debug("Restoring Sound_EnableMusic CVAR")
end
if self.Options.musicPlaying then--Primarily so DBM doesn't call StopMusic unless DBM is one that started it. We don't want to screw with other addons
StopMusic()
self.Options.musicPlaying = nil
self:Debug("Stopping music")
end
fireEvent("DBM_MusicStop", "ZoneOrCombatEndTransition")
return
end
if LastInstanceType ~= "raid" and LastInstanceType ~= "party" and not force then return end
fireEvent("DBM_MusicStart", "RaidOrDungeon")
if self.Options.EventSoundDungeonBGM and self.Options.EventSoundDungeonBGM ~= "None" and self.Options.EventSoundDungeonBGM ~= "" and not (self.Options.EventDungMusicMythicFilter and (savedDifficulty == "mythic" or savedDifficulty == "challenge")) then
if not self.Options.RestoreSettingMusic then
self.Options.RestoreSettingMusic = tonumber(GetCVar("Sound_EnableMusic")) or 1
if self.Options.RestoreSettingMusic == 0 then
SetCVar("Sound_EnableMusic", 1)
else
self.Options.RestoreSettingMusic = nil--Don't actually need it
end
end
local path = "MISSING"
if self.Options.EventSoundDungeonBGM == "Random" then
local usedTable = self.Options.EventSoundMusicCombined and DBM.Music or DBM.DungeonMusic
if #usedTable >= 3 then
local random = fastrandom(3, #usedTable)
path = usedTable[random].value
end
else
path = self.Options.EventSoundDungeonBGM
end
if path ~= "MISSING" then
PlayMusic(path)
self.Options.musicPlaying = true
self:Debug("Starting Dungeon music with file: "..path)
end
end
end
local function SecondaryLoadCheck(self)
local _, instanceType, difficulty, _, _, _, _, mapID, instanceGroupSize = GetInstanceInfo()
local currentDifficulty, currentDifficultyText, _, _, keystoneLevel = self:GetCurrentInstanceDifficulty()
if currentDifficulty ~= savedDifficulty then
savedDifficulty, difficultyText, difficultyModifier = currentDifficulty, currentDifficultyText, keystoneLevel
end
self:Debug("Instance Check fired with mapID "..mapID.." and difficulty "..difficulty, 2)
-- Auto Logging for entire zone if record only bosses is off
-- This Bypasses Same ID check because we still need to recheck this on keystone difficulty check
if not self.Options.RecordOnlyBosses then
if LastInstanceType == "raid" or LastInstanceType == "party" then
self:StartLogging(0, nil)
else
self:StopLogging()
end
end
--These can still change even if mapID doesn't
difficultyIndex = difficulty
LastGroupSize = instanceGroupSize
if LastInstanceMapID == mapID then
self:TransitionToDungeonBGM()
self:Debug("No action taken because mapID hasn't changed since last check", 2)
return
end--ID hasn't changed, don't waste cpu doing anything else (example situation, porting into garrosh stage 4 is a loading screen)
LastInstanceMapID = mapID
if instanceType == "none" or C_Garrison:IsOnGarrisonMap() then
LastInstanceType = "none"
if not targetEventsRegistered then
self:RegisterShortTermEvents("UPDATE_MOUSEOVER_UNIT")
targetEventsRegistered = true
end
else
LastInstanceType = instanceType
if targetEventsRegistered then
self:UnregisterShortTermEvents()
targetEventsRegistered = false
end
if savedDifficulty == "worldboss" then
for i = #inCombat, 1, -1 do
self:EndCombat(inCombat[i], true)
end
end
end
-- LoadMod
self:LoadModsOnDemand("mapId", mapID)
if self.Options.ShowReminders then
self:CheckAvailableMods()
end
if self:HasMapRestrictions() then
self.Arrow:Hide()
self.HudMap:Disable()
if self.RangeCheck:IsRadarShown() then
self.RangeCheck:Hide(true)
end
end
end
--Faster and more accurate loading for instances, but useless outside of them
function DBM:LOADING_SCREEN_DISABLED()
DBT:CancelBar(L.LFG_INVITE)--Disable bar here since LFG_PROPOSAL_SUCCEEDED seems broken right now
fireEvent("DBM_TimerStop", "DBMLFGTimer")
timerRequestInProgress = false
self:Debug("LOADING_SCREEN_DISABLED fired")
self:Unschedule(SecondaryLoadCheck)
--SecondaryLoadCheck(self)
self:Schedule(1, SecondaryLoadCheck, self)--Now delayed by one second to work around an issue on 8.x where spec info isn't available yet on reloadui
self:TransitionToDungeonBGM(false, true)
self:Schedule(5, SecondaryLoadCheck, self)
if self:HasMapRestrictions() then
self.Arrow:Hide()
self.HudMap:Disable()
if self.RangeCheck:IsRadarShown() then
self.RangeCheck:Hide(true)
end
end
end
function DBM:CHALLENGE_MODE_RESET()
if not self.Options.RecordOnlyBosses then
self:StartLogging(0, nil, true)
end
end
function DBM:LOADING_SCREEN_ENABLED()
--TimerTracker Cleanup, required to work around logic code blizzard put into TimerTracker for /countdown timers
--TimerTracker is hard coded that if a type 3 timer exists, to give it prio over type 1 and type 2. This causes the M+ timer not to show, even if only like 0.01 sec was left on the /countdown
--We want to avoid situations where players start a 10 second timer, but click keystone with fractions of a second left, preventing them from seeing the M+ timer
if not DBM.Options.DontShowPTCountdownText then
for _, tttimer in pairs(TimerTracker.timerList) do
if tttimer.type == 3 and not tttimer.isFree then
FreeTimerTrackerTimer(tttimer)
break
end
end
end
end
function DBM:LoadModsOnDemand(checkTable, checkValue)
self:Debug("LoadModsOnDemand fired")
for _, v in ipairs(self.AddOns) do
local modTable = v[checkTable]
local enabled = GetAddOnEnableState(playerName, v.modId)
--self:Debug(v.modId.." is "..enabled, 2)
if not IsAddOnLoaded(v.modId) and modTable and checkEntry(modTable, checkValue) then
if enabled ~= 0 then
self:LoadMod(v)
else
if self.Options.ShowReminders then
self:AddMsg(L.LOAD_MOD_DISABLED:format(v.name))
end
end
end
end
self:ScenarioCheck()--Do not filter. Because ScenarioCheck function includes filter.
end
end
--Scenario mods
function DBM:ScenarioCheck()
if dbmIsEnabled and combatInfo[LastInstanceMapID] then
for _, v in ipairs(combatInfo[LastInstanceMapID]) do
if (v.type == "scenario") and checkEntry(v.msgs, LastInstanceMapID) then
self:StartCombat(v.mod, 0, "LOADING_SCREEN_DISABLED")
end
end
end
end
function DBM:LoadMod(mod, force)
if type(mod) ~= "table" then
self:Debug("LoadMod failed because mod table not valid")
return false
end
--Block loading world boss mods by zoneID, except if it's a heroic warfront
if mod.isWorldBoss and not IsInInstance() and not force and difficultyIndex ~= 149 then
return
end
if mod.minRevision > self.Revision then
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
self:AddMsg(L.LOAD_MOD_VER_MISMATCH:format(mod.name))
end
return
end
if mod.minExpansion > GetExpansionLevel() then
self:AddMsg(L.LOAD_MOD_EXP_MISMATCH:format(mod.name))
return
elseif not testBuild and mod.minToc > wowTOC then
self:AddMsg(L.LOAD_MOD_TOC_MISMATCH:format(mod.name, mod.minToc))
return
end
if not currentSpecID then
self:SetCurrentSpecInfo()
end
if not difficultyIndex then -- prevent error in EJ_SetDifficulty if not yet set
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
end
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.
self:Debug("LoadAddOn should have fired for "..mod.name, 2)
local loaded, reason = LoadAddOn(mod.modId)
if not loaded then
if reason then
self:AddMsg(L.LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_"..reason or ""])))
else
self:Debug("LoadAddOn failed and did not give reason")
end
return false
else
self:Debug("LoadAddOn should have succeeded for "..mod.name, 2)
self:AddMsg(L.LOAD_MOD_SUCCESS:format(tostring(mod.name)))
if self.NewerVersion and showConstantReminder >= 1 then
AddMsg(self, L.UPDATEREMINDER_HEADER:format(self.NewerVersion, showRealDate(self.HighestRelease)))
end
self:LoadModOptions(mod.modId, InCombatLockdown(), true)
if DBM_GUI then
DBM_GUI:UpdateModList()
end
if LastInstanceType ~= "pvp" and #inCombat == 0 and IsInGroup() then--do timer recovery only mod load
if not timerRequestInProgress then
timerRequestInProgress = true
-- Request timer to 3 person to prevent failure.
self:Unschedule(self.RequestTimers)
self:Schedule(7, self.RequestTimers, self, 1)
self:Schedule(10, self.RequestTimers, self, 2)
self:Schedule(13, self.RequestTimers, self, 3)
C_TimerAfter(15, function() timerRequestInProgress = false end)
self:GROUP_ROSTER_UPDATE(true)
end
end
if not InCombatLockdown() and not UnitAffectingCombat("player") and not IsFalling() then--We loaded in combat but still need to avoid garbage collect in combat
collectgarbage("collect")
end
return true
end
end
do
local function loadModByUnit(uId)
if not uId then
uId = "mouseover"
else
uId = uId.."target"
end
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.
local guid = UnitGUID(uId)
if guid and DBM:IsCreatureGUID(guid) then
local cId = DBM:GetCIDFromGUID(guid)
for bosscId, addon in pairs(loadcIds) do
local enabled = GetAddOnEnableState(playerName, addon)
if cId and bosscId and cId == bosscId and not IsAddOnLoaded(addon) and enabled ~= 0 then
for _, v in ipairs(DBM.AddOns) do
if v.modId == addon then
DBM:LoadMod(v, true)
break
end
end
end
end
end
end
--Loading routeens hacks for world bosses based on target or mouseover.
function DBM:UPDATE_MOUSEOVER_UNIT()
loadModByUnit()
end
function DBM:UNIT_TARGET_UNFILTERED(uId)
if targetEventsRegistered then--Allow outdoor mod loading
loadModByUnit(uId)
end
--Debug options for seeing where BossUnitTargetScanner can be used.
local transcriptor = _G["Transcriptor"]
if (self.Options.DebugLevel > 2 or (transcriptor and transcriptor:IsLogging())) and uId:find("boss") then
local targetName = UnitName(uId.."target") or "nil"
self:Debug(uId.." changed targets to "..targetName)
end
--Active BossUnitTargetScanner
if targetMonitor[uId] and UnitExists(uId.."target") and UnitPlayerOrPetInRaid(uId.."target") then
self:Debug("targetMonitor for this unit exists, target exists", 2)
local modId, returnFunc = targetMonitor[uId].modid, targetMonitor[uId].returnFunc
self:Debug("targetMonitor: "..modId..", "..uId..", "..returnFunc, 2)
if not targetMonitor[uId].allowTank then
local tanking, status = UnitDetailedThreatSituation(uId, uId.."target")--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
if tanking or (status == 3) then
self:Debug("targetMonitor ending for unit without 'allowTank', ignoring target", 2)
return
end
end
local mod = self:GetModByName(modId)
self:Debug("targetMonitor success for this unit, a valid target for returnFunc", 2)
mod[returnFunc](mod, self:GetUnitFullName(uId.."target"), uId.."target", uId)--Return results to warning function with all variables.
targetMonitor[uId] = nil
end
end
end
-----------------------------
-- Handle Incoming Syncs --
-----------------------------
do
local function checkForActualPull()
if DBM.Options.RecordOnlyBosses and #inCombat == 0 then
DBM:StopLogging()
end
end
local syncHandlers = {}
local whisperSyncHandlers = {}
local guildSyncHandlers = {}
-- 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
-- M = Mod
-- C = Combat start
-- GC = Guild Combat Start
-- IS = Icon set info
-- K = Kill
-- H = Hi!
-- V = Incoming version information
-- U = User Timer
-- PT = Pull Timer (for sound effects, the timer itself is still sent as a normal timer)
-- RT = Request Timers
-- CI = Combat Info
-- TR = Timer Recovery
-- IR = Instance Info Request
-- IRE = Instance Info Requested Ended/Canceled
-- II = Instance Info
-- WBE = World Boss engage info
-- WBD = World Boss defeat info
-- DSW = Disable Send Whisper
-- NS = Note Share
syncHandlers["M"] = function(sender, mod, revision, event, ...)
mod = DBM:GetModByName(mod or "")
if mod and event and revision then
revision = tonumber(revision) or 0
mod:ReceiveSync(event, sender, revision, ...)
end
end
syncHandlers["NS"] = function(sender, modid, modvar, text, abilityName)
if sender == playerName then return end
if DBM.Options.BlockNoteShare or InCombatLockdown() or UnitAffectingCombat("player") or IsFalling() then return end--or DBM:GetRaidRank(sender) == 0
if IsInGroup(2) and IsInInstance() then return end
--^^You are in LFR, BG, or LFG. Block note syncs. They shouldn't be sendable, but in case someone edits DBM^^
local mod = DBM:GetModByName(modid or "")
local ability = abilityName or L.UNKNOWN
if mod and modvar and text and text ~= "" then
if DBM:AntiSpam(5, modvar) then--Don't allow calling same note more than once per 5 seconds
DBM:AddMsg(L.NOTE_SHARE_SUCCESS:format(sender, ability))
DBM:AddMsg(("|HDBM:noteshare:%s:%s:%s:%s:%s|h|cff3588ff[%s]|r|h"):format(modid, modvar, ability, text, sender, L.NOTE_SHARE_LINK))
--DBM:ShowNoteEditor(mod, modvar, ability, text, sender)
else
DBM:Debug(sender.." is attempting to send too many notes so notes are being throttled")
end
else
DBM:AddMsg(L.NOTE_SHARE_FAIL:format(sender, ability))
end
end
syncHandlers["C"] = function(sender, delay, mod, modRevision, startHp, dbmRevision, modHFRevision, event)
if not dbmIsEnabled or sender == playerName then return end
if LastInstanceType == "pvp" then return end
if LastInstanceType == "none" and (not UnitAffectingCombat("player") or #inCombat > 0) then--world boss
local senderuId = DBM:GetRaidUnitId(sender)
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.
local _, _, _, playerZone = UnitPosition("player")
local _, _, _, senderZone = UnitPosition(senderuId)
if playerZone ~= senderZone then return end--not same zone
local range = DBM.RangeCheck:GetDistance("player", senderuId)--Same zone, so check range
if not range or range > 120 then return end
end
if not cSyncSender[sender] then
cSyncSender[sender] = true
cSyncReceived = cSyncReceived + 1
if cSyncReceived > 2 then -- need at least 3 sync to combat start. (for security)
local lag = select(4, GetNetStats()) / 1000
delay = tonumber(delay or 0) or 0
mod = DBM:GetModByName(mod or "")
modRevision = tonumber(modRevision or 0) or 0
dbmRevision = tonumber(dbmRevision or 0) or 0
modHFRevision = tonumber(modHFRevision or 0) or 0
startHp = tonumber(startHp or -1) or -1
if dbmRevision < 10481 then return end
if mod and delay and (not mod.zones or mod.zones[LastInstanceMapID]) and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) then
DBM:StartCombat(mod, delay + lag, "SYNC from - "..sender, true, startHp, event)
if mod.revision < modHFRevision then--mod.revision because we want to compare to OUR revision not senders
--There is a newer RELEASE version of DBM out that has this mods fixes that we do not possess
if DBM.HighestRelease >= modHFRevision and DBM.ReleaseRevision < modHFRevision then
showConstantReminder = 2
if DBM:AntiSpam(3, "HOTFIX") then
AddMsg(DBM, L.UPDATEREMINDER_HOTFIX)
end
else--This mods fixes are in an alpha version
if DBM:AntiSpam(3, "HOTFIX") then
AddMsg(DBM, L.UPDATEREMINDER_HOTFIX_ALPHA)
end
end
end
end
end
end
end
syncHandlers["DSW"] = function(sender)
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
statusWhisperDisabled = true
DBM:Debug("Raid leader has disabled status whispers")
end
syncHandlers["DGP"] = function(sender)
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
statusGuildDisabled = true
DBM:Debug("Raid leader has disabled guild progress messages")
end
syncHandlers["IS"] = function(sender, guid, ver, optionName)
ver = tonumber(ver) or 0
if ver > (iconSetRevision[optionName] or 0) then--Save first synced version and person, ignore same version. refresh occurs only above version (fastest person)
iconSetRevision[optionName] = ver
iconSetPerson[optionName] = guid
end
if iconSetPerson[optionName] == UnitGUID("player") then--Check if that highest version was from ourself
canSetIcons[optionName] = true
else--Not from self, it means someone with a higher version than us probably sent it
canSetIcons[optionName] = false
end
local name = DBM:GetFullPlayerNameByGUID(iconSetPerson[optionName]) or L.UNKNOWN
DBM:Debug(name.." was elected icon setter for "..optionName, 2)
end
syncHandlers["K"] = function(sender, cId)
if select(2, IsInInstance()) == "pvp" or select(2, IsInInstance()) == "none" then return end
cId = tonumber(cId or "")
if cId then DBM:OnMobKill(cId, true) end
end
syncHandlers["EE"] = function(sender, eId, success, mod, modRevision)
if select(2, IsInInstance()) == "pvp" then return end
eId = tonumber(eId or "")
success = tonumber(success)
mod = DBM:GetModByName(mod or "")
modRevision = tonumber(modRevision or 0) or 0
if mod and eId and success and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) and not eeSyncSender[sender] then
eeSyncSender[sender] = true
eeSyncReceived = eeSyncReceived + 1
if eeSyncReceived > 2 then -- need at least 3 person to combat end. (for security)
DBM:EndCombat(mod, success == 0)
end
end
end
local dummyMod -- dummy mod for the pull timer
syncHandlers["PT"] = function(sender, timer, senderMapID, target)
if DBM.Options.DontShowUserTimers then return end
local LFGTankException = IsPartyLFG() and UnitGroupRolesAssigned(sender) == "TANK"
if (DBM:GetRaidRank(sender) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
return
end
--Abort if mapID filter is enabled and sender actually sent a mapID. if no mapID is sent, it's always passed through (IE BW pull timers)
if DBM.Options.DontShowPTNoID and senderMapID and tonumber(senderMapID) ~= LastInstanceMapID then return end
timer = tonumber(timer or 0)
--We want to permit 0 itself, but block anything negative number or anything between 0 and 3 or anything longer than minute
if timer > 60 or (timer > 0 and timer < 3) or timer < 0 then
return
end
if not dummyMod then
local threshold = DBM.Options.PTCountThreshold2
threshold = floor(threshold)
dummyMod = DBM:NewMod("PullTimerCountdownDummy")
DBM:GetModLocalization("PullTimerCountdownDummy"):SetGeneralLocalization{ name = L.MINIMAP_TOOLTIP_HEADER }
dummyMod.text = dummyMod:NewAnnounce("%s", 1, "132349")
dummyMod.geartext = dummyMod:NewSpecialWarning(" %s ", nil, nil, nil, 3)
dummyMod.timer = dummyMod:NewTimer(20, "%s", "132349", nil, nil, 0, nil, nil, DBM.Options.DontPlayPTCountdown and 0 or 1, threshold)
end
--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)
if not DBM.Options.DontShowPT2 then--and DBT:GetBar(L.TIMER_PULL)
dummyMod.timer:Stop()
end
local timerTrackerRunning = false
if not DBM.Options.DontShowPTCountdownText then
for _, tttimer in pairs(TimerTracker.timerList) do
if not tttimer.isFree then--Timer event running
if tttimer.type == 3 then--Its a pull timer event, this is one we cancel before starting a new pull timer
FreeTimerTrackerTimer(tttimer)
else--Verify that a TimerTracker event NOT started by DBM isn't running, if it is, prevent executing new TimerTracker events below
timerTrackerRunning = true
end
end
end
end
dummyMod.text:Cancel()
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)
DBM:FlashClientIcon()
if not DBM.Options.DontShowPT2 then
dummyMod.timer:Start(timer, L.TIMER_PULL)
end
if not DBM.Options.DontShowPTCountdownText then
if not timerTrackerRunning then--if a TimerTracker event is running not started by DBM, block creating one of our own (object gets buggy if it has 2+ events running)
--Start A TimerTracker timer using the new countdown type 3 type (ie what C_PartyInfo.DoCountdown triggers, but without sending it to entire group)
TimerTracker_OnEvent(TimerTracker, "START_TIMER", 3, timer, timer)
--Find the timer object DBM just created and hack our own changes into it.
for _, tttimer in pairs(TimerTracker.timerList) do
if tttimer.type == 3 and not tttimer.isFree then
--We don't want the PVP bar, we only want timer text
if timer > 10 then
--b.startNumbers:Play()
tttimer.bar:Hide()
end
break
end
end
end
end
if not DBM.Options.DontShowPTText then
if target then
dummyMod.text:Show(L.ANNOUNCE_PULL_TARGET:format(target, timer, sender))
dummyMod.text:Schedule(timer, L.ANNOUNCE_PULL_NOW_TARGET:format(target))
else
dummyMod.text:Show(L.ANNOUNCE_PULL:format(timer, sender))
dummyMod.text:Schedule(timer, L.ANNOUNCE_PULL_NOW)
end
end
if DBM.Options.RecordOnlyBosses then
DBM:StartLogging(timer, checkForActualPull)--Start logging here to catch pre pots.
end
if DBM.Options.CheckGear and not testBuild then
local bagilvl, equippedilvl = GetAverageItemLevel()
local difference = bagilvl - equippedilvl
local weapon = GetInventoryItemLink("player", 16)
local fishingPole = false
if weapon then
local _, _, _, _, _, _, type = GetItemInfo(weapon)
if type and type == L.GEAR_FISHING_POLE then
fishingPole = true
end
end
if IsInRaid() and difference >= 30 then
dummyMod.geartext:Show(L.GEAR_WARNING:format(floor(difference)))
elseif IsInRaid() and (not weapon or fishingPole) then
dummyMod.geartext:Show(L.GEAR_WARNING_WEAPON)
end
end
end
do
local dummyMod2 -- dummy mod for the break timer
function breakTimerStart(self, timer, sender)
if not dummyMod2 then
local threshold = DBM.Options.PTCountThreshold2
threshold = floor(threshold)
dummyMod2 = DBM:NewMod("BreakTimerCountdownDummy")
DBM:GetModLocalization("BreakTimerCountdownDummy"):SetGeneralLocalization{ name = L.MINIMAP_TOOLTIP_HEADER }
dummyMod2.text = dummyMod2:NewAnnounce("%s", 1, "237538")
dummyMod2.timer = dummyMod2:NewTimer(20, L.TIMER_BREAK, "237538", nil, nil, 0, nil, nil, DBM.Options.DontPlayPTCountdown and 0 or 1, threshold)
end
--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)
if not DBM.Options.DontShowPT2 then--and DBT:GetBar(L.TIMER_BREAK)
dummyMod2.timer:Stop()
end
dummyMod2.text:Cancel()
DBM.Options.RestoreSettingBreakTimer = nil
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)
self.Options.RestoreSettingBreakTimer = timer.."/"..time()
if not self.Options.DontShowPT2 then
dummyMod2.timer:Start(timer)
end
if not self.Options.DontShowPTText then
local hour, minute = GetGameTime()
minute = minute+(timer/60)
if minute >= 60 then
hour = hour + 1
minute = minute - 60
end
minute = floor(minute)
if minute < 10 then
minute = tostring(0 .. minute)
end
dummyMod2.text:Show(L.BREAK_START:format(strFromTime(timer).." ("..hour..":"..minute..")", sender))
if timer/60 > 10 then dummyMod2.text:Schedule(timer - 10*60, L.BREAK_MIN:format(10)) end
if timer/60 > 5 then dummyMod2.text:Schedule(timer - 5*60, L.BREAK_MIN:format(5)) end
if timer/60 > 2 then dummyMod2.text:Schedule(timer - 2*60, L.BREAK_MIN:format(2)) end
if timer/60 > 1 then dummyMod2.text:Schedule(timer - 1*60, L.BREAK_MIN:format(1)) end
dummyMod2.text:Schedule(timer, L.ANNOUNCE_BREAK_OVER:format(hour..":"..minute))
end
C_TimerAfter(timer, function() self.Options.RestoreSettingBreakTimer = nil end)
end
end
syncHandlers["BT"] = function(sender, timer)
if DBM.Options.DontShowUserTimers then return end
timer = tonumber(timer or 0)
if timer > 3600 then return end
if (DBM:GetRaidRank(sender) == 0 and IsInGroup()) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
return
end
breakTimerStart(DBM, timer, sender)
end
whisperSyncHandlers["BTR3"] = function(sender, timer)
if DBM.Options.DontShowUserTimers then return end
timer = tonumber(timer or 0)
if timer > 3600 then return end
DBM:Unschedule(DBM.RequestTimers)--IF we got BTR3 sync, then we know immediately RequestTimers was successful, so abort others
if #inCombat >= 1 then return end
if DBT:GetBar(L.TIMER_BREAK) then return end--Already recovered. Prevent duplicate recovery
breakTimerStart(DBM, timer, sender)
end
local function SendVersion(guild)
if guild then
local message = ("%s\t%s\t%s"):format(tostring(DBM.Revision), tostring(DBM.ReleaseRevision), DBM.DisplayVersion)
SendAddonMessage("D4", "GV\t" .. message, "GUILD")
return
end
if DBM.Options.FakeBWVersion then
SendAddonMessage("BigWigs", bwVersionResponseString:format(fakeBWVersion, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY")
return
end
--(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)
local VPVersion
local VoicePack = DBM.Options.ChosenVoicePack
if not voiceSessionDisabled and VoicePack ~= "None" and DBM.VoiceVersions[VoicePack] then
VPVersion = "/ VP"..VoicePack..": v"..DBM.VoiceVersions[VoicePack]
end
if VPVersion then
sendSync("V", ("%s\t%s\t%s\t%s\t%s\t%s"):format(tostring(DBM.Revision), tostring(DBM.ReleaseRevision), DBM.DisplayVersion, GetLocale(), tostring(not DBM.Options.DontSetIcons), VPVersion))
else
sendSync("V", ("%s\t%s\t%s\t%s\t%s"):format(tostring(DBM.Revision), tostring(DBM.ReleaseRevision), DBM.DisplayVersion, GetLocale(), tostring(not DBM.Options.DontSetIcons)))
end
end
local function HandleVersion(revision, version, displayVersion, sender)
if version > DBM.Revision then -- Update reminder
if #newerVersionPerson < 4 then
if not checkEntry(newerVersionPerson, sender) then
newerVersionPerson[#newerVersionPerson + 1] = sender
DBM:Debug("Newer version detected from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
end
if #newerVersionPerson == 2 and updateNotificationDisplayed < 2 then--Only requires 2 for update notification.
if DBM.HighestRelease < version then
DBM.HighestRelease = version--Increase HighestRelease
DBM.NewerVersion = displayVersion--Apply NewerVersion
--UGLY hack to get release version number instead of alpha one
if DBM.NewerVersion:find("alpha") then
local temp1, _ = string.split(" ", DBM.NewerVersion)--Strip down to just version, no alpha
if temp1 then
local temp3, temp4, temp5 = string.split(".", temp1)--Strip version down to 3 numbers
if temp3 and temp4 and temp5 and tonumber(temp5) then
temp5 = tonumber(temp5)
temp5 = temp5 - 1
temp5 = tostring(temp5)
DBM.NewerVersion = temp3.."."..temp4.."."..temp5
end
end
end
end
--Find min revision.
updateNotificationDisplayed = 2
AddMsg(DBM, L.UPDATEREMINDER_HEADER:match("([^\n]*)"))
AddMsg(DBM, L.UPDATEREMINDER_HEADER:match("\n(.*)"):format(displayVersion, showRealDate(version)))
showConstantReminder = 1
elseif #newerVersionPerson == 3 and raid[newerVersionPerson[1]] and raid[newerVersionPerson[2]] and raid[newerVersionPerson[3]] and updateNotificationDisplayed < 3 then--The following code requires at least THREE people to send that higher revision. That should be more than adaquate
--Disable if out of date and it's a major patch.
if not testBuild and dbmToc < wowTOC then
updateNotificationDisplayed = 3
AddMsg(DBM, L.UPDATEREMINDER_MAJORPATCH)
DBM:Disable(true)
--Disallow out of date to run during beta/ptr what so ever.
elseif testBuild then
updateNotificationDisplayed = 3
AddMsg(DBM, L.UPDATEREMINDER_DISABLE)
DBM:Disable(true)
end
end
end
end
end
-- TODO: is there a good reason that version information is broadcasted and not unicasted?
syncHandlers["H"] = function(sender)
DBM:Unschedule(SendVersion)--Throttle so we don't needlessly send tons of comms during initial raid invites
DBM:Schedule(3, SendVersion)--Send version if 3 seconds have past since last "Hi" sync
end
guildSyncHandlers["GH"] = function(sender)
if DBM.ReleaseRevision >= DBM.HighestRelease then--Do not send version to guild if it's not up to date, since this is only used for update notifcation
DBM:Unschedule(SendVersion, true)
--Throttle so we don't needlessly send tons of comms
--For every 50 players online, DBM has an increasingly lower chance of replying to a version check request. This is because only 3 people actually need to reply
--50 people or less, 100% chance anyone who saw request will reply
--100 people on, only 50% chance DBM users replies to request
--150 people on, only 33% chance a DBM user replies to request
--1000 people online, only 5% chance a DBM user replies to request
local _, online = GetNumGuildMembers()
local chances = (online or 1) / 50
if chances < 1 then chances = 1 end
if mrandom(1, chances) == 1 then
DBM:Schedule(5, SendVersion, true)--Send version if 5 seconds have past since last "Hi" sync
end
end
end
syncHandlers["BV"] = function(sender, version, hash)--Parsed from bigwigs V7+
if version and raid[sender] then
raid[sender].bwversion = version
raid[sender].bwhash = hash or ""
end
end
syncHandlers["V"] = function(sender, revision, version, displayVersion, locale, iconEnabled, VPVersion)
revision, version = tonumber(revision), tonumber(version)
if revision and version and displayVersion and raid[sender] then
raid[sender].revision = revision
raid[sender].version = version
raid[sender].displayVersion = displayVersion
raid[sender].VPVersion = VPVersion
raid[sender].locale = locale
raid[sender].enabledIcons = iconEnabled or "false"
DBM:Debug("Received version info from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
HandleVersion(revision, version, displayVersion, sender)
end
DBM:GROUP_ROSTER_UPDATE()
end
guildSyncHandlers["GV"] = function(sender, revision, version, displayVersion)
revision, version = tonumber(revision), tonumber(version)
if revision and version and displayVersion then
DBM:Debug("Received G version info from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
HandleVersion(revision, version, displayVersion, sender)
end
end
syncHandlers["U"] = function(sender, time, text)
if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
if DBM.Options.DontShowUserTimers then return end
if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end
if sender == playerName then return end
time = tonumber(time or 0)
text = tostring(text)
if time and text then
DBM:CreatePizzaTimer(time, text, nil, sender)
end
end
whisperSyncHandlers["UW"] = function(sender, time, text)
if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
if DBM.Options.DontShowUserTimers then return end
if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end--Block in LFR, or if not an assistant
if sender == playerName then return end
time = tonumber(time or 0)
text = tostring(text)
if time and text then
DBM:CreatePizzaTimer(time, text, nil, sender)
end
end
guildSyncHandlers["GCB"] = function(sender, modId, ver, difficulty, difficultyModifier, name)
if not DBM.Options.ShowGuildMessages or not difficulty then return end
if not ver or not (ver == "3") then return end--Ignore old versions
if DBM:AntiSpam(10, "GCB") then
if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
difficulty = tonumber(difficulty)
if not DBM.Options.ShowGuildMessagesPlus and difficulty == 8 then return end
modId = tonumber(modId)
local bossName = modId and EJ_GetEncounterInfo(modId) or name or L.UNKNOWN
local difficultyName
if difficulty == 8 then
if difficultyModifier and difficultyModifier ~= 0 then
difficultyName = PLAYER_DIFFICULTY6.."+ ("..difficultyModifier..")"
else
difficultyName = PLAYER_DIFFICULTY6.."+"
end
elseif difficulty == 16 then
difficultyName = PLAYER_DIFFICULTY6
elseif difficulty == 15 then
difficultyName = PLAYER_DIFFICULTY2
else
difficultyName = PLAYER_DIFFICULTY1
end
DBM:AddMsg(L.GUILD_COMBAT_STARTED:format(difficultyName.."-"..bossName))
end
end
guildSyncHandlers["GCE"] = function(sender, modId, ver, wipe, time, difficulty, difficultyModifier, name, wipeHP)
if not DBM.Options.ShowGuildMessages or not difficulty then return end
if not ver or not (ver == "6") then return end--Ignore old versions
if DBM:AntiSpam(5, "GCE") then
if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
difficulty = tonumber(difficulty)
if not DBM.Options.ShowGuildMessagesPlus and difficulty == 8 then return end
modId = tonumber(modId)
local bossName = modId and EJ_GetEncounterInfo(modId) or name or L.UNKNOWN
local difficultyName
if difficulty == 8 then
if difficultyModifier and difficultyModifier ~= 0 then
difficultyName = PLAYER_DIFFICULTY6.."+ ("..difficultyModifier..")"
else
difficultyName = PLAYER_DIFFICULTY6.."+"
end
elseif difficulty == 16 then
difficultyName = PLAYER_DIFFICULTY6
elseif difficulty == 15 then
difficultyName = PLAYER_DIFFICULTY2
else
difficultyName = PLAYER_DIFFICULTY1
end
if wipe == "1" then
DBM:AddMsg(L.GUILD_COMBAT_ENDED_AT:format(difficultyName.."-"..bossName, wipeHP, time))
else
DBM:AddMsg(L.GUILD_BOSS_DOWN:format(difficultyName.."-"..bossName, time))
end
end
end
guildSyncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
if not ver or not (ver == "8") then return end--Ignore old versions
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.
lastBossEngage[modId..realm] = GetTime()
if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
modId = tonumber(modId)--If it fails to convert into number, this makes it nil
local bossName = modId and EJ_GetEncounterInfo(modId) or name or L.UNKNOWN
DBM:AddMsg(L.WORLDBOSS_ENGAGED:format(bossName, floor(health), sender))
end
end
guildSyncHandlers["WBD"] = function(sender, modId, realm, ver, name)
if not ver or not (ver == "8") then return end--Ignore old versions
if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
lastBossDefeat[modId..realm] = GetTime()
if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
modId = tonumber(modId)--If it fails to convert into number, this makes it nil
local bossName = modId and EJ_GetEncounterInfo(modId) or name or L.UNKNOWN
DBM:AddMsg(L.WORLDBOSS_DEFEATED:format(bossName, sender))
end
end
whisperSyncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
if not ver or not (ver == "8") then return end--Ignore old versions
if lastBossEngage[modId..realm] and (GetTime() - lastBossEngage[modId..realm] < 30) then return end
lastBossEngage[modId..realm] = GetTime()
if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
local gameAccountInfo = C_BattleNet.GetGameAccountInfoByID(sender)
local toonName = gameAccountInfo and gameAccountInfo.characterName or L.UNKNOWN
modId = tonumber(modId)--If it fails to convert into number, this makes it nil
local bossName = modId and EJ_GetEncounterInfo(modId) or name or L.UNKNOWN
DBM:AddMsg(L.WORLDBOSS_ENGAGED:format(bossName, floor(health), toonName))
end
end
whisperSyncHandlers["WBD"] = function(sender, modId, realm, ver, name)
if not ver or not (ver == "8") then return end--Ignore old versions
if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
lastBossDefeat[modId..realm] = GetTime()
if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
local gameAccountInfo = C_BattleNet.GetGameAccountInfoByID(sender)
local toonName = gameAccountInfo and gameAccountInfo.characterName or L.UNKNOWN
modId = tonumber(modId)--If it fails to convert into number, this makes it nil
local bossName = modId and EJ_GetEncounterInfo(modId) or name or L.UNKNOWN
DBM:AddMsg(L.WORLDBOSS_DEFEATED:format(bossName, toonName))
end
end
whisperSyncHandlers["RT"] = function(sender)
if UnitInBattleground("player") then
DBM:SendPVPTimers(sender)
else
DBM:SendTimers(sender)
end
end
whisperSyncHandlers["CI"] = function(sender, mod, time)
mod = DBM:GetModByName(mod or "")
time = tonumber(time or 0)
if mod and time then
DBM:ReceiveCombatInfo(sender, mod, time)
end
end
whisperSyncHandlers["TR"] = function(sender, mod, timeLeft, totalTime, id, paused, ...)
mod = DBM:GetModByName(mod or "")
timeLeft = tonumber(timeLeft or 0)
totalTime = tonumber(totalTime or 0)
if mod and timeLeft and timeLeft > 0 and totalTime and totalTime > 0 and id then
DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, paused and paused == "1" and true or false, ...)
end
end
whisperSyncHandlers["VI"] = function(sender, mod, name, value)
mod = DBM:GetModByName(mod or "")
value = tonumber(value) or value
if mod and name and value then
DBM:ReceiveVariableInfo(sender, mod, name, value)
end
end
handleSync = function(channel, sender, prefix, ...)
if not prefix then
return
end
local handler
--Can only be from a friend
if channel == "BN_WHISPER" then
handler = whisperSyncHandlers[prefix]
--Whisper syncs sent from non friends are automatically rejected if not from a friend or someone in your group
elseif channel == "WHISPER" and sender ~= playerName then -- separate between broadcast and unicast, broadcast must not be sent as unicast or vice-versa
if (checkForSafeSender(sender, true) or DBM:GetRaidUnitId(sender)) then--Sender passes safety check, or is in group
handler = whisperSyncHandlers[prefix]
end
elseif channel == "GUILD" then
handler = guildSyncHandlers[prefix]
else
handler = syncHandlers[prefix]
end
if handler then
return handler(sender, ...)
end
end
function DBM:CHAT_MSG_ADDON(prefix, msg, channel, sender)
if prefix == "D4" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT" or channel == "WHISPER" or channel == "GUILD") then
sender = Ambiguate(sender, "none")
handleSync(channel, sender, strsplit("\t", msg))
elseif prefix == "BigWigs" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT") then
local bwPrefix, bwMsg, extra = strsplit("^", msg)
if bwPrefix and bwMsg then
if bwPrefix == "V" and extra then--Nil check "extra" to avoid error from older version
local verString, hash = bwMsg, extra
local version = tonumber(verString) or 0
if version == 0 then return end--Just a query
sender = Ambiguate(sender, "none")
handleSync(channel, sender, "BV", version, hash)--Prefix changed, so it's not handled by DBMs "V" handler
if version > fakeBWVersion then--Newer revision found, upgrade!
fakeBWVersion = version
fakeBWHash = hash
end
elseif bwPrefix == "Q" then--Version request prefix
self:Unschedule(SendVersion)
self:Schedule(3, SendVersion)
elseif bwPrefix == "B" then--Boss Mod Sync
for i = 1, #inCombat do
local mod = inCombat[i]
if mod and mod.OnBWSync then
mod:OnBWSync(bwMsg, extra, sender)
end
end
for i = 1, #oocBWComms do
local mod = oocBWComms[i]
if mod and mod.OnBWSync then
mod:OnBWSync(bwMsg, extra, sender)
end
end
end
end
elseif prefix == "Transcriptor" and msg then
for i = 1, #inCombat do
local mod = inCombat[i]
if mod and mod.OnTranscriptorSync then
mod:OnTranscriptorSync(msg, sender)
end
end
local transcriptor = _G["Transcriptor"]
if msg:find("spell:") and (DBM.Options.DebugLevel > 2 or (transcriptor and transcriptor:IsLogging())) then
local spellId = string.match(msg, "spell:(%d+)") or L.UNKNOWN
local spellName = string.match(msg, "h%[(.-)%]|h") or L.UNKNOWN
local message = "RAID_BOSS_WHISPER on "..sender.." with spell of "..spellName.." ("..spellId..")"
self:Debug(message)
end
end
end
DBM.CHAT_MSG_ADDON_LOGGED = DBM.CHAT_MSG_ADDON
function DBM:BN_CHAT_MSG_ADDON(prefix, msg, channel, sender)
if prefix == "D4" and msg then
handleSync("BN_WHISPER", sender, strsplit("\t", msg))
end
end
end
-----------------------
-- Update Reminder --
-----------------------
do
local frame, fontstring, fontstringFooter, editBox, urlText
local function createFrame()
frame = CreateFrame("Frame", "DBMUpdateReminder", UIParent, DBM:IsShadowlands() and "BackdropTemplate")
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...)
frame:SetWidth(430)
frame:SetHeight(140)
frame:SetPoint("TOP", 0, -230)
frame.backdropInfo = {
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", -- 131071
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", -- 131072
tile = true,
tileSize = 32,
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 },
}
if not DBM:IsShadowlands() then
frame:SetBackdrop(frame.backdropInfo)
else
frame:ApplyBackdrop()
end
fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
fontstring:SetWidth(410)
fontstring:SetHeight(0)
fontstring:SetPoint("TOP", 0, -16)
editBox = CreateFrame("EditBox", nil, frame)
do
local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
editBoxLeft:SetTexture(130959)--"Interface\\ChatFrame\\UI-ChatInputBorder-Left"
editBoxLeft:SetHeight(32)
editBoxLeft:SetWidth(32)
editBoxLeft:SetPoint("LEFT", -14, 0)
editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
editBoxRight:SetTexture(130960)--"Interface\\ChatFrame\\UI-ChatInputBorder-Right"
editBoxRight:SetHeight(32)
editBoxRight:SetWidth(32)
editBoxRight:SetPoint("RIGHT", 6, 0)
editBoxRight:SetTexCoord(0.875, 1, 0, 1)
editBoxMiddle:SetTexture(130960)--"Interface\\ChatFrame\\UI-ChatInputBorder-Right"
editBoxMiddle:SetHeight(32)
editBoxMiddle:SetWidth(1)
editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
end
editBox:SetHeight(32)
editBox:SetWidth(250)
editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
editBox:SetFontObject("GameFontHighlight")
editBox:SetTextInsets(0, 0, 0, 1)
editBox:SetFocus()
editBox:SetText(urlText)
editBox:HighlightText()
editBox:SetScript("OnTextChanged", function(self)
editBox:SetText(urlText)
editBox:HighlightText()
end)
fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
fontstringFooter:SetWidth(410)
fontstringFooter:SetHeight(0)
fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
local button = CreateFrame("Button", nil, frame)
button:SetHeight(24)
button:SetWidth(75)
button:SetPoint("BOTTOM", 0, 13)
button:SetNormalFontObject("GameFontNormal")
button:SetHighlightFontObject("GameFontHighlight")
button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
button:SetText(OKAY)
button:SetScript("OnClick", function(self)
frame:Hide()
end)
end
function DBM:ShowUpdateReminder(newVersion, newRevision, text, url)
urlText = url or "https://github.com/DeadlyBossMods/DeadlyBossMods/wiki"
if not frame then
createFrame()
else
editBox:SetText(urlText)
editBox:HighlightText()
end
frame:Show()
if newVersion then
fontstring:SetText(L.UPDATEREMINDER_HEADER:format(newVersion, newRevision))
fontstringFooter:SetText(L.UPDATEREMINDER_FOOTER)
elseif text then
fontstring:SetText(text)
fontstringFooter:SetText(L.UPDATEREMINDER_FOOTER_GENERIC)
end
end
end
--------------------
-- Notes Editor --
--------------------
do
local frame, fontstring, fontstringFooter, editBox, button3
local function createFrame()
frame = CreateFrame("Frame", "DBMNotesEditor", UIParent, DBM:IsShadowlands() and "BackdropTemplate")
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...)
frame:SetWidth(430)
frame:SetHeight(140)
frame:SetPoint("TOP", 0, -230)
frame.backdropInfo = {
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", -- 131071
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", -- 131072
tile = true,
tileSize = 32,
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
}
if not DBM:IsShadowlands() then
frame:SetBackdrop(frame.backdropInfo)
else
frame:ApplyBackdrop()
end
fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
fontstring:SetWidth(410)
fontstring:SetHeight(0)
fontstring:SetPoint("TOP", 0, -16)
editBox = CreateFrame("EditBox", nil, frame)
do
local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
editBoxLeft:SetTexture(130959)--"Interface\\ChatFrame\\UI-ChatInputBorder-Left"
editBoxLeft:SetHeight(32)
editBoxLeft:SetWidth(32)
editBoxLeft:SetPoint("LEFT", -14, 0)
editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
editBoxRight:SetTexture(130960)--"Interface\\ChatFrame\\UI-ChatInputBorder-Right"
editBoxRight:SetHeight(32)
editBoxRight:SetWidth(32)
editBoxRight:SetPoint("RIGHT", 6, 0)
editBoxRight:SetTexCoord(0.875, 1, 0, 1)
editBoxMiddle:SetTexture(130960)--"Interface\\ChatFrame\\UI-ChatInputBorder-Right"
editBoxMiddle:SetHeight(32)
editBoxMiddle:SetWidth(1)
editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
end
editBox:SetHeight(32)
editBox:SetWidth(250)
editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
editBox:SetFontObject("GameFontHighlight")
editBox:SetTextInsets(0, 0, 0, 1)
editBox:SetFocus()
editBox:SetText("")
fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
fontstringFooter:SetWidth(410)
fontstringFooter:SetHeight(0)
fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
local button = CreateFrame("Button", nil, frame)
button:SetHeight(24)
button:SetWidth(75)
button:SetPoint("BOTTOM", 80, 13)
button:SetNormalFontObject("GameFontNormal")
button:SetHighlightFontObject("GameFontHighlight")
button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
button:SetText(OKAY)
button:SetScript("OnClick", function(self)
local mod = DBM.Noteframe.mod
local modvar = DBM.Noteframe.modvar
mod.Options[modvar .. "SWNote"] = editBox:GetText() or ""
DBM.Noteframe.mod = nil
DBM.Noteframe.modvar = nil
DBM.Noteframe.abilityName = nil
frame:Hide()
end)
local button2 = CreateFrame("Button", nil, frame)
button2:SetHeight(24)
button2:SetWidth(75)
button2:SetPoint("BOTTOM", 0, 13)
button2:SetNormalFontObject("GameFontNormal")
button2:SetHighlightFontObject("GameFontHighlight")
button2:SetNormalTexture(button2:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
button2:SetPushedTexture(button2:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
button2:SetHighlightTexture(button2:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
button2:SetText(CANCEL)
button2:SetScript("OnClick", function(self)
DBM.Noteframe.mod = nil
DBM.Noteframe.modvar = nil
DBM.Noteframe.abilityName = nil
frame:Hide()
end)
button3 = CreateFrame("Button", nil, frame)
button3:SetHeight(24)
button3:SetWidth(75)
button3:SetPoint("BOTTOM", -80, 13)
button3:SetNormalFontObject("GameFontNormal")
button3:SetHighlightFontObject("GameFontHighlight")
button3:SetNormalTexture(button3:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
button3:SetPushedTexture(button3:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
button3:SetHighlightTexture(button3:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
button3:SetText(SHARE_QUEST_ABBREV)
button3:SetScript("OnClick", function(self)
local modid = DBM.Noteframe.mod.id
local modvar = DBM.Noteframe.modvar
local abilityName = DBM.Noteframe.abilityName
local syncText = editBox:GetText() or ""
if syncText == "" then
DBM:AddMsg(L.NOTESHAREERRORBLANK)
elseif IsInGroup(2) and IsInInstance() then--For BGs, LFR and LFG (we also check IsInInstance() so if you're in queue but fighting something outside like a world boss, it'll sync in "RAID" instead)
DBM:AddMsg(L.NOTESHAREERRORGROUPFINDER)
else
local msg = modid.."\t"..modvar.."\t"..syncText.."\t"..abilityName
if IsInRaid() then
--if DBM:GetRaidRank(playerName) == 0 then
-- DBM:AddMsg(L.ERROR_NO_PERMISSION)
--else
SendAddonMessage("D4", "NS\t" .. msg, "RAID")
DBM:AddMsg(L.NOTESHARED)
--end
elseif IsInGroup(1) then
--if DBM:GetRaidRank(playerName) == 0 then
-- DBM:AddMsg(L.ERROR_NO_PERMISSION)
--else
SendAddonMessage("D4", "NS\t" .. msg, "PARTY")
DBM:AddMsg(L.NOTESHARED)
--end
else--Solo
DBM:AddMsg(L.NOTESHAREERRORSOLO)
end
end
end)
end
function DBM:ShowNoteEditor(mod, modvar, abilityName, syncText, sender)
if not frame then
createFrame()
self.Noteframe = frame
else
if frame:IsShown() and syncText then
self:AddMsg(L.NOTESHAREERRORALREADYOPEN)
return
end
end
frame:Show()
fontstringFooter:SetText(L.NOTEFOOTER)
self.Noteframe.mod = mod
self.Noteframe.modvar = modvar
self.Noteframe.abilityName = abilityName
if syncText then
button3:Hide()--Don't show share button in shared notes
fontstring:SetText(L.NOTESHAREDHEADER:format(sender, abilityName))
editBox:SetText(syncText)
else
button3:Show()
fontstring:SetText(L.NOTEHEADER:format(abilityName))
if type(mod.Options[modvar .. "SWNote"]) == "string" then
editBox:SetText(mod.Options[modvar .. "SWNote"])
else
editBox:SetText("")
end
end
end
end
----------------------
-- Pull Detection --
----------------------
do
local targetList = {}
local function buildTargetList()
local uId = (IsInRaid() and "raid") or "party"
for i = 0, GetNumGroupMembers() do
local id = (i == 0 and "target") or uId..i.."target"
local guid = UnitGUID(id)
if guid and DBM:IsCreatureGUID(guid) then
local cId = DBM:GetCIDFromGUID(guid)
targetList[cId] = id
end
end
end
local function clearTargetList()
twipe(targetList)
end
local function scanForCombat(mod, mob, delay)
if not checkEntry(inCombat, mob) then
buildTargetList()
if targetList[mob] then
if mod.noFriendlyEngagement and UnitIsFriend("player", targetList[mob]) then return end
if delay > 0 and UnitAffectingCombat(targetList[mob]) and not (UnitPlayerOrPetInRaid(targetList[mob]) or UnitPlayerOrPetInParty(targetList[mob])) then
DBM:StartCombat(mod, delay, "PLAYER_REGEN_DISABLED")
elseif (delay == 0) then
DBM:StartCombat(mod, 0, "PLAYER_REGEN_DISABLED_AND_MESSAGE")
end
end
clearTargetList()
end
end
local function checkForPull(mob, combatInfo)
healthCombatInitialized = false
--This just can't be avoided, tryig to save cpu by using C_TimerAfter broke this
--This needs the redundancy and ability to pass args.
DBM:Schedule(0.5, scanForCombat, combatInfo.mod, mob, 0.5)
DBM:Schedule(2, scanForCombat, combatInfo.mod, mob, 2)
C_TimerAfter(2.1, function()
healthCombatInitialized = true
end)
end
-- TODO: fix the duplicate code that was added for quick & dirty support of zone IDs
-- 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
function DBM:PLAYER_REGEN_DISABLED()
lastCombatStarted = GetTime()
if not combatInitialized then return end
if dbmIsEnabled and combatInfo[LastInstanceMapID] then
for _, v in ipairs(combatInfo[LastInstanceMapID]) do
if v.type:find("combat") and not v.noRegenDetection then
if v.multiMobPullDetection then
for _, mob in ipairs(v.multiMobPullDetection) do
if checkForPull(mob, v) then
break
end
end
else
checkForPull(v.mob, v)
end
end
end
end
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.
self:FlashClientIcon()
local voice = DBM.Options.ChosenVoicePack
local path = 8585--"Sound\\Creature\\CThun\\CThunYouWillDIe.ogg"
if not voiceSessionDisabled and voice ~= "None" then
path = "Interface\\AddOns\\DBM-VP"..voice.."\\checkhp.ogg"
end
self:PlaySound(path)
if UnitHealthMax("player") ~= 0 then
local health = UnitHealth("player") / UnitHealthMax("player") * 100
self:AddMsg(L.AFK_WARNING:format(health))
end
end
end
function DBM:PLAYER_REGEN_ENABLED()
if delayedFunction then--Will throw error if not a function, purposely not doing and type(delayedFunction) == "function" for now to make sure code works though cause it always should be function
delayedFunction()
delayedFunction = nil
end
if watchFrameRestore then
--ObjectiveTrackerFrame:Show()
ObjectiveTracker_Expand()
watchFrameRestore = false
end
end
local function isBossEngaged(cId)
-- note that this is designed to work with any number of bosses, but it might be sufficient to check the first 5 unit ids
local i = 1
repeat
local bossUnitId = "boss"..i
local bossGUID = not UnitIsDead(bossUnitId) and UnitGUID(bossUnitId) -- check for UnitIsVisible maybe?
local bossCId = bossGUID and DBM:GetCIDFromGUID(bossGUID)
if bossCId and (type(cId) == "number" and cId == bossCId or type(cId) == "table" and checkEntry(cId, bossCId)) then
return true
end
i = i + 1
until not bossGUID
end
function DBM:INSTANCE_ENCOUNTER_ENGAGE_UNIT()
if timerRequestInProgress then return end--do not start ieeu combat if timer request is progressing. (not to break Timer Recovery stuff)
if dbmIsEnabled and combatInfo[LastInstanceMapID] then
self:Debug("INSTANCE_ENCOUNTER_ENGAGE_UNIT event fired for zoneId"..LastInstanceMapID, 3)
for _, v in ipairs(combatInfo[LastInstanceMapID]) do
if not v.noIEEUDetection then
if v.type:find("combat") and isBossEngaged(v.multiMobPullDetection or v.mob) then
self:StartCombat(v.mod, 0, "IEEU")
end
end
end
end
end
function DBM:UNIT_TARGETABLE_CHANGED(uId)
local transcriptor = _G["Transcriptor"]
if self.Options.DebugLevel > 2 or (transcriptor and transcriptor:IsLogging()) then
local active = UnitExists(uId) and "true" or "false"
self:Debug("UNIT_TARGETABLE_CHANGED event fired for "..UnitName(uId)..". Active: "..active)
end
end
function DBM:UNIT_SPELLCAST_SUCCEEDED(uId, _, spellId)
local spellName = self:GetSpellInfo(spellId)
self:Debug("UNIT_SPELLCAST_SUCCEEDED fired: "..UnitName(uId).."'s "..spellName.."("..spellId..")", 3)
end
function DBM:ENCOUNTER_START(encounterID, name, difficulty, size)
self:Debug("ENCOUNTER_START event fired: "..encounterID.." "..name.." "..difficulty.." "..size)
if dbmIsEnabled then
if self.Options.ShowReminders then
self:CheckAvailableMods()
end
if combatInfo[LastInstanceMapID] then
for _, v in ipairs(combatInfo[LastInstanceMapID]) do
if not v.noESDetection then
if v.multiEncounterPullDetection then
for _, eId in ipairs(v.multiEncounterPullDetection) do
if encounterID == eId then
self:StartCombat(v.mod, 0, "ENCOUNTER_START")
return
end
end
elseif encounterID == v.eId then
self:StartCombat(v.mod, 0, "ENCOUNTER_START")
return
end
end
end
end
end
end
function DBM:ENCOUNTER_END(encounterID, name, difficulty, size, success)
self:Debug("ENCOUNTER_END event fired: "..encounterID.." "..name.." "..difficulty.." "..size.." "..success)
for i = #inCombat, 1, -1 do
local v = inCombat[i]
if not v.combatInfo then return end
if v.noEEDetection then return end
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.
name = string.split(",", name)
DBT:CreateBar(v.respawnTime, L.TIMER_RESPAWN:format(name), 237538)--Interface\\Icons\\Spell_Holy_BorrowedTime
fireEvent("DBM_TimerStart", "DBMRespawnTimer", L.TIMER_RESPAWN:format(name), v.respawnTime, "237538", "extratimer", nil, 0, v.id)
end
if v.multiEncounterPullDetection then
for _, eId in ipairs(v.multiEncounterPullDetection) do
if encounterID == eId then
self:EndCombat(v, success == 0)
sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
return
end
end
elseif encounterID == v.combatInfo.eId then
self:EndCombat(v, success == 0)
sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
return
end
end
end
function DBM:BOSS_KILL(encounterID, name)
self:Debug("BOSS_KILL event fired: "..encounterID.." "..name)
for i = #inCombat, 1, -1 do
local v = inCombat[i]
if not v.combatInfo then return end
if v.noBKDetection then return end
if v.multiEncounterPullDetection then
for _, eId in ipairs(v.multiEncounterPullDetection) do
if encounterID == eId then
self:EndCombat(v)
sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
return
end
end
elseif encounterID == v.combatInfo.eId then
self:EndCombat(v)
sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
return
end
end
end
local function checkExpressionList(exp, str)
for _, v in ipairs(exp) do
if str:match(v) then
return true
end
end
return false
end
-- called for all mob chat events
local function onMonsterMessage(self, type, msg)
-- pull detection
if dbmIsEnabled and combatInfo[LastInstanceMapID] then
for _, v in ipairs(combatInfo[LastInstanceMapID]) do
if v.type == type and checkEntry(v.msgs, msg) or v.type == type .. "_regex" and checkExpressionList(v.msgs, msg) then
self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
elseif v.type == "combat_" .. type .. "find" and findEntry(v.msgs, msg) or v.type == "combat_" .. type and checkEntry(v.msgs, msg) then
if IsInInstance() then--Indoor boss that uses both combat and message for combat, so in other words (such as hodir), don't require "target" of boss for yell like scanForCombat does for World Bosses
self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
else--World Boss
scanForCombat(v.mod, v.mob, 0)
if v.mod.readyCheckQuestId and (self.Options.WorldBossNearAlert or v.mod.Options.ReadyCheck) and not IsQuestFlaggedCompleted(v.mod.readyCheckQuestId) and v.mod.readyCheckMaxLevel >= playerLevel then
self:FlashClientIcon()
self:PlaySound(8960, true)
end
end
end
end
end
-- kill detection (wipe detection would also be nice to have)
-- todo: add sync
for i = #inCombat, 1, -1 do
local v = inCombat[i]
if not v.combatInfo then return end
if v.combatInfo.killType == type and v.combatInfo.killMsgs[msg] then
self:EndCombat(v)
end
end
end
function DBM:CHAT_MSG_MONSTER_YELL(msg, npc, _, _, target)
if IsEncounterInProgress() or (IsInInstance() and InCombatLockdown()) then--Too many 5 mans/old raids don't properly return encounterinprogress
local targetName = target or "nil"
self:Debug("CHAT_MSG_MONSTER_YELL from "..npc.." while looking at "..targetName, 2)
end
return onMonsterMessage(self, "yell", msg)
end
function DBM:CHAT_MSG_MONSTER_EMOTE(msg)
return onMonsterMessage(self, "emote", msg)
end
function DBM:CHAT_MSG_RAID_BOSS_EMOTE(msg, sender, ...)
onMonsterMessage(self, "emote", msg)
local id = msg:match("|Hspell:([^|]+)|h")
if id then
local spellId = tonumber(id)
if spellId then
local spellName = DBM:GetSpellInfo(spellId) or L.UNKNOWN
self:Debug("CHAT_MSG_RAID_BOSS_EMOTE fired: "..sender.."'s "..spellName.."("..spellId..")", 2)
end
end
return self:FilterRaidBossEmote(msg, sender, ...)
end
function DBM:RAID_BOSS_EMOTE(msg, ...)--This is a mirror of above prototype only it has less args, both still exist for some reason.
onMonsterMessage(self, "emote", msg)
return self:FilterRaidBossEmote(msg, ...)
end
function DBM:RAID_BOSS_WHISPER(msg)
--Make it easier for devs to detect whispers they are unable to see
--TINTERFACE\\ICONS\\ability_socererking_arcanewrath.blp:20|t You have been branded by |cFFF00000|Hspell:156238|h[Arcane Wrath]|h|r!"
if IsInGroup() and not _G["BigWigs"] then
SendAddonMessage("Transcriptor", msg, IsInGroup(2) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY")--Send any emote to transcriptor, even if no spellid
end
end
function DBM:CHAT_MSG_MONSTER_SAY(msg)
return onMonsterMessage(self, "say", msg)
end
end
---------------------------
-- Kill/Wipe Detection --
---------------------------
function checkWipe(self, confirm)
if #inCombat > 0 then
if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = self:GetCurrentInstanceDifficulty()
end
--hack for no iEEU information is provided.
if not bossuIdFound then
for i = 1, 5 do
if UnitExists("boss"..i) then
bossuIdFound = true
break
end
end
end
local wipe -- 0: no wipe, 1: normal wipe, 2: wipe by UnitExists check.
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.
wipe = 0
elseif IsEncounterInProgress() then -- Encounter Progress marked, you obviously in combat with boss. So do not Wipe
wipe = 0
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.
wipe = 0
elseif bossuIdFound and LastInstanceType == "raid" then -- Combat started by IEEU and no boss exist and no EncounterProgress marked, that means wipe
wipe = 2
for i = 1, 5 do
if UnitExists("boss"..i) then
wipe = 0 -- Boss found. No wipe
break
end
end
else -- Unit combat status detection. No combat unit in your party and no EncounterProgress marked, that means wipe
wipe = 1
local uId = (IsInRaid() and "raid") or "party"
for i = 0, GetNumGroupMembers() do
local id = (i == 0 and "player") or uId..i
if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
wipe = 0 -- Someone still in combat. No wipe
break
end
end
end
if wipe == 0 then
self:Schedule(3, checkWipe, self)
elseif confirm then
for i = #inCombat, 1, -1 do
local reason = (wipe == 1 and "No combat unit found in your party." or "No boss found : "..(wipe or "nil"))
self:Debug("You wiped. Reason : "..reason)
self:EndCombat(inCombat[i], true)
end
else
local maxDelayTime = (savedDifficulty == "worldboss" and 15) or 5 --wait 10s more on worldboss do actual wipe.
for _, v in ipairs(inCombat) do
maxDelayTime = v.combatInfo and v.combatInfo.wipeTimer and v.combatInfo.wipeTimer > maxDelayTime and v.combatInfo.wipeTimer or maxDelayTime
end
self:Schedule(maxDelayTime, checkWipe, self, true)
end
end
end
function checkBossHealth(self, onlyHighest)
if #inCombat > 0 then
for _, v in ipairs(inCombat) do
if not v.multiMobPullDetection or v.mainBoss then
self:GetBossHP(v.mainBoss or v.combatInfo.mob or -1, onlyHighest)
else
for _, mob in ipairs(v.multiMobPullDetection) do
self:GetBossHP(mob, onlyHighest)
end
end
end
self:Schedule(1, checkBossHealth, self, onlyHighest)
end
end
function checkCustomBossHealth(self, mod)
mod:CustomHealthUpdate()
self:Schedule(1, checkCustomBossHealth, self, mod)
end
do
local tooltipsHidden = false
--Delayed Guild Combat sync object so we allow time for RL to disable them
local function delayedGCSync(modId, difficultyIndex, difficultyModifier, name, thisTime, wipeHP)
if not statusGuildDisabled and updateNotificationDisplayed == 0 then
if thisTime then--Wipe event
SendAddonMessage("D4", "GCE\t"..modId.."\t6\t1\t"..thisTime.."\t"..difficultyIndex.."\t"..difficultyModifier.."\t"..name.."\t"..wipeHP, "GUILD")
else
SendAddonMessage("D4", "GCB\t"..modId.."\t3\t"..difficultyIndex.."\t"..difficultyModifier.."\t"..name, "GUILD")
end
end
end
local statVarTable = {
--Current
["event5"] = "normal",
["event20"] = "lfr25",
["event40"] = "lfr25",
["normal5"] = "normal",
["heroic5"] = "heroic",
["challenge5"] = "challenge",
["lfr"] = "lfr25",
["normal"] = "normal",
["heroic"] = "heroic",
["mythic"] = "mythic",
["worldboss"] = "normal",
["timewalker"] = "timewalker",
["progressivechallenges"] = "normal",
--BFA
["normalwarfront"] = "normal",
["heroicwarfront"] = "heroic",
["normalisland"] = "normal",
["heroicisland"] = "heroic",
["mythicisland"] = "mythic",
["teamingisland"] = "mythic",--Blizz uses mythic as fallback, so I will too
--Legacy
["lfr25"] = "lfr25",
["normal10"] = "normal",
["normal20"] = "normal",
["normal25"] = "normal25",--Legacy raids that have two normal difficulties still (10/25)
["normal40"] = "normal",
["heroic10"] = "heroic",
["heroic25"] = "heroic25",--Legacy raids that have two heroic difficulties still (10/25)
["normalscenario"] = "normal",
["heroicscenario"] = "heroic",
}
function DBM:StartCombat(mod, delay, event, synced, syncedStartHp, syncedEvent)
cSyncSender = {}
cSyncReceived = 0
if not checkEntry(inCombat, mod) then
if not mod.Options.Enabled then return end
if not mod.combatInfo then return end
if mod.combatInfo.noCombatInVehicle and UnitInVehicle("player") then -- HACK
return
end
--HACK: makes sure that we don't detect a false pull if the event fires again when the boss dies...
if mod.lastKillTime and GetTime() - mod.lastKillTime < (mod.reCombatTime or 120) and event ~= "LOADING_SCREEN_DISABLED" then return end
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
if event then
self:Debug("StartCombat called by : "..event..". LastInstanceMapID is "..LastInstanceMapID)
if event ~= "ENCOUNTER_START" then
self:Debug("This event is started by"..event..". Review ENCOUNTER_START event to ensure if this is still needed", 2)
end
else
self:Debug("StartCombat called by individual mod or unknown reason. LastInstanceMapID is "..LastInstanceMapID)
event = ""
end
--check completed. starting combat
tinsert(inCombat, mod)
if mod.inCombatOnlyEvents and not mod.inCombatOnlyEventsRegistered then
mod.inCombatOnlyEventsRegistered = 1
mod:RegisterEvents(unpack(mod.inCombatOnlyEvents))
end
--Fix for "attempt to perform arithmetic on field 'stats' (a nil value)"
if not mod.stats and not mod.noStatistics then
self:AddMsg(L.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
mod.ignoreBestkill = true--Force this to true so we don't check any more occurances of "stats"
elseif event == "TIMER_RECOVERY" then --add a lag time to delay when TIMER_RECOVERY
delay = delay + select(4, GetNetStats()) / 1000
end
--set mod default info
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = self:GetCurrentInstanceDifficulty()
local name = mod.combatInfo.name
local modId = mod.id
if C_Scenario.IsInScenario() and (mod.addon.type == "SCENARIO") then
mod.inScenario = true
end
mod.inCombat = true
mod.blockSyncs = nil
mod.combatInfo.pull = GetTime() - (delay or 0)
bossuIdFound = event == "IEEU"
if mod.minCombatTime then
self:Schedule(mmax((mod.minCombatTime - delay), 3), checkWipe, self)
else
self:Schedule(3, checkWipe, self)
end
--get boss hp at pull
if syncedStartHp and syncedStartHp < 1 then
syncedStartHp = syncedStartHp * 100
end
local startHp = syncedStartHp or mod:GetBossHP(mod.mainBoss or mod.combatInfo.mob or -1) or 100
--check boss engaged first?
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
mod.ignoreBestkill = true
elseif mod.inScenario then
local _, currentStage, numStages = C_Scenario.GetInfo()
if currentStage > 1 and numStages > 1 then
mod.ignoreBestkill = true
end
else--Reset ignoreBestkill after wipe
mod.ignoreBestkill = false
--It was a clean pull, so cancel any RequestTimers which might fire after boss was pulled if boss was pulled right after mod load
--Only want timer recovery on in progress bosses, not clean pulls
if startHp > 98 and (savedDifficulty == "worldboss" or event == "IEEU") or event == "ENCOUNTER_START" then
self:Unschedule(self.RequestTimers)
end
end
if not mod.inScenario then
if self.Options.HideTooltips then
--Better or cleaner way?
tooltipsHidden = true
GameTooltip.Temphide = function() GameTooltip:Hide() end; GameTooltip:SetScript("OnShow", GameTooltip.Temphide)
end
if self.Options.DisableSFX and GetCVar("Sound_EnableSFX") == "1" then
SetCVar("Sound_EnableSFX", 0)
end
--boss health info scheduler
if mod.CustomHealthUpdate then
self:Schedule(1, checkCustomBossHealth, self, mod)
else
self:Schedule(1, checkBossHealth, self, mod.onlyHighest)
end
end
--process global options
self:HideBlizzardEvents(1)
if self.Options.RecordOnlyBosses then
self:StartLogging(0, nil)
end
if self.Options.HideObjectivesFrame and mod.addon.type ~= "SCENARIO" and GetNumTrackedAchievements() == 0 and difficultyIndex ~= 8 and not InCombatLockdown() then
if ObjectiveTrackerFrame:IsVisible() then
--ObjectiveTrackerFrame:Hide()
ObjectiveTracker_Collapse()
watchFrameRestore = true
end
end
fireEvent("DBM_Pull", mod, delay, synced, startHp)
self:FlashClientIcon()
--serperate timer recovery and normal start.
if event ~= "TIMER_RECOVERY" then
--add pull count
if mod.stats and not mod.noStatistics then
if not mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Pulls"] = 0 end
mod.stats[statVarTable[savedDifficulty].."Pulls"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] + 1
end
--show speed timer
if self.Options.AlwaysShowSpeedKillTimer2 and mod.stats and not mod.ignoreBestkill and not mod.noStatistics then
local bestTime
if difficultyIndex == 8 then--Mythic+/Challenge Mode
local bestMPRank = mod.stats.challengeBestRank or 0
if bestMPRank == difficultyModifier then
--Don't show speed kill timer if not our highest rank. DBM only stores highest rank
bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
end
else
bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
end
if bestTime and bestTime > 0 then
local speedTimer = mod:NewTimer(bestTime, L.SPEED_KILL_TIMER_TEXT, "237538", nil, false)
speedTimer:Start()
end
end
--update boss left
if mod.numBoss then
mod.vb.bossLeft = mod.numBoss
end
--elect icon person
if mod.findFastestComputer and not self.Options.DontSetIcons then
if self:GetRaidRank() > 0 then
for i = 1, #mod.findFastestComputer do
local option = mod.findFastestComputer[i]
if mod.Options[option] then
sendSync("IS", UnitGUID("player").."\t"..tostring(self.Revision).."\t"..option)
end
end
elseif not IsInGroup() then
for i = 1, #mod.findFastestComputer do
local option = mod.findFastestComputer[i]
if mod.Options[option] then
canSetIcons[option] = true
end
end
end
end
--call OnCombatStart
if mod.OnCombatStart then
local startEvent = syncedEvent or event
mod:OnCombatStart(delay or 0, startEvent == "PLAYER_REGEN_DISABLED_AND_MESSAGE" or startEvent == "SPELL_CAST_SUCCESS" or startEvent == "MONSTER_MESSAGE", startEvent == "ENCOUNTER_START")
end
--send "C" sync
if not synced then
sendSync("C", (delay or 0).."\t"..modId.."\t"..(mod.revision or 0).."\t"..startHp.."\t"..tostring(self.Revision).."\t"..(mod.hotfixNoticeRev or 0).."\t"..event)
end
if UnitIsGroupLeader("player") then
if self.Options.DisableGuildStatus then
sendSync("DGP")
end
if self.Options.DisableStatusWhisper and (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) then
sendSync("DSW")
end
end
if self.Options.oRA3AnnounceConsumables and _G["oRA3Frame"] then
local oRA3 = LibStub("AceAddon-3.0"):GetAddon("oRA3", true)
if oRA3 then
local consumables = oRA3:GetModule("Consumables", true)
if consumables then
consumables:OutputResults()
end
end
end
--show engage message
if self.Options.ShowEngageMessage and not mod.noStatistics then
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.
self:AddMsg(L.COMBAT_STARTED_IN_PROGRESS:format(difficultyText..name))
elseif mod.ignoreBestkill and mod.inScenario then
self:AddMsg(L.SCENARIO_STARTED_IN_PROGRESS:format(difficultyText..name))
else
if mod.addon.type == "SCENARIO" then
self:AddMsg(L.SCENARIO_STARTED:format(difficultyText..name))
else
self:AddMsg(L.COMBAT_STARTED:format(difficultyText..name))
if (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not statusGuildDisabled and not self.Options.DisableGuildStatus then--Only send relevant content, not guild beating down lich king or LFR.
self:Schedule(1.5, delayedGCSync, modId, difficultyIndex, difficultyModifier, name)
end
end
end
end
--stop pull count
local dummyMod = self:GetModByName("PullTimerCountdownDummy")
if dummyMod then--stop pull timer
dummyMod.text:Cancel()
dummyMod.timer:Stop()
if not self.Options.DontShowPTCountdownText then
for _, tttimer in pairs(TimerTracker.timerList) do
if tttimer.type == 3 and not tttimer.isFree then
FreeTimerTrackerTimer(tttimer)
break
end
end
end
end
local bigWigs = _G["BigWigs"]
if bigWigs and bigWigs.db.profile.raidicon and not self.Options.DontSetIcons and self:GetRaidRank() > 0 then--Both DBM and bigwigs have raid icon marking turned on.
self:AddMsg(L.BIGWIGS_ICON_CONFLICT)--Warn that one of them should be turned off to prevent conflict (which they turn off is obviously up to raid leaders preference, dbm accepts either or turned off to stop this alert)
end
if self.Options.EventSoundEngage2 and self.Options.EventSoundEngage2 ~= "" and self.Options.EventSoundEngage2 ~= "None" then
self:PlaySoundFile(self.Options.EventSoundEngage2, nil, true)
end
if self.Options.EventSoundMusic and self.Options.EventSoundMusic ~= "None" and self.Options.EventSoundMusic ~= "" and not (self.Options.EventMusicMythicFilter and (savedDifficulty == "mythic" or savedDifficulty == "challenge")) and not mod.noStatistics then
fireEvent("DBM_MusicStart", "BossEncounter")
if not self.Options.RestoreSettingMusic then
self.Options.RestoreSettingMusic = tonumber(GetCVar("Sound_EnableMusic")) or 1
if self.Options.RestoreSettingMusic == 0 then
SetCVar("Sound_EnableMusic", 1)
else
self.Options.RestoreSettingMusic = nil--Don't actually need it
end
end
local path = "MISSING"
if self.Options.EventSoundMusic == "Random" then
local usedTable = self.Options.EventSoundMusicCombined and self.Music or mod.inScenario and self.DungeonMusic or self.BattleMusic
if #usedTable >= 3 then
local random = fastrandom(3, #usedTable)
path = usedTable[random].value
end
else
path = self.Options.EventSoundMusic
end
if path ~= "MISSING" then
PlayMusic(path)
self.Options.musicPlaying = true
self:Debug("Starting combat music with file: "..path)
end
end
else
self:AddMsg(L.COMBAT_STATE_RECOVERED:format(difficultyText..name, strFromTime(delay)))
if mod.OnTimerRecovery then
mod:OnTimerRecovery()
end
end
if savedDifficulty == "worldboss" and self.Options.EnableWBSharing and not mod.noWBEsync then
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.
lastBossEngage[modId..playerRealm] = GetTime()--Update last engage time, that way we ignore our own sync
SendWorldSync(self, "WBE", modId.."\t"..playerRealm.."\t"..startHp.."\t8\t"..name)
end
end
end
function DBM:UNIT_HEALTH(uId)
local cId = self:GetCIDFromGUID(UnitGUID(uId))
local health
if UnitHealthMax(uId) ~= 0 then
health = UnitHealth(uId) / UnitHealthMax(uId) * 100
end
if not health or health < 2 then return end -- no worthy of combat start if health is below 2%
if dbmIsEnabled and InCombatLockdown() then
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.
if combatInfo[LastInstanceMapID] then
for _, v in ipairs(combatInfo[LastInstanceMapID]) do
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
if v.mod.noFriendlyEngagement and UnitIsFriend("player", uId) then return end
-- Delay set, > 97% = 0.5 (consider as normal pulling), max dealy limited to 20s.
self:StartCombat(v.mod, health > 97 and 0.5 or mmin(GetTime() - lastCombatStarted, 20), "UNIT_HEALTH", nil, health)
end
end
end
end
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.
self:PlaySound(8585)--So fire an alert sound to save yourself from this person's behavior.
self:AddMsg(L.AFK_WARNING:format(health))
end
end
end
function DBM:EndCombat(mod, wipe, srmIncluded)
if removeEntry(inCombat, mod) then
local scenario = mod.addon.type == "SCENARIO" and not mod.soloChallenge
if mod.inCombatOnlyEvents and mod.inCombatOnlyEventsRegistered then
if srmIncluded then-- unregister all events including SPELL_AURA_REMOVED events
mod:UnregisterInCombatEvents(false, true)
else-- unregister all events except for SPELL_AURA_REMOVED events (might still be needed to remove icons etc...)
mod:UnregisterInCombatEvents()
self:Schedule(2, mod.UnregisterInCombatEvents, mod, true) -- 2 seconds should be enough for all auras to fade
end
self:Schedule(3, mod.Stop, mod) -- Remove accident started timers.
mod.inCombatOnlyEventsRegistered = nil
if mod.OnCombatEnd then
self:Schedule(3, mod.OnCombatEnd, mod, wipe, true) -- Remove accidentally shown frames
end
end
if mod.updateInterval then
mod:UnregisterOnUpdateHandler()
end
mod:Stop()
if enableIcons and not self.Options.DontSetIcons and not self.Options.DontRestoreIcons then
-- restore saved previous icon
for uId, icon in pairs(mod.iconRestore) do
SetRaidTarget(uId, icon)
end
twipe(mod.iconRestore)
end
mod.inCombat = false
mod.blockSyncs = true
if mod.combatInfo.killMobs then
for i, _ in pairs(mod.combatInfo.killMobs) do
mod.combatInfo.killMobs[i] = true
end
end
if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
end
local name = mod.combatInfo.name
local modId = mod.id
if wipe and mod.stats and not mod.noStatistics then
mod.lastWipeTime = GetTime()
--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)
local thisTime = GetTime() - mod.combatInfo.pull
local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
local wipeHP = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or L.UNKNOWN
if mod.vb.phase then
wipeHP = wipeHP.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
end
if mod.numBoss and mod.vb.bossLeft and mod.numBoss > 1 then
local bossesKilled = mod.numBoss - mod.vb.bossLeft
wipeHP = wipeHP.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
end
local totalPulls = mod.stats[statVarTable[savedDifficulty].."Pulls"]
local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
if thisTime < 30 then -- Normally, one attempt will last at least 30 sec.
totalPulls = totalPulls - 1
mod.stats[statVarTable[savedDifficulty].."Pulls"] = totalPulls
if self.Options.ShowDefeatMessage then
if scenario then
self:AddMsg(L.SCENARIO_ENDED_AT:format(difficultyText..name, strFromTime(thisTime)))
else
self:AddMsg(L.COMBAT_ENDED_AT:format(difficultyText..name, wipeHP, strFromTime(thisTime)))
--No reason to GCE it here, so omited on purpose.
end
end
else
if self.Options.ShowDefeatMessage then
if scenario then
self:AddMsg(L.SCENARIO_ENDED_AT_LONG:format(difficultyText..name, strFromTime(thisTime), totalPulls - totalKills))
else
self:AddMsg(L.COMBAT_ENDED_AT_LONG:format(difficultyText..name, wipeHP, strFromTime(thisTime), totalPulls - totalKills))
if (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not self.Options.DisableGuildStatus then
self:Schedule(1.5, delayedGCSync, modId, difficultyIndex, difficultyModifier, name, strFromTime(thisTime), wipeHP)
end
end
end
if self.Options.EventSoundWipe and self.Options.EventSoundWipe ~= "None" and self.Options.EventSoundWipe ~= "" then
if self.Options.EventSoundWipe == "Random" then
if #self.Defeat >= 3 then
local random = fastrandom(3, #self.Defeat)
self:PlaySoundFile(self.Defeat[random].value)
end
else
self:PlaySoundFile(self.Options.EventSoundWipe, nil, true)
end
end
end
if showConstantReminder == 2 and IsInGroup() then
showConstantReminder = 1
--Show message any time this is a mod that has a newer hotfix revision and it's a wipe
--These people need to know the wipe could very well be their fault.
self:AddMsg(L.OUT_OF_DATE_NAG)
end
local msg
for k, _ in pairs(autoRespondSpam) do
if self.Options.WhisperStats then
if scenario then
msg = msg or chatPrefixShort..L.WHISPER_SCENARIO_END_WIPE_STATS:format(playerName, difficultyText..(name or ""), totalPulls - totalKills)
else
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_WIPE_STATS_AT:format(playerName, difficultyText..(name or ""), wipeHP, totalPulls - totalKills)
end
else
if scenario then
msg = msg or chatPrefixShort..L.WHISPER_SCENARIO_END_WIPE:format(playerName, difficultyText..(name or ""))
else
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_WIPE_AT:format(playerName, difficultyText..(name or ""), wipeHP)
end
end
sendWhisper(k, msg)
end
fireEvent("DBM_Wipe", mod)
elseif not wipe and mod.stats and not mod.noStatistics then
mod.lastKillTime = GetTime()
local thisTime = GetTime() - (mod.combatInfo.pull or 0)
local lastTime = mod.stats[statVarTable[savedDifficulty].."LastTime"]
local bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
if not mod.stats[statVarTable[savedDifficulty].."Kills"] or mod.stats[statVarTable[savedDifficulty].."Kills"] < 0 then mod.stats[statVarTable[savedDifficulty].."Kills"] = 0 end
--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.
mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Kills"] + 1
if mod.stats[statVarTable[savedDifficulty].."Kills"] > mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] end
if not mod.ignoreBestkill and mod.combatInfo.pull then
mod.stats[statVarTable[savedDifficulty].."LastTime"] = thisTime
--Just to prevent pre mature end combat calls from broken mods from saving bad time stats.
if bestTime and bestTime > 0 and bestTime < 1.5 then
mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime
else
if difficultyIndex == 8 then--Mythic+/Challenge Mode
if mod.stats.challengeBestRank > difficultyModifier then--Don't save time stats at all
--DO nothing
elseif mod.stats.challengeBestRank < difficultyModifier then--Update best time and best rank, even if best time is lower (for a lower rank)
mod.stats.challengeBestRank = difficultyModifier--Update best rank
mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime--Write this time no matter what.
else--Best rank must match current rank, so update time normally
mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
end
else
mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
end
end
end
local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
if self.Options.ShowDefeatMessage then
local msg
local thisTimeString = thisTime and strFromTime(thisTime)
if not mod.combatInfo.pull then--was a bad pull so we ignored thisTime, should never happen
if scenario then
msg = L.SCENARIO_COMPLETE:format(difficultyText..name, L.UNKNOWN)
else
msg = L.BOSS_DOWN:format(difficultyText..name, L.UNKNOWN)
end
elseif mod.ignoreBestkill then--Should never happen in a scenario so no need for scenario check.
if scenario then
msg = L.SCENARIO_COMPLETE_I:format(difficultyText..name, totalKills)
else
msg = L.BOSS_DOWN_I:format(difficultyText..name, totalKills)
end
elseif not lastTime then
if scenario then
msg = L.SCENARIO_COMPLETE:format(difficultyText..name, thisTimeString)
else
msg = L.BOSS_DOWN:format(difficultyText..name, thisTimeString)
end
elseif thisTime < (bestTime or mhuge) then
if scenario then
msg = L.SCENARIO_COMPLETE_NR:format(difficultyText..name, thisTimeString, strFromTime(bestTime), totalKills)
else
msg = L.BOSS_DOWN_NR:format(difficultyText..name, thisTimeString, strFromTime(bestTime), totalKills)
end
else
if scenario then
msg = L.SCENARIO_COMPLETE_L:format(difficultyText..name, thisTimeString, strFromTime(lastTime), strFromTime(bestTime), totalKills)
else
msg = L.BOSS_DOWN_L:format(difficultyText..name, thisTimeString, strFromTime(lastTime), strFromTime(bestTime), totalKills)
end
end
if not scenario and thisTimeString and (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not statusGuildDisabled and not self.Options.DisableGuildStatus and updateNotificationDisplayed == 0 then
SendAddonMessage("D4", "GCE\t"..modId.."\t6\t0\t"..thisTimeString.."\t"..difficultyIndex.."\t"..difficultyModifier.."\t"..name, "GUILD")
end
self:Schedule(1, self.AddMsg, self, msg)
end
local msg
for k, _ in pairs(autoRespondSpam) do
if self.Options.WhisperStats then
if scenario then
msg = msg or chatPrefixShort..L.WHISPER_SCENARIO_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
else
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
end
else
if scenario then
msg = msg or chatPrefixShort..L.WHISPER_SCENARIO_END_KILL:format(playerName, difficultyText..(name or ""))
else
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_KILL:format(playerName, difficultyText..(name or ""))
end
end
sendWhisper(k, msg)
end
fireEvent("DBM_Kill", mod)
if savedDifficulty == "worldboss" and self.Options.EnableWBSharing and not mod.noWBEsync then
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.
lastBossDefeat[modId..playerRealm] = GetTime()--Update last defeat time before we send it, so we don't handle our own sync
SendWorldSync(self, "WBD", modId.."\t"..playerRealm.."\t8\t"..name)
end
if self.Options.EventSoundVictory2 and self.Options.EventSoundVictory2 ~= "None" and self.Options.EventSoundVictory2 ~= "" then
if self.Options.EventSoundVictory2 == "Random" then
if #self.Victory >= 3 then
local random = fastrandom(3, #self.Victory)
self:PlaySoundFile(self.Victory[random].value)
end
else
self:PlaySoundFile(self.Options.EventSoundVictory2, nil, true)
end
end
end
if mod.OnCombatEnd then mod:OnCombatEnd(wipe) end
if mod.OnLeavingCombat then delayedFunction = mod.OnLeavingCombat end
if #inCombat == 0 then--prevent error if you pulled multiple boss. (Earth, Wind and Fire)
statusWhisperDisabled = false
statusGuildDisabled = false
if self.Options.RecordOnlyBosses then
self:Schedule(10, self.StopLogging, self)--small delay to catch kill/died combatlog events
end
self:HideBlizzardEvents(0)
self:Unschedule(checkBossHealth)
self:Unschedule(checkCustomBossHealth)
self.Arrow:Hide(true)
if watchFrameRestore and not InCombatLockdown() then
--ObjectiveTrackerFrame:Show()
ObjectiveTracker_Expand()
watchFrameRestore = false
end
if tooltipsHidden then
--Better or cleaner way?
tooltipsHidden = false
GameTooltip:SetScript("OnShow", GameTooltip.Show)
end
if self.Options.DisableSFX then
SetCVar("Sound_EnableSFX", 1)
end
--cache table
twipe(autoRespondSpam)
twipe(bossHealth)
twipe(bossHealthuIdCache)
twipe(bossuIdCache)
--sync table
twipe(canSetIcons)
twipe(iconSetRevision)
twipe(iconSetPerson)
twipe(addsGUIDs)
bossuIdFound = false
eeSyncSender = {}
eeSyncReceived = 0
twipe(targetMonitor)
self:CreatePizzaTimer(time, "", nil, nil, nil, true)--Auto Terminate infinite loop timers on combat end
self:TransitionToDungeonBGM(false, true)
self:Schedule(22, self.TransitionToDungeonBGM, self)
end
end
end
end
function DBM:OnMobKill(cId, synced)
for i = #inCombat, 1, -1 do
local v = inCombat[i]
if not v.combatInfo then
return
end
if v.combatInfo.noBossDeathKill then return end
if v.combatInfo.killMobs and v.combatInfo.killMobs[cId] then
if not synced then
sendSync("K", cId)
end
v.combatInfo.killMobs[cId] = false
if v.numBoss then
v.vb.bossLeft = (v.vb.bossLeft or v.numBoss) - 1
self:Debug("Boss left - "..v.vb.bossLeft.."/"..v.numBoss, 2)
end
local allMobsDown = true
for _, k in pairs(v.combatInfo.killMobs) do
if k then
allMobsDown = false
break
end
end
if allMobsDown then
self:EndCombat(v)
end
elseif cId == v.combatInfo.mob and not v.combatInfo.killMobs and not v.combatInfo.multiMobPullDetection then
if not synced then
sendSync("K", cId)
end
self:EndCombat(v)
end
end
end
do
local autoLog = false
local autoTLog = false
local function isCurrentContent()
if instanceDifficultyBylevel[LastInstanceMapID] and (instanceDifficultyBylevel[LastInstanceMapID][1] >= playerLevel) and (instanceDifficultyBylevel[LastInstanceMapID][2] == 3) or (difficultyIndex or 0) == 8 then--current player level raid or any M+ dungeon
return true
end
return false
end
function DBM:StartLogging(timer, checkFunc, force)
self:Unschedule(DBM.StopLogging)
if not force and self.Options.LogOnlyNonTrivial and ((LastInstanceType ~= "raid" and difficultyIndex ~= 8) or IsPartyLFG() or not isCurrentContent()) then return end
if self.Options.AutologBosses then
if not LoggingCombat() then
autoLog = true
self:AddMsg("|cffffff00"..COMBATLOGENABLED.."|r")
LoggingCombat(true)
end
end
local transcriptor = _G["Transcriptor"]
if self.Options.AdvancedAutologBosses and transcriptor then
if not transcriptor:IsLogging() then
autoTLog = true
self:AddMsg("|cffffff00"..L.TRANSCRIPTOR_LOG_START.."|r")
transcriptor:StartLog(1)
end
end
if checkFunc and (autoLog or autoTLog) then
self:Unschedule(checkFunc)
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
end
end
function DBM:StopLogging()
if self.Options.AutologBosses and LoggingCombat() and autoLog then
autoLog = false
self:AddMsg("|cffffff00"..COMBATLOGDISABLED.."|r")
LoggingCombat(false)
end
local transcriptor = _G["Transcriptor"]
if self.Options.AdvancedAutologBosses and transcriptor and autoTLog then
if transcriptor:IsLogging() then
autoTLog = false
self:AddMsg("|cffffff00"..L.TRANSCRIPTOR_LOG_END.."|r")
transcriptor:StopLog(1)
end
end
end
end
do
--In event api fails to pull any data at all, just assign classes to their initial template roles from exiles reach
local fallbackClassToRole = {
["MAGE"] = 1449,
["PALADIN"] = 1451,
["WARRIOR"] = 1446,
["DRUID"] = 1447,
["DEATHKNIGHT"] = 1455,
["HUNTER"] = 1448,
["PRIEST"] = 1452,
["ROGUE"] = 1453,
["SHAMAN"] = 1444,
["WARLOCK"] = 1454,
["MONK"] = 1450,
["DEMONHUNTER"] = 1456,
}
function DBM:SetCurrentSpecInfo()
currentSpecGroup = GetSpecialization() or 1
if GetSpecializationInfo(currentSpecGroup) then
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.
currentSpecID = tonumber(currentSpecID)
else
currentSpecID, currentSpecName = fallbackClassToRole[playerClass], playerClass
end
end
end
--TODO C_IslandsQueue.GetIslandDifficultyInfo(), if 38-40 don't work
function DBM:GetCurrentInstanceDifficulty()
local _, instanceType, difficulty, difficultyName, _, _, _, _, instanceGroupSize = GetInstanceInfo()
if difficulty == 0 or difficulty == 172 or (difficulty == 1 and instanceType == "none") or C_Garrison:IsOnGarrisonMap() then--draenor field returns 1, causing world boss mod bug.
return "worldboss", RAID_INFO_WORLD_BOSS.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 1 then--5 man Normal Dungeon
return "normal5", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 2 then--5 man Heroic Dungeon
return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 3 then--Legacy 10 man Normal Raid
return "normal10", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 4 then--Legacy 25 man Normal Raid
return "normal25", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 5 then--Legacy 10 man Heroic Raid
return "heroic10", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 6 then--Legacy 25 man Heroic Raid
return "heroic25", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 7 then--Legacy 25 man LFR (ie pre WoD zones)
return "lfr25", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 8 then--Dungeon, Mythic+ (Challenge modes in mists and wod)
local keystoneLevel = C_ChallengeMode.GetActiveKeystoneInfo() or 0
return "challenge5", PLAYER_DIFFICULTY6.."+ ("..keystoneLevel..") - ", difficulty, instanceGroupSize, keystoneLevel
elseif difficulty == 9 then--Legacy 40 man raids, no longer returned as index 3 (normal 10man raids)
return "normal40", difficultyName.." - ",difficulty, instanceGroupSize, 0
elseif difficulty == 11 then--Heroic Scenario (mostly Mists of pandaria)
return "heroicscenario", difficultyName.." - ",difficulty, instanceGroupSize, 0
elseif difficulty == 12 then--Normal Scenario (mostly Mists of pandaria)
return "normalscenario", difficultyName.." - ",difficulty, instanceGroupSize, 0
elseif difficulty == 14 then--Flexible Normal Raid
return "normal", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 15 then--Flexible Heroic Raid
return "heroic", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 16 then--Mythic 20 man Raid
return "mythic", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 17 or difficulty == 151 then--Flexible LFR (ie post WoD zones)/8.3+ LFR
return "lfr", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 18 then--Special event 40 player LFR Queue (used by molten core aniversery event)
return "event40", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 19 then--Special event 5 player queue (used by wod pre expansion event that had miniturized version of UBRS remake)
return "event5", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 20 then--Special event 20 player LFR Queue (never used yet)
return "event20", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 23 then--Mythic 5 man Dungeon
return "mythic", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 24 or difficulty == 33 then--Timewalking Dungeon, Timewalking Raid
return "timewalker", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 38 then--Normal BfA Island expedition
return "normalisland", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 39 then--Heroic BfA Island expedition
return "heroicisland", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 40 then--Mythic BfA Island expedition
return "mythicisland", difficultyName.." - ", difficulty, instanceGroupSize, 0
elseif difficulty == 147 then--Normal BfA Warfront
return "normalwarfront", difficultyName.." - ",difficulty, instanceGroupSize, 0
elseif difficulty == 148 then--20 man classic raid
return "normal20", difficultyName.." - ",difficulty, instanceGroupSize, 0
elseif difficulty == 149 then--Heroic BfA Warfront
return "heroicwarfront", difficultyName.." - ",difficulty, instanceGroupSize, 0
elseif difficulty == 152 or difficulty == 167 then--Visions of Nzoth (bfa), Torghast (shadowlands)
return "progressivechallenges", difficultyName.." - ",difficulty, instanceGroupSize, 0
elseif difficulty == 153 then---Teaming BfA? Island expedition
return "teamingisland", difficultyName.." - ",difficulty, instanceGroupSize, 0
else--failsafe
return "normal", "", difficulty, instanceGroupSize, 0
end
end
function DBM:GetCurrentArea()
return LastInstanceMapID
end
function DBM:GetGroupSize()
return LastGroupSize
end
--Public api for requesting what phase a boss is in, in case they missed the DBM_SetStage callback
--ModId would be journal Id or mod string of mod.
--If not mod is not provided, it'll simply return stage for first boss in combat table if a boss is engaged
function DBM:GetStage(modId)
if modId then
local mod = self:GetModByName(modId)
if mod and mod.inCombat then
return mod.vb.phase or 0
end
else
if #inCombat > 0 then--At least one boss is engaged
local mod = inCombat[1]--Get first mod in table
if mod then
return mod.vb.phase or 0
end
end
end
end
function DBM:GetKeyStoneLevel()
return difficultyModifier
end
function DBM:HasMapRestrictions()
--Check playerX and playerY. if they are nil restrictions are active
--Restrictions active in all party, raid, pvp, arena maps. No restrictions in "none" or "scenario"
local playerX, playerY = UnitPosition("player")
if not playerX or not playerY then
return true
end
return false
end
do
local LSMMediaCacheBuilt, sharedMediaFileCache, validateCache = false, {}, {}
local function buildLSMFileCache()
local hashtable = LibStub("LibSharedMedia-3.0", true):HashTable("sound")
local keytable = {}
for k in next, hashtable do
tinsert(keytable, k)
end
for i = 1, #keytable do
sharedMediaFileCache[hashtable[keytable[i]]] = true
end
LSMMediaCacheBuilt = true
end
function DBM:ValidateSound(path, log, ignoreCustom)
-- Validate LibSharedMedia
if not LSMMediaCacheBuilt then
buildLSMFileCache()
end
if not sharedMediaFileCache[path] and not path:find("DBM") then
if log then
if ignoreCustom then
-- This uses debug print because it has potential to cause mid fight spam
self:Debug("PlaySoundFile failed do to missing media at " .. path .. ". To fix this, re-add missing sound or change setting using this sound to a different sound.")
else
AddMsg(self, "PlaySoundFile failed do to missing media at " .. path .. ". To fix this, re-add missing sound or change setting using this sound to a different sound.")
end
end
return false
end
-- Validate audio packs
if not validateCache[path] then
local splitTable = {}
for split in string.gmatch(path, "[^\\/]+") do -- Matches \ and / as path delimiters (incl. more than one)
tinsert(splitTable, split)
end
if #splitTable >= 3 and splitTable[3]:lower() == "dbm-customsounds" then
validateCache[path] = {
exists = ignoreCustom or false
}
elseif #splitTable >= 3 and splitTable[1]:lower() == "interface" and splitTable[2]:lower() == "addons" then -- We're an addon sound
validateCache[path] = {
exists = IsAddOnLoaded(splitTable[3]),
AddOn = splitTable[3]
}
else
validateCache[path] = {
exists = true
}
end
end
if validateCache[path] and not validateCache[path].exists then
if log then
-- This uses actual user print because these events only occure at start or end of instance or fight.
AddMsg(self, "PlaySoundFile failed do to missing media at " .. path .. ". To fix this, re-add/enable " .. validateCache[path].AddOn .. " or change setting using this sound to a different sound.")
end
return false
end
return true
end
local function playSound(self, path, ignoreSFX, validate)
if self.Options.SilentMode or path == "" or path == "None" then
return
end
local soundSetting = self.Options.UseSoundChannel
if type(path) == "number" then
self:Debug("PlaySound playing with media " .. path, 3)
if soundSetting == "Dialog" then
PlaySound(path, "Dialog", false)
elseif ignoreSFX or soundSetting == "Master" then
PlaySound(path, "Master", false)
else
PlaySound(path) -- Using SFX channel, leave forceNoDuplicates on.
end
fireEvent("DBM_PlaySound", path)
else
if validate and not self:ValidateSound(path, true, true) then
return
end
self:Debug("PlaySoundFile playing with media " .. path, 3)
if soundSetting == "Dialog" then
PlaySoundFile(path, "Dialog")
elseif ignoreSFX or soundSetting == "Master" then
PlaySoundFile(path, "Master")
else
PlaySoundFile(path)
end
fireEvent("DBM_PlaySound", path)
end
end
function DBM:PlaySoundFile(path, ignoreSFX, validate)
playSound(self, path, ignoreSFX, validate)
end
function DBM:PlaySound(path, ignoreSFX, validate)
playSound(self, path, ignoreSFX, validate)
end
end
--Future proofing EJ_GetSectionInfo compat layer to make it easier updatable.
function DBM:EJ_GetSectionInfo(sectionID)
local info = EJ_GetSectionInfo(sectionID)
if not info then
if self.Options.BadIDAlert then
self:AddMsg("|cffff0000Invalid call to EJ_GetSectionInfo for sectionID: |r"..sectionID..". Please report this bug")
else
self:Debug("|cffff0000Invalid call to EJ_GetSectionInfo for sectionID: |r"..sectionID)
end
return nil
end
local flag1, flag2, flag3, flag4
local flags = GetSectionIconFlags(sectionID)
if flags then
flag1, flag2, flag3, flag4 = unpack(flags)
end
return info.title, info.description, info.headerType, info.abilityIcon, info.creatureDisplayID, info.siblingSectionID, info.firstChildSectionID, info.filteredByDifficulty, info.link, info.startsOpen, flag1, flag2, flag3, flag4
end
function DBM:GetDungeonInfo(id)
local temp = GetDungeonInfo(id)
if type(temp) == "table" then
return temp.name
end
return temp
end
--Handle new spell name requesting with wrapper, to make api changes easier to handle
--Keep an eye on C_SpellBook.GetSpellInfo, but don't use it YET as direction of existing GetSpellInfo isn't finalized yet
function DBM:GetSpellInfo(spellId)
local name, rank, icon, castingTime, minRange, maxRange, returnedSpellId = GetSpellInfo(spellId)
if not returnedSpellId then--Bad request all together
if type(spellId) == "string" then
self:Debug("|cffff0000Invalid call to GetSpellInfo for spellID: |r"..spellId.." as a string!")
else
if spellId > 4 then
if self.Options.BadIDAlert then
self:AddMsg("|cffff0000Invalid call to GetSpellInfo for spellID: |r"..spellId..". Please report this bug")
else
self:Debug("|cffff0000Invalid call to GetSpellInfo for spellID: |r"..spellId)
end
end
end
return nil
else--Good request, return now
return name, rank, icon, castingTime, minRange, maxRange, returnedSpellId
end
end
function DBM:UnitAura(uId, spellInput, spellInput2, spellInput3, spellInput4)
if not uId then return end
for i = 1, 60 do
local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitAura(uId, i)
if not spellName then return end
if spellInput == spellName or spellInput == spellId or spellInput2 == spellName or spellInput2 == spellId or spellInput3 == spellName or spellInput3 == spellId or spellInput4 == spellName or spellInput4 == spellId then
return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3
end
end
end
function DBM:UnitDebuff(uId, spellInput, spellInput2, spellInput3, spellInput4)
if not uId then return end
for i = 1, 60 do
local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitDebuff(uId, i)
if not spellName then return end
if spellInput == spellName or spellInput == spellId or spellInput2 == spellName or spellInput2 == spellId or spellInput3 == spellName or spellInput3 == spellId or spellInput4 == spellName or spellInput4 == spellId then
return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3
end
end
end
function DBM:UnitBuff(uId, spellInput, spellInput2, spellInput3, spellInput4)
if not uId then return end
for i = 1, 60 do
local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff(uId, i)
if not spellName then return end
if spellInput == spellName or spellInput == spellId or spellInput2 == spellName or spellInput2 == spellId or spellInput3 == spellName or spellInput3 == spellId or spellInput4 == spellName or spellInput4 == spellId then
return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3
end
end
end
function DBM:UNIT_DIED(args)
local GUID = args.destGUID
if self:IsCreatureGUID(GUID) then
self:OnMobKill(self:GetCIDFromGUID(GUID))
end
----GUIDIsPlayer
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.
self:FlashClientIcon()
self:PlaySound(8585)--So fire an alert sound to save yourself from this person's behavior.
self:AddMsg(L.AFK_WARNING:format(0))
end
end
DBM.UNIT_DESTROYED = DBM.UNIT_DIED
----------------------
-- Timer recovery --
----------------------
do
local requestedFrom = {}
local requestTime = 0
local clientUsed = {}
local sortMe = {}
local function sort(v1, v2)
if v1.revision and not v2.revision then
return true
elseif v2.revision and not v1.revision then
return false
elseif v1.revision and v2.revision then
return v1.revision > v2.revision
else
return (v1.bwversion or 0) > (v2.bwversion or 0)
end
end
function DBM:RequestTimers(requestNum)
twipe(sortMe)
for _, v in pairs(raid) do
tinsert(sortMe, v)
end
tsort(sortMe, sort)
self:Debug("RequestTimers Running", 2)
local selectedClient
local listNum = 0
for _, v in ipairs(sortMe) do
-- If selectedClient player's realm is not same with your's, timer recovery by selectedClient not works at all.
-- 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?)
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
listNum = listNum + 1
if listNum == requestNum then
selectedClient = v
clientUsed[v.name] = GetTime()
break
end
end
end
if not selectedClient then return end
self:Debug("Requesting timer recovery to "..selectedClient.name)
requestedFrom[selectedClient.name] = true
requestTime = GetTime()
SendAddonMessage("D4", "RT", "WHISPER", selectedClient.name)
end
function DBM:ReceiveCombatInfo(sender, mod, time)
if dbmIsEnabled and requestedFrom[sender] and (GetTime() - requestTime) < 5 and #inCombat == 0 then
self:StartCombat(mod, time, "TIMER_RECOVERY")
--Recovery successful, someone sent info, abort other recovery requests
self:Unschedule(self.RequestTimers)
twipe(requestedFrom)
end
end
function DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, paused, ...)
if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
local lag = paused and 0 or select(4, GetNetStats()) / 1000
for _, v in ipairs(mod.timers) do
if v.id == id then
v:Start(totalTime, ...)
if paused then
v.paused = true
end
v:Update(totalTime - timeLeft + lag, totalTime, ...)
end
end
end
end
function DBM:ReceiveVariableInfo(sender, mod, name, value)
if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
if value == "true" then
mod.vb[name] = true
elseif value == "false" then
mod.vb[name] = false
else
mod.vb[name] = value
end
end
end
end
do
local spamProtection = {}
function DBM:SendTimers(target)
self:Debug("SendTimers requested by "..target, 2)
local spamForTarget = spamProtection[target] or 0
-- just try to clean up the table, that should keep the hash table at max. 4 entries or something :)
for k, v in pairs(spamProtection) do
if GetTime() - v >= 1 then
spamProtection[k] = nil
end
end
if GetTime() - spamForTarget < 1 then -- just to prevent players from flooding this on purpose
return
end
spamProtection[target] = GetTime()
if #inCombat < 1 then
--Break timer is up, so send that
--But only if we are not in combat with a boss
if DBT:GetBar(L.TIMER_BREAK) then
local remaining = DBT:GetBar(L.TIMER_BREAK).timer
SendAddonMessage("D4", "BTR3\t"..remaining, "WHISPER", target)
end
return
end
local mod
for _, v in ipairs(inCombat) do
mod = not v.isCustomMod and v
end
mod = mod or inCombat[1]
self:SendCombatInfo(mod, target)
self:SendVariableInfo(mod, target)
self:SendTimerInfo(mod, target)
end
function DBM:SendPVPTimers(target)
self:Debug("SendPVPTimers requested by "..target, 2)
local spamForTarget = spamProtection[target] or 0
local time = GetTime()
-- just try to clean up the table, that should keep the hash table at max. 4 entries or something :)
for k, v in pairs(spamProtection) do
if time - v >= 1 then
spamProtection[k] = nil
end
end
if time - spamForTarget < 1 then -- just to prevent players from flooding this on purpose
return
end
spamProtection[target] = time
local mod = self:GetModByName("PvPGeneral")
if mod then
self:SendTimerInfo(mod, target)
end
end
end
function DBM:SendCombatInfo(mod, target)
return SendAddonMessage("D4", ("CI\t%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target)
end
function DBM:SendTimerInfo(mod, target)
for _, v in ipairs(mod.timers) do
--Pass on any timer that has no type, or has one that isn't an ai timer
if not v.type or v.type and v.type ~= "ai" then
for _, uId in ipairs(v.startedTimers) do
local elapsed, totalTime, timeLeft
if select("#", string.split("\t", uId)) > 1 then
elapsed, totalTime = v:GetTime(select(2, string.split("\t", uId)))
else
elapsed, totalTime = v:GetTime()
end
timeLeft = totalTime - elapsed
if timeLeft > 0 and totalTime > 0 then
SendAddonMessage("D4", ("TR\t%s\t%s\t%s\t%s\t%s"):format(mod.id, timeLeft, totalTime, uId, v.paused and "1" or "0"), "WHISPER", target)
end
end
end
end
end
function DBM:SendVariableInfo(mod, target)
for vname, v in pairs(mod.vb) do
local v2 = tostring(v)
if v2 then
SendAddonMessage("D4", ("VI\t%s\t%s\t%s"):format(mod.id, vname, v2), "WHISPER", target)
end
end
end
do
function DBM:PLAYER_ENTERING_WORLD()
if self.Options.ShowReminders then
C_TimerAfter(25, function() if self.Options.SilentMode then self:AddMsg(L.SILENT_REMINDER) end end)
C_TimerAfter(30, function() if not self.Options.SettingsMessageShown then self.Options.SettingsMessageShown = true self:AddMsg(L.HOW_TO_USE_MOD) end end)
end
if type(C_ChatInfo.RegisterAddonMessagePrefix) == "function" then
if not C_ChatInfo.RegisterAddonMessagePrefix("D4") then -- main prefix for DBM4
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
end
if not C_ChatInfo.IsAddonMessagePrefixRegistered("BigWigs") then
if not C_ChatInfo.RegisterAddonMessagePrefix("BigWigs") then
self:AddMsg("Error: unable to register BigWigs addon message prefix (reached client side addon message filter limit), BigWigs version checks will be unavailable")
end
end
if not C_ChatInfo.IsAddonMessagePrefixRegistered("Transcriptor") then
if not C_ChatInfo.RegisterAddonMessagePrefix("Transcriptor") then
self:AddMsg("Error: unable to register Transcriptor addon message prefix (reached client side addon message filter limit)")
end
end
end
--Check if any previous changed cvars were not restored and restore them
if self.Options.DisableSFX then
SetCVar("Sound_EnableSFX", 1)
self:Debug("Restoring Sound_EnableSFX CVAR")
end
--RestoreSettingMusic doens't need restoring here, since zone change transition will handle it
end
end
------------------------------------
-- Auto-respond/Status whispers --
------------------------------------
do
local function getNumAlivePlayers()
local alive = 0
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
end
else
alive = (UnitIsDeadOrGhost("player") and 0) or 1
for i = 1, GetNumSubgroupMembers() do
alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
end
end
return alive
end
--Cleanup in 8.x with C_Map.GetMapGroupMembersInfo
local function getNumRealAlivePlayers()
local alive = 0
local isInInstance = IsInInstance() or false
local currentMapId = isInInstance and select(4, UnitPosition("player")) or C_Map.GetBestMapForUnit("player") or 0
local currentMapName = C_Map.GetMapInfo(currentMapId) or L.UNKNOWN
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
if isInInstance and select(4, UnitPosition("raid"..i)) == currentMapId or select(7, GetRaidRosterInfo(i)) == currentMapName then
alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
end
end
else
alive = (UnitIsDeadOrGhost("player") and 0) or 1
for i = 1, GetNumSubgroupMembers() do
if isInInstance and select(4, UnitPosition("party"..i)) == currentMapId or select(7, GetRaidRosterInfo(i)) == currentMapName then
alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
end
end
end
return alive
end
function DBM:NumRealAlivePlayers()
return getNumRealAlivePlayers()
end
-- sender is a presenceId for real id messages, a character name otherwise
local function onWhisper(msg, sender, isRealIdMessage)
if statusWhisperDisabled then return end--RL has disabled status whispers for entire raid.
if not checkForSafeSender(sender, true, true, true, isRealIdMessage) then return end--Automatically reject all whisper functions from non friends, non guildies, or people in group with us
if msg:find(chatPrefix) and not InCombatLockdown() and DBM:AntiSpam(60, "Ogron") and DBM.Options.AutoReplySound then
--Might need more validation if people figure out they can just whisper people with chatPrefix to trigger it.
--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
DBM:PlaySound(41928)--"sound\\creature\\aggron1\\VO_60_HIGHMAUL_AGGRON_1_AGGRO_1.ogg"
elseif msg == "status" and #inCombat > 0 and DBM.Options.AutoRespond then
if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
end
local mod
for _, v in ipairs(inCombat) do
mod = not v.isCustomMod and v
end
mod = mod or inCombat[1]
if mod.noStatistics then return end
if IsInScenarioGroup() and not mod.soloChallenge then return end--status not really useful on scenario mods since there is no way to report progress as a percent. We just ignore it.
local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or L.UNKNOWN
if mod.vb.phase then
hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
end
if mod.numBoss and mod.vb.bossLeft and mod.numBoss > 1 then
local bossesKilled = mod.numBoss - mod.vb.bossLeft
hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
end
sendWhisper(sender, chatPrefix..L.STATUS_WHISPER:format(difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
elseif #inCombat > 0 and DBM.Options.AutoRespond then
if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
end
local mod
for _, v in ipairs(inCombat) do
mod = not v.isCustomMod and v
end
mod = mod or inCombat[1]
if mod.noStatistics then return end
local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or L.UNKNOWN
if mod.vb.phase then
hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
end
if mod.numBoss and mod.vb.bossLeft and mod.numBoss > 1 then
local bossesKilled = mod.numBoss - mod.vb.bossLeft
hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
end
if not autoRespondSpam[sender] then
if IsInScenarioGroup() and not mod.soloChallenge then
sendWhisper(sender, chatPrefix..L.AUTO_RESPOND_WHISPER_SCENARIO:format(playerName, difficultyText..(mod.combatInfo.name or ""), getNumAlivePlayers(), DBM:GetNumGroupMembers()))
else
sendWhisper(sender, chatPrefix..L.AUTO_RESPOND_WHISPER:format(playerName, difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
end
DBM:AddMsg(L.AUTO_RESPONDED)
end
autoRespondSpam[sender] = true
end
end
function DBM:CHAT_MSG_WHISPER(msg, name, _, _, _, status)
if status ~= "GM" then
name = Ambiguate(name, "none")
return onWhisper(msg, name, false)
end
end
function DBM:CHAT_MSG_BN_WHISPER(msg, ...)
local presenceId = select(12, ...) -- srsly?
return onWhisper(msg, presenceId, true)
end
end
--This completely unregisteres or registers distruptive events so they don't obstruct combat
--Toggle is for if we are turning off or on.
--Custom is for external mods to call function without duplication and allowing pvp mods custom toggle.
do
local unregisteredEvents = {}
local function DisableEvent(frameName, eventName)
if frameName:IsEventRegistered(eventName) then
frameName:UnregisterEvent(eventName)
unregisteredEvents[eventName] = true
end
end
local function EnableEvent(frameName, eventName)
if unregisteredEvents[eventName] then
frameName:RegisterEvent(eventName)
unregisteredEvents[eventName] = nil
end
end
function DBM:HideBlizzardEvents(toggle, custom)
if toggle == 1 then
if (self.Options.HideBossEmoteFrame2 or custom) and not testBuild then
DisableEvent(RaidBossEmoteFrame, "RAID_BOSS_EMOTE")
DisableEvent(RaidBossEmoteFrame, "RAID_BOSS_WHISPER")
DisableEvent(RaidBossEmoteFrame, "CLEAR_BOSS_EMOTES")
SOUNDKIT.UI_RAID_BOSS_WHISPER_WARNING = 999999--Since blizzard can still play the sound via RaidBossEmoteFrame_OnEvent (line 148) via encounter scripts in certain cases despite the frame having no registered events
end
if self.Options.HideGarrisonToasts or custom then
DisableEvent(AlertFrame, "GARRISON_MISSION_FINISHED")
DisableEvent(AlertFrame, "GARRISON_BUILDING_ACTIVATABLE")
end
if self.Options.HideGuildChallengeUpdates or custom then
DisableEvent(AlertFrame, "GUILD_CHALLENGE_COMPLETED")
end
elseif toggle == 0 then
if (self.Options.HideBossEmoteFrame2 or custom) and not testBuild then
EnableEvent(RaidBossEmoteFrame, "RAID_BOSS_EMOTE")
EnableEvent(RaidBossEmoteFrame, "RAID_BOSS_WHISPER")
EnableEvent(RaidBossEmoteFrame, "CLEAR_BOSS_EMOTES")
SOUNDKIT.UI_RAID_BOSS_WHISPER_WARNING = 37666--restore it
end
if self.Options.HideGarrisonToasts then
EnableEvent(AlertFrame, "GARRISON_MISSION_FINISHED")
EnableEvent(AlertFrame, "GARRISON_BUILDING_ACTIVATABLE")
end
if self.Options.HideGuildChallengeUpdates then
EnableEvent(AlertFrame, "GUILD_CHALLENGE_COMPLETED")
end
end
end
end
--------------------------
-- Enable/Disable DBM --
--------------------------
do
local forceDisabled = false
function DBM:Disable(forceDisable)
unscheduleAll()
dbmIsEnabled = false
forceDisabled = forceDisable
end
function DBM:Enable()
if not forceDisabled then
dbmIsEnabled = true
end
end
function DBM:IsEnabled()
return dbmIsEnabled
end
end
-----------------------
-- Misc. Functions --
-----------------------
function DBM:AddMsg(text, prefix)
local tag = prefix or (self.localization and self.localization.general.name) or L.DBM
local frame = DBM.Options.ChatFrame and _G[tostring(DBM.Options.ChatFrame)] or DEFAULT_CHAT_FRAME
frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
if prefix ~= false then
frame:AddMessage(("|cffff7d0a<|r|cffffd200%s|r|cffff7d0a>|r %s"):format(tostring(tag), tostring(text)), 0.41, 0.8, 0.94)
else
frame:AddMessage(text, 0.41, 0.8, 0.94)
end
end
AddMsg = DBM.AddMsg
function DBM:Debug(text, level)
--But we still want to generate callbacks for level 1 and 2 events
if (level or 1) < 3 then
fireEvent("DBM_Debug", text, level)
end
if not self.Options or not self.Options.DebugMode then return end
if (level or 1) <= self.Options.DebugLevel then
local frame = _G[tostring(self.Options.ChatFrame)]
frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
frame:AddMessage("|cffff7d0aDBM Debug:|r "..text, 1, 1, 1)
end
end
do
local testMod
local testWarning1, testWarning2, testWarning3
local testTimer1, testTimer2, testTimer3, testTimer4, testTimer5, testTimer6, testTimer7, testTimer8
local testSpecialWarning1, testSpecialWarning2, testSpecialWarning3
function DBM:DemoMode()
if not testMod then
testMod = self:NewMod("TestMod")
self:GetModLocalization("TestMod"):SetGeneralLocalization{ name = "Test Mod" }
testWarning1 = testMod:NewAnnounce("%s", 1, "136116")--Interface\\Icons\\Spell_Nature_WispSplode
testWarning2 = testMod:NewAnnounce("%s", 2, "136194")
testWarning3 = testMod:NewAnnounce("%s", 3, "135826")
testTimer1 = testMod:NewTimer(20, "%s", "136116", nil, nil)
testTimer2 = testMod:NewTimer(20, "%s ", "134170", nil, nil, 1)
testTimer3 = testMod:NewTimer(20, "%s ", "136194", nil, nil, 3, L.MAGIC_ICON, nil, 1, 4)--inlineIcon, keep, countdown, countdownMax
testTimer4 = testMod:NewTimer(20, "%s ", "136116", nil, nil, 4, L.INTERRUPT_ICON)
testTimer5 = testMod:NewTimer(20, "%s ", "135826", nil, nil, 2, L.HEALER_ICON, nil, 3, 4)--inlineIcon, keep, countdown, countdownMax
testTimer6 = testMod:NewTimer(20, "%s ", "136116", nil, nil, 5, L.TANK_ICON, nil, 2, 4)--inlineIcon, keep, countdown, countdownMax
testTimer7 = testMod:NewTimer(20, "%s ", "136116", nil, nil, 6)
testTimer8 = testMod:NewTimer(20, "%s ", "136116", nil, nil, 7)
testSpecialWarning1 = testMod:NewSpecialWarning("%s", nil, nil, nil, 1, 2)
testSpecialWarning2 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 2, 2)
testSpecialWarning3 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 3, 2) -- hack: non auto-generated special warnings need distinct names (we could go ahead and give them proper names with proper localization entries, but this is much easier)
end
testTimer1:Stop("Test Bar")
testTimer2:Stop("Adds")
testTimer3:Stop("Evil Debuff")
testTimer4:Stop("Important Interrupt")
testTimer5:Stop("Boom!")
testTimer6:Stop("Handle your Role")
testTimer7:Stop("Next Stage")
testTimer8:Stop("Custom User Bar")
testTimer1:Start(10, "Test Bar")
testTimer2:Start(30, "Adds")
testTimer3:Start(43, "Evil Debuff")
testTimer4:Start(20, "Important Interrupt")
testTimer5:Start(60, "Boom!")
testTimer6:Start(35, "Handle your Role")
testTimer7:Start(50, "Next Stage")
testTimer8:Start(55, "Custom User Bar")
testWarning1:Cancel()
testWarning2:Cancel()
testWarning3:Cancel()
testSpecialWarning1:Cancel()
testSpecialWarning1:CancelVoice()
testSpecialWarning2:Cancel()
testSpecialWarning2:CancelVoice()
testSpecialWarning3:Cancel()
testSpecialWarning3:CancelVoice()
testWarning1:Show("Test-mode started...")
testWarning1:Schedule(62, "Test-mode finished!")
testWarning3:Schedule(50, "Boom in 10 sec!")
testWarning3:Schedule(20, "Pew Pew Laser Owl!")
testWarning2:Schedule(38, "Evil Spell in 5 sec!")
testWarning2:Schedule(43, "Evil Spell!")
testWarning1:Schedule(10, "Test bar expired!")
testSpecialWarning1:Schedule(20, "Pew Pew Laser Owl")
testSpecialWarning1:ScheduleVoice(20, "runaway")
testSpecialWarning2:Schedule(43, "Fear!")
testSpecialWarning2:ScheduleVoice(43, "fearsoon")
testSpecialWarning3:Schedule(60, "Boom!")
testSpecialWarning3:ScheduleVoice(60, "defensive")
end
end
DBT:SetAnnounceHook(function(bar)
local prefix
if bar.color and bar.color.r == 1 and bar.color.g == 0 and bar.color.b == 0 then
prefix = L.HORDE or FACTION_HORDE
elseif bar.color and bar.color.r == 0 and bar.color.g == 0 and bar.color.b == 1 then
prefix = L.ALLIANCE or FACTION_ALLIANCE
end
if prefix then
return ("%s: %s %d:%02d"):format(prefix, _G[bar.frame:GetName().."BarName"]:GetText(), floor(bar.timer / 60), bar.timer % 60)
end
end)
function DBM:Capitalize(str)
local firstByte = str:byte(1, 1)
local numBytes = 1
if firstByte >= 0xF0 then -- firstByte & 0b11110000
numBytes = 4
elseif firstByte >= 0xE0 then -- firstByte & 0b11100000
numBytes = 3
elseif firstByte >= 0xC0 then -- firstByte & 0b11000000
numBytes = 2
end
return str:sub(1, numBytes):upper()..str:sub(numBytes + 1):lower()
end
--copied from big wigs with permission from funkydude. Modified by MysticalOS
function DBM:RoleCheck(ignoreLoot)
local spec = GetSpecialization()
if not spec then return end
local role = GetSpecializationRole(spec)
if not role then return end
local specID = GetLootSpecialization()
local _, _, _, _, lootrole = GetSpecializationInfoByID(specID)
if not InCombatLockdown() and not IsFalling() and ((IsPartyLFG() and (difficultyIndex == 14 or difficultyIndex == 15)) or not IsPartyLFG()) then
if UnitGroupRolesAssigned("player") ~= role then
UnitSetRole("player", role)
end
end
--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.
if not ignoreLoot and lootrole and (role ~= lootrole) and self.Options.RoleSpecAlert then
self:AddMsg(L.LOOT_SPEC_REMINDER:format(_G[role] or L.UNKNOWN, _G[lootrole]))
end
end
-- An anti spam function to throttle spammy events (e.g. SPELL_AURA_APPLIED on all group members)
-- @param time the time to wait between two events (optional, default 2.5 seconds)
-- @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)
function DBM:AntiSpam(time, id)
if GetTime() - (id and (self["lastAntiSpam" .. tostring(id)] or 0) or self.lastAntiSpam or 0) > (time or 2.5) then
if id then
self["lastAntiSpam" .. tostring(id)] = GetTime()
else
self.lastAntiSpam = GetTime()
end
return true
else
return false
end
end
function DBM:InCombat()
if #inCombat > 0 then
return true
end
return false
end
function DBM:FlashClientIcon()
if self:AntiSpam(5, "FLASH") then
FlashClientIcon()
end
end
do
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,}
function DBM:IconNumToString(number)
return iconStrings[number] or number
end
function DBM:IconNumToTexture(number)
return "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_"..number..".blp:12:12|t" or number
end
end
--To speed up creating new mods.
function DBM:FindDungeonMapIDs(low, peak, contains)
local start = low or 1
local range = peak or 4000
self:AddMsg("-----------------")
for i = start, range do
local dungeon = GetRealZoneText(i)
if dungeon and dungeon ~= "" then
if not contains or contains and dungeon:find(contains) then
self:AddMsg(i..": "..dungeon)
end
end
end
end
function DBM:FindInstanceIDs(low, peak, contains)
local start = low or 1
local range = peak or 3000
self:AddMsg("-----------------")
for i = start, range do
local instance = EJ_GetInstanceInfo(i)
if instance then
if not contains or contains and instance:find(contains) then
self:AddMsg(i..": "..instance)
end
end
end
end
function DBM:FindScenarioIDs(low, peak, contains)
local start = low or 1
local range = peak or 3000
self:AddMsg("-----------------")
for i = start, range do
local instance = self:GetDungeonInfo(i)
if instance and (not contains or contains and instance:find(contains)) then
self:AddMsg(i..": "..instance)
end
end
end
--/run DBM:FindEncounterIDs(1192)--Shadowlands
--/run DBM:FindEncounterIDs(1178, 23)--Dungeon Template (mythic difficulty)
--/run DBM:FindEncounterIDs(237, 1)--Classic Dungeons need diff 1 specified
--/run DBM:FindDungeonMapIDs(1, 500)--Find Classic Dungeon Map IDs
--/run DBM:FindInstanceIDs(1, 300)--Find Classic Dungeon Journal IDs
function DBM:FindEncounterIDs(instanceID, diff)
if not instanceID then
self:AddMsg("Error: Function requires instanceID be provided")
end
if not diff then diff = 14 end--Default to "normal" in 6.0+ if diff arg not given.
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
self:AddMsg("-----------------")
for i=1, 25 do
local name, _, encounterID = EJ_GetEncounterInfoByIndex(i, instanceID)
if name then
self:AddMsg(encounterID..": "..name)
end
end
end
--Taint the script that disables /run /dump, etc
--ScriptsDisallowedForBeta = function() return false end
-------------------
-- Movie Filter --
-------------------
do
local neverFilter = {
[486] = true, -- Tomb of Sarg Intro
[487] = true, -- Alliance Broken Shore cut-scene
[488] = true, -- Horde Broken Shore cut-scene
[489] = true, -- Unknown, currently encrypted
[490] = true, -- Unknown, currently encrypted
}
function DBM:PLAY_MOVIE(id)
if id and not neverFilter[id] then
self:Debug("PLAY_MOVIE fired for ID: "..id, 2)
local isInstance, instanceType = IsInInstance()
if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or self.Options.MovieFilter2 == "Never" or self.Options.MovieFilter2 == "OnlyFight" and not IsEncounterInProgress() then return end
if self.Options.MovieFilter2 == "Block" or (self.Options.MovieFilter2 == "AfterFirst" or self.Options.MovieFilter2 == "OnlyFight") and self.Options.MoviesSeen[id] then
MovieFrame:Hide()--can only just hide movie frame safely now, which means can't stop audio anymore :\
self:AddMsg(L.MOVIE_SKIPPED)
else
self.Options.MoviesSeen[id] = true
end
end
self:TransitionToDungeonBGM(false, true)
end
function DBM:CINEMATIC_START()
self:Debug("CINEMATIC_START fired", 2)
self.HudMap:SupressCanvas()
local isInstance, instanceType = IsInInstance()
if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or self.Options.MovieFilter2 == "Never" or DBM.Options.MovieFilter2 == "OnlyFight" and not IsEncounterInProgress() then return end
local currentMapID = C_Map.GetBestMapForUnit("player")
local currentSubZone = GetSubZoneText() or ""
if not currentMapID then return end--Protection from map failures in zones that have no maps yet
if self.Options.MovieFilter2 == "Block" or (self.Options.MovieFilter2 == "AfterFirst" or self.Options.MovieFilter2 == "OnlyFight") and self.Options.MoviesSeen[currentMapID..currentSubZone] then
CinematicFrame_CancelCinematic()
self:AddMsg(L.MOVIE_SKIPPED)
else
self.Options.MoviesSeen[currentMapID..currentSubZone] = true
end
self:TransitionToDungeonBGM(false, true)
end
function DBM:CINEMATIC_STOP()
self:Debug("CINEMATIC_STOP fired", 2)
self.HudMap:UnSupressCanvas()
end
end
----------------------------
-- Boss Mod Constructor --
----------------------------
do
local modsById = setmetatable({}, {__mode = "v"})
local mt = {__index = bossModPrototype}
function DBM:NewMod(name, modId, modSubTab, instanceId, nameModifier)
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
if name == "DBM-ProfilesDummy" then return end
if modsById[name] then error("DBM:NewMod(): Mod names are used as IDs and must therefore be unique.", 2) end
local obj = setmetatable(
{
Options = {
Enabled = true,
},
DefaultOptions = {
Enabled = true,
},
subTab = modSubTab,
optionCategories = {
},
categorySort = {"announce", "announceother", "announcepersonal", "announcerole", "timer", "sound", "yell", "nameplate", "icon", "misc"},
id = name,
announces = {},
specwarns = {},
timers = {},
vb = {},
iconRestore = {},
modId = modId,
instanceId = instanceId,
revision = 0,
SyncThreshold = 8,
localization = self:GetModLocalization(name)
},
mt
)
for _, v in ipairs(self.AddOns) do
if v.modId == modId then
obj.addon = v
break
end
end
if tonumber(name) and EJ_GetEncounterInfo then
local t = EJ_GetEncounterInfo(tonumber(name))
if type(nameModifier) == "number" then--Get name form EJ_GetCreatureInfo
t = select(2, EJ_GetCreatureInfo(nameModifier, tonumber(name)))
elseif type(nameModifier) == "function" then--custom name modify function
t = nameModifier(t or name)
else--default name modify
t = tostring(t)
t = string.split(",", t or name)
end
obj.localization.general.name = t or name
obj.modelId = select(4, EJ_GetCreatureInfo(1, tonumber(name)))
elseif name:match("z%d+") then
local t = GetRealZoneText(string.sub(name, 2))
if type(nameModifier) == "number" then--do nothing
elseif type(nameModifier) == "function" then--custom name modify function
t = nameModifier(t or name)
else--default name modify
t = string.split(",", t or name)
end
obj.localization.general.name = t or name
elseif name:match("d%d+") then
local t = self:GetDungeonInfo(string.sub(name, 2))
if type(nameModifier) == "number" then--do nothing
elseif type(nameModifier) == "function" then--custom name modify function
t = nameModifier(t or name)
else--default name modify
t = string.split(",", t or name)
end
obj.localization.general.name = t or name
else
obj.localization.general.name = obj.localization.general.name or name
end
tinsert(self.Mods, obj)
if modId then
self.ModLists[modId] = self.ModLists[modId] or {}
tinsert(self.ModLists[modId], name)
end
modsById[name] = obj
obj:SetZone()
return obj
end
function DBM:GetModByName(name)
return modsById[tostring(name)]
end
end
-----------------------
-- General Methods --
-----------------------
bossModPrototype.RegisterEvents = DBM.RegisterEvents
bossModPrototype.UnregisterInCombatEvents = DBM.UnregisterInCombatEvents
bossModPrototype.AddMsg = DBM.AddMsg
bossModPrototype.RegisterShortTermEvents = DBM.RegisterShortTermEvents
bossModPrototype.UnregisterShortTermEvents = DBM.UnregisterShortTermEvents
function bossModPrototype:SetZone(...)
if select("#", ...) == 0 then
self.zones = {}
if self.addon and self.addon.mapId then
for _, v in ipairs(self.addon.mapId) do
self.zones[v] = true
end
end
elseif select(1, ...) ~= DBM_DISABLE_ZONE_DETECTION then
self.zones = {}
for i = 1, select("#", ...) do
self.zones[select(i, ...)] = true
end
else -- disable zone detection
self.zones = nil
end
end
function bossModPrototype:Toggle()
if self.Options.Enabled then
self:DisableMod()
else
self:EnableMod()
end
end
function bossModPrototype:EnableMod()
self.Options.Enabled = true
end
function bossModPrototype:DisableMod()
self:Stop()
self.Options.Enabled = false
end
function bossModPrototype:Stop()
for _, v in ipairs(self.timers) do
v:Stop()
end
self:Unschedule()
end
function bossModPrototype:SetUsedIcons(...)
self.usedIcons = {}
for i = 1, select("#", ...) do
self.usedIcons[select(i, ...)] = true
end
end
function bossModPrototype:RegisterOnUpdateHandler(func, interval)
startScheduler()
if type(func) ~= "function" then return end
self.elapsed = 0
self.updateInterval = interval or 0
updateFunctions[self] = func
end
function bossModPrototype:UnregisterOnUpdateHandler()
self.elapsed = nil
self.updateInterval = nil
twipe(updateFunctions)
end
function bossModPrototype:SetStage(stage)
if stage == 0 then--Increment request instead of hard value
self.vb.phase = self.vb.phase + 1
else
self.vb.phase = stage
end
if self.inCombat then--Safety, in event mod manages to run any phase change calls out of combat/during a wipe we'll just safely ignore it
fireEvent("DBM_SetStage", self, self.id, self.vb.phase, self.encounterId)--Mod, modId, Stage, Encounter Id (if available).
--Note, in Wrath dungeons some encounters return multiple Ids years ago, but blizzard consolidated them recently such as 217, 265 consolidated to just 1972
--TODO, see if Wrath Classic uses consolidated Ids or original dual Id system. if wrath classic uses dual Ids, DBM_SetStage using self.encounterId will need to be fixed
end
end
--------------
-- Events --
--------------
function bossModPrototype:RegisterEventsInCombat(...)
if self.inCombatOnlyEvents then
geterrorhandler()("combat events already set")
end
self.inCombatOnlyEvents = {...}
for k, v in pairs(self.inCombatOnlyEvents) do
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
-- legacy event, oh noes
self.inCombatOnlyEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
end
end
end
-----------------------
-- Utility Methods --
-----------------------
function bossModPrototype:IsDifficulty(...)
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
for i = 1, select("#", ...) do
if diff == select(i, ...) then
return true
end
end
return false
end
function bossModPrototype:IsLFR()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "lfr" or diff == "lfr25" then
return true
end
return false
end
--Dungeons: normal, heroic. (Raids excluded)
function bossModPrototype:IsEasyDungeon()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "heroic5" or diff == "normal5" then
return true
end
return false
end
--Dungeons: normal, heroic. Raids: LFR, normal
function bossModPrototype:IsEasy()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "normal" or diff == "lfr" or diff == "lfr25" or diff == "heroic5" or diff == "normal5" then
return true
end
return false
end
--Dungeons: mythic, mythic+. Raids: heroic, mythic
function bossModPrototype:IsHard()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "mythic" or diff == "challenge5" or diff == "heroic" then
return true
end
return false
end
--Pretty much ANYTHING that has a normal mode
function bossModPrototype:IsNormal()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "normal" or diff == "normal5" or diff == "normal10" or diff == "normal20" or diff == "normal25" or diff == "normal40" or diff == "normalisland" or diff == "normalwarfront" then
return true
end
return false
end
--Pretty much ANYTHING that has a heroic mode
function bossModPrototype:IsHeroic()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "heroic" or diff == "heroic5" or diff == "heroic10" or diff == "heroic25" or diff == "heroicisland" or diff == "heroicwarfront" then
return true
end
return false
end
--Pretty much ANYTHING that has mythic mode, with mythic+ included
function bossModPrototype:IsMythic()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "mythic" or diff == "challenge5" or diff == "mythicisland" then
return true
end
return false
end
function bossModPrototype:IsEvent()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "event5" or diff == "event20" or diff == "event40" then
return true
end
return false
end
function bossModPrototype:IsWarfront()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "normalwarfront" or diff == "heroicwarfront" then
return true
end
return false
end
function bossModPrototype:IsIsland()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "normalisland" or diff == "heroicisland" or diff == "mythicisland" then
return true
end
return false
end
function bossModPrototype:IsScenario()
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
if diff == "normalscenario" or diff == "heroicscenario" then
return true
end
return false
end
function bossModPrototype:IsValidWarning(sourceGUID, customunitID, loose)
if loose and InCombatLockdown() and GetNumGroupMembers() < 2 then return true end--In a loose check, this basically just checks if we're in combat, important for solo runs of torghast to not gimp mod too much
if customunitID then
if UnitExists(customunitID) and UnitGUID(customunitID) == sourceGUID and UnitAffectingCombat(customunitID) then return true end
else
for uId in DBM:GetGroupMembers() do
local target = uId.."target"
if UnitExists(target) and UnitGUID(target) == sourceGUID and UnitAffectingCombat(target) then return true end
local targettwo = uId.."targettarget"
if UnitExists(targettwo) and UnitGUID(targettwo) == sourceGUID and UnitAffectingCombat(targettwo) then return true end
end
end
return false
end
--force param is used when CheckInterruptFilter is actually being used for a simpe target/focus check and nothing more.
--checkCooldown should never be passed with skip or COUNT interrupt warnings. It should be passed with any other interrupt filter
function bossModPrototype:CheckInterruptFilter(sourceGUID, force, checkCooldown, ignoreTandF)
if DBM.Options.FilterInterrupt2 == "None" and not force then return true end--user doesn't want to use interrupt filter, always return true
if DBM.Options.FilterInterrupt2 == "Always" and not force then return false end--user wants to filter ALL interrupts, always return false
--Pummel, Mind Freeze, Counterspell, Kick, Skull Bash, Rebuke, Silence, Wind Shear, Disrupt, Solar Beam
local InterruptAvailable = true
local requireCooldown = checkCooldown
if (DBM.Options.FilterInterrupt2 == "onlyTandF") or self.isTrashMod and (DBM.Options.FilterInterrupt2 == "TandFandBossCooldown") then
requireCooldown = false
end
if requireCooldown and (UnitIsDeadOrGhost("player") or (GetSpellCooldown(6552)) ~= 0 or (GetSpellCooldown(47528)) ~= 0 or (GetSpellCooldown(282151)) ~= 0 or (GetSpellCooldown(2139)) ~= 0 or (GetSpellCooldown(1766)) ~= 0 or (GetSpellCooldown(106839)) ~= 0 or (GetSpellCooldown(96231)) ~= 0 or (GetSpellCooldown(15487)) ~= 0 or (GetSpellCooldown(57994)) ~= 0 or (GetSpellCooldown(183752)) ~= 0 or (GetSpellCooldown(78675)) ~= 0) then
InterruptAvailable = false--checkCooldown check requested and player has no spell that can interrupt available (or is dead)
end
local unitID = (UnitGUID("target") == sourceGUID) and "target" or (UnitGUID("focus") == sourceGUID) and "focus" or nil
if InterruptAvailable and (ignoreTandF or unitID) then
--Check if it's casting something that's not interruptable at the moment
--needed for torghast since many mobs can have interrupt immunity with same spellIds as other mobs that can be interrupted
if unitID then
if UnitCastingInfo(unitID) then
local _, _, _, _, _, _, _, notInterruptible = UnitCastingInfo(unitID)
if notInterruptible then return false end
elseif UnitChannelInfo(unitID) then
local _, _, _, _, _, _, notInterruptible = UnitChannelInfo(unitID)
if notInterruptible then return false end
end
end
return true
end
return false
end
function bossModPrototype:CheckDispelFilter()
if not DBM.Options.FilterDispel then return true end
--Druid: Nature's Cure (88423), Remove Corruption (2782), Monk: Detox (115450) Monk: Detox (218164), Priest: Purify (527) Priest: Purify Disease (213634), Paladin: Cleanse (4987), Shaman: Cleanse Spirit (51886), Purify Spirit (77130), Mage: Remove Curse (475), Warlock: Singe Magic (89808)
--start, duration, enable = GetSpellCooldown
--start & duration == 0 if spell not on cd
if UnitIsDeadOrGhost("player") then return false end--if dead, can't dispel
if (GetSpellCooldown(88423)) ~= 0 or (GetSpellCooldown(2782)) ~= 0 or (GetSpellCooldown(115450)) ~= 0 or (GetSpellCooldown(218164)) ~= 0 or (GetSpellCooldown(527)) ~= 0 or (GetSpellCooldown(213634)) ~= 0 or (GetSpellCooldown(4987)) ~= 0 or (GetSpellCooldown(51886)) ~= 0 or (GetSpellCooldown(77130)) ~= 0 or (GetSpellCooldown(475)) ~= 0 or (GetSpellCooldown(89808)) ~= 0 then
return false
end
return true
end
function bossModPrototype:IsCriteriaCompleted(criteriaIDToCheck)
if not criteriaIDToCheck then
geterrorhandler()("usage: mod:IsCriteriaComplected(criteriaId)")
return false
end
local _, _, numCriteria = C_Scenario.GetStepInfo()
for i = 1, numCriteria do
local _, _, criteriaCompleted, _, _, _, _, _, criteriaID = C_Scenario.GetCriteriaInfo(i)
if criteriaID == criteriaIDToCheck and criteriaCompleted then
return true
end
end
return false
end
function bossModPrototype:LatencyCheck(custom)
return select(4, GetNetStats()) < (custom or DBM.Options.LatencyThreshold)
end
function bossModPrototype:CheckBigWigs(name)
if raid[name] and raid[name].bwversion then
return raid[name].bwversion
else
return false
end
end
bossModPrototype.IconNumToString = DBM.IconNumToString
bossModPrototype.IconNumToTexture = DBM.IconNumToTexture
bossModPrototype.AntiSpam = DBM.AntiSpam
bossModPrototype.HasMapRestrictions = DBM.HasMapRestrictions
bossModPrototype.GetUnitCreatureId = DBM.GetUnitCreatureId
bossModPrototype.GetCIDFromGUID = DBM.GetCIDFromGUID
bossModPrototype.IsCreatureGUID = DBM.IsCreatureGUID
bossModPrototype.GetUnitIdFromGUID = DBM.GetUnitIdFromGUID
bossModPrototype.CheckNearby = DBM.CheckNearby
bossModPrototype.IsTrivial = DBM.IsTrivial
do
local bossTargetuIds = {
"boss1", "boss2", "boss3", "boss4", "boss5", "focus", "target"
}
local targetScanCount = {}
local repeatedScanEnabled = {}
local function getBossTarget(guid, scanOnlyBoss)
local name, uid, bossuid
local cacheuid = bossuIdCache[guid] or "boss1"
if UnitGUID(cacheuid) == guid then
bossuid = cacheuid
name = DBM:GetUnitFullName(cacheuid.."target")
uid = cacheuid.."target"
bossuIdCache[guid] = bossuid
end
if name then return name, uid, bossuid end
for _, uId in ipairs(bossTargetuIds) do
if UnitGUID(uId) == guid then
bossuid = uId
name = DBM:GetUnitFullName(uId.."target")
uid = uId.."target"
bossuIdCache[guid] = bossuid
break
end
end
if name or scanOnlyBoss then return name, uid, bossuid end
-- Now lets check nameplates
for i = 1, 40 do
if UnitGUID("nameplate"..i) == guid then
bossuid = "nameplate"..i
name = DBM:GetUnitFullName("nameplate"..i.."target")
uid = "nameplate"..i.."target"
bossuIdCache[guid] = bossuid
break
end
end
if name then return name, uid, bossuid end
-- failed to detect from default uIds, scan all group members's target.
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
if UnitGUID("raid"..i.."target") == guid then
bossuid = "raid"..i.."target"
name = DBM:GetUnitFullName("raid"..i.."targettarget")
uid = "raid"..i.."targettarget"
bossuIdCache[guid] = bossuid
break
end
end
elseif IsInGroup() then
for i = 1, GetNumSubgroupMembers() do
if UnitGUID("party"..i.."target") == guid then
bossuid = "party"..i.."target"
name = DBM:GetUnitFullName("party"..i.."targettarget")
uid = "party"..i.."targettarget"
bossuIdCache[guid] = bossuid
break
end
end
end
return name, uid, bossuid
end
function bossModPrototype:GetBossTarget(cidOrGuid, scanOnlyBoss)
local name, uid, bossuid
if type(cidOrGuid) == "number" then
cidOrGuid = cidOrGuid or self.creatureId
local cacheuid = bossuIdCache[cidOrGuid] or "boss1"
if self:GetUnitCreatureId(cacheuid) == cidOrGuid then
bossuIdCache[cidOrGuid] = cacheuid
bossuIdCache[UnitGUID(cacheuid)] = cacheuid
name, uid, bossuid = getBossTarget(UnitGUID(cacheuid), scanOnlyBoss)
else
local found = false
for _, uId in ipairs(bossTargetuIds) do
if self:GetUnitCreatureId(uId) == cidOrGuid then
found = true
bossuIdCache[cidOrGuid] = uId
bossuIdCache[UnitGUID(uId)] = uId
name, uid, bossuid = getBossTarget(UnitGUID(uId), scanOnlyBoss)
break
end
end
if not found and not scanOnlyBoss then
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
if self:GetUnitCreatureId("raid"..i.."target") == cidOrGuid then
bossuIdCache[cidOrGuid] = "raid"..i.."target"
bossuIdCache[UnitGUID("raid"..i.."target")] = "raid"..i.."target"
name, uid, bossuid = getBossTarget(UnitGUID("raid"..i.."target"))
break
end
end
elseif IsInGroup() then
for i = 1, GetNumSubgroupMembers() do
if self:GetUnitCreatureId("party"..i.."target") == cidOrGuid then
bossuIdCache[cidOrGuid] = "party"..i.."target"
bossuIdCache[UnitGUID("party"..i.."target")] = "party"..i.."target"
name, uid, bossuid = getBossTarget(UnitGUID("party"..i.."target"))
break
end
end
end
end
end
else
name, uid, bossuid = getBossTarget(cidOrGuid, scanOnlyBoss)
end
if uid then
local cid = DBM:GetUnitCreatureId(uid)
if cid == 24207 or cid == 80258 or cid == 87519 then--filter army of the dead/Garrison Footman (basically same thing as army)
return nil, nil, nil
end
end
return name, uid, bossuid
end
function bossModPrototype:BossTargetScannerAbort(cidOrGuid, returnFunc)
targetScanCount[cidOrGuid] = nil--Reset count for later use.
self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)
DBM:Debug("Boss target scan for "..cidOrGuid.." should be aborting.", 3)
end
function bossModPrototype:BossUnitTargetScannerAbort(uId)
if not uId then--Not called with unit, means mod requested to clear all used units
DBM:Debug("BossUnitTargetScannerAbort called without unit, clearing all targetMonitor units", 2)
table.wipe(targetMonitor)
return
end
if targetMonitor[uId] and targetMonitor[uId].allowTank and UnitExists(uId.."target") and UnitPlayerOrPetInRaid(uId.."target") then
self:Debug("targetMonitor unit exists, allowTank target exists", 2)
local modId, returnFunc = targetMonitor[uId].modid, targetMonitor[uId].returnFunc
self:Debug("targetMonitor: "..modId..", "..uId..", "..returnFunc, 2)
local mod = self:GetModByName(modId)
self:Debug("targetMonitor found a target that probably is a tank", 2)
mod[returnFunc](mod, self:GetUnitFullName(uId.."target"), uId.."target", uId)--Return results to warning function with all variables.
end
targetMonitor[uId] = nil
DBM:Debug("Boss unit target scan should be aborting for "..uId, 3)
end
function bossModPrototype:BossUnitTargetScanner(uId, returnFunc, scanTime, allowTank)
--UNIT_TARGET event monitor target scanner. Will instantly detect a target change of a registered Unit
--If target change occurs before this method is called (or if boss doesn't change target because cast ends up actually being on the tank, target scan will fail completely
--If allowTank is passed, it basically tells this scanner to return current target of unitId at time of failure/abort when scanTime is complete
local scanDuration = scanTime or 1.5
targetMonitor[uId] = {}
targetMonitor[uId].modid, targetMonitor[uId].returnFunc, targetMonitor[uId].allowTank = self.id, returnFunc, allowTank
self:ScheduleMethod(scanDuration, "BossUnitTargetScannerAbort", uId)--In case of BossUnitTargetScanner firing too late, and boss already having changed target before monitor started, it needs to abort after x seconds
end
function bossModPrototype:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, isFinalScan, targetFilter, tankFilter, onlyPlayers)
--Increase scan count
cidOrGuid = cidOrGuid or self.creatureId
if not cidOrGuid then return end
if not targetScanCount[cidOrGuid] then targetScanCount[cidOrGuid] = 0 end
targetScanCount[cidOrGuid] = targetScanCount[cidOrGuid] + 1
--Set default values
scanInterval = scanInterval or 0.05
scanTimes = scanTimes or 16
local targetname, targetuid, bossuid = self:GetBossTarget(cidOrGuid, scanOnlyBoss)
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
--Do scan
if targetname and targetname ~= L.UNKNOWN and (not targetFilter or (targetFilter and targetFilter ~= targetname)) then
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
if (isEnemyScan and UnitIsFriend("player", targetuid) or (onlyPlayers and not UnitIsPlayer("player", targetuid)) or self:IsTanking(targetuid, bossuid)) and not isFinalScan then--On player scan, ignore tanks. On enemy scan, ignore friendly player. On Only player, ignore npcs and pets
if targetScanCount[cidOrGuid] < scanTimes then--Make sure no infinite loop.
self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter, onlyPlayers)--Scan multiple times to be sure it's not on something other then tank (or friend on enemy scan, or npc/pet on only person)
else--Go final scan.
self:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, true, targetFilter, tankFilter, onlyPlayers)
end
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)
targetScanCount[cidOrGuid] = nil--Reset count for later use.
self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
if (tankFilter and self:IsTanking(targetuid, bossuid)) or (isFinalScan and isEnemyScan) or onlyPlayers and not UnitIsPlayer("player", targetuid) then return end--If enemyScan and playerDetected, return nothing
local scanningTime = (targetScanCount[cidOrGuid] or 1) * scanInterval
self[returnFunc](self, targetname, targetuid, bossuid, scanningTime)--Return results to warning function with all variables.
end
else--target was nil, lets schedule a rescan here too.
if targetScanCount[cidOrGuid] < scanTimes then--Make sure not to infinite loop here as well.
self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter, onlyPlayers)
else
targetScanCount[cidOrGuid] = nil--Reset count for later use.
self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
end
end
end
--infinite scanner. so use this carefully.
local function repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
if repeatedScanEnabled[returnFunc] then
cidOrGuid = cidOrGuid or mod.creatureId
scanInterval = scanInterval or 0.1
local targetname, targetuid, bossuid = mod:GetBossTarget(cidOrGuid, scanOnlyBoss)
if targetname and (includeTank or not mod:IsTanking(targetuid, bossuid)) then
mod[returnFunc](mod, targetname, targetuid, bossuid)
end
DBM:Schedule(scanInterval, repeatedScanner, cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
end
end
function bossModPrototype:StartRepeatedScan(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
repeatedScanEnabled[returnFunc] = true
repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, self)
end
function bossModPrototype:StopRepeatedScan(returnFunc)
repeatedScanEnabled[returnFunc] = nil
end
end
do
local bossCache = {}
local lastTank
function bossModPrototype:GetCurrentTank(cidOrGuid)
if lastTank and GetTime() - (bossCache[cidOrGuid] or 0) < 2 then -- return last tank within 2 seconds of call
return lastTank
else
cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
local uId
local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid)
if mobuId then--Have a valid mob unit ID
--First, use trust threat more than fallbackuId and see what we pull from it first.
--This is because for GetCurrentTank we want to know who is tanking it, not who it's targeting.
local unitId = (IsInRaid() and "raid") or "party"
for i = 0, GetNumGroupMembers() do
local id = (i == 0 and "target") or unitId..i
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
if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
if uId then break end
end
--Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
if fallbackuId and not uId then
uId = fallbackuId
end
end
if uId then--Now we have a valid uId
bossCache[cidOrGuid] = GetTime()
lastTank = UnitName(uId)
return UnitName(lastTank), uId
end
return false
end
end
end
--Now this function works perfectly. But have some limitation due to DBM.RangeCheck:GetDistance() function.
--Unfortunely, DBM.RangeCheck:GetDistance() function cannot reflects altitude difference. This makes range unreliable.
--So, we need to cafefully check range in difference altitude (Especially, tower top and bottom)
do
local rangeCache = {}
local rangeUpdated = {}
function bossModPrototype:CheckBossDistance(cidOrGuid, onlyBoss, itemId, defaultReturn)
if not DBM.Options.DontShowFarWarnings then return true end--Global disable.
cidOrGuid = cidOrGuid or self.creatureId
local uId = DBM:GetUnitIdFromGUID(cidOrGuid, onlyBoss)
if uId then
itemId = itemId or 32698
local inRange = IsItemInRange(itemId, uId)
if inRange then--IsItemInRange was a success
return inRange
else--IsItemInRange doesn't work on all bosses/npcs, but tank checks do
DBM:Debug("CheckBossDistance failed on IsItemInRange for: "..cidOrGuid, 2)
return self:CheckTankDistance(cidOrGuid, nil, onlyBoss, defaultReturn)--Return tank distance check fallback
end
end
DBM:Debug("CheckBossDistance failed on uId for: "..cidOrGuid, 2)
return (defaultReturn == nil) or defaultReturn--When we simply can't figure anything out, return true and allow warnings using this filter to fire
end
function bossModPrototype:CheckTankDistance(cidOrGuid, distance, onlyBoss, defaultReturn)
if not DBM.Options.DontShowFarWarnings then return true end--Global disable.
distance = distance or 43
if rangeCache[cidOrGuid] and (GetTime() - (rangeUpdated[cidOrGuid] or 0)) < 2 then -- return same range within 2 sec call
if rangeCache[cidOrGuid] > distance then
return false
else
return true
end
else
cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
local uId
local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid, onlyBoss)
if mobuId then--Have a valid mob unit ID
--First, use trust threat more than fallbackuId and see what we pull from it first.
--This is because for CheckTankDistance we want to know who is tanking it, not who it's targeting.
local unitId = (IsInRaid() and "raid") or "party"
for i = 0, GetNumGroupMembers() do
local id = (i == 0 and "target") or unitId..i
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
if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
if uId then break end
end
--Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
if fallbackuId and not uId then
uId = fallbackuId
end
end
if uId then--Now we have a valid uId
if UnitIsUnit(uId, "player") then return true end--If "player" is target, avoid doing any complicated stuff
if not UnitIsPlayer(uId) then
local inRange2, checkedRange = UnitInRange(uId)--43
if checkedRange then--checkedRange only returns true if api worked, so if we get false, true then we are not near npc
return inRange2 and true or false
else--Its probably a totem or just something we can't assess. Fall back to no filtering
return true
end
end
local inRange = DBM.RangeCheck:GetDistance("player", uId)--We check how far we are from the tank who has that boss
rangeCache[cidOrGuid] = inRange
rangeUpdated[cidOrGuid] = GetTime()
if inRange and (inRange > distance) then--You are not near the person tanking boss
return false
end
--Tank in range, return true.
return true
end
DBM:Debug("CheckTankDistance failed on uId for: "..cidOrGuid, 2)
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.
end
end
end
---------------------
-- Class Methods --
---------------------
do
--[[local specFlags ={
["Tank"] = true,
["Dps"] = true,
["Healer"] = true,
["Melee"] = true,--ANY melee, including tanks or healers that are 100% excempt from healer/ranged mechanics (like mistweaver monks)
["MeleeDps"] = true,
["Physical"] = true,
["Ranged"] = true,--ANY ranged, healer and dps included
["RangedDps"] = true,--Only ranged dps
["ManaUser"] = true,--Affected by things like mana drains, or mana detonation, etc
["SpellCaster"] = true,--Has channeled casts, can be interrupted/spell locked by roars, etc, include healers. Use CasterDps if dealing with reflect
["CasterDps"] = true,--Ranged dps that uses spells, relevant for spell reflect type abilities that only reflect spells but not ranged physical such as hunters
["RaidCooldown"] = true,
["RemovePoison"] = true,--from ally
["RemoveDisease"] = true,--from ally
["RemoveCurse"] = true,--from ally
["RemoveMagic"] = true,--from ally
["RemoveEnrage"] = true,--Can remove enemy enrage. returned in 8.x+!
["MagicDispeller"] = true,--from ENEMY, not debuffs on players. use "Healer" or "RemoveMagic" for ally magic dispels. ALL healers can do that on retail, and warlock Imps
["ImmunityDispeller"] = true,--Priest mass dispel or Warrior Shattering Throw (shadowlands)
["HasInterrupt"] = true,--Has an interrupt that is 24 seconds or less CD that is BASELINE (not a talent)
["HasImmunity"] = true,--Has an immunity that can prevent or remove a spell effect (not just one that reduces damage like turtle or dispursion)
}]]
local specRoleTable = {
[62] = { --Arcane Mage
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,
["MagicDispeller"] = true,
["HasInterrupt"] = true,
["HasImmunity"] = true,
["RemoveCurse"] = true,
},
[1449] = { --Initial Mage (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,
},
[65] = { --Holy Paladin
["Healer"] = true,
["Ranged"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["RaidCooldown"] = true,--Devotion Aura
["RemovePoison"] = true,
["RemoveDisease"] = true,
["RemoveMagic"] = true,
["HasImmunity"] = true,
},
[66] = { --Protection Paladin
["Tank"] = true,
["Melee"] = true,
["ManaUser"] = true,
["Physical"] = true,
["RemovePoison"] = true,
["RemoveDisease"] = true,
["HasInterrupt"] = true,
["HasImmunity"] = true,
},
[70] = { --Retribution Paladin
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["ManaUser"] = true,
["Physical"] = true,
["RemovePoison"] = true,
["RemoveDisease"] = true,
["HasInterrupt"] = true,
["HasImmunity"] = true,
},
[1451] = { --Initial Paladin (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Healer"] = true,
["Tank"] = true,
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["ManaUser"] = true,
["Physical"] = true,
["SpellCaster"] = true,
},
[71] = { --Arms Warrior
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["RaidCooldown"] = true,--Rallying Cry
["Physical"] = true,
["HasInterrupt"] = true,
["ImmunityDispeller"] = true,
},
[73] = { --Protection Warrior
["Tank"] = true,
["Melee"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
["RaidCooldown"] = true,--Rallying Cry
["ImmunityDispeller"] = true,
},
[1446] = { --Initial Warrior (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Tank"] = true,
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
},
[102] = { --Balance Druid
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,
["RemoveCurse"] = true,
["RemovePoison"] = true,
["RemoveEnrage"] = true,
},
[103] = { --Feral Druid
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["RemoveCurse"] = true,
["RemovePoison"] = true,
["HasInterrupt"] = true,
["RemoveEnrage"] = true,
},
[104] = { --Guardian Druid
["Tank"] = true,
["Melee"] = true,
["Physical"] = true,
["RemoveCurse"] = true,
["RemovePoison"] = true,
["HasInterrupt"] = true,
["RemoveEnrage"] = true,
},
[105] = { -- Restoration Druid
["Healer"] = true,
["Ranged"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["RaidCooldown"] = true,--Tranquility
["RemoveCurse"] = true,
["RemovePoison"] = true,
["RemoveEnrage"] = true,
["RemoveMagic"] = true,
},
[1447] = { -- Initial Druid (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Tank"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["Healer"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
},
[250] = { --Blood DK
["Tank"] = true,
["Melee"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
},
[251] = { --Frost DK
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
},
[1455] = { --Initial DK (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Tank"] = true,
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
},
[253] = { --Beastmaster Hunter
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
["MagicDispeller"] = true,
["RemoveEnrage"] = true,
},
[254] = { --Markmanship Hunter Hunter
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
["MagicDispeller"] = true,
["RemoveEnrage"] = true,
},
[255] = { --Survival Hunter (Legion+)
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
["MagicDispeller"] = true,
["RemoveEnrage"] = true,
},
[1448] = { --Initial Hunter (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["Physical"] = true,
},
[256] = { --Discipline Priest
["Healer"] = true,
["Ranged"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,--Iffy. Technically yes, but this can't be used to determine eligable target for dps only debuffs
["RaidCooldown"] = true,--Power Word: Barrier(Discipline) / Divine Hymn (Holy)
["RemoveDisease"] = true,
["RemoveMagic"] = true,
["MagicDispeller"] = true,
["ImmunityDispeller"] = true,
},
[258] = { --Shadow Priest
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,
["MagicDispeller"] = true,
["ImmunityDispeller"] = true,
["HasInterrupt"] = true,
["RemoveDisease"] = true,
},
[1452] = { --Initial Priest (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Dps"] = true,
["Healer"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,
},
[259] = { --Assassination Rogue
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
["HasImmunity"] = true,
},
[1453] = { --Initial Rogue (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
},
[262] = { --Elemental Shaman
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,
["RemoveCurse"] = true,
["MagicDispeller"] = true,
["HasInterrupt"] = true,
},
[263] = { --Enhancement Shaman
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["Physical"] = true,
["RemoveCurse"] = true,
["MagicDispeller"] = true,
["HasInterrupt"] = true,
},
[264] = { --Restoration Shaman
["Healer"] = true,
["Ranged"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["RaidCooldown"] = true,--Spirit Link Totem
["RemoveCurse"] = true,
["RemoveMagic"] = true,
["MagicDispeller"] = true,
["HasInterrupt"] = true,
},
[1444] = { --Initial Shaman (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Healer"] = true,
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["Physical"] = true,
},
[265] = { --Affliction Warlock
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["RemoveMagic"] = true,--Singe Magic (Imp)
["CasterDps"] = true,
},
[1454] = { --Initial Warlock (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Dps"] = true,
["Ranged"] = true,
["RangedDps"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["CasterDps"] = true,
},
[268] = { --Brewmaster Monk
["Tank"] = true,
["Melee"] = true,
["Physical"] = true,
["RemovePoison"] = true,
["RemoveDisease"] = true,
["HasInterrupt"] = true,
},
[269] = { --Windwalker Monk
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["RemovePoison"] = true,
["RemoveDisease"] = true,
["HasInterrupt"] = true,
},
[270] = { --Mistweaver Monk
["Healer"] = true,
["Melee"] = true,
["Ranged"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
["RaidCooldown"] = true,--Revival
["RemovePoison"] = true,
["RemoveDisease"] = true,
["RemoveMagic"] = true,
},
[1450] = { --Initial Monk (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Tank"] = true,
["Healer"] = true,
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["Ranged"] = true,
["ManaUser"] = true,
["SpellCaster"] = true,
},
[577] = { --Havok Demon Hunter
["Dps"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
["MagicDispeller"] = true,
},
[581] = { --Vengeance Demon Hunter
["Tank"] = true,
["Melee"] = true,
["Physical"] = true,
["HasInterrupt"] = true,
["MagicDispeller"] = true,
},
[1456] = { --Initial Demon Hunter (used in exiles reach tutorial mode). Treated as hybrid. Utility disabled because that'd require checking tutorial progress
["Tank"] = true,
["Melee"] = true,
["MeleeDps"] = true,
["Physical"] = true,
},
}
specRoleTable[63] = specRoleTable[62]--Frost Mage same as arcane
specRoleTable[64] = specRoleTable[62]--Fire Mage same as arcane
specRoleTable[72] = specRoleTable[71]--Fury Warrior same as Arms
specRoleTable[252] = specRoleTable[251]--Unholy DK same as frost
specRoleTable[257] = specRoleTable[256]--Holy Priest same as disc
specRoleTable[260] = specRoleTable[259]--Combat Rogue same as Assassination
specRoleTable[261] = specRoleTable[259]--Subtlety Rogue same as Assassination
specRoleTable[266] = specRoleTable[265]--Demonology Warlock same as Affliction
specRoleTable[267] = specRoleTable[265]--Destruction Warlock same as Affliction
--[[function bossModPrototype:GetRoleFlagValue(flag)
if not flag then return false end
local flags = {strsplit("|", flag)}
for i = 1, #flags do
local flagText = flags[i]
flagText = flagText:gsub("-", "")
if not specFlags[flagText] then
print("bad flag found : "..flagText)
end
end
self:GetRoleFlagValue2(flag)
end]]
--to check flag is correct, remove comment block specFlags table and GetRoleFlagValue function, change this to GetRoleFlagValue2
--disable flag check normally because double flag check comsumes more cpu on mod load.
function bossModPrototype:GetRoleFlagValue(flag)
if not flag then return false end
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
local flags = {strsplit("|", flag)}
for i = 1, #flags do
local flagText = flags[i]
if flagText:match("^-") then
flagText = flagText:gsub("-", "")
if not specRoleTable[currentSpecID][flagText] then
return true
end
else
if specRoleTable[currentSpecID][flagText] then
return true
end
end
end
return false
end
function bossModPrototype:IsMeleeDps(uId)
if uId then--This version includes ONLY melee dps
local role = UnitGroupRolesAssigned(uId)
if role == "HEALER" or role == "TANK" then--Auto filter healer/tank from dps check
return false
end
local _, class = UnitClass(uId)
if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" or class == "DEMONHUNTER" then
return true
end
--Inspect throttle exists, so have to do it this way
if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" or class == "MONK" then
local unitMaxPower = UnitPowerMax(uId)
local powerType = UnitPowerType(uId)
local altPowerType = UnitPower(uId, 8)--Additional check for balance druids shapeshifted into bear/cat but may still have > 0 lunar power
--Healers all have 50k mana at 60, dps have 10k mana, plus healers still filtered by role check too
--Tanks are already filtered out by role check
--Maelstrom and Lunar power filtered out because they'd also return less than 11000 power (they'd both be 100)
--feral druids, enhance shamans, windwalker monks, ret paladins should all be caught by less than 11000 power checks after filters
if powerType ~= 11 and powerType ~= 8 and altPowerType == 0 and unitMaxPower < 11000 then--Maelstrom and Lunar power filters
return true
end
end
return false
end
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
if specRoleTable[currentSpecID]["MeleeDps"] then
return true
else
return false
end
end
function bossModPrototype:IsMelee(uId, mechanical)--mechanical arg means the check is asking if boss mechanics consider them melee (even if they aren't, such as holy paladin/mistweaver monks)
if uId then--This version includes monk healers as melee and tanks as melee
local _, class = UnitClass(uId)
--In mechanical check, ALL paladins are melee so don't need anything fancy, as for rest of classes here, same deal
if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" or class == "MONK" or class == "DEMONHUNTER" or (mechanical and class == "PALADIN") then
return true
end
--Inspect throttle exists, so have to do it this way
if (class == "DRUID" or class == "SHAMAN" or class == "PALADIN") then
local powerType = UnitPowerType(uId)
local unitMaxPower = UnitPowerMax(uId)
local altPowerType = UnitPower(uId, 8)--Additional check for balance druids shapeshifted into bear/cat but may still have > 0 lunar power
--Hunters are now all flagged ranged because it's no longer possible to tell a survival hunter from marksman. neither will be using a pet and both have 100 focus.
--Druids without lunar poewr or 50k mana are either feral or guardian
--Shamans without maelstrom and 50k mana can only be enhancement
--Paladins without 50k mana can only be prot or ret
if powerType ~= 11 and powerType ~= 8 and altPowerType == 0 and unitMaxPower < 11000 then--Maelstrom and Lunar power filters
return true
end
end
return false
end
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
if specRoleTable[currentSpecID]["Melee"] then
return true
else
return false
end
end
function bossModPrototype:IsRanged()
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
if specRoleTable[currentSpecID]["Ranged"] then
return true
else
return false
end
end
function bossModPrototype:IsSpellCaster()
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
if specRoleTable[currentSpecID]["SpellCaster"] then
return true
else
return false
end
end
function bossModPrototype:IsMagicDispeller()
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
if specRoleTable[currentSpecID]["MagicDispeller"] then
return true
else
return false
end
end
end
function bossModPrototype:UnitClass(uId)
if uId then--Return unit requested
local _, class = UnitClass(uId)
return class
end
return playerClass--else return "player"
end
function bossModPrototype:IsTank()
--IsTanking already handles external calls, no need here.
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
if role == "TANK" then
return true
else
return false
end
end
function bossModPrototype:IsDps(uId)
if uId then--External unit call.
if UnitGroupRolesAssigned(uId) == "DAMAGER" then
return true
end
return false
end
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
if role == "DAMAGER" then
return true
else
return false
end
end
function bossModPrototype:IsHealer(uId)
if uId then--External unit call.
if UnitGroupRolesAssigned(uId) == "HEALER" then
return true
end
return false
end
if not currentSpecID then
DBM:SetCurrentSpecInfo()
end
local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
if role == "HEALER" then
return true
else
return false
end
end
function bossModPrototype:IsTanking(unit, boss, isName, onlyRequested, bossGUID, includeTarget)
if isName then--Passed combat log name, so pull unit ID
unit = DBM:GetRaidUnitId(unit)
end
if not unit then
DBM:Debug("IsTanking passed with invalid unit", 2)
return false
end
--Prefer threat target first
if boss then--Only checking one bossID as requested
--Check threat first
local tanking, status = UnitDetailedThreatSituation(unit, boss)
if tanking or (status == 3) then
return true
end
--Non threat fallback
if includeTarget and UnitExists(boss) then
local guid = UnitGUID(boss)
local _, targetuid = self:GetBossTarget(guid, true)
if UnitIsUnit(unit, targetuid) then
return true
end
end
else--Check all of them if one isn't defined
for i = 1, 5 do
local unitID = "boss"..i
local guid = UnitGUID(unitID)
--No GUID, any unit having threat returns true, GUID, only specific unit matching guid
if not bossGUID or (guid and guid == bossGUID) then
--Check threat first
local tanking, status = UnitDetailedThreatSituation(unit, unitID)
if tanking or (status == 3) then
return true
end
--Non threat fallback
if includeTarget and UnitExists(unitID) then
local _, targetuid = self:GetBossTarget(guid, true)
if UnitIsUnit(unit, targetuid) then
return true
end
end
end
end
--Check group targets if no boss unitIDs found and bossGUID passed.
--This allows IsTanking to be used in situations boss UnitIds don't exist
if bossGUID then
local groupType = (IsInRaid() and "raid") or "party"
for i = 0, GetNumGroupMembers() do
local unitID = (i == 0 and "target") or groupType..i.."target"
local guid = UnitGUID(unitID)
if guid and guid == bossGUID then
--Check threat first
local tanking, status = UnitDetailedThreatSituation(unit, unitID)
if tanking or (status == 3) then
return true
end
--Non threat fallback
if includeTarget and UnitExists(unitID) then
local _, targetuid = self:GetBossTarget(guid, true)
if UnitIsUnit(unit, targetuid) then
return true
end
end
end
end
end
end
if not onlyRequested then
--Use these as fallback if threat target not found
if GetPartyAssignment("MAINTANK", unit, 1) then
return true
end
if UnitGroupRolesAssigned(unit) == "TANK" then
return true
end
end
return false
end
function bossModPrototype:GetNumAliveTanks()
if not IsInGroup() then return 1 end--Solo raid, you're the "tank"
local count = 0
local uId = (IsInRaid() and "raid") or "party"
for i = 1, DBM:GetNumRealGroupMembers() do
if UnitGroupRolesAssigned(uId..i) == "TANK" and not UnitIsDeadOrGhost(uId..i) then
count = count + 1
end
end
return count
end
----------------------------
-- Boss Health Function --
----------------------------
function DBM:GetBossHP(cIdOrGUID, onlyHighest)
local uId = bossHealthuIdCache[cIdOrGUID] or "target"
local guid = UnitGUID(uId)
--Target or Cached (if already called with this cid or GUID before)
if (self:GetCIDFromGUID(guid) == cIdOrGUID or guid == cIdOrGUID) and UnitHealthMax(uId) ~= 0 then
if bossHealth[cIdOrGUID] and (UnitHealth(uId) == 0 and not UnitIsDead(uId)) then return bossHealth[cIdOrGUID], uId, UnitName(uId) end--Return last non 0 value if value is 0, since it's last valid value we had.
local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
if not onlyHighest or onlyHighest and hp > (bossHealth[cIdOrGUID] or 0) then
bossHealth[cIdOrGUID] = hp
end
return hp, uId, UnitName(uId)
--Focus
elseif (self:GetCIDFromGUID(UnitGUID("focus")) == cIdOrGUID or UnitGUID("focus") == cIdOrGUID) and UnitHealthMax("focus") ~= 0 then
if bossHealth[cIdOrGUID] and (UnitHealth("focus") == 0 and not UnitIsDead("focus")) then return bossHealth[cIdOrGUID], "focus", UnitName("focus") end--Return last non 0 value if value is 0, since it's last valid value we had.
local hp = UnitHealth("focus") / UnitHealthMax("focus") * 100
if not onlyHighest or onlyHighest and hp > (bossHealth[cIdOrGUID] or 0) then
bossHealth[cIdOrGUID] = hp
end
return hp, "focus", UnitName("focus")
else
--Boss UnitIds
for i = 1, 5 do
local unitID = "boss"..i
local bossguid = UnitGUID(unitID)
if (self:GetCIDFromGUID(bossguid) == cIdOrGUID or bossguid == cIdOrGUID) and UnitHealthMax(unitID) ~= 0 then
if bossHealth[cIdOrGUID] and (UnitHealth(unitID) == 0 and not UnitIsDead(unitID)) then return bossHealth[cIdOrGUID], unitID, UnitName(unitID) end--Return last non 0 value if value is 0, since it's last valid value we had.
local hp = UnitHealth(unitID) / UnitHealthMax(unitID) * 100
if not onlyHighest or onlyHighest and hp > (bossHealth[cIdOrGUID] or 0) then
bossHealth[cIdOrGUID] = hp
end
bossHealthuIdCache[cIdOrGUID] = unitID
return hp, unitID, UnitName(unitID)
end
end
--Scan raid/party target frames
local idType = (IsInRaid() and "raid") or "party"
for i = 0, GetNumGroupMembers() do
local unitId = ((i == 0) and "target") or idType..i.."target"
local bossguid = UnitGUID(unitId)
if (self:GetCIDFromGUID(bossguid) == cIdOrGUID or bossguid == cIdOrGUID) and UnitHealthMax(unitId) ~= 0 then
if bossHealth[cIdOrGUID] and (UnitHealth(unitId) == 0 and not UnitIsDead(unitId)) then return bossHealth[cIdOrGUID], unitId, UnitName(unitId) end--Return last non 0 value if value is 0, since it's last valid value we had.
local hp = UnitHealth(unitId) / UnitHealthMax(unitId) * 100
if not onlyHighest or onlyHighest and hp > (bossHealth[cIdOrGUID] or 0) then
bossHealth[cIdOrGUID] = hp
end
bossHealthuIdCache[cIdOrGUID] = unitId
return hp, unitId, UnitName(unitId)
end
end
end
return nil
end
function DBM:GetBossHPByUnitID(uId)
if UnitHealthMax(uId) ~= 0 then
local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
bossHealth[uId] = hp
return hp, uId, UnitName(uId)
end
return nil
end
function bossModPrototype:SetMainBossID(cid)
self.mainBoss = cid
end
function bossModPrototype:SetBossHPInfoToHighest(numBoss)
if numBoss ~= false then
self.numBoss = numBoss or (self.multiMobPullDetection and #self.multiMobPullDetection)
end
self.highesthealth = true
end
function bossModPrototype:GetHighestBossHealth()
local hp
if not self.multiMobPullDetection or self.mainBoss then
hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
if hp and (hp > 100 or hp <= 0) then
hp = nil
end
else
for _, mob in ipairs(self.multiMobPullDetection) do
if (bossHealth[mob] or 0) > (hp or 0) and (bossHealth[mob] or 0) < 100 then-- ignore full health.
hp = bossHealth[mob]
end
end
end
return hp
end
function bossModPrototype:GetLowestBossHealth()
local hp
if not self.multiMobPullDetection or self.mainBoss then
hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
if hp and (hp > 100 or hp <= 0) then
hp = nil
end
else
for _, mob in ipairs(self.multiMobPullDetection) do
if (bossHealth[mob] or 100) < (hp or 100) and (bossHealth[mob] or 100) > 0 then-- ignore zero health.
hp = bossHealth[mob]
end
end
end
return hp
end
bossModPrototype.GetBossHP = DBM.GetBossHP
-----------------------
-- Announce Object --
-----------------------
do
local frame = CreateFrame("Frame", "DBMWarning", UIParent)
local font1u = CreateFrame("Frame", "DBMWarning1Updater", UIParent)
local font2u = CreateFrame("Frame", "DBMWarning2Updater", UIParent)
local font3u = CreateFrame("Frame", "DBMWarning3Updater", UIParent)
local font1 = frame:CreateFontString("DBMWarning1", "OVERLAY", "GameFontNormal")
font1:SetWidth(1024)
font1:SetHeight(0)
font1:SetPoint("TOP", 0, 0)
local font2 = frame:CreateFontString("DBMWarning2", "OVERLAY", "GameFontNormal")
font2:SetWidth(1024)
font2:SetHeight(0)
font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
local font3 = frame:CreateFontString("DBMWarning3", "OVERLAY", "GameFontNormal")
font3:SetWidth(1024)
font3:SetHeight(0)
font3:SetPoint("TOP", font2, "BOTTOM", 0, 0)
frame:SetMovable(1)
frame:SetWidth(1)
frame:SetHeight(1)
frame:SetFrameStrata("HIGH")
frame:SetClampedToScreen()
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 300)
font1u:Hide()
font2u:Hide()
font3u:Hide()
local font1elapsed, font2elapsed, font3elapsed
local function fontHide1()
local duration = DBM.Options.WarningDuration2
if font1elapsed > duration * 1.3 then
font1u:Hide()
font1:Hide()
if frame.font1ticker then
frame.font1ticker:Cancel()
frame.font1ticker = nil
end
elseif font1elapsed > duration then
font1elapsed = font1elapsed + 0.05
local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
font1:SetAlpha(alpha > 0 and alpha or 0)
else
font1elapsed = font1elapsed + 0.05
font1:SetAlpha(1)
end
end
local function fontHide2()
local duration = DBM.Options.WarningDuration2
if font2elapsed > duration * 1.3 then
font2u:Hide()
font2:Hide()
if frame.font2ticker then
frame.font2ticker:Cancel()
frame.font2ticker = nil
end
elseif font2elapsed > duration then
font2elapsed = font2elapsed + 0.05
local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
font2:SetAlpha(alpha > 0 and alpha or 0)
else
font2elapsed = font2elapsed + 0.05
font2:SetAlpha(1)
end
end
local function fontHide3()
local duration = DBM.Options.WarningDuration2
if font3elapsed > duration * 1.3 then
font3u:Hide()
font3:Hide()
if frame.font3ticker then
frame.font3ticker:Cancel()
frame.font3ticker = nil
end
elseif font3elapsed > duration then
font3elapsed = font3elapsed + 0.05
local alpha = 1 - (font3elapsed - duration) / (duration * 0.3)
font3:SetAlpha(alpha > 0 and alpha or 0)
else
font3elapsed = font3elapsed + 0.05
font3:SetAlpha(1)
end
end
font1u:SetScript("OnUpdate", function(self)
local diff = GetTime() - font1.lastUpdate
local origSize = DBM.Options.WarningFontSize
if diff > 0.4 then
font1:SetTextHeight(origSize)
self:Hide()
elseif diff > 0.2 then
font1:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
else
font1:SetTextHeight(origSize * (1 + diff * 2.5))
end
end)
font2u:SetScript("OnUpdate", function(self)
local diff = GetTime() - font2.lastUpdate
local origSize = DBM.Options.WarningFontSize
if diff > 0.4 then
font2:SetTextHeight(origSize)
self:Hide()
elseif diff > 0.2 then
font2:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
else
font2:SetTextHeight(origSize * (1 + diff * 2.5))
end
end)
font3u:SetScript("OnUpdate", function(self)
local diff = GetTime() - font3.lastUpdate
local origSize = DBM.Options.WarningFontSize
if diff > 0.4 then
font3:SetTextHeight(origSize)
self:Hide()
elseif diff > 0.2 then
font3:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
else
font3:SetTextHeight(origSize * (1 + diff * 2.5))
end
end)
function DBM:UpdateWarningOptions()
frame:ClearAllPoints()
frame:SetPoint(self.Options.WarningPoint, UIParent, self.Options.WarningPoint, self.Options.WarningX, self.Options.WarningY)
local font = self.Options.WarningFont == "standardFont" and standardFont or self.Options.WarningFont
font1:SetFont(font, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
font2:SetFont(font, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
font3:SetFont(font, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
if self.Options.WarningFontShadow then
font1:SetShadowOffset(1, -1)
font2:SetShadowOffset(1, -1)
font3:SetShadowOffset(1, -1)
else
font1:SetShadowOffset(0, 0)
font2:SetShadowOffset(0, 0)
font3:SetShadowOffset(0, 0)
end
end
function DBM:AddWarning(text, force)
local added = false
if not frame.font1ticker then
font1elapsed = 0
font1.lastUpdate = GetTime()
font1:SetText(text)
font1:Show()
font1u:Show()
added = true
frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
elseif not frame.font2ticker then
font2elapsed = 0
font2.lastUpdate = GetTime()
font2:SetText(text)
font2:Show()
font2u:Show()
added = true
frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
elseif not frame.font3ticker or force then
font3elapsed = 0
font3.lastUpdate = GetTime()
font3:SetText(text)
font3:Show()
font3u:Show()
fontHide3()
added = true
frame.font3ticker = frame.font3ticker or C_TimerNewTicker(0.05, fontHide3)
end
if not added then
local prevText1 = font2:GetText()
local prevText2 = font3:GetText()
font1:SetText(prevText1)
font1elapsed = font2elapsed
font2:SetText(prevText2)
font2elapsed = font3elapsed
self:AddWarning(text, true)
end
end
do
local anchorFrame
local function moveEnd(self)
anchorFrame:Hide()
if anchorFrame.ticker then
anchorFrame.ticker:Cancel()
anchorFrame.ticker = nil
end
font1elapsed = self.Options.WarningDuration2
font2elapsed = self.Options.WarningDuration2
font3elapsed = self.Options.WarningDuration2
frame:SetFrameStrata("HIGH")
self:Unschedule(moveEnd)
DBT:CancelBar(L.MOVE_WARNING_BAR)
end
function DBM:MoveWarning()
if not anchorFrame then
anchorFrame = CreateFrame("Frame", nil, frame)
anchorFrame:SetWidth(32)
anchorFrame:SetHeight(32)
anchorFrame:EnableMouse(true)
anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
anchorFrame:RegisterForDrag("LeftButton")
anchorFrame:SetClampedToScreen()
anchorFrame:Hide()
local texture = anchorFrame:CreateTexture()
texture:SetTexture("Interface\\Addons\\DBM-Core\\textures\\dot.blp")
texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
texture:SetWidth(32)
texture:SetHeight(32)
anchorFrame:SetScript("OnDragStart", function()
frame:StartMoving()
self:Unschedule(moveEnd)
DBT:CancelBar(L.MOVE_WARNING_BAR)
end)
anchorFrame:SetScript("OnDragStop", function()
frame:StopMovingOrSizing()
local point, _, _, xOfs, yOfs = frame:GetPoint(1)
self.Options.WarningPoint = point
self.Options.WarningX = xOfs
self.Options.WarningY = yOfs
self:Schedule(15, moveEnd, self)
DBT:CreateBar(15, L.MOVE_WARNING_BAR, 237538)
end)
end
if anchorFrame:IsShown() then
moveEnd(self)
else
anchorFrame:Show()
anchorFrame.ticker = anchorFrame.ticker or C_TimerNewTicker(5, function() self:AddWarning(L.MOVE_WARNING_MESSAGE) end)
self:AddWarning(L.MOVE_WARNING_MESSAGE)
self:Schedule(15, moveEnd, self)
DBT:CreateBar(15, L.MOVE_WARNING_BAR, 237538)
frame:Show()
frame:SetFrameStrata("TOOLTIP")
frame:SetAlpha(1)
end
end
end
local textureCode = " |T%s:12:12|t "
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?
local announcePrototype = {}
local mt = {__index = announcePrototype}
-- TODO: is there a good reason that this is a weak table?
local cachedColorFunctions = setmetatable({}, {__mode = "kv"})
local function setText(announceType, spellId, castTime, preWarnTime)
local spellName
if type(spellId) == "string" and spellId:match("ej%d+") then
spellId = string.sub(spellId, 3)
spellName = DBM:EJ_GetSectionInfo(spellId) or L.UNKNOWN
else
spellName = (spellId or 0) >= 6 and DBM:GetSpellInfo(spellId) or L.UNKNOWN
end
local text
if announceType == "cast" then
local spellHaste = select(4, DBM:GetSpellInfo(10059)) / 10000 -- 10059 = Stormwind Portal, should have 10000 ms cast time
local timer = (select(4, DBM:GetSpellInfo(spellId)) or 1000) / spellHaste
text = L.AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, castTime or (timer / 1000))
elseif announceType == "prewarn" then
if type(preWarnTime) == "string" then
text = L.AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, preWarnTime)
else
text = L.AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, L.SEC_FMT:format(tostring(preWarnTime or 5)))
end
elseif announceType == "stage" or announceType == "prestage" then
text = L.AUTO_ANNOUNCE_TEXTS[announceType]:format(tostring(spellId))
elseif announceType == "stagechange" then
text = L.AUTO_ANNOUNCE_TEXTS.spell
else
text = L.AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName)
end
return text, spellName
end
-- TODO: this function is an abomination, it needs to be rewritten. Also: check if these work-arounds are still necessary
function announcePrototype:Show(...) -- todo: reduce amount of unneeded strings
if not self.option or self.mod.Options[self.option] then
if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
if DBM.Options.DontShowTargetAnnouncements and (self.announceType == "target" or self.announceType == "targetcount") and not self.noFilter then return end--don't show announces that are generic target announces
local argTable = {...}
local colorCode = ("|cff%.2x%.2x%.2x"):format(self.color.r * 255, self.color.g * 255, self.color.b * 255)
if #self.combinedtext > 0 then
--Throttle spam.
if DBM.Options.WarningAlphabetical then
tsort(self.combinedtext)
end
local combinedText = tconcat(self.combinedtext, "<, >")
if self.combinedcount == 1 then
combinedText = combinedText.." "..L.GENERIC_WARNING_OTHERS
elseif self.combinedcount > 1 then
combinedText = combinedText.." "..L.GENERIC_WARNING_OTHERS2:format(self.combinedcount)
end
--Process
for i = 1, #argTable do
if type(argTable[i]) == "string" then
argTable[i] = combinedText
end
end
end
local message = pformat(self.text, unpack(argTable))
local text = ("%s%s%s|r%s"):format(
(DBM.Options.WarningIconLeft and self.icon and textureCode:format(self.icon)) or "",
colorCode,
message,
(DBM.Options.WarningIconRight and self.icon and textureCode:format(self.icon)) or ""
)
self.combinedcount = 0
self.combinedtext = {}
if not cachedColorFunctions[self.color] then
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)
cachedColorFunctions[color] = function(cap)
cap = cap:sub(2, -2)
local noStrip = cap:match("noStrip ")
if not noStrip then
local name = cap
local playerClass, playerIcon = DBM:GetRaidClass(name)
if playerClass ~= "UNKNOWN" then
cap = DBM:GetShortServerName(cap)--Only run realm strip function if class color was valid (IE it's an actual playername)
end
local playerColor = RAID_CLASS_COLORS[playerClass] or color
if playerColor then
if playerIcon > 0 then
cap = ("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_%d:0|t"):format(playerIcon) .. ("|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)
else
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)
end
end
else
cap = cap:sub(9)
end
return cap
end
end
text = text:gsub(">.-<", cachedColorFunctions[self.color])
DBM:AddWarning(text)
if DBM.Options.ShowWarningsInChat then
if not DBM.Options.WarningIconChat then
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?
end
self.mod:AddMsg(text, nil)
end
if self.sound > 0 then
if self.sound > 1 and DBM.Options.ChosenVoicePack ~= "None" and not voiceSessionDisabled and self.sound <= SWFilterDisabed then return end
if not self.option or self.mod.Options[self.option.."SWSound"] ~= "None" then
DBM:PlaySoundFile(DBM.Options.RaidWarningSound, nil, true)--Validate true
end
end
--Message: Full message text
--Icon: Texture path/id for icon
--Type: Announce type
----Types: you, target, targetsource, spell, ends, endtarget, fades, adds, count, stack, cast, soon, sooncount, prewarn, bait, stage, stagechange, prestage, moveto
------Personal/Role (Applies to you, or your job): you, stack, bait, moveto, fades
------General Target Messages (informative, doesn't usually apply to you): target, targetsource
------Fight Changes (Stages, adds, boss buff/debuff, etc): stage, stagechange, prestage, adds, ends, endtarget
------General (can really apply to anything): spell, count, soon, sooncount, prewarn
--SpellId: Raw spell or encounter journal Id if available.
--Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
--boolean: Whether or not this warning is a special warning (higher priority).
fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false)
else
self.combinedcount = 0
self.combinedtext = {}
end
end
function announcePrototype:CombinedShow(delay, ...)
if self.option and not self.mod.Options[self.option] then return end
if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
if DBM.Options.DontShowTargetAnnouncements and (self.announceType == "target" or self.announceType == "targetcount") and not self.noFilter then return end--don't show announces that are generic target announces
local argTable = {...}
for i = 1, #argTable do
if type(argTable[i]) == "string" then
if #self.combinedtext < 7 then--Throttle spam. We may not need more than 6 targets..
if not checkEntry(self.combinedtext, argTable[i]) then
self.combinedtext[#self.combinedtext + 1] = argTable[i]
end
else
self.combinedcount = self.combinedcount + 1
end
end
end
unschedule(self.Show, self.mod, self)
schedule(delay or 0.5, self.Show, self.mod, self, ...)
end
function announcePrototype:Schedule(t, ...)
return schedule(t, self.Show, self.mod, self, ...)
end
function announcePrototype:Countdown(time, numAnnounces, ...)
scheduleCountdown(time, numAnnounces, self.Show, self.mod, self, ...)
end
function announcePrototype:Cancel(...)
return unschedule(self.Show, self.mod, self, ...)
end
function announcePrototype:Play(name, customPath)
local voice = DBM.Options.ChosenVoicePack
if voiceSessionDisabled or voice == "None" then return end
local always = DBM.Options.AlwaysPlayVoice
if DBM.Options.DontShowTargetAnnouncements and (self.announceType == "target" or self.announceType == "targetcount") and not self.noFilter and not always then return end--don't show announces that are generic target announces
if (not DBM.Options.DontShowBossAnnounces and (not self.option or self.mod.Options[self.option]) or always) and self.sound <= SWFilterDisabed then
--Filter tank specific voice alerts for non tanks if tank filter enabled
--But still allow AlwaysPlayVoice to play as well.
if (name == "changemt" or name == "tauntboss") and DBM.Options.FilterTankSpec and not self.mod:IsTank() and not always then return end
local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
DBM:PlaySoundFile(path)
end
end
function announcePrototype:ScheduleVoice(t, ...)
if voiceSessionDisabled or DBM.Options.ChosenVoicePack == "None" then return end
unschedule(self.Play, self.mod, self)--Allow ScheduleVoice to be used in same way as CombinedShow
return schedule(t, self.Play, self.mod, self, ...)
end
--Object Permits scheduling voice multiple times for same object
function announcePrototype:ScheduleVoiceOverLap(t, ...)
if voiceSessionDisabled or DBM.Options.ChosenVoicePack == "None" then return end
return schedule(t, self.Play, self.mod, self, ...)
end
function announcePrototype:CancelVoice(...)
if voiceSessionDisabled or DBM.Options.ChosenVoicePack == "None" then return end
return unschedule(self.Play, self.mod, self, ...)
end
-- old constructor (no auto-localize)
function bossModPrototype:NewAnnounce(text, color, icon, optionDefault, optionName, soundOption)
if not text then
error("NewAnnounce: you must provide announce text", 2)
return
end
if type(text) == "number" then
DBM:Debug("|cffff0000NewAnnounce: Non auto localized text cannot be numbers, fix this for |r"..text)
end
if type(optionName) == "number" then
DBM:Debug("|cffff0000NewAnnounce: Non auto localized optionNames cannot be numbers, fix this for |r"..text)
optionName = nil
end
if soundOption and type(soundOption) == "boolean" then
soundOption = 0--No Sound
end
local obj = setmetatable(
{
text = self.localization.warnings[text],
combinedtext = {},
combinedcount = 0,
color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
sound = soundOption or 1,
mod = self,
icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or tonumber(icon) or 136116,
},
mt
)
if optionName then
obj.option = optionName
self:AddBoolOption(obj.option, optionDefault, "announce")
elseif not (optionName == false) then
obj.option = text
self:AddBoolOption(obj.option, optionDefault, "announce")
end
tinsert(self.announces, obj)
return obj
end
-- new constructor (partially auto-localized warnings and options, yay!)
local function newAnnounce(self, announceType, spellId, color, icon, optionDefault, optionName, castTime, preWarnTime, soundOption, noFilter)
if not spellId then
error("newAnnounce: you must provide spellId", 2)
return
end
local optionVersion, alternateSpellId
if type(optionName) == "number" then
if optionName > 1000 then--Being used as spell name shortening
if DBM.Options.WarningShortText then
alternateSpellId = optionName
end
else--Being used as option version
optionVersion = optionName
end
optionName = nil
end
if soundOption and type(soundOption) == "boolean" then
soundOption = 0--No Sound
end
if type(spellId) == "string" and spellId:match("OptionVersion") then
print("newAnnounce for "..color.." is using OptionVersion hack. this is depricated")
return
end
local text, spellName = setText(announceType, alternateSpellId or spellId, castTime, preWarnTime)
icon = icon or spellId
local obj = setmetatable( -- todo: fix duplicate code
{
text = text,
combinedtext = {},
combinedcount = 0,
announceType = announceType,
color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
mod = self,
icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or tonumber(icon) or 136116,
sound = soundOption or 1,
type = announceType,
spellId = spellId,
spellName = spellName,
noFilter = noFilter,
castTime = castTime,
preWarnTime = preWarnTime,
},
mt
)
local catType = "announce"--Default to General announce
--Change if Personal or Other
if announceType == "target" or announceType == "targetcount" or announceType == "stack" then
catType = "announceother"
end
if optionName then
obj.option = optionName
self:AddBoolOption(obj.option, optionDefault, catType)
elseif not (optionName == false) then
obj.option = catType..spellId..announceType..(optionVersion or "")
self:AddBoolOption(obj.option, optionDefault, catType)
if noFilter and announceType == "target" then
self.localization.options[obj.option] = L.AUTO_ANNOUNCE_OPTIONS["targetNF"]:format(spellId)
else
self.localization.options[obj.option] = L.AUTO_ANNOUNCE_OPTIONS[announceType]:format(spellId)
end
end
tinsert(self.announces, obj)
return obj
end
function bossModPrototype:NewYouAnnounce(spellId, color, ...)
return newAnnounce(self, "you", spellId, color or 1, ...)
end
function bossModPrototype:NewTargetNoFilterAnnounce(spellId, color, icon, optionDefault, optionName, castTime, preWarnTime, soundOption, noFilter)
return newAnnounce(self, "target", spellId, color or 3, icon, optionDefault, optionName, castTime, preWarnTime, soundOption, true)
end
function bossModPrototype:NewTargetAnnounce(spellId, color, ...)
return newAnnounce(self, "target", spellId, color or 3, ...)
end
function bossModPrototype:NewTargetSourceAnnounce(spellId, color, ...)
return newAnnounce(self, "targetsource", spellId, color or 3, ...)
end
function bossModPrototype:NewTargetCountAnnounce(spellId, color, ...)
return newAnnounce(self, "targetcount", spellId, color or 3, ...)
end
function bossModPrototype:NewSpellAnnounce(spellId, color, ...)
return newAnnounce(self, "spell", spellId, color or 2, ...)
end
function bossModPrototype:NewEndAnnounce(spellId, color, ...)
return newAnnounce(self, "ends", spellId, color or 2, ...)
end
function bossModPrototype:NewEndTargetAnnounce(spellId, color, ...)
return newAnnounce(self, "endtarget", spellId, color or 2, ...)
end
function bossModPrototype:NewFadesAnnounce(spellId, color, ...)
return newAnnounce(self, "fades", spellId, color or 2, ...)
end
function bossModPrototype:NewAddsLeftAnnounce(spellId, color, ...)
return newAnnounce(self, "adds", spellId, color or 3, ...)
end
function bossModPrototype:NewCountAnnounce(spellId, color, ...)
return newAnnounce(self, "count", spellId, color or 2, ...)
end
function bossModPrototype:NewStackAnnounce(spellId, color, ...)
return newAnnounce(self, "stack", spellId, color or 2, ...)
end
function bossModPrototype:NewCastAnnounce(spellId, color, castTime, icon, optionDefault, optionName, noArg, soundOption)
return newAnnounce(self, "cast", spellId, color or 3, icon, optionDefault, optionName, castTime, nil, soundOption)
end
function bossModPrototype:NewSoonAnnounce(spellId, color, ...)
return newAnnounce(self, "soon", spellId, color or 2, ...)
end
function bossModPrototype:NewSoonCountAnnounce(spellId, color, ...)
return newAnnounce(self, "sooncount", spellId, color or 2, ...)
end
--This object disables sounds, it's almost always used in combation with a countdown timer. Even if not a countdown, its a text only spam not a sound spam
function bossModPrototype:NewCountdownAnnounce(spellId, color, icon, optionDefault, optionName, castTime, preWarnTime, soundOption, noFilter)
return newAnnounce(self, "countdown", spellId, color or 4, icon, optionDefault, optionName, castTime, preWarnTime, 0, noFilter)
end
function bossModPrototype:NewPreWarnAnnounce(spellId, time, color, icon, optionDefault, optionName, noArg, soundOption)
return newAnnounce(self, "prewarn", spellId, color or 2, icon, optionDefault, optionName, nil, time, soundOption)
end
function bossModPrototype:NewBaitAnnounce(spellId, color, ...)
return newAnnounce(self, "bait", spellId, color or 3, ...)
end
function bossModPrototype:NewPhaseAnnounce(stage, color, icon, ...)
return newAnnounce(self, "stage", stage, color or 2, icon or "136116", ...)
end
function bossModPrototype:NewPhaseChangeAnnounce(color, icon, ...)
return newAnnounce(self, "stagechange", 0, color or 2, icon or "136116", ...)
end
function bossModPrototype:NewPrePhaseAnnounce(stage, color, icon, ...)
return newAnnounce(self, "prestage", stage, color or 2, icon or "136116", ...)
end
function bossModPrototype:NewMoveToAnnounce(spellId, color, ...)
return newAnnounce(self, "moveto", spellId, color or 3, ...)
end
end
--------------------
-- Yell Object --
--------------------
do
local voidForm = GetSpellInfo(194249)
local yellPrototype = {}
local mt = { __index = yellPrototype }
local function newYell(self, yellType, spellId, yellText, optionDefault, optionName, chatType)
if not spellId and not yellText then
error("NewYell: you must provide either spellId or yellText", 2)
return
end
if type(spellId) == "string" and spellId:match("OptionVersion") then
print("newYell for: "..yellText.." is using OptionVersion hack. This is depricated")
return
end
local optionVersion
if type(optionName) == "number" then
optionVersion = optionName
optionName = nil
end
local displayText
if not yellText then
if type(spellId) == "string" and spellId:match("ej%d+") then
displayText = L.AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:EJ_GetSectionInfo(string.sub(spellId, 3)) or L.UNKNOWN)
else
displayText = L.AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:GetSpellInfo(spellId) or L.UNKNOWN)
end
end
--Passed spellid as yellText.
--Auto localize spelltext using yellText instead
if yellText and type(yellText) == "number" then
displayText = L.AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:GetSpellInfo(yellText) or L.UNKNOWN)
end
local obj = setmetatable(
{
spellId = spellId,
text = displayText or yellText,
mod = self,
chatType = chatType,
yellType = yellType
},
mt
)
if optionName then
obj.option = optionName
self:AddBoolOption(obj.option, optionDefault, "yell")
elseif not (optionName == false) then
obj.option = "Yell"..(spellId or yellText)..(yellType ~= "yell" and yellType or "")..(optionVersion or "")
self:AddBoolOption(obj.option, optionDefault, "yell")
self.localization.options[obj.option] = L.AUTO_YELL_OPTION_TEXT[yellType]:format(spellId)
end
return obj
end
--Standard "Yell" object that will use SAY/YELL based on what's defined in the object (Defaulting to SAY if nil)
--I realize object being :Yell is counter intuitive to default being "SAY" but for many years the default was YELL and it's too many years of mods to change now
function yellPrototype:Yell(...)
if DBM.Options.DontSendYells or self.yellType and self.yellType == "position" and DBM:UnitBuff("player", voidForm) and DBM.Options.FilterVoidFormSay then return end
if not self.option or self.mod.Options[self.option] then
SendChatMessage(pformat(self.text, ...), self.chatType or "SAY")
end
end
yellPrototype.Show = yellPrototype.Yell
--Force override to use say message, even when object defines "YELL"
function yellPrototype:Say(...)
if DBM.Options.DontSendYells or self.yellType and self.yellType == "position" and DBM:UnitBuff("player", voidForm) and DBM.Options.FilterVoidFormSay then return end
if not self.option or self.mod.Options[self.option] then
SendChatMessage(pformat(self.text, ...), "SAY")
end
end
function yellPrototype:Schedule(t, ...)
return schedule(t, self.Yell, self.mod, self, ...)
end
--Standard schedule object to schedule a say/yell based on what's defined in object
function yellPrototype:Countdown(time, numAnnounces, ...)
if time > 60 then--It's a spellID not a time
local _, _, _, _, _, expireTime = DBM:UnitDebuff("player", time)
if expireTime then
local remaining = expireTime-GetTime()
scheduleCountdown(remaining, numAnnounces, self.Yell, self.mod, self, ...)
end
else
scheduleCountdown(time, numAnnounces, self.Yell, self.mod, self, ...)
end
end
--Scheduled Force override to use SAY message, even when object defines "YELL"
function yellPrototype:CountdownSay(time, numAnnounces, ...)
if time > 60 then--It's a spellID not a time
local _, _, _, _, _, expireTime = DBM:UnitDebuff("player", time)
if expireTime then
local remaining = expireTime-GetTime()
scheduleCountdown(remaining, numAnnounces, self.Yell, self.mod, self, ...)
end
else
scheduleCountdown(time, numAnnounces, self.Say, self.mod, self, ...)
end
end
function yellPrototype:Repeat(...)
scheduleRepeat(time, self.Yell, self.spellId, self.mod, self, ...)
end
function yellPrototype:Cancel(...)
return unschedule(self.Yell, self.mod, self, ...)
end
function bossModPrototype:NewYell(...)
return newYell(self, "yell", ...)
end
function bossModPrototype:NewShortYell(...)
return newYell(self, "shortyell", ...)
end
function bossModPrototype:NewCountYell(...)
return newYell(self, "count", ...)
end
function bossModPrototype:NewFadesYell(...)
return newYell(self, "fade", ...)
end
function bossModPrototype:NewShortFadesYell(...)
return newYell(self, "shortfade", ...)
end
function bossModPrototype:NewIconFadesYell(...)
return newYell(self, "iconfade", ...)
end
function bossModPrototype:NewPosYell(...)
return newYell(self, "position", ...)
end
function bossModPrototype:NewShortPosYell(...)
return newYell(self, "shortposition", ...)
end
function bossModPrototype:NewComboYell(...)
return newYell(self, "combo", ...)
end
function bossModPrototype:NewPlayerRepeatYell(...)
return newYell(self, "repeatplayer", ...)
end
function bossModPrototype:NewIconRepeatYell(...)
return newYell(self, "repeaticon", ...)
end
end
------------------------------
-- Special Warning Object --
------------------------------
do
local frame = CreateFrame("Frame", "DBMSpecialWarning", UIParent)
local font1 = frame:CreateFontString("DBMSpecialWarning1", "OVERLAY", "ZoneTextFont")
font1:SetWidth(1024)
font1:SetHeight(0)
font1:SetPoint("TOP", 0, 0)
local font2 = frame:CreateFontString("DBMSpecialWarning2", "OVERLAY", "ZoneTextFont")
font2:SetWidth(1024)
font2:SetHeight(0)
font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
frame:SetMovable(1)
frame:SetWidth(1)
frame:SetHeight(1)
frame:SetFrameStrata("HIGH")
frame:SetClampedToScreen()
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
local font1elapsed, font2elapsed, moving
local function fontHide1()
local duration = DBM.Options.SpecialWarningDuration2
if font1elapsed > duration * 1.3 then
font1:Hide()
if frame.font1ticker then
frame.font1ticker:Cancel()
frame.font1ticker = nil
end
elseif font1elapsed > duration then
font1elapsed = font1elapsed + 0.05
local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
font1:SetAlpha(alpha > 0 and alpha or 0)
else
font1elapsed = font1elapsed + 0.05
font1:SetAlpha(1)
end
end
local function fontHide2()
local duration = DBM.Options.SpecialWarningDuration2
if font2elapsed > duration * 1.3 then
font2:Hide()
if frame.font2ticker then
frame.font2ticker:Cancel()
frame.font2ticker = nil
end
elseif font2elapsed > duration then
font2elapsed = font2elapsed + 0.05
local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
font2:SetAlpha(alpha > 0 and alpha or 0)
else
font2elapsed = font2elapsed + 0.05
font2:SetAlpha(1)
end
end
function DBM:UpdateSpecialWarningOptions()
frame:ClearAllPoints()
local font = self.Options.SpecialWarningFont == "standardFont" and standardFont or self.Options.SpecialWarningFont
frame:SetPoint(self.Options.SpecialWarningPoint, UIParent, self.Options.SpecialWarningPoint, self.Options.SpecialWarningX, self.Options.SpecialWarningY)
font1:SetFont(font, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
font2:SetFont(font, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
font1:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
font2:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
if self.Options.SpecialWarningFontShadow then
font1:SetShadowOffset(1, -1)
font2:SetShadowOffset(1, -1)
else
font1:SetShadowOffset(0, 0)
font2:SetShadowOffset(0, 0)
end
end
function DBM:AddSpecialWarning(text, force)
local added = false
if not frame.font1ticker then
font1elapsed = 0
font1.lastUpdate = GetTime()
font1:SetText(text)
font1:Show()
added = true
frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
elseif not frame.font2ticker or force then
font2elapsed = 0
font2.lastUpdate = GetTime()
font2:SetText(text)
font2:Show()
added = true
frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
end
if not added then
local prevText1 = font2:GetText()
font1:SetText(prevText1)
font1elapsed = font2elapsed
self:AddSpecialWarning(text, true)
end
end
do
local anchorFrame
local function moveEnd(self)
moving = false
anchorFrame:Hide()
font1elapsed = self.Options.SpecialWarningDuration2
font2elapsed = self.Options.SpecialWarningDuration2
frame:SetFrameStrata("HIGH")
self:Unschedule(moveEnd)
DBT:CancelBar(L.MOVE_SPECIAL_WARNING_BAR)
end
function DBM:MoveSpecialWarning()
if not anchorFrame then
anchorFrame = CreateFrame("Frame", nil, frame)
anchorFrame:SetWidth(32)
anchorFrame:SetHeight(32)
anchorFrame:EnableMouse(true)
anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
anchorFrame:RegisterForDrag("LeftButton")
anchorFrame:SetClampedToScreen()
anchorFrame:Hide()
local texture = anchorFrame:CreateTexture()
texture:SetTexture("Interface\\Addons\\DBM-Core\\textures\\dot.blp")
texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
texture:SetWidth(32)
texture:SetHeight(32)
anchorFrame:SetScript("OnDragStart", function()
frame:StartMoving()
self:Unschedule(moveEnd)
DBT:CancelBar(L.MOVE_SPECIAL_WARNING_BAR)
end)
anchorFrame:SetScript("OnDragStop", function()
frame:StopMovingOrSizing()
local point, _, _, xOfs, yOfs = frame:GetPoint(1)
self.Options.SpecialWarningPoint = point
self.Options.SpecialWarningX = xOfs
self.Options.SpecialWarningY = yOfs
self:Schedule(15, moveEnd, self)
DBT:CreateBar(15, L.MOVE_SPECIAL_WARNING_BAR, 237538)
end)
end
if anchorFrame:IsShown() then
moveEnd(self)
else
moving = true
anchorFrame:Show()
self:AddSpecialWarning(L.MOVE_SPECIAL_WARNING_TEXT)
self:AddSpecialWarning(L.MOVE_SPECIAL_WARNING_TEXT)
self:Schedule(15, moveEnd, self)
DBT:CreateBar(15, L.MOVE_SPECIAL_WARNING_BAR, 237538)
frame:Show()
frame:SetFrameStrata("TOOLTIP")
frame:SetAlpha(1)
end
end
end
local specialWarningPrototype = {}
local mt = {__index = specialWarningPrototype}
local function classColoringFunction(cap)
cap = cap:sub(2, -2)
local noStrip = cap:match("noStrip ")
if not noStrip then
local name = cap
local playerClass, playerIcon = DBM:GetRaidClass(name)
if playerClass ~= "UNKNOWN" then
cap = DBM:GetShortServerName(cap)--Only run strip code on valid player classes
if DBM.Options.SWarnClassColor then
local playerColor = RAID_CLASS_COLORS[playerClass]
if playerColor then
if playerIcon > 0 then
cap = ("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_%d:0|t"):format(playerIcon) .. ("|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)
else
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)
end
end
end
end
else
cap = cap:sub(9)
end
return cap
end
local textureCode = " |T%s:12:12|t "
local function setText(announceType, spellId, stacks)
local text, spellName
if type(spellId) == "string" and spellId:match("ej%d+") then
spellName = DBM:EJ_GetSectionInfo(string.sub(spellId, 3)) or L.UNKNOWN
else
spellName = (spellId or 0) >= 6 and DBM:GetSpellInfo(spellId) or L.UNKNOWN
end
if announceType == "prewarn" then
if type(stacks) == "string" then
text = L.AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, stacks)
else
text = L.AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, L.SEC_FMT:format(tostring(stacks or 5)))
end
else
text = L.AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName)
end
return text, spellName
end
function specialWarningPrototype:Show(...)
--Check if option for this warning is even enabled
if (not self.option or self.mod.Options[self.option]) and not moving and frame then
--Now, check if all special warning filters are enabled to save cpu and abort immediately if true.
if DBM.Options.DontPlaySpecialWarningSound and DBM.Options.DontShowSpecialWarningFlash and DBM.Options.DontShowSpecialWarningText then return end
--Next, we check if trash mod warning and if so check the filter trash warning filter for trivial difficulties
if self.mod:IsEasyDungeon() and self.mod.isTrashMod and DBM.Options.FilterTrashWarnings2 then return end
--Lastly, we check if it's a tank warning and filter if not in tank spec. This is done because tank warnings on by default and handled fluidly by spec, not option setting
if self.announceType == "taunt" and DBM.Options.FilterTankSpec and not self.mod:IsTank() then return end--Don't tell non tanks to taunt, ever.
local argTable = {...}
-- add a default parameter for move away warnings
if self.announceType == "gtfo" then
if DBM:UnitBuff("player", 27827) then return end--Don't tell a priest in spirit of redemption form to GTFO, they can't, and they don't take damage from it anyhow
if #argTable == 0 then
argTable[1] = L.BAD
end
end
if #self.combinedtext > 0 then
--Throttle spam.
if DBM.Options.SWarningAlphabetical then
tsort(self.combinedtext)
end
local combinedText = tconcat(self.combinedtext, "<, >")
if self.combinedcount == 1 then
combinedText = combinedText.." "..L.GENERIC_WARNING_OTHERS
elseif self.combinedcount > 1 then
combinedText = combinedText.." "..L.GENERIC_WARNING_OTHERS2:format(self.combinedcount)
end
--Process
for i = 1, #argTable do
if type(argTable[i]) == "string" then
argTable[i] = combinedText
end
end
end
local message = pformat(self.text, unpack(argTable))
local text = ("%s%s%s"):format(
(DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or "",
message,
(DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or ""
)
local noteHasName = false
if self.option then
local noteText = self.mod.Options[self.option .. "SWNote"]
if noteText and type(noteText) == "string" and noteText ~= "" then--Filter false bool and empty strings
local count1 = self.announceType == "count" or self.announceType == "switchcount" or self.announceType == "targetcount"
local count2 = self.announceType == "interruptcount"
if count1 or count2 then--Counts support different note for EACH count
local noteCount
local notesTable = {string.split("/", noteText)}
if count1 then
noteCount = argTable[1]--Count should be first arg in table
elseif count2 then
noteCount = argTable[2]--Count should be second arg in table
end
if type(noteCount) == "string" then
--Probably a hypehnated double count like inferno slice or marked for death
local mainCount = string.split("-", noteCount)
noteCount = tonumber(mainCount)
end
noteText = notesTable[noteCount]
if noteText and type(noteText) == "string" and noteText ~= "" then--Refilter after string split to make sure a note for this count exists
local hasPlayerName = noteText:find(playerName)
if DBM.Options.SWarnNameInNote and hasPlayerName then
noteHasName = 5
end
--Terminate special warning, it's an interrupt count warning without player name and filter enabled
if count2 and DBM.Options.FilterInterruptNoteName and not hasPlayerName then return end
noteText = " ("..noteText..")"
text = text..noteText
end
else--Non count warnings will have one note, period
if DBM.Options.SWarnNameInNote and noteText:find(playerName) then
noteHasName = 5
end
if self.announceType and not self.announceType:find("switch") then
noteText = noteText:gsub(">.-<", classColoringFunction)--Class color note text before combining with warning text.
end
noteText = " ("..noteText..")"
text = text..noteText
end
end
end
--Text is disabled, suresss text from screen and chat frame
if not DBM.Options.DontShowSpecialWarningText then
--No stripping on switch warnings, ever. They will NEVER have player name, but often have adds with "-" in name
if self.announceType and not self.announceType:find("switch") then
text = text:gsub(">.-<", classColoringFunction)
end
DBM:AddSpecialWarning(text)
if DBM.Options.ShowSWarningsInChat then
local colorCode = ("|cff%.2x%.2x%.2x"):format(DBM.Options.SpecialWarningFontCol[1] * 255, DBM.Options.SpecialWarningFontCol[2] * 255, DBM.Options.SpecialWarningFontCol[3] * 255)
self.mod:AddMsg(colorCode.."["..L.MOVE_SPECIAL_WARNING_TEXT.."] "..text.."|r", nil)
end
end
self.combinedcount = 0
self.combinedtext = {}
if not UnitIsDeadOrGhost("player") and not DBM.Options.DontShowSpecialWarningFlash then
if noteHasName then
if DBM.Options.SpecialWarningFlash5 then--Not included in above if statement on purpose. we don't want to trigger else rule if noteHasName is true but SpecialWarningFlash5 is false
local repeatCount = DBM.Options.SpecialWarningFlashCount5 or 1
DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol5[1],DBM.Options.SpecialWarningFlashCol5[2], DBM.Options.SpecialWarningFlashCol5[3], DBM.Options.SpecialWarningFlashDura5, DBM.Options.SpecialWarningFlashAlph5, repeatCount-1)
end
else
local number = self.flash
if DBM.Options["SpecialWarningFlash"..number] then
local repeatCount = DBM.Options["SpecialWarningFlashCount"..number] or 1
local flashcolor = DBM.Options["SpecialWarningFlashCol"..number]
DBM.Flash:Show(flashcolor[1], flashcolor[2], flashcolor[3], DBM.Options["SpecialWarningFlashDura"..number], DBM.Options["SpecialWarningFlashAlph"..number], repeatCount-1)
end
end
end
--Text: Full message text
--Icon: Texture path/id for icon
--Type: Announce type
----Types: spell, ends, fades, soon, bait, dispel, interrupt, interruptcount, you, youcount, youpos, soakpos, target, targetcount, defensive, taunt, close, move, keepmove, stopmove,
----gtfo, dodge, dodgecount, dodgeloc, moveaway, moveawaycount, moveto, soak, jump, run, cast, lookaway, reflect, count, sooncount, stack, switch, switchcount, adds, addscustom, targetchange, prewarn
------General Target Messages (but since it's a special warning, it applies to you in some way): target, targetcount
------Fight Changes (Stages, adds, boss buff/debuff, etc): adds, addscustom, targetchange, switch, switchcount, ends
------General (can really apply to anything): spell, count, soon, sooncount, prewarn
------Personal/Role (Applies to you, or your job): Everything Else
--SpellId: Raw spell or encounter journal Id if available.
--Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
--boolean: Whether or not this warning is a special warning (higher priority).
fireEvent("DBM_Announce", text, self.icon, self.type, self.spellId, self.mod.id, true)
if self.sound and not DBM.Options.DontPlaySpecialWarningSound then
local soundId = self.option and self.mod.Options[self.option .. "SWSound"] or self.flash
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
if self.hasVoice and DBM.Options.ChosenVoicePack ~= "None" and not voiceSessionDisabled and self.hasVoice <= SWFilterDisabed and (type(soundId) == "number" and soundId < 5 and DBM.Options.VoiceOverSpecW2 == "DefaultOnly" or DBM.Options.VoiceOverSpecW2 == "All") then return end
if not self.option or self.mod.Options[self.option.."SWSound"] ~= "None" then
DBM:PlaySpecialWarningSound(soundId or 1)
end
end
else
self.combinedcount = 0
self.combinedtext = {}
end
end
function specialWarningPrototype:CombinedShow(delay, ...)
--Check if option for this warning is even enabled
if self.option and not self.mod.Options[self.option] then return end
--Now, check if all special warning filters are enabled to save cpu and abort immediately if true.
if DBM.Options.DontPlaySpecialWarningSound and DBM.Options.DontShowSpecialWarningFlash and DBM.Options.DontShowSpecialWarningText then return end
--Next, we check if trash mod warning and if so check the filter trash warning filter for trivial difficulties
if self.mod:IsEasyDungeon() and self.mod.isTrashMod and DBM.Options.FilterTrashWarnings2 then return end
local argTable = {...}
for i = 1, #argTable do
if type(argTable[i]) == "string" then
if #self.combinedtext < 6 then--Throttle spam. We may not need more than 5 targets..
if not checkEntry(self.combinedtext, argTable[i]) then
self.combinedtext[#self.combinedtext + 1] = argTable[i]
end
else
self.combinedcount = self.combinedcount + 1
end
end
end
unschedule(self.Show, self.mod, self)
schedule(delay or 0.5, self.Show, self.mod, self, ...)
end
function specialWarningPrototype:DelayedShow(delay, ...)
unschedule(self.Show, self.mod, self, ...)
schedule(delay or 0.5, self.Show, self.mod, self, ...)
end
function specialWarningPrototype:Schedule(t, ...)
return schedule(t, self.Show, self.mod, self, ...)
end
function specialWarningPrototype:Countdown(time, numAnnounces, ...)
scheduleCountdown(time, numAnnounces, self.Show, self.mod, self, ...)
end
function specialWarningPrototype:Cancel(t, ...)
return unschedule(self.Show, self.mod, self, ...)
end
function specialWarningPrototype:Play(name, customPath)
local always = DBM.Options.AlwaysPlayVoice
local voice = DBM.Options.ChosenVoicePack
if voiceSessionDisabled or voice == "None" then return end
if self.mod:IsEasyDungeon() and self.mod.isTrashMod and DBM.Options.FilterTrashWarnings2 then return end
if ((not self.option or self.mod.Options[self.option]) or always) and self.hasVoice <= SWFilterDisabed then
--Filter tank specific voice alerts for non tanks if tank filter enabled
--But still allow AlwaysPlayVoice to play as well.
if (name == "changemt" or name == "tauntboss") and DBM.Options.FilterTankSpec and not self.mod:IsTank() and not always then return end
local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
DBM:PlaySoundFile(path)
end
end
function specialWarningPrototype:ScheduleVoice(t, ...)
if voiceSessionDisabled or DBM.Options.ChosenVoicePack == "None" then return end
unschedule(self.Play, self.mod, self)--Allow ScheduleVoice to be used in same way as CombinedShow
return schedule(t, self.Play, self.mod, self, ...)
end
--Object Permits scheduling voice multiple times for same object
function specialWarningPrototype:ScheduleVoiceOverLap(t, ...)
if voiceSessionDisabled or DBM.Options.ChosenVoicePack == "None" then return end
return schedule(t, self.Play, self.mod, self, ...)
end
function specialWarningPrototype:CancelVoice(...)
if voiceSessionDisabled or DBM.Options.ChosenVoicePack == "None" then return end
return unschedule(self.Play, self.mod, self, ...)
end
function bossModPrototype:NewSpecialWarning(text, optionDefault, optionName, optionVersion, runSound, hasVoice, difficulty, texture)
if not text then
error("NewSpecialWarning: you must provide special warning text", 2)
return
end
if type(text) == "string" and text:match("OptionVersion") then
print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
return
end
if runSound == true then
runSound = 2
elseif not runSound then
runSound = 1
end
if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
hasVoice = 2
end
local seticon
if texture then
seticon = (type(texture) == "string" and texture:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(texture, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(texture, 3)))) or (type(texture) == "number" and GetSpellTexture(texture)) or nil
end
local obj = setmetatable(
{
text = self.localization.warnings[text],
combinedtext = {},
combinedcount = 0,
mod = self,
sound = runSound>0,
flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
hasVoice = hasVoice,
difficulty = difficulty,
icon = seticon,
},
mt
)
local optionId = optionName or optionName ~= false and text
if optionId then
obj.voiceOptionId = hasVoice and "Voice"..optionId or nil
obj.option = optionId..(optionVersion or "")
self:AddSpecialWarningOption(optionId, optionDefault, runSound, "announce")
end
tinsert(self.specwarns, obj)
return obj
end
local function newSpecialWarning(self, announceType, spellId, stacks, optionDefault, optionName, optionVersion, runSound, hasVoice, difficulty)
if not spellId then
error("newSpecialWarning: you must provide spellId", 2)
return
end
if runSound == true then
runSound = 2
elseif not runSound then
runSound = 1
end
if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
hasVoice = 2
end
local alternateSpellId
if type(optionName) == "number" then
if DBM.Options.SpecialWarningShortText then
alternateSpellId = optionName
end
optionName = nil
end
local text, spellName = setText(announceType, alternateSpellId or spellId, stacks)
local obj = setmetatable( -- todo: fix duplicate code
{
text = text,
combinedtext = {},
combinedcount = 0,
announceType = announceType,
mod = self,
sound = runSound>0,
flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
hasVoice = hasVoice,
difficulty = difficulty,
type = announceType,
spellId = spellId,
spellName = spellName,
stacks = stacks,
icon = (type(spellId) == "string" and spellId:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3)))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or nil
},
mt
)
if optionName then
obj.option = optionName
elseif not (optionName == false) then
local difficultyIcon = ""
if difficulty then
--1 LFR, 2 Normal, 3 Heroic, 4 Mythic
--Likely 1 and 2 will never be used, but being prototyped just in case
if difficulty == 3 then
difficultyIcon = "|TInterface\\EncounterJournal\\UI-EJ-Icons.blp:18:18:0:0:255:66:102:118:7:27|t"
elseif difficulty == 4 then
difficultyIcon = "|TInterface\\EncounterJournal\\UI-EJ-Icons.blp:18:18:0:0:255:66:133:153:40:58|t"
end
end
obj.option = "SpecWarn"..spellId..announceType..(optionVersion or "")
if announceType == "stack" then
self.localization.options[obj.option] = difficultyIcon..L.AUTO_SPEC_WARN_OPTIONS[announceType]:format(stacks or 3, spellId)
elseif announceType == "prewarn" then
self.localization.options[obj.option] = difficultyIcon..L.AUTO_SPEC_WARN_OPTIONS[announceType]:format(tostring(stacks or 5), spellId)
else
self.localization.options[obj.option] = difficultyIcon..L.AUTO_SPEC_WARN_OPTIONS[announceType]:format(spellId)
end
end
if obj.option then
local catType = "announce"--Default to General announce
--Directly affects another target (boss or player) that you need to know about
if announceType == "target" or announceType == "targetcount" or announceType == "close" or announceType == "reflect" then
catType = "announceother"
--Directly affects you
elseif announceType == "you" or announceType == "youcount" or announceType == "youpos" or announceType == "move" or announceType == "dodge" or announceType == "dodgecount" or announceType == "moveaway" or announceType == "moveawaycount" or announceType == "keepmove" or announceType == "stopmove" or announceType == "run" or announceType == "stack" or announceType == "moveto" or announceType == "soak" or announceType == "soakpos" then
catType = "announcepersonal"
--Things you have to do to fulfil your role
elseif announceType == "taunt" or announceType == "dispel" or announceType == "interrupt" or announceType == "interruptcount" or announceType == "switch" or announceType == "switchcount" then
catType = "announcerole"
end
self:AddSpecialWarningOption(obj.option, optionDefault, runSound, catType)
end
obj.voiceOptionId = hasVoice and "Voice"..spellId or nil
tinsert(self.specwarns, obj)
return obj
end
function bossModPrototype:NewSpecialWarningSpell(text, optionDefault, ...)
return newSpecialWarning(self, "spell", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningEnd(text, optionDefault, ...)
return newSpecialWarning(self, "ends", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningFades(text, optionDefault, ...)
return newSpecialWarning(self, "fades", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningSoon(text, optionDefault, ...)
return newSpecialWarning(self, "soon", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningBait(text, optionDefault, ...)
return newSpecialWarning(self, "bait", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningDispel(text, optionDefault, ...)
return newSpecialWarning(self, "dispel", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningInterrupt(text, optionDefault, ...)
return newSpecialWarning(self, "interrupt", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningInterruptCount(text, optionDefault, ...)
return newSpecialWarning(self, "interruptcount", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningYou(text, optionDefault, ...)
return newSpecialWarning(self, "you", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningYouCount(text, optionDefault, ...)
return newSpecialWarning(self, "youcount", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningYouPos(text, optionDefault, ...)
return newSpecialWarning(self, "youpos", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningSoakPos(text, optionDefault, ...)
return newSpecialWarning(self, "soakpos", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningTarget(text, optionDefault, ...)
return newSpecialWarning(self, "target", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningTargetCount(text, optionDefault, ...)
return newSpecialWarning(self, "targetcount", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningDefensive(text, optionDefault, ...)
return newSpecialWarning(self, "defensive", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningTaunt(text, optionDefault, ...)
return newSpecialWarning(self, "taunt", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningClose(text, optionDefault, ...)
return newSpecialWarning(self, "close", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningMove(text, optionDefault, ...)
return newSpecialWarning(self, "move", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningKeepMove(text, optionDefault, ...)
return newSpecialWarning(self, "keepmove", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningStopMove(text, optionDefault, ...)
return newSpecialWarning(self, "stopmove", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningGTFO(text, optionDefault, ...)
return newSpecialWarning(self, "gtfo", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningDodge(text, optionDefault, ...)
return newSpecialWarning(self, "dodge", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningDodgeCount(text, optionDefault, ...)
return newSpecialWarning(self, "dodgecount", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningDodgeLoc(text, optionDefault, ...)
return newSpecialWarning(self, "dodgeloc", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningMoveAway(text, optionDefault, ...)
return newSpecialWarning(self, "moveaway", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningMoveAwayCount(text, optionDefault, ...)
return newSpecialWarning(self, "moveawaycount", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningMoveTo(text, optionDefault, ...)
return newSpecialWarning(self, "moveto", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningSoak(text, optionDefault, ...)
return newSpecialWarning(self, "soak", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningJump(text, optionDefault, ...)
return newSpecialWarning(self, "jump", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningRun(text, optionDefault, optionName, optionVersion, runSound, ...)
return newSpecialWarning(self, "run", text, nil, optionDefault, optionName, optionVersion, runSound or 4, ...)
end
function bossModPrototype:NewSpecialWarningCast(text, optionDefault, ...)
return newSpecialWarning(self, "cast", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningLookAway(text, optionDefault, ...)
return newSpecialWarning(self, "lookaway", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningReflect(text, optionDefault, ...)
return newSpecialWarning(self, "reflect", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningCount(text, optionDefault, ...)
return newSpecialWarning(self, "count", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningSoonCount(text, optionDefault, ...)
return newSpecialWarning(self, "sooncount", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningStack(text, optionDefault, stacks, ...)
return newSpecialWarning(self, "stack", text, stacks, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningSwitch(text, optionDefault, ...)
return newSpecialWarning(self, "switch", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningSwitchCount(text, optionDefault, ...)
return newSpecialWarning(self, "switchcount", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningAdds(text, optionDefault, ...)
return newSpecialWarning(self, "adds", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningAddsCustom(text, optionDefault, ...)
return newSpecialWarning(self, "addscustom", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningTargetChange(text, optionDefault, ...)
return newSpecialWarning(self, "targetchange", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningPreWarn(text, optionDefault, time, ...)
return newSpecialWarning(self, "prewarn", text, time, optionDefault, ...)
end
function DBM:PlayCountSound(number, forceVoice, forcePath)
if number > 10 then return end
local voice
if forceVoice then--For options example
voice = forceVoice
else
voice = self.Options.CountdownVoice
end
local path
local maxCount = 5
if forcePath then
path = forcePath
else
for i = 1, #self.Counts do
if self.Counts[i].value == voice then
path = self.Counts[i].path
maxCount = self.Counts[i].max
break
end
end
end
if not path or (number > maxCount) then return end
self:PlaySoundFile(path..number..".ogg")
end
function DBM:RegisterCountSound(t, v, p, m)
--Prevent duplicate insert
for i = 1, #self.Counts do
if self.Counts[i].value == v then return end
end
--Insert into counts table.
if t and v and p and m then
tinsert(self.Counts, { text = t, value = v, path = p, max = m })
end
end
function DBM:CheckVoicePackVersion(value)
local activeVP = self.Options.ChosenVoicePack
--Check if voice pack out of date
if activeVP ~= "None" and activeVP == value then
if self.VoiceVersions[value] < 11 then--Version will be bumped when new voice packs released that contain new voices.
if self.Options.ShowReminders then
self:AddMsg(L.VOICE_PACK_OUTDATED)
end
SWFilterDisabed = self.VoiceVersions[value]--Set disable to version on current voice pack
else
SWFilterDisabed = 11
end
end
end
function DBM:PlaySpecialWarningSound(soundId, force)
local sound
if not force and self:IsTrivial() and self.Options.DontPlayTrivialSpecialWarningSound then
sound = self.Options.RaidWarningSound
else
sound = type(soundId) == "number" and self.Options["SpecialWarningSound" .. (soundId == 1 and "" or soundId)] or soundId or self.Options.SpecialWarningSound
end
self:PlaySoundFile(sound, nil, true)
end
local function testWarningEnd()
frame:SetFrameStrata("HIGH")
end
function DBM:ShowTestSpecialWarning(text, number, noSound, force)
if moving then
return
end
self:AddSpecialWarning(L.MOVE_SPECIAL_WARNING_TEXT)
frame:SetFrameStrata("TOOLTIP")
self:Unschedule(testWarningEnd)
self:Schedule(self.Options.SpecialWarningDuration2 * 1.3, testWarningEnd)
if number and not noSound then
self:PlaySpecialWarningSound(number, force)
end
if number and DBM.Options["SpecialWarningFlash"..number] then
if not force and self:IsTrivial() and self.Options.DontPlayTrivialSpecialWarningSound then return end--No flash if trivial
local flashColor = self.Options["SpecialWarningFlashCol"..number]
local repeatCount = self.Options["SpecialWarningFlashCount"..number] or 1
self.Flash:Show(flashColor[1], flashColor[2], flashColor[3], self.Options["SpecialWarningFlashDura"..number], self.Options["SpecialWarningFlashAlph"..number], repeatCount-1)
end
end
end
--------------------
-- Timer Object --
--------------------
do
local timerPrototype = {}
local mt = {__index = timerPrototype}
local countvoice1, countvoice2, countvoice3
local countvoice1max, countvoice2max, countvoice3max = 5, 5, 5
local countpath1, countpath2, countpath3
--Merged countdown object for timers with build-in countdown
function DBM:BuildVoiceCountdownCache()
countvoice1 = self.Options.CountdownVoice
countvoice2 = self.Options.CountdownVoice2
countvoice3 = self.Options.CountdownVoice3
for i = 1, #self.Counts do
local curVoice = self.Counts[i]
if curVoice.value == countvoice1 then
countpath1 = curVoice.path
countvoice1max = curVoice.max
end
if curVoice.value == countvoice2 then
countpath2 = curVoice.path
countvoice2max = curVoice.max
end
if curVoice.value == countvoice3 then
countpath3 = curVoice.path
countvoice3max = curVoice.max
end
end
end
local function playCountSound(timerId, path)
DBM:PlaySoundFile(path)
end
local function playCountdown(timerId, timer, voice, count)
if DBM.Options.DontPlayCountdowns then return end
timer = timer or 10
count = count or 5
voice = voice or 1
if timer <= count then count = floor(timer) end
if not countpath1 or not countpath2 or not countpath3 then
DBM:Debug("Voice cache not built at time of playCountdown. On fly caching.", 3)
DBM:BuildVoiceCountdownCache()
end
local maxCount, path
if type(voice) == "string" then
maxCount = 5--Safe to assume if it's not one of the built ins, it's likely heroes/OW, which has a max of 5
path = voice
elseif voice == 2 then
maxCount = countvoice2max or 10
path = countpath2 or "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\"
elseif voice == 3 then
maxCount = countvoice3max or 5
path = countpath3 or "Interface\\AddOns\\DBM-Core\\Sounds\\Smooth\\"
else
maxCount = countvoice1max or 10
path = countpath1 or "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\"
end
if not path then--Should not happen but apparently it does somehow
DBM:Debug("Voice path failed in countdownProtoType:Start.")
return
end
if count == 0 then--If a count of 0 is passed,then it's a "Countout" timer, not "Countdown"
for i = 1, timer do
if i < maxCount then
DBM:Schedule(i, playCountSound, timerId, path..i..".ogg")
end
end
else
for i = count, 1, -1 do
if i <= maxCount then
DBM:Schedule(timer-i, playCountSound, timerId, path..i..".ogg")
end
end
end
end
function timerPrototype:Start(timer, ...)
if DBM.Options.DontShowBossTimers then return end
if timer and type(timer) ~= "number" then
return self:Start(nil, timer, ...) -- first argument is optional!
end
if not self.option or self.mod.Options[self.option] then
if self.type and (self.type == "cdcount" or self.type == "nextcount") and not self.allowdouble then--remove previous timer.
for i = #self.startedTimers, 1, -1 do
if DBM.Options.BadTimerAlert or DBM.Options.DebugMode and DBM.Options.DebugLevel > 1 then
local bar = DBT:GetBar(self.startedTimers[i])
if bar then
local remaining = ("%.1f"):format(bar.timer)
local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
ttext = ttext.."("..self.id..")"
if bar.timer > 0.2 then
local phaseText = self.mod.vb.phase and " ("..SCENARIO_STAGE:format(self.mod.vb.phase)..")" or ""
if DBM.Options.BadTimerAlert and bar.timer > 1 then--If greater than 1 seconds off, report this out of debug mode to all users
DBM:AddMsg("Timer "..ttext..phaseText.. " refreshed before expired. Remaining time is : "..remaining..". Please report this bug")
fireEvent("DBM_Debug", "Timer "..ttext..phaseText.. " refreshed before expired. Remaining time is : "..remaining..". Please report this bug", 2)
else
DBM:Debug("Timer "..ttext..phaseText.. " refreshed before expired. Remaining time is : "..remaining, 2)
end
end
end
end
DBT:CancelBar(self.startedTimers[i])
fireEvent("DBM_TimerStop", self.startedTimers[i])
self.startedTimers[i] = nil
end
end
timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer
--AI timer api:
--Starting ai timer with (1) indicates it's a first timer after pull
--Starting timer with (2) or (3) indicates it's a stage 2 or stage 3 first timer
--Starting AI timer with anything above 3 indicarets it's a regular timer and to use shortest time in between two regular casts
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
if self.type == "ai" then--A learning timer
if not DBM.Options.AITimer then return end
if timer > 4 then--Normal behavior.
local newPhase = false
for i = 1, 4 do
--Check for any phase timers that are strings, if a string it means last cast of this ability was first case of a given stage
if self["phase"..i.."CastTimer"] and type(self["phase"..i.."CastTimer"]) == "string" then--This is first cast of spell, we need to generate self.firstPullTimer
self["phase"..i.."CastTimer"] = tonumber(self["phase"..i.."CastTimer"])
self["phase"..i.."CastTimer"] = GetTime() - self["phase"..i.."CastTimer"]--We have generated a self.phase1CastTimer! Next pull, DBM should know timer for first cast next pull. FANCY!
DBM:Debug("AI timer learned a first timer for current phase of "..self["phase"..i.."CastTimer"], 2)
newPhase = true
end
end
if self.lastCast and not newPhase then--We have a GetTime() on last cast and it's not affected by a phase change
local timeLastCast = GetTime() - self.lastCast--Get time between current cast and last cast
if timeLastCast > 4 then--Prevent infinite loop cpu hang. Plus anything shorter than 5 seconds doesn't need a timer
if not self.lowestSeenCast or (self.lowestSeenCast and self.lowestSeenCast > timeLastCast) then--Always use lowest seen cast for a timer
self.lowestSeenCast = timeLastCast
DBM:Debug("AI timer learned a new lowest timer of "..self.lowestSeenCast, 2)
end
end
end
self.lastCast = GetTime()
if self.lowestSeenCast then--Always use lowest seen cast for timer
timer = self.lowestSeenCast
else
return--Don't start the bogus timer shoved into timer field in the mod
end
else--AI timer passed with 4 or less is indicating phase change, with timer as phase number
if self["phase"..timer.."CastTimer"] and type(self["phase"..timer.."CastTimer"]) == "number" then
--Check if timer is shorter than previous learned first timer by scanning remaining time on existing bar
local bar = DBT:GetBar(id)
if bar then
local remaining = ("%.1f"):format(bar.timer)
if bar.timer > 0.2 then
self["phase"..timer.."CastTimer"] = self["phase"..timer.."CastTimer"] - remaining
DBM:Debug("AI timer learned a lower first timer for current phase of "..self["phase"..timer.."CastTimer"], 2)
end
end
timer = self["phase"..timer.."CastTimer"]
else--No first pull timer generated yet, set it to GetTime, as a string
self["phase"..timer.."CastTimer"] = tostring(GetTime())
return--Don't start the x second timer
end
end
end
if DBM.Options.BadTimerAlert or DBM.Options.DebugMode and DBM.Options.DebugLevel > 1 then
if not self.type or (self.type ~= "target" and self.type ~= "active" and self.type ~= "fades" and self.type ~= "ai") then
local bar = DBT:GetBar(id)
if bar then
local remaining = ("%.1f"):format(bar.timer)
local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
ttext = ttext.."("..self.id..")"
if bar.timer > 0.2 then
local phaseText = self.mod.vb.phase and " ("..SCENARIO_STAGE:format(self.mod.vb.phase)..")" or ""
if DBM.Options.BadTimerAlert and bar.timer > 1 then--If greater than 1 seconds off, report this out of debug mode to all users
DBM:AddMsg("Timer "..ttext..phaseText.. " refreshed before expired. Remaining time is : "..remaining..". Please report this bug")
fireEvent("DBM_Debug", "Timer "..ttext..phaseText.. " refreshed before expired. Remaining time is : "..remaining..". Please report this bug", 2)
else
DBM:Debug("Timer "..ttext..phaseText.. " refreshed before expired. Remaining time is : "..remaining, 2)
end
end
end
end
end
local colorId = 0
if self.option then
colorId = self.mod.Options[self.option .. "TColor"]
elseif self.colorType and type(self.colorType) == "string" then--No option for specific timer, but another bool option given that tells us where to look for TColor
colorId = self.mod.Options[self.colorType .. "TColor"] or 0
end
local countVoice, countVoiceMax = 0, self.countdownMax or 4
if self.option then
countVoice = self.mod.Options[self.option .. "CVoice"]
if not self.fade and (type(countVoice) == "string" or countVoice > 0) then--Started without faded and has count voice assigned
playCountdown(id, timer, countVoice, countVoiceMax)--timerId, timer, voice, count
end
end
local bar = DBT:CreateBar(timer, id, self.icon, nil, nil, nil, nil, colorId, nil, self.keep, self.fade, countVoice, countVoiceMax)
if not bar then
return false, "error" -- creating the timer failed somehow, maybe hit the hard-coded timer limit of 15
end
local msg
if self.type and not self.text then
msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.spellId, self.name), ...)
else
if type(self.text) == "number" then--spellId passed in timer text, it's a timer with short text
msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.text, self.name), ...)
else
msg = pformat(self.text, ...)
end
end
msg = msg:gsub(">.-<", stripServerName)
bar:SetText(msg, self.inlineIcon)
--ID: Internal DBM timer ID
--msg: Timer Text (Do not use msg has an event trigger, it varies language to language or based on user timer options. Use this to DISPLAY only (such as timer replacement UI). use spellId field 99% of time
--timer: Raw timer value (number).
--Icon: Texture Path for Icon
--type: Timer type (Cooldowns: cd, cdcount, nextcount, nextsource, cdspecial, nextspecial, stage, ai. Durations: target, active, fades, roleplay. Casting: cast)
--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
--colorID: Type classification (1-Add, 2-Aoe, 3-targeted ability, 4-Interrupt, 5-Role, 6-Stage, 7-User(custom))
--Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
--Keep: true or nil, whether or not to keep bar on screen when it expires (if true, timer should be retained until an actual TimerStop occurs or a new TimerStart with same barId happens (in which case you replace bar with new one)
--fade: true or nil, whether or not to fade a bar (set alpha to usersetting/2)
--spellName: Sent so users can use a spell name instead of spellId, if they choose. Mostly to be more classic wow friendly, spellID is still preferred method (even for classic)
--MobGUID if it could be parsed out of args
local guid
if select("#", ...) > 0 then--If timer has args
for i = 1, select("#", ...) do
local v = select(i, ...)
if DBM:IsNonPlayableGUID(v) then--Then scan them for a mob guid
guid = v--If found, guid will be passed in DBM_TimerStart callback
end
end
end
fireEvent("DBM_TimerStart", id, msg, timer, self.icon, self.type, self.spellId, colorId, self.mod.id, self.keep, self.fade, self.name, guid)
tinsert(self.startedTimers, id)
if not self.keep then--Don't ever remove startedTimers on a schedule, if it's a keep timer
self.mod:Unschedule(removeEntry, self.startedTimers, id)
self.mod:Schedule(timer, removeEntry, self.startedTimers, id)
end
return bar
else
return false, "disabled"
end
end
timerPrototype.Show = timerPrototype.Start
--A way to set the fade to yes or no, overriding hardcoded value in NewTimer object with temporary one
--If this method is used, it WILL persist until reload or changing it back
function timerPrototype:SetFade(fadeOn, ...)
--Done this way so SetFade can be used with :Start without needless performance cost (ie, ApplyStyle won't run unless it needs to)
if fadeOn and not self.fade then
self.fade = true--set timer object metatable, which will make sure next bar started uses fade
--Find and Update an existing bar that's already started
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar and not bar.fade then
fireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, true)--Timer ID, spellId, modId, true/nil (new callback only needed if we update an existing timers fade, self.fade is passed in timer start object for new timers)
bar.fade = true--Set bar object metatable, which is copied from timer metatable at bar start only
bar:ApplyStyle()
DBM:Unschedule(playCountSound, id)--Don't even need to check option, it's faster cpu wise to just unschedule countdown either way
end
elseif not fadeOn and self.fade then
self.fade = nil--set timer object metatable, which will make sure next bar started does NOT use fade
--Find and Update an existing bar that's already started
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar and bar.fade then
fireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, nil)--Timer ID, spellId, modId, true/nil (new callback only needed if we update an existing timers fade, self.fade is passed in timer start object for new timers)
bar.fade = nil--Set bar object metatable, which is copied from timer metatable at bar start only
bar:ApplyStyle()
if self.option then
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
if (type(countVoice) == "string" or countVoice > 0) then--Unfading bar, start countdown
DBM:Unschedule(playCountSound, id)
playCountdown(id, bar.timer, countVoice, bar.countdownMax)--timerId, timer, voice, count
DBM:Debug("Re-enabling a countdown on bar ID: "..id.." after a SetFade disable call")
end
end
end
end
end
--This version does NOT set timer object meta, only started bar meta
--Use this if you only want to alter an already STARTED temporarily
--As such it also only needs fadeOn. fadeoff isn't needed since this temp alter never affects newly started bars
function timerPrototype:SetSTFade(fadeOn, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
if fadeOn and not bar.fade then
fireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, true)--Timer ID, spellId, modId, true/nil (new callback only needed if we update an existing timers fade, self.fade is passed in timer start object for new timers)
bar.fade = true--Set bar object metatable, which is copied from timer metatable at bar start only
bar:ApplyStyle()
DBM:Unschedule(playCountSound, id)
elseif not fadeOn and bar.fade then
fireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, nil)
bar.fade = false
bar:ApplyStyle()
if self.option then
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
if (type(countVoice) == "string" or countVoice > 0) then--Unfading bar, start countdown
DBM:Unschedule(playCountSound, id)
playCountdown(id, bar.timer, countVoice, bar.countdownMax)--timerId, timer, voice, count
DBM:Debug("Re-enabling a countdown on bar ID: "..id.." after a SetSTFade disable call")
end
end
end
end
end
function timerPrototype:SetSTKeep(keepOn, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
if keepOn and not bar.keep then
bar.keep = true--Set bar object metatable, which is copied from timer metatable at bar start only
bar:ApplyStyle()
elseif not keepOn and bar.keep then
fireEvent("DBM_TimerFadeUpdate", id, self.spellId, self.mod.id, nil)
bar.keep = false
bar:ApplyStyle()
end
end
end
function timerPrototype:DelayedStart(delay, ...)
unschedule(self.Start, self.mod, self, ...)
schedule(delay or 0.5, self.Start, self.mod, self, ...)
end
timerPrototype.DelayedShow = timerPrototype.DelayedStart
function timerPrototype:Schedule(t, ...)
return schedule(t, self.Start, self.mod, self, ...)
end
function timerPrototype:Unschedule(...)
return unschedule(self.Start, self.mod, self, ...)
end
--TODO, figure out why this function doesn't properly stop count timers when calling stop without count on count timers
function timerPrototype:Stop(...)
if select("#", ...) == 0 then
for i = #self.startedTimers, 1, -1 do
fireEvent("DBM_TimerStop", self.startedTimers[i])
DBT:CancelBar(self.startedTimers[i])
DBM:Unschedule(playCountSound, self.startedTimers[i])--Unschedule countdown by timerId
self.startedTimers[i] = nil
end
else
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
for i = #self.startedTimers, 1, -1 do
if self.startedTimers[i] == id then
local guid
for j = 1, select("#", ...) do
local v = select(j, ...)
if DBM:IsNonPlayableGUID(v) then--Then scan them for a mob guid
guid = v--If found, guid will be passed in DBM_TimerStart callback
end
end
fireEvent("DBM_TimerStop", id, guid)
DBT:CancelBar(id)
DBM:Unschedule(playCountSound, id)--Unschedule countdown by timerId
tremove(self.startedTimers, i)
end
end
end
if self.type == "ai" then--A learning timer
if not DBM.Options.AITimer then return end
self.lastCast = nil
for i = 1, 4 do
--Check for any phase timers that are strings and never got a chance to become AI timers, then wipe them
if self["phase"..i.."CastTimer"] and type(self["phase"..i.."CastTimer"]) == "string" then
self["phase"..i.."CastTimer"] = nil
DBM:Debug("Wiping incomplete new timer of stage "..i, 2)
end
end
end
end
function timerPrototype:Cancel(...)
self:Stop(...)
self:Unschedule(...)
end
function timerPrototype:GetTime(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
return bar and (bar.totalTime - bar.timer) or 0, (bar and bar.totalTime) or 0
end
function timerPrototype:GetRemaining(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
return bar and bar.timer or 0
end
function timerPrototype:IsStarted(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
return bar and true
end
function timerPrototype:SetTimer(timer)
self.timer = timer
end
function timerPrototype:Update(elapsed, totalTime, ...)
if DBM.Options.DontShowBossTimers then return end
if self:GetTime(...) == 0 then
self:Start(totalTime, ...)
end
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
fireEvent("DBM_TimerUpdate", id, elapsed, totalTime)
if bar and self.option then
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
if (type(countVoice) == "string" or countVoice > 0) then
DBM:Unschedule(playCountSound, id)
if not bar.fade then--Don't start countdown voice if it's faded bar
local newRemaining = totalTime-elapsed
if newRemaining > 2 then
playCountdown(id, newRemaining, countVoice, bar.countdownMax)--timerId, timer, voice, count
DBM:Debug("Updating a countdown after a timer Update call for timer ID:"..id)
end
end
end
end
return DBT:UpdateBar(id, elapsed, totalTime)
end
function timerPrototype:AddTime(extendAmount, ...)
if DBM.Options.DontShowBossTimers then return end
if self:GetTime(...) == 0 then
return self:Start(extendAmount, ...)
else
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
if elapsed and total then
if self.option then
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
if (type(countVoice) == "string" or countVoice > 0) then
DBM:Unschedule(playCountSound, id)
if not bar.fade then--Don't start countdown voice if it's faded bar
local newRemaining = (total+extendAmount) - elapsed
playCountdown(id, newRemaining, countVoice, bar.countdownMax)--timerId, timer, voice, count
DBM:Debug("Updating a countdown after a timer AddTime call for timer ID:"..id)
end
end
end
fireEvent("DBM_TimerUpdate", id, elapsed, total+extendAmount)
return DBT:UpdateBar(id, elapsed, total+extendAmount)
end
end
end
end
function timerPrototype:RemoveTime(reduceAmount, ...)
if DBM.Options.DontShowBossTimers then return end
if self:GetTime(...) == 0 then
return--Do nothing
else
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
if elapsed and total then
local newRemaining = (total-reduceAmount) - elapsed
if newRemaining > 0 then
if self.option and newRemaining > 2 then
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
if (type(countVoice) == "string" or countVoice > 0) then
DBM:Unschedule(playCountSound, id)
if not bar.fade then--Don't start countdown voice if it's faded bar
if newRemaining > 2 then
playCountdown(id, newRemaining, countVoice, bar.countdownMax)--timerId, timer, voice, count
DBM:Debug("Updating a countdown after a timer RemoveTime call for timer ID:"..id)
end
end
end
end
fireEvent("DBM_TimerUpdate", id, elapsed, total-reduceAmount)
return DBT:UpdateBar(id, elapsed, total-reduceAmount)
else--New remaining less than 0
DBM:Unschedule(playCountSound, id)
fireEvent("DBM_TimerStop", id)
return DBT:CancelBar(id)
end
end
end
end
end
function timerPrototype:Pause(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
fireEvent("DBM_TimerPause", id)
return bar:Pause()
end
end
function timerPrototype:Resume(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
fireEvent("DBM_TimerResume", id)
return bar:Resume()
end
end
function timerPrototype:UpdateIcon(icon, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
return bar:SetIcon((type(icon) == "string" and icon:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or tonumber(icon) or 136116)
end
end
function timerPrototype:UpdateInline(newInline, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
return bar:SetText(ttext, newInline or self.inlineIcon)
end
end
function timerPrototype:UpdateName(name, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
return bar:SetText(name, self.inlineIcon)
end
end
function timerPrototype:SetColor(c, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
return bar:SetColor(c)
end
end
function timerPrototype:DisableEnlarge(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBT:GetBar(id)
if bar then
bar.small = true
end
end
function timerPrototype:AddOption(optionDefault, optionName, colorType, countdown)
if optionName ~= false then
self.option = optionName or self.id
self.mod:AddBoolOption(self.option, optionDefault, "timer", nil, colorType, countdown)
end
end
--If a new countdown default is added to a NewTimer object, change optionName of timer to reset a new default
function bossModPrototype:NewTimer(timer, name, texture, optionDefault, optionName, colorType, inlineIcon, keep, countdown, countdownMax, r, g, b)
if r and type(r) == "string" then
DBM:Debug("|cffff0000r probably has inline icon in it and needs to be fixed for |r"..name..r)
r = nil--Fix it for users
end
if inlineIcon and type(inlineIcon) == "number" then
DBM:Debug("|cffff0000spellID texture path or colorType is in inlineIcon field and needs to be fixed for |r"..name..inlineIcon)
inlineIcon = nil--Fix it for users
end
local icon = (type(texture) == "string" and texture:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(texture, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(texture, 3)))) or (type(texture) == "number" and GetSpellTexture(texture)) or tonumber(texture) or "136116"
local obj = setmetatable(
{
text = self.localization.timers[name],
timer = timer,
id = name,
icon = icon,
colorType = colorType,
inlineIcon = inlineIcon,
keep = keep,
countdown = countdown,
countdownMax = countdownMax,
r = r,
g = g,
b = b,
startedTimers = {},
mod = self,
},
mt
)
obj:AddOption(optionDefault, optionName, colorType, countdown)
tinsert(self.timers, obj)
return obj
end
-- new constructor for the new auto-localized timer types
-- note that the function might look unclear because it needs to handle different timer types, especially achievement timers need special treatment
-- If a new countdown is added to an existing timer that didn't have one before, use optionName (number) to force timer to reset defaults by assigning it a new variable
local function newTimer(self, timerType, timer, spellId, timerText, optionDefault, optionName, colorType, texture, inlineIcon, keep, countdown, countdownMax, r, g, b)
if type(timer) == "string" and timer:match("OptionVersion") then
DBM:Debug("|cffff0000OptionVersion hack depricated, remove it from: |r"..spellId)
return
end
if type(colorType) == "number" and colorType > 7 then
DBM:Debug("|cffff0000texture is in the colorType arg for: |r"..spellId)
end
--Use option optionName for optionVersion as well, no reason to split.
--This ensures that remaining arg positions match for auto generated and regular NewTimer
local optionVersion
if type(optionName) == "number" then
optionVersion = optionName
optionName = nil
end
local allowdouble
if type(timer) == "string" and timer:match("d%d+") then
allowdouble = true
timer = tonumber(string.sub(timer, 2))
end
local spellName, icon
local unparsedId = spellId
if timerType == "achievement" then
spellName = select(2, GetAchievementInfo(spellId))
icon = type(texture) == "number" and select(10, GetAchievementInfo(texture)) or tonumber(texture) or spellId and select(10, GetAchievementInfo(spellId))
elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "stage" then
icon = type(texture) == "number" and GetSpellTexture(texture) or tonumber(texture) or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or 136116
if timerType == "stage" then
colorType = 6
end
elseif timerType == "roleplay" then
icon = type(texture) == "number" and GetSpellTexture(texture) or tonumber(texture) or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or 237538
colorType = 6
elseif timerType == "adds" or timerType == "addscustom" then
icon = type(texture) == "number" and GetSpellTexture(texture) or tonumber(texture) or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or 136116
colorType = 1
else
if type(spellId) == "string" and spellId:match("ej%d+") then
spellName = DBM:EJ_GetSectionInfo(string.sub(spellId, 3)) or ""
else
spellName = DBM:GetSpellInfo(spellId or 0)
end
if spellName then
icon = type(texture) == "number" and GetSpellTexture(texture) or tonumber(texture) or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId))
else
icon = nil
end
end
--spellName = spellName or tostring(spellId)--this actually breaks stuff in 9.0 when spell info fails to return on first try
local timerTextValue
if timerText then
--If timertext is a number, accept it as a secondary auto translate spellid
if DBM.Options.ShortTimerText and type(timerText) == "number" then
timerTextValue = timerText
spellName = DBM:GetSpellInfo(timerText or 0)--Override Cached spell Name
else
timerTextValue = self.localization.timers[timerText] or timerText--Check timers table first, otherwise accept it as literal timer text
end
end
local id = "Timer"..(spellId or 0)..timerType..(optionVersion or "")
local obj = setmetatable(
{
text = timerTextValue,
type = timerType,
spellId = spellId,
name = spellName,--If name gets stored as nil, it'll be corrected later in Timer start, if spell name returns in a later attempt
timer = timer,
id = id,
icon = icon,
colorType = colorType,
inlineIcon = inlineIcon,
keep = keep,
countdown = countdown,
countdownMax = countdownMax,
r = r,
g = g,
b = b,
allowdouble = allowdouble,
startedTimers = {},
mod = self,
},
mt
)
obj:AddOption(optionDefault, optionName, colorType, countdown)
tinsert(self.timers, obj)
-- todo: move the string creation to the GUI with SetFormattedString...
if timerType == "achievement" then
self.localization.options[id] = L.AUTO_TIMER_OPTIONS[timerType]:format(GetAchievementLink(spellId):gsub("%[(.+)%]", "%1"))
elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "stage" or timerType == "roleplay" then--Timers without spellid, generic
self.localization.options[id] = L.AUTO_TIMER_OPTIONS[timerType]--Using more than 1 stage timer or more than 1 special timer will break this, fortunately you should NEVER use more than 1 of either in a mod
else
self.localization.options[id] = L.AUTO_TIMER_OPTIONS[timerType]:format(unparsedId)
end
return obj
end
function bossModPrototype:NewTargetTimer(...)
return newTimer(self, "target", ...)
end
--Buff/Debuff/event on boss
function bossModPrototype:NewBuffActiveTimer(...)
return newTimer(self, "active", ...)
end
----Buff/Debuff on players
function bossModPrototype:NewBuffFadesTimer(...)
return newTimer(self, "fades", ...)
end
function bossModPrototype:NewCastTimer(timer, ...)
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 ;)
local spellId = timer
timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
local spellHaste = select(4, DBM:GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
timer = timer / spellHaste -- calculate the real cast time of the spell...
return self:NewCastTimer(timer / 1000, spellId, ...)
end
return newTimer(self, "cast", timer, ...)
end
function bossModPrototype:NewCastCountTimer(timer, ...)
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 ;)
local spellId = timer
timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
local spellHaste = select(4, DBM:GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
timer = timer / spellHaste -- calculate the real cast time of the spell...
return self:NewCastTimer(timer / 1000, spellId, ...)
end
return newTimer(self, "castcount", timer, ...)
end
function bossModPrototype:NewCastSourceTimer(timer, ...)
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 ;)
local spellId = timer
timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
local spellHaste = select(4, DBM:GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
timer = timer / spellHaste -- calculate the real cast time of the spell...
return self:NewCastSourceTimer(timer / 1000, spellId, ...)
end
return newTimer(self, "castsource", timer, ...)
end
function bossModPrototype:NewCDTimer(...)
return newTimer(self, "cd", ...)
end
function bossModPrototype:NewCDCountTimer(...)
return newTimer(self, "cdcount", ...)
end
function bossModPrototype:NewCDSourceTimer(...)
return newTimer(self, "cdsource", ...)
end
function bossModPrototype:NewNextTimer(...)
return newTimer(self, "next", ...)
end
function bossModPrototype:NewNextCountTimer(...)
return newTimer(self, "nextcount", ...)
end
function bossModPrototype:NewNextSourceTimer(...)
return newTimer(self, "nextsource", ...)
end
function bossModPrototype:NewAchievementTimer(...)
return newTimer(self, "achievement", ...)
end
function bossModPrototype:NewCDSpecialTimer(...)
return newTimer(self, "cdspecial", ...)
end
function bossModPrototype:NewNextSpecialTimer(...)
return newTimer(self, "nextspecial", ...)
end
function bossModPrototype:NewPhaseTimer(...)
return newTimer(self, "stage", ...)
end
function bossModPrototype:NewRPTimer(...)
return newTimer(self, "roleplay", ...)
end
function bossModPrototype:NewAddsTimer(...)
return newTimer(self, "adds", ...)
end
function bossModPrototype:NewAddsCustomTimer(...)
return newTimer(self, "addscustom", ...)
end
function bossModPrototype:NewAITimer(...)
return newTimer(self, "ai", ...)
end
function bossModPrototype:GetLocalizedTimerText(timerType, spellId, Name)
local spellName
if Name then
spellName = Name--Pull from name stored in object
elseif spellId then
DBM:Debug("|cffff0000GetLocalizedTimerText fallback, this should not happen and is a bug. this fallback should be deleted if this message is never seen after async code is live|r")
if timerType == "achievement" then
spellName = select(2, GetAchievementInfo(spellId))
elseif type(spellId) == "string" and spellId:match("ej%d+") then
spellName = DBM:EJ_GetSectionInfo(string.sub(spellId, 3))
else
spellName = DBM:GetSpellInfo(spellId)
end
--Name wasn't provided, but we succeeded in gettinga name, generate one into object now for caching purposes
--This would really only happen if GetSpellInfo failed to return spell name on first attempt (which now happens in 9.0)
if spellName then
self.name = spellName
end
end
if L.AUTO_TIMER_TEXTS[timerType.."short"] and DBT.Options.StripCDText then
return pformat(L.AUTO_TIMER_TEXTS[timerType.."short"], spellName)
else
return pformat(L.AUTO_TIMER_TEXTS[timerType], spellName)
end
end
end
------------------------------
-- Berserk/Combat Objects --
------------------------------
do
local enragePrototype = {}
local mt = {__index = enragePrototype}
function enragePrototype:Start(timer)
timer = timer or self.timer or 600
timer = timer <= 0 and self.timer - timer or timer
self.bar:SetTimer(timer)
self.bar:Start()
if self.warning1 then
if timer > 660 then self.warning1:Schedule(timer - 600, 10, L.MIN) end
if timer > 300 then self.warning1:Schedule(timer - 300, 5, L.MIN) end
if timer > 180 then self.warning2:Schedule(timer - 180, 3, L.MIN) end
end
if self.warning2 then
if timer > 60 then self.warning2:Schedule(timer - 60, 1, L.MIN) end
if timer > 30 then self.warning2:Schedule(timer - 30, 30, L.SEC) end
if timer > 10 then self.warning2:Schedule(timer - 10, 10, L.SEC) end
end
end
function enragePrototype:Schedule(t)
return self.owner:Schedule(t, self.Start, self)
end
function enragePrototype:Cancel()
self.owner:Unschedule(self.Start, self)
if self.warning1 then
self.warning1:Cancel()
end
if self.warning2 then
self.warning2:Cancel()
end
self.bar:Stop()
end
enragePrototype.Stop = enragePrototype.Cancel
function bossModPrototype:NewBerserkTimer(timer, text, barText, barIcon)
timer = timer or 600
local warning1 = self:NewAnnounce(text or L.GENERIC_WARNING_BERSERK, 1, nil, "warning_berserk", false)
local warning2 = self:NewAnnounce(text or L.GENERIC_WARNING_BERSERK, 4, nil, "warning_berserk", false)
local bar = self:NewTimer(timer, barText or L.GENERIC_TIMER_BERSERK, barIcon or 28131, nil, "timer_berserk")
local obj = setmetatable(
{
warning1 = warning1,
warning2 = warning2,
bar = bar,
timer = timer,
owner = self
},
mt
)
return obj
end
function bossModPrototype:NewCombatTimer(timer, text, barText, barIcon)
timer = timer or 10
--NewTimer(timer, name, texture, optionDefault, optionName, colorType, inlineIcon, keep, countdown, countdownMax, r, g, b)
local bar = self:NewTimer(timer, barText or L.GENERIC_TIMER_COMBAT, barIcon or "132349", nil, "timer_combat", nil, nil, nil, 1, 5)
local obj = setmetatable(
{
bar = bar,
timer = timer,
owner = self
},
mt
)
return obj
end
end
---------------
-- Options --
---------------
function bossModPrototype:AddBoolOption(name, default, cat, func, extraOption, extraOptionTwo)
cat = cat or "misc"
self.DefaultOptions[name] = (default == nil) or default
if cat == "timer" then
self.DefaultOptions[name.."TColor"] = extraOption or 0
self.DefaultOptions[name.."CVoice"] = extraOptionTwo or 0
end
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options[name] = (default == nil) or default
if cat == "timer" then
self.Options[name.."TColor"] = extraOption or 0
self.Options[name.."CVoice"] = extraOptionTwo or 0
end
self:SetOptionCategory(name, cat)
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:AddSpecialWarningOption(name, default, defaultSound, cat)
cat = cat or "misc"
self.DefaultOptions[name] = (default == nil) or default
self.DefaultOptions[name.."SWSound"] = defaultSound or 1
self.DefaultOptions[name.."SWNote"] = true
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options[name] = (default == nil) or default
self.Options[name.."SWSound"] = defaultSound or 1
self.Options[name.."SWNote"] = true
self:SetOptionCategory(name, cat)
end
function bossModPrototype:AddSetIconOption(name, spellId, default, isHostile, iconsUsed)
self.DefaultOptions[name] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options[name] = (default == nil) or default
self:SetOptionCategory(name, "icon")
if isHostile then
if not self.findFastestComputer then
self.findFastestComputer = {}
end
self.findFastestComputer[#self.findFastestComputer + 1] = name
self.localization.options[name] = L.AUTO_ICONS_OPTION_TEXT2:format(spellId)
else
self.localization.options[name] = L.AUTO_ICONS_OPTION_TEXT:format(spellId)
end
--A table defining used icons by number, insert icon textures to end of option
if iconsUsed then
self.localization.options[name] = self.localization.options[name].." ("
for i=1, #iconsUsed do
--Texture ID 137009 if direct calling RaidTargetingIcons stops working one day
if iconsUsed[i] == 1 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:0:16:0:16|t"
elseif iconsUsed[i] == 2 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:16:32:0:16|t"
elseif iconsUsed[i] == 3 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:32:48:0:16|t"
elseif iconsUsed[i] == 4 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:48:64:0:16|t"
elseif iconsUsed[i] == 5 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:0:16:16:32|t"
elseif iconsUsed[i] == 6 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:16:32:16:32|t"
elseif iconsUsed[i] == 7 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:32:48:16:32|t"
elseif iconsUsed[i] == 8 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:48:64:16:32|t"
end
end
self.localization.options[name] = self.localization.options[name]..")"
end
end
function bossModPrototype:AddArrowOption(name, spellId, default, isRunTo)
if isRunTo == true then isRunTo = 2 end--Support legacy
self.DefaultOptions[name] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options[name] = (default == nil) or default
self:SetOptionCategory(name, "misc")
if isRunTo == 2 then
self.localization.options[name] = L.AUTO_ARROW_OPTION_TEXT:format(spellId)
elseif isRunTo == 3 then
self.localization.options[name] = L.AUTO_ARROW_OPTION_TEXT3:format(spellId)
else
self.localization.options[name] = L.AUTO_ARROW_OPTION_TEXT2:format(spellId)
end
end
function bossModPrototype:AddRangeFrameOption(range, spellId, default)
self.DefaultOptions["RangeFrame"] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options["RangeFrame"] = (default == nil) or default
self:SetOptionCategory("RangeFrame", "misc")
if spellId then
self.localization.options["RangeFrame"] = L.AUTO_RANGE_OPTION_TEXT:format(range, spellId)
else
self.localization.options["RangeFrame"] = L.AUTO_RANGE_OPTION_TEXT_SHORT:format(range)
end
end
function bossModPrototype:AddHudMapOption(name, spellId, default)
self.DefaultOptions[name] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options[name] = (default == nil) or default
self:SetOptionCategory(name, "misc")
if spellId then
self.localization.options[name] = L.AUTO_HUD_OPTION_TEXT:format(spellId)
else
self.localization.options[name] = L.AUTO_HUD_OPTION_TEXT_MULTI
end
end
function bossModPrototype:AddNamePlateOption(name, spellId, default)
if not spellId then
error("AddNamePlateOption must provide valid spellId", 2)
end
self.DefaultOptions[name] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options[name] = (default == nil) or default
self:SetOptionCategory(name, "nameplate")
self.localization.options[name] = L.AUTO_NAMEPLATE_OPTION_TEXT:format(spellId)
end
function bossModPrototype:AddInfoFrameOption(spellId, default, optionVersion)
local oVersion = ""
if optionVersion then
oVersion = tostring(optionVersion)
end
self.DefaultOptions["InfoFrame"..oVersion] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options["InfoFrame"..oVersion] = (default == nil) or default
self:SetOptionCategory("InfoFrame"..oVersion, "misc")
if spellId then
self.localization.options["InfoFrame"..oVersion] = L.AUTO_INFO_FRAME_OPTION_TEXT:format(spellId)
else
self.localization.options["InfoFrame"..oVersion] = L.AUTO_INFO_FRAME_OPTION_TEXT2
end
end
function bossModPrototype:AddReadyCheckOption(questId, default, maxLevel)
self.readyCheckQuestId = questId
self.readyCheckMaxLevel = maxLevel or 999
self.DefaultOptions["ReadyCheck"] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options["ReadyCheck"] = (default == nil) or default
self.localization.options["ReadyCheck"] = L.AUTO_READY_CHECK_OPTION_TEXT
self:SetOptionCategory("ReadyCheck", "misc")
end
function bossModPrototype:AddSpeedClearOption(name, default)
self.DefaultOptions["SpeedClearTimer"] = (default == nil) or default
if default and type(default) == "string" then
default = self:GetRoleFlagValue(default)
end
self.Options["SpeedClearTimer"] = (default == nil) or default
self:SetOptionCategory("SpeedClearTimer", "timer")
self.localization.options["SpeedClearTimer"] = L.AUTO_SPEEDCLEAR_OPTION_TEXT:format(name)
end
function bossModPrototype:AddSliderOption(name, minValue, maxValue, valueStep, default, cat, func)
cat = cat or "misc"
self.DefaultOptions[name] = {type = "slider", value = default or 0}
self.Options[name] = default or 0
self:SetOptionCategory(name, cat)
self.sliders = self.sliders or {}
self.sliders[name] = {
minValue = minValue,
maxValue = maxValue,
valueStep = valueStep,
}
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:AddEditboxOption(name, default, cat, width, height, func)
cat = cat or "misc"
self.DefaultOptions[name] = {type = "editbox", value = default or ""}
self.Options[name] = default or ""
self:SetOptionCategory(name, cat)
self.editboxes = self.editboxes or {}
self.editboxes[name] = {
width = width,
height = height
}
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:AddButton(name, onClick, cat, width, height, fontObject)
cat = cat or "misc"
self:SetOptionCategory(name, cat)
self.buttons = self.buttons or {}
self.buttons[name] = {
onClick = onClick,
width = width,
height = height,
fontObject = fontObject
}
end
-- 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
-- this will be fixed as soon as it is necessary due to removed options ;-)
function bossModPrototype:AddDropdownOption(name, options, default, cat, func)
cat = cat or "misc"
self.DefaultOptions[name] = {type = "dropdown", value = default}
self.Options[name] = default
self:SetOptionCategory(name, cat)
self.dropdowns = self.dropdowns or {}
self.dropdowns[name] = options
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:AddOptionSpacer(cat)
cat = cat or "misc"
if self.optionCategories[cat] then
tinsert(self.optionCategories[cat], DBM_OPTION_SPACER)
end
end
function bossModPrototype:AddOptionLine(text, cat)
cat = cat or "misc"
if not self.optionCategories[cat] then
self.optionCategories[cat] = {}
end
if self.optionCategories[cat] then
tinsert(self.optionCategories[cat], {line = true, text = text})
end
end
function bossModPrototype:AddAnnounceSpacer()
return self:AddOptionSpacer("announce")
end
function bossModPrototype:AddTimerSpacer()
return self:AddOptionSpacer("timer")
end
function bossModPrototype:AddAnnounceLine(text)
return self:AddOptionLine(text, "announce")
end
function bossModPrototype:AddTimerLine(text)
return self:AddOptionLine(text, "timer")
end
function bossModPrototype:AddMiscLine(text)
return self:AddOptionLine(text, "misc")
end
function bossModPrototype:RemoveOption(name)
self.Options[name] = nil
for i, options in pairs(self.optionCategories) do
removeEntry(options, name)
if #options == 0 then
self.optionCategories[i] = nil
end
end
if self.optionFuncs then
self.optionFuncs[name] = nil
end
end
function bossModPrototype:SetOptionCategory(name, cat)
for _, options in pairs(self.optionCategories) do
removeEntry(options, name)
end
if not self.optionCategories[cat] then
self.optionCategories[cat] = {}
end
tinsert(self.optionCategories[cat], name)
tinsert(self.categorySort, cat)
end
--------------
-- Combat --
--------------
function bossModPrototype:RegisterCombat(cType, ...)
if cType then
cType = cType:lower()
end
local info = {
type = cType,
mob = self.creatureId,
eId = self.encounterId,
name = self.localization.general.name or self.id,
msgs = (cType ~= "combat") and {...},
mod = self
}
if self.multiMobPullDetection then
info.multiMobPullDetection = self.multiMobPullDetection
end
if self.multiEncounterPullDetection then
info.multiEncounterPullDetection = self.multiEncounterPullDetection
end
if self.noESDetection then
info.noESDetection = self.noESDetection
end
if self.noEEDetection then
info.noEEDetection = self.noEEDetection
end
if self.noBKDetection then
info.noBKDetection = self.noBKDetection
end
if self.noIEEUDetection then
info.noIEEUDetection = self.noIEEUDetection
end
if self.noFriendlyEngagement then
info.noFriendlyEngagement = self.noFriendlyEngagement
end
if self.noRegenDetection then
info.noRegenDetection = self.noRegenDetection
end
if self.noWBEsync then
info.noWBEsync = self.noWBEsync
end
if self.noBossDeathKill then
info.noBossDeathKill = self.noBossDeathKill
end
-- use pull-mobs as kill mobs by default, can be overriden by RegisterKill
if self.multiMobPullDetection then
for _, v in ipairs(self.multiMobPullDetection) do
info.killMobs = info.killMobs or {}
info.killMobs[v] = true
end
end
self.combatInfo = info
if not self.zones then return end
for v in pairs(self.zones) do
combatInfo[v] = combatInfo[v] or {}
tinsert(combatInfo[v], info)
end
end
-- needs to be called _AFTER_ RegisterCombat
function bossModPrototype:RegisterKill(msgType, ...)
if not self.combatInfo then
error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
end
if msgType == "kill" then
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
self.combatInfo.killMobs = {}
for i = 1, select("#", ...) do
local v = select(i, ...)
if type(v) == "number" then
self.combatInfo.killMobs[v] = true
end
end
end
else
self.combatInfo.killType = msgType
self.combatInfo.killMsgs = {}
for i = 1, select("#", ...) do
local v = select(i, ...)
self.combatInfo.killMsgs[v] = true
end
end
end
function bossModPrototype:SetDetectCombatInVehicle(flag)
if not self.combatInfo then
error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
end
self.combatInfo.noCombatInVehicle = not flag
end
function bossModPrototype:SetCreatureID(...)
self.creatureId = ...
if select("#", ...) > 1 then
self.multiMobPullDetection = {...}
if self.combatInfo then
self.combatInfo.multiMobPullDetection = self.multiMobPullDetection
self.numBoss = #self.multiMobPullDetection
if self.inCombat then
--Called mid combat, fix some variables
self.vb.bossLeft = self.numBoss
end
end
for i = 1, select("#", ...) do
local cId = select(i, ...)
bossIds[cId] = true
end
else
local cId = ...
bossIds[cId] = true
self.numBoss = 1
end
end
function bossModPrototype:SetEncounterID(...)
self.encounterId = ...
if select("#", ...) > 1 then
self.multiEncounterPullDetection = {...}
if self.combatInfo then
self.combatInfo.multiEncounterPullDetection = self.multiEncounterPullDetection
end
end
end
function bossModPrototype:DisableESCombatDetection()
self.noESDetection = true
if self.combatInfo then
self.combatInfo.noESDetection = true
end
end
function bossModPrototype:DisableEEKillDetection()
self.noEEDetection = true
if self.combatInfo then
self.combatInfo.noEEDetection = true
end
end
function bossModPrototype:DisableBKKillDetection()
self.noBKDetection = true
if self.combatInfo then
self.combatInfo.noBKDetection = true
end
end
function bossModPrototype:DisableIEEUCombatDetection()
self.noIEEUDetection = true
if self.combatInfo then
self.combatInfo.noIEEUDetection = true
end
end
function bossModPrototype:DisableFriendlyDetection()
self.noFriendlyEngagement = true
if self.combatInfo then
self.combatInfo.noFriendlyEngagement = true
end
end
function bossModPrototype:DisableRegenDetection()
self.noRegenDetection = true
if self.combatInfo then
self.combatInfo.noRegenDetection = true
end
end
function bossModPrototype:DisableWBEngageSync()
self.noWBEsync = true
if self.combatInfo then
self.combatInfo.noWBEsync = true
end
end
function bossModPrototype:IsInCombat()
return self.inCombat
end
function bossModPrototype:IsAlive()
return not UnitIsDeadOrGhost("player")
end
function bossModPrototype:SetMinCombatTime(t)
self.minCombatTime = t
end
-- needs to be called after RegisterCombat
function bossModPrototype:SetWipeTime(t)
if not self.combatInfo then
error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
end
self.combatInfo.wipeTimer = t
end
-- fix for LFR ToES Tsulong combat detection bug after killed.
function bossModPrototype:SetReCombatTime(t, t2)--T1, after kill. T2 after wipe
self.reCombatTime = t
self.reCombatTime2 = t2
end
function bossModPrototype:SetOOCBWComms()
tinsert(oocBWComms, self)
end
-----------------------
-- Synchronization --
-----------------------
function bossModPrototype:SendSync(event, ...)
event = event or ""
local arg = select("#", ...) > 0 and strjoin("\t", tostringall(...)) or ""
local str = ("%s\t%s\t%s\t%s"):format(self.id, self.revision or 0, event, arg)
local spamId = self.id .. event .. arg -- *not* the same as the sync string, as it doesn't use the revision information
local time = GetTime()
--Mod syncs are more strict and enforce latency threshold always.
--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.
if not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > 8 then
self:ReceiveSync(event, nil, self.revision or 0, tostringall(...))
sendSync("M", str)
end
end
function bossModPrototype:SendBigWigsSync(msg, extra)
msg = "B^".. msg
if extra then
msg = msg .."^".. extra
end
if IsInGroup() then
SendAddonMessage("BigWigs", msg, IsInGroup(2) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY")
end
end
function bossModPrototype:ReceiveSync(event, sender, revision, ...)
local spamId = self.id .. event .. strjoin("\t", ...)
local time = GetTime()
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
modSyncSpam[spamId] = time
-- we have to use the sender as last argument for compatibility reasons (stupid old API...)
-- avoid table allocations for frequently used number of arguments
if select("#", ...) <= 1 then
-- syncs with no arguments have an empty argument (also for compatibility reasons)
self:OnSync(event, ... or "", sender)
elseif select("#", ...) == 2 then
self:OnSync(event, ..., select(2, ...), sender)
else
local tmp = { ... }
tmp[#tmp + 1] = sender
self:OnSync(event, unpack(tmp))
end
end
end
function bossModPrototype:SetRevision(revision)
revision = parseCurseDate(revision or "")
if not revision or revision == "20210701004539" then
-- bad revision: either forgot the svn keyword or using github
revision = DBM.Revision
end
self.revision = revision
end
--Either treat it as a valid number, or a curse string that needs to be made into a valid number
function bossModPrototype:SetMinSyncRevision(revision)
self.minSyncRevision = (type(revision or "") == "number") and revision or parseCurseDate(revision)
end
function bossModPrototype:SetHotfixNoticeRev(revision)
self.hotfixNoticeRev = (type(revision or "") == "number") and revision or parseCurseDate(revision)
end
-----------------
-- Scheduler --
-----------------
function bossModPrototype:Schedule(t, f, ...)
return schedule(t, f, self, ...)
end
function bossModPrototype:Unschedule(f, ...)
return unschedule(f, self, ...)
end
function bossModPrototype:ScheduleMethod(t, method, ...)
if not self[method] then
error(("Method %s does not exist"):format(tostring(method)), 2)
end
return self:Schedule(t, self[method], self, ...)
end
bossModPrototype.ScheduleEvent = bossModPrototype.ScheduleMethod
function bossModPrototype:UnscheduleMethod(method, ...)
if not self[method] then
error(("Method %s does not exist"):format(tostring(method)), 2)
end
return self:Unschedule(self[method], self, ...)
end
bossModPrototype.UnscheduleEvent = bossModPrototype.UnscheduleMethod
-------------
-- Icons --
-------------
do
local scanExpires = {}
local addsIcon = {}
local addsIconSet = {}
function bossModPrototype:SetIcon(target, icon, timer)
if not target then return end--Fix a rare bug where target becomes nil at last second (end combat fires and clears targets)
if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
return
end
self:UnscheduleMethod("SetIcon", target)
if type(icon) ~= "number" or type(target) ~= "string" then--icon/target probably backwards.
DBM:Debug("|cffff0000SetIcon is being used impropperly. Check icon/target order|r")
return--Fail silently instead of spamming icon lua errors if we screw up
end
icon = icon and icon >= 0 and icon <= 8 and icon or 8
local uId = DBM:GetRaidUnitId(target)
if uId and UnitIsUnit(uId, "player") and DBM:GetNumRealGroupMembers() < 2 then return end--Solo raid, no reason to put icon on yourself.
if uId or UnitExists(target) then--target accepts uid, unitname both.
uId = uId or target
--save previous icon into a table.
local oldIcon = self:GetIcon(uId) or 0
if not self.iconRestore[uId] then
self.iconRestore[uId] = oldIcon
end
--set icon
if oldIcon ~= icon then--Don't set icon if it's already set to what we're setting it to
SetRaidTarget(uId, self.iconRestore[uId] and icon == 0 and self.iconRestore[uId] or icon)
end
--schedule restoring old icon if timer enabled.
if timer then
self:ScheduleMethod(timer, "SetIcon", target, 0)
end
end
end
do
local iconSortTable = {}
local iconSet = {}
local function sort_by_group(v1, v2)
return DBM:GetRaidSubgroup(DBM:GetUnitFullName(v1)) < DBM:GetRaidSubgroup(DBM:GetUnitFullName(v2))
end
local function clearSortTable(scanID)
iconSortTable[scanID] = nil
iconSet[scanID] = nil
end
function bossModPrototype:SetIconByAlphaTable(returnFunc, scanID)
tsort(iconSortTable[scanID])--Sorted alphabetically
for i = 1, #iconSortTable[scanID] do
local target = iconSortTable[scanID][i]
if i > 8 then
DBM:Debug("|cffff0000Too many players to set icons, reconsider where using icons|r", 2)
return
end
if not self.iconRestore[target] then
local oldIcon = self:GetIcon(target) or 0
self.iconRestore[target] = oldIcon
end
SetRaidTarget(target, i)--Icons match number in table in alpha sort
if returnFunc then
self[returnFunc](self, target, i)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
end
end
DBM:Schedule(1.5, clearSortTable, scanID)--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.
end
function bossModPrototype:SetAlphaIcon(delay, target, maxIcon, returnFunc, scanID)
if not target then return end
if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
return
end
scanID = scanID or 1
local uId = DBM:GetRaidUnitId(target)
if uId or UnitExists(target) then--target accepts uid, unitname both.
uId = uId or target
if not iconSortTable[scanID] then iconSortTable[scanID] = {} end
if not iconSet[scanID] then iconSet[scanID] = 0 end
local foundDuplicate = false
for i = #iconSortTable[scanID], 1, -1 do
if iconSortTable[scanID][i] == uId then
foundDuplicate = true
break
end
end
if not foundDuplicate then
iconSet[scanID] = iconSet[scanID] + 1
tinsert(iconSortTable[scanID], uId)
end
self:UnscheduleMethod("SetIconByAlphaTable")
if maxIcon and iconSet[scanID] == maxIcon then
self:SetIconByAlphaTable(returnFunc, scanID)
elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
self:ScheduleMethod(delay or 0.5, "SetIconByAlphaTable", returnFunc, scanID)
end
end
end
function bossModPrototype:SetIconBySortedTable(startIcon, reverseIcon, returnFunc, scanID)
tsort(iconSortTable[scanID], sort_by_group)
local icon, CustomIcons
if startIcon and type(startIcon) == "table" then--Specific gapped icons
CustomIcons = true
icon = 1
else
icon = startIcon or 1
end
for _, v in ipairs(iconSortTable[scanID]) do
if not self.iconRestore[v] then
local oldIcon = self:GetIcon(v) or 0
self.iconRestore[v] = oldIcon
end
if CustomIcons then
SetRaidTarget(v, startIcon[icon])--do not use SetIcon function again. It already checked in SetSortedIcon function.
icon = icon + 1
if returnFunc then
self[returnFunc](self, v, startIcon[icon])--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
end
else
SetRaidTarget(v, icon)--do not use SetIcon function again. It already checked in SetSortedIcon function.
if reverseIcon then
icon = icon - 1
else
icon = icon + 1
end
if returnFunc then
self[returnFunc](self, v, icon)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
end
end
end
DBM:Schedule(1.5, clearSortTable, scanID)--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.
end
function bossModPrototype:SetSortedIcon(delay, target, startIcon, maxIcon, reverseIcon, returnFunc, scanID)
if not target then return end
if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
return
end
scanID = scanID or 1
if not startIcon then startIcon = 1 end
local uId = DBM:GetRaidUnitId(target)
if uId or UnitExists(target) then--target accepts uid, unitname both.
uId = uId or target
if not iconSortTable[scanID] then iconSortTable[scanID] = {} end
if not iconSet[scanID] then iconSet[scanID] = 0 end
local foundDuplicate = false
for i = #iconSortTable[scanID], 1, -1 do
if iconSortTable[scanID][i] == uId then
foundDuplicate = true
break
end
end
if not foundDuplicate then
iconSet[scanID] = iconSet[scanID] + 1
tinsert(iconSortTable[scanID], uId)
end
self:UnscheduleMethod("SetIconBySortedTable")
if maxIcon and iconSet[scanID] == maxIcon then
self:SetIconBySortedTable(startIcon, reverseIcon, returnFunc, scanID)
elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
self:ScheduleMethod(delay or 0.5, "SetIconBySortedTable", startIcon, reverseIcon, returnFunc, scanID)
end
end
end
end
function bossModPrototype:GetIcon(uIdOrTarget)
local uId = DBM:GetRaidUnitId(uIdOrTarget) or uIdOrTarget
return UnitExists(uId) and GetRaidTargetIndex(uId)
end
function bossModPrototype:RemoveIcon(target)
return self:SetIcon(target, 0)
end
function bossModPrototype:ClearIcons()
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
if UnitExists("raid"..i) and GetRaidTargetIndex("raid"..i) then
SetRaidTarget("raid"..i, 0)
end
end
else
for i = 1, GetNumSubgroupMembers() do
if UnitExists("party"..i) and GetRaidTargetIndex("party"..i) then
SetRaidTarget("party"..i, 0)
end
end
end
end
function bossModPrototype:CanSetIcon(optionName)
if canSetIcons[optionName] then
return true
end
return false
end
local mobUids = {"boss1", "boss2", "boss3", "boss4", "boss5",
"nameplate1", "nameplate2", "nameplate3", "nameplate4", "nameplate5", "nameplate6", "nameplate7", "nameplate8", "nameplate9", "nameplate10",
"nameplate11", "nameplate12", "nameplate13", "nameplate14", "nameplate15", "nameplate16", "nameplate17", "nameplate18", "nameplate19", "nameplate20",
"nameplate21", "nameplate22", "nameplate23", "nameplate24", "nameplate25", "nameplate26", "nameplate27", "nameplate28", "nameplate29", "nameplate30",
"nameplate31", "nameplate32", "nameplate33", "nameplate34", "nameplate35", "nameplate36", "nameplate37", "nameplate38", "nameplate39", "nameplate40",
"mouseover", "target"}
function bossModPrototype:ScanForMobs(creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID, skipMarked)
if not optionName then optionName = self.findFastestComputer[1] end
if canSetIcons[optionName] then
--Declare variables.
DBM:Debug("canSetIcons true", 2)
local timeNow = GetTime()
if not creatureID then--This function must not be used to boss, so remove self.creatureId. Accepts cid, guid and cid table
error("DBM:ScanForMobs calld without creatureID")
return
end
iconSetMethod = iconSetMethod or 0--Set IconSetMethod -- 0: Descending / 1:Ascending / 2: Force Set / 9:Force Stop
scanningTime = scanningTime or 8
maxIcon = maxIcon or 8 --We only have 8 icons.
isFriendly = isFriendly or false
secondCreatureID = secondCreatureID or 0
scanInterval = scanInterval or 0.2
--With different scanID, this function can support multi scanning same time. Required for Nazgrim.
local scanID = 0
if type(creatureID) == "number" then
scanID = creatureID --guid and table no not supports multi scanning. only cid supports multi scanning
end
if iconSetMethod == 9 then--Force stop scanning
--clear variables
scanExpires[scanID] = nil
addsIcon[scanID] = nil
addsIconSet[scanID] = nil
return
end
if not addsIcon[scanID] then addsIcon[scanID] = mobIcon or 8 end
if not addsIconSet[scanID] then addsIconSet[scanID] = 0 end
if not scanExpires[scanID] then scanExpires[scanID] = timeNow + scanningTime end
--DO SCAN NOW
for _, unitid2 in ipairs(mobUids) do
local guid2 = UnitGUID(unitid2)
local cid2 = self:GetCIDFromGUID(guid2)
local isEnemy = UnitIsEnemy("player", unitid2) or true--If api returns nil, assume it's an enemy
local isFiltered = false
if (not isFriendly and not isEnemy) or (skipMarked and not GetRaidTargetIndex(unitid2)) then
isFiltered = true
DBM:Debug("A unit skipped because it's a filtered mob", 3)
end
if not isFiltered then
if guid2 and type(creatureID) == "table" and creatureID[cid2] and not addsGUIDs[guid2] then
DBM:Debug("Match found, SHOULD be setting icon", 2)
if type(creatureID[cid2]) == "number" then
SetRaidTarget(unitid2, creatureID[cid2])
else
SetRaidTarget(unitid2, addsIcon[scanID])
if iconSetMethod == 1 then
addsIcon[scanID] = addsIcon[scanID] + 1
else
addsIcon[scanID] = addsIcon[scanID] - 1
end
end
addsGUIDs[guid2] = true
addsIconSet[scanID] = addsIconSet[scanID] + 1
if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
--clear variables
scanExpires[scanID] = nil
addsIcon[scanID] = nil
addsIconSet[scanID] = nil
return
end
elseif guid2 and ((guid2 == creatureID) or (cid2 == creatureID) or (cid2 == secondCreatureID)) and not addsGUIDs[guid2] then
DBM:Debug("Match found, SHOULD be setting icon", 2)
if iconSetMethod == 2 then
SetRaidTarget(unitid2, mobIcon)
else
SetRaidTarget(unitid2, addsIcon[scanID])
if iconSetMethod == 1 then
addsIcon[scanID] = addsIcon[scanID] + 1
else
addsIcon[scanID] = addsIcon[scanID] - 1
end
end
addsGUIDs[guid2] = true
addsIconSet[scanID] = addsIconSet[scanID] + 1
if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
--clear variables
scanExpires[scanID] = nil
addsIcon[scanID] = nil
addsIconSet[scanID] = nil
return
end
end
end
end
for uId in DBM:GetGroupMembers() do
local unitid = uId.."target"
local guid = UnitGUID(unitid)
local cid = self:GetCIDFromGUID(guid)
local isEnemy = UnitIsEnemy("player", unitid) or true--If api returns nil, assume it's an enemy
local isFiltered = false
if (not isFriendly and not isEnemy) or (skipMarked and not GetRaidTargetIndex(unitid)) then
isFiltered = true
DBM:Debug("ScanForMobs aborting because filtered mob", 2)
end
if not isFiltered then
if guid and type(creatureID) == "table" and creatureID[cid] and not addsGUIDs[guid] then
DBM:Debug("Match found, SHOULD be setting icon", 2)
if type(creatureID[cid]) == "number" then
SetRaidTarget(unitid, creatureID[cid])
else
SetRaidTarget(unitid, addsIcon[scanID])
if iconSetMethod == 1 then
addsIcon[scanID] = addsIcon[scanID] + 1
else
addsIcon[scanID] = addsIcon[scanID] - 1
end
end
addsGUIDs[guid] = true
addsIconSet[scanID] = addsIconSet[scanID] + 1
if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
--clear variables
scanExpires[scanID] = nil
addsIcon[scanID] = nil
addsIconSet[scanID] = nil
return
end
elseif guid and ((guid == creatureID) or (cid == creatureID) or (cid == secondCreatureID)) and not addsGUIDs[guid] then
DBM:Debug("Match found, SHOULD be setting icon", 2)
if iconSetMethod == 2 then
SetRaidTarget(unitid, mobIcon)
else
SetRaidTarget(unitid, addsIcon[scanID])
if iconSetMethod == 1 then
addsIcon[scanID] = addsIcon[scanID] + 1
else
addsIcon[scanID] = addsIcon[scanID] - 1
end
end
addsGUIDs[guid] = true
addsIconSet[scanID] = addsIconSet[scanID] + 1
if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
--clear variables
scanExpires[scanID] = nil
addsIcon[scanID] = nil
addsIconSet[scanID] = nil
return
end
end
end
end
if timeNow < scanExpires[scanID] then--scan for limited times.
self:ScheduleMethod(scanInterval, "ScanForMobs", creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID)
else
DBM:Debug("Stopping ScanForMobs for: "..(optionName or "nil"), 2)
--clear variables
scanExpires[scanID] = nil
addsIcon[scanID] = nil
addsIconSet[scanID] = nil
--Do not wipe adds GUID table here, it's wiped by :Stop() which is called by EndCombat
end
else
DBM:Debug("Not elected to set icons for "..(optionName or "nil"), 2)
end
end
end
-----------------------
-- Model Functions --
-----------------------
function bossModPrototype:SetModelScale(scale)
self.modelScale = scale
end
function bossModPrototype:SetModelOffset(x, y, z)
self.modelOffsetX = x
self.modelOffsetY = y
self.modelOffsetZ = z
end
function bossModPrototype:SetModelRotation(r)
self.modelRotation = r
end
function bossModPrototype:SetModelSequence(v)
self.modelSequence = v
end
function bossModPrototype:SetModelID(id)
self.modelId = id
end
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.
self.modelSoundLong = long
self.modelSoundShort = short
end
--------------------
-- Localization --
--------------------
function bossModPrototype:GetLocalizedStrings()
self.localization.miscStrings.name = self.localization.general.name
return self.localization.miscStrings
end
-- Not really good, needs a few updates
do
local modLocalizations = {}
local modLocalizationPrototype = {}
local mt = {__index = modLocalizationPrototype}
local returnKey = {__index = function(t, k) return k end}
local defaultCatLocalization = {
__index = setmetatable({
timer = L.OPTION_CATEGORY_TIMERS,
announce = L.OPTION_CATEGORY_WARNINGS,
announceother = L.OPTION_CATEGORY_WARNINGS_OTHER,
announcepersonal = L.OPTION_CATEGORY_WARNINGS_YOU,
announcerole = L.OPTION_CATEGORY_WARNINGS_ROLE,
sound = L.OPTION_CATEGORY_SOUNDS,
yell = L.OPTION_CATEGORY_YELLS,
icon = L.OPTION_CATEGORY_ICONS,
nameplate = L.OPTION_CATEGORY_NAMEPLATES,
misc = MISCELLANEOUS
}, returnKey)
}
local defaultTimerLocalization = {
__index = setmetatable({
timer_berserk = L.GENERIC_TIMER_BERSERK,
timer_combat = L.GENERIC_TIMER_COMBAT
}, returnKey)
}
local defaultAnnounceLocalization = {
__index = setmetatable({
warning_berserk = L.GENERIC_WARNING_BERSERK
}, returnKey)
}
local defaultOptionLocalization = {
__index = setmetatable({
timer_berserk = L.OPTION_TIMER_BERSERK,
timer_combat = L.OPTION_TIMER_COMBAT,
}, returnKey)
}
local defaultMiscLocalization = {
__index = {}
}
function modLocalizationPrototype:SetGeneralLocalization(t)
for i, v in pairs(t) do
self.general[i] = v
end
end
function modLocalizationPrototype:SetWarningLocalization(t)
for i, v in pairs(t) do
self.warnings[i] = v
end
end
function modLocalizationPrototype:SetTimerLocalization(t)
for i, v in pairs(t) do
self.timers[i] = v
end
end
function modLocalizationPrototype:SetOptionLocalization(t)
for i, v in pairs(t) do
self.options[i] = v
end
end
function modLocalizationPrototype:SetOptionCatLocalization(t)
for i, v in pairs(t) do
self.cats[i] = v
end
end
function modLocalizationPrototype:SetMiscLocalization(t)
for i, v in pairs(t) do
self.miscStrings[i] = v
end
end
function DBM:CreateModLocalization(name)
name = tostring(name)
local obj = {
general = setmetatable({}, returnKey),
warnings = setmetatable({}, defaultAnnounceLocalization),
options = setmetatable({}, defaultOptionLocalization),
timers = setmetatable({}, defaultTimerLocalization),
miscStrings = setmetatable({}, defaultMiscLocalization),
cats = setmetatable({}, defaultCatLocalization),
}
setmetatable(obj, mt)
modLocalizations[name] = obj
return obj
end
function DBM:GetModLocalization(name)
name = tostring(name)
return modLocalizations[name] or self:CreateModLocalization(name)
end
end