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.
12199 lines
513 KiB
12199 lines
513 KiB
-- *********************************************************
|
|
-- ** Deadly Boss Mods - Core **
|
|
-- ** https://deadlybossmods.com **
|
|
-- ** https://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
|
|
-- * ruRU: TOM_RUS https://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
|
|
--
|
|
local _, private = ...
|
|
|
|
local wowVersionString, wowBuild, _, wowTOC = GetBuildInfo()
|
|
local testBuild = IsTestBuild()
|
|
local isRetail = WOW_PROJECT_ID == (WOW_PROJECT_MAINLINE or 1)
|
|
local isClassic = WOW_PROJECT_ID == (WOW_PROJECT_CLASSIC or 2)
|
|
local isHardcoreServer = C_GameRules and C_GameRules.IsHardcoreActive and C_GameRules.IsHardcoreActive()
|
|
local isBCC = WOW_PROJECT_ID == (WOW_PROJECT_BURNING_CRUSADE_CLASSIC or 5)
|
|
local isWrath = WOW_PROJECT_ID == (WOW_PROJECT_WRATH_CLASSIC or 11)
|
|
--local isCata = WOW_PROJECT_ID == (WOW_PROJECT_CATA_CLASSIC or 99)
|
|
|
|
local DBMPrefix = isClassic and "D5C" or isWrath and "D5WC" or "D5"--D5 will be used for all future classic flavors as well
|
|
local DBMSyncProtocol = 1
|
|
private.DBMPrefix = DBMPrefix
|
|
private.DBMSyncProtocol = DBMSyncProtocol
|
|
|
|
local L = DBM_CORE_L
|
|
local CL = DBM_COMMON_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("20231114043954"),
|
|
}
|
|
|
|
local fakeBWVersion, fakeBWHash = 290, "894cc27"
|
|
local bwVersionResponseString = "V^%d^%s"
|
|
local PForceDisable
|
|
-- The string that is shown as version
|
|
if isRetail then
|
|
DBM.DisplayVersion = "10.2.3"
|
|
DBM.ReleaseRevision = releaseDate(2023, 11, 13) -- the date of the latest stable version that is available, optionally pass hours, minutes, and seconds for multiple releases in one day
|
|
PForceDisable = 8--When this is incremented, trigger force disable regardless of major patch
|
|
elseif isClassic then
|
|
DBM.DisplayVersion = "1.14.50 alpha"
|
|
DBM.ReleaseRevision = releaseDate(2023, 10, 14) -- the date of the latest stable version that is available, optionally pass hours, minutes, and seconds for multiple releases in one day
|
|
PForceDisable = 3--When this is incremented, trigger force disable regardless of major patch
|
|
elseif isBCC then
|
|
DBM.DisplayVersion = "2.6.0 alpha"--When TBC returns (and it will one day). It'll probably be game version 2.6
|
|
DBM.ReleaseRevision = releaseDate(2023, 10, 10) -- the date of the latest stable version that is available, optionally pass hours, minutes, and seconds for multiple releases in one day
|
|
PForceDisable = 2--When this is incremented, trigger force disable regardless of major patch
|
|
elseif isWrath then
|
|
DBM.DisplayVersion = "3.4.55 alpha"
|
|
DBM.ReleaseRevision = releaseDate(2023, 11, 7) -- the date of the latest stable version that is available, optionally pass hours, minutes, and seconds for multiple releases in one day
|
|
PForceDisable = 3--When this is incremented, trigger force disable regardless of major patch
|
|
end
|
|
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
|
|
|
|
function DBM:GetTOC()
|
|
return wowTOC, testBuild, wowVersionString, wowBuild
|
|
end
|
|
|
|
-- dual profile setup
|
|
local _, playerClass = UnitClass("player")
|
|
DBM_UseDualProfile = true
|
|
if playerClass == "MAGE" or playerClass == "WARLOCK" or playerClass == "ROGUE" or (not isRetail and playerClass == "HUNTER") 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 = 566558,--"Sound\\Doodad\\BellTollNightElf.ogg"
|
|
SpecialWarningSound = 569200,--"Sound\\Spells\\PVPFlagTaken.ogg"
|
|
SpecialWarningSound2 = isRetail and 543587 or "Interface\\AddOns\\DBM-Core\\sounds\\ClassicSupport\\UR_Algalon_BHole01.ogg",--"Sound\\Creature\\AlgalonTheObserver\\UR_Algalon_BHole01.ogg"
|
|
SpecialWarningSound3 = "Interface\\AddOns\\DBM-Core\\sounds\\AirHorn.ogg",
|
|
SpecialWarningSound4 = not isClassic and 552035 or "Interface\\AddOns\\DBM-Core\\sounds\\ClassicSupport\\HoodWolfTransformPlayer01.ogg",--"Sound\\Creature\\HoodWolf\\HoodWolfTransformPlayer01.ogg"
|
|
SpecialWarningSound5 = 554236,--"Sound\\Creature\\Loathstare\\Loa_Naxx_Aggro02.ogg"
|
|
ModelSoundValue = "Short",
|
|
CountdownVoice = "Corsica",
|
|
CountdownVoice2 = "Kolt",
|
|
CountdownVoice3 = "Smooth",
|
|
PullVoice = "Corsica",
|
|
ChosenVoicePack2 = (GetLocale() == "enUS" or GetLocale() == "enGB") and "VEM" or "None",
|
|
VPReplacesAnnounce = true,
|
|
VPReplacesSA1 = true,
|
|
VPReplacesSA2 = true,
|
|
VPReplacesSA3 = true,
|
|
VPReplacesSA4 = true,
|
|
VPReplacesGTFO = true,
|
|
VPReplacesCustom = false,
|
|
AlwaysPlayVoice = false,
|
|
VPDontMuteSounds = false,
|
|
EventSoundVictory2 = "Interface\\AddOns\\DBM-Core\\sounds\\Victory\\SmoothMcGroove_Fanfare.ogg",
|
|
EventSoundWipe = "None",
|
|
EventSoundPullTimer = "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,
|
|
DisableRaidIcons = false,
|
|
DisableChatBubbles = false,
|
|
OverrideBossAnnounce = false,
|
|
OverrideBossTimer = false,
|
|
OverrideBossIcon = false,
|
|
OverrideBossSay = false,
|
|
NoAnnounceOverride = true,
|
|
NoTimerOverridee = true,
|
|
ReplaceMyConfigOnOverride = false,
|
|
HideBossEmoteFrame2 = true,
|
|
SWarningAlphabetical = true,
|
|
SWarnNameInNote = true,
|
|
CustomSounds = 0,
|
|
FilterTankSpec = true,
|
|
FilterBTargetFocus = true,
|
|
FilterBInterruptCooldown = true,
|
|
FilterBInterruptHealer = false,
|
|
FilterInterruptNoteName = false,
|
|
FilterTTargetFocus = true,
|
|
FilterTInterruptCooldown = true,
|
|
FilterTInterruptHealer = false,
|
|
FilterDispel = true,
|
|
FilterTrashWarnings2 = true,
|
|
FilterVoidFormSay = true,
|
|
AutologBosses = false,
|
|
AdvancedAutologBosses = false,
|
|
RecordOnlyBosses = false,
|
|
DoNotLogLFG = true,
|
|
LogCurrentMythicRaids = true,
|
|
LogCurrentRaids = true,
|
|
LogCurrentMPlus = true,
|
|
LogCurrentMythicZero = false,
|
|
LogCurrentHeroic = false,
|
|
LogTrivialRaids = false,
|
|
LogTWRaids = false,
|
|
LogTrivialDungeons = false,
|
|
LogTWDungeons = false,
|
|
UseSoundChannel = "Master",
|
|
LFDEnhance = true,
|
|
WorldBossNearAlert = false,
|
|
RLReadyCheckSound = true,
|
|
AFKHealthWarning2 = isHardcoreServer and true or false,
|
|
AutoReplySound = true,
|
|
HideObjectivesFrame = true,
|
|
HideGarrisonToasts = true,
|
|
HideGuildChallengeUpdates = true,
|
|
HideTooltips = false,
|
|
DisableSFX = false,
|
|
EnableModels = true,
|
|
GUIWidth = 800,
|
|
GUIHeight = 600,
|
|
GroupOptionsExcludeIcon = false,
|
|
GroupOptionsExcludePAura = false,
|
|
AutoExpandSpellGroups = not isRetail,
|
|
ShowWAKeys = true,
|
|
--ShowSpellDescWhenExpanded = false,
|
|
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.3,
|
|
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,
|
|
SpecialWarningVibrate1 = false,
|
|
SpecialWarningVibrate2 = false,
|
|
SpecialWarningVibrate3 = true,
|
|
SpecialWarningVibrate4 = true,
|
|
SpecialWarningVibrate5 = true,
|
|
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,
|
|
DontDoSpecialWarningVibrate = false,
|
|
DontPlaySpecialWarningSound = false,
|
|
DontPlayPrivateAuraSound = false,
|
|
DontPlayTrivialSpecialWarningSound = true,
|
|
SpamSpecInformationalOnly = false,
|
|
SpamSpecRoledispel = false,
|
|
SpamSpecRoleinterrupt = false,
|
|
SpamSpecRoledefensive = false,
|
|
SpamSpecRoletaunt = false,
|
|
SpamSpecRolesoak = false,
|
|
SpamSpecRolestack = false,
|
|
SpamSpecRoleswitch = false,
|
|
SpamSpecRolegtfo = false,
|
|
DontShowBossTimers = false,
|
|
DontShowTrashTimers = false,
|
|
DontShowEventTimers = false,
|
|
DontShowUserTimers = false,
|
|
DontShowFarWarnings = true,
|
|
DontSetIcons = false,
|
|
DontRestoreIcons = false,
|
|
DontShowRangeFrame = false,
|
|
DontRestoreRange = false,
|
|
DontShowInfoFrame = false,
|
|
DontShowHudMap2 = false,
|
|
UseNameplateHandoff = true,--Power user setting, no longer shown in GUI
|
|
DontShowNameplateIcons = false,
|
|
DontShowNameplateIconsCD = false,
|
|
DontSendBossGUIDs = false,
|
|
NPAuraText = true,
|
|
NPIconSize = 30,
|
|
NPIconXOffset = 0,
|
|
NPIconYOffset = 0,
|
|
NPIconGrowthDirection = "CENTER",
|
|
DontPlayCountdowns = false,
|
|
DontSendYells = false,
|
|
BlockNoteShare = false,
|
|
DontAutoGossip = false,
|
|
DontShowPT2 = false,
|
|
DontShowPTCountdownText = false,
|
|
DontPlayPTCountdown = false,
|
|
DontShowPTText = false,
|
|
DontShowPTNoID = false,
|
|
PTCountThreshold2 = 5,
|
|
LatencyThreshold = 250,
|
|
oRA3AnnounceConsumables = false,
|
|
SettingsMessageShown = false,
|
|
NewsMessageShown2 = 1,--Apparently varaible without 2 can still exist in some configs (config cleanup of no longer existing variables not working?)
|
|
AlwaysShowSpeedKillTimer2 = false,
|
|
ShowRespawn = true,
|
|
ShowQueuePop = true,
|
|
ShowBerserkWarnings = true,
|
|
HelpMessageVersion = 3,
|
|
MoviesSeen = {},
|
|
MovieFilter2 = "Never",
|
|
LastRevision = 0,
|
|
DebugMode = false,
|
|
DebugLevel = 1,
|
|
RoleSpecAlert = true,
|
|
CheckGear = true,
|
|
WorldBossAlert = not isRetail,
|
|
WorldBuffAlert = not isRetail,
|
|
BadTimerAlert = false,
|
|
AutoAcceptFriendInvite = false,
|
|
AutoAcceptGuildInvite = false,
|
|
FakeBWVersion = false,
|
|
AITimer = true,
|
|
ShortTimerText = true,
|
|
ChatFrame = "DEFAULT_CHAT_FRAME",
|
|
CoreSavedRevision = 1,
|
|
SilentMode = false,
|
|
}
|
|
|
|
DBM.Mods = {}
|
|
DBM.ModLists = {}
|
|
local checkDuplicateObjects = {}
|
|
|
|
------------------------
|
|
-- Global Identifiers --
|
|
------------------------
|
|
DBM_DISABLE_ZONE_DETECTION = newproxy(false)
|
|
DBM_OPTION_SPACER = newproxy(false)
|
|
|
|
--------------
|
|
-- Privates --
|
|
--------------
|
|
private.modSyncSpam = {}
|
|
private.updateFunctions = {}
|
|
--Raid Leader Disable variables
|
|
private.statusGuildDisabled, private.statusWhisperDisabled, private.raidIconsDisabled, private.chatBubblesDisabled = false, false, false, false
|
|
|
|
--------------
|
|
-- Locals --
|
|
--------------
|
|
local bossModPrototype = {}
|
|
local mainFrame = CreateFrame("Frame", "DBMMainFrame")
|
|
local playerName = UnitName("player")
|
|
local playerLevel = UnitLevel("player")
|
|
local playerRealm = GetRealmName()
|
|
local normalizedPlayerRealm = playerRealm:gsub("[%s-]+", "")
|
|
local lastCombatStarted = GetTime()
|
|
local chatPrefixShort = "<" .. L.DBM .. "> "
|
|
local usedProfile = "Default"
|
|
local dbmIsEnabled = true
|
|
private.dbmIsEnabled = dbmIsEnabled
|
|
-- Table variables
|
|
local newerVersionPerson, forceDisablePerson, cSyncSender, eeSyncSender, iconSetRevision, iconSetPerson, loadcIds, inCombat, oocBWComms, combatInfo, bossIds, raid, autoRespondSpam, queuedBattlefield, bossHealth, bossHealthuIdCache, lastBossEngage, lastBossDefeat = {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
|
|
-- False variables
|
|
local voiceSessionDisabled, targetEventsRegistered, combatInitialized, healthCombatInitialized, watchFrameRestore, questieWatchRestore, bossuIdFound, timerRequestInProgress = false, false, false, false, false, false, false, false
|
|
-- Nil variables
|
|
local currentSpecID, currentSpecName, currentSpecGroup, pformat, loadOptions, checkWipe, checkBossHealth, checkCustomBossHealth, fireEvent, LastInstanceType, breakTimerStart, AddMsg, delayedFunction, handleSync, savedDifficulty, difficultyText, difficultyIndex, lastGroupLeader
|
|
-- 0 variables
|
|
local dbmToc, eeSyncReceived, cSyncReceived, showConstantReminder, updateNotificationDisplayed, difficultyModifier, LastGroupSize = 0, 0, 0, 0, 0, 0, 0
|
|
local LastInstanceMapID = -1
|
|
local SWFilterDisabled = 12
|
|
|
|
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
|
|
"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-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
|
|
"DBM-RaidLeadTools", -- Killed plugin
|
|
"DBM-Party-Classic", -- Renamed to DBM-Party-Vanilla
|
|
--The Culling
|
|
"DBM-Ulduar",--Combined into DBM-Raids-WoTLK
|
|
"DBM-VoA",--Combined into DBM-Raids-WoTLK
|
|
"DBM-ChamberOfAspects",--Combined into DBM-Raids-WoTLK
|
|
"DBM-Coliseum",--Combined into DBM-Raids-WoTLK
|
|
"DBM-EyeOfEternity",--Combined into DBM-Raids-WoTLK
|
|
"DBM-Icecrown",--Combined into DBM-Raids-WoTLK
|
|
|
|
"DBM-Onyxia",--Combined into DBM-Raids-WoTLK
|
|
"DBM-Naxx",--Combined into DBM-Raids-WoTLK
|
|
"DBM-ZG", -- Part of Cataclysm party mods on retail, and part on DBM-Raids-BC on classic
|
|
"DBM-AQ20",--Combined into DBM-Raids-Vanilla
|
|
"DBM-AQ40",--Combined into DBM-Raids-Vanilla
|
|
"DBM-BWL",--Combined into DBM-Raids-Vanilla
|
|
"DBM-MC",--Combined into DBM-Raids-Vanilla
|
|
|
|
"DBM-Karazhan",--Combined into DBM-Raids-BC
|
|
"DBM-BlackTemple",--Combined into DBM-Raids-BC
|
|
"DBM-Hyjal",--Combined into DBM-Raids-BC
|
|
"DBM-Sunwell",--Combined into DBM-Raids-BC
|
|
"DBM-TheEye",--Combined into DBM-Raids-BC
|
|
"DBM-Serpentshrine",--Combined into DBM-Raids-BC
|
|
"DBM-ZulAman", -- Part of Cataclysm party mods on retail, and merged into DBM-Raids-BC on classic
|
|
|
|
"DBM-BaradinHold",--Combined into DBM-Raids-Cata
|
|
"DBM-BastionTwilight",--Combined into DBM-Raids-Cata
|
|
"DBM-BlackwingDescent",--Combined into DBM-Raids-Cata
|
|
"DBM-DragonSoul",--Combined into DBM-Raids-Cata
|
|
"DBM-Firelands",--Combined into DBM-Raids-Cata
|
|
"DBM-ThroneFourWinds",--Combined into DBM-Raids-Cata
|
|
|
|
"DBM-HeartofFear",--Combined into DBM-Raids-MoP
|
|
"DBM-MogushanVaults",--Combined into DBM-Raids-MoP
|
|
"DBM-TerraceofEndlessSpring",--Combined into DBM-Raids-MoP
|
|
"DBM-ThroneofThunder",--Combined into DBM-Raids-MoP
|
|
"DBM-SiegeOfOrgrimmarV2",--Combined into DBM-Raids-MoP
|
|
|
|
"DBM-Highmaul",--Combined into DBM-Raids-WoD
|
|
"DBM-BlackrockFoundry",--Combined into DBM-Raids-WoD
|
|
"DBM-HellfireCitadel",--Combined into DBM-Raids-WoD
|
|
|
|
"DBM-AntorusBurningThrone",--Combined into DBM-Raids-Legion
|
|
"DBM-EmeraldNightmare",--Combined into DBM-Raids-Legion
|
|
"DBM-Nighthold",--Combined into DBM-Raids-Legion
|
|
"DBM-TombofSargeras",--Combined into DBM-Raids-Legion
|
|
"DBM-TrialofValor",--Combined into DBM-Raids-Legion
|
|
|
|
"DBM-Uldir",--Combined into DBM-Raids-BfA
|
|
"DBM-CrucibleofStorms",--Combined into DBM-Raids-BfA
|
|
"DBM-ZuldazarRaid",--Combined into DBM-Raids-BfA
|
|
"DBM-EternalPalace",--Combined into DBM-Raids-BfA
|
|
"DBM-Nyalotha",--Combined into DBM-Raids-BfA
|
|
|
|
"DBM-SanctumOfDomination",--Combined into DBM-Raids-Shadowlands
|
|
"DBM-CastleNathria",--Combined into DBM-Raids-Shadowlands
|
|
"DBM-Sepulcher",--Combined into DBM-Raids-Shadowlands
|
|
|
|
"DBM-VaultoftheIncarnates",--Combined into DBM-Raids-Dragonflight
|
|
|
|
"DBM-DMF",--Combined into DBM-WorldEvents
|
|
}
|
|
if isRetail then
|
|
--Retail doesn't use this folder, classic era, bc, and wrath still do
|
|
table.insert(bannedMods, "DBM-Azeroth")--Merged into DBM-Core events mod.
|
|
end
|
|
|
|
--[InstanceID]={level,zoneType}
|
|
--zoneType: 1 = outdoor, 2 = dungeon, 3 = raid
|
|
local instanceDifficultyBylevel
|
|
if isRetail then
|
|
instanceDifficultyBylevel = {
|
|
--World
|
|
[0]={50,1},[1]={50, 1},--Eastern Kingdoms and Kalimdor world events/bosses. 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
|
|
[2222]={60, 1},[2374]={60, 1},--Shadowlands World Bosses
|
|
[2444]={70, 1},[2512]={70, 1},[2574]={70, 1},[2454]={70, 1},[2548]={70, 1},--Dragonflight World Bosses
|
|
--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},[2481]={60, 3},--Shadowlands Raids (yes, only 3 kekw, seconded)
|
|
[2522]={70, 3},[2569]={70, 3},[2549]={70, 3},--Dragonflight 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
|
|
[2520]={70, 2},[2451]={70, 2},[2516]={70, 2},[2519]={70, 2},[2526]={70, 2},[2515]={70, 2},[2521]={70, 2},[2527]={70, 2},[2579]={70, 2},--Dragonflight Dungeons
|
|
}
|
|
elseif isWrath then--Since naxx is moved to northrend, wrath can't use tbc/classics table
|
|
instanceDifficultyBylevel = {
|
|
--World
|
|
[0]={60,1},[1]={60, 1},--Eastern Kingdoms and Kalimdor world bosses.
|
|
[530]={70, 1},--Outlands World Bosses
|
|
--Raids
|
|
[509]={60, 3},[531]={60, 3},[469]={60, 3},[409]={60, 3},[309]={60, 3},--Classic Raids (309 is legacy ZG)
|
|
[564]={70, 3},[534]={70, 3},[532]={70, 3},[565]={70, 3},[544]={70, 3},[548]={70, 3},[580]={70, 3},[550]={70, 3},[568]={70, 3},--BC Raids (568 is legacy ZA)
|
|
[615]={80, 3},[724]={80, 3},[649]={80, 3},[616]={80, 3},[631]={80, 3},[533]={80, 3},[249]={80, 3},[603]={80, 3},[624]={80, 3},--Wrath Raids
|
|
--Dungeons
|
|
[429]={45, 2},[389]={18, 2},[349]={52, 2},[329]={60, 2},[289]={60, 2},[230]={60, 2},[229]={60, 2},[209]={54, 2},[189]={45, 2},[129]={47, 2},[109]={60, 2},[90]={34, 2},[70]={52, 2},[48]={32, 2},[47]={42, 2},[43]={27, 2},[36]={25, 2},[34]={32, 2},[33]={30, 2},--Classic Dungeons
|
|
[540]={70, 2},[558]={70, 2},[556]={70, 2},[555]={70, 2},[542]={70, 2},[546]={70, 2},[545]={70, 2},[547]={70, 2},[553]={70, 2},[554]={70, 2},[552]={70, 2},[557]={70, 2},[269]={70, 2},[560]={70, 2},[543]={70, 2},[585]={70, 2},--BC Dungeons
|
|
[619]={80, 2},[601]={80, 2},[595]={80, 2},[600]={80, 2},[604]={80, 2},[602]={80, 2},[599]={80, 2},[576]={80, 2},[578]={80, 2},[574]={80, 2},[575]={80, 2},[608]={80, 2},[658]={80, 2},[632]={80, 2},[668]={80, 2},[650]={80, 2},--Wrath Dungeons
|
|
}
|
|
else--TBC and Vanilla
|
|
instanceDifficultyBylevel = {
|
|
--World
|
|
[0]={60,1},[1]={60, 1},--Eastern Kingdoms and Kalimdor world bosses.
|
|
[530]={70, 1},--Outlands World Bosses
|
|
--Raids
|
|
[509]={60, 3},[531]={60, 3},[469]={60, 3},[409]={60, 3},[533]={60, 3},[309]={60, 3},[249]={60, 3},--Classic Raids (309 is legacy ZG)
|
|
[564]={70, 3},[534]={70, 3},[532]={70, 3},[565]={70, 3},[544]={70, 3},[548]={70, 3},[580]={70, 3},[550]={70, 3},[568]={70, 3},--BC Raids (568 is legacy ZA)
|
|
--Dungeons
|
|
[429]={45, 2},[389]={18, 2},[349]={52, 2},[329]={60, 2},[289]={60, 2},[230]={60, 2},[229]={60, 2},[209]={54, 2},[189]={45, 2},[129]={47, 2},[109]={60, 2},[90]={34, 2},[70]={52, 2},[48]={32, 2},[47]={42, 2},[43]={27, 2},[36]={25, 2},[34]={32, 2},[33]={30, 2},--Classic Dungeons
|
|
[540]={70, 2},[558]={70, 2},[556]={70, 2},[555]={70, 2},[542]={70, 2},[546]={70, 2},[545]={70, 2},[547]={70, 2},[553]={70, 2},[554]={70, 2},[552]={70, 2},[557]={70, 2},[269]={70, 2},[560]={70, 2},[543]={70, 2},[585]={70, 2},--BC Dungeons
|
|
}
|
|
end
|
|
|
|
-----------------
|
|
-- Libraries --
|
|
-----------------
|
|
local LibStub = _G["LibStub"]
|
|
local LibSpec
|
|
do
|
|
if isRetail and LibStub then
|
|
LibSpec = LibStub("LibSpecialization", true)
|
|
if LibSpec then
|
|
local function update(specID, _, _, playerName)
|
|
if raid[playerName] then
|
|
raid[playerName].specID = specID
|
|
end
|
|
end
|
|
LibSpec:Register(DBM, update)
|
|
end
|
|
end
|
|
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 mabs, floor, mhuge, mmin, mmax, mrandom = math.abs, 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 UnitTokenFromGUID, UnitPercentHealthFromGUID = UnitTokenFromGUID, UnitPercentHealthFromGUID
|
|
local GetSpellInfo, GetDungeonInfo, GetSpellTexture, GetSpellCooldown = GetSpellInfo, C_LFGInfo and C_LFGInfo.GetDungeonInfo or GetDungeonInfo, GetSpellTexture, GetSpellCooldown
|
|
local GetAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or GetAddOnMetadata
|
|
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 = PlaySoundFile
|
|
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 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
|
|
|
|
local function OrderedTable()
|
|
local nextkey, firstkey = {}, {}
|
|
nextkey[nextkey] = firstkey
|
|
|
|
local function onext(self, key)
|
|
while key ~= nil do
|
|
key = nextkey[key]
|
|
local val = self[key]
|
|
if val ~= nil then
|
|
return key, val
|
|
end
|
|
end
|
|
end
|
|
|
|
local selfmeta = firstkey
|
|
selfmeta.__nextkey = nextkey
|
|
|
|
function selfmeta:__newindex(key, val)
|
|
rawset(self, key, val)
|
|
if nextkey[key] == nil then
|
|
nextkey[nextkey[nextkey]] = key
|
|
nextkey[nextkey] = key
|
|
end
|
|
end
|
|
|
|
function selfmeta:__pairs() return
|
|
onext, self, firstkey
|
|
end
|
|
|
|
return setmetatable({}, selfmeta)
|
|
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
|
|
return not (filterRaid and DBM:GetRaidUnitId(toonName)) -- Person is in raid group and filter raid enabled
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
--Check if it's a non bnet friend
|
|
local friendInfo = C_FriendList.GetFriendInfo(sender)
|
|
if friendInfo then
|
|
return not (filterRaid and DBM:GetRaidUnitId(friendInfo.name)) -- Person is in raid group and filter raid enabled
|
|
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
|
|
return not (filterRaid and DBM:GetRaidUnitId(name))
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- automatically sends an addon message to the appropriate channel (INSTANCE_CHAT, RAID or PARTY)
|
|
local function sendSync(protocol, prefix, msg)
|
|
if dbmIsEnabled or prefix == "V" or prefix == "H" then--Only show version checks if force disabled, nothing else
|
|
msg = msg or ""
|
|
local fullname = playerName.."-"..normalizedPlayerRealm
|
|
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(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, "INSTANCE_CHAT")
|
|
else
|
|
if IsInRaid() then
|
|
SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, "RAID")
|
|
elseif IsInGroup(1) then
|
|
SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, "PARTY")
|
|
else--for solo raid
|
|
handleSync("SOLO", playerName, nil, (protocol or DBMSyncProtocol), prefix, strsplit("\t", msg))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
private.sendSync = sendSync
|
|
|
|
local function sendGuildSync(protocol, prefix, msg)
|
|
if IsInGuild() and (dbmIsEnabled or prefix == "V" or prefix == "H") then--Only show version checks if force disabled, nothing else
|
|
msg = msg or ""
|
|
local fullname = playerName.."-"..normalizedPlayerRealm
|
|
SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix.."\t"..msg, "GUILD")--Even guild syncs send realm so we can keep antispam the same across realid as well.
|
|
end
|
|
end
|
|
private.sendGuildSync = sendGuildSync
|
|
|
|
--Custom sync function that should only be used for user generated sync messages
|
|
local function sendLoggedSync(protocol, prefix, msg)
|
|
if dbmIsEnabled then
|
|
msg = msg or ""
|
|
local fullname = playerName.."-"..normalizedPlayerRealm
|
|
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(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, "INSTANCE_CHAT")
|
|
else
|
|
if IsInRaid() then
|
|
C_ChatInfo.SendAddonMessageLogged(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, "RAID")
|
|
elseif IsInGroup(1) then
|
|
C_ChatInfo.SendAddonMessageLogged(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, "PARTY")
|
|
else--for solo raid
|
|
handleSync("SOLO", playerName, nil, (protocol or DBMSyncProtocol), prefix, strsplit("\t", msg))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--Sync Object specifically for out in the world sync messages that have different rules than standard syncs
|
|
local function SendWorldSync(self, protocol, prefix, msg, noBNet)
|
|
if not dbmIsEnabled then return end--Block all world syncs if force disabled
|
|
DBM:Debug("SendWorldSync running for "..prefix)
|
|
local fullname = playerName.."-"..normalizedPlayerRealm
|
|
if IsInRaid() then
|
|
SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix.."\t"..msg, "RAID")
|
|
elseif IsInGroup(1) then
|
|
SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix.."\t"..msg, "PARTY")
|
|
else--for solo raid
|
|
handleSync("SOLO", playerName, nil, (protocol or DBMSyncProtocol), prefix, strsplit("\t", msg))
|
|
end
|
|
if IsInGuild() then
|
|
SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix.."\t"..msg, "GUILD")--Even guild syncs send realm so we can keep antispam the same across realid as well.
|
|
end
|
|
if self.Options.EnableWBSharing and not noBNet then
|
|
local _, numBNetOnline = BNGetNumFriends()
|
|
local connectedServers = GetAutoCompleteRealms()
|
|
for i = 1, numBNetOnline do
|
|
local gameAccountID, isOnline, realmName
|
|
local accountInfo = C_BattleNet.GetFriendAccountInfo(i)
|
|
if accountInfo then
|
|
gameAccountID, isOnline, realmName = accountInfo.gameAccountInfo.gameAccountID, accountInfo.gameAccountInfo.isOnline, accountInfo.gameAccountInfo.realmName
|
|
end
|
|
if gameAccountID and isOnline and realmName then
|
|
local sameRealm = false
|
|
if connectedServers then
|
|
for j = 1, #connectedServers do
|
|
if realmName == connectedServers[j] then
|
|
sameRealm = true
|
|
break
|
|
end
|
|
end
|
|
else
|
|
if realmName == playerRealm or realmName == normalizedPlayerRealm then
|
|
sameRealm = true
|
|
end
|
|
end
|
|
if sameRealm then
|
|
BNSendGameData(gameAccountID, DBMPrefix, DBMSyncProtocol .. "\t" .. prefix.."\t"..msg)--Just send users realm for pull, so we can eliminate connectedServers checks on sync handler
|
|
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)
|
|
return cap1 == "%" and CL.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)
|
|
end
|
|
elseif type(target) == "string" then
|
|
SendChatMessage(msg, "WHISPER", nil, target) -- Whispering to ourselves here is okay and somewhat useful for whisper-warnings
|
|
end
|
|
end
|
|
|
|
--Another custom server name strip function that first strips out the "><" DBM wraps around playernames
|
|
local function stripServerName(cap)
|
|
return DBM:GetShortServerName(cap:sub(2, -2))
|
|
end
|
|
|
|
local function parseSpellIcon(spellId, objectType, fallbackIcon)
|
|
local icon
|
|
if objectType and objectType == "achievement" then
|
|
icon = select(10, GetAchievementInfo(spellId))
|
|
elseif type(spellId) == "string" then--Journal ID in old format
|
|
if spellId:match("ej%d+") then
|
|
icon = select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3)))
|
|
else--Icon texture ID (passed as string by module so core knows it's a FDID and not spellID
|
|
icon = spellId
|
|
end
|
|
elseif type(spellId) == "number" then--SpellId or journal Id
|
|
if spellId < 0 then--Journal ID in new format
|
|
icon = select(4, DBM:EJ_GetSectionInfo(-spellId))
|
|
else--SpellId
|
|
icon = spellId >= 6 and GetSpellTexture(spellId)
|
|
end
|
|
end
|
|
return icon or fallbackIcon or 136116
|
|
end
|
|
|
|
local function parseSpellName(spellId, objectType)
|
|
local spellName
|
|
if objectType and objectType == "achievement" then
|
|
spellName = select(2, GetAchievementInfo(spellId))
|
|
elseif type(spellId) == "string" and spellId:match("ej%d+") then--Old Journal Format
|
|
spellName = DBM:EJ_GetSectionInfo(string.sub(spellId, 3))
|
|
elseif type(spellId) == "number" then
|
|
if spellId < 0 then--New Journal Format
|
|
spellName = DBM:EJ_GetSectionInfo(-spellId)
|
|
else
|
|
spellName = DBM:GetSpellInfo(spellId)
|
|
end
|
|
end
|
|
return spellName
|
|
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 exclusively used in classic era to make it a little cleaner to mass unifiy modules to auto check spellid or spellName based on game flavor
|
|
function argsMT.__index:IsSpell(...)
|
|
if isClassic then
|
|
--ugly ass performance wasting checks that have to first convert Ids to names because #nochanges
|
|
for _, spellId in ipairs({...}) do
|
|
local spellName = DBM:GetSpellInfo(spellId)
|
|
if spellName and spellName == args.spellName then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
else
|
|
--Just simple table comoparison
|
|
return tIndexOf({...}, args.spellId) ~= nil
|
|
end
|
|
end
|
|
|
|
function argsMT.__index:IsPlayer()
|
|
-- If blizzard ever removes destFlags, this will automatically switch to fallback
|
|
if args.destFlags and COMBATLOG_OBJECT_AFFILIATION_MINE then
|
|
return bband(args.destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
|
|
else
|
|
return raid[args.destName] ~= nil--Unit in group, friendly
|
|
end
|
|
end
|
|
|
|
function argsMT.__index:IsPlayerSource()
|
|
-- If blizzard ever removes sourceFlags, this will automatically switch to fallback
|
|
if args.sourceFlags and COMBATLOG_OBJECT_AFFILIATION_MINE then
|
|
return bband(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
|
|
else
|
|
return raid[args.sourceName] ~= nil -- Unit in group, friendly
|
|
end
|
|
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()
|
|
-- If blizzard ever removes sourceFlags, this will automatically switch to fallback
|
|
if args.sourceFlags and COMBATLOG_OBJECT_TYPE_PLAYER then
|
|
return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
|
|
else
|
|
return raid[args.sourceName] ~= nil -- Unit in group, friendly
|
|
end
|
|
end
|
|
|
|
function argsMT.__index:IsDestTypePlayer()
|
|
-- If blizzard ever removes destFlags, this will automatically switch to fallback
|
|
if args.destFlags and COMBATLOG_OBJECT_TYPE_PLAYER then
|
|
return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
|
|
else
|
|
return raid[args.destName] ~= nil -- Unit in group, friendly
|
|
end
|
|
end
|
|
|
|
function argsMT.__index:IsSrcTypeHostile()
|
|
-- If blizzard ever removes sourceFlags, this will automatically switch to fallback
|
|
if args.sourceFlags and COMBATLOG_OBJECT_REACTION_HOSTILE then
|
|
return bband(args.sourceFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
|
|
else
|
|
return raid[args.sourceName] ~= nil -- Unit in group, friendly
|
|
end
|
|
end
|
|
|
|
function argsMT.__index:IsDestTypeHostile()
|
|
-- If blizzard ever removes destFlags, this will automatically switch to fallback
|
|
if args.destFlags and COMBATLOG_OBJECT_REACTION_HOSTILE then
|
|
return bband(args.destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
|
|
else
|
|
return raid[args.destName] ~= nil -- Unit in group, friendly
|
|
end
|
|
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, _, ...)
|
|
-- 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:Debug("DBM RegisterEvents Warning: "..spellId.." is not a number!")
|
|
return
|
|
end
|
|
local spellName = DBM:GetSpellInfo(spellId)
|
|
if spellId and not spellName then
|
|
DBM:Debug("DBM RegisterEvents Warning: "..spellId.." spell id does not exist!")
|
|
return
|
|
end
|
|
if not registeredSpellIds[event] then
|
|
registeredSpellIds[event] = {}
|
|
end
|
|
if isClassic then
|
|
if not registeredSpellIds[event][spellName] then--Don't register duplicate spell Names
|
|
registeredSpellIds[event][spellName] = (registeredSpellIds[event][spellName] or 0) + 1--But classic needs spellNames
|
|
end
|
|
else
|
|
registeredSpellIds[event][spellId] = (registeredSpellIds[event][spellId] or 0) + 1
|
|
end
|
|
end
|
|
|
|
function unregisterSpellId(event, spellId)
|
|
if not registeredSpellIds[event] then return end
|
|
local spellName = DBM:GetSpellInfo(spellId)
|
|
if spellId and not spellName then
|
|
DBM:Debug("DBM unregisterSpellId Warning: "..spellId.." spell id does not exist!")
|
|
return
|
|
end
|
|
local regName = isClassic and spellName or spellId
|
|
local refs = (registeredSpellIds[event][regName] or 1) - 1
|
|
registeredSpellIds[event][regName] = refs
|
|
if refs <= 0 then
|
|
registeredSpellIds[event][regName] = 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 _, event in ipairs({...}) do
|
|
-- 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(-11) ~= "_UNFILTERED" then -- no arguments given, support for legacy mods
|
|
eventWithArgs = event .. " boss1 boss2 boss3 boss4 boss5 target"
|
|
if not isClassic then
|
|
eventWithArgs = eventWithArgs .. " focus"
|
|
end
|
|
event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", eventWithArgs)
|
|
end
|
|
if event:sub(-11) == "_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(-11) == "_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(-11) ~= "_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(...)
|
|
DBM:Debug("RegisterShortTermEvents fired", 2)
|
|
local _shortTermRegisterEvents = {...}
|
|
for k, v in pairs(_shortTermRegisterEvents) do
|
|
if v:sub(0, 5) == "UNIT_" and v:sub(-11) ~= "_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: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()
|
|
DBM:Debug("UnregisterShortTermEvents fired", 2)
|
|
if self.shortTermRegisterEvents then
|
|
DBM:Debug("UnregisterShortTermEvents found registered shortTermRegisterEvents", 2)
|
|
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(-11) ~= "_UNFILTERED" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED") then
|
|
unregisterUEvent(self, event)
|
|
DBM:Debug("unregisterUEvent for unit event "..event.." unregistered", 3)
|
|
end
|
|
if #mods == 0 then
|
|
registeredEvents[event] = nil
|
|
DBM:Debug("registeredEvents for event "..event.." nilled", 3)
|
|
end
|
|
end
|
|
end
|
|
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][isClassic and extraArg2 or extraArg1] then return end -- SpellName filter for Classic
|
|
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 and Ambiguate(sourceName, "none"), sourceFlags, sourceRaidFlags, destGUID, destName and Ambiguate(destName, "none"), 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 and Ambiguate(sourceName, "none")
|
|
args.sourceFlags = sourceFlags
|
|
args.sourceRaidFlags = sourceRaidFlags
|
|
args.destGUID = destGUID
|
|
args.destName = destName and Ambiguate(destName, "none")
|
|
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, 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.ChosenVoicePack2
|
|
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
|
|
--Since VEM is now bundled, users may elect to disable it by simiply disabling the module
|
|
--let's not nag them, only remind for 3rd party because then we know user installed it themselves
|
|
if activeVP ~= "VEM" then
|
|
AddMsg(self, L.VOICE_MISSING)
|
|
end
|
|
end
|
|
end
|
|
--Check if any of countdown sounds are using missing voice pack
|
|
local found1, found2, found3, found4 = false, false, false, false
|
|
for _, count in pairs(DBM:GetCountSounds()) do
|
|
local voice = count.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
|
|
if voice == self.Options.PullVoice then
|
|
found4 = 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
|
|
if not found4 then
|
|
AddMsg(self, L.VOICE_COUNT_MISSING:format(4, self.DefaultOptions.PullVoice))
|
|
self.Options.PullVoice = self.DefaultOptions.PullVoice
|
|
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
|
|
sendGuildSync(DBMSyncProtocol, "GH")
|
|
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" .. (isClassic and "-Classic" or isBCC and "-BCC" or isWrath and "-Wrath" or "")))
|
|
isLoaded = true
|
|
for _, v in ipairs(onLoadCallbacks) do
|
|
xpcall(v, geterrorhandler())
|
|
end
|
|
onLoadCallbacks = nil
|
|
loadOptions(self)
|
|
DBT:LoadOptions("DBM")
|
|
self.AddOns = {}
|
|
private:OnModuleLoad()
|
|
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
|
|
--DBM plater nameplate cooldown icons are enabled, but platers are not. Inform user feature is not fully enabled or fully disabled
|
|
if Plater and not Plater.db.profile.bossmod_support_bars_enabled and not DBM.Options.DontShowNameplateIconsCD then
|
|
C_TimerAfter(15, function() AddMsg(self, L.PLATER_NP_AURAS_MSG) end)
|
|
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
|
|
self.Arrow:LoadPosition()
|
|
-- LibDBIcon setup
|
|
if type(DBM_MinimapIcon) ~= "table" then
|
|
DBM_MinimapIcon = {}
|
|
end
|
|
if LibStub and LibStub("LibDBIcon-1.0", true) then
|
|
LibStub("LibDBIcon-1.0"):Register("DBM", private.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.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 "")}
|
|
|
|
local minToc = tonumber(GetAddOnMetadata(i, "X-Min-Interface") or 0)
|
|
if isBCC then
|
|
minToc = tonumber(GetAddOnMetadata(i, "X-Min-Interface-BCC") or minToc)
|
|
elseif isWrath then
|
|
minToc = tonumber(GetAddOnMetadata(i, "X-Min-Interface-Wrath") or minToc)
|
|
end
|
|
|
|
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 = isWrath and GetAddOnMetadata(i, "X-DBM-StatTypes-Wrath") or GetAddOnMetadata(i, "X-DBM-StatTypes") or "",
|
|
name = GetAddOnMetadata(i, "X-DBM-Mod-Name") or GetRealZoneText(tonumber(mapIdTable[1])) or CL.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 = minToc,
|
|
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
|
|
self.AddOns[#self.AddOns].subTabs[k] = strsplit("-", GetRealZoneText(id):trim() or id)--For handling zones like Warfront: Arathi - Alliance
|
|
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
|
|
DBM:AddCountSound(GetAddOnMetadata(i, "X-DBM-Voice-Name"), "VP:"..voiceValue, "Interface\\AddOns\\DBM-VP"..voiceValue.."\\count\\")
|
|
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",
|
|
"ENCOUNTER_START",
|
|
"ENCOUNTER_END",
|
|
"BOSS_KILL",
|
|
"UNIT_DIED",
|
|
"UNIT_DESTROYED",
|
|
"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",
|
|
"READY_CHECK",
|
|
"UPDATE_BATTLEFIELD_STATUS",
|
|
"PLAY_MOVIE",
|
|
"CINEMATIC_START",
|
|
"CINEMATIC_STOP",
|
|
"PLAYER_LEVEL_CHANGED",
|
|
"PARTY_INVITE_REQUEST",
|
|
"LOADING_SCREEN_DISABLED",
|
|
"LOADING_SCREEN_ENABLED"
|
|
)
|
|
if not isClassic then -- Retail, WoTLKC, and BCC
|
|
self:RegisterEvents(
|
|
"LFG_PROPOSAL_FAILED",
|
|
"LFG_PROPOSAL_SHOW",
|
|
"LFG_PROPOSAL_SUCCEEDED",
|
|
"LFG_ROLE_CHECK_SHOW"
|
|
)
|
|
end
|
|
if isRetail then
|
|
self:RegisterEvents(
|
|
"UNIT_HEALTH mouseover target focus player",--Is Frequent on retail, and _FREQUENT deleted
|
|
"CHALLENGE_MODE_RESET",
|
|
"PLAYER_SPECIALIZATION_CHANGED",
|
|
"SCENARIO_COMPLETED",
|
|
"GOSSIP_SHOW"
|
|
)
|
|
elseif isWrath then -- WoTLKC
|
|
self:RegisterEvents(
|
|
"UNIT_HEALTH_FREQUENT mouseover target focus player",--Still exists in classic and non frequent is slow and less reliable
|
|
"CHARACTER_POINTS_CHANGED",
|
|
"PLAYER_TALENT_UPDATE"
|
|
)
|
|
else -- BCC and Classic
|
|
self:RegisterEvents(
|
|
"UNIT_HEALTH_FREQUENT mouseover target focus player",--Still exists in classic and non frequent is slow and less reliable
|
|
"CHARACTER_POINTS_CHANGED"
|
|
)
|
|
end
|
|
if RolePollPopup and RolePollPopup:IsEventRegistered("ROLE_POLL_BEGIN") and isRetail 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: DBM: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
|
|
error("Usage: DBM:UnregisterCallback(event, callbackFunc)", 2)
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------
|
|
-- OnUpdate/Scheduler --
|
|
--------------------------
|
|
local DBMScheduler = private:GetModule("DBMScheduler")
|
|
|
|
function DBM:Schedule(t, f, ...)
|
|
return DBMScheduler:Schedule(t, f, nil, ...)
|
|
end
|
|
|
|
function DBM:Unschedule(f, ...)
|
|
return DBMScheduler: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 CL.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 CL.UNKNOWN))
|
|
return
|
|
elseif name == usedProfile then
|
|
self:AddMsg(L.PROFILE_COPY_ERROR_SELF)
|
|
return
|
|
end
|
|
DBM_AllSavedOptions[usedProfile] = CopyTable(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 CL.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 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)
|
|
local sortMe, outdatedUsers = {}, {}
|
|
for _, v in pairs(raid) do
|
|
tinsert(sortMe, v)
|
|
end
|
|
tsort(sortMe, Sort)
|
|
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 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 TotalUsers = #sortMe
|
|
self:AddMsg("---", false)
|
|
self:AddMsg(L.VERSIONCHECK_FOOTER:format(TotalUsers - NoDBM, TotalUsers - NoBigwigs), false)
|
|
self:AddMsg(L.VERSIONCHECK_OUTDATED:format(OldMod, #outdatedUsers > 0 and tconcat(outdatedUsers, ", ") or NONE), false)
|
|
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(DBMPrefix, (DBMSyncProtocol .. "\tUW\t0\t%s"):format(text), "WHISPER", whisperTarget)
|
|
else
|
|
sendLoggedSync(DBMSyncProtocol, "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, isRetail and 237538 or 134376)
|
|
fireEvent("DBM_TimerStart", "DBMPizzaTimer", text, time, isRetail and "237538" or "134376", "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(DBMPrefix, (DBMSyncProtocol .. "\tUW\t%s\t%s"):format(time, text), "WHISPER", whisperTarget)
|
|
else
|
|
sendLoggedSync(DBMSyncProtocol, "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
|
|
|
|
-----------------
|
|
-- 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, "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:ForceDisableSpam()
|
|
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(CL.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
|
|
|
|
-------------------------------------------------
|
|
-- Raid/Party Handling and Unit ID Utilities --
|
|
-------------------------------------------------
|
|
do
|
|
local UnitInRaid = UnitInRaid
|
|
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
|
|
twipe(newerVersionPerson)--Wipe guild syncs on group join so we trigger a new out of date notice on raid join even if one triggered on login
|
|
twipe(forceDisablePerson)
|
|
inRaid = true
|
|
sendSync(DBMSyncProtocol, "H")
|
|
if dbmIsEnabled then
|
|
SendAddonMessage("BigWigs", bwVersionQueryString:format(0, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "RAID")
|
|
end
|
|
if isRetail then
|
|
self:Schedule(2, self.RoleCheck, false, self)
|
|
end
|
|
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, _, isOnline = 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
|
|
raid[name].isOnline = isOnline
|
|
raidGuids[UnitGUID(id) or ""] = name
|
|
if rank == 2 then
|
|
lastGroupLeader = name
|
|
end
|
|
end
|
|
end
|
|
private.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)
|
|
removeEntry(forceDisablePerson, i)
|
|
fireEvent("DBM_raidLeave", i)
|
|
else
|
|
v.updated = nil
|
|
if v.revision and v.isOnline 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
|
|
private.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
|
|
private.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
|
|
--Recheck elected icon if group changed mid combat, so we don't end up in situation no icons are set because setter bounced
|
|
if #inCombat > 0 then--At least one boss is engaged
|
|
for i = #inCombat, 1, -1 do
|
|
local mod = inCombat[i]
|
|
if mod then
|
|
self:ElectIconSetter(mod)
|
|
end
|
|
end
|
|
end
|
|
elseif IsInGroup() then
|
|
if not inRaid then
|
|
-- joined a new party
|
|
twipe(newerVersionPerson)--Wipe guild syncs on group join so we trigger a new out of date notice on raid join even if one triggered on login
|
|
twipe(forceDisablePerson)
|
|
inRaid = true
|
|
sendSync(DBMSyncProtocol, "H")
|
|
if dbmIsEnabled then
|
|
SendAddonMessage("BigWigs", bwVersionQueryString:format(0, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "PARTY")
|
|
end
|
|
if isRetail then
|
|
self:Schedule(2, self.RoleCheck, false, self)
|
|
end
|
|
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)
|
|
local isOnline = UnitIsConnected(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
|
|
raid[name].isOnline = isOnline
|
|
raidGuids[UnitGUID(id) or ""] = name
|
|
if rank >= 1 then
|
|
lastGroupLeader = name
|
|
end
|
|
end
|
|
private.enableIcons = false
|
|
twipe(iconSeter)
|
|
for k, v in pairs(raid) do
|
|
if not v.updated then
|
|
raidGuids[v.guid] = nil
|
|
raid[k] = nil
|
|
removeEntry(newerVersionPerson, k)
|
|
removeEntry(forceDisablePerson, k)
|
|
fireEvent("DBM_partyLeave", k)
|
|
else
|
|
v.updated = nil
|
|
if v.revision and v.isOnline 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
|
|
private.enableIcons = true
|
|
end
|
|
end
|
|
--Recheck elected icon if group changed mid combat, so we don't end up in situation no icons are set because setter bounced
|
|
if #inCombat > 0 then--At least one boss is engaged
|
|
for i = #inCombat, 1, -1 do
|
|
local mod = inCombat[i]
|
|
if mod then
|
|
self:ElectIconSetter(mod)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- left the current group/raid
|
|
inRaid = false
|
|
private.enableIcons = true
|
|
fireEvent("DBM_raidLeave", playerName)
|
|
twipe(raid)
|
|
twipe(newerVersionPerson)
|
|
twipe(forceDisablePerson)
|
|
-- 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
|
|
lastGroupLeader = nil
|
|
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:GetNumGuildPlayersInZone() -- Classic/BCC only
|
|
if not IsInGroup() then return 1 end
|
|
local total = 0
|
|
local myGuild = GetGuildInfo("player")
|
|
if IsInRaid() then
|
|
for i = 1, GetNumGroupMembers() do
|
|
local unitGuild = GetGuildInfo("raid"..i)--This api only works if unit is nearby, so don't even need to check location
|
|
if unitGuild and unitGuild == myGuild then
|
|
total = total + 1
|
|
end
|
|
end
|
|
else
|
|
total = 1--add player/self for "party" count
|
|
for i = 1, GetNumSubgroupMembers() do
|
|
local unitGuild = GetGuildInfo("party"..i)--This api only works if unit is nearby, so don't even need to check location
|
|
if unitGuild and unitGuild == myGuild 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:GetRaidRoster(name)
|
|
if name then
|
|
return raid[name] ~= nil
|
|
end
|
|
return raid
|
|
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, 10 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
|
|
|
|
local fullUids = {
|
|
"boss1", "boss2", "boss3", "boss4", "boss5", "boss6", "boss7", "boss8", "boss9", "boss10",
|
|
"mouseover", "target", "focus", "focustarget", "targettarget", "mouseovertarget",
|
|
"party1target", "party2target", "party3target", "party4target",
|
|
"raid1target", "raid2target", "raid3target", "raid4target", "raid5target", "raid6target", "raid7target", "raid8target", "raid9target", "raid10target",
|
|
"raid11target", "raid12target", "raid13target", "raid14target", "raid15target", "raid16target", "raid17target", "raid18target", "raid19target", "raid20target",
|
|
"raid21target", "raid22target", "raid23target", "raid24target", "raid25target", "raid26target", "raid27target", "raid28target", "raid29target", "raid30target",
|
|
"raid31target", "raid32target", "raid33target", "raid34target", "raid35target", "raid36target", "raid37target", "raid38target", "raid39target", "raid40target",
|
|
"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"
|
|
}
|
|
|
|
local bossTargetuIds = {
|
|
"boss1", "boss2", "boss3", "boss4", "boss5", "boss6", "boss7", "boss8", "boss9", "boss10", "focus", "target"
|
|
}
|
|
|
|
--Not to be confused with GetUnitIdFromCID
|
|
function DBM:GetUnitIdFromGUID(guid, scanOnlyBoss)
|
|
local unitID
|
|
--First use blizzard internal client token check, but only if it's not boss only (because blizzard checks every token imaginable, even more than fullUids does)
|
|
if UnitTokenFromGUID and not scanOnlyBoss then
|
|
unitID = UnitTokenFromGUID(guid)
|
|
end
|
|
if unitID then
|
|
return unitID
|
|
else
|
|
local usedTable = scanOnlyBoss and bossTargetuIds or fullUids
|
|
for _, unitId in ipairs(usedTable) do
|
|
local guid2 = UnitGUID(unitId)
|
|
if guid == guid2 then
|
|
return unitId
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function DBM:GetPlayerGUIDByName(name)
|
|
return raid[name] and raid[name].guid
|
|
end
|
|
|
|
function DBM:GetMyPlayerInfo()
|
|
return playerName, playerLevel, playerRealm, normalizedPlayerRealm
|
|
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 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 and serverName ~= normalizedPlayerRealm 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, higher)
|
|
local raidMember = raid[name] or raid[GetUnitName(name, true) or ""]
|
|
return raidMember and raidMember.groupId or UnitInRaid(name) or higher and 99 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)
|
|
return self:GetCIDFromGUID(UnitGUID(uId))
|
|
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) ~= "string" then return false end
|
|
local guidType = strsplit("-", guid or "")
|
|
return guidType and (guidType == "Creature" or guidType == "Vehicle" or guidType == "NPC")--To determine, add pet or not?
|
|
end
|
|
|
|
function DBM:IsCreatureGUID(guid)
|
|
local guidType = strsplit("-", guid or "")
|
|
return guidType and (guidType == "Creature" or guidType == "Vehicle")--To determine, add pet or not?
|
|
end
|
|
|
|
function DBM:GetBossUnitId(name, bossOnly)--Deprecated, only old mods use this
|
|
local returnUnitID
|
|
for i = 1, 10 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
|
|
|
|
--Not to be confused with GetUnitIdFromGUID
|
|
function DBM:GetUnitIdFromCID(creatureID, bossOnly)
|
|
local returnUnitID
|
|
for i = 1, 10 do
|
|
local unitId = "boss"..i
|
|
local bossGUID = UnitGUID(unitId)
|
|
local cid = self:GetCIDFromGUID(bossGUID)
|
|
if cid == creatureID then
|
|
returnUnitID = unitId
|
|
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(bossGUID)
|
|
if cid == creatureID 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 or challenge modes. it's always non trivial content
|
|
if C_PlayerInfo.IsPlayerInChromieTime and C_PlayerInfo.IsPlayerInChromieTime() or difficultyIndex == 24 or difficultyIndex == 33 or difficultyIndex == 8 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 instance 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
|
|
local levelDiff = isRetail and 10 or 15
|
|
if playerLevel >= (instanceDifficultyBylevel[LastInstanceMapID][1]+levelDiff) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--Ugly, Needs improvement in code style to just dump all numeric values as args
|
|
--it's not meant to just wrap C_GossipInfo.GetOptions() but to dump out the meaningful values from it
|
|
function DBM:GetGossipID(force)
|
|
if self.Options.DontAutoGossip and not force then return false end
|
|
local table = C_GossipInfo.GetOptions()
|
|
local tempTable = {}
|
|
if table then
|
|
for i = 1, #table do
|
|
if table[i].gossipOptionID then
|
|
tempTable[#tempTable+1] = table[i].gossipOptionID
|
|
elseif table[i].orderIndex then
|
|
tempTable[#tempTable+1] = table[i].orderIndex
|
|
end
|
|
end
|
|
if tempTable[1] then
|
|
return unpack(tempTable)
|
|
end
|
|
return false
|
|
end
|
|
return false
|
|
end
|
|
|
|
--Hybrid all in one object to auto check and confirm multiple gossip IDs at once
|
|
function DBM:SelectMatchingGossip(confirm, ...)
|
|
if self.Options.DontAutoGossip then return false end
|
|
local requestedIds = {...}
|
|
local table = C_GossipInfo.GetOptions()
|
|
if not table then
|
|
return false
|
|
end
|
|
for i = 1, #table do
|
|
if table[i].gossipOptionID then
|
|
local tindex = tIndexOf(requestedIds, table[i].gossipOptionID)
|
|
if tindex then
|
|
self:SelectGossip(requestedIds[tindex], confirm)
|
|
end
|
|
elseif table[i].orderIndex then
|
|
local tindex = tIndexOf(requestedIds, table[i].orderIndex)
|
|
if tindex then
|
|
self:SelectGossip(requestedIds[tindex], confirm)
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function DBM:SelectGossip(gossipOptionID, confirm)
|
|
if gossipOptionID and not self.Options.DontAutoGossip then
|
|
if gossipOptionID < 10 then--Using Index
|
|
if C_GossipInfo.SelectOptionByIndex then--10.0.7
|
|
C_GossipInfo.SelectOptionByIndex(gossipOptionID, "", confirm)
|
|
else--10.0.5
|
|
local options = C_GossipInfo.GetOptions()
|
|
if options and options[1] then
|
|
local realGossipOptionID = options[gossipOptionID] and options[gossipOptionID].gossipOptionID
|
|
if realGossipOptionID then
|
|
C_GossipInfo.SelectOption(realGossipOptionID, "", confirm)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
C_GossipInfo.SelectOption(gossipOptionID, "", confirm)
|
|
end
|
|
end
|
|
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
|
|
|
|
local soundMigrationtable = {
|
|
[8174] = 569200,--PVPFlagTaken
|
|
[15391] = 543587,--UR_Algalon_BHole01
|
|
[9278] = 552035,--HoodWolfTransformPlayer01
|
|
[6674] = 566558,--BellTollNightElf
|
|
[11742] = 566558,--BellTollNightElf
|
|
[8585] = 546633,--CThunYouWillDIe
|
|
[11965] = 551703,--Horseman_Laugh_01
|
|
[37666] = 876098,--Blizzard Raid Emote
|
|
[11466] = 552503,--BLACK_Illidan_04
|
|
[68563] = 1412178,--VO_703_Illidan_Stormrage_03
|
|
[11052] = 553050,--CAV_Kaz_Mark02
|
|
[12506] = 553193,--KILJAEDEN02
|
|
[11482] = 553566,--BLCKTMPLE_LadyMal_Aggro01
|
|
[8826] = 554236,--Loa_Naxx_Aggro02
|
|
[128466] = 554236,--Loa_Naxx_Aggro02
|
|
[49764] = 555337,--TEMPEST_Millhouse_Pyro01
|
|
[11213] = 563787,--TEMPEST_VoidRvr_Aggro01
|
|
[15757] = 564859,--UR_YoggSaron_Slay01
|
|
[25780] = 572130,--VO_BH_ALIZABAL_RESET_01
|
|
[109293] = 2016732,--VO_801_Bwonsamdi_35_M
|
|
[109295] = 2016734,--VO_801_Bwonsamdi_37_M
|
|
[109296] = 2016735,--VO_801_Bwonsamdi_38_M
|
|
[109308] = 2016747,--VO_801_Bwonsamdi_50_M
|
|
[15588] = 553345,--UR_Kologarn_Slay02
|
|
[15553] = 552023,--UR_Hodir_Slay01
|
|
[109069] = 2015891,--VO_801_Scrollsage_Nola_34_F
|
|
[15742] = 562111,--UR_Thorim_P1Wipe01
|
|
[17067] = 563333,--IC_Valithria_Berserk01
|
|
[16971] = 555967,--IC_Muradin_Saurfang02
|
|
}
|
|
|
|
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 or (currentSpecName or "") == playerClass 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("|cffff0000Option type invalid: |r"..option)
|
|
end
|
|
if (mod.DefaultOptions[option] == nil) and (type(option) == "number" or 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 using soundkit Ids not and File Data Ids
|
|
elseif option:find("SWSound") then
|
|
local checkedOption = savedOptions[id][profileNum][option]
|
|
if checkedOption and (type(checkedOption) == "number") and soundMigrationtable[checkedOption] then
|
|
savedOptions[id][profileNum][option] = soundMigrationtable[checkedOption]
|
|
self:Debug("Migrated "..option.." to file data Id")
|
|
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 CL.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
|
|
table.wipe(checkDuplicateObjects)
|
|
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() -- Classic this is "CHARACTER_POINTS_CHANGED", but we just use this function anyway
|
|
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 or (currentSpecName or "") == playerClass 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 or (currentSpecName or "") == playerClass 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 or (currentSpecName or "") == playerClass 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] = CopyTable(_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 or (currentSpecName or "") == playerClass 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 or (currentSpecName or "") == playerClass 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
|
|
local gsub = string.gsub
|
|
|
|
local function FixElv(optionName)
|
|
if DBM.Options[optionName]:lower():find("interface\\addons\\elvui\\media\\") then
|
|
DBM.Options[optionName] = gsub(DBM.Options[optionName], gsub("Interface\\AddOns\\ElvUI\\Media\\", "(%a)", function(v)
|
|
return "[" .. v:upper() .. v:lower() .. "]"
|
|
end), "Interface\\AddOns\\ElvUI\\Core\\Media\\")
|
|
end
|
|
end
|
|
|
|
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 {}
|
|
self:Enable()
|
|
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
|
|
if not self.Options.WarningFont then
|
|
self.Options.WarningFont = "standardFont"
|
|
end
|
|
if not self.Options.SpecialWarningFont then
|
|
self.Options.SpecialWarningFont = "standardFont"
|
|
end
|
|
--If users previous voice pack was not set to none, don't force change it to VEM, honor whatever it was set to before
|
|
if self.Options.ChosenVoicePack and self.Options.ChosenVoicePack ~= "None" then
|
|
self.Options.ChosenVoicePack2 = self.Options.ChosenVoicePack
|
|
self.Options.ChosenVoicePack = nil
|
|
end
|
|
|
|
for _, setting in ipairs({
|
|
-- Sounds
|
|
"RaidWarningSound", "SpecialWarningSound", "SpecialWarningSound2", "SpecialWarningSound3", "SpecialWarningSound4", "SpecialWarningSound5", "EventSoundVictory2",
|
|
"EventSoundWipe", "EventSoundEngage2", "EventSoundMusic", "EventSoundDungeonBGM", "RangeFrameSound1", "RangeFrameSound2",
|
|
-- Fonts
|
|
"InfoFrameFont", "WarningFont", "SpecialWarningFont"
|
|
}) do
|
|
-- Migrate ElvUI changes
|
|
if type(self.Options[setting]) == "string" and self.Options[setting]:lower() ~= "none" then
|
|
FixElv(setting)
|
|
end
|
|
-- Migrate soundkit to FileData ID changes
|
|
if type(self.Options[setting]) == "number" and soundMigrationtable[self.Options[setting]] then
|
|
self.Options[setting] = soundMigrationtable[self.Options[setting]]
|
|
end
|
|
end
|
|
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:PlaySoundFile(567478, 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.DontShowEventTimers 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:PlaySoundFile(567478, 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:PlaySoundFile(567478, 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
|
|
|
|
do
|
|
local function throttledTalentCheck(self)
|
|
local lastSpecID = currentSpecID
|
|
self:SetCurrentSpecInfo()
|
|
if not InCombatLockdown() then
|
|
--Refresh entire spec table if not in combat
|
|
DBMExtraGlobal:rebuildSpecTable()
|
|
end
|
|
if currentSpecID ~= lastSpecID then--Don't fire specchanged unless spec actually has changed.
|
|
self:SpecChanged()
|
|
if isRetail and IsInGroup() then
|
|
self:RoleCheck(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
--Retail API doesn't need throttle
|
|
function DBM:PLAYER_SPECIALIZATION_CHANGED()
|
|
self:Unschedule(throttledTalentCheck)
|
|
throttledTalentCheck(self)
|
|
end
|
|
--Throttle checks on talent point updates so that if multiple CHARACTER_POINTS_CHANGED fire in succession
|
|
--It doesnt spam DBMs code and cause performance lag
|
|
function DBM:CHARACTER_POINTS_CHANGED() -- Classic/BCC support
|
|
self:Unschedule(throttledTalentCheck)
|
|
self:Schedule(2, throttledTalentCheck, self)
|
|
end
|
|
--Throttle this api too.
|
|
DBM.PLAYER_TALENT_UPDATE = DBM.CHARACTER_POINTS_CHANGED -- Wrath support
|
|
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.DontShowEventTimers then
|
|
queuedBattlefield[i] = select(2, GetBattlefieldStatus(i))
|
|
local expiration = GetBattlefieldPortExpiration(queueID)
|
|
local timerIcon = (isRetail and GetPlayerFactionGroup("player") or UnitFactionGroup("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:PlaySoundFile(567478, 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")
|
|
tremove(queuedBattlefield, i)
|
|
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, nil, nil, "SCENARIO_COMPLETED")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------
|
|
-- Load Boss Mods on Demand --
|
|
--------------------------------
|
|
do
|
|
local pvpShown = 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 shadowlandsZones = {[2296]=true,[2450]=true,[2481]=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,[1170]=true,[1504]=true,[1505]=true,[1552]=true,[1681]=true,[1672]=true,[1803]=true,[1825]=true,[1911]=true,[2106]=true,[2107]=true,[2118]=true,[2167]=true,[2177]=true,[2197]=true,[2245]=true,[2373]=true,[2509]=true,[2511]=true,[2547]=true,[2563]=true}
|
|
--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 roll over)
|
|
function DBM:CheckAvailableMods()
|
|
if _G["BigWigs"] 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
|
|
if isRetail then
|
|
if not self:IsTrivial() then
|
|
if instanceDifficultyBylevel[LastInstanceMapID] and instanceDifficultyBylevel[LastInstanceMapID][2] == 2 and not GetAddOnInfo("DBM-Party-Dragonflight") then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM Dungeon mods"))
|
|
elseif (classicZones[LastInstanceMapID] or bcZones[LastInstanceMapID]) and not GetAddOnInfo("DBM-Raids-BC") then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM BC/Vanilla mods"))
|
|
elseif wrathZones[LastInstanceMapID] and not GetAddOnInfo("DBM-Raids-WoTLK") then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM Wrath of the Lich King mods"))
|
|
elseif cataZones[LastInstanceMapID] and not GetAddOnInfo("DBM-Raids-Cata") then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM Cataclysm mods"))
|
|
elseif mopZones[LastInstanceMapID] and not GetAddOnInfo("DBM-Raids-MoP") then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM Mists of Pandaria mods"))
|
|
elseif wodZones[LastInstanceMapID] and not GetAddOnInfo("DBM-Raids-WoD") then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM Warlords of Draenor mods"))
|
|
elseif legionZones[LastInstanceMapID] and not GetAddOnInfo("DBM-Raids-Legion") 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"))
|
|
elseif bfaZones[LastInstanceMapID] and not GetAddOnInfo("DBM-Raids-BfA") 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"))
|
|
elseif shadowlandsZones[LastInstanceMapID] and not GetAddOnInfo("DBM-Raids-Shadowlands") then--Technically 50, but tuning and huge loss of player power, zones are even HARDER at 60
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM Shadowlands mods"))
|
|
end
|
|
elseif challengeScenarios[LastInstanceMapID] and not GetAddOnInfo("DBM-Challenges") then--No trivial check on challenge scenarios
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM-Challenges"))
|
|
end
|
|
else--Classic
|
|
if instanceDifficultyBylevel[LastInstanceMapID] and instanceDifficultyBylevel[LastInstanceMapID][2] == 2 and not GetAddOnInfo("DBM-Party-Vanilla") then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM Dungeon mods"))
|
|
end
|
|
end
|
|
if pvpZones[LastInstanceMapID] and not GetAddOnInfo("DBM-PvP") and not pvpShown then
|
|
AddMsg(self, L.MOD_AVAILABLE:format("DBM-PvP"))
|
|
pvpShown = 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:GetMusic() or DBM:GetDungeonMusic()
|
|
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)
|
|
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
|
|
DBMScheduler:UpdateZone()--Also update zone in scheduler
|
|
fireEvent("DBM_UpdateZone", mapID)
|
|
if instanceType == "none" or (C_Garrison and C_Garrison:IsOnGarrisonMap()) then
|
|
LastInstanceType = "none"
|
|
if not targetEventsRegistered then
|
|
self:RegisterShortTermEvents("UPDATE_MOUSEOVER_UNIT", "NAME_PLATE_UNIT_ADDED", "UNIT_TARGET_UNFILTERED")
|
|
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, nil, "Left zone of world boss")
|
|
end
|
|
end
|
|
end
|
|
-- LoadMod
|
|
self:LoadModsOnDemand("mapId", mapID)
|
|
self:CheckAvailableMods()
|
|
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()
|
|
if isRetail then
|
|
DBT:CancelBar(L.LFG_INVITE)--Disable bar here since LFG_PROPOSAL_SUCCEEDED seems broken right now
|
|
end
|
|
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()
|
|
difficultyIndex = 8
|
|
self:CheckAvailableMods()
|
|
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 and TimerTracker then -- Doesn't exist in classic
|
|
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
|
|
self:AddMsg(L.LOAD_MOD_DISABLED:format(v.name))
|
|
end
|
|
end
|
|
end
|
|
if isRetail then
|
|
self:ScenarioCheck()--Do not filter. Because ScenarioCheck function includes filter.
|
|
end
|
|
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 or darkmoon faire island
|
|
if mod.isWorldBoss and not IsInInstance() and not force and (not isRetail or difficultyIndex ~= 149) and LastInstanceMapID ~= 974 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 or (currentSpecName or "") == playerClass then
|
|
self:SetCurrentSpecInfo()
|
|
end
|
|
if not difficultyIndex then -- prevent error in EJ_SetDifficulty if not yet set
|
|
savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = self:GetCurrentInstanceDifficulty()
|
|
end
|
|
if isRetail then
|
|
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.
|
|
end
|
|
self:Debug("LoadAddOn should have fired for "..mod.name, 2)
|
|
local loaded, reason = LoadAddOn(mod.modId)
|
|
if not loaded then
|
|
if reason then
|
|
if reason == "DISABLED" then
|
|
self:AddMsg(L.LOAD_MOD_DISABLED:format(mod.name))
|
|
else
|
|
self:AddMsg(L.LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_"..reason or ""])))
|
|
end
|
|
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 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 checks for world bosses based on target or mouseover or nameplate.
|
|
function DBM:UPDATE_MOUSEOVER_UNIT()
|
|
loadModByUnit("mouseover")
|
|
end
|
|
|
|
function DBM:NAME_PLATE_UNIT_ADDED(uId)
|
|
loadModByUnit(uId)
|
|
end
|
|
|
|
function DBM:UNIT_TARGET_UNFILTERED(uId)
|
|
loadModByUnit(uId.."target")
|
|
end
|
|
end
|
|
|
|
-----------------------------
|
|
-- Handle Incoming Syncs --
|
|
-----------------------------
|
|
|
|
--NOTE. Don't ever try to move this out of core. My testing showed it required storing nearly every local variable in core in private, gravely poluting and inflating it beyond any kind of rational
|
|
|
|
do
|
|
local function checkForActualPull()
|
|
if (DBM.Options.RecordOnlyBosses and #inCombat == 0) or (not isRetail and difficultyIndex ~= 8) then
|
|
DBM:StopLogging()
|
|
end
|
|
end
|
|
|
|
local syncHandlers, whisperSyncHandlers, 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
|
|
-- WBA = World Buff Activation
|
|
-- RLO = Raid Leader Override
|
|
-- 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 CL.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(("|Hgarrmission:DBM: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) and not (#inCombat > 0 and mod.noMultiBoss) 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["RLO"] = function(sender, protocol, statusWhisper, guildStatus, raidIcons, chatBubbles)
|
|
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
|
|
if not protocol or protocol ~= 2 then return end--Ignore old versions
|
|
DBM:Debug("Raid leader override comm Received")
|
|
statusWhisper, guildStatus, raidIcons, chatBubbles = tonumber(statusWhisper) or 0, tonumber(guildStatus) or 0, tonumber(raidIcons) or 0, tonumber(chatBubbles) or 0
|
|
local activated = false
|
|
if statusWhisper == 1 then
|
|
activated = true
|
|
private.statusWhisperDisabled = true
|
|
end
|
|
if guildStatus == 1 then
|
|
activated = true
|
|
private.statusGuildDisabled = true
|
|
end
|
|
if raidIcons == 1 then
|
|
activated = true
|
|
private.raidIconsDisabled = true
|
|
end
|
|
if chatBubbles == 1 then
|
|
activated = true
|
|
private.chatBubblesDisabled = true
|
|
end
|
|
if activated then
|
|
AddMsg(L.OVERRIDE_ACTIVATED)
|
|
end
|
|
end
|
|
|
|
syncHandlers["IS"] = function(_, _, 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
|
|
private.canSetIcons[optionName] = true
|
|
else--Not from self, it means someone with a higher version than us probably sent it
|
|
private.canSetIcons[optionName] = false
|
|
end
|
|
local name = DBM:GetFullPlayerNameByGUID(iconSetPerson[optionName]) or CL.UNKNOWN
|
|
DBM:Debug(name.." was elected icon setter for "..optionName, 2)
|
|
end
|
|
|
|
syncHandlers["K"] = function(_, _, 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 > (isRetail and 2 or 0) then -- need at least 3 person to combat end. (for security) (only 1 on classic because classic breaks too badly otherwise)
|
|
DBM:EndCombat(mod, success == 0, nil, "ENCOUNTER_END synced")
|
|
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 = isRetail and 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 timer == 0 or DBM:AntiSpam(1, "PT"..sender) then--prevent double pull timer from BW and other mods that are sending D4 and D5 at same time
|
|
if not dummyMod then
|
|
local threshold = DBM.Options.PTCountThreshold2
|
|
threshold = floor(threshold)
|
|
dummyMod = DBM:NewMod("PullTimerCountdownDummy")
|
|
dummyMod.isDummyMod = true
|
|
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 4, threshold, nil, nil, nil, nil, nil, nil, "pull")
|
|
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 and TimerTracker 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 and TimerTracker 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.EventSoundPullTimer and DBM.Options.EventSoundPullTimer ~= "" and DBM.Options.EventSoundPullTimer ~= "None" then
|
|
DBM:PlaySoundFile(DBM.Options.EventSoundPullTimer, nil, true)
|
|
end
|
|
if DBM.Options.RecordOnlyBosses then
|
|
DBM:StartLogging(timer, checkForActualPull)--Start logging here to catch pre pots.
|
|
end
|
|
if isRetail and 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
|
|
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")
|
|
dummyMod2.isDummyMod = true
|
|
DBM:GetModLocalization("BreakTimerCountdownDummy"):SetGeneralLocalization{ name = L.MINIMAP_TOOLTIP_HEADER }
|
|
dummyMod2.text = dummyMod2:NewAnnounce("%s", 1, isRetail and "237538" or "136106")
|
|
--timer, name, icon, optionDefault, optionName, colorType, inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType
|
|
dummyMod2.timer = dummyMod2:NewTimer(20, L.TIMER_BREAK, isRetail and "237538" or "136106", nil, nil, 0, nil, nil, DBM.Options.DontPlayPTCountdown and 0 or 1, threshold, nil, nil, nil, nil, nil, nil, "break")
|
|
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
|
|
if timer == 0 or DBM:AntiSpam(1, "BT"..sender) then
|
|
breakTimerStart(DBM, timer, sender)
|
|
end
|
|
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\t%s"):format(tostring(DBM.Revision), tostring(DBM.ReleaseRevision), DBM.DisplayVersion, tostring(PForceDisable))
|
|
sendGuildSync(2, "GV", message)
|
|
return
|
|
end
|
|
if DBM.Options.FakeBWVersion and not dbmIsEnabled 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.ChosenVoicePack2
|
|
if not voiceSessionDisabled and VoicePack ~= "None" and DBM.VoiceVersions[VoicePack] then
|
|
VPVersion = "/ VP"..VoicePack..": v"..DBM.VoiceVersions[VoicePack]
|
|
end
|
|
if VPVersion then
|
|
sendSync(2, "V", ("%s\t%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), tostring(PForceDisable), VPVersion))
|
|
else
|
|
sendSync(2, "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), tostring(PForceDisable)))
|
|
end
|
|
end
|
|
|
|
local function HandleVersion(revision, version, displayVersion, forceDisable, 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)
|
|
if forceDisable > PForceDisable and not checkEntry(forceDisablePerson, sender) then
|
|
forceDisablePerson[#forceDisablePerson + 1] = sender
|
|
DBM:Debug("Newer force disable detected from "..sender.." : Rev - "..forceDisable, 3)
|
|
end
|
|
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 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 at least 3 players sent a higher forceDisable revision
|
|
if not testBuild and #forceDisablePerson == 3 then
|
|
-- Start days check
|
|
local curseDate = tostring(version)
|
|
local daysPerMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
|
|
local year, month, day = tonumber(curseDate:sub(1, 4)), tonumber(curseDate:sub(5, 6)), tonumber(curseDate:sub(7, 8))
|
|
if day + 2 > daysPerMonth[month] then
|
|
day = day + 2 - daysPerMonth[month]
|
|
month = month + 1
|
|
else
|
|
day = day + 2
|
|
end
|
|
if month > 12 then
|
|
month = 1
|
|
year = year + 1
|
|
end
|
|
local currentDateTable = date("*t")
|
|
if currentDateTable.year < year or currentDateTable.month < month or currentDateTable.day < day then
|
|
return
|
|
end
|
|
-- End days check
|
|
updateNotificationDisplayed = 3
|
|
DBM:ForceDisableSpam()
|
|
DBM:Disable(true)
|
|
--Disallow out of date to run during beta/ptr what so ever regardless of forceDisable revision
|
|
elseif testBuild then
|
|
updateNotificationDisplayed = 3
|
|
DBM:ForceDisableSpam()
|
|
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()
|
|
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()
|
|
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, protocol, revision, version, displayVersion, locale, iconEnabled, forceDisable, VPVersion)
|
|
revision, version = tonumber(revision), tonumber(version)
|
|
if protocol >= 2 then
|
|
forceDisable = tonumber(forceDisable) or 0
|
|
else
|
|
-- Protocol 1 did not send forceDisable, VPVersion was in that position
|
|
VPVersion = forceDisable
|
|
forceDisable = 0
|
|
end
|
|
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, forceDisable, sender)
|
|
end
|
|
DBM:GROUP_ROSTER_UPDATE()
|
|
end
|
|
|
|
guildSyncHandlers["GV"] = function(sender, _, revision, version, displayVersion, forceDisable)
|
|
revision, version, forceDisable = tonumber(revision), tonumber(version), tonumber(forceDisable) or 0
|
|
if revision and version and displayVersion then
|
|
DBM:Debug("Received G version info from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision)..", Display Version "..displayVersion, 3)
|
|
HandleVersion(revision, version, displayVersion, forceDisable, 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(_, protocol, modId, difficulty, difficultyModifier, name, groupLeader)
|
|
if not DBM.Options.ShowGuildMessages or not difficulty or DBM:GetRaidRank(groupLeader or "") == 2 then return end
|
|
if not protocol or protocol ~= 4 then return end--Ignore old versions
|
|
if DBM:AntiSpam(isRetail and 10 or 20, "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 and EJ_GetEncounterInfo(modId) or DBM:GetModLocalization(modId).general.name) or name or CL.UNKNOWN
|
|
if isRetail or isWrath then
|
|
local difficultyName
|
|
if difficulty == 8 then
|
|
if difficultyModifier and difficultyModifier ~= 0 then
|
|
difficultyName = PLAYER_DIFFICULTY6.."+ ("..difficultyModifier..")"
|
|
else
|
|
difficultyName = PLAYER_DIFFICULTY6.."+"
|
|
end
|
|
elseif difficulty == 3 or difficulty == 175 then
|
|
difficultyName = RAID_DIFFICULTY1
|
|
elseif difficulty == 4 or difficulty == 176 then
|
|
difficultyName = RAID_DIFFICULTY2
|
|
elseif difficulty == 5 or difficulty == 193 then
|
|
difficultyName = RAID_DIFFICULTY3
|
|
elseif difficulty == 6 or difficulty == 194 then
|
|
difficultyName = RAID_DIFFICULTY4
|
|
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, groupLeader))-- "%s has been engaged by %s's guild group"
|
|
else--Vanilla and TBC single format raids
|
|
DBM:AddMsg(L.GUILD_COMBAT_STARTED:format(bossName, groupLeader))
|
|
end
|
|
end
|
|
end
|
|
|
|
guildSyncHandlers["GCE"] = function(_, protocol, modId, wipe, time, difficulty, difficultyModifier, name, groupLeader, wipeHP)
|
|
if not DBM.Options.ShowGuildMessages or not difficulty or DBM:GetRaidRank(groupLeader or "") == 2 then return end
|
|
if not protocol or protocol ~= 8 then return end--Ignore old versions
|
|
if DBM:AntiSpam(isRetail and 10 or 20, "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 and EJ_GetEncounterInfo(modId) or DBM:GetModLocalization(modId).general.name) or name or CL.UNKNOWN
|
|
if isRetail or isWrath then
|
|
local difficultyName
|
|
if difficulty == 8 then
|
|
if difficultyModifier and difficultyModifier ~= 0 then
|
|
difficultyName = PLAYER_DIFFICULTY6.."+ ("..difficultyModifier..")"
|
|
else
|
|
difficultyName = PLAYER_DIFFICULTY6.."+"
|
|
end
|
|
elseif difficulty == 3 or difficulty == 175 then
|
|
difficultyName = RAID_DIFFICULTY1
|
|
elseif difficulty == 4 or difficulty == 176 then
|
|
difficultyName = RAID_DIFFICULTY2
|
|
elseif difficulty == 5 or difficulty == 193 then
|
|
difficultyName = RAID_DIFFICULTY3
|
|
elseif difficulty == 6 or difficulty == 194 then
|
|
difficultyName = RAID_DIFFICULTY4
|
|
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(groupLeader or CL.UNKNOWN, difficultyName.." - "..bossName, wipeHP, time))--"%s's Guild group has wiped on %s (%s) after %s.
|
|
else
|
|
DBM:AddMsg(L.GUILD_BOSS_DOWN:format(difficultyName.." - "..bossName, groupLeader or CL.UNKNOWN, time))--"%s has been defeated by %s's guild group after %s!"
|
|
end
|
|
else--Vanilla and TBC single format raids
|
|
if wipe == "1" then
|
|
DBM:AddMsg(L.GUILD_COMBAT_ENDED_AT:format(groupLeader or CL.UNKNOWN, bossName, wipeHP, time))
|
|
else
|
|
DBM:AddMsg(L.GUILD_BOSS_DOWN:format(bossName, groupLeader or CL.UNKNOWN, time))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
guildSyncHandlers["WBE"] = function(sender, protocol, modId, realm, health, name)
|
|
if not protocol or protocol ~= 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 or realm == normalizedPlayerRealm) 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 and EJ_GetEncounterInfo(modId) or DBM:GetModLocalization(modId).general.name) or name or CL.UNKNOWN
|
|
DBM:AddMsg(L.WORLDBOSS_ENGAGED:format(bossName, floor(health), sender))
|
|
end
|
|
end
|
|
|
|
guildSyncHandlers["WBD"] = function(sender, protocol, modId, realm, name)
|
|
if not protocol or protocol ~= 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 or realm == normalizedPlayerRealm) 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 and EJ_GetEncounterInfo(modId) or DBM:GetModLocalization(modId).general.name) or name or CL.UNKNOWN
|
|
DBM:AddMsg(L.WORLDBOSS_DEFEATED:format(bossName, sender))
|
|
end
|
|
end
|
|
|
|
guildSyncHandlers["WBA"] = function(sender, protocol, bossName, faction, spellId, time) -- Classic only
|
|
DBM:Debug("WBA sync recieved")
|
|
if not protocol or protocol ~= 4 or isRetail then return end--Ignore old versions
|
|
if lastBossEngage[bossName..faction] and (GetTime() - lastBossEngage[bossName..faction] < 30) then return end--We recently got a sync about this buff on this realm, so do nothing.
|
|
lastBossEngage[bossName..faction] = GetTime()
|
|
if DBM.Options.WorldBuffAlert and #inCombat == 0 then
|
|
DBM:Debug("WBA sync processing")
|
|
local factionText = faction == "Alliance" and FACTION_ALLIANCE or faction == "Horde" and FACTION_HORDE or CL.BOTH
|
|
local buffName, _, buffIcon = DBM:GetSpellInfo(tonumber(spellId) or 0)
|
|
DBM:AddMsg(L.WORLDBUFF_STARTED:format(buffName or CL.UNKNOWN, factionText, sender))
|
|
DBM:PlaySoundFile(DBM.Options.RaidWarningSound, true)
|
|
time = tonumber(time)
|
|
if time then
|
|
DBT:CreateBar(time, buffName or CL.UNKNOWN, buffIcon or 136106)
|
|
end
|
|
end
|
|
end
|
|
|
|
whisperSyncHandlers["WBE"] = function(sender, protocol, modId, realm, health, name)
|
|
if not protocol or protocol ~= 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 or realm == normalizedPlayerRealm) and DBM.Options.WorldBossAlert and (isRetail and not IsEncounterInProgress() or #inCombat == 0) then
|
|
local gameAccountInfo = C_BattleNet.GetGameAccountInfoByID(sender)
|
|
local toonName = gameAccountInfo and gameAccountInfo.characterName or CL.UNKNOWN
|
|
modId = tonumber(modId)--If it fails to convert into number, this makes it nil
|
|
local bossName = modId and (EJ_GetEncounterInfo and EJ_GetEncounterInfo(modId) or DBM:GetModLocalization(modId).general.name) or name or CL.UNKNOWN
|
|
DBM:AddMsg(L.WORLDBOSS_ENGAGED:format(bossName, floor(health), toonName))
|
|
end
|
|
end
|
|
|
|
whisperSyncHandlers["WBD"] = function(sender, protocol, modId, realm, name)
|
|
if not protocol or protocol ~= 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 or realm == normalizedPlayerRealm) and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
|
|
local gameAccountInfo = C_BattleNet.GetGameAccountInfoByID(sender)
|
|
local toonName = gameAccountInfo and gameAccountInfo.characterName or CL.UNKNOWN
|
|
modId = tonumber(modId)--If it fails to convert into number, this makes it nil
|
|
local bossName = modId and (EJ_GetEncounterInfo and EJ_GetEncounterInfo(modId) or DBM:GetModLocalization(modId).general.name) or name or CL.UNKNOWN
|
|
DBM:AddMsg(L.WORLDBOSS_DEFEATED:format(bossName, toonName))
|
|
end
|
|
end
|
|
|
|
whisperSyncHandlers["WBA"] = function(sender, protocol, bossName, faction, spellId, time) -- Classic only
|
|
DBM:Debug("WBA sync recieved")
|
|
if not protocol or protocol ~= 4 or isRetail then return end--Ignore old versions
|
|
if lastBossEngage[bossName..faction] and (GetTime() - lastBossEngage[bossName..faction] < 30) then return end--We recently got a sync about this buff on this realm, so do nothing.
|
|
lastBossEngage[bossName..faction] = GetTime()
|
|
if DBM.Options.WorldBuffAlert and #inCombat == 0 then
|
|
DBM:Debug("WBA sync processing")
|
|
local factionText = faction == "Alliance" and FACTION_ALLIANCE or faction == "Horde" and FACTION_HORDE or L.BOTH
|
|
local buffName, _, buffIcon = DBM:GetSpellInfo(tonumber(spellId) or 0)
|
|
DBM:AddMsg(L.WORLDBUFF_STARTED:format(buffName or CL.UNKNOWN, factionText, sender))
|
|
DBM:PlaySoundFile(DBM.Options.RaidWarningSound, true)
|
|
time = tonumber(time)
|
|
if time then
|
|
DBT:CreateBar(time, buffName or CL.UNKNOWN, buffIcon or 136106)
|
|
end
|
|
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
|
|
|
|
--Function to correct a blizzard bug where off realm players have realm name stripped
|
|
--Had to be custom function due to bugs with two players with same name on different realms
|
|
--local function VerifyRaidName(apiName, SyncedName)
|
|
-- local _, serverName = string.split("-", SyncedName)
|
|
-- if serverName and serverName ~= playerRealm and serverName ~= normalizedPlayerRealm then
|
|
-- return SyncedName--Use synced name with realm added back on
|
|
-- else
|
|
-- return apiName--Use api name without realm
|
|
-- end
|
|
--end
|
|
|
|
handleSync = function(channel, sender, _, protocol, prefix, ...)--dbmSender unused for now
|
|
protocol = tonumber(protocol)
|
|
if not protocol then
|
|
return
|
|
end
|
|
if protocol < DBMSyncProtocol then
|
|
return
|
|
end
|
|
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-- Instance, Raid, Party
|
|
handler = syncHandlers[prefix]
|
|
end
|
|
if handler then
|
|
--if dbmSender then
|
|
-- --Strip spaces from realm name, since this is what Unit Tokens expect
|
|
-- --(newer versions of DBM do this on send, but we double check for older versions)
|
|
-- dbmSender = dbmSender:gsub("[%s-]+", "")--Needs to be fixed, if this is ever uncommented as right now it'd strip realm
|
|
-- sender = VerifyRaidName(sender, dbmSender)
|
|
--end
|
|
return handler(sender, protocol, ...)
|
|
end
|
|
end
|
|
|
|
local function GetCorrectSender(senderOne, senderTwo)
|
|
local correctSender = senderOne
|
|
if senderOne:find("-") then--first sender arg has realm name
|
|
correctSender = Ambiguate(senderOne, "none")
|
|
elseif senderTwo and senderTwo:find("-") then--Second sender arg has realm name
|
|
correctSender = Ambiguate(senderTwo, "none")
|
|
end
|
|
return correctSender
|
|
end
|
|
|
|
function DBM:CHAT_MSG_ADDON(prefix, msg, channel, senderOne, senderTwo)
|
|
if prefix == DBMPrefix and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT" or channel == "WHISPER" or channel == "GUILD") then
|
|
local correctSender = GetCorrectSender(senderOne, senderTwo)
|
|
if channel == "WHISPER" then
|
|
handleSync(channel, correctSender, nil, strsplit("\t", msg))
|
|
else
|
|
handleSync(channel, correctSender, strsplit("\t", msg))
|
|
end
|
|
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
|
|
local correctSender = GetCorrectSender(senderOne, senderTwo)
|
|
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
|
|
handleSync(channel, correctSender, nil, DBMSyncProtocol, "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 = #inCombat, 1, -1 do
|
|
local mod = inCombat[i]
|
|
if mod and mod.OnBWSync then
|
|
mod:OnBWSync(bwMsg, extra, correctSender)
|
|
end
|
|
end
|
|
for i = 1, #oocBWComms do
|
|
local mod = oocBWComms[i]
|
|
if mod and mod.OnBWSync then
|
|
mod:OnBWSync(bwMsg, extra, correctSender)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif prefix == "Transcriptor" and msg then
|
|
local correctSender = GetCorrectSender(senderOne, senderTwo)
|
|
for i = #inCombat, 1, -1 do
|
|
local mod = inCombat[i]
|
|
if mod and mod.OnTranscriptorSync then
|
|
mod:OnTranscriptorSync(msg, correctSender)
|
|
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 CL.UNKNOWN
|
|
local spellName = string.match(msg, "h%[(.-)%]|h") or CL.UNKNOWN
|
|
local message = "RAID_BOSS_WHISPER on "..correctSender.." 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, _, sender)
|
|
if prefix == DBMPrefix and msg then
|
|
handleSync("BN_WHISPER", sender, nil, strsplit("\t", msg))
|
|
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
|
|
targetList[DBM:GetCIDFromGUID(guid)] = 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)
|
|
if not isRetail then
|
|
DBM:Schedule(1.25, scanForCombat, combatInfo.mod, mob, 1.25)
|
|
end
|
|
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 and not (#inCombat > 0 and v.noMultiBoss) 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.AFKHealthWarning2 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.ChosenVoicePack2
|
|
local path = 546633--"Sound\\Creature\\CThun\\CThunYouWillDIe.ogg"
|
|
if not voiceSessionDisabled and voice ~= "None" then
|
|
path = "Interface\\AddOns\\DBM-VP"..voice.."\\checkhp.ogg"
|
|
end
|
|
self:PlaySoundFile(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 because it always should be function
|
|
delayedFunction()
|
|
delayedFunction = nil
|
|
end
|
|
if watchFrameRestore then
|
|
if isRetail then
|
|
ObjectiveTracker_Expand()
|
|
elseif isWrath then
|
|
WatchFrame:Show()
|
|
else -- Classic Era / BCC
|
|
QuestWatchFrame:Show()
|
|
end
|
|
watchFrameRestore = false
|
|
end
|
|
local QuestieLoader = _G["QuestieLoader"]
|
|
if QuestieLoader then
|
|
local QuestieTracker = _G["QuestieTracker"] or QuestieLoader:ImportModule("QuestieTracker")--Might be a global in some versions, but not a global in others
|
|
if QuestieTracker and questieWatchRestore and QuestieTracker.Enable then
|
|
QuestieTracker:Enable()
|
|
questieWatchRestore = false
|
|
end
|
|
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 and not (#inCombat > 0 and v.noMultiBoss) 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:ENCOUNTER_START(encounterID, name, difficulty, size)
|
|
self:Debug("ENCOUNTER_START event fired: "..encounterID.." "..name.." "..difficulty.." "..size)
|
|
if dbmIsEnabled then
|
|
self:CheckAvailableMods()
|
|
if combatInfo[LastInstanceMapID] then
|
|
for _, v in ipairs(combatInfo[LastInstanceMapID]) do
|
|
if not v.noESDetection and not (#inCombat > 0 and v.noMultiBoss) 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)
|
|
self:CheckAvailableMods()
|
|
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.DontShowEventTimers 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), isRetail and 237538 or 136106)--Interface\\Icons\\Spell_Holy_BorrowedTime, Spell_nature_timestop
|
|
fireEvent("DBM_TimerStart", "DBMRespawnTimer", L.TIMER_RESPAWN:format(name), v.respawnTime, isRetail and "237538" or "136106", "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, nil, "ENCOUNTER_END")
|
|
sendSync(DBMSyncProtocol, "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, nil, "ENCOUNTER_END")
|
|
sendSync(DBMSyncProtocol, "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, nil, nil, "BOSS_KILL")
|
|
sendSync(DBMSyncProtocol, "EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
|
|
return
|
|
end
|
|
end
|
|
elseif encounterID == v.combatInfo.eId then
|
|
self:EndCombat(v, nil, nil, "BOSS_KILL")
|
|
sendSync(DBMSyncProtocol, "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) and not (#inCombat > 0 and v.noMultiBoss) then
|
|
self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
|
|
elseif v.type == "combat_" .. type .. "find" and tContains(v.msgs, msg) or v.type == "combat_" .. type and checkEntry(v.msgs, msg) and not (#inCombat > 0 and v.noMultiBoss) 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:PlaySoundFile(567478, 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, nil, nil, "onMonsterMessage")
|
|
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
|
|
if not isRetail and not IsInInstance() then
|
|
if msg:find(L.WORLD_BUFFS.hordeOny) then
|
|
SendWorldSync(self, 4, "WBA", "Onyxia\tHorde\t22888\t15\t4")
|
|
DBM:Debug("L.WORLD_BUFFS.hordeOny detected")
|
|
elseif msg:find(L.WORLD_BUFFS.allianceOny) then
|
|
SendWorldSync(self, 4, "WBA", "Onyxia\tAlliance\t22888\t15\t4")
|
|
DBM:Debug("L.WORLD_BUFFS.allianceOny detected")
|
|
elseif msg:find(L.WORLD_BUFFS.hordeNef) then
|
|
SendWorldSync(self, 4, "WBA", "Nefarian\tHorde\t22888\t16\t4")
|
|
DBM:Debug("L.WORLD_BUFFS.hordeNef detected")
|
|
elseif msg:find(L.WORLD_BUFFS.allianceNef) then
|
|
SendWorldSync(self, 4, "WBA", "Nefarian\tAlliance\t22888\t16\t4")
|
|
DBM:Debug("L.WORLD_BUFFS.allianceNef detected")
|
|
elseif msg:find(L.WORLD_BUFFS.rendHead) then
|
|
SendWorldSync(self, 4, "WBA", "rendBlackhand\tHorde\t16609\t7\t4")
|
|
DBM:Debug("L.WORLD_BUFFS.rendHead detected")
|
|
elseif msg:find(L.WORLD_BUFFS.zgHeartYojamba) then
|
|
-- zg buff transcripts https://gist.github.com/venuatu/18174f0e98759f83b9834574371b8d20
|
|
-- 28.58, 28.67, 27.77, 29.39, 28.67, 29.03, 28.12, 28.19, 29.61
|
|
SendWorldSync(self, 4, "WBA", "Zandalar\tBoth\t24425\t28\t4")
|
|
DBM:Debug("L.WORLD_BUFFS.zgHeartYojamba detected")
|
|
elseif msg:find(L.WORLD_BUFFS.zgHeartBooty) then
|
|
-- 48.7, 49.76, 50.64, 49.42, 49.8, 50.67, 50.94, 51.06
|
|
SendWorldSync(self, 4, "WBA", "Zandalar\tBoth\t24425\t49\t4")
|
|
DBM:Debug("L.WORLD_BUFFS.zgHeartBooty detected")
|
|
end
|
|
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 CL.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 msg and msg ~= "" and 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:GOSSIP_SHOW()
|
|
if not IsInInstance() then return end--Don't really care about it if not in a dungeon or raid
|
|
local cid = self:GetUnitCreatureId("npc") or 0
|
|
local gossipOptionID = self:GetGossipID(true)
|
|
if gossipOptionID then--At least one must return for debug
|
|
self:Debug("GOSSIP_SHOW triggered with a gossip ID(s) of "..strjoin(", ", self:GetGossipID(true)).." on creatureID "..cid)
|
|
end
|
|
end
|
|
|
|
function DBM:CHAT_MSG_MONSTER_SAY(msg)
|
|
if not isRetail and not IsInInstance() then
|
|
if msg:find(L.WORLD_BUFFS.zgHeart) then
|
|
-- 51.01 51.82 51.85 51.53
|
|
SendWorldSync(self, 4, "WBA", "Zandalar\tBoth\t24425\t51\t4")
|
|
end
|
|
end
|
|
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, 10 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 (isRetail and 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, 10 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 mod = inCombat[i]
|
|
if not mod.noStatistics then
|
|
self:Debug("You wiped. Reason : " .. (wipe == 1 and "No combat unit found in your party." or "No boss found : "..(wipe or "nil")))
|
|
end
|
|
self:EndCombat(mod, true, nil, "checkWipe")
|
|
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 dbmIsEnabled then return end
|
|
if not private.statusGuildDisabled and updateNotificationDisplayed == 0 then
|
|
if thisTime then--Wipe event
|
|
if wipeHP then
|
|
sendGuildSync(8, "GCE", modId.."\t1\t"..thisTime.."\t"..difficultyIndex.."\t"..difficultyModifier.."\t"..name.."\t"..lastGroupLeader.."\t"..wipeHP)
|
|
else
|
|
sendGuildSync(8, "GCE", modId.."\t0\t"..thisTime.."\t"..difficultyIndex.."\t"..difficultyModifier.."\t"..name.."\t"..lastGroupLeader)
|
|
end
|
|
else
|
|
sendGuildSync(4, "GCB", modId.."\t"..difficultyIndex.."\t"..difficultyModifier.."\t"..name.."\t"..lastGroupLeader)
|
|
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
|
|
--Shadowlands
|
|
["couragescenario"] = "normal",--Map PoA scenaris to different stats for each difficulty
|
|
["loyaltyscenario"] = "heroic",
|
|
["wisdomscenario"] = "mythic",
|
|
["humilityscenario"] = "challenge",
|
|
--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 isRetail then
|
|
if mod.addon.type == "SCENARIO" and C_Scenario.IsInScenario() and not mod.soloChallenge then
|
|
mod.inScenario = true
|
|
end
|
|
end
|
|
mod.engagedDiff = savedDifficulty
|
|
mod.engagedDiffText = difficultyText
|
|
mod.engagedDiffIndex = difficultyIndex
|
|
mod.inCombat = true
|
|
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)
|
|
end
|
|
local trackedAchievements
|
|
if isClassic or isBCC then
|
|
trackedAchievements = false
|
|
elseif isWrath then
|
|
trackedAchievements = (GetNumTrackedAchievements() > 0)
|
|
else
|
|
trackedAchievements = (C_ContentTracking and C_ContentTracking.GetTrackedIDs(2)[1])
|
|
end
|
|
if self.Options.HideObjectivesFrame and mod.addon.type ~= "SCENARIO" and not trackedAchievements and difficultyIndex ~= 8 and not InCombatLockdown() then
|
|
if isRetail then--Do nothing due to taint and breaking
|
|
--if ObjectiveTrackerFrame:IsVisible() then
|
|
-- ObjectiveTracker_Collapse()
|
|
-- watchFrameRestore = true
|
|
--end
|
|
else
|
|
if isWrath then
|
|
if WatchFrame:IsVisible() then
|
|
WatchFrame:Hide()
|
|
watchFrameRestore = true
|
|
end
|
|
elseif QuestWatchFrame:IsVisible() then -- Classic Era / BCC
|
|
QuestWatchFrame:Hide()
|
|
watchFrameRestore = true
|
|
end
|
|
local QuestieLoader = _G["QuestieLoader"]
|
|
if QuestieLoader then
|
|
local QuestieTracker = _G["QuestieTracker"] or QuestieLoader:ImportModule("QuestieTracker")--Might be a global in some versions, but not a global in others
|
|
local Questie = _G["Questie"] or QuestieLoader:ImportModule("Questie")
|
|
if QuestieTracker and Questie and Questie.db.global.trackerEnabled and QuestieTracker.Disable then
|
|
--Will only hide questie tracker if it's not already hidden.
|
|
QuestieTracker:Disable()
|
|
questieWatchRestore = true
|
|
end
|
|
end
|
|
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, isRetail and "237538" or "136106", nil, false)
|
|
speedTimer:Start()
|
|
end
|
|
end
|
|
--update boss left
|
|
if mod.numBoss then
|
|
mod.vb.bossLeft = mod.numBoss
|
|
end
|
|
--Update Elected Icon Setter
|
|
self:ElectIconSetter(mod)
|
|
--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 and not mod.soloChallenge then
|
|
sendSync(DBMSyncProtocol, "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
|
|
--Global disables require normal, heroic, mythic raid on retail, or 10 man normal, 25 man normal, 40 man normal, 10 man heroic, or 25 man heroic on classic
|
|
if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 or difficultyIndex == 175 or difficultyIndex == 176 or difficultyIndex == 186 or difficultyIndex == 193 or difficultyIndex == 194 then
|
|
local statusWhisper, guildStatus, raidIcons, chatBubbles = self.Options.DisableStatusWhisper and 1 or 0, self.Options.DisableGuildStatus and 1 or 0, self.Options.DisableRaidIcons and 1 or 0, self.Options.DisableChatBubbles and 1 or 0
|
|
if statusWhisper ~= 0 or guildStatus ~= 0 or raidIcons ~= 0 or chatBubbles ~= 0 then
|
|
sendSync(2, "RLO", statusWhisper.."\t"..guildStatus.."\t"..raidIcons.."\t"..chatBubbles)
|
|
end
|
|
end
|
|
end
|
|
if self.Options.oRA3AnnounceConsumables and _G["oRA3Frame"] then
|
|
local oRA3 = LibStub and 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))
|
|
local check = not private.statusGuildDisabled and (isRetail and ((difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty()) or difficultyIndex ~= 1 and DBM:GetNumGuildPlayersInZone() >= 10)
|
|
if check and not self.Options.DisableGuildStatus then--Only send relevant content, not guild beating down lich king or LFR.
|
|
self:Unschedule(delayedGCSync, modId)
|
|
self:Schedule(isRetail and 1.5 or 3, 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 and TimerTracker 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 mod.WBEsync then
|
|
if lastBossEngage[modId..normalizedPlayerRealm] and (GetTime() - lastBossEngage[modId..normalizedPlayerRealm] < 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..normalizedPlayerRealm] = GetTime()--Update last engage time, that way we ignore our own sync
|
|
SendWorldSync(self, 8, "WBE", modId.."\t"..normalizedPlayerRealm.."\t"..startHp.."\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) and not (#inCombat > 0 and v.noMultiBoss) 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.AFKHealthWarning2 and UnitIsUnit(uId, "player") and (health < (isHardcoreServer and 95 or 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.
|
|
local voice = DBM.Options.ChosenVoicePack2
|
|
local path = 546633--"Sound\\Creature\\CThun\\CThunYouWillDIe.ogg"
|
|
if not voiceSessionDisabled and voice ~= "None" then
|
|
path = "Interface\\AddOns\\DBM-VP"..voice.."\\checkhp.ogg"
|
|
end
|
|
self:PlaySoundFile(path)
|
|
self:AddMsg(L.AFK_WARNING:format(health))
|
|
end
|
|
end
|
|
end
|
|
DBM.UNIT_HEALTH_FREQUENT = DBM.UNIT_HEALTH
|
|
|
|
function DBM:EndCombat(mod, wipe, srmIncluded, event)
|
|
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 mod.paSounds then
|
|
mod:DisablePrivateAuraSounds()
|
|
end
|
|
if event then
|
|
self:Debug("EndCombat called by : "..event..". LastInstanceMapID is "..LastInstanceMapID)
|
|
end
|
|
if private.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
|
|
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
|
|
--Fix stupid classic behavior where wipes only happen after release which causes all the instance difficulty info to be wrong
|
|
--This uses stored values from engage first, and only current values as fallback
|
|
local usedDifficulty = mod.engagedDiff or savedDifficulty
|
|
local usedDifficultyText = mod.engagedDiffText or difficultyText
|
|
local usedDifficultyIndex = mod.engagedDiffIndex or difficultyIndex
|
|
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 CL.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[usedDifficulty].."Pulls"]
|
|
local totalKills = mod.stats[statVarTable[usedDifficulty].."Kills"]
|
|
if thisTime < 30 then -- Normally, one attempt will last at least 30 sec.
|
|
totalPulls = totalPulls - 1
|
|
mod.stats[statVarTable[usedDifficulty].."Pulls"] = totalPulls
|
|
if self.Options.ShowDefeatMessage then
|
|
if scenario then
|
|
self:AddMsg(L.SCENARIO_ENDED_AT:format(usedDifficultyText..name, strFromTime(thisTime)))
|
|
else
|
|
self:AddMsg(L.COMBAT_ENDED_AT:format(usedDifficultyText..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(usedDifficultyText..name, strFromTime(thisTime), totalPulls - totalKills))
|
|
else
|
|
self:AddMsg(L.COMBAT_ENDED_AT_LONG:format(usedDifficultyText..name, wipeHP, strFromTime(thisTime), totalPulls - totalKills))
|
|
local check = isRetail and
|
|
((usedDifficultyIndex == 8 or usedDifficultyIndex == 14 or usedDifficultyIndex == 15 or usedDifficultyIndex == 16) and InGuildParty()) or
|
|
usedDifficultyIndex ~= 1 and DBM:GetNumGuildPlayersInZone() >= 10 -- Classic
|
|
if check and not self.Options.DisableGuildStatus then
|
|
self:Unschedule(delayedGCSync, modId)
|
|
self:Schedule(isRetail and 1.5 or 3, delayedGCSync, modId, usedDifficultyIndex, 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
|
|
local defeatSounds = DBM:GetDefeatSounds()
|
|
if #defeatSounds >= 3 then
|
|
self:PlaySoundFile(defeatSounds[fastrandom(3, #defeatSounds)].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, usedDifficultyText..(name or ""), totalPulls - totalKills)
|
|
else
|
|
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_WIPE_STATS_AT:format(playerName, usedDifficultyText..(name or ""), wipeHP, totalPulls - totalKills)
|
|
end
|
|
else
|
|
if scenario then
|
|
msg = msg or chatPrefixShort..L.WHISPER_SCENARIO_END_WIPE:format(playerName, usedDifficultyText..(name or ""))
|
|
else
|
|
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_WIPE_AT:format(playerName, usedDifficultyText..(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[usedDifficulty].."LastTime"]
|
|
local bestTime = mod.stats[statVarTable[usedDifficulty].."BestTime"]
|
|
if not mod.stats[statVarTable[usedDifficulty].."Kills"] or mod.stats[statVarTable[usedDifficulty].."Kills"] < 0 then mod.stats[statVarTable[usedDifficulty].."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[usedDifficulty].."Kills"] = mod.stats[statVarTable[usedDifficulty].."Kills"] + 1
|
|
if mod.stats[statVarTable[usedDifficulty].."Kills"] > mod.stats[statVarTable[usedDifficulty].."Pulls"] then mod.stats[statVarTable[usedDifficulty].."Kills"] = mod.stats[statVarTable[usedDifficulty].."Pulls"] end
|
|
if not mod.ignoreBestkill and mod.combatInfo.pull then
|
|
mod.stats[statVarTable[usedDifficulty].."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[usedDifficulty].."BestTime"] = thisTime
|
|
else
|
|
if usedDifficultyIndex == 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[usedDifficulty].."BestTime"] = thisTime--Write this time no matter what.
|
|
else--Best rank must match current rank, so update time normally
|
|
mod.stats[statVarTable[usedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
|
|
end
|
|
else
|
|
mod.stats[statVarTable[usedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
|
|
end
|
|
end
|
|
end
|
|
local totalKills = mod.stats[statVarTable[usedDifficulty].."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(usedDifficultyText..name, CL.UNKNOWN)
|
|
else
|
|
msg = L.BOSS_DOWN:format(usedDifficultyText..name, CL.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(usedDifficultyText..name, totalKills)
|
|
else
|
|
msg = L.BOSS_DOWN_I:format(usedDifficultyText..name, totalKills)
|
|
end
|
|
elseif not lastTime then
|
|
if scenario then
|
|
msg = L.SCENARIO_COMPLETE:format(usedDifficultyText..name, thisTimeString)
|
|
else
|
|
msg = L.BOSS_DOWN:format(usedDifficultyText..name, thisTimeString)
|
|
end
|
|
elseif thisTime < (bestTime or mhuge) then
|
|
if scenario then
|
|
msg = L.SCENARIO_COMPLETE_NR:format(usedDifficultyText..name, thisTimeString, strFromTime(bestTime), totalKills)
|
|
else
|
|
msg = L.BOSS_DOWN_NR:format(usedDifficultyText..name, thisTimeString, strFromTime(bestTime), totalKills)
|
|
end
|
|
else
|
|
if scenario then
|
|
msg = L.SCENARIO_COMPLETE_L:format(usedDifficultyText..name, thisTimeString, strFromTime(lastTime), strFromTime(bestTime), totalKills)
|
|
else
|
|
msg = L.BOSS_DOWN_L:format(usedDifficultyText..name, thisTimeString, strFromTime(lastTime), strFromTime(bestTime), totalKills)
|
|
end
|
|
end
|
|
local check = not private.statusGuildDisabled and (isRetail and ((usedDifficultyIndex == 8 or usedDifficultyIndex == 14 or usedDifficultyIndex == 15 or usedDifficultyIndex == 16) and InGuildParty()) or usedDifficultyIndex ~= 1 and DBM:GetNumGuildPlayersInZone() >= 10) -- Classic
|
|
if not scenario and thisTimeString and check and not self.Options.DisableGuildStatus and updateNotificationDisplayed == 0 then
|
|
self:Unschedule(delayedGCSync, modId)
|
|
self:Schedule(isRetail and 1.5 or 3, delayedGCSync, modId, usedDifficultyIndex, difficultyModifier, name, thisTimeString)
|
|
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, usedDifficultyText..(name or ""), totalKills)
|
|
else
|
|
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_KILL_STATS:format(playerName, usedDifficultyText..(name or ""), totalKills)
|
|
end
|
|
else
|
|
if scenario then
|
|
msg = msg or chatPrefixShort..L.WHISPER_SCENARIO_END_KILL:format(playerName, usedDifficultyText..(name or ""))
|
|
else
|
|
msg = msg or chatPrefixShort..L.WHISPER_COMBAT_END_KILL:format(playerName, usedDifficultyText..(name or ""))
|
|
end
|
|
end
|
|
sendWhisper(k, msg)
|
|
end
|
|
fireEvent("DBM_Kill", mod)
|
|
if usedDifficulty == "worldboss" and mod.WBEsync then
|
|
if lastBossDefeat[modId..normalizedPlayerRealm] and (GetTime() - lastBossDefeat[modId..normalizedPlayerRealm] < 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..normalizedPlayerRealm] = GetTime()--Update last defeat time before we send it, so we don't handle our own sync
|
|
SendWorldSync(self, 8, "WBD", modId.."\t"..normalizedPlayerRealm.."\t"..name)
|
|
end
|
|
if self.Options.EventSoundVictory2 and self.Options.EventSoundVictory2 ~= "None" and self.Options.EventSoundVictory2 ~= "" then
|
|
if self.Options.EventSoundVictory2 == "Random" then
|
|
local victorySounds = DBM:GetVictorySounds()
|
|
if #victorySounds >= 3 then
|
|
self:PlaySoundFile(victorySounds[fastrandom(3, #victorySounds)].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
|
|
mod.engagedDiff = nil
|
|
mod.engagedDiffText = nil
|
|
mod.engagedDiffIndex = nil
|
|
mod.vb.stageTotality = nil
|
|
if #inCombat == 0 then--prevent error if you pulled multiple boss. (Earth, Wind and Fire)
|
|
private.statusGuildDisabled, private.statusWhisperDisabled, private.raidIconsDisabled, private.chatBubblesDisabled = false, false, false, 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 not InCombatLockdown() then
|
|
if watchFrameRestore then
|
|
if isRetail then
|
|
--ObjectiveTracker_Expand()
|
|
elseif isWrath then
|
|
WatchFrame:Show()
|
|
else -- Classic Era / BCC
|
|
QuestWatchFrame:Show()
|
|
end
|
|
watchFrameRestore = false
|
|
end
|
|
local QuestieLoader = _G["QuestieLoader"]
|
|
if QuestieLoader then
|
|
local QuestieTracker = _G["QuestieTracker"] or QuestieLoader:ImportModule("QuestieTracker")--Might be a global in some versions, but not a global in others
|
|
if QuestieTracker and questieWatchRestore and QuestieTracker.Enable then
|
|
QuestieTracker:Enable()
|
|
questieWatchRestore = false
|
|
end
|
|
end
|
|
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)
|
|
--sync table
|
|
twipe(private.canSetIcons)
|
|
twipe(iconSetRevision)
|
|
twipe(iconSetPerson)
|
|
bossuIdFound = false
|
|
eeSyncSender = {}
|
|
eeSyncReceived = 0
|
|
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)
|
|
--module cleanup
|
|
private:ClearModuleTasks()
|
|
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(DBMSyncProtocol, "K", cId)
|
|
end
|
|
v.combatInfo.killMobs[cId] = false
|
|
if v.numBoss and (v.vb.bossLeft or 0) > 0 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 and not v.multiIDSingleBoss then--More hacks. don't let combat end for mutli CID single bosses
|
|
self:EndCombat(v, nil, nil, "All Mobs Down")
|
|
end
|
|
elseif cId == v.combatInfo.mob and not v.combatInfo.killMobs and not v.combatInfo.multiMobPullDetection then
|
|
if not synced then
|
|
sendSync(DBMSyncProtocol, "K", cId)
|
|
end
|
|
self:EndCombat(v, nil, nil, "Main CID Down")
|
|
end
|
|
end
|
|
end
|
|
|
|
do
|
|
local autoLog = false
|
|
local autoTLog = false
|
|
|
|
local function isLogableContent(self, force)
|
|
--1: Check for any broad global filters like LFG/LFR filter
|
|
--2: Check for what content specifically selected for logging
|
|
--3: Boss Only filter is handled somewhere else (where StartLogging is called)
|
|
|
|
if self.Options.DoNotLogLFG and isRetail and IsPartyLFG() then
|
|
return false
|
|
end
|
|
|
|
--First checks are manual index checks versus table because even old content can be scaled up using M+ or TW scaling tech
|
|
--Current player level Mythic+
|
|
if self.Options.LogCurrentMPlus and (force or (difficultyIndex or 0) == 8) then
|
|
return true
|
|
end
|
|
--Timewalking or Chromie Time raid
|
|
if self.Options.LogTWRaids and (C_PlayerInfo.IsPlayerInChromieTime and C_PlayerInfo.IsPlayerInChromieTime() or difficultyIndex == 24 or difficultyIndex == 33) and (instanceDifficultyBylevel[LastInstanceMapID] and instanceDifficultyBylevel[LastInstanceMapID][2] == 3) then
|
|
return true
|
|
end
|
|
--Timewalking or Chromie Time Dungeon
|
|
if self.Options.LogTWDungeons and (C_PlayerInfo.IsPlayerInChromieTime and C_PlayerInfo.IsPlayerInChromieTime() or difficultyIndex == 24 or difficultyIndex == 33) and (instanceDifficultyBylevel[LastInstanceMapID] and instanceDifficultyBylevel[LastInstanceMapID][2] == 2) then
|
|
return true
|
|
end
|
|
|
|
--Now we do checks relying on pre coded trivial check table
|
|
--Current level Mythic raid
|
|
if self.Options.LogCurrentMythicRaids and instanceDifficultyBylevel[LastInstanceMapID] and (instanceDifficultyBylevel[LastInstanceMapID][1] >= playerLevel) and (instanceDifficultyBylevel[LastInstanceMapID] and instanceDifficultyBylevel[LastInstanceMapID][2] == 3) and difficultyIndex == 16 then
|
|
return true
|
|
end
|
|
--Current player level non Mythic raid
|
|
if self.Options.LogCurrentRaids and instanceDifficultyBylevel[LastInstanceMapID] and (instanceDifficultyBylevel[LastInstanceMapID][1] >= playerLevel) and (instanceDifficultyBylevel[LastInstanceMapID][2] == 3) and difficultyIndex ~= 16 then
|
|
return true
|
|
end
|
|
--Trivial raid (ie one below players level)
|
|
if self.Options.LogTrivialRaids and instanceDifficultyBylevel[LastInstanceMapID] and (instanceDifficultyBylevel[LastInstanceMapID][1] < playerLevel) and (instanceDifficultyBylevel[LastInstanceMapID][2] == 3) then
|
|
return true
|
|
end
|
|
--Current level Mythic dungeon
|
|
if self.Options.LogCurrentMythicZero and instanceDifficultyBylevel[LastInstanceMapID] and (instanceDifficultyBylevel[LastInstanceMapID][1] >= playerLevel) and (instanceDifficultyBylevel[LastInstanceMapID][2] == 2) and difficultyIndex == 23 then
|
|
return true
|
|
end
|
|
--Current level Heroic dungeon
|
|
if self.Options.LogCurrentHeroic and instanceDifficultyBylevel[LastInstanceMapID] and (instanceDifficultyBylevel[LastInstanceMapID][1] >= playerLevel) and (instanceDifficultyBylevel[LastInstanceMapID][2] == 2) and (difficultyIndex == 2 or difficultyIndex == 174) then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function DBM:StartLogging(timer, checkFunc, force)
|
|
self:Unschedule(DBM.StopLogging)
|
|
if isLogableContent(self, force) then
|
|
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
|
|
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,
|
|
["EVOKER"] = 1465,
|
|
}
|
|
|
|
function DBM:SetCurrentSpecInfo()
|
|
if isRetail then
|
|
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
|
|
else
|
|
local numTabs = GetNumTalentTabs()
|
|
local highestPointsSpent = 0
|
|
if MAX_TALENT_TABS then
|
|
for i=1, MAX_TALENT_TABS do
|
|
if ( i <= numTabs ) then
|
|
local _, _, pointsSpent = GetTalentTabInfo(i)
|
|
if pointsSpent > highestPointsSpent then
|
|
highestPointsSpent = pointsSpent
|
|
currentSpecGroup = i
|
|
currentSpecID = playerClass..tostring(i)--Associate specID with class name and tabnumber (class is used because spec name is shared in some spots like "holy")
|
|
currentSpecName = currentSpecID
|
|
end
|
|
end
|
|
end
|
|
end
|
|
--If 0 talents are spent, then just set them to first spec to prevent nil errors
|
|
--This should only happen for a level 1 player or someone who's in middle of respecing
|
|
if not currentSpecID then currentSpecID = playerClass..tostring(1) end
|
|
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 and 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 or difficulty == 173 or difficulty == 184 or difficulty == 150 then--5 man Normal Dungeon
|
|
return "normal5", difficultyName.." - ", difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 2 or difficulty == 174 then--5 man Heroic Dungeon
|
|
return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 3 or difficulty == 175 then--Legacy 10 man Normal Raid
|
|
return "normal10", difficultyName.." - ", difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 4 or difficulty == 176 then--Legacy 25 man Normal Raid
|
|
return "normal25", difficultyName.." - ", difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 5 or difficulty == 193 then--Legacy 10 man Heroic Raid
|
|
return "heroic10", difficultyName.." - ", difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 6 or difficulty == 194 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 and C_ChallengeMode.GetActiveKeystoneInfo() or 0
|
|
return "challenge5", PLAYER_DIFFICULTY6.."+ ("..keystoneLevel..") - ", difficulty, instanceGroupSize, keystoneLevel
|
|
elseif difficulty == 9 or difficulty == 186 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 or difficulty == 152 then--Normal Scenario (mostly Mists of pandaria and Visions of Nzoth scenarios)
|
|
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 or difficulty == 185 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
|
|
elseif difficulty == 168 then--Path of Ascention (Shadowlands)
|
|
return "couragescenario", difficultyName.." - ",difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 169 then--Path of Ascention (Shadowlands)
|
|
return "loyaltyscenario", difficultyName.." - ",difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 170 then--Path of Ascention (Shadowlands)
|
|
return "wisdomscenario", difficultyName.." - ",difficulty, instanceGroupSize, 0
|
|
elseif difficulty == 171 then--Path of Ascention (Shadowlands)
|
|
return "humilityscenario", difficultyName.." - ",difficulty, instanceGroupSize, 0
|
|
else--failsafe
|
|
return "normal", "", difficulty, instanceGroupSize, 0
|
|
end
|
|
end
|
|
|
|
function DBM:GetCurrentArea()
|
|
return LastInstanceMapID
|
|
end
|
|
|
|
function DBM:GetCurrentDifficulty()
|
|
return difficultyIndex
|
|
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.
|
|
--Encounter ID, so api can be used in event two or more bosses are engaged at same time, ID can be used to verify which encounter they're requesting
|
|
--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, mod.vb.stageTotality 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, mod.vb.stageTotality or 0, mod.multiEncounterPullDetection and mod.multiEncounterPullDetection[1] or mod.encounterId
|
|
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")
|
|
return not playerX or not playerY
|
|
end
|
|
|
|
do
|
|
local LSMMediaCacheBuilt, sharedMediaFileCache, validateCache = false, {}, {}
|
|
|
|
local function buildLSMFileCache()
|
|
local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
|
|
if LSM then
|
|
local hashtable = LSM: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
|
|
end
|
|
|
|
function DBM:ValidateSound(path, log, ignoreCustom)
|
|
-- Ignore built in sounds
|
|
if type(path) == "number" or string.find(path:lower(), "^sound[\\/]+") then
|
|
return true
|
|
end
|
|
-- 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
|
|
|
|
function DBM:PlaySoundFile(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--Build in media using FileDataID
|
|
self:Debug("PlaySoundFile playing with FileDataID " .. path, 3)
|
|
if soundSetting == "Dialog" then
|
|
PlaySoundFile(path, "Dialog")
|
|
elseif ignoreSFX or soundSetting == "Master" then
|
|
PlaySoundFile(path, "Master")
|
|
else
|
|
PlaySoundFile(path) -- Using SFX channel, leave forceNoDuplicates on.
|
|
end
|
|
fireEvent("DBM_PlaySound", path)
|
|
else--External media, which needs path validation to avoid lua errors
|
|
if validate and not self:ValidateSound(path, true, true) then
|
|
return
|
|
end
|
|
self:Debug("PlaySoundFile playing with file path " .. 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
|
|
end
|
|
|
|
--Future proofing EJ_GetSectionInfo compat layer to make it easier updatable.
|
|
function DBM:EJ_GetSectionInfo(sectionID)
|
|
if not isRetail then
|
|
return "EJ_GetSectionInfo not supported on Classic, please report this message and boss"
|
|
end
|
|
local info = EJ_GetSectionInfo(sectionID)
|
|
if not info then
|
|
self:Debug("|cffff0000Invalid call to EJ_GetSectionInfo for sectionID: |r"..sectionID)
|
|
return
|
|
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)
|
|
return type(temp) == "table" and temp.name or 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)
|
|
--I want this to fail, and fail loudly (ie get reported when mods are completely missing the spellId)
|
|
if not spellId or spellId == "" then
|
|
error("|cffff0000Invalid call to GetSpellInfo for spellId. spellId is missing! |r")
|
|
end
|
|
local name, rank, icon, castingTime, minRange, maxRange, returnedSpellId = GetSpellInfo(spellId)
|
|
--I want this for debug purposes to catch spellids that are removed from game/changed, but quietly to end user
|
|
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
|
|
self:Debug("|cffff0000Invalid call to GetSpellInfo for spellId: |r"..spellId)
|
|
end
|
|
end
|
|
return
|
|
end--Good request, return now
|
|
return name, rank, icon, castingTime, minRange, maxRange, returnedSpellId
|
|
end
|
|
|
|
function DBM:UnitAura(uId, spellInput, spellInput2, spellInput3, spellInput4, spellInput5)
|
|
if not uId then return end
|
|
if isRetail and type(spellInput) == "number" and not spellInput2 and UnitIsUnit(uId, "player") then--A simple single spellId check should use more efficent direct blizzard method
|
|
local spellTable = C_UnitAuras.GetPlayerAuraBySpellID(spellInput)
|
|
if not spellTable then return end
|
|
return spellTable.name, spellTable.icon, spellTable.applications, spellTable.dispelName, spellTable.duration, spellTable.expirationTime, spellTable.sourceUnit, spellTable.isStealable, spellTable.nameplateShowPersonal, spellTable.spellId, spellTable.canApplyAura, spellTable.isBossAura, spellTable.isFromPlayerOrPlayerPet, spellTable.nameplateShowAll, spellTable.timeMod, spellTable.points[1] or nil, spellTable.points[2] or nil, spellTable.points[3] or nil
|
|
else--Either a multi spell check, spell name check, or C_UnitAuras.GetPlayerAuraBySpellID is unavailable
|
|
for i = 1, 60 do
|
|
local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, 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 or spellInput5 == spellName or spellInput5 == spellId then
|
|
return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod, value1, value2, value3
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--In classic, instead of adding rank back in at beginning where it was pre 8.0, it's 15th arg return at end (value 1)
|
|
function DBM:UnitDebuff(uId, spellInput, spellInput2, spellInput3, spellInput4, spellInput5)
|
|
if not uId then return end
|
|
if isRetail and type(spellInput) == "number" and not spellInput2 and UnitIsUnit(uId, "player") then--A simple single spellId check should use more efficent direct blizzard method
|
|
local spellTable = C_UnitAuras.GetPlayerAuraBySpellID(spellInput)
|
|
if not spellTable then return end
|
|
return spellTable.name, spellTable.icon, spellTable.applications, spellTable.dispelName, spellTable.duration, spellTable.expirationTime, spellTable.sourceUnit, spellTable.isStealable, spellTable.nameplateShowPersonal, spellTable.spellId, spellTable.canApplyAura, spellTable.isBossAura, spellTable.isFromPlayerOrPlayerPet, spellTable.nameplateShowAll, spellTable.timeMod, spellTable.points[1] or nil, spellTable.points[2] or nil, spellTable.points[3] or nil
|
|
else--Either a multi spell check, spell name check, or C_UnitAuras.GetPlayerAuraBySpellID is unavailable
|
|
for i = 1, 60 do
|
|
local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, 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 or spellInput5 == spellName or spellInput5 == spellId then
|
|
return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod, value1, value2, value3
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--In classic, instead of adding rank back in at beginning where it was pre 8.0, it's 15th arg return at end (value 1)
|
|
function DBM:UnitBuff(uId, spellInput, spellInput2, spellInput3, spellInput4, spellInput5)
|
|
if not uId then return end
|
|
if isRetail and type(spellInput) == "number" and not spellInput2 and UnitIsUnit(uId, "player") then--A simple single spellId check should use more efficent direct blizzard method
|
|
local spellTable = C_UnitAuras.GetPlayerAuraBySpellID(spellInput)
|
|
if not spellTable then return end
|
|
return spellTable.name, spellTable.icon, spellTable.applications, spellTable.dispelName, spellTable.duration, spellTable.expirationTime, spellTable.sourceUnit, spellTable.isStealable, spellTable.nameplateShowPersonal, spellTable.spellId, spellTable.canApplyAura, spellTable.isBossAura, spellTable.isFromPlayerOrPlayerPet, spellTable.nameplateShowAll, spellTable.timeMod, spellTable.points[1] or nil, spellTable.points[2] or nil, spellTable.points[3] or nil
|
|
else--Either a multi spell check, spell name check, or C_UnitAuras.GetPlayerAuraBySpellID is unavailable
|
|
for i = 1, 60 do
|
|
local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, 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 or spellInput5 == spellName or spellInput5 == spellId then
|
|
return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod, value1, value2, value3
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function DBM:UNIT_DIED(args)
|
|
local GUID = args.destGUID
|
|
if self:IsCreatureGUID(GUID) then
|
|
self:OnMobKill(self:GetCIDFromGUID(GUID))
|
|
end
|
|
----GUIDIsPlayer
|
|
--no point in playing alert on death itself on hardcore. if you're dead it's over, no reason to salt the wound
|
|
if not isHardcoreServer and self.Options.AFKHealthWarning2 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()
|
|
local voice = DBM.Options.ChosenVoicePack2
|
|
local path = 546633--"Sound\\Creature\\CThun\\CThunYouWillDIe.ogg"
|
|
if not voiceSessionDisabled and voice ~= "None" then
|
|
path = "Interface\\AddOns\\DBM-VP"..voice.."\\checkhp.ogg"
|
|
end
|
|
self:PlaySoundFile(path)
|
|
self:AddMsg(L.AFK_WARNING:format(0))
|
|
end
|
|
end
|
|
DBM.UNIT_DESTROYED = DBM.UNIT_DIED
|
|
|
|
----------------------
|
|
-- Timer recovery --
|
|
----------------------
|
|
do
|
|
local requestedFrom = {}
|
|
local requestTime = 0
|
|
|
|
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
|
|
end
|
|
return (v1.bwversion or 0) > (v2.bwversion or 0)
|
|
end
|
|
|
|
function DBM:RequestTimers(requestNum)
|
|
if not dbmIsEnabled then return end
|
|
local sortMe, clientUsed = {}, {}
|
|
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 UnitIsPlayer(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(DBMPrefix, DBMSyncProtocol .. "\tRT", "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
|
|
if name == "phase" then
|
|
mod:SetStage(value)--Fire stage callback for 3rd party mods when stage is recovered
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
do
|
|
local spamProtection = {}
|
|
function DBM:SendTimers(target)
|
|
if not dbmIsEnabled then return end
|
|
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(DBMPrefix, DBMSyncProtocol .. "\tBTR3\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)
|
|
if not dbmIsEnabled then return end
|
|
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)
|
|
if not dbmIsEnabled then return end
|
|
return SendAddonMessage(DBMPrefix, (DBMSyncProtocol .. "\tCI\t%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target)
|
|
end
|
|
|
|
function DBM:SendTimerInfo(mod, target)
|
|
if not dbmIsEnabled then return end
|
|
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(DBMPrefix, (DBMSyncProtocol .. "\tTR\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)
|
|
if not dbmIsEnabled then return end
|
|
for vname, v in pairs(mod.vb) do
|
|
local v2 = tostring(v)
|
|
if v2 then
|
|
SendAddonMessage(DBMPrefix, (DBMSyncProtocol .. "\tVI\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)
|
|
-- C_TimerAfter(35, function() if self.Options.NewsMessageShown2 < 2 then self.Options.NewsMessageShown2 = 2 self:AddMsg(L.NEWS_UPDATE) end end)
|
|
end
|
|
if type(C_ChatInfo.RegisterAddonMessagePrefix) == "function" then
|
|
if not C_ChatInfo.RegisterAddonMessagePrefix(DBMPrefix) 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
|
|
if self.Options.RestoreSettingQuestTooltips then
|
|
SetCVar("showQuestTrackingTooltips", self.Options.RestoreSettingQuestTooltips)
|
|
self.Options.RestoreSettingQuestTooltips = nil
|
|
self:Debug("Restoring showQuestTrackingTooltips 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()
|
|
local currentMapId = isInInstance and select(4, UnitPosition("player")) or C_Map.GetBestMapForUnit("player") or 0
|
|
local currentMapName = C_Map.GetMapInfo(currentMapId) or CL.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 private.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(chatPrefixShort) 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:PlaySoundFile(997890)--"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 isRetail and not mod.soloChallenge and IsInScenarioGroup() then return end--status not really useful on scenario mods since there is no way to report progress as a percent. We just ignore it.
|
|
local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
|
|
local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or CL.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, chatPrefixShort..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 CL.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 isRetail and not mod.soloChallenge and IsInScenarioGroup() then
|
|
sendWhisper(sender, chatPrefixShort..L.AUTO_RESPOND_WHISPER_SCENARIO:format(playerName, difficultyText..(mod.combatInfo.name or ""), getNumAlivePlayers(), DBM:GetNumGroupMembers()))
|
|
else
|
|
sendWhisper(sender, chatPrefixShort..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.RestoreSettingQuestTooltips then
|
|
SetCVar("showQuestTrackingTooltips", self.Options.RestoreSettingQuestTooltips)
|
|
self.Options.RestoreSettingQuestTooltips = nil
|
|
self:Debug("Restoring Quest Tooltip CVAR")
|
|
end
|
|
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)
|
|
DBMScheduler:Unschedule()
|
|
dbmIsEnabled = false
|
|
private.dbmIsEnabled = false
|
|
forceDisabled = forceDisable
|
|
end
|
|
|
|
function DBM:Enable()
|
|
if not forceDisabled then
|
|
dbmIsEnabled = true
|
|
end
|
|
end
|
|
|
|
function DBM:IsEnabled()
|
|
return dbmIsEnabled
|
|
end
|
|
|
|
function DBM:ForceDisableSpam()
|
|
if testBuild then
|
|
DBM:AddMsg(L.UPDATEREMINDER_DISABLETEST)
|
|
elseif dbmToc < wowTOC then
|
|
DBM:AddMsg(L.UPDATEREMINDER_MAJORPATCH)
|
|
else
|
|
DBM:AddMsg(L.UPDATEREMINDER_DISABLE)
|
|
end
|
|
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
|
|
if not frame or not frame:IsShown() then
|
|
frame = DEFAULT_CHAT_FRAME
|
|
end
|
|
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
|
|
|
|
do
|
|
local testMod
|
|
local testWarning1, testWarning2, testWarning3
|
|
local testTimer1, testTimer2, testTimer3, testTimer4, testTimer5, testTimer6, testTimer7, testTimer8
|
|
local testSpecialWarning1, testSpecialWarning2, testSpecialWarning3
|
|
function DBM:DemoMode()
|
|
fireEvent("DBM_TestModStarted")
|
|
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, isRetail and "136194" or "136221")
|
|
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 ", isRetail and "136194" or "136221", nil, nil, 3, L.MAGIC_ICON, nil, 1, 4, nil, nil, nil, nil, nil, nil, "next")--inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType
|
|
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, nil, nil, nil, nil, nil, nil, "next")--inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType
|
|
testTimer6 = testMod:NewTimer(20, "%s ", "136116", nil, nil, 5, L.TANK_ICON, nil, 2, 4, nil, nil, nil, nil, nil, nil, "next")--inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType
|
|
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)
|
|
|
|
--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 CL.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
|
|
end
|
|
return false
|
|
end
|
|
|
|
function DBM:InCombat()
|
|
return #inCombat > 0
|
|
end
|
|
|
|
function DBM:FlashClientIcon()
|
|
if self:AntiSpam(5, "FLASH") then
|
|
FlashClientIcon()
|
|
end
|
|
end
|
|
|
|
function DBM:VibrateController()
|
|
if self:AntiSpam(2, "VIBRATE") then
|
|
if C_GamePad and C_GamePad.SetVibration then
|
|
C_GamePad.SetVibration("High", 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
do
|
|
--Search Tags: iconto, toicon, raid icon, diamond, star, triangle
|
|
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
|
|
|
|
do
|
|
local DevTools = private:GetModule("DevTools")
|
|
function DBM:Debug(...)
|
|
return DevTools:Debug(...)
|
|
end
|
|
|
|
function DBM:FindDungeonMapIDs(...)
|
|
return DevTools:FindDungeonMapIDs(...)
|
|
end
|
|
|
|
function DBM:FindInstanceIDs(...)
|
|
return DevTools:FindInstanceIDs(...)
|
|
end
|
|
|
|
function DBM:FindScenarioIDs(...)
|
|
return DevTools:FindScenarioIDs(...)
|
|
end
|
|
|
|
function DBM:FindEncounterIDs(...)
|
|
return DevTools:FindEncounterIDs(...)
|
|
end
|
|
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 and 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 and 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)
|
|
-- self:AddMsg(L.MOVIE_NOTSKIPPED)
|
|
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", "specialannounce", "timer", "sound", "yell", "nameplate", "paura", "icon", "misc"},
|
|
id = name,
|
|
announces = {},
|
|
specwarns = {},
|
|
timers = {},
|
|
vb = {},
|
|
iconRestore = {},
|
|
modId = modId,
|
|
instanceId = instanceId,
|
|
revision = 0,
|
|
SyncThreshold = 8,
|
|
localization = self:GetModLocalization(name),
|
|
groupSpells = {},
|
|
groupOptions = OrderedTable(),
|
|
},
|
|
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 obj.localization.general.name 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)
|
|
if type(func) ~= "function" then return end
|
|
DBM:Debug("Registering RegisterOnUpdateHandler")
|
|
DBMScheduler:StartScheduler()
|
|
self.elapsed = 0
|
|
self.updateInterval = interval or 0
|
|
private.updateFunctions[self] = func
|
|
end
|
|
|
|
function bossModPrototype:UnregisterOnUpdateHandler()
|
|
self.elapsed = nil
|
|
self.updateInterval = nil
|
|
twipe(private.updateFunctions)
|
|
end
|
|
|
|
function bossModPrototype:SetStage(stage)
|
|
if stage == 0 then--Increment request instead of hard value
|
|
if not self.vb.phase then return end--Person DCed mid fight and somehow managed to perfectly time running SetStage with a value of 0 before getting variable recovery
|
|
self.vb.phase = self.vb.phase + 1
|
|
elseif stage == 0.5 then--Half Increment request instead of hard value
|
|
self.vb.phase = self.vb.phase + 0.5
|
|
else
|
|
self.vb.phase = stage
|
|
end
|
|
--Separate variable to use SetStage totality for very niche weak aura practices
|
|
if not self.vb.stageTotality then
|
|
self.vb.stageTotality = 0
|
|
end
|
|
self.vb.stageTotality = self.vb.stageTotality + 1
|
|
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.multiEncounterPullDetection and self.multiEncounterPullDetection[1] or self.encounterId, self.vb.stageTotality)--Mod, modId, Stage, Encounter Id (if available), total number of times SetStage has been called since combat start
|
|
--Note, some encounters have more than one encounter Id, for these encounters, the first ID from mod is always returned regardless of actual engage ID triggered fight
|
|
DBM:Debug("DBM_SetStage: " .. self.vb.phase .. "/" .. self.vb.stageTotality)
|
|
end
|
|
end
|
|
|
|
--If args are passed, returns true or false
|
|
--If no args given, just returns current stage and stage total
|
|
--stage: stage value to checkf or true/false rules
|
|
--checkType: 0 or nil for just current stage match, 1 for less than check, 2 for greater than check, 3 not equal check
|
|
--useTotal: uses stage total instead of current
|
|
function bossModPrototype:GetStage(stage, checkType, useTotal)
|
|
local currentStage, currentTotal = self.vb.phase or 0, self.vb.stageTotality or 0
|
|
if stage then
|
|
checkType = checkType or 0--Optional pass if just an exact match check
|
|
if (checkType == 0) and (useTotal and currentTotal or currentStage) == stage then
|
|
return true
|
|
elseif (checkType == 1) and (useTotal and currentTotal or currentStage) < stage then
|
|
return true
|
|
elseif (checkType == 2) and (useTotal and currentTotal or currentStage) > stage then
|
|
return true
|
|
elseif (checkType == 3) and (useTotal and currentTotal or currentStage) ~= stage then
|
|
return true
|
|
end
|
|
return false
|
|
else
|
|
return currentStage, currentTotal--This api doesn't need encounter Id return, since this is the local mod prototype version, which means it's already linked to specific encounter
|
|
end
|
|
end
|
|
|
|
function bossModPrototype:AffixEvent(eventType, stage, timeAdjust, spellDebit)
|
|
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_AffixEvent", self, self.id, eventType, self.multiEncounterPullDetection and self.multiEncounterPullDetection[1] or self.encounterId, stage or 1, timeAdjust, spellDebit)--Mod, modId, type (0 end, 1, begin, 2, timerExtend), Encounter Id (if available), stage, amount of time to extend to, spellDebit, whether to subtrack the previous extend arg from next timer
|
|
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(-11) ~= "_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()
|
|
return diff == "lfr" or diff == "lfr25"
|
|
end
|
|
|
|
--Dungeons: normal, heroic. (Raids excluded)
|
|
function bossModPrototype:IsEasyDungeon()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "heroic5" or diff == "normal5"
|
|
end
|
|
|
|
--Dungeons: normal, heroic. Raids: LFR, normal
|
|
function bossModPrototype:IsEasy()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "normal" or diff == "lfr" or diff == "lfr25" or diff == "heroic5" or diff == "normal5"
|
|
end
|
|
|
|
--Dungeons: mythic, mythic+. Raids: heroic, mythic
|
|
function bossModPrototype:IsHard()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "mythic" or diff == "challenge5" or diff == "heroic" or diff == "humilityscenario"
|
|
end
|
|
|
|
--Pretty much ANYTHING that has a normal mode
|
|
function bossModPrototype:IsNormal()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "normal" or diff == "normal5" or diff == "normal10" or diff == "normal20" or diff == "normal25" or diff == "normal40" or diff == "normalisland" or diff == "normalwarfront"
|
|
end
|
|
|
|
do
|
|
local isSeasonal
|
|
function DBM:IsSeasonal()
|
|
--Once set to true, we stop checking api an return cache
|
|
--But if not set true we keep checking api because the api (or buff) will return false if called too early and we don't want to cache that
|
|
if not isSeasonal then
|
|
local IsClassicSeason = C_Seasons and C_Seasons.HasActiveSeason()
|
|
if IsClassicSeason then
|
|
isSeasonal = true
|
|
DBM:Debug("Setting Classic seasonal to true")
|
|
else
|
|
isSeasonal = false
|
|
DBM:Debug("Setting Classic seasonal to false")
|
|
end
|
|
end
|
|
return isSeasonal
|
|
end
|
|
bossModPrototype.IsSeasonal = DBM.IsSeasonal
|
|
end
|
|
|
|
function DBM:IsFated()
|
|
--Returns table if fated, nil otherwise
|
|
if C_ModifiedInstance and C_ModifiedInstance.GetModifiedInstanceInfoFromMapID(LastInstanceMapID) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
bossModPrototype.IsFated = DBM.IsFated
|
|
|
|
--Catch alls to basically allow encounter mods to use pre retail changes within mods
|
|
function DBM:IsClassic()
|
|
return not isRetail
|
|
end
|
|
bossModPrototype.IsClassic = DBM.IsClassic
|
|
|
|
function DBM:IsRetail()
|
|
return isRetail
|
|
end
|
|
bossModPrototype.IsRetail = DBM.IsRetail
|
|
|
|
--Pretty much ANYTHING that has a heroic mode
|
|
function bossModPrototype:IsHeroic()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "heroic" or diff == "heroic5" or diff == "heroic10" or diff == "heroic25" or diff == "heroicisland" or diff == "heroicwarfront"
|
|
end
|
|
|
|
--Pretty much ANYTHING that has mythic mode, with mythic+ included
|
|
function bossModPrototype:IsMythic()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "mythic" or diff == "challenge5" or diff == "mythicisland"
|
|
end
|
|
|
|
function bossModPrototype:IsMythicPlus()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "challenge5"
|
|
end
|
|
|
|
function bossModPrototype:IsEvent()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "event5" or diff == "event20" or diff == "event40"
|
|
end
|
|
|
|
function bossModPrototype:IsWarfront()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "normalwarfront" or diff == "heroicwarfront"
|
|
end
|
|
|
|
function bossModPrototype:IsIsland()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "normalisland" or diff == "heroicisland" or diff == "mythicisland"
|
|
end
|
|
|
|
function bossModPrototype:IsScenario()
|
|
local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
|
|
return diff == "normalscenario" or diff == "heroicscenario" or diff == "couragescenario" or diff == "loyaltyscenario" or diff == "wisdomscenario" or diff == "humilityscenario"
|
|
end
|
|
|
|
function bossModPrototype:IsValidWarning(sourceGUID, customunitID, loose, allowFriendly)
|
|
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) and (allowFriendly or not UnitIsFriend("player", customunitID)) then return true end
|
|
else
|
|
local unitId = DBM:GetUnitIdFromGUID(sourceGUID)
|
|
if unitId and UnitExists(unitId) and UnitAffectingCombat(unitId) and (allowFriendly or not UnitIsFriend("player", unitId)) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function bossModPrototype:IsCriteriaCompleted(criteriaIDToCheck)
|
|
if not isRetail then
|
|
print("bossModPrototype:IsCriteriaCompleted should not be called in classic, report this message")
|
|
return false
|
|
end
|
|
if not criteriaIDToCheck then
|
|
error("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.GetUnitIdFromCID = DBM.GetUnitIdFromCID
|
|
bossModPrototype.GetUnitIdFromGUID = DBM.GetUnitIdFromGUID
|
|
bossModPrototype.CheckNearby = DBM.CheckNearby
|
|
bossModPrototype.IsTrivial = DBM.IsTrivial
|
|
bossModPrototype.GetGossipID = DBM.GetGossipID
|
|
bossModPrototype.SelectMatchingGossip = DBM.SelectMatchingGossip
|
|
bossModPrototype.SelectGossip = DBM.SelectGossip
|
|
|
|
do
|
|
local TargetScanning = private:GetModule("TargetScanning")
|
|
|
|
function bossModPrototype:GetBossTarget(...)
|
|
return TargetScanning:GetBossTarget(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:BossTargetScannerAbort(...)
|
|
return TargetScanning:BossTargetScannerAbort(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:BossUnitTargetScannerAbort(...)
|
|
return TargetScanning:BossUnitTargetScannerAbort(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:BossUnitTargetScanner(...)
|
|
return TargetScanning:BossUnitTargetScanner(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:BossTargetScanner(...)
|
|
return TargetScanning:BossTargetScanner(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:StartRepeatedScan(...)
|
|
return TargetScanning:StartRepeatedScan(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:StopRepeatedScan(...)
|
|
return TargetScanning:StopRepeatedScan(...)--self/bossModPrototype missing not a bug, function doesn't need it
|
|
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, distance, defaultReturn)
|
|
if not DBM.Options.DontShowFarWarnings then return true end--Global disable.
|
|
cidOrGuid = cidOrGuid or self.creatureId
|
|
local uId
|
|
if type(cidOrGuid) == "number" then--CID passed
|
|
uId = DBM:GetUnitIdFromCID(cidOrGuid, onlyBoss)
|
|
else--GUID
|
|
uId = DBM:GetUnitIdFromGUID(cidOrGuid, onlyBoss)
|
|
end
|
|
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, distance, 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
|
|
return rangeCache[cidOrGuid] < distance
|
|
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
|
|
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
|
|
--[[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 private.specRoleTable[currentSpecID][flagText] then
|
|
return true
|
|
end
|
|
elseif private.specRoleTable[currentSpecID][flagText] then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function bossModPrototype:IsMeleeDps(uId)
|
|
if uId then--This version includes ONLY melee dps
|
|
local name = GetUnitName(uId, true)
|
|
--First we check if we have acccess to specID (ie remote player is using DBM or Bigwigs)
|
|
if isRetail and raid[name].specID then--We know their specId
|
|
local specID = raid[name].specID
|
|
return private.specRoleTable[specID]["MeleeDps"]
|
|
else
|
|
--Role checks are second best thing
|
|
local role = UnitGroupRolesAssigned(uId)
|
|
if isRetail and (role == "HEALER" or role == "TANK") or GetPartyAssignment("MAINTANK", uId, 1) then--Auto filter healer/tank from dps check, can't filter healers in classic
|
|
return false
|
|
end
|
|
--Class checks for things that are a sure thing anywyas
|
|
local _, class = UnitClass(uId)
|
|
if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" or class == "DEMONHUNTER" then
|
|
return true
|
|
end
|
|
--Now we do the ugly checks thanks to Inspect throttle
|
|
if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" or class == "MONK" then
|
|
local unitMaxPower = UnitPowerMax(uId)
|
|
if not isRetail and unitMaxPower < 7500 then
|
|
return true
|
|
end
|
|
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
|
|
end
|
|
return false
|
|
end
|
|
--Personal check Only
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
return private.specRoleTable[currentSpecID]["MeleeDps"]
|
|
end
|
|
|
|
function DBM: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
|
|
--Class checks performed first due to mechanical check needing to be broader than a specID check
|
|
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
|
|
--Now we check if we have acccess to specID (ie remote player is using DBM or Bigwigs)
|
|
local name = GetUnitName(uId, true)
|
|
if isRetail and raid[name].specID then--We know their specId
|
|
local specID = raid[name].specID
|
|
return private.specRoleTable[specID]["Melee"]
|
|
else
|
|
--Now we do the ugly checks thanks to Inspect throttle
|
|
if (class == "DRUID" or class == "SHAMAN" or class == "PALADIN") then
|
|
local unitMaxPower = UnitPowerMax(uId)
|
|
if not isRetail and unitMaxPower < 7500 then
|
|
return true
|
|
end
|
|
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
|
|
--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
|
|
end
|
|
return false
|
|
end
|
|
--Personal check Only
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
return private.specRoleTable[currentSpecID]["Melee"]
|
|
end
|
|
bossModPrototype.IsMelee = DBM.IsMelee
|
|
|
|
function DBM:IsRanged(uId)
|
|
if uId then
|
|
local name = GetUnitName(uId, true)
|
|
if isRetail and raid[name].specID then--We know their specId
|
|
local specID = raid[name].specID
|
|
return private.specRoleTable[specID]["Ranged"]
|
|
else
|
|
print("bossModPrototype:IsRanged should not be called on external units if specID is unavailable, report this message")
|
|
end
|
|
end
|
|
--Personal check Only
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
return private.specRoleTable[currentSpecID]["Ranged"]
|
|
end
|
|
bossModPrototype.IsRanged = DBM.IsRanged
|
|
|
|
function bossModPrototype:IsSpellCaster(uId)
|
|
if uId then
|
|
local name = GetUnitName(uId, true)
|
|
if isRetail and raid[name].specID then--We know their specId
|
|
local specID = raid[name].specID
|
|
return private.specRoleTable[specID]["SpellCaster"]
|
|
else
|
|
print("bossModPrototype:IsSpellCaster should not be called on external units if specID is unavailable, report this message")
|
|
end
|
|
end
|
|
--Personal check Only
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
return private.specRoleTable[currentSpecID]["SpellCaster"]
|
|
end
|
|
|
|
function bossModPrototype:IsMagicDispeller(uId)
|
|
if uId then
|
|
local name = GetUnitName(uId, true)
|
|
if isRetail and raid[name].specID then--We know their specId
|
|
local specID = raid[name].specID
|
|
return private.specRoleTable[specID]["MagicDispeller"]
|
|
else
|
|
print("bossModPrototype:IsMagicDispeller should not be called on external units if specID is unavailable, report this message")
|
|
end
|
|
end
|
|
--Personal check Only
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
return private.specRoleTable[currentSpecID]["MagicDispeller"]
|
|
end
|
|
|
|
---------------------
|
|
-- Sort Methods --
|
|
---------------------
|
|
function DBM.SortByGroup(v1, v2)
|
|
return DBM:GetGroupId(DBM:GetUnitFullName(v1), true) < DBM:GetGroupId(DBM:GetUnitFullName(v2), true)
|
|
end
|
|
function DBM.SortByTankAlpha(v1, v2)
|
|
--Tank > Melee > Ranged prio, and if two of any of types, alphabetical names are preferred
|
|
if DBM:IsTanking(v1) == DBM:IsTanking(v2) then
|
|
return DBM:GetUnitFullName(v1) < DBM:GetUnitFullName(v2)
|
|
--if one is tank and one isn't, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsTanking(v1) and not DBM:IsTanking(v2) then
|
|
return true
|
|
elseif DBM:IsTanking(v2) and not DBM:IsTanking(v1) then
|
|
return false
|
|
elseif DBM:IsMelee(v1) == DBM:IsMelee(v2) then
|
|
return DBM:GetUnitFullName(v1) < DBM:GetUnitFullName(v2)
|
|
--if one is melee and one is ranged, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsMelee(v1) and not DBM:IsMelee(v2) then
|
|
return true
|
|
elseif DBM:IsMelee(v2) and not DBM:IsMelee(v1) then
|
|
return false
|
|
end
|
|
end
|
|
function DBM.SortByTankRoster(v1, v2)
|
|
--Tank > Melee > Ranged prio, and if two of any of types, roster index as secondary
|
|
if DBM:IsTanking(v1) == DBM:IsTanking(v2) then
|
|
return DBM:GetGroupId(DBM:GetUnitFullName(v1), true) < DBM:GetGroupId(DBM:GetUnitFullName(v2), true)
|
|
--if one is melee and one is ranged, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsTanking(v1) and not DBM:IsTanking(v2) then
|
|
return true
|
|
elseif DBM:IsTanking(v2) and not DBM:IsTanking(v1) then
|
|
return false
|
|
elseif DBM:IsMelee(v1) == DBM:IsMelee(v2) then
|
|
return DBM:GetGroupId(DBM:GetUnitFullName(v1), true) < DBM:GetGroupId(DBM:GetUnitFullName(v2), true)
|
|
--if one is melee and one is ranged, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsMelee(v1) and not DBM:IsMelee(v2) then
|
|
return true
|
|
elseif DBM:IsMelee(v2) and not DBM:IsMelee(v1) then
|
|
return false
|
|
end
|
|
end
|
|
function DBM.SortByMeleeAlpha(v1, v2)
|
|
--if both are melee, the return values are equal and we use alpha sort
|
|
--if both are ranged, the return values are equal and we use alpha sort
|
|
if DBM:IsMelee(v1) == DBM:IsMelee(v2) then
|
|
return DBM:GetUnitFullName(v1) < DBM:GetUnitFullName(v2)
|
|
--if one is melee and one is ranged, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsMelee(v1) and not DBM:IsMelee(v2) then
|
|
return true
|
|
elseif DBM:IsMelee(v2) and not DBM:IsMelee(v1) then
|
|
return false
|
|
end
|
|
end
|
|
function DBM.SortByMeleeRoster(v1, v2)
|
|
--if both are melee, the return values are equal and we use raid roster index sort
|
|
--if both are ranged, the return values are equal and we use raid roster index sort
|
|
if DBM:IsMelee(v1) == DBM:IsMelee(v2) then
|
|
return DBM:GetGroupId(DBM:GetUnitFullName(v1), true) < DBM:GetGroupId(DBM:GetUnitFullName(v2), true)
|
|
--if one is melee and one is ranged, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsMelee(v1) and not DBM:IsMelee(v2) then
|
|
return true
|
|
elseif DBM:IsMelee(v2) and not DBM:IsMelee(v1) then
|
|
return false
|
|
end
|
|
end
|
|
function DBM.SortByRangedAlpha(v1, v2)
|
|
--if both are melee, the return values are equal and we use alpha sort
|
|
--if both are ranged, the return values are equal and we use alpha sort
|
|
if DBM:IsRanged(v1) == DBM:IsRanged(v2) then
|
|
return DBM:GetUnitFullName(v1) < DBM:GetUnitFullName(v2)
|
|
--if one is melee and one is ranged, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsRanged(v1) and not DBM:IsRanged(v2) then
|
|
return true
|
|
elseif DBM:IsRanged(v2) and not DBM:IsRanged(v1) then
|
|
return false
|
|
end
|
|
end
|
|
function DBM.SortByRangedRoster(v1, v2)
|
|
--if both are melee, the return values are equal and we use raid roster index sort
|
|
--if both are ranged, the return values are equal and we use raid roster index sort
|
|
if DBM:IsRanged(v1) == DBM:IsRanged(v2) then
|
|
return DBM:GetGroupId(DBM:GetUnitFullName(v1), true) < DBM:GetGroupId(DBM:GetUnitFullName(v2), true)
|
|
--if one is melee and one is ranged, they are not equal so it goes to the below elseifs that prio melee
|
|
elseif DBM:IsRanged(v1) and not DBM:IsRanged(v2) then
|
|
return true
|
|
elseif DBM:IsRanged(v2) and not DBM:IsRanged(v1) then
|
|
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
|
|
|
|
-- if we catch someone in a tank stance keep sending them warnings, classic only
|
|
local playerIsTank = false
|
|
|
|
function bossModPrototype:IsTank()
|
|
--IsTanking already handles external calls, no need here.
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
if not isRetail then
|
|
if private.specRoleTable[currentSpecID]["Tank"] then
|
|
-- 17 defensive stance, 5487 bear form, 9634 dire bear, 25780 righteous fury
|
|
if playerIsTank or GetShapeshiftFormID() == 18 or DBM:UnitBuff('player', 5487, 9634) then
|
|
playerIsTank = true
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
|
|
return role == "TANK"
|
|
end
|
|
|
|
function bossModPrototype:IsDps(uId)
|
|
if uId then--External unit call.
|
|
--no SpecID checks because SpecID is only availalbe with DBM/Bigwigs, but both DBM/Bigwigs auto set DAMAGER/HEALER/TANK roles anyways so it'd be redundant
|
|
return isRetail and UnitGroupRolesAssigned(uId) == "DAMAGER" or not GetPartyAssignment("MAINTANK", uId, 1)
|
|
end
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
if not isRetail then
|
|
return private.specRoleTable[currentSpecID]["Dps"]
|
|
end
|
|
local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
|
|
return role == "DAMAGER"
|
|
end
|
|
|
|
function DBM:IsHealer(uId)
|
|
if uId then--External unit call.
|
|
if not isRetail then
|
|
print("bossModPrototype:IsHealer should not be called in classic, report this message")
|
|
return false
|
|
end
|
|
--no SpecID checks because SpecID is only availalbe with DBM/Bigwigs, but both DBM/Bigwigs auto set DAMAGER/HEALER/TANK roles anyways so it'd be redundant
|
|
return UnitGroupRolesAssigned(uId) == "HEALER"
|
|
end
|
|
if not currentSpecID then
|
|
DBM:SetCurrentSpecInfo()
|
|
end
|
|
if not isRetail then
|
|
if private.specRoleTable[currentSpecID]["Healer"] then
|
|
if playerClass == "DRUID" then
|
|
-- not in form (moonkin for balance, cat/bear for ferals)
|
|
return GetShapeshiftFormID() == nil
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
|
|
return role == "HEALER"
|
|
end
|
|
bossModPrototype.IsHealer = DBM.IsHealer
|
|
|
|
function DBM:IsTanking(playerUnitID, enemyUnitID, isName, onlyRequested, enemyGUID, includeTarget, onlyS3)
|
|
--Didn't have playerUnitID so combat log name was passed
|
|
if isName then
|
|
playerUnitID = DBM:GetRaidUnitId(playerUnitID)
|
|
end
|
|
if not playerUnitID then
|
|
DBM:Debug("IsTanking passed with invalid unit", 2)
|
|
return false
|
|
end
|
|
--If we don't know enemy unit token, but know it's GUID
|
|
if not enemyUnitID and enemyGUID then
|
|
enemyUnitID = DBM:GetUnitIdFromGUID(enemyGUID)
|
|
end
|
|
|
|
--Threat/Tanking Checks
|
|
--We have both units. No need to find unitID
|
|
if enemyUnitID then
|
|
--Check threat first
|
|
local tanking, status = UnitDetailedThreatSituation(playerUnitID, enemyUnitID)
|
|
if (not onlyS3 and tanking) or (status == 3) then
|
|
return true
|
|
end
|
|
--Non threat fallback
|
|
if includeTarget and UnitExists(enemyUnitID.."target") then
|
|
if UnitIsUnit(playerUnitID, enemyUnitID.."target") then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
--if onlyRequested is false/nil, it means we also accept anyone that's a tank role or tanking any boss unit
|
|
if not onlyRequested then
|
|
--Use these as fallback if threat target not found
|
|
if GetPartyAssignment("MAINTANK", playerUnitID, 1) then
|
|
return true
|
|
end
|
|
if isRetail then
|
|
--no SpecID checks because SpecID is only availalbe with DBM/Bigwigs, but both DBM/Bigwigs auto set DAMAGER/HEALER/TANK roles anyways so it'd be redundant
|
|
if UnitGroupRolesAssigned(playerUnitID) == "TANK" then
|
|
return true
|
|
end
|
|
for i = 1, 10 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 enemyGUID or (guid and guid == enemyGUID) then
|
|
--Check threat first
|
|
local tanking, status = UnitDetailedThreatSituation(playerUnitID, unitID)
|
|
if (not onlyS3 and tanking) or (status == 3) then
|
|
return true
|
|
end
|
|
--Non threat fallback
|
|
if includeTarget and UnitExists(unitID.."target") then
|
|
if UnitIsUnit(playerUnitID, unitID.."target") then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
bossModPrototype.IsTanking = DBM.IsTanking
|
|
|
|
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 (isRetail and UnitGroupRolesAssigned(uId..i) == "TANK" or GetPartyAssignment("MAINTANK", uId..i, 1)) and not UnitIsDeadOrGhost(uId..i) then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
-----------------------
|
|
-- Filter Methods --
|
|
-----------------------
|
|
|
|
do
|
|
local interruptSpells = {
|
|
[1766] = true,--Rogue Kick
|
|
[2139] = true,--Mage Counterspell
|
|
[6552] = true,--Warrior Pummel
|
|
[15487] = true,--Priest Silence
|
|
[19647] = true,--Warlock pet Spell Lock
|
|
[47528] = true,--Death Knight Mind Freeze
|
|
[57994] = true,--Shaman Wind Shear
|
|
[78675] = true,--Druid Solar Beam
|
|
[89766] = true,--Warlock Pet Axe Toss
|
|
[96231] = true,--Paldin Rebuke
|
|
[106839] = true,--Druid Skull Bash
|
|
[116705] = true,--Monk Spear Hand Strike
|
|
[147362] = true,--Hunter Countershot
|
|
[183752] = true,--Demon hunter Disrupt
|
|
[351338] = true,--Evoker Quell
|
|
}
|
|
--onlyTandF param is used when CheckInterruptFilter is actually being used for a simpe target/focus check and nothing more.
|
|
--checkCooldown should always be passed true except for special rotations like count warnings when you should be alerted it's your turn even if you dropped ball and put it on CD at wrong time
|
|
--ignoreTandF is passed usually when interrupt is on a main boss or event that is global to entire raid and should always be alerted regardless of targetting.
|
|
function bossModPrototype:CheckInterruptFilter(sourceGUID, checkOnlyTandF, checkCooldown, ignoreTandF)
|
|
--Check healer spec filter
|
|
if self:IsHealer() and (self.isTrashMod and DBM.Options.FilterTInterruptHealer or not self.isTrashMod and DBM.Options.FilterBInterruptHealer) then
|
|
return false
|
|
end
|
|
|
|
--Check if cooldown check is required
|
|
if checkCooldown and (self.isTrashMod and DBM.Options.FilterTInterruptCooldown or not self.isTrashMod and DBM.Options.FilterBInterruptCooldown) then
|
|
for spellID, _ in pairs(interruptSpells) do
|
|
--For an inverse check, don't need to check if it's known, if it's on cooldown it's known
|
|
--This is possible since no class has 2 interrupt spells (well, actual interrupt spells)
|
|
if (GetSpellCooldown(spellID)) ~= 0 then--Spell is on cooldown
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
local unitID = (UnitGUID("target") == sourceGUID) and "target" or not isClassic and (UnitGUID("focus") == sourceGUID) and "focus"
|
|
--Check if target/focus is required (or if onlyTandF is used, meaning this isn't actually an interrupt API check)
|
|
if checkOnlyTandF or (self.isTrashMod and DBM.Options.FilterTTargetFocus or not self.isTrashMod and DBM.Options.FilterBTargetFocus) then
|
|
--Just return false if source isn't our target or focus, no need to do further checks
|
|
if not ignoreTandF and not unitID then
|
|
return false
|
|
end
|
|
end
|
|
|
|
--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 isRetail and 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
|
|
end
|
|
|
|
do
|
|
--lazyCheck mostly for migration, doesn't distinquish dispel types
|
|
local lazyCheck = {
|
|
[88423] = true,--Druid: Nature's Cure (Dps: Magic only. Healer: Magic, Curse, Poison)
|
|
[2782] = true,--Druid: Remove Corruption (Curse and Poison)
|
|
[115450] = true,--Monk: Detox (Healer) (Magic, Poison, and Disease)
|
|
[218164] = true,--Monk: Detox (non Healer) (Poison and Disease)
|
|
[527] = true,--Priest: Purify (Magic and Disease)
|
|
[213634] = true,--Priest: Purify Disease (Disease)
|
|
[4987] = true,--Paladin: Cleanse ( Dps/Healer: Magic. Healer Only: Poison, Disease)
|
|
[51886] = true,--Shaman: Cleanse Spirit (Curse)
|
|
[77130] = true,--Shaman: Purify Spirit (Magic and Curse)
|
|
[475] = true,--Mage: Remove Curse (Curse)
|
|
[89808] = true,--Warlock: Singe Magic (Magic)
|
|
[360823] = true,--Evoker: Naturalize (Magic and Poison)
|
|
[374251] = true,--Evoker: Cauterizing Flame (Bleed, Poison, Curse, and Disease)
|
|
[365585] = true,--Evoker: Expunge (Poison)
|
|
}
|
|
--Obviously only checks spells releventt for the dispel type
|
|
local typeCheck = {
|
|
["magic"] = {
|
|
[88423] = true,--Druid: Nature's Cure (Dps: Magic only. Healer: Magic, Curse, Poison)
|
|
[115450] = true,--Monk: Detox (Healer) (Magic, Poison, and Disease)
|
|
[527] = true,--Priest: Purify (Magic and Disease)
|
|
[4987] = true,--Paladin: Cleanse ( Dps/Healer: Magic. Healer Only: Poison, Disease)
|
|
[77130] = true,--Shaman: Purify Spirit (Magic and Curse)
|
|
[89808] = true,--Warlock: Singe Magic (Magic)
|
|
[360823] = true,--Evoker: Naturalize (Magic and Poison)
|
|
},
|
|
["curse"] = {
|
|
[88423] = true,--Druid: Nature's Cure (Dps: Magic only. Healer: Magic, Curse, Poison)
|
|
[2782] = true,--Druid: Remove Corruption (Curse and Poison)
|
|
[51886] = true,--Shaman: Cleanse Spirit (Curse)
|
|
[77130] = true,--Shaman: Purify Spirit (Magic and Curse)
|
|
[475] = true,--Mage: Remove Curse (Curse)
|
|
[374251] = true,--Evoker: Cauterizing Flame (Bleed, Poison, Curse, and Disease)
|
|
},
|
|
["poison"] = {
|
|
[88423] = true,--Druid: Nature's Cure (Dps: Magic only. Healer: Magic, Curse, Poison)
|
|
[2782] = true,--Druid: Remove Corruption (Curse and Poison)
|
|
[115450] = true,--Monk: Detox (Healer) (Magic, Poison, and Disease)
|
|
[218164] = true,--Monk: Detox (non Healer) (Poison and Disease)
|
|
[4987] = true,--Paladin: Cleanse ( Dps/Healer: Magic. Healer Only: Poison, Disease)
|
|
[360823] = true,--Evoker: Naturalize (Magic and Poison)
|
|
[374251] = true,--Evoker: Cauterizing Flame (Bleed, Poison, Curse, and Disease)
|
|
[365585] = true,--Evoker: Expunge (Poison)
|
|
},
|
|
["disease"] = {
|
|
[115450] = true,--Monk: Detox (Healer) (Magic, Poison, and Disease)
|
|
[218164] = true,--Monk: Detox (non Healer) (Poison and Disease)
|
|
[527] = true,--Priest: Purify (Magic and Disease)
|
|
[213634] = true,--Priest: Purify Disease (Disease)
|
|
[4987] = true,--Paladin: Cleanse ( Dps/Healer: Magic. Healer Only: Poison, Disease)
|
|
[374251] = true,--Evoker: Cauterizing Flame (Bleed, Poison, Curse, and Disease)
|
|
},
|
|
["bleed"] = {
|
|
[374251] = true,--Evoker: Cauterizing Flame (Bleed, Poison, Curse, and Disease)
|
|
},
|
|
}
|
|
local lastCheck, lastReturn = 0, true
|
|
function bossModPrototype:CheckDispelFilter(dispelType)
|
|
if not DBM.Options.FilterDispel then return true end
|
|
-- Retail - 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)
|
|
-- Classic - Druid: Remove Curse (2782), Priest: Purify (527), Paladin: Cleanse (4987), Mage: Remove Curse (475)
|
|
--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 GetTime() - lastCheck < 0.1 then--Recently returned status, return same status to save cpu from aggressive api checks caused by CheckDispelFilter running on multiple raid members getting debuffed at once
|
|
return lastReturn
|
|
end
|
|
if dispelType then
|
|
--Singe magic requires checking if pet is out
|
|
if dispelType == "magic" and (GetSpellCooldown(89808)) == 0 and (UnitExists("pet") and self:GetCIDFromGUID(UnitGUID("pet")) == 416) then
|
|
lastCheck = GetTime()
|
|
lastReturn = true
|
|
return true
|
|
end
|
|
--We cannot do inverse check here because some classes actually have two dispels for same type (such as evoker)
|
|
--Therefor, we can't go false if only one of them are on cooldown. We have to go true of any of them aren't on CD instead
|
|
--As such, we have to check if a spell is known in addition to it not being on cooldown
|
|
for spellID, _ in pairs(typeCheck[dispelType]) do
|
|
if typeCheck[dispelType][spellID] and IsSpellKnown(spellID) and (GetSpellCooldown(spellID)) == 0 then--Spell is known and not on cooldown
|
|
lastCheck = GetTime()
|
|
if (spellID == 4987 or spellID == 88423) and not DBM:IsHealer() then--These spellIds can only dispel if healer specced
|
|
lastReturn = false
|
|
else--We trust the table return
|
|
lastReturn = true
|
|
end
|
|
return lastReturn
|
|
end
|
|
end
|
|
else--use lazy check until all mods are migrated to define type
|
|
for spellID, _ in pairs(lazyCheck) do
|
|
if IsSpellKnown(spellID) and (GetSpellCooldown(spellID)) == 0 then--Spell is known and not on cooldown
|
|
lastCheck = GetTime()
|
|
lastReturn = true
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
lastCheck = GetTime()
|
|
lastReturn = false
|
|
return false
|
|
end
|
|
end
|
|
|
|
----------------------------
|
|
-- Boss Health Function --
|
|
----------------------------
|
|
--This accepts both CID and GUID which makes switching to UnitPercentHealthFromGUID and UnitTokenFromGUID not as cut and dry
|
|
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, does not exist in classic
|
|
elseif isRetail and ((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
|
|
if isRetail then
|
|
for i = 1, 10 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
|
|
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
|
|
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
|
|
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
|
|
|
|
-------------------------
|
|
-- Timers Table Util --
|
|
-------------------------
|
|
function bossModPrototype:GetFromTimersTable(table, difficultyName, phase, spellId, count)
|
|
local prev = table
|
|
|
|
if difficultyName ~= false then
|
|
if not difficultyName or not prev[difficultyName] then
|
|
DBM:Debug("difficultyName is missing from table")
|
|
return
|
|
end
|
|
prev = prev[difficultyName]
|
|
end
|
|
|
|
if phase ~= false then
|
|
if not phase or not prev[phase] then
|
|
DBM:Debug("phase is missing from table")
|
|
return
|
|
end
|
|
prev = prev[phase]
|
|
end
|
|
|
|
if not prev[spellId] then
|
|
DBM:Debug("spellId is missing from table")
|
|
return
|
|
end
|
|
prev = prev[spellId]
|
|
|
|
if count then
|
|
prev = prev[count]
|
|
end
|
|
|
|
return prev
|
|
end
|
|
|
|
-----------------------
|
|
-- 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(true)
|
|
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(true)
|
|
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, isRetail and 237538 or 136106)
|
|
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, isRetail and 237538 or 136106)
|
|
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, customName, originalSpellID)
|
|
local spellName
|
|
if customName then
|
|
spellName = customName
|
|
else
|
|
spellName = parseSpellName(spellId, announceType) or CL.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(originalSpellID or 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
|
|
|
|
function announcePrototype:SetText(customName)
|
|
local text, spellName = setText(self.announceType, self.spellId, self.castTime, self.preWarnTime, customName)
|
|
self.text = text
|
|
self.spellName = spellName
|
|
end
|
|
|
|
--Not to be confused with SetText, which only sets the text of object.
|
|
--This changes actual ID so announce callback also swaps ID for WAs
|
|
function announcePrototype:UpdateKey(altSpellId)
|
|
self.spellId = altSpellId
|
|
self.icon = parseSpellIcon(altSpellId, self.announceType, self.icon)
|
|
if self.announceType then
|
|
--Regenerate auto localized text if it's an auto localized alert
|
|
local text, spellName = setText(self.announceType, self.spellId, self.castTime, self.preWarnTime)
|
|
self.text = text
|
|
self.spellName = spellName
|
|
else--Just regenerating spellName not message text because it's likely a custom text object such as NewSpecialWarning
|
|
self.spellName = parseSpellName(altSpellId)
|
|
end
|
|
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 announceCount
|
|
if self.announceType and (self.announceType == "count" or self.announceType == "targetcount" or self.announceType == "sooncount" or self.announceType == "incomingcount") then--Don't use find "count" here, it'll match countdown
|
|
--Stage triggers don't pass count, but they do not need to, there is a stage callback and trigger option in WA that should be used
|
|
if type(argTable[1]) == "number" then
|
|
announceCount = argTable[1]
|
|
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 and playerIcon <= 8 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
|
|
--Message: Full message text
|
|
--Icon: Texture path/id for icon
|
|
--Type: Announce type
|
|
----Types: you, target, targetcount, 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, targetcount
|
|
------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). BW would call this "emphasized"
|
|
--announceCount: If it's a count announce, this will provide access to the number value of that count. This, along with spellId should be used instead of message text scanning for most weak auras that need to target specific count casts
|
|
fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false, announceCount)
|
|
if self.sound > 0 then--0 means muted, 1 means no voice pack support, 2 means voice pack version/support
|
|
if self.sound > 1 and DBM.Options.ChosenVoicePack2 ~= "None" and DBM.Options.VPReplacesAnnounce and not voiceSessionDisabled and not DBM.Options.VPDontMuteSounds and self.sound <= SWFilterDisabled 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
|
|
else
|
|
self.combinedcount = 0
|
|
self.combinedtext = {}
|
|
end
|
|
end
|
|
|
|
--Object that's used when precision isn't possible (number of targets variable or unknown
|
|
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
|
|
DBMScheduler:Unschedule(self.Show, self.mod, self)
|
|
DBMScheduler:Schedule(delay or 0.5, self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
--New object that allows defining count instead of scheduling for more efficient and immediate warnings when precise count is known
|
|
function announcePrototype:PreciseShow(maxTotal, ...)
|
|
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
|
|
DBMScheduler:Unschedule(self.Show, self.mod, self)
|
|
local viableTotal = DBM:NumRealAlivePlayers()
|
|
if (maxTotal == #self.combinedtext) or (viableTotal == #self.combinedtext) then--All targets gathered, show immediately
|
|
self:Show(...)--Does this need self or mod? will it have this bug? https://github.com/DeadlyBossMods/DBM-Unified/issues/153
|
|
else--And even still, use scheduling backup in case counts still fail
|
|
DBMScheduler:Schedule(1.2, self.Show, self.mod, self, ...)
|
|
end
|
|
end
|
|
|
|
function announcePrototype:Schedule(t, ...)
|
|
return DBMScheduler:Schedule(t, self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
function announcePrototype:Countdown(time, numAnnounces, ...)
|
|
DBMScheduler:ScheduleCountdown(time, numAnnounces, self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
function announcePrototype:Cancel(...)
|
|
return DBMScheduler:Unschedule(self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
function announcePrototype:Play(name, customPath)
|
|
local voice = DBM.Options.ChosenVoicePack2
|
|
if voiceSessionDisabled or voice == "None" or not DBM.Options.VPReplacesAnnounce 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 <= SWFilterDisabled 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.ChosenVoicePack2 == "None" or not DBM.Options.VPReplacesAnnounce then return end
|
|
DBMScheduler:Unschedule(self.Play, self.mod, self)--Allow ScheduleVoice to be used in same way as CombinedShow
|
|
return DBMScheduler: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.ChosenVoicePack2 == "None" or not DBM.Options.VPReplacesAnnounce then return end
|
|
return DBMScheduler:Schedule(t, self.Play, self.mod, self, ...)
|
|
end
|
|
|
|
function announcePrototype:CancelVoice(...)
|
|
if voiceSessionDisabled or DBM.Options.ChosenVoicePack2 == "None" or not DBM.Options.VPReplacesAnnounce then return end
|
|
return DBMScheduler:Unschedule(self.Play, self.mod, self, ...)
|
|
end
|
|
|
|
-- old constructor (no auto-localize)
|
|
function bossModPrototype:NewAnnounce(text, color, icon, optionDefault, optionName, soundOption, spellID, waCustomName)
|
|
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
|
|
icon = parseSpellIcon(icon)
|
|
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 = icon,
|
|
spellId = spellID,--For WeakAuras / other callbacks
|
|
},
|
|
mt
|
|
)
|
|
if optionName then
|
|
obj.option = optionName
|
|
self:AddBoolOption(obj.option, optionDefault, "announce", nil, nil, nil, spellID, nil, waCustomName)
|
|
elseif optionName ~= false then
|
|
obj.option = text
|
|
self:AddBoolOption(obj.option, optionDefault, "announce", nil, nil, nil, spellID, nil, waCustomName)
|
|
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 > 10 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
|
|
local text, spellName = setText(announceType, alternateSpellId or spellId, castTime, preWarnTime, nil, spellId)
|
|
icon = parseSpellIcon(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 = icon,
|
|
sound = soundOption or 1,
|
|
type = announceType,
|
|
spellId = spellId,
|
|
spellName = spellName,
|
|
noFilter = noFilter,
|
|
castTime = castTime,
|
|
preWarnTime = preWarnTime,
|
|
},
|
|
mt
|
|
)
|
|
local catType = "announce"--Default to General announce
|
|
if not self.NoSortAnnounce then--ALL announce objects will be assigned "announce", usually for mods that sort by phase instead
|
|
--Change if Personal or Other
|
|
if announceType == "target" or announceType == "targetcount" or announceType == "stack" then
|
|
catType = "announceother"
|
|
end
|
|
end
|
|
if optionName then
|
|
obj.option = optionName
|
|
self:AddBoolOption(obj.option, optionDefault, catType, nil, nil, nil, spellId, announceType)
|
|
elseif optionName ~= false then
|
|
obj.option = catType..spellId..announceType..(optionVersion or "")
|
|
self:AddBoolOption(obj.option, optionDefault, catType, nil, nil, nil, spellId, announceType)
|
|
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) -- 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:NewIncomingAnnounce(spellId, color, ...)
|
|
return newAnnounce(self, "incoming", spellId, color or 2, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewIncomingCountAnnounce(spellId, color, ...)
|
|
return newAnnounce(self, "incomingcount", 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, "addsleft", 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, _, soundOption) -- 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, _, noFilter) -- 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, _, soundOption) -- 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
|
|
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--Old Format Journal
|
|
displayText = L.AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:EJ_GetSectionInfo(string.sub(spellId, 3)) or CL.UNKNOWN)
|
|
elseif type(spellId) == "number" then
|
|
if spellId < 0 then--New format Journal
|
|
spellId = -spellId
|
|
displayText = L.AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:EJ_GetSectionInfo(spellId) or CL.UNKNOWN)
|
|
else
|
|
displayText = L.AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:GetSpellInfo(spellId) or CL.UNKNOWN)
|
|
end
|
|
else
|
|
displayText = L.AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(CL.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 CL.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", nil, nil, nil, spellId, yellType)
|
|
elseif optionName ~= false then
|
|
obj.option = "Yell"..(spellId or yellText)..(yellType ~= "yell" and yellType or "")..(optionVersion or "")
|
|
self:AddBoolOption(obj.option, optionDefault, "yell", nil, nil, nil, spellId, yellType)
|
|
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 not IsInInstance() then--as of 8.2.5+, forbidden in outdoor world
|
|
DBM:Debug("WARNING: A mod is still trying to call chat SAY/YELL messages outdoors, FIXME")
|
|
return
|
|
end
|
|
if DBM.Options.DontSendYells or private.chatBubblesDisabled or self.yellType and self.yellType == "position" and (not isRetail or DBM:UnitBuff("player", voidForm) and DBM.Options.FilterVoidFormSay) then return end
|
|
if not self.option or self.mod.Options[self.option] then
|
|
if self.yellType == "combo" then
|
|
SendChatMessage(pformat(self.text, ...), self.chatType or "YELL")
|
|
else
|
|
SendChatMessage(pformat(self.text, ...), self.chatType or "SAY")
|
|
end
|
|
end
|
|
end
|
|
yellPrototype.Show = yellPrototype.Yell
|
|
|
|
--Force override to use say message, even when object defines "YELL"
|
|
function yellPrototype:Say(...)
|
|
if not IsInInstance() then--as of 8.2.5+, forbidden in outdoor world
|
|
DBM:Debug("WARNING: A mod is still trying to call chat SAY/YELL messages outdoors, FIXME")
|
|
return
|
|
end
|
|
if DBM.Options.DontSendYells or private.chatBubblesDisabled or self.yellType and self.yellType == "position" and (not isRetail or 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 DBMScheduler: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()
|
|
DBMScheduler:ScheduleCountdown(remaining, numAnnounces, self.Yell, self.mod, self, ...)
|
|
end
|
|
else
|
|
DBMScheduler: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()
|
|
DBMScheduler:ScheduleCountdown(remaining, numAnnounces, self.Say, self.mod, self, ...)
|
|
end
|
|
else
|
|
DBMScheduler:ScheduleCountdown(time, numAnnounces, self.Say, self.mod, self, ...)
|
|
end
|
|
end
|
|
|
|
function yellPrototype:Cancel(...)
|
|
return DBMScheduler: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(true)
|
|
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(true)
|
|
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, isRetail and 237538 or 136106)
|
|
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, isRetail and 237538 or 136106)
|
|
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 and playerIcon <= 8 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 specInstructionalRemapTable = {
|
|
["dispel"] = "target",
|
|
["interrupt"] = "spell",
|
|
["interruptcount"] = "count",
|
|
["defensive"] = "spell",
|
|
["taunt"] = "target",
|
|
["soak"] = "spell",
|
|
["soakcount"] = "count",
|
|
["soakpos"] = "spell",
|
|
["switch"] = "spell",
|
|
["switchcount"] = "count",
|
|
-- ["adds"] = "spell",
|
|
-- ["addscount"] = "spell",
|
|
-- ["addscustom"] = "spell",
|
|
["targetchange"] = "target",
|
|
["gtfo"] = "spell",
|
|
["bait"] = "soon",
|
|
["youpos"] = "you",
|
|
["youposcount"] = "youcount",
|
|
["move"] = "spell",
|
|
["keepmove"] = "spell",
|
|
["stopmove"] = "spell",
|
|
["dodge"] = "spell",
|
|
["dodgecount"] = "count",
|
|
["dodgeloc"] = "spell",
|
|
["moveaway"] = "spell",
|
|
["moveawaycount"] = "count",
|
|
["moveto"] = "spell",
|
|
["jump"] = "spell",
|
|
["run"] = "spell",
|
|
["runcount"] = "spell",
|
|
["cast"] = "spell",
|
|
["lookaway"] = "spell",
|
|
["reflect"] = "target",
|
|
}
|
|
|
|
local function setText(announceType, spellId, stacks, customName)
|
|
local text, spellName
|
|
if customName then
|
|
spellName = customName
|
|
else
|
|
spellName = parseSpellName(spellId, announceType) or CL.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
|
|
if DBM.Options.SpamSpecInformationalOnly and specInstructionalRemapTable[announceType] then
|
|
local newType = specInstructionalRemapTable[announceType]
|
|
text = L.AUTO_SPEC_WARN_TEXTS[newType]:format(spellName)
|
|
else
|
|
text = L.AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName)
|
|
end
|
|
end
|
|
return text, spellName
|
|
end
|
|
|
|
function specialWarningPrototype:SetText(customName)
|
|
local text, spellName = setText(self.announceType, self.spellId, self.stacks, customName)
|
|
self.text = text
|
|
self.spellName = spellName
|
|
end
|
|
|
|
function specialWarningPrototype:UpdateKey(altSpellId)
|
|
self.spellId = altSpellId
|
|
self.icon = parseSpellIcon(altSpellId, self.announceType, self.icon)
|
|
if self.announceType then
|
|
--Regenerate auto localized text if it's an auto localized alert
|
|
local text, spellName = setText(self.announceType, self.spellId, self.stacks)
|
|
self.text = text
|
|
self.spellName = spellName
|
|
else--Just regenerating spellName not message text because it's likely a custom text object such as NewSpecialWarning
|
|
self.spellName = parseSpellName(altSpellId)
|
|
end
|
|
end
|
|
|
|
local function canVoiceReplace(self, soundId)
|
|
if voiceSessionDisabled or DBM.Options.ChosenVoicePack2 == "None" then
|
|
return false
|
|
end
|
|
soundId = soundId or self.option and self.mod.Options[self.option .. "SWSound"] or self.flash
|
|
local isVoicePackUsed
|
|
if self.announceType == "gtfo" then
|
|
isVoicePackUsed = DBM.Options.VPReplacesGTFO
|
|
elseif type(soundId) == "number" and soundId < 5 then--Value 1-4 are SW1 defaults, otherwise it's file data ID and handled by Custom
|
|
isVoicePackUsed = DBM.Options["VPReplacesSA"..soundId]
|
|
else
|
|
isVoicePackUsed = DBM.Options.VPReplacesCustom
|
|
end
|
|
return isVoicePackUsed
|
|
end
|
|
|
|
local specTypeFilterTable = {
|
|
["dispel"] = "dispel",
|
|
["interrupt"] = "interrupt",
|
|
["interruptcount"] = "interrupt",
|
|
["defensive"] = "defensive",
|
|
["taunt"] = "taunt",
|
|
["soak"] = "soak",
|
|
["soakcount"] = "soak",
|
|
["soakpos"] = "soak",
|
|
["stack"] = "stack",
|
|
["switch"] = "switch",
|
|
["switchcount"] = "switch",
|
|
["adds"] = "switch",
|
|
["addscount"] = "switch",
|
|
["addscustom"] = "switch",
|
|
["targetchange"] = "switch",
|
|
["gtfo"] = "gtfo",
|
|
}
|
|
|
|
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.isTrashMod and DBM.Options.FilterTrashWarnings2 and (self.mod:IsEasyDungeon() or DBM:IsTrivial()) then return end
|
|
--We also check if person has the role filter turned on (typical for highest end raiders who don't want as much handholding from DBM)
|
|
if specTypeFilterTable[self.announceType] then
|
|
if DBM.Options["SpamSpecRole"..specTypeFilterTable[self.announceType]] then return end
|
|
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
|
|
--Grab count for both the callback and the notes feature
|
|
local announceCount
|
|
if self.announceType and self.announceType:find("count") then
|
|
if self.announceType == "interruptcount" then
|
|
announceCount = argTable[2]--Count should be second arg in table
|
|
else
|
|
announceCount = argTable[1]--Count should be first arg in table
|
|
end
|
|
if type(announceCount) == "string" then
|
|
--Probably a hypehnated double count like inferno slice or marked for death
|
|
--This is pretty atypical in newer content cause it's a bit hacky
|
|
local mainCount = string.split("-", announceCount)
|
|
announceCount = tonumber(mainCount)
|
|
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
|
|
if announceCount then--Counts support different note for EACH count
|
|
local notesTable = {string.split("/", noteText)}
|
|
noteText = notesTable[announceCount]
|
|
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 (self.announceType == "interruptcount") 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") then
|
|
if noteHasName then
|
|
if not DBM.Options.DontShowSpecialWarningFlash and 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
|
|
if not DBM.Options.DontDoSpecialWarningVibrate and DBM.Options.SpecialWarningVibrate5 then
|
|
DBM:VibrateController()
|
|
end
|
|
else
|
|
local number = self.flash
|
|
if not DBM.Options.DontShowSpecialWarningFlash and 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
|
|
if not DBM.Options.DontDoSpecialWarningVibrate and DBM.Options["SpecialWarningVibrate"..number] then
|
|
DBM:VibrateController()
|
|
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, addscount, 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, addscount, 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). BW would call this "emphasized"
|
|
--announceCount: If it's a count announce, this will provide access to the number value of that count. This, along with spellId should be used instead of message text scanning for most weak auras that need to target specific count casts
|
|
fireEvent("DBM_Announce", text, self.icon, self.type, self.spellId, self.mod.id, true, announceCount)
|
|
if self.sound and not DBM.Options.DontPlaySpecialWarningSound and (not self.option or self.mod.Options[self.option.."SWSound"] ~= "None") 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 not DBM.Options.VPDontMuteSounds and canVoiceReplace(self, soundId) and self.hasVoice <= SWFilterDisabled then return end
|
|
DBM:PlaySpecialWarningSound(soundId or 1)
|
|
end
|
|
else
|
|
self.combinedcount = 0
|
|
self.combinedtext = {}
|
|
end
|
|
end
|
|
|
|
--Object that's used when precision isn't possible (number of targets variable or unknown
|
|
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
|
|
DBMScheduler:Unschedule(self.Show, self.mod, self)
|
|
DBMScheduler:Schedule(delay or 0.5, self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
--New object that allows defining count instead of scheduling for more efficient and immediate warnings when precise count is known
|
|
function specialWarningPrototype:PreciseShow(maxTotal, ...)
|
|
--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
|
|
DBMScheduler:Unschedule(self.Show, self.mod, self)
|
|
local viableTotal = DBM:NumRealAlivePlayers()
|
|
if (maxTotal == #self.combinedtext) or (viableTotal == #self.combinedtext) then--All targets gathered, show immediately
|
|
self:Show(...)--Does this need self or mod? will it have this bug? https://github.com/DeadlyBossMods/DBM-Unified/issues/153
|
|
else--And even still, use scheduling backup in case counts still fail
|
|
DBMScheduler:Schedule(1.2, self.Show, self.mod, self, ...)
|
|
end
|
|
end
|
|
|
|
function specialWarningPrototype:DelayedShow(delay, ...)
|
|
DBMScheduler:Unschedule(self.Show, self.mod, self, ...)
|
|
DBMScheduler:Schedule(delay or 0.5, self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
function specialWarningPrototype:Schedule(t, ...)
|
|
return DBMScheduler:Schedule(t, self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
function specialWarningPrototype:Countdown(time, numAnnounces, ...)
|
|
DBMScheduler:ScheduleCountdown(time, numAnnounces, self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
function specialWarningPrototype:Cancel(_, ...) -- t, ...
|
|
return DBMScheduler:Unschedule(self.Show, self.mod, self, ...)
|
|
end
|
|
|
|
--Several voice lines still need generic alternatives that don't feel "instructional"
|
|
local specInstructionalRemapVoiceTable = {
|
|
-- ["dispel"] = "target",
|
|
-- ["interrupt"] = "spell",
|
|
-- ["interruptcount"] = "count",
|
|
-- ["defensive"] = "spell",
|
|
["taunt"] = "changemt",--Remaps sound to say a swap is happening, rather than telling you to taunt boss
|
|
-- ["soak"] = "spell",
|
|
-- ["soakcount"] = "count",
|
|
-- ["soakpos"] = "spell",
|
|
-- ["switch"] = "spell",
|
|
-- ["switchcount"] = "count",
|
|
["adds"] = "mobsoon",--Remaps sound to say mobs incoming only, not to kill them or cc them or anything else.
|
|
["addscount"] = "mobsoon",
|
|
["addscustom"] = "mobsoon",--Remaps sound to say mobs incoming only, not to kill them or cc them or anything else.
|
|
-- ["targetchange"] = "target",
|
|
-- ["gtfo"] = "spell",
|
|
-- ["bait"] = "soon",
|
|
["you"] = "targetyou",--Remaps personal alert to just say "target you", without instruction
|
|
["youpos"] = "targetyou",--Remaps personal alert to just say "target you", without instruction
|
|
["youposcount"] = "targetyou",--Remaps personal alert to just say "target you", without instruction
|
|
-- ["move"] = "spell",
|
|
-- ["keepmove"] = "spell",
|
|
-- ["stopmove"] = "spell",
|
|
-- ["dodge"] = "spell",
|
|
-- ["dodgecount"] = "count",
|
|
-- ["dodgeloc"] = "spell",
|
|
["moveaway"] = "targetyou",--Remaps personal alert to just say "target you", without instruction
|
|
["moveawaycount"] = "targetyou",--Remaps personal alert to just say "target you", without instruction
|
|
-- ["moveto"] = "spell",
|
|
-- ["jump"] = "spell",
|
|
-- ["run"] = "spell",
|
|
-- ["runcount"] = "spell",
|
|
-- ["cast"] = "spell",
|
|
-- ["lookaway"] = "spell",
|
|
-- ["reflect"] = "target",
|
|
}
|
|
|
|
function specialWarningPrototype:Play(name, customPath)
|
|
local always = DBM.Options.AlwaysPlayVoice
|
|
local voice = DBM.Options.ChosenVoicePack2
|
|
local soundId = self.option and self.mod.Options[self.option .. "SWSound"] or self.flash
|
|
if not canVoiceReplace(self, soundId) then return end
|
|
if self.mod:IsEasyDungeon() and self.mod.isTrashMod and DBM.Options.FilterTrashWarnings2 then return end
|
|
if specTypeFilterTable[self.announceType] then
|
|
--Filtered warning, filtered voice
|
|
if DBM.Options["SpamSpecRole"..specTypeFilterTable[self.announceType]] then return end
|
|
elseif DBM.Options.SpamSpecInformationalOnly and specInstructionalRemapVoiceTable[self.announceType] then
|
|
--Instructional disabled, remap to a less instructional voice line
|
|
name = specInstructionalRemapVoiceTable[self.announceType]
|
|
end
|
|
if ((not self.option or self.mod.Options[self.option]) or always) and self.hasVoice <= SWFilterDisabled 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
|
|
--Mute VP if SW sound is set to None in the boss mod.
|
|
if soundId == "None" then return end
|
|
local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
|
|
DBM:PlaySoundFile(path)
|
|
end
|
|
end
|
|
|
|
function specialWarningPrototype:ScheduleVoice(t, ...)
|
|
if not canVoiceReplace(self) then return end
|
|
DBMScheduler:Unschedule(self.Play, self.mod, self)--Allow ScheduleVoice to be used in same way as CombinedShow
|
|
return DBMScheduler:Schedule(t, self.Play, self.mod, self, ...)
|
|
end
|
|
|
|
--Object Permits scheduling voice multiple times for same object
|
|
function specialWarningPrototype:ScheduleVoiceOverLap(t, ...)
|
|
if not canVoiceReplace(self) then return end
|
|
return DBMScheduler:Schedule(t, self.Play, self.mod, self, ...)
|
|
end
|
|
|
|
function specialWarningPrototype:CancelVoice(...)
|
|
if not canVoiceReplace(self) then return end
|
|
return DBMScheduler:Unschedule(self.Play, self.mod, self, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarning(text, optionDefault, optionName, optionVersion, runSound, hasVoice, difficulty, icon, spellID, waCustomName)
|
|
if not text then
|
|
error("NewSpecialWarning: you must provide special warning text", 2)
|
|
return
|
|
end
|
|
if type(text) == "string" and text:match("OptionVersion") then
|
|
error("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
|
|
icon = parseSpellIcon(icon)
|
|
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,
|
|
spellId = spellID,--For WeakAuras / other callbacks
|
|
icon = icon,
|
|
},
|
|
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(obj.option, optionDefault, runSound, self.NoSortAnnounce and "specialannounce" or "announce", spellID, nil, waCustomName)
|
|
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 icon = parseSpellIcon(spellId)
|
|
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 = icon,
|
|
},
|
|
mt
|
|
)
|
|
if optionName then
|
|
obj.option = optionName
|
|
elseif 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
|
|
local path = isRetail and "EncounterJournal" or "AddOns\\DBM-Core\\textures"
|
|
if difficulty == 3 then
|
|
difficultyIcon = "|TInterface\\" .. path .. "\\UI-EJ-Icons.blp:18:18:0:0:255:66:102:118:7:27|t"
|
|
elseif difficulty == 4 then
|
|
difficultyIcon = "|TInterface\\" .. path .. "\\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
|
|
if self.NoSortAnnounce then--ALL special announce objects will be assigned "specialannounce", usually for mods that sort by phase instead
|
|
catType = "specialannounce"
|
|
else
|
|
--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 == "runcount" or announceType == "stack" or announceType == "moveto" or announceType == "soak" or announceType == "soakcount" 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
|
|
end
|
|
self:AddSpecialWarningOption(obj.option, optionDefault, runSound, catType, spellId, announceType)
|
|
end
|
|
obj.voiceOptionId = hasVoice and "Voice"..spellId or nil
|
|
tinsert(self.specwarns, obj)
|
|
return obj
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSpell(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "spell", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningEnd(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "ends", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningFades(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "fades", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSoon(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "soon", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningBait(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "bait", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningDispel(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "dispel", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningInterrupt(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "interrupt", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningInterruptCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "interruptcount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningYou(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "you", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningYouCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "youcount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningYouPos(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "youpos", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningYouPosCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "youposcount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSoakPos(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "soakpos", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningTarget(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "target", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningTargetCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "targetcount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningDefensive(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "defensive", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningTaunt(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "taunt", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningClose(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "close", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningMove(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "move", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningKeepMove(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "keepmove", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningStopMove(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "stopmove", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningGTFO(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "gtfo", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningDodge(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "dodge", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningDodgeCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "dodgecount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningDodgeLoc(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "dodgeloc", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningMoveAway(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "moveaway", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningMoveAwayCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "moveawaycount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningMoveTo(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "moveto", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSoak(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "soak", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSoakCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "soakcount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningJump(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "jump", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningRun(spellId, optionDefault, optionName, optionVersion, runSound, ...)
|
|
return newSpecialWarning(self, "run", spellId, nil, optionDefault, optionName, optionVersion, runSound or 4, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningRunCount(spellId, optionDefault, optionName, optionVersion, runSound, ...)
|
|
return newSpecialWarning(self, "runcount", spellId, nil, optionDefault, optionName, optionVersion, runSound or 4, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningCast(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "cast", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningLookAway(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "lookaway", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningReflect(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "reflect", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "count", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSoonCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "sooncount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningStack(spellId, optionDefault, stacks, ...)
|
|
return newSpecialWarning(self, "stack", spellId, stacks, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSwitch(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "switch", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningSwitchCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "switchcount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningAdds(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "adds", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningAddsCount(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "addscount", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningAddsCustom(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "addscustom", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningTargetChange(spellId, optionDefault, ...)
|
|
return newSpecialWarning(self, "targetchange", spellId, nil, optionDefault, ...)
|
|
end
|
|
|
|
function bossModPrototype:NewSpecialWarningPreWarn(spellId, optionDefault, time, ...)
|
|
return newSpecialWarning(self, "prewarn", spellId, 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 _, count in pairs(DBM:GetCountSounds()) do
|
|
if count.value == voice then
|
|
path = count.path
|
|
maxCount = count.max
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not path or (number > maxCount) then return end
|
|
self:PlaySoundFile(path..number..".ogg")
|
|
end
|
|
|
|
do
|
|
local minVoicePackVersion = isRetail and 15 or 10
|
|
|
|
function DBM:CheckVoicePackVersion(value)
|
|
local activeVP = self.Options.ChosenVoicePack2
|
|
--Check if voice pack out of date
|
|
if activeVP ~= "None" and activeVP == value then
|
|
-- User might reselect "missing" entry shown in GUI if previously selected voice pack is uninstalled or disabled
|
|
if self.VoiceVersions[value] then
|
|
voiceSessionDisabled = false
|
|
if self.VoiceVersions[value] < minVoicePackVersion 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
|
|
SWFilterDisabled = self.VoiceVersions[value]--Set disable to version on current voice pack
|
|
else
|
|
SWFilterDisabled = minVoicePackVersion
|
|
end
|
|
else
|
|
voiceSessionDisabled = true
|
|
end
|
|
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(_, number, noSound, force) -- 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 then
|
|
if self.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
|
|
if not self.Options.DontDoSpecialWarningVibrate and self.Options["SpecialWarningVibrate"..number] then
|
|
self:VibrateController()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------
|
|
-- Timer Object --
|
|
--------------------
|
|
do
|
|
local timerPrototype = {}
|
|
local mt = {__index = timerPrototype}
|
|
local countvoice1, countvoice2, countvoice3, countvoice4
|
|
local countvoice1max, countvoice2max, countvoice3max, countvoice4max = 5, 5, 5, 5
|
|
local countpath1, countpath2, countpath3, countpath4
|
|
|
|
--Merged countdown object for timers with build-in countdown
|
|
function DBM:BuildVoiceCountdownCache()
|
|
countvoice1 = self.Options.CountdownVoice
|
|
countvoice2 = self.Options.CountdownVoice2
|
|
countvoice3 = self.Options.CountdownVoice3
|
|
countvoice4 = self.Options.PullVoice
|
|
for _, count in pairs(DBM:GetCountSounds()) do
|
|
if count.value == countvoice1 then
|
|
countpath1 = count.path
|
|
countvoice1max = count.max
|
|
end
|
|
if count.value == countvoice2 then
|
|
countpath2 = count.path
|
|
countvoice2max = count.max
|
|
end
|
|
if count.value == countvoice3 then
|
|
countpath3 = count.path
|
|
countvoice3max = count.max
|
|
end
|
|
if count.value == countvoice4 then
|
|
countpath4 = count.path
|
|
countvoice4max = count.max
|
|
end
|
|
end
|
|
end
|
|
|
|
local function playCountSound(_, path, requiresCombat) -- timerId, path
|
|
if requiresCombat and not (InCombatLockdown() or UnitAffectingCombat("player")) then return end
|
|
DBM:PlaySoundFile(path)
|
|
end
|
|
|
|
local function playCountdown(timerId, timer, voice, count, requiresCombat)
|
|
if DBM.Options.DontPlayCountdowns then return end
|
|
timer = timer or 10
|
|
count = count or 4
|
|
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\\"
|
|
elseif voice == 4 then
|
|
maxCount = countvoice4max or 10
|
|
path = countpath4 or "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\"
|
|
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", requiresCombat)
|
|
end
|
|
end
|
|
else
|
|
for i = count, 1, -1 do
|
|
if i <= maxCount then
|
|
DBM:Schedule(timer-i, playCountSound, timerId, path..i..".ogg", requiresCombat)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--"break" and "pull" timers have custom classifications that are straight forward and not in this table
|
|
local timerTypeSimplification = {
|
|
--All cooldown times, be they approx cd or next exact, or even AI timers, map to "CD"
|
|
["cdcount"] = "cd",
|
|
["cdsource"] = "cd",
|
|
["nextcount"] = "cd",
|
|
["nextsource"] = "cd",
|
|
["cdspecial"] = "cd",
|
|
["nextspecial"] = "cd",
|
|
["ai"] = "cd",
|
|
["adds"] = "cd",
|
|
["addscustom"] = "cd",
|
|
["cdnp"] = "cd",
|
|
["nextnp"] = "cd",
|
|
|
|
--Combatstart, RPs all map to "warmup"
|
|
["combat"] = "warmup",
|
|
["roleplay"] = "warmup",
|
|
|
|
--all stage types will map to "stage"
|
|
["achievement"] = "stage",
|
|
["stagecount"] = "stage",
|
|
["stagecountcycle"] = "stage",
|
|
["stagecontext"] = "stage",
|
|
["stagecontextcount"] = "stage",
|
|
["intermission"] = "stage",
|
|
["intermissioncount"] = "stage",
|
|
|
|
--Target Bars such as buff/debuff on another player, on self, or on the boss, RPs all map to "target"
|
|
["targetcount"] = "target",
|
|
["fades"] = "target",--Fades is usually used as a personal target timer. So like debuff on other player is "debuff (targetname)" but on self it's just "debuff fades"
|
|
|
|
--All cast bar types map to "cast"
|
|
["active"] = "cast",--Active bars are usually things like Whirlwind is active on the boss, or a channeled cast is being done. so effectively it's for channeled casts, as upposed to regular casts
|
|
["castsource"] = "cast",
|
|
["castcount"] = "cast",
|
|
}
|
|
|
|
--Very similar to above but more specific to key replacement and not type replacement, to match BW behavior for unification of WAs
|
|
local waKeyOverrides = {
|
|
["combat"] = "warmup",
|
|
["roleplay"] = "warmup",
|
|
["achievement"] = "stages",
|
|
["stagecount"] = "stages",
|
|
["stagecountcycle"] = "stages",
|
|
["stagecontext"] = "stages",
|
|
["stagecontextcount"] = "stages",
|
|
["intermission"] = "stages",
|
|
["intermissioncount"] = "stages",
|
|
}
|
|
|
|
function timerPrototype:Start(timer, ...)
|
|
if not self.mod.isDummyMod then--Don't apply following rulesets to pull timers and such
|
|
if DBM.Options.DontShowBossTimers and not self.mod.isTrashMod then return end
|
|
if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end
|
|
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
|
|
local isCountTimer = false
|
|
if self.type and (self.type == "cdcount" or self.type == "nextcount" or self.type == "stagecount" or self.type == "stagecontextcount" or self.type == "stagecountcycle" or self.type == "intermissioncount") then
|
|
isCountTimer = true
|
|
end
|
|
if isCountTimer 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])
|
|
DBM:Unschedule(playCountSound, self.startedTimers[i])
|
|
fireEvent("DBM_TimerStop", self.startedTimers[i])
|
|
tremove(self.startedTimers, i)
|
|
end
|
|
end
|
|
timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer
|
|
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
|
|
--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
|
|
if self.type == "ai" then--A learning timer
|
|
if not DBM.Options.AITimer then return end
|
|
if timer > 5 then--Normal behavior.
|
|
local newPhase = false
|
|
for i = 1, 5 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 > 5 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 5 or less is indicating phase change, with timer as phase number
|
|
if not isRetail then
|
|
timer = floor(timer)--Floor inprecise timers in classic because combat is mostly caused by PLAYER_REGEN in dungeons
|
|
end
|
|
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") and not self.allowdouble 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
|
|
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"]
|
|
else--No option, or secondary option, set colorId to hardcoded color type
|
|
colorId = self.colorType
|
|
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, self.requiresCombat)--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, self.simpType == "cd")
|
|
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, which is one of only 7 possible types: "cd" for coolodwns, "target" for target bars such as debuff on a player, "stage" for any kind of stage timer (stage ends, next stage, or even just a warmup timer like "fight begins"), and then "cast" timer which is used for both a regular cast and a channeled cast (ie boss is casting frostbolt, or boss is channeling whirlwind). Lastly, break, pull, and berserk timers are "breaK", "pull", and "berserk" respectively
|
|
--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
|
|
--timerCount if current timer is a count timer. Returns number (count value) needed to have weak auras that trigger off a specific timer count without using localized message text
|
|
local guid, timerCount
|
|
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
|
|
--Not most efficient way to do it, but since it's already being done for guid, it's best not to repeat the work
|
|
if isCountTimer and type(v) == "number" then
|
|
timerCount = v
|
|
end
|
|
end
|
|
end
|
|
--Mods that have specifically flagged that it's safe to assume all timers from that boss mod belong to boss1
|
|
--This check is performed secondary to args scan so that no adds guids are overwritten
|
|
if not guid and self.mod.sendMainBossGUID and not DBM.Options.DontSendBossGUIDs and (self.type == "cd" or self.type == "next" or self.type == "cdcount" or self.type == "nextcount" or self.type == "cdspecial" or self.type == "ai") then
|
|
guid = UnitGUID("boss1")
|
|
end
|
|
fireEvent("DBM_TimerStart", id, msg, timer, self.icon, self.simpType, self.waSpecialKey or self.spellId, colorId, self.mod.id, self.keep, self.fade, self.name, guid, timerCount)
|
|
--Bssically tops bar from starting if it's being put on a plater nameplate, to give plater users option to have nameplate CDs without actually using the bars
|
|
--This filter will only apply to trash mods though, boss timers will always be shown due to need to have them exist for Pause, Resume, Update, and GetTime/GetRemaining methods
|
|
if guid and (self.type == "cdnp" or self.type == "nextnp") and not (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then
|
|
DBT:CancelBar(id)--Cancel bar without stop callback
|
|
return false, "disabled"
|
|
end
|
|
if not tContains(self.startedTimers, id) then--Make sure timer doesn't exist already before adding it
|
|
tinsert(self.startedTimers, id)
|
|
end
|
|
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, self.name)--Timer ID, spellId, modId, true/nil, spellName (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, self.name)--Timer ID, spellId, modId, true/nil, spellName (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, bar.requiresCombat)--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, self.name)--Timer ID, spellId, modId, true/nil, spellName (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, self.name)
|
|
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, bar.requiresCombat)--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, ...)
|
|
DBMScheduler:Unschedule(self.Start, self.mod, self, ...)
|
|
DBMScheduler:Schedule(delay or 0.5, self.Start, self.mod, self, ...)
|
|
end
|
|
timerPrototype.DelayedShow = timerPrototype.DelayedStart
|
|
|
|
function timerPrototype:Schedule(t, ...)
|
|
return DBMScheduler:Schedule(t, self.Start, self.mod, self, ...)
|
|
end
|
|
|
|
function timerPrototype:Unschedule(...)
|
|
return DBMScheduler: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
|
|
tremove(self.startedTimers, i)
|
|
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
|
|
--Mods that have specifically flagged that it's safe to assume all timers from that boss mod belong to boss1
|
|
--This check is performed secondary to args scan so that no adds guids are overwritten
|
|
if not guid and self.mod.sendMainBossGUID and not DBM.Options.DontSendBossGUIDs and (self.type == "cd" or self.type == "next" or self.type == "cdcount" or self.type == "nextcount" or self.type == "cdspecial" or self.type == "ai") then
|
|
guid = UnitGUID("boss1")
|
|
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
|
|
|
|
--HardStop is a method used when you want to force stop all varients of a timer by ID, period, but still pass a GUID for callbacks
|
|
--This is especially useful for count timers where guid is 2nd arg and count is 1st
|
|
--where Stop(guid) would mismatch object and not stop a bar and calling stop on every possible count is silly and stop without args wouldn't send GUID
|
|
function timerPrototype:HardStop(guid)
|
|
--Mods that have specifically flagged that it's safe to assume all timers from that boss mod belong to boss1
|
|
--This check is performed secondary to args scan so that no adds guids are overwritten
|
|
if not guid and self.mod.sendMainBossGUID and not DBM.Options.DontSendBossGUIDs and (self.type == "cd" or self.type == "next" or self.type == "cdcount" or self.type == "nextcount" or self.type == "cdspecial" or self.type == "ai") then
|
|
guid = UnitGUID("boss1")
|
|
end
|
|
for i = #self.startedTimers, 1, -1 do
|
|
fireEvent("DBM_TimerStop", self.startedTimers[i], guid)
|
|
DBT:CancelBar(self.startedTimers[i])
|
|
DBM:Unschedule(playCountSound, self.startedTimers[i])--Unschedule countdown by timerId
|
|
tremove(self.startedTimers, i)
|
|
end
|
|
end
|
|
|
|
--In past boss mods have always had to manually call Stop just to restart a timer, to avoid triggering false debug messages
|
|
--This function should simplify boss mod creation by allowing you to "Restart" a timer with one call in mod instead of 2
|
|
function timerPrototype:Restart(timer, ...)
|
|
if self.type and (self.type == "cdcount" or self.type == "nextcount") and not self.allowdouble then
|
|
self:Stop()--Cleanup any count timers left over on a restart
|
|
else
|
|
self:Stop(...)
|
|
end
|
|
self:Unschedule(...)--Also unschedules not yet started timers that used timer:Schedule()
|
|
self:Start(timer, ...)
|
|
end
|
|
timerPrototype.Reboot = timerPrototype.Restart
|
|
|
|
function timerPrototype:Cancel(...)
|
|
self:Stop(...)
|
|
self:Unschedule(...)--Also unschedules not yet started timers that used timer:Schedule()
|
|
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 and not self.mod.isTrashMod then return end
|
|
if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end
|
|
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
|
|
DBM:Unschedule(playCountSound, id)
|
|
local bar = DBT:GetBar(id)
|
|
if not bar then
|
|
bar = self:Start(totalTime, ...)
|
|
end
|
|
fireEvent("DBM_TimerUpdate", id, elapsed, totalTime)
|
|
if bar then -- still need to check as :Start() can return nil instead of actually starting the timer
|
|
local newRemaining = totalTime-elapsed
|
|
if not bar.keep and newRemaining > 0 then
|
|
--Correct table for tracked timer objects for adjusted time, or else timers may get stuck if stop is called on them
|
|
self.mod:Unschedule(removeEntry, self.startedTimers, id)
|
|
self.mod:Schedule(newRemaining, removeEntry, self.startedTimers, id)
|
|
end
|
|
if self.option then
|
|
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
|
|
if (type(countVoice) == "string" or countVoice > 0) then
|
|
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, bar.requiresCombat)--timerId, timer, voice, count
|
|
DBM:Debug("Updating a countdown after a timer Update call for timer ID:"..id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return DBT:UpdateBar(id, elapsed, totalTime)
|
|
end
|
|
|
|
function timerPrototype:AddTime(extendAmount, ...)
|
|
if DBM.Options.DontShowBossTimers and not self.mod.isTrashMod then return end
|
|
if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end
|
|
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
|
|
DBM:Unschedule(playCountSound, id)
|
|
local bar = DBT:GetBar(id)
|
|
if not bar then
|
|
return self:Start(extendAmount, ...)
|
|
else
|
|
local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
|
|
if elapsed and total then
|
|
local newRemaining = (total+extendAmount) - elapsed
|
|
if not bar.keep then
|
|
--Correct table for tracked timer objects for adjusted time, or else timers may get stuck if stop is called on them
|
|
self.mod:Unschedule(removeEntry, self.startedTimers, id)
|
|
self.mod:Schedule(newRemaining, removeEntry, self.startedTimers, id)
|
|
end
|
|
if self.option then
|
|
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
|
|
if (type(countVoice) == "string" or countVoice > 0) then
|
|
if not bar.fade then--Don't start countdown voice if it's faded bar
|
|
playCountdown(id, newRemaining, countVoice, bar.countdownMax, bar.requiresCombat)--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
|
|
|
|
function timerPrototype:RemoveTime(reduceAmount, ...)
|
|
if DBM.Options.DontShowBossTimers and not self.mod.isTrashMod then return end
|
|
if DBM.Options.DontShowTrashTimers and self.mod.isTrashMod then return end
|
|
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
|
|
DBM:Unschedule(playCountSound, id)--Needs to be unscheduled here, or countdown might not be canceled if removing time made it cease to have a > 0 value
|
|
local bar = DBT:GetBar(id)
|
|
if not bar then
|
|
return--Do nothing
|
|
else
|
|
if not bar.keep then
|
|
self.mod:Unschedule(removeEntry, self.startedTimers, id)--Needs to be unscheduled here, or the entry might just get left in table until original expire time, if new expire time is less than 0
|
|
end
|
|
local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
|
|
if elapsed and total then
|
|
local newRemaining = (total-reduceAmount) - elapsed
|
|
if newRemaining > 0 then
|
|
--Correct table for tracked timer objects for adjusted time, or else timers may get stuck if stop is called on them
|
|
if not bar.keep then
|
|
self.mod:Schedule(newRemaining, removeEntry, self.startedTimers, id)
|
|
end
|
|
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
|
|
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, bar.requiresCombat)--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
|
|
fireEvent("DBM_TimerStop", id)
|
|
removeEntry(self.startedTimers, id)
|
|
return DBT:CancelBar(id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function timerPrototype:Pause(...)
|
|
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
|
|
local bar = DBT:GetBar(id)
|
|
DBM:Unschedule(playCountSound, id)--Kill countdown on pause
|
|
if bar then
|
|
if not bar.keep then
|
|
self.mod:Unschedule(removeEntry, self.startedTimers, id)--Prevent removal from startedTimers table while bar is paused
|
|
end
|
|
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
|
|
local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
|
|
if elapsed and total then
|
|
local remaining = total - elapsed
|
|
if not bar.keep then
|
|
self.mod:Schedule(remaining, removeEntry, self.startedTimers, id)--Re-schedule the auto remove entry stuff
|
|
end
|
|
--Have to check if paused bar had a countdown on resume so we can restore it
|
|
if self.option and not bar.fade then
|
|
local countVoice = self.mod.Options[self.option .. "CVoice"] or 0
|
|
if (type(countVoice) == "string" or countVoice > 0) then
|
|
playCountdown(id, remaining, countVoice, bar.countdownMax, bar.requiresCombat)--timerId, timer, voice, count
|
|
DBM:Debug("Updating a countdown after a timer Resume call for timer ID:"..id)
|
|
end
|
|
end
|
|
end
|
|
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
|
|
icon = parseSpellIcon(icon)
|
|
return bar:SetIcon(icon)
|
|
end
|
|
end
|
|
|
|
--This function changes the spellname and callback key (but not option key) of timer object
|
|
--This is needed for faction bosses where we need to swap out a spell key/name on fly after a boss is engaged
|
|
function timerPrototype:UpdateKey(altSpellId, ...)
|
|
--Check if existing bar first,
|
|
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
|
|
local bar = DBT:GetBar(id)
|
|
self.spellId = altSpellId
|
|
self.icon = parseSpellIcon(altSpellId, self.type, self.icon)
|
|
self.name = nil--By wiping name, it becomes uncached and can get replaced by GetLocalizedTimerText in :Start
|
|
if bar then
|
|
--If a bar exists while updating key we"
|
|
--Get remainig, kill old timer, start new one with ID/name replacement applied
|
|
local remaining = bar.timer
|
|
self:Stop(...)
|
|
self:Unschedule(...)
|
|
DBM:Unschedule(playCountSound, id)
|
|
self:Start(remaining, ...)--Restart it
|
|
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, spellId, optionType, waCustomName)
|
|
if optionName ~= false then
|
|
self.option = optionName or self.id
|
|
self.mod:AddBoolOption(self.option, optionDefault, "timer", nil, colorType, countdown, spellId, optionType, waCustomName)
|
|
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, icon, optionDefault, optionName, colorType, inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType)
|
|
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
|
|
icon = parseSpellIcon(icon)
|
|
local waSpecialKey, simpType
|
|
if customType then
|
|
simpType = timerTypeSimplification[customType] or customType
|
|
waSpecialKey = waKeyOverrides[customType]
|
|
end
|
|
local obj = setmetatable(
|
|
{
|
|
text = self.localization.timers[name],
|
|
type = customType or "cd",--Auto assign
|
|
simpType = simpType or "cd",
|
|
waSpecialKey = waSpecialKey,
|
|
spellId = spellId,--Allows Localized timer text to still have a spellId arg weak auras can latch onto
|
|
timer = timer,
|
|
id = name,
|
|
icon = icon,
|
|
colorType = colorType,
|
|
inlineIcon = inlineIcon,
|
|
keep = keep,
|
|
countdown = countdown,
|
|
countdownMax = countdownMax,
|
|
r = r,
|
|
g = g,
|
|
b = b,
|
|
requiresCombat = requiresCombat,
|
|
startedTimers = {},
|
|
mod = self,
|
|
},
|
|
mt
|
|
)
|
|
obj:AddOption(optionDefault, optionName, colorType, countdown, spellId, nil, waCustomName)
|
|
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, requiresCombat)
|
|
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
|
|
spellName = parseSpellName(spellId, timerType)
|
|
local unparsedId = spellId
|
|
if timerType == "achievement" then
|
|
icon = parseSpellIcon(texture or spellId, timerType)
|
|
elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "stage" or timerType == "stagecount" or timerType == "stagecountcycle" or timerType == "stagecontext" or timerType == "stagecontextcount" or timerType == "intermission" or timerType == "intermissioncount" then
|
|
icon = parseSpellIcon(texture or spellId, timerType)
|
|
if timerType == "stage" or timerType == "stagecount" or timerType == "stagecountcycle" or timerType == "stagecontext" or timerType == "stagecontextcount" or timerType == "intermission" or timerType == "intermissioncount" then
|
|
colorType = 6
|
|
end
|
|
elseif timerType == "roleplay" then
|
|
icon = parseSpellIcon(texture or spellId, timerType, isRetail and 237538 or 136106)
|
|
colorType = 6
|
|
elseif timerType == "adds" or timerType == "addscustom" then
|
|
icon = parseSpellIcon(texture or spellId, timerType, 136116)
|
|
colorType = 1
|
|
else
|
|
icon = parseSpellIcon(texture or spellId, timerType)
|
|
end
|
|
local timerTextValue
|
|
if timerText then
|
|
--If timertext is a number, accept it as a secondary auto translate spellid
|
|
--First check if it's shorttext
|
|
if DBM.Options.ShortTimerText then
|
|
if 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
|
|
else--Short text is off, we want to be more aggressive in non setting short text if auto localize non short text available
|
|
if spellId and type(spellId) == "number" then
|
|
--Still use fully localized timer object text if there, cause that's not short text
|
|
timerTextValue = self.localization.timers[timerText]
|
|
else
|
|
--if spellId isn't valid, we need to accept timerText in any form as fallback
|
|
timerTextValue = self.localization.timers[timerText] or timerText
|
|
end
|
|
end
|
|
end
|
|
local id = "Timer"..(spellId or 0)..timerType..(optionVersion or "")
|
|
local simpType = timerTypeSimplification[timerType] or timerType
|
|
local waSpecialKey = waKeyOverrides[timerType]
|
|
local obj = setmetatable(
|
|
{
|
|
text = timerTextValue,
|
|
type = timerType,
|
|
simpType = simpType,
|
|
waSpecialKey = waSpecialKey,--Not same as simpType, this overrides option key
|
|
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,
|
|
requiresCombat = requiresCombat,
|
|
allowdouble = allowdouble,
|
|
startedTimers = {},
|
|
mod = self,
|
|
},
|
|
mt
|
|
)
|
|
obj:AddOption(optionDefault, optionName, colorType, countdown, spellId, timerType)
|
|
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 == "stagecount" or timerType == "stagecountcycle" or timerType == "intermission" or timerType == "intermissioncount" or timerType == "roleplay" then--Timers without spellid, generic (do not add stagecontext here, it has spellname parsing)
|
|
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
|
|
|
|
function bossModPrototype:NewTargetCountTimer(...)
|
|
return newTimer(self, "targetcount", ...)
|
|
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(10059)) / 10000 -- 10059 = Stormwind 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(10059)) / 10000 -- 10059 = Stormwind 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(10059)) / 10000 -- 10059 = Stormwind 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:NewStageTimer(...)
|
|
return newTimer(self, "stage", ...)
|
|
end
|
|
bossModPrototype.NewPhaseTimer = bossModPrototype.NewStageTimer--Deprecated naming, once all mods are converted over, NewPhaseTimer will be wiped out for NewStageTimer
|
|
|
|
function bossModPrototype:NewStageCountTimer(...)
|
|
return newTimer(self, "stagecount", ...)
|
|
end
|
|
|
|
--Used mainly for compat with BW/LW timers where they use "stages" but then use the spell/journal descriptor instead of "stage d"
|
|
--Basically, it's a generic spellName timer for "stages" callback
|
|
function bossModPrototype:NewStageContextTimer(...)
|
|
return newTimer(self, "stagecontext", ...)
|
|
end
|
|
|
|
--Same as above, with count
|
|
function bossModPrototype:NewStageContextCountTimer(...)
|
|
return newTimer(self, "stagecontextcount", ...)
|
|
end
|
|
|
|
function bossModPrototype:NewStageCountCycleTimer(...)
|
|
return newTimer(self, "stagecountcycle", ...)
|
|
end
|
|
|
|
function bossModPrototype:NewIntermissionTimer(...)
|
|
return newTimer(self, "intermission", ...)
|
|
end
|
|
|
|
function bossModPrototype:NewIntermissionCountTimer(...)
|
|
return newTimer(self, "intermissioncount", ...)
|
|
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:NewCDNPTimer(...)
|
|
return newTimer(self, "cdnp", ...)
|
|
end
|
|
|
|
function bossModPrototype:NewNextNPTimer(...)
|
|
return newTimer(self, "nextnp", ...)
|
|
end
|
|
|
|
function bossModPrototype:NewCDCountNPTimer(...)
|
|
return newTimer(self, "cdcountnp", ...)
|
|
end
|
|
|
|
function bossModPrototype:NewNextCountNPTimer(...)
|
|
return newTimer(self, "nextcountnp", ...)
|
|
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")
|
|
spellName = parseSpellName(spellId, timerType)
|
|
--Name wasn't provided, but we succeeded in getting a 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
|
|
return pformat(L.AUTO_TIMER_TEXTS[timerType], spellName)
|
|
end
|
|
end
|
|
|
|
------------------------------
|
|
-- Berserk/Combat Objects --
|
|
------------------------------
|
|
do
|
|
local enragePrototype = {}
|
|
local mt = {__index = enragePrototype}
|
|
|
|
function enragePrototype:Start(timer)
|
|
--User only has timer object exposed in mod options, check that here to also prevent the warnings.
|
|
if not self.owner.Options.timer_berserk then return end
|
|
timer = timer or self.timer or 600
|
|
timer = timer <= 0 and self.timer - mabs(timer) or timer
|
|
self.bar:SetTimer(timer)
|
|
self.bar:Start()
|
|
if not DBM.Options.ShowBerserkWarnings then return end
|
|
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, nil, false)
|
|
local warning2 = self:NewAnnounce(text or L.GENERIC_WARNING_BERSERK, 4, nil, nil, false)
|
|
--timer, name, icon, optionDefault, optionName, colorType, inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType
|
|
local bar = self:NewTimer(timer, barText or L.GENERIC_TIMER_BERSERK, barIcon or 28131, nil, "timer_berserk", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "berserk")
|
|
local obj = setmetatable(
|
|
{
|
|
warning1 = warning1,
|
|
warning2 = warning2,
|
|
bar = bar,
|
|
timer = timer,
|
|
owner = self
|
|
},
|
|
mt
|
|
)
|
|
return obj
|
|
end
|
|
function bossModPrototype:NewCombatTimer(timer, _, barText, barIcon) -- timer, text, barText, barIcon
|
|
timer = timer or 10
|
|
--timer, name, icon, optionDefault, optionName, colorType, inlineIcon, keep, countdown, countdownMax, r, g, b, spellId, requiresCombat, waCustomName, customType
|
|
local bar = self:NewTimer(timer, barText or L.GENERIC_TIMER_COMBAT, barIcon or "132349", nil, "timer_combat", nil, nil, nil, 1, 5, nil, nil, nil, nil, nil, nil, "combat")
|
|
|
|
local obj = setmetatable(
|
|
{
|
|
bar = bar,
|
|
timer = timer,
|
|
owner = self
|
|
},
|
|
mt
|
|
)
|
|
return obj
|
|
end
|
|
end
|
|
|
|
---------------
|
|
-- Options --
|
|
---------------
|
|
function bossModPrototype:AddBoolOption(name, default, cat, func, extraOption, extraOptionTwo, spellId, optionType, waCustomName)
|
|
if checkDuplicateObjects[name] and name ~= "timer_berserk" then
|
|
DBM:Debug("|cffff0000Option already exists for: |r"..name)
|
|
else
|
|
checkDuplicateObjects[name] = true
|
|
end
|
|
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
|
|
if spellId then
|
|
if waCustomName then--Do custom shit for options using invalid spellIds as weak auras keys
|
|
self:GroupWASpells(waCustomName, spellId, name)
|
|
else
|
|
if optionType and optionType == "achievement" then
|
|
spellId = "at"..spellId--"at" for achievement timer
|
|
end
|
|
local optionTypeMatch = optionType or ""
|
|
if not optionTypeMatch:find("stage") then
|
|
self:GroupSpells(spellId, name)
|
|
end
|
|
end
|
|
end
|
|
self:SetOptionCategory(name, cat, optionType, waCustomName)
|
|
if func then
|
|
self.optionFuncs = self.optionFuncs or {}
|
|
self.optionFuncs[name] = func
|
|
end
|
|
end
|
|
|
|
function bossModPrototype:AddSpecialWarningOption(name, default, defaultSound, cat, spellId, optionType, waCustomName)
|
|
if checkDuplicateObjects[name] then
|
|
DBM:Debug("|cffff0000Option already exists for: |r"..name)
|
|
else
|
|
checkDuplicateObjects[name] = true
|
|
end
|
|
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
|
|
if spellId then
|
|
if waCustomName then--Do custom shit for options using invalid spellIds as weak auras keys
|
|
self:GroupWASpells(waCustomName, spellId, name)
|
|
else
|
|
self:GroupSpells(spellId, name)
|
|
end
|
|
end
|
|
self:SetOptionCategory(name, cat, optionType, waCustomName)
|
|
end
|
|
|
|
--auraspellId must match debuff ID so EnablePrivateAuraSound function can call right option key and right debuff ID
|
|
--groupSpellId is used if a diff option key is used in all other options with spell (will be quite common)
|
|
--defaultSound is used to set default Special announce sound (1-4) just like regular special announce objects
|
|
function bossModPrototype:AddPrivateAuraSoundOption(auraspellId, default, groupSpellId, defaultSound)
|
|
self.DefaultOptions["PrivateAuraSound"..auraspellId] = (default == nil) or default
|
|
self.DefaultOptions["PrivateAuraSound"..auraspellId.."SWSound"] = defaultSound or 1
|
|
if default and type(default) == "string" then
|
|
default = self:GetRoleFlagValue(default)
|
|
end
|
|
self.Options["PrivateAuraSound"..auraspellId] = (default == nil) or default
|
|
self.Options["PrivateAuraSound"..auraspellId.."SWSound"] = defaultSound or 1
|
|
self.localization.options["PrivateAuraSound"..auraspellId] = L.AUTO_PRIVATEAURA_OPTION_TEXT:format(auraspellId)
|
|
if not DBM.Options.GroupOptionsExcludePAura then
|
|
self:GroupSpells(groupSpellId or auraspellId, "PrivateAuraSound"..auraspellId)
|
|
end
|
|
self:SetOptionCategory("PrivateAuraSound"..auraspellId, "paura")
|
|
end
|
|
|
|
--Function to actually register specific media to specific auras
|
|
--auraspellId: Private aura spellId
|
|
--voice: voice pack media path
|
|
--voiceVersion: Required voice pack verion (if not met, falls back to airhorn
|
|
function bossModPrototype:EnablePrivateAuraSound(auraspellId, voice, voiceVersion, altOptionId)
|
|
if DBM.Options.DontPlayPrivateAuraSound then return end
|
|
local optionId = altOptionId or auraspellId
|
|
if self.Options["PrivateAuraSound"..optionId] then
|
|
if not self.paSounds then self.paSounds = {} end
|
|
local soundId = self.Options["PrivateAuraSound"..optionId.."SWSound"] or DBM.Options.SpecialWarningSound--Shouldn't be nil value, but just in case options fail to load, fallback to default SW1 sound
|
|
local mediaPath
|
|
--Check valid voice pack sound
|
|
local chosenVoice = DBM.Options.ChosenVoicePack2
|
|
if chosenVoice ~= "None" and not voiceSessionDisabled and voiceVersion <= SWFilterDisabled then
|
|
local isVoicePackUsed
|
|
--Vet if user has voice pack enabled by sound ID
|
|
if type(soundId) == "number" and soundId < 5 then--Value 1-4 are SW1 defaults, otherwise it's file data ID and handled by Custom
|
|
isVoicePackUsed = DBM.Options["VPReplacesSA"..soundId]
|
|
else
|
|
isVoicePackUsed = DBM.Options.VPReplacesCustom
|
|
end
|
|
if isVoicePackUsed then
|
|
mediaPath = "Interface\\AddOns\\DBM-VP"..chosenVoice.."\\"..voice..".ogg"
|
|
else
|
|
mediaPath = type(soundId) == "number" and DBM.Options["SpecialWarningSound" .. (soundId == 1 and "" or soundId)] or soundId
|
|
end
|
|
else
|
|
mediaPath = type(soundId) == "number" and DBM.Options["SpecialWarningSound" .. (soundId == 1 and "" or soundId)] or soundId
|
|
end
|
|
--Absolute media path is still a number, so at this point we know it's file data Id, we need to set soundFileID
|
|
if type(mediaPath) == "number" then
|
|
self.paSounds[#self.paSounds + 1] = C_UnitAuras.AddPrivateAuraAppliedSound({
|
|
spellID = auraspellId,
|
|
unitToken = "player",
|
|
soundFileID = mediaPath,
|
|
outputChannel = "master",
|
|
})
|
|
else--It's a string, so it's not an ID, we need to set soundFileName instead
|
|
self.paSounds[#self.paSounds + 1] = C_UnitAuras.AddPrivateAuraAppliedSound({
|
|
spellID = auraspellId,
|
|
unitToken = "player",
|
|
soundFileName = mediaPath,
|
|
outputChannel = "master",
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
function bossModPrototype:DisablePrivateAuraSounds()
|
|
if DBM.Options.DontPlayPrivateAuraSound then return end
|
|
for _, id in next, self.paSounds do
|
|
C_UnitAuras.RemovePrivateAuraAppliedSound(id)
|
|
end
|
|
self.paSounds = nil
|
|
end
|
|
|
|
--Extended Icon Usage Notes
|
|
--Any time extended icons is used, option must be OFF by default
|
|
--Option must be hidden from GUI if extended icoins not enabled
|
|
--If extended icons are disabled, then on mod load, users option is reset to default (off) to prevent their mod from still executing SetIcon functions (this is because even if it's hidden from GUI, if option was created and enabled, it'd still run)
|
|
function bossModPrototype:AddSetIconOption(name, spellId, default, iconType, iconsUsed, conflictWarning, groupSpellId)
|
|
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
|
|
if (groupSpellId or spellId) and not DBM.Options.GroupOptionsExcludeIcon then
|
|
self:GroupSpells(groupSpellId or spellId, name)
|
|
end
|
|
self:SetOptionCategory(name, "icon")
|
|
--Legacy bool and nil support
|
|
if type(iconType) ~= "number" then
|
|
if iconType then--true
|
|
iconType = 5
|
|
else --false/nil
|
|
iconType = 0
|
|
end
|
|
end
|
|
if iconType == 1 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_MELEE_A:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 2 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_MELEE_R:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 3 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_RANGED_A:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 4 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_RANGED_R:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 5 then
|
|
--NPC/Mob setting uses icon elect feature and needs to establish latency check table
|
|
if not self.findFastestComputer then
|
|
self.findFastestComputer = {}
|
|
end
|
|
self.findFastestComputer[#self.findFastestComputer + 1] = name
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_NPCS:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 6 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_ALPHA:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 7 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_ROSTER:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 8 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_TANK_A:format(spellId) or self.localization.options[name]
|
|
elseif iconType == 9 then
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS_TANK_R:format(spellId) or self.localization.options[name]
|
|
else--Type 0 (Generic for targets)
|
|
self.localization.options[name] = spellId and L.AUTO_ICONS_OPTION_TARGETS:format(spellId) or self.localization.options[name]
|
|
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"
|
|
-- elseif iconsUsed[i] == 9 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:0:16:32:48|t"
|
|
-- elseif iconsUsed[i] == 10 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:16:32:32:48|t"
|
|
-- elseif iconsUsed[i] == 11 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:32:48:32:48|t"
|
|
-- elseif iconsUsed[i] == 12 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:48:64:32:48|t"
|
|
-- elseif iconsUsed[i] == 13 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:0:16:48:64|t"
|
|
-- elseif iconsUsed[i] == 14 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:16:32:48:64|t"
|
|
-- elseif iconsUsed[i] == 15 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:32:48:48:64|t"
|
|
-- elseif iconsUsed[i] == 16 then self.localization.options[name] = self.localization.options[name].."|TInterface\\TargetingFrame\\UI-RaidTargetingIcons.blp:13:13:0:0:64:64:48:64:48:64|t"
|
|
end
|
|
end
|
|
self.localization.options[name] = self.localization.options[name]..")"
|
|
if conflictWarning then
|
|
self.localization.options[name] = self.localization.options[name] .. L.AUTO_ICONS_OPTION_CONFLICT
|
|
end
|
|
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:GroupSpells(spellId, name)
|
|
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
|
|
if spellId then
|
|
self:GroupSpells(spellId, "RangeFrame")
|
|
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
|
|
self:SetOptionCategory("RangeFrame", "misc")
|
|
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
|
|
if spellId then
|
|
self:GroupSpells(spellId, name)
|
|
self.localization.options[name] = L.AUTO_HUD_OPTION_TEXT:format(spellId)
|
|
else
|
|
self.localization.options[name] = L.AUTO_HUD_OPTION_TEXT_MULTI
|
|
end
|
|
self:SetOptionCategory(name, "misc")
|
|
end
|
|
|
|
function bossModPrototype:AddNamePlateOption(name, spellId, default, forceDBM)
|
|
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:GroupSpells(spellId, name)
|
|
self:SetOptionCategory(name, "nameplate")
|
|
self.localization.options[name] = forceDBM and L.AUTO_NAMEPLATE_OPTION_TEXT_FORCED:format(spellId) or L.AUTO_NAMEPLATE_OPTION_TEXT:format(spellId)
|
|
end
|
|
|
|
function bossModPrototype:AddInfoFrameOption(spellId, default, optionVersion, optionalThreshold)
|
|
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
|
|
if spellId then
|
|
self:GroupSpells(spellId, "InfoFrame" .. oVersion)
|
|
if optionalThreshold then
|
|
self.localization.options["InfoFrame"..oVersion] = L.AUTO_INFO_FRAME_OPTION_TEXT3:format(spellId, optionalThreshold)
|
|
else
|
|
self.localization.options["InfoFrame"..oVersion] = L.AUTO_INFO_FRAME_OPTION_TEXT:format(spellId)
|
|
end
|
|
else
|
|
self.localization.options["InfoFrame"..oVersion] = L.AUTO_INFO_FRAME_OPTION_TEXT2
|
|
end
|
|
self:SetOptionCategory("InfoFrame"..oVersion, "misc")
|
|
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
|
|
|
|
-- 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, spellId)
|
|
cat = cat or "misc"
|
|
self.DefaultOptions[name] = {type = "dropdown", value = default}
|
|
self.Options[name] = default
|
|
if spellId then
|
|
self:GroupSpells(spellId, name)
|
|
end
|
|
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
|
|
|
|
do
|
|
local lineCount = 1
|
|
|
|
function bossModPrototype:AddOptionLine(text, cat, forceIgnore)
|
|
if self.addon and not forceIgnore then
|
|
self.groupOptions["line" .. lineCount] = text
|
|
lineCount = lineCount + 1
|
|
else
|
|
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
|
|
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:AddNamePlateLine(text)
|
|
return self:AddOptionLine(text, "nameplate")
|
|
end
|
|
|
|
function bossModPrototype:AddIconLine(text)
|
|
return self:AddOptionLine(text, "icon")
|
|
end
|
|
|
|
function bossModPrototype:AddMiscLine(text)
|
|
return self:AddOptionLine(text, "misc", true)
|
|
end
|
|
|
|
function bossModPrototype:RemoveOption(name)
|
|
self.Options[name] = nil
|
|
for k, options in pairs(self.optionCategories) do
|
|
removeEntry(options, name)
|
|
if #options == 0 then
|
|
self.optionCategories[k] = nil
|
|
end
|
|
end
|
|
if self.optionFuncs then
|
|
self.optionFuncs[name] = nil
|
|
end
|
|
end
|
|
|
|
--This function, which will be called after all iterations of GroupWASpells/GroupSpells will just straight up say "ok now ignore keys these made and just use custom ones" for extremely niche cases
|
|
function bossModPrototype:JustSetCustomKeys(catSpell, customKeys)
|
|
catSpell = tostring(catSpell)
|
|
if not self.groupSpells[catSpell] then
|
|
self.groupSpells[catSpell] = {}
|
|
end
|
|
if not self.groupOptions[catSpell] then
|
|
self.groupOptions[catSpell] = {}
|
|
end
|
|
self.groupOptions[catSpell].customKeys = customKeys
|
|
end
|
|
|
|
--Custom function for handling group spells where we want to group by ID, but not use that IDs name (basically a fake Id for purpose of a unified WA key)
|
|
--This lets us group options up that aren't using valid IDs, and show the ID it is using for WA in the gui next to custom name
|
|
function bossModPrototype:GroupWASpells(customName, ...)
|
|
local spells = {...}
|
|
local catSpell = tostring(tremove(spells, 1))
|
|
if not self.groupSpells[catSpell] then
|
|
self.groupSpells[catSpell] = {}
|
|
end
|
|
for _, spell in ipairs(spells) do
|
|
local sSpell = tostring(spell)
|
|
self.groupSpells[sSpell] = catSpell
|
|
if sSpell ~= catSpell and self.groupOptions[sSpell] then
|
|
if not self.groupOptions[catSpell] then
|
|
self.groupOptions[catSpell] = {}
|
|
self.groupOptions[catSpell].title = customName
|
|
end
|
|
for _, spell2 in ipairs(self.groupOptions[sSpell]) do
|
|
tinsert(self.groupOptions[catSpell], spell2)
|
|
end
|
|
self.groupOptions[sSpell] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
function bossModPrototype:GroupSpells(...)
|
|
local spells = {...}
|
|
local catSpell = tostring(tremove(spells, 1))
|
|
if not self.groupSpells[catSpell] then
|
|
self.groupSpells[catSpell] = {}
|
|
end
|
|
for _, spell in ipairs(spells) do
|
|
local sSpell = tostring(spell)
|
|
self.groupSpells[sSpell] = catSpell
|
|
if sSpell ~= catSpell and self.groupOptions[sSpell] then
|
|
if not self.groupOptions[catSpell] then
|
|
self.groupOptions[catSpell] = {}
|
|
end
|
|
for _, spell2 in ipairs(self.groupOptions[sSpell]) do
|
|
tinsert(self.groupOptions[catSpell], spell2)
|
|
end
|
|
self.groupOptions[sSpell] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
function bossModPrototype:SetOptionCategory(name, cat, optionType, waCustomName)
|
|
optionType = optionType or ""
|
|
for _, options in pairs(self.optionCategories) do
|
|
removeEntry(options, name)
|
|
end
|
|
if self.addon and self.groupSpells[name] and not (optionType == "gtfo" or optionType == "adds" or optionType == "addscount" or optionType == "addscustom" or optionType:find("stage") or cat == "icon" and DBM.Options.GroupOptionsExcludeIcon or cat == "paura" and DBM.Options.GroupOptionsExcludePAura) then
|
|
local sSpell = self.groupSpells[name]
|
|
if not self.groupOptions[sSpell] then
|
|
self.groupOptions[sSpell] = {}
|
|
end
|
|
if waCustomName and not self.groupOptions[sSpell].title then
|
|
self.groupOptions[sSpell].title = waCustomName
|
|
end
|
|
tinsert(self.groupOptions[sSpell], name)
|
|
else
|
|
if not self.optionCategories[cat] then
|
|
self.optionCategories[cat] = {}
|
|
end
|
|
tinsert(self.optionCategories[cat], name)
|
|
tinsert(self.categorySort, cat)
|
|
end
|
|
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.noMultiBoss then
|
|
info.noMultiBoss = self.noMultiBoss
|
|
end
|
|
if self.WBEsync then
|
|
info.WBEsync = self.WBEsync
|
|
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
|
|
if not self.multiIDSingleBoss then
|
|
self.numBoss = #self.multiMobPullDetection
|
|
if self.inCombat then
|
|
--Called mid combat, fix some variables
|
|
self.vb.bossLeft = self.numBoss
|
|
end
|
|
else
|
|
self.numBoss = 1
|
|
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:DisableMultiBossPulls()
|
|
self.noMultiBoss = true
|
|
if self.combatInfo then
|
|
self.combatInfo.noMultiBoss = true
|
|
end
|
|
end
|
|
|
|
function bossModPrototype:EnableWBEngageSync()
|
|
self.WBEsync = true
|
|
if self.combatInfo then
|
|
self.combatInfo.WBEsync = true
|
|
end
|
|
end
|
|
|
|
--used for knowing if a specific mod is engaged
|
|
function bossModPrototype:IsInCombat()
|
|
return self.inCombat
|
|
end
|
|
|
|
--Used for knowing if any person is in any kind of combat period
|
|
function bossModPrototype:GroupInCombat()
|
|
local combatFound = false
|
|
--Any Boss engaged
|
|
if IsEncounterInProgress() then
|
|
combatFound = true
|
|
end
|
|
--Self in Combat
|
|
if InCombatLockdown() or UnitAffectingCombat("player") then
|
|
combatFound = true
|
|
end
|
|
--Any Other group member in combat
|
|
if not combatFound then
|
|
for uId in DBM:GetGroupMembers() do
|
|
if UnitAffectingCombat(uId) and not UnitIsDeadOrGhost(uId) then
|
|
combatFound = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
return combatFound
|
|
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 private.modSyncSpam[spamId] or (time - private.modSyncSpam[spamId]) > 8 then
|
|
self:ReceiveSync(event, playerName, self.revision or 0, tostringall(...))
|
|
sendSync(DBMSyncProtocol, "M", str)
|
|
end
|
|
end
|
|
|
|
function bossModPrototype:SendBigWigsSync(msg, extra)
|
|
if not dbmIsEnabled then return end
|
|
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 private.modSyncSpam[spamId] or (time - private.modSyncSpam[spamId]) > self.SyncThreshold) and self.OnSync and (not self.minSyncRevision or revision >= self.minSyncRevision) then
|
|
private.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 type(revision) == "string" 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 DBMScheduler:Schedule(t, f, self, ...)
|
|
end
|
|
|
|
function bossModPrototype:Unschedule(f, ...)
|
|
return DBMScheduler: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
|
|
function DBM:ElectIconSetter(mod)
|
|
--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(DBMSyncProtocol, "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
|
|
private.canSetIcons[option] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local iconsModule = private:GetModule("Icons")
|
|
|
|
function bossModPrototype:SetIcon(...)
|
|
return iconsModule:SetIcon(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:SetIconByTable(...)
|
|
return iconsModule:SetIconByTable(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:SetUnsortedIcon(...)
|
|
return iconsModule:SetUnsortedIcon(self, ...)
|
|
end
|
|
|
|
--Backwards compat for old mods using this method, which is now merged into SetSortedIcon
|
|
function bossModPrototype:SetAlphaIcon(delay, target, maxIcon, returnFunc, scanId, ...)
|
|
return iconsModule:SetSortedIcon(self, "alpha", delay, target, 1, maxIcon, false, returnFunc, scanId, ...)
|
|
end
|
|
|
|
function bossModPrototype:SetIconBySortedTable(...)
|
|
return iconsModule:SetIconBySortedTable(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:SetSortedIcon(...)
|
|
return iconsModule:SetSortedIcon(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:ScanForMobs(...)
|
|
return iconsModule:ScanForMobs(self, ...)
|
|
end
|
|
|
|
function bossModPrototype:RemoveIcon(...)
|
|
return iconsModule:RemoveIcon(self, ...)
|
|
end
|
|
|
|
bossModPrototype.GetIcon = iconsModule.GetIcon
|
|
bossModPrototype.ClearIcons = iconsModule.ClearIcons
|
|
bossModPrototype.CanSetIcon = iconsModule.CanSetIcon
|
|
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
|
|
|
|
function bossModPrototype:GetLocalizedStrings()
|
|
self.localization.miscStrings.name = self.localization.general.name
|
|
return self.localization.miscStrings
|
|
end
|
|
|