-- ********************************************************* -- ** 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 -- ---@class DBMCoreNamespace local private = select(2, ...) --WARNING: DBM is dangerously close too 200 local variables, avoid adding locals to the file scope. --More modulation or scoping is needed to reduce this local DBMPrefix = "D5" local DBMSyncProtocol = 1 private.DBMPrefix = DBMPrefix private.DBMSyncProtocol = DBMSyncProtocol local L = DBM_CORE_L local CL = DBM_COMMON_L local stringUtils = private:GetPrototype("StringUtils") local tableUtils = private:GetPrototype("TableUtils") local difficulties = private:GetPrototype("Difficulties") local test = private:GetPrototype("DBMTest") ------------------------------- -- Globals/Default Options -- ------------------------------- local function releaseDate(year, month, day, hour, minute, second) hour = hour or 0 minute = minute or 0 second = second or 0 ---@format disable 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 ---@class DBM local DBM = private:GetPrototype("DBM") _G.DBM = DBM DBM.Revision = parseCurseDate("20240723001246") local fakeBWVersion, fakeBWHash = 341, "51c5bf8"--341.1 local bwVersionResponseString = "V^%d^%s" local PForceDisable -- The string that is shown as version DBM.DisplayVersion = "11.0.0"--Core version DBM.classicSubVersion = 0 DBM.ReleaseRevision = releaseDate(2024, 7, 22) -- the date of the latest stable version that is available, optionally pass hours, minutes, and seconds for multiple releases in one day PForceDisable = (private.isWrath or private.isClassic) and 13 or 12--When this is incremented, trigger force disable regardless of major patch 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() local wowVersionString, wowBuild, _, wowTOC = GetBuildInfo() return wowTOC, private.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 private.isRetail and playerClass == "HUNTER") then DBM_UseDualProfile = false end DBM_CharSavedRevision = 2 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 = private.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 private.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, FilterCrowdControl = 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 = private.isHardcoreServer and true or false, AutoReplySound = true, HideObjectivesFrame = true, HideGarrisonToasts = true, HideGuildChallengeUpdates = true, HideTooltips = false, DisableSFX = false, DisableAmbiance = false, DisableMusic = false, EnableModels = true, GUIWidth = 800, GUIHeight = 600, GroupOptionsExcludeIcon = false, AutoExpandSpellGroups = not private.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, NPIconSpacing = 0, NPIconGrowthDirection = "CENTER", NPIconAnchorPoint = "TOP", NPIconTimerEnabled = true, NPIconTimerFont = "standardFont", NPIconTimerFontStyle = "None", NPIconTimerFontSize = 18, NPIconTextEnabled = true, NPIconTextFont = "standardFont", NPIconTextFontStyle = "None", NPIconTextFontSize = 10, NPIconTextMaxLen = 7, DontPlayCountdowns = false, DontSendYells = false, BlockNoteShare = false, DontAutoGossip = false, DontShowPT2 = false, DontPlayPTCountdown = false, DontShowPTText = false, DontShowPTNoID = false, PTCountThreshold2 = 5, LatencyThreshold = 250, oRA3AnnounceConsumables = false, SettingsMessageShown = false, NewsMessageShown2 = 2,--Apparently variable 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 = {}, HideMovieDuringFight = true, HideMovieInstanceAnywhere = true, HideMovieNonInstanceAnywhere = false, HideMovieOnlyAfterSeen = true, LastRevision = 0, DebugMode = false, DebugLevel = 1, DebugSound = true, RoleSpecAlert = true, CheckGear = true, WorldBossAlert = not private.isRetail, WorldBuffAlert = not private.isRetail, BadTimerAlert = false, AutoAcceptFriendInvite = false, AutoAcceptGuildInvite = false, FakeBWVersion = false, AITimer = true, ShortTimerText = true, ChatFrame = "DEFAULT_CHAT_FRAME", CoreSavedRevision = 1, SilentMode = false, } ---@type DBMMod[] 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 -- -------------- ---@class DBMMod local bossModPrototype = private:GetPrototype("DBMMod") local mainFrame = CreateFrame("Frame", "DBMMainFrame") local playerName = UnitName("player") or error("failed to get player name") private.playerLevel = UnitLevel("player") local playerRealm = GetRealmName() local normalizedPlayerRealm = playerRealm:gsub("[%s-]+", "") local lastCombatStarted = GetTime() local chatPrefixShort = "<" .. L.DBM .. "> " local usedProfile = "Default" local dbmIsEnabled = true -- Table variables local newerVersionPerson, newersubVersionPerson, forceDisablePerson, cSyncSender, eeSyncSender, iconSetRevision, iconSetPerson, loadcIds, oocBWComms, bossIds, raid, autoRespondSpam, queuedBattlefield, bossHealth, bossHealthuIdCache, lastBossEngage, lastBossDefeat = {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} local inCombat = {} ---@type DBMMod[] local combatInfo = {} ---@type table -- False variables local targetEventsRegistered, combatInitialized, healthCombatInitialized, watchFrameRestore, questieWatchRestore, bossuIdFound, timerRequestInProgress = false, false, false, false, false, false, false -- Nil variables local currentSpecID, currentSpecName, currentSpecGroup, loadOptions, checkWipe, checkBossHealth, checkCustomBossHealth, fireEvent, LastInstanceType, breakTimerStart, AddMsg, delayedFunction, handleSync, lastGroupLeader -- 0 variables local eeSyncReceived, cSyncReceived, showConstantReminder, updateNotificationDisplayed, updateSubNotificationDisplayed = 0, 0, 0, 0, 0 local LastInstanceMapID = -1 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-Aberrus",--Combined into DBM-Raids-Dragonflight "DBM-DMF",--Combined into DBM-WorldEvents } ----------------- -- Libraries -- ----------------- local LibSpec do if (private.isRetail or private.isCata) 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 ---@diagnostic disable-next-line: undefined-field LibSpec:Register(DBM, update) end end end -------------------------------------------------------- -- Cache frequently used global variables in locals -- -------------------------------------------------------- -- these global functions are accessed all the time by the event handler -- so caching them is worth the effort local ipairs, pairs, next = ipairs, pairs, next local tonumber, tostring = tonumber, tostring local tinsert, tremove, twipe, tsort, tconcat = table.insert, table.remove, table.wipe, table.sort, table.concat local type, select = type, select local GetTime = GetTime local bband = bit.band local floor, mhuge, mmin, mmax, mrandom = math.floor, math.huge, math.min, math.max, math.random local GetNumGroupMembers, GetRaidRosterInfo = GetNumGroupMembers, GetRaidRosterInfo local UnitName, GetUnitName = UnitName, GetUnitName local IsInRaid, IsInGroup, IsInInstance = IsInRaid, IsInGroup, IsInInstance local UnitAffectingCombat, InCombatLockdown, IsFalling, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty = UnitAffectingCombat, InCombatLockdown, IsFalling, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty local UnitGUID, UnitHealth, UnitHealthMax = UnitGUID, UnitHealth, UnitHealthMax local UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit = UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit --local UnitTokenFromGUID, UnitPercentHealthFromGUID = UnitTokenFromGUID, UnitPercentHealthFromGUID local GetDungeonInfo = C_LFGInfo.GetDungeonInfo or GetDungeonInfo -- Classic has C_LFGInfo but not C_LFGInfo.GetDungeonInfo, need to use global for classic local EJ_GetSectionInfo, GetSectionIconFlags if C_EncounterJournal then EJ_GetSectionInfo, GetSectionIconFlags = C_EncounterJournal.GetSectionInfo, C_EncounterJournal.GetSectionIconFlags end 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 pformat = stringUtils.pformat local SendAddonMessage = C_ChatInfo.SendAddonMessage -- Store globals that can be hooked/overriden by tests in private private.GetInstanceInfo = GetInstanceInfo private.IsEncounterInProgress = IsEncounterInProgress local RAID_CLASS_COLORS = _G["CUSTOM_CLASS_COLORS"] or RAID_CLASS_COLORS-- for Phanx' Class Colors -- Polyfill for C_AddOns, Cata, Era and Retail have the fully featured table, Wrath has only Metadata (as of Jun 6th 2024) local C_AddOns do local cachedAddOns = nil C_AddOns = { GetAddOnMetadata = _G.C_AddOns.GetAddOnMetadata, GetNumAddOns = _G.C_AddOns.GetNumAddOns or GetNumAddOns, ---@diagnostic disable-line:deprecated GetAddOnInfo = _G.C_AddOns.GetAddOnInfo or GetAddOnInfo, ---@diagnostic disable-line:deprecated LoadAddOn = _G.C_AddOns.LoadAddOn or LoadAddOn, ---@diagnostic disable-line:deprecated IsAddOnLoaded = _G.C_AddOns.IsAddOnLoaded or IsAddOnLoaded, ---@diagnostic disable-line:deprecated EnableAddOn = _G.C_AddOns.EnableAddOn or EnableAddOn, ---@diagnostic disable-line:deprecated GetAddOnEnableState = _G.C_AddOns.GetAddOnEnableState or function(addon, character) return GetAddOnEnableState(character, addon) ---@diagnostic disable-line:deprecated end, DoesAddOnExist = _G.C_AddOns.DoesAddOnExist or function(addon) if not cachedAddOns then cachedAddOns = {} for i = 1, GetNumAddOns() do ---@diagnostic disable-line:deprecated cachedAddOns[GetAddOnInfo(i)] = true ---@diagnostic disable-line:deprecated end end return cachedAddOns[addon] end, } end -- this is not technically a lib and instead a standalone addon but the api is available via LibStub local CustomNames = C_AddOns.IsAddOnLoaded("CustomNames") and LibStub and LibStub("CustomNames") --------------------------------- -- General (local) functions -- --------------------------------- local checkEntry, removeEntry = tableUtils.checkEntry, tableUtils.removeEntry ---Whisper/Whisper Sync filter function ---@param sender any string for non realId and number for realId. Pass to true for realID ---@param checkFriends boolean? checks sender against friends list ---@param checkGuild boolean? checks sender against guild roster ---@param filterRaid boolean? checks sender against group members of your raid ---@param isRealIdMessage boolean? set true if this is a RealID whisper/comm ---@return boolean 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 --TODO, test UnitIsInMyGuild in both classics, and retail, especially for cross faction guild members. That can save a lot of cpu by removing iterating over literally entire guild roster 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) ---@param protocol number ---@param prefix string ---@param msg any 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 local sendChannel = "SOLO" if not IsTrialAccount() then 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) sendChannel = "INSTANCE_CHAT" else if IsInRaid() then sendChannel = "RAID" elseif IsInGroup(1) then sendChannel = "PARTY" end end end if sendChannel == "SOLO" then handleSync("SOLO", playerName, nil, (protocol or DBMSyncProtocol), prefix, strsplit("\t", msg)) else --Per https://warcraft.wiki.gg/wiki/Patch_10.2.7/API_changes#Addon_messaging_changes --We want to start watching for situations DBM exceeds it's 10 messages per 10 seconds limits --While at it, catch other failure types too local result = select(-1, SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, sendChannel)) if type(result) == "number" and result ~= 0 then DBM:Debug("|cffff0000sendSync failed with a result of " ..result.. " for prefix |r" .. prefix) end end end end private.sendSync = sendSync ---Customized syncing specifically for guild comms ---@param protocol number ---@param prefix string ---@param msg any 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 local result = select(-1, 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. if type(result) == "number" and result ~= 0 then DBM:Debug("|cffff0000sendGuildSync failed with a result of " ..result.. " for prefix |r" .. prefix) end end end private.sendGuildSync = sendGuildSync ---Custom sync function that should only be used for user generated sync messages ---@param protocol number ---@param prefix string ---@param msg any local function sendLoggedSync(protocol, prefix, msg) if dbmIsEnabled then msg = msg or "" local fullname = playerName .. "-" .. normalizedPlayerRealm local sendChannel = "SOLO" if not IsTrialAccount() then 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) sendChannel = "INSTANCE_CHAT" else if IsInRaid() then sendChannel = "RAID" elseif IsInGroup(1) then sendChannel = "PARTY" end end end if sendChannel == "SOLO" then handleSync("SOLO", playerName, nil, (protocol or DBMSyncProtocol), prefix, strsplit("\t", msg)) else local result = select(-1, C_ChatInfo.SendAddonMessageLogged(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, sendChannel)) if type(result) == "number" and result ~= 0 then DBM:Debug("|cffff0000sendLoggedSync failed with a result of " ..result.. " for prefix |r" .. prefix) end end end end ---Sync Object specifically for out in the world sync messages that have different rules than standard syncs ---@param self DBM ---@param protocol number ---@param prefix string ---@param msg any ---@param noBNet boolean? 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 local sendChannel = "SOLO" if not IsTrialAccount() then 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) sendChannel = "INSTANCE_CHAT" else if IsInRaid() then sendChannel = "RAID" elseif IsInGroup(1) then sendChannel = "PARTY" end end end if sendChannel == "SOLO" then handleSync("SOLO", playerName, nil, (protocol or DBMSyncProtocol), prefix, strsplit("\t", msg)) else local result = select(-1, SendAddonMessage(DBMPrefix, fullname .. "\t" .. (protocol or DBMSyncProtocol) .. "\t" .. prefix .. "\t" .. msg, sendChannel)) if type(result) == "number" and result ~= 0 then DBM:Debug("|cffff0000SendWorldSync failed with a result of " ..result.. " for prefix |r" .. prefix) end end if IsInGuild() and sendChannel ~= "SOLO" 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 -- sends a whisper to a player by their character name or BNet presence id -- returns true if the message was sent, nil otherwise local function sendWhisper(target, msg) if IsTrialAccount() then return end 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 ---Automatic spell icon parsing ---@param spellId any ---@param objectType string? ---@param fallbackIcon any? ---@return any function DBM: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 DBM:GetSpellTexture(spellId) end end return icon or fallbackIcon or 136116 end ---Automatic spell name parsing ---@param spellId any ---@param objectType string? ---@return any function DBM: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:GetSpellName(spellId) end end return spellName end -------------- -- Events -- -------------- ---@alias DBMModOrDBM DBMMod|DBM ---@alias DBMEvent WowEvent|CombatlogEvent|DBMUnfilteredEvent do local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo ---@type table local registeredEvents = {} ---@type table> local registeredSpellIds = {} ---@type table> local unfilteredCLEUEvents = {} ---@type table local registeredUnitEventIds = {} ---@class DBMCombatLogArgs local argsMT = {} ---@class DBMCombatLogArgs local args = setmetatable({ -- Explicitly define them because type deduction from CLEU events is ~impossible event = nil, ---@type string timestamp = nil, ---@type number sourceGUID = nil, ---@type string sourceName = nil, ---@type string? sourceFlags = nil, ---@type number sourceRaidFlags = nil, ---@type number destGUID = nil, ---@type string destName = nil, ---@type string? destFlags = nil, ---@type number destRaidFlags = nil, ---@type number spellId = nil, ---@type number spellName = nil, ---@type string extraSpellId = nil, ---@type number extraSpellName = nil, ---@type string environmentalType = nil, ---@type string amount = nil, ---@type number overkill = nil, ---@type number school = nil, ---@type number blocked = nil, ---@type number? absorbed = nil, ---@type number? critical = nil, ---@type boolean glancing = nil, ---@type boolean crushing = nil, ---@type boolean }, {__index = argsMT}) function argsMT:IsSpellID(...) for i = 1, select('#', ...) do if args.spellId == select(i, ...) then return true end end return false 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 --As of 1.15.0 classic era now has spellids, but want to keep wrapper for now in case they ever revert this or they decide to do classic fresh with no IDs one day function argsMT:IsSpell(...) -- if isClassic then -- --ugly ass performance wasting checks that have to first convert Ids to names because #nochanges -- for i = 1, select('#', ...) do -- local spellName = DBM:GetSpellInfo(select(i, ...)) -- if spellName and spellName == args.spellName then -- return true -- end -- end -- return false -- else --Just simple table comoparison for i = 1, select('#', ...) do if args.spellId == select(i, ...) then return true end end return false -- end end --- Check if the player is the target. *The* player, not *a* player, see IsDestTypePlayer() for unit type checks. ---@ref function argsMT: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 args.destName == playerName end end --- Check if the player is the source. *The* player, not *a* player, see IsSrcTypePlayer() for unit type checks. function argsMT: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 args.sourceName == playerName end end function argsMT:IsNPC() return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0 end function argsMT:IsPet() return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0 end function argsMT:IsPetSource() return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0 end function argsMT: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: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: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: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:GetSrcCreatureID() return DBM:GetCIDFromGUID(self.sourceGUID) end function argsMT: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 private.mainEventHandler = handleEvent -- Only for testing. 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) ---@param mod DBMModOrDBM 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 ---@param mod DBMModOrDBM 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("|cffff0000DBM RegisterEvents Warning: " .. spellId .. " is not a number!|r") return end local spellName = DBM:GetSpellName(spellId) if spellId and not spellName then DBM:Debug("|cffff0000DBM RegisterEvents Warning: " .. spellId .. " id does not exist!|r") 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:GetSpellName(spellId) if spellId and not spellName then DBM:Debug("|cffff0000DBM unregisterSpellId Warning: " .. spellId .. " id does not exist!|r") return end --local regName = isClassic and spellName or spellId local refs = (registeredSpellIds[event][spellId] or 1) - 1 registeredSpellIds[event][spellId] = refs if refs <= 0 then registeredSpellIds[event][spellId] = nil end end --There are 2 tables. unfilteredCLEUEvents and registeredSpellIds table. --unfilteredCLEUEvents saves UNFILTERED cleu event count. this is count table to prevent bad unregister. --registeredSpellIds tables filtered table. this saves event and spell ids. works smiliar with unfilteredCLEUEvents table. ---@param mod DBMModOrDBM 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 ---@param mod DBMModOrDBM function unregisterCLEUEvent(mod, event) test:Trace(mod, "UnregisterEvents", "Regular", 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 ---@param self DBMModOrDBM ---@param ... DBMEvent|string function DBM:RegisterEvents(...) test:Trace(self, "RegisterEvents", "Regular", ...) for i = 1, select('#', ...) do local event = select(i, ...) -- spell events with special care. if event:sub(0, 6) == "SPELL_" and event ~= "SPELL_NAME_UPDATE" or event:sub(0, 6) == "RANGE_" or event:sub(0, 6) == "SWING_" or event == "UNIT_DIED" or event == "UNIT_DESTROYED" or event == "PARTY_KILL" then registerCLEUEvent(self, event) else local eventWithArgs = event -- unit events need special care if event:sub(0, 5) == "UNIT_" then -- unit events are limited to 8 "parameters", as there is no good reason to ever use more than 5 (it's just that the code old code supported 8 (boss1-5, target, focus)) local arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event) if not arg1 and event:sub(-11) ~= "_UNFILTERED" then -- no arguments given, support for legacy mods eventWithArgs = event .. " target" if not private.isClassic then eventWithArgs = eventWithArgs .. " focus boss1 boss2 boss3 boss4 boss5" 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 ---@param mod DBMModOrDBM local function unregisterUEvent(mod, event) if event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED" then test:Trace(mod, "UnregisterEvents", "Regular", event) 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 ---@param self DBMModOrDBM 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 test:Trace(self, "UnregisterEvents", "InCombat", event) 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 ---@param self DBMModOrDBM ---@param ... DBMEvent|string function DBM:RegisterShortTermEvents(...) DBM:Debug("RegisterShortTermEvents fired", 2) test:Trace(self, "RegisterEvents", "ShortTerm", ...) 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 ---@param self DBMModOrDBM function DBM:UnregisterShortTermEvents() DBM:Debug("UnregisterShortTermEvents fired", 3) 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 test:Trace(self, "UnregisterEvents", "ShortTerm", event) 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 event or not registeredEvents[event] then return end local eventSub6 = event:sub(0, 6) if (eventSub6 == "SPELL_" or eventSub6 == "RANGE_") and not unfilteredCLEUEvents[event] and registeredSpellIds[event] then if not registeredSpellIds[event][extraArg1] then return end end -- process some high volume events without building the whole table which is somewhat faster -- this prevents work-around with mods that used to have their own event handler to prevent this overhead if noArgTableEvents[event] then return handleEvent(nil, event, sourceGUID, sourceName 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) test:RegisterLocalHook("CombatLogGetCurrentEventInfo", function(val) local old = CombatLogGetCurrentEventInfo CombatLogGetCurrentEventInfo = val return old end) end -------------- -- OnLoad -- -------------- do local isLoaded = false local onLoadCallbacks, disabledMods = {}, {} ---@param self DBM local function infiniteLoopNotice(self, message) AddMsg(self, message) self:Schedule(30, infiniteLoopNotice, self, message) end ---@param self DBM 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 private.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(self, remaining, playerName, nil, true) else--It must have ended while we were offline, kill variable. self.Options.RestoreSettingBreakTimer = nil end end if not IsInInstance() then sendGuildSync(DBMSyncProtocol, "GH") end difficulties:RefreshCache() 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 --Establish a classic sub mod version for version checks and out of date notification/checking if not private.isRetail then local checkedSubmodule = private.isCata and "DBM-Raids-Cata" or private.isWrath and "DBM-Raids-WoTLK" or private.isBCC and "DBM-Raids-BC" or private.isClassic and "DBM-Raids-Vanilla" if checkedSubmodule and C_AddOns.DoesAddOnExist(checkedSubmodule) then local version = C_AddOns.GetAddOnMetadata(checkedSubmodule, "Version") or "r0" DBM.classicSubVersion = tonumber(string.sub(version, 2, 4)) or 0 end end isLoaded = true for _, v in ipairs(onLoadCallbacks) do xpcall(v, geterrorhandler()) end onLoadCallbacks = nil loadOptions(self) DBT:LoadOptions("DBM") self.AddOns = {} private:OnModuleLoad() if C_AddOns.GetAddOnEnableState("VEM-Core", playerName) >= 1 then self:Disable(true) self:Schedule(15, infiniteLoopNotice, self, L.VEM) return end if C_AddOns.GetAddOnEnableState("DBM-Profiles", playerName) >= 1 then self:Disable(true) self:Schedule(15, infiniteLoopNotice, self, L.OUTDATEDPROFILES) return end if C_AddOns.GetAddOnEnableState("DBM-SpellTimers", playerName) >= 1 then ---@type string|number local version = C_AddOns.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, infiniteLoopNotice, 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 --LuaLS doesn't like Plater ---@diagnostic disable-next-line: undefined-global 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 C_AddOns.GetAddOnEnableState("DPMCore", playerName) >= 1 then self:Disable(true) self:Schedule(15, infiniteLoopNotice, self, L.DPMCORE) return end if C_AddOns.GetAddOnEnableState("DBM-VictorySound", playerName) >= 1 then self:Disable(true) C_TimerAfter(15, function() AddMsg(self, L.VICTORYSOUND) end) return end if C_AddOns.GetAddOnEnableState("DBM-LDB", playerName) >= 1 then C_TimerAfter(15, function() AddMsg(self, L.DBMLDB) end) end if C_AddOns.GetAddOnEnableState("DBM-LootReminder", playerName) >= 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 local LibDBIcon = LibStub("LibDBIcon-1.0") ---@diagnostic disable-next-line: param-type-mismatch LibDBIcon:Register("DBM", private.dataBroker, DBM_MinimapIcon) if DBM_MinimapIcon.showInCompartment == nil then LibDBIcon:AddButtonToCompartment("DBM") end 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, C_AddOns.GetNumAddOns() do local addonName = C_AddOns.GetAddOnInfo(i) local enabled = C_AddOns.GetAddOnEnableState(i, playerName) if C_AddOns.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(",", C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-MapID") or "")} local minToc = tonumber(C_AddOns.GetAddOnMetadata(i, "X-Min-Interface") or 0) local firstMapId = mapIdTable[1] local firstMapName if tonumber(firstMapId) then firstMapName = GetRealZoneText(tonumber(firstMapId)) elseif firstMapId:sub(1, 1) == "m" then firstMapName = C_Map.GetMapInfo(tonumber(firstMapId:sub(2)) or 0) firstMapName = firstMapName and firstMapName.name end for j = #mapIdTable, 1, -1 do local id = tonumber(mapIdTable[j]) if id then mapIdTable[j] = id elseif not (mapIdTable[j]:sub(1, 1) == "m" and tonumber(mapIdTable[j]:sub(2))) then tremove(mapIdTable, j) end end tinsert(self.AddOns, { sort = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Sort") or mhuge) or mhuge, type = C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Type") or "OTHER", category = C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Category") or "Other", statTypes = C_AddOns.GetAddOnMetadata(i, "X-DBM-StatTypes") or "", name = C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Name") or firstMapName or CL.UNKNOWN, mapId = mapIdTable, subTabs = C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-SubCategoriesID") and {strsplit(",", C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-SubCategoriesID"))} or C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-SubCategories") and {strsplit(",", C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-SubCategories"))}, oneFormat = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Has-Single-Format") or 0) == 1, -- Deprecated hasLFR = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Has-LFR") or 0) == 1, -- Deprecated hasChallenge = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Has-Challenge") or 0) == 1, -- Deprecated noHeroic = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-No-Heroic") or 0) == 1, -- Deprecated hasMythic = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Has-Mythic") or 0) == 1, -- Deprecated hasTimeWalker = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Has-TimeWalker") or 0) == 1, -- Deprecated noStatistics = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-No-Statistics") or 0) == 1, isWorldBoss = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-World-Boss") or 0) == 1, isExpedition = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-Expedition") or 0) == 1, minRevision = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-MinCoreRevision") or 0), minExpansion = tonumber(C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-MinExpansion") or 0), minToc = minToc, modId = addonName, }) 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 --For handling zones like Warfront: Arathi - Alliance local subTabName = GetRealZoneText(id):trim() or id for w in string.gmatch(subTabName, " - ") do if w:trim() ~= "" then subTabName = w break end end self.AddOns[#self.AddOns].subTabs[k] = subTabName else self.AddOns[#self.AddOns].subTabs[k] = (subTabs[k]):trim() end end end end if C_AddOns.GetAddOnMetadata(i, "X-DBM-Mod-LoadCID") then local idTable = {strsplit(",", C_AddOns.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 C_AddOns.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 = C_AddOns.GetAddOnMetadata(i, "X-DBM-Voice-ShortName") local voiceVersion = tonumber(C_AddOns.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 = C_AddOns.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 C_AddOns.GetAddOnMetadata(i, "X-DBM-Voice-HasCount") then--Supports adding countdown options, insert new countdown into table DBM:AddCountSound(C_AddOns.GetAddOnMetadata(i, "X-DBM-Voice-Name"), "VP: " .. voiceValue, "Interface\\AddOns\\DBM-VP" .. voiceValue .. "\\count\\") end end) end end if C_AddOns.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 = C_AddOns.LoadAddOn(addonName) C_TimerAfter(0.01, function() local voiceGlobal = C_AddOns.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 C_AddOns.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 = C_AddOns.LoadAddOn(addonName) C_TimerAfter(0.01, function() local victoryGlobal = C_AddOns.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 C_AddOns.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 = C_AddOns.LoadAddOn(addonName) C_TimerAfter(0.01, function() local defeatGlobal = C_AddOns.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 C_AddOns.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 = C_AddOns.LoadAddOn(addonName) C_TimerAfter(0.01, function() local musicGlobal = C_AddOns.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", "ZONE_CHANGED_NEW_AREA" ) if private.newShit then self:RegisterEvents( "START_PLAYER_COUNTDOWN", "CANCEL_PLAYER_COUNTDOWN" ) end if private.wowTOC >= 110000 then self:RegisterEvents( "PLAYER_MAP_CHANGED" ) end if not private.isClassic then -- Retail, WoTLKC, and BCC self:RegisterEvents( "LFG_PROPOSAL_FAILED", "LFG_PROPOSAL_SHOW", "LFG_PROPOSAL_SUCCEEDED", "LFG_ROLE_CHECK_SHOW" ) end if private.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 private.isBCC or private.isClassic then self:RegisterEvents( "UNIT_HEALTH_FREQUENT mouseover target focus player",--Still exists in classic and non frequent is slow and less reliable "CHARACTER_POINTS_CHANGED" ) else -- Wrath and Cata 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" ) end if RolePollPopup and RolePollPopup:IsEventRegistered("ROLE_POLL_BEGIN") and private.isRetail then RolePollPopup:UnregisterEvent("ROLE_POLL_BEGIN") end self:GROUP_ROSTER_UPDATE(true) 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) self:ZONE_CHANGED_NEW_AREA() end end function DBM:PLAYER_ENTERING_WORLD(isLogin, isReload) if isLogin or isReload then 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) if not private.isRetail then --Shown only once per character on login. Repeat showings now handled by the raid module check on raid zone in, and boss pull and wipes within vanilla and wrath raids C_TimerAfter(60, function() if self.Options.NewsMessageShown2 < 3 then self.Options.NewsMessageShown2 = 3 self:AddMsg(L.NEWS_UPDATE) end end) end end if not C_ChatInfo.IsAddonMessagePrefixRegistered(DBMPrefix) then C_ChatInfo.RegisterAddonMessagePrefix(DBMPrefix) -- main prefix for DBM4 end if not C_ChatInfo.IsAddonMessagePrefixRegistered("BigWigs") then C_ChatInfo.RegisterAddonMessagePrefix("BigWigs") end if not C_ChatInfo.IsAddonMessagePrefixRegistered("Transcriptor") then C_ChatInfo.RegisterAddonMessagePrefix("Transcriptor") end --Check if any previous changed cvars were not restored and restore them if self.Options.RestoreSettingSFX then SetCVar("Sound_EnableSFX", 1) self.Options.RestoreSettingSFX = nil self:Debug("Restoring Sound_EnableSFX CVAR") end if self.Options.RestoreSettingAmbiance then SetCVar("Sound_EnableAmbience", 1) self.Options.RestoreSettingAmbiance = nil self:Debug("Restoring Sound_EnableAmbience CVAR") end if self.Options.RestoreSettingMusic then SetCVar("Sound_EnableMusic", 1) self.Options.RestoreSettingMusic = nil self:Debug("Restoring Sound_EnableMusic CVAR") end --RestoreSettingCustomMusic doens't need restoring here, since zone change transition will handle it end end end ----------------- -- Callbacks -- ----------------- do local callbacks = {} function fireEvent(event, ...) if not callbacks[event] then return end for _, v in ipairs(callbacks[event]) do securecall(v, event, ...) end end ---@param event string ---@param ... any? 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 if v.classicSubVers then self:AddMsg(L.VERSIONCHECK_ENTRY:format(name, L.DBM .. " " .. v.displayVersion .. " / " .. v.classicSubVers, showRealDate(v.revision), v.VPVersion or ""), false)--Only display VP version if not running two mods else 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 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 ---@param time number --time in seconds ---@param text string --timer text ---@param broadcast boolean? --if it should be broadcast to the raid ---@param sender any --who sent it (if it was started by sync) ---@param loop boolean? --if the timer should loop indefinitely ---@param terminate boolean? --if this is true, terminates the timer ---@param whisperTarget any 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 and not IsTrialAccount() then text = text:sub(1, 16) text = text:gsub("%%t", UnitName("target") or "") 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 "") if time < 3 then self:AddMsg(L.PIZZA_ERROR_USAGE) return end DBT:CreateBar(time, text, private.isRetail and 237538 or 134376) fireEvent("DBM_TimerStart", "DBMPizzaTimer", text, time, private.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 C_AddOns.GetAddOnEnableState("VEM-Core", playerName) >= 1 then self:AddMsg(L.VEM) return end if C_AddOns.GetAddOnEnableState("DBM-Profiles", playerName) >= 1 then self:AddMsg(L.OUTDATEDPROFILES) return end if C_AddOns.GetAddOnEnableState("DBM-SpellTimers", playerName) >= 1 then ---@type number|string local version = C_AddOns.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 C_AddOns.GetAddOnEnableState("DPMCore", playerName) >= 1 then self:AddMsg(L.DPMCORE) return end if C_AddOns.GetAddOnEnableState("DBM-VictorySound", playerName) >= 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 C_AddOns.IsAddOnLoaded("DBM-GUI") then local enabled = C_AddOns.GetAddOnEnableState("DBM-GUI", playerName) if enabled == 0 then C_AddOns.EnableAddOn("DBM-GUI") end local loaded, reason = C_AddOns.LoadAddOn("DBM-GUI") if not loaded then if reason and _G["ADDON_" .. reason] then self:AddMsg(L.LOAD_GUI_ERROR:format(tostring(_G["ADDON_" .. reason]))) 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 if not private.isRetail then raid[playerName].classicSubVers = DBM.classicSubVersion end raid[playerName].locale = GetLocale() raid[playerName].enabledIcons = tostring(not DBM.Options.DontSetIcons) raidGuids[UnitGUID("player") or ""] = playerName end end) end) ---Throttled Roster Updating to save cpu and reduce comms ---@param self DBM 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(newersubVersionPerson) twipe(forceDisablePerson) inRaid = true sendSync(DBMSyncProtocol, "H") if dbmIsEnabled and not IsTrialAccount() then SendAddonMessage("BigWigs", bwVersionQueryString:format(0, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "RAID") end if private.isRetail or private.isCata 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(newersubVersionPerson, 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(newersubVersionPerson) twipe(forceDisablePerson) inRaid = true sendSync(DBMSyncProtocol, "H") if dbmIsEnabled and not IsTrialAccount() then SendAddonMessage("BigWigs", bwVersionQueryString:format(0, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "PARTY") end if private.isRetail or private.isCata 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(newersubVersionPerson, 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(newersubVersionPerson) 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 if not private.isRetail then raid[playerName].classicSubVers = DBM.classicSubVersion end raid[playerName].locale = GetLocale() raidGuids[UnitGUID("player")] = playerName lastGroupLeader = nil end end function DBM:GROUP_ROSTER_UPDATE(force) self:Unschedule(updateAllRoster) --Updated with no throttle on ADDON_LOADDED, DBM:LoadMod and if in combat with a boss if force or #inCombat > 0 then updateAllRoster(self) else self:Schedule(3.5, updateAllRoster, self) end end function DBM:INSTANCE_GROUP_SIZE_CHANGED() difficulties:RefreshCache(true) end --C_Map.GetMapGroupMembersInfo function DBM:GetNumRealPlayersInZone() if not IsInGroup() then return 1 end local total = 0 local currentMapId = select(-1, UnitPosition("player")) if IsInRaid() then for i = 1, GetNumGroupMembers() do local targetMapId = select(-1, 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 = select(-1, 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 ---Gets raid rank (0-2) for requested player by name ---@param name string? --optional, if omited returns playerName rank ---@return integer 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 ---Gets raid subgroup for requested player by name ---@param name string? --optional, if omited returns playerName subgroup ---@return integer function DBM:GetRaidSubgroup(name) name = name or playerName return (raid[name] and raid[name].subgroup) or 0 end ---@param name string ---@return boolean ---@overload fun(self: DBM): table 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 ---This is primarily used for cached player unitIds by name lookup ---
I'm not even sure why a boss check is in it, since GetBossUnitId existed (and is now deprecated) ---
I'll leave the boss checking for now since I don't know if any mods (or core) are using this function this way 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" } ---Not to be confused with GetUnitIdFromCID ---@param self DBMModOrDBM ---@param guid string ---@param bossOnly boolean? --Used when you only need to check "boss" unitids. Bypasses UnitTokenFromGUID (which checks EVERYTHING) function DBM:GetUnitIdFromGUID(guid, bossOnly) local returnUnitID --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 and they have boss as the END in their order selection) --DBM prioiritzes the most common/useful tokens first, and rarely iterates passed even boss1, which is first token in OUR priorities if UnitTokenFromGUID and not bossOnly then returnUnitID = UnitTokenFromGUID(guid) end if returnUnitID then return returnUnitID else local usedTable = bossOnly and bossTargetuIds or fullUids for _, unitId in ipairs(usedTable) do local guid2 = UnitGUID(unitId) if guid == guid2 then return unitId end end end end ---Not to be confused with GetUnitIdFromGUID, in this function we don't know a specific guid so can't use UnitTokenFromGUID ---@param self DBMModOrDBM ---@param creatureID number ---@param bossOnly boolean? --Used when you only need to check "boss" unitids. function DBM:GetUnitIdFromCID(creatureID, bossOnly) --Always prioritize a quick boss unit scan on retail first if not private.isClassic and not private.isBCC then for i = 1, 10 do local unitId = "boss" .. i local bossGUID = UnitGUID(unitId) local cid = self:GetCIDFromGUID(bossGUID) if cid == creatureID then return unitId end end end if not bossOnly then for _, unitId in ipairs(fullUids) do local guid2 = UnitGUID(unitId) local cid = self:GetCIDFromGUID(guid2) if cid == creatureID then return unitId end end end end ---Deprecated, only old mods use this (newer mods use GetUnitIdFromGUID or GetUnitIdFromCID) ---@param name string ---@param bossOnly boolean? --Used when you only need to check "boss" unitids. function DBM:GetBossUnitId(name, bossOnly) local returnUnitID if not private.isClassic and not private.isBCC then for i = 1, 10 do if UnitName("boss" .. i) == name then returnUnitID = "boss" .. i end 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 ---@param name string function DBM:GetPlayerGUIDByName(name) return raid[name] and raid[name].guid end function DBM:GetMyPlayerInfo() return playerName, private.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 if CustomNames then ---@diagnostic disable-next-line: undefined-field name = CustomNames.Get(name) end local shortName, serverName = string.split("-", name) if serverName and serverName ~= playerRealm and serverName ~= normalizedPlayerRealm then return shortName .. "*" else return name end end ---@param guid string function DBM:GetFullPlayerNameByGUID(guid) return raidGuids[guid] end ---@param guid string function DBM:GetPlayerNameByGUID(guid) return raidGuids[guid] and raidGuids[guid]:gsub("%-.*$", "") end ---@param name any ---@param higher boolean? ---@return number 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 = select(-1, UnitPosition("player")) local realGroupMembers = 0 if IsInGroup() then for uId in self:GetGroupMembers() do local targetMapId = select(-1, UnitPosition(uId)) if targetMapId == currentMapId then realGroupMembers = realGroupMembers + 1 end end else return 1 end return realGroupMembers end ---@param self DBMModOrDBM function DBM:GetUnitCreatureId(uId) return self:GetCIDFromGUID(UnitGUID(uId)) end --Creature/Vehicle/Pet ----:::::: --Player/Item ----:: ---@param self DBMModOrDBM 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 ---@param self DBMModOrDBM function DBM:IsCreatureGUID(guid) local guidType = strsplit("-", guid or "") return guidType and (guidType == "Creature" or guidType == "Vehicle")--To determine, add pet or not? end --Scope, will only check if a unit is within 43 yards now ---@param self DBMModOrDBM ---@param range number ---@param targetname string? 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 restrictionsActive = private.isRetail and DBM:HasMapRestrictions() local inRange = DBM.RangeCheck:GetDistance(uId)--Do not use self on this function, because self might be bossModPrototype if inRange and inRange < (restrictionsActive and 43 or range) + 0.5 then return true end 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 ---@param self DBMModOrDBM ---@param force boolean? ---@return number? function DBM:GetGossipID(force) if self.Options.DontAutoGossip and not force then return nil 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 nil end return nil end ---Hybrid all in one object to auto check and confirm multiple gossip IDs at once ---@param self DBMModOrDBM ---@param confirm boolean? 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 ---@param self DBMModOrDBM ---@param gossipOptionID number ---@param confirm boolean? 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 do 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:GetSoundMigration(sound) return soundMigrationtable[sound] end end function DBM:LoadModOptions(modId, inCombat, first) local oldSavedVarsName = modId:gsub("-", "") .. "_SavedVars" local savedVarsName = modId:gsub("-", "") .. "_AllSavedVars" local savedStatsName = modId:gsub("-", "") .. "_SavedStats" local fullname = playerName .. "-" .. playerRealm if not currentSpecID or not currentSpecGroup or (currentSpecName or "") == playerClass then self:SetCurrentSpecInfo() end local profileNum = private.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") or option:find("MoviesSeen"))) 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 self:GetSoundMigration(checkedOption) then savedOptions[id][profileNum][option] = self:GetSoundMigration(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.followerKills = stats.followerKills or 0 stats.followerPulls = stats.followerPulls or 0 stats.storyKills = stats.storyKills or 0 stats.storyPulls = stats.storyPulls or 0 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") or id:find("MoviesSeen")) 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() private.playerLevel = UnitLevel("player") if private.playerLevel < 15 and private.playerLevel > 9 then self:PLAYER_SPECIALIZATION_CHANGED("player") -- 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 = private.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 ---@param mod DBMMod 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 = private.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 = private.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 = private.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 = private.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:CreateDefaultModStats() ---@class ModStats local defaultStats = {} defaultStats.followerKills = 0 defaultStats.followerPulls = 0 defaultStats.storyKills = 0 defaultStats.storyPulls = 0 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 return defaultStats 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) local defaultStats = DBM:CreateDefaultModStats() 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 ---@param self DBM 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 self:GetSoundMigration(self.Options[setting]) then self.Options[setting] = self:GetSoundMigration(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() --LuaLS doesn't like Plater ---@diagnostic disable-next-line: undefined-global 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 ---@param self DBM 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 (private.isRetail or private.isCata) and IsInGroup() then self:RoleCheck(false) end end end --Retail API doesn't need throttle function DBM:PLAYER_SPECIALIZATION_CHANGED(unit) if unit == "player" then self:Unschedule(throttledTalentCheck) throttledTalentCheck(self) end 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/Cata 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 = (private.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 dungeonShown = false local sodRaids = {[48] = true, [90] = true, [109] = true} 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 dragonflightZones = {[2522] = true, [2569] = true, [2549] = 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} local seasonalZones = {[2516] = true, [2526] = true, [2515] = true, [2521] = true, [2527] = true, [2519] = true, [2451] = true, [2520] = true}--DF Season 4 --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 not self:IsTrivial() then --TODO, bump checkedDungeon to WarWithin dungeon mods on retail in prepatch local checkedDungeon = private.isRetail and "DBM-Party-Dragonflight" or private.isCata and "DBM-Party-Cataclysm" or private.isWrath and "DBM-Party-WotLK" or private.isBCC and "DBM-Party-BC" or "DBM-Party-Vanilla" if (seasonalZones[LastInstanceMapID] or difficulties:InstanceType(LastInstanceMapID) == 2) and not C_AddOns.DoesAddOnExist(checkedDungeon) and not dungeonShown then AddMsg(self, L.MOD_AVAILABLE:format("DBM Dungeon mods"), nil, private.isRetail or private.isCata) dungeonShown = true elseif (self:IsSeasonal("SeasonOfDiscovery") and sodRaids[LastInstanceMapID] or classicZones[LastInstanceMapID]) and not C_AddOns.DoesAddOnExist("DBM-Raids-Vanilla") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Vanilla/SoD mods"), nil, private.isClassic)--Play sound only in Vanilla --Reshow news message as well in classic flavors --if not isRetail and (DBM.classicSubVersion or 0) < 1 then -- C_TimerAfter(5, function() self:AddMsg(L.NEWS_UPDATE_REPEAT, nil, true) end) --end elseif bcZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-BC") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Burning Crusade mods"), nil, private.isBCC)--Play sound only in TBC elseif wrathZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-WoTLK") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Wrath of the Lich King mods"), nil, private.isWrath)--Play sound only in wrath --Reshow news message as well in classic flavors --if not isRetail and (DBM.classicSubVersion or 0) < 1 then -- C_TimerAfter(5, function() self:AddMsg(L.NEWS_UPDATE_REPEAT, nil, true) end) --end elseif cataZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-Cata") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Cataclysm mods"), nil, private.isCata)--Play sound only in cata elseif mopZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-MoP") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Mists of Pandaria mods")) elseif wodZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-WoD") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Warlords of Draenor mods")) elseif legionZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-Legion") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Legion mods")) elseif bfaZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-BfA") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Battle for Azeroth mods")) elseif shadowlandsZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-Shadowlands") then AddMsg(self, L.MOD_AVAILABLE:format("DBM Shadowlands mods")) -- elseif dragonflightZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Raids-Dragonflight") then--Uncomment in War Within on mod split -- AddMsg(self, L.MOD_AVAILABLE:format("DBM Dragonflight mods")) end end if challengeScenarios[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-Challenges") then--No trivial check on challenge scenarios AddMsg(self, L.MOD_AVAILABLE:format("DBM-Challenges"), nil, true) end if pvpZones[LastInstanceMapID] and not C_AddOns.DoesAddOnExist("DBM-PvP") and not pvpShown then AddMsg(self, L.MOD_AVAILABLE:format("DBM-PvP"), nil, true) pvpShown = true end end local sodPvpZones = { [1440] = true, -- Ashenvale [1434] = true, -- Stranglethorn Vale } function DBM:CheckAvailableModsByMap() local mapId = C_Map.GetBestMapForUnit("player") if not mapId then return end if UnitOnTaxi("player") then return end -- Don't spam the player if they are just passing through if self:IsSeasonal("SeasonOfDiscovery") then if sodPvpZones[mapId] and not pvpShown and not C_AddOns.DoesAddOnExist("DBM-PvP") then self:AddMsg(L.MOD_AVAILABLE:format("DBM-PvP")) pvpShown = true end end end ---@param force boolean? Only used when /dbm musicstart is used directly by user ---@param cleanup boolean? Runs on zone change/cinematic Start (first load delay) and combat end function DBM:TransitionToDungeonBGM(force, cleanup) if cleanup then self:Unschedule(self.TransitionToDungeonBGM) if self.Options.RestoreSettingCustomMusic then SetCVar("Sound_EnableMusic", self.Options.RestoreSettingCustomMusic) self.Options.RestoreSettingCustomMusic = 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 if self.Options.RestoreSettingMusic then return end--Music was disabled by the music disable override, abort here fireEvent("DBM_MusicStart", "RaidOrDungeon") if self.Options.EventSoundDungeonBGM and self.Options.EventSoundDungeonBGM ~= "None" and self.Options.EventSoundDungeonBGM ~= "" and not (self.Options.EventDungMusicMythicFilter and (difficulties.savedDifficulty == "mythic" or difficulties.savedDifficulty == "challenge")) then if not self.Options.RestoreSettingCustomMusic then self.Options.RestoreSettingCustomMusic = tonumber(GetCVar("Sound_EnableMusic")) or 1 if self.Options.RestoreSettingCustomMusic == 0 then SetCVar("Sound_EnableMusic", 1) else self.Options.RestoreSettingCustomMusic = 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 ---@param self DBM ---@param delay number? local function SecondaryLoadCheck(self, delay) local _, instanceType, difficulty, _, _, _, _, mapID, instanceGroupSize = private.GetInstanceInfo() difficulties:RefreshCache(true) LastGroupSize = instanceGroupSize self:Debug("Instance Check fired with mapID " .. mapID .. " and difficulty " .. difficulty .. " and delay " .. (delay or 0), 2) -- Difficulty index also checked because in challenge modes and M+, difficulty changes with no ID change -- if ID changes we need to execute updated autologging and checkavailable mods checks -- ID and difficulty hasn't changed, don't waste cpu doing anything else (example situation, porting into garrosh stage 4 is a loading screen) if LastInstanceMapID == mapID and difficulties.difficultyIndex == difficulty then self:TransitionToDungeonBGM() self:Debug("|c00F2F200No action taken because mapID and difficultyID hasn't changed since last check |r", 2) return end self:Debug("|c0069CCF0mapID or difficulty has changed, updating LastInstanceMapID to |r" .. mapID, 2) 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 difficulties.savedDifficulty == "worldboss" then for i = #inCombat, 1, -1 do self:EndCombat(inCombat[i], true, nil, "Left zone of world boss") end end end -- Auto Logging for entire zone if record only bosses is off if not self.Options.RecordOnlyBosses then if LastInstanceType == "raid" or LastInstanceType == "party" then self:StartLogging(0) else self:StopLogging() end end -- LoadMod self:LoadModsOnDemand("mapId", mapID, delay or 0) self:CheckAvailableMods() if self:HasMapRestrictions() then self.Arrow:Hide() self.HudMap:Disable() if (private.isRetail and self.RangeCheck:IsShown()) or 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(delayedCheck) --Extra stuff we want to clean up after loading screens only if not private.isClassic and not private.isBCC then DBT:CancelBar(L.LFG_INVITE)--Disable bar here since LFG_PROPOSAL_SUCCEEDED seems broken right now end fireEvent("DBM_TimerStop", "DBMLFGTimer") timerRequestInProgress = false --Regular load zone code beyond this point self:Debug("LOADING_SCREEN_DISABLED fired", 2) self:Unschedule(SecondaryLoadCheck) --SecondaryLoadCheck(self) --In instance tranfers with no loading screen, InstanceInfo can actually return nil for first few seconds if not delayedCheck then self:Schedule(1, SecondaryLoadCheck, self)--Minimum time delayed by one second to work around an issue on 8.x where spec info isn't available yet on reloadui end self:TransitionToDungeonBGM(false, true) self:Schedule(5, SecondaryLoadCheck, self, 5) if self:HasMapRestrictions() then self.Arrow:Hide() self.HudMap:Disable() if (private.isRetail and self.RangeCheck:IsShown()) or self.RangeCheck:IsRadarShown() then self.RangeCheck:Hide(true) end end end --Zones that change without loading screen local specialZoneIDs = { [2454] = true,--Zaralek Caverns [2574] = true,--Dragon Isles [2444] = true,--Dragon Isles -- [2601] = true,--Khaz Algar (Underground) -- [2774] = true,--Khaz Algar (Underground) -- [2552] = true,--Khaz Algar (Surface) } -- Load based on MapIDs function DBM:ZONE_CHANGED_NEW_AREA() local mapID = C_Map.GetBestMapForUnit("player") if mapID then self:LoadModsOnDemand("mapId", "m" .. mapID) end DBM:CheckAvailableModsByMap() --if a special zone, we need to force update LastInstanceMapID and run zone change functions without loading screen --This hack and table can go away in TWW pre patch when we gain access to PLAYER_MAP_CHANGED if private.wowTOC < 110000 and specialZoneIDs[LastInstanceMapID] then--or difficulties:InstanceType(LastInstanceMapID) == 4 DBM:Debug("Forcing LOADING_SCREEN_DISABLED", 2) self:LOADING_SCREEN_DISABLED(true) end end ---Special event that fires when changing zones in TWW ---@param oldZone number if oldZone is -1, it means it's a loading screen ---@param newZone number function DBM:PLAYER_MAP_CHANGED(oldZone, newZone) self:Debug("PLAYER_MAP_CHANGED fired with oldZone " .. oldZone .. " and newZone " .. newZone, 2) if oldZone == -1 then return end--Let legacy LOADING_SCREEN_DISABLED handle it for now. In future, PLAYER_MAP_CHANGED may replace LSD if classic gets it if LastInstanceMapID ~= newZone then self:Debug("Zone changed, firing secondary load check", 2) --Different ID than cached, run secondary load checks --Delay is still needed due to GetInstanceInfo not returning new information yet instantly on PLAYER_MAP_CHANGED self:TransitionToDungeonBGM(false, true) self:Unschedule(SecondaryLoadCheck) self:Schedule(1, SecondaryLoadCheck, self, 1) self:Schedule(5, SecondaryLoadCheck, self, 5) if self:HasMapRestrictions() then self.Arrow:Hide() self.HudMap:Disable() if (private.isRetail and self.RangeCheck:IsShown()) or self.RangeCheck:IsRadarShown() then self.RangeCheck:Hide(true) end end end end function DBM:CHALLENGE_MODE_RESET() --TODO, if blizzard ever removes loading screen from challenge modes start, then we need to run additional stuff from SecondaryLoadCheck here difficulties.difficultyIndex = 8 self:CheckAvailableMods() if not self.Options.RecordOnlyBosses then self:StartLogging(0, nil, true) end end ---@return string? local function isDmfActiveClassic() if DBM:IsSeasonal("SeasonOfDiscovery") then -- GetServerTime() returns local time in classic and there doesn't seem to be a good way to get actual server date in classic. This is good enough. local dmfOffset = (GetServerTime() - 1713736800) / (60 * 60 * 24 * 28) % 1 return dmfOffset <= 0.25 and "m1456" -- Thunderbluff or dmfOffset >= 0.5 and dmfOffset <= 0.75 and "m1429" -- Elwynn or nil -- Not active else return nil -- TODO: implement Classic era logic and whatever Cataclysm is doing. Slightly more annoying to calculate than SoD end end ---@param checkTable string ---@param checkValue any ---@param delay number? function DBM:LoadModsOnDemand(checkTable, checkValue, delay) self:Debug("LoadModsOnDemand fired for table " .. checkTable .. " value " .. tostring(checkValue)) local dmfMod for _, v in ipairs(self.AddOns) do local modTable = v[checkTable] local enabled = C_AddOns.GetAddOnEnableState(v.modId, playerName) if v.modId == "DBM-WorldEvents" and enabled ~= 0 and not C_AddOns.IsAddOnLoaded(v.modId) then dmfMod = v end --self:Debug(v.modId .. " is " .. enabled, 2) if not C_AddOns.IsAddOnLoaded(v.modId) and modTable and checkEntry(modTable, checkValue) then if enabled ~= 0 then if self:IsSeasonal("SeasonOfDiscovery") and sodRaids[LastInstanceMapID] and v.modId == "DBM-Party-Vanilla" then --Don't load dungeon mods in SoD Raids return end self:LoadMod(v) else self:AddMsg(L.LOAD_MOD_DISABLED:format(v.name)) end end end if private.isRetail and delay then self:ScenarioCheck(delay)--Do not filter. Because ScenarioCheck function includes filter. end -- Hard-code loading logic for DMF classic which depends on time and map if dmfMod and checkTable == "mapId" and private.isClassic and isDmfActiveClassic() == checkValue then self:LoadMod(dmfMod, true) end end end --Scenario mods function DBM:ScenarioCheck(delay) 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, delay or 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 private.isRetail or difficulties.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() and not force then self:AddMsg(L.LOAD_MOD_EXP_MISMATCH:format(mod.name)) return elseif not private.testBuild and mod.minToc > private.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 difficulties:RefreshCache() if private.isRetail then EJ_SetDifficulty(difficulties.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 = C_AddOns.LoadAddOn(mod.modId) if not loaded then if reason == "DISABLED" then self:AddMsg(L.LOAD_MOD_DISABLED:format(mod.name)) elseif reason then self:AddMsg(L.LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_" .. reason] or CL.UNKNOWN))) 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() DBM_GUI:CreateBossModTab(mod, mod.panel) if DBM_GUI.currentViewing == mod.panel.frame then _G["DBM_GUI_OptionsFrame"]:DisplayFrame(mod.panel.frame) end 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 function DBM:LoadModByName(modName, force) for _, v in ipairs(self.AddOns) do if v.modId == modName then self:LoadMod(v, force) end 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 = C_AddOns.GetAddOnEnableState(addon, playerName) if cId and bosscId and cId == bosscId and not C_AddOns.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 GetItemInfo = C_Item and C_Item.GetItemInfo or GetItemInfo local function checkForActualPull() if (DBM.Options.RecordOnlyBosses and #inCombat == 0) or (not private.isRetail and difficulties.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 = select(-1, UnitPosition("player")) local senderZone = select(-1, UnitPosition(senderuId)) if playerZone ~= senderZone then return end--not same zone 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(DBM, 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 > (private.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 ---@param self DBM ---@param sender string ---@param timer any string or number only, but luaLS bitches if I actually tell it that ---@param blizzardTimer boolean? local function pullTimerStart(self, sender, timer, blizzardTimer) if not timer then return end if private.newShit and not blizzardTimer then return end--Ignore old DBM version comms local unitId if sender then--Blizzard cancel events triggered by system (such as encounter start) have no sender if blizzardTimer then unitId = self:GetUnitIdFromGUID(sender) sender = self:GetUnitFullName(unitId) or sender else unitId = self:GetRaidUnitId(sender) end local LFGTankException = IsPartyLFG and IsPartyLFG() and UnitGroupRolesAssigned(sender) == "TANK" if (self:GetRaidRank(sender) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or private.IsEncounterInProgress() then return end 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 unitId then local senderMapID = IsInInstance() and select(-1, UnitPosition(unitId)) or C_Map.GetBestMapForUnit(unitId) or 0 local playerMapID = IsInInstance() and select(-1, UnitPosition("player")) or C_Map.GetBestMapForUnit("player") or 0 if self.Options.DontShowPTNoID and senderMapID and playerMapID and senderMapID ~= playerMapID then return end 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 > 0 and timer < 3) then--timer > 60 or return end if timer <= 0 or self:AntiSpam(1, "PT" .. (sender or "SYSTEM")) then--prevent double pull timer from BW and other mods that are sending D4 and D5 at same time (DELETE AntiSpam Later) if not dummyMod then local threshold = self.Options.PTCountThreshold2 threshold = floor(threshold) ---@class DBMDummyMod: DBMMod dummyMod = self:NewMod("PullTimerCountdownDummy") dummyMod.isDummyMod = true self: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, self.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 self.Options.DontShowPT2 then--and DBT:GetBar(L.TIMER_PULL) dummyMod.timer:Stop() 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) self:FlashClientIcon() if not self.Options.DontShowPT2 then dummyMod.timer:Start(timer, L.TIMER_PULL) end if not self.Options.DontShowPTText and timer then local target = unitId and DBM:GetUnitFullName(unitId.."target") if target and not raid[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 self.Options.EventSoundPullTimer and self.Options.EventSoundPullTimer ~= "" and self.Options.EventSoundPullTimer ~= "None" then self:PlaySoundFile(self.Options.EventSoundPullTimer, nil, true) end if self.Options.RecordOnlyBosses then self:StartLogging(timer, checkForActualPull)--Start logging here to catch pre pots. end if private.isRetail and self.Options.CheckGear and not private.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 >= 18 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 syncHandlers["PT"] = function(sender, _, timer) if DBM.Options.DontShowUserTimers or private.newShit then return end pullTimerStart(DBM, sender, timer) end do local dummyMod2 -- dummy mod for the break timer function breakTimerStart(self, timer, sender)--, blizzardTimer, isRecovery -- if private.newShit and not blizzardTimer and not isRecovery then return end --if sender then--Blizzard cancel events triggered by system (such as encounter start) have no sender -- if blizzardTimer then -- local unitId = self:GetUnitIdFromGUID(sender) -- sender = self:GetUnitFullName(unitId) or sender -- end local LFGTankException = IsPartyLFG and IsPartyLFG() and UnitGroupRolesAssigned(sender) == "TANK" if (self:GetRaidRank(sender) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or private.IsEncounterInProgress() then return end --end if not dummyMod2 then local threshold = self.Options.PTCountThreshold2 threshold = floor(threshold) ---@class DBMDummyMod2: DBMMod dummyMod2 = self:NewMod("BreakTimerCountdownDummy") dummyMod2.isDummyMod = true self:GetModLocalization("BreakTimerCountdownDummy"):SetGeneralLocalization{name = L.MINIMAP_TOOLTIP_HEADER} dummyMod2.text = dummyMod2:NewAnnounce("%s", 1, private.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, private.isRetail and "237538" or "136106", nil, nil, 0, nil, nil, self.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 self.Options.DontShowPT2 then--and DBT:GetBar(L.TIMER_BREAK) dummyMod2.timer:Stop() end dummyMod2.text:Cancel() self.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 ---@type number, string|number 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(stringUtils.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--or private.newShit timer = tonumber(timer or 0) if timer > 3600 then return end if (DBM:GetRaidRank(sender) == 0 and IsInGroup()) or select(2, IsInInstance()) == "pvp" or private.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)--, nil, true end local function SendVersion(guild) --Due to increasing addon comm throttling in instances, guild version sharing is disabled in instances to reduce comms if guild and not IsInInstance() then local message if not private.isRetail and DBM.classicSubVersion then message = ("%s\t%s\t%s\t%s\t%s"):format(tostring(DBM.Revision), tostring(DBM.ReleaseRevision), DBM.DisplayVersion, tostring(PForceDisable), tostring(DBM.classicSubVersion)) sendGuildSync(3, "GV", message) else message = ("%s\t%s\t%s\t%s"):format(tostring(DBM.Revision), tostring(DBM.ReleaseRevision), DBM.DisplayVersion, tostring(PForceDisable)) sendGuildSync(2, "GV", message) end return end if DBM.Options.FakeBWVersion and not dbmIsEnabled and not IsTrialAccount() 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 private.voiceSessionDisabled and VoicePack ~= "None" and DBM.VoiceVersions[VoicePack] then VPVersion = "/ VP" .. VoicePack .. ": v" .. DBM.VoiceVersions[VoicePack] end if VPVersion then sendSync(3, "V", ("%s\t%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), tostring(DBM.classicSubVersion or 0), VPVersion)) else sendSync(3, "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), tostring(DBM.classicSubVersion or 0))) end end local function HandleVersion(revision, version, displayVersion, forceDisable, sender, classicSubVers) if version > DBM.Revision then -- Update reminder --Core Version Handling 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 private.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 private.testBuild then updateNotificationDisplayed = 3 DBM:ForceDisableSpam() DBM:Disable(true) end end end end if not private.isRetail and type(classicSubVers) == 'number' and classicSubVers > DBM.classicSubVersion then -- Update reminder if #newersubVersionPerson < 4 then if not checkEntry(newersubVersionPerson, sender) then newersubVersionPerson[#newersubVersionPerson + 1] = sender DBM:Debug("Newer classic subversion detected from " .. sender .. " : Rev - " .. classicSubVers .. ", Rev Diff - " .. (classicSubVers - DBM.classicSubVersion), 3) end if #newersubVersionPerson == 2 and updateSubNotificationDisplayed < 2 then--Only requires 2 for update notification. updateSubNotificationDisplayed = 2 local checkedSubmodule = private.isCata and "DBM-Raids-Cata" or private.isWrath and "DBM-Raids-WoTLK" or private.isBCC and "DBM-Raids-BC" or "DBM-Raids-Vanilla" AddMsg(DBM, L.UPDATEREMINDER_HEADER_SUBMODULE:match("\n(.*)"):format(checkedSubmodule, classicSubVers)) showConstantReminder = 1 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, classicSubVers, VPVersion) revision, version, classicSubVers = tonumber(revision), tonumber(version), tonumber(classicSubVers) if protocol >= 3 then --Nil it out on retail, replace with string on classic versions if classicSubVers and classicSubVers == 0 then if private.isRetail then classicSubVers = nil else classicSubVers = L.MOD_MISSING end end forceDisable = tonumber(forceDisable) or 0 elseif protocol >= 2 then --Protocol 2 did not send classicSubVers VPVersion = classicSubVers 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 if not private.isRetail then raid[sender].classicSubVers = classicSubVers end 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, classicSubVers) end DBM:GROUP_ROSTER_UPDATE() end guildSyncHandlers["GV"] = function(sender, _, revision, version, displayVersion, forceDisable, classicSubVers) revision, version, forceDisable, classicSubVers = tonumber(revision), tonumber(version), tonumber(forceDisable) or 0, tonumber(classicSubVers) --Nil it out on retail, replace with string on classic versions if classicSubVers and classicSubVers == 0 then if private.isRetail then classicSubVers = nil else classicSubVers = L.MOD_MISSING end end 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, classicSubVers) 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 difficulties.difficultyIndex == 7 or difficulties.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 difficulties.difficultyIndex == 7 or difficulties.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(private.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 not private.isClassic and not private.isBCC 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(private.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 not private.isClassic and not private.isBCC 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 private.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 private.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 if DBM:IsSeasonal("SeasonOfDiscovery") then -- All World Buffs are spammy in SoD, disable return end if not protocol or protocol ~= 4 or private.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 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 (private.isRetail and not private.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 private.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 if not protocol or protocol ~= 4 or private.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 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["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 function DBM:START_PLAYER_COUNTDOWN(initiatedByGuid, timeSeconds, _, _, initiatedByName)--totalTime, informChat --Ignore this event in combat if #inCombat > 0 then return end -- if timeSeconds > 60 then--treat as a break timer -- breakTimerStart(self, timeSeconds, initiatedBy, true) -- else--Treat as a pull timer --In TWW, initiatedByName is in a diff place. We solve this by simply checking new location cause that'll be nil on live pullTimerStart(self, initiatedByName or initiatedByGuid, timeSeconds, true) -- end end function DBM:CANCEL_PLAYER_COUNTDOWN(initiatedByGuid, _, initiatedByName)--informChat --when CANCEL_PLAYER_COUNTDOWN is called by ENCOUNTER_START, sender is nil -- breakTimerStart(self, 0, initiatedBy, true) --In TWW, initiatedByName is in a diff place. We solve this by simply checking new location cause that'll be nil on live pullTimerStart(self, initiatedByName or initiatedByGuid, 0, true) 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 ---@param mod DBMMod 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 ---@param combatInfo CombatInfo 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 private.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 private.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 private.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 private.isRetail then ObjectiveTracker_Expand() elseif private.isCata or private.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 --Only nag in raids on engage if IsInRaid() then self:CheckAvailableMods() end 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) if success == 0 then --Only nag on wipes (in any content) self:CheckAvailableMods() end 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), private.isRetail and 237538 or 136106)--Interface\\Icons\\Spell_Holy_BorrowedTime, Spell_nature_timestop fireEvent("DBM_TimerStart", "DBMRespawnTimer", L.TIMER_RESPAWN:format(name), v.respawnTime, private.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") if self:AntiSpam(3, "EE") then--Most bosses have both BOSS_KILL and ENCOUNTER_END, we don't want to send two EE syncs if we don't have to sendSync(DBMSyncProtocol, "EE", encounterID .. "\t" .. success .. "\t" .. v.id .. "\t" .. (v.revision or 0)) end return end end elseif encounterID == v.combatInfo.eId then self:EndCombat(v, success == 0, nil, "ENCOUNTER_END") if self:AntiSpam(3, "EE") then--Most bosses have both BOSS_KILL and ENCOUNTER_END, we don't want to send two EE syncs if we don't have to sendSync(DBMSyncProtocol, "EE", encounterID .. "\t" .. success .. "\t" .. v.id .. "\t" .. (v.revision or 0)) end 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") if self:AntiSpam(3, "EE") then--Most bosses have both BOSS_KILL and ENCOUNTER_END, we don't want to send two EE syncs if we don't have to sendSync(DBMSyncProtocol, "EE", encounterID .. "\t1\t" .. v.id .. "\t" .. (v.revision or 0)) end return end end elseif encounterID == v.combatInfo.eId then self:EndCombat(v, nil, nil, "BOSS_KILL") if self:AntiSpam(3, "EE") then--Most bosses have both BOSS_KILL and ENCOUNTER_END, we don't want to send two EE syncs if we don't have to sendSync(DBMSyncProtocol, "EE", encounterID .. "\t1\t" .. v.id .. "\t" .. (v.revision or 0)) end 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 ---@param self DBM ---@param type string ---@param msg string 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 >= private.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 private.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 private.isRetail and not IsInInstance() then if DBM:IsSeasonal("SeasonOfDiscovery") then -- All World Buffs are spammy in SoD, disable return end if msg:find(L.WORLD_BUFFS.hordeOny) then SendWorldSync(self, 4, "WBA", "Onyxia\tHorde\t22888\t15\t4") elseif msg:find(L.WORLD_BUFFS.allianceOny) then SendWorldSync(self, 4, "WBA", "Onyxia\tAlliance\t22888\t15\t4") elseif msg:find(L.WORLD_BUFFS.hordeNef) then SendWorldSync(self, 4, "WBA", "Nefarian\tHorde\t22888\t16\t4") elseif msg:find(L.WORLD_BUFFS.allianceNef) then SendWorldSync(self, 4, "WBA", "Nefarian\tAlliance\t22888\t16\t4") elseif msg:find(L.WORLD_BUFFS.rendHead) then SendWorldSync(self, 4, "WBA", "rendBlackhand\tHorde\t16609\t7\t4") 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") 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") elseif msg:find(L.WORLD_BUFFS.blackfathomBoon) then --SendWorldSync(self, 4, "WBA", "Blackfathom\tBoth\t430947\t6\t4") 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:GetSpellName(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"] and not IsTrialAccount() 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(", ", tostring(gossipOptionID)) .. " on creatureID " .. cid) end end function DBM:CHAT_MSG_MONSTER_SAY(msg) if not private.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 -- --------------------------- do local lastValidCombat = 0 ---@param self DBM ---@param confirm boolean? ---@param confirmTime number? function checkWipe(self, confirm, confirmTime) if #inCombat > 0 then difficulties:RefreshCache() --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 (private.isRetail and IsInScenarioGroup()) or (difficulties.difficultyIndex == 11) or (difficulties.difficultyIndex == 12) or (difficulties.difficultyIndex == 208) then -- Scenario mod uses special combat start and must be enabled before sceniro end. So do not wipe. wipe = 0 elseif private.IsEncounterInProgress() then -- Encounter Progress marked, you obviously in combat with boss. So do not Wipe wipe = 0 elseif difficulties.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 lastValidCombat = GetTime()--Time stamp last valid in combat self:Schedule(3, checkWipe, self) elseif confirm then local timeSinceValid = GetTime() - lastValidCombat if timeSinceValid > confirmTime 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--Have not reached required out of combat time yet, check again every 3 seconds until we do self:Schedule(3, checkWipe, self, true, confirmTime) end else local maxDelayTime = (difficulties.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(3, checkWipe, self, true, maxDelayTime) end end end ---@param self DBM ---@param mod DBMMod function checkBossHealth(self, mod) 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, mod.onlyHighest) else for _, mob in ipairs(v.multiMobPullDetection) do self:GetBossHP(mob, mod.onlyHighest) end end end self:Schedule(mod.bossHealthUpdateTime or 1, checkBossHealth, self, mod) end end ---@param self DBM ---@param mod DBMMod function checkCustomBossHealth(self, mod) mod:CustomHealthUpdate() self:Schedule(mod.bossHealthUpdateTime or 1, checkCustomBossHealth, self, mod) end 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", ["quest"] = "follower",--For now, unless a conflict arises ["follower"] = "follower", ["story"] = "story", ["normal5"] = "normal", ["heroic5"] = "heroic", ["challenge5"] = "challenge", ["lfr"] = "lfr25", ["normal"] = "normal", ["heroic"] = "heroic", ["mythic"] = "mythic", ["mythic5"] = "mythic", ["worldboss"] = "normal", ["timewalker"] = "timewalker", ["progressivechallenges"] = "normal", ["delves"] = "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", } ---@param mod DBMMod ---@param delay number ---@param event string? ---@param synced boolean? ---@param syncedStartHp number? ---@param syncedEvent string? function DBM:StartCombat(mod, delay, event, synced, syncedStartHp, syncedEvent) ---@class DBMMod mod = mod 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 test:Trace(mod, "StartCombat", event) tinsert(inCombat, mod) -- Pull time is always considered as in combat, this makes sure checkWipe() triggers only after the minimum time without combat has passed since start. lastValidCombat = GetTime() 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 difficulties:RefreshCache(true) local name = mod.combatInfo.name local modId = mod.id if private.isRetail then if mod.addon.type == "SCENARIO" and C_Scenario.IsInScenario() and not mod.soloChallenge then mod.inScenario = true end end mod.engagedDiff = difficulties.savedDifficulty mod.engagedDiffText = difficulties.difficultyText mod.engagedDiffIndex = difficulties.difficultyIndex mod.inCombat = true ---@class CombatInfo local combatInfo = mod.combatInfo 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 (difficulties.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 scenarioType, currentStage, numStages = C_Scenario.GetInfo() --Delves start in stage 2 of 3 because stage 1 is "entering" apparently. if currentStage > (scenarioType == "Delves" and 2 or 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 (difficulties.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) self.Options.RestoreSettingSFX = true end if self.Options.DisableAmbiance and GetCVar("Sound_EnableAmbience") == "1" then SetCVar("Sound_EnableAmbience", 0) self.Options.RestoreSettingAmbiance = true end if self.Options.DisableMusic and GetCVar("Sound_EnableMusic") == "1" then SetCVar("Sound_EnableMusic", 0) self.Options.RestoreSettingMusic = true end --boss health info scheduler if mod.CustomHealthUpdate then self:Schedule(mod.bossHealthUpdateTime or 1, checkCustomBossHealth, self, mod) else self:Schedule(mod.bossHealthUpdateTime or 1, checkBossHealth, self, mod) end end --process global options self:HideBlizzardEvents(1) if self.Options.RecordOnlyBosses then self:StartLogging(0) end local trackedAchievements if private.isClassic or private.isBCC then trackedAchievements = false elseif private.isWrath or private.isCata 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 difficulties.difficultyIndex ~= 8 and not InCombatLockdown() then if private.isRetail then--Do nothing due to taint and breaking --if ObjectiveTrackerFrame:IsVisible() then -- ObjectiveTracker_Collapse() -- watchFrameRestore = true --end else if WatchFrame 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[difficulties.savedDifficulty] .. "Pulls"] then mod.stats[statVarTable[difficulties.savedDifficulty] .. "Pulls"] = 0 end mod.stats[statVarTable[difficulties.savedDifficulty] .. "Pulls"] = mod.stats[statVarTable[difficulties.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 difficulties.difficultyIndex == 8 or difficulties.difficultyIndex == 208 then--Mythic+/Challenge Mode and Delves local bestMPRank = mod.stats.challengeBestRank or 0 if bestMPRank == difficulties.difficultyModifier then --Don't show speed kill timer if not our highest rank. DBM only stores highest rank bestTime = mod.stats[statVarTable[difficulties.savedDifficulty] .. "BestTime"] end else bestTime = mod.stats[statVarTable[difficulties.savedDifficulty] .. "BestTime"] end if bestTime and bestTime > 0 then local speedTimer = mod:NewTimer(bestTime, L.SPEED_KILL_TIMER_TEXT, private.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 difficulties.difficultyIndex == 14 or difficulties.difficultyIndex == 15 or difficulties.difficultyIndex == 16 or difficulties.difficultyIndex == 175 or difficulties.difficultyIndex == 176 or difficulties.difficultyIndex == 186 or difficulties.difficultyIndex == 193 or difficulties.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 ---@diagnostic disable-next-line: undefined-field consumables:OutputResults() end end end --show engage message if self.Options.ShowEngageMessage and not mod.noStatistics then if mod.ignoreBestkill and (difficulties.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(difficulties.difficultyText .. name)) elseif mod.ignoreBestkill and mod.inScenario then self:AddMsg(L.SCENARIO_STARTED_IN_PROGRESS:format(difficulties.difficultyText .. name)) else if mod.addon.type == "SCENARIO" then self:AddMsg(L.SCENARIO_STARTED:format(difficulties.difficultyText .. name)) else self:AddMsg(L.COMBAT_STARTED:format(difficulties.difficultyText .. name)) local check = not private.statusGuildDisabled and (private.isRetail and ((difficulties.difficultyIndex == 8 or difficulties.difficultyIndex == 14 or difficulties.difficultyIndex == 15 or difficulties.difficultyIndex == 16) and InGuildParty()) or difficulties.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(private.isRetail and 1.5 or 3, delayedGCSync, modId, difficulties.difficultyIndex, difficulties.difficultyModifier, name) end end end end --stop pull count ---@class DBMDummyMod: DBMMod local dummyMod = self:GetModByName("PullTimerCountdownDummy") if dummyMod then--stop pull timer dummyMod.text:Cancel() dummyMod.timer:Stop() 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 (difficulties.savedDifficulty == "mythic" or difficulties.savedDifficulty == "challenge")) and not mod.noStatistics and not self.Options.RestoreSettingMusic then fireEvent("DBM_MusicStart", "BossEncounter") if not self.Options.RestoreSettingCustomMusic then self.Options.RestoreSettingCustomMusic = tonumber(GetCVar("Sound_EnableMusic")) or 1 if self.Options.RestoreSettingCustomMusic == 0 then SetCVar("Sound_EnableMusic", 1) else self.Options.RestoreSettingCustomMusic = nil--Don't actually need it end end local path = "MISSING" if self.Options.EventSoundMusic == "Random" then local usedTable = self.Options.EventSoundMusicCombined and self:GetMusic() or mod.inScenario and self:GetDungeonMusic() or self:GetBattleMusic() 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(difficulties.difficultyText .. name, stringUtils.strFromTime(delay))) if mod.OnTimerRecovery then mod:OnTimerRecovery() end end if difficulties.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 < (private.isHardcoreServer and 95 or 85)) and not private.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 private.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 ---@param mod DBMMod ---@param wipe boolean? ---@param srmIncluded boolean? unregister all events including SPELL_AURA_REMOVED events ---@param event string? function DBM:EndCombat(mod, wipe, srmIncluded, event) ---@class DBMMod mod = mod if removeEntry(inCombat, mod) then test:Trace(mod, "EndCombat", event) local scenario = mod.addon.type == "SCENARIO" and not mod.soloChallenge if mod.inCombatOnlyEvents and mod.inCombatOnlyEventsRegistered then if srmIncluded then mod:UnregisterInCombatEvents(false, true) else 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 difficulties:RefreshCache(true) --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 difficulties.savedDifficulty local usedDifficultyText = mod.engagedDiffText or difficulties.difficultyText local usedDifficultyIndex = mod.engagedDiffIndex or difficulties.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, stringUtils.strFromTime(thisTime))) else self:AddMsg(L.COMBAT_ENDED_AT:format(usedDifficultyText .. name, wipeHP, stringUtils.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, stringUtils.strFromTime(thisTime), totalPulls - totalKills)) else self:AddMsg(L.COMBAT_ENDED_AT_LONG:format(usedDifficultyText .. name, wipeHP, stringUtils.strFromTime(thisTime), totalPulls - totalKills)) local check = private.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(private.isRetail and 1.5 or 3, delayedGCSync, modId, usedDifficultyIndex, difficulties.difficultyModifier, name, stringUtils.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 or usedDifficultyIndex == 208 then--Mythic+/Challenge Mode if mod.stats.challengeBestRank > difficulties.difficultyModifier then--Don't save time stats at all --DO nothing elseif mod.stats.challengeBestRank < difficulties.difficultyModifier then--Update best time and best rank, even if best time is lower (for a lower rank) mod.stats.challengeBestRank = difficulties.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 stringUtils.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, stringUtils.strFromTime(bestTime), totalKills) else msg = L.BOSS_DOWN_NR:format(usedDifficultyText .. name, thisTimeString, stringUtils.strFromTime(bestTime), totalKills) end else if scenario then msg = L.SCENARIO_COMPLETE_L:format(usedDifficultyText .. name, thisTimeString, stringUtils.strFromTime(lastTime), stringUtils.strFromTime(bestTime), totalKills) else msg = L.BOSS_DOWN_L:format(usedDifficultyText .. name, thisTimeString, stringUtils.strFromTime(lastTime), stringUtils.strFromTime(bestTime), totalKills) end end local check = not private.statusGuildDisabled and (private.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(private.isRetail and 1.5 or 3, delayedGCSync, modId, usedDifficultyIndex, difficulties.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 or false) 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() if not InCombatLockdown() then if watchFrameRestore then if private.isRetail then --ObjectiveTracker_Expand() elseif private.isCata or private.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.RestoreSettingSFX then SetCVar("Sound_EnableSFX", 1) self.Options.RestoreSettingSFX = nil end if self.Options.RestoreSettingAmbiance then SetCVar("Sound_EnableAmbience", 1) self.Options.RestoreSettingAmbiance = nil end if self.Options.RestoreSettingMusic then SetCVar("Sound_EnableMusic", 1) self.Options.RestoreSettingMusic = nil 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(0, "", 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 function DBM:StartLogging(timer, checkFunc, force) self:Unschedule(DBM.StopLogging) if self:IsLogableContent(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, } --In event api fails to pull any data at all, just assign classes to generic DPS role (typically unspecced players such as sub level 11) local catafallbackClassToRole = { ["MAGE"] = 799,--Arcane Mage ["PALADIN"] = 855,--Ret Paladin ["WARRIOR"] = 746,--Arms Warrior ["DRUID"] = 752,--Balance druid ["DEATHKNIGHT"] = 399,--Frost DK ["HUNTER"] = 811,--Beastmaster Hunter ["PRIEST"] = 795,--Shadow Priest ["ROGUE"] = 182,--Assassination Rogue ["SHAMAN"] = 263,--Enhancement Shaman ["WARLOCK"] = 871,--Affliction Warlock } function DBM:SetCurrentSpecInfo() if private.isRetail then currentSpecGroup = GetSpecialization() if currentSpecGroup and GetSpecializationInfo(currentSpecGroup) then currentSpecID, currentSpecName = GetSpecializationInfo(currentSpecGroup) currentSpecID = tonumber(currentSpecID) else currentSpecID, currentSpecName = fallbackClassToRole[playerClass], playerClass--give temp first spec id for non-specialization char. no one should use dbm with no specialization, below level 10, should not need dbm. end DBM:Debug("Current specID set to: "..currentSpecID, 2) elseif private.isCata then currentSpecGroup = GetPrimaryTalentTree() if currentSpecGroup and GetTalentTabInfo(currentSpecGroup) then currentSpecID, currentSpecName = GetTalentTabInfo(currentSpecGroup) currentSpecID = tonumber(currentSpecID) else currentSpecID, currentSpecName = catafallbackClassToRole[playerClass], playerClass--give temp first spec id for non-specialization char. no one should use dbm with no specialization, below level 10, should not need dbm. end DBM:Debug("Current specID set to: "..currentSpecID, 2) else local numTabs = GetNumTalentTabs() local highestPointsSpent = 0 if MAX_TALENT_TABS then for i = 1, MAX_TALENT_TABS do if i <= numTabs then local _, _, wrathPointsSpent, _, pointsSpent = GetTalentTabInfo(i)--specID, specName will be used in next update once era spec table rebuilt local usedPoints = private.isWrath and wrathPointsSpent or pointsSpent if usedPoints > highestPointsSpent then highestPointsSpent = usedPoints 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 if not InCombatLockdown() and currentSpecID and not private.specRoleTable[currentSpecID] then --Refresh entire spec table if not in combat and it's still missing for some reason DBMExtraGlobal:rebuildSpecTable() end end end function DBM:GetCurrentArea() return LastInstanceMapID 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 ---@param self DBMModOrDBM 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 = C_AddOns.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 test:Trace(self, "PlaySound", path) end end ---Future proofing EJ_GetSectionInfo compat layer to make it easier updatable. function DBM:EJ_GetSectionInfo(sectionID)--Should be number, but accepts string too since Blizzards api converts strings to number. if not sectionID then return end if private.isClassic or private.isBCC or private.isWrath then return "EJ_GetSectionInfo not supported on Classic, please report this message and boss" end --Built in wow api extension doesn't know EJ_GetSectionInfo can accept strings 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 --Built in wow api extension doesn't know EJ_GetSectionInfo can accept strings 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 tostring(temp) end do --Handle new spell name requesting with wrapper, to make api changes easier to handle local GetSpellInfo, GetSpellTexture, GetSpellCooldown, GetSpellName local newPath if C_Spell and C_Spell.GetSpellInfo then newPath = true GetSpellInfo, GetSpellTexture, GetSpellCooldown, GetSpellName = C_Spell.GetSpellInfo, C_Spell.GetSpellTexture, C_Spell.GetSpellCooldown, C_Spell.GetSpellName else newPath = false GetSpellInfo, GetSpellTexture, GetSpellCooldown = _G.GetSpellInfo, _G.GetSpellTexture, _G.GetSpellCooldown end ---Wrapper for Blizzard GetSpellInfo global that converts new table returns to old arg returns ---
This avoids having to significantly update nearly 20 years of boss mods. ---@param spellId string|number Should be number, but accepts string too since Blizzards api converts strings to number. 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 if newPath then local spellTable = GetSpellInfo(spellId) if spellTable then ---@diagnostic disable-next-line: undefined-field name, rank, icon, castingTime, minRange, maxRange, returnedSpellId = spellTable.name, nil, spellTable.iconID, spellTable.castTime, spellTable.minRange, spellTable.maxRange, spellTable.spellID end else 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 end return name, rank, icon, castingTime, minRange, maxRange, returnedSpellId end ---@param spellId string|number --Should be number, but accepts string too since Blizzards api converts strings to number. function DBM:GetSpellTexture(spellId) if not spellId then return end--Unlike 10.x and older, 11.x now errors if called without a spellId --Doesn't need a table at this time local texture --if newPath then -- local spellTable = GetSpellTexture(spellId) -- if spellTable then -- texture = spellTable.texture -- end --else texture = GetSpellTexture(spellId) --end return texture end ---Wrapper for Blizzard GetSpellName global that auto handles using GetSpellName on newer clients and GetSpellInfo for older ones ---@param spellId string|number --Should be number, but accepts string too since Blizzards api converts strings to number. function DBM:GetSpellName(spellId) if not spellId then return end--Unlike 10.x and older, 11.x now errors if called without a spellId local spellName if newPath then--Use spellname only function, avoid pulling entire spellinfo table if not needed spellName = GetSpellName(spellId) else spellName = self:GetSpellInfo(spellId) end return spellName end ---Wrapper for Blizzard GetSpellCooldown global that converts new table returns to old arg returns ---
This avoids having to significantly update nearly 20 years of boss mods. ---@param spellId string|number --Should be number, but accepts string too since Blizzards api converts strings to number. ---@return number, number, number function DBM:GetSpellCooldown(spellId) local start, duration, enable if newPath then local spellTable = GetSpellCooldown(spellId) if spellTable then ---@diagnostic disable-next-line: undefined-field start, duration, enable = spellTable.startTime, spellTable.duration, spellTable.isEnabled end else start, duration, enable = GetSpellCooldown(spellId) end return start, duration, enable end end do local UnitAura = C_UnitAuras and C_UnitAuras.GetAuraDataByIndex or UnitAura local GetPlayerAuraBySpellID = C_UnitAuras and C_UnitAuras.GetPlayerAuraBySpellID local GetAuraDataBySpellName = C_UnitAuras and C_UnitAuras.GetAuraDataBySpellName local newUnitAuraAPIs = C_UnitAuras and C_UnitAuras.GetAuraDataBySpellName and true--Purposely separate from GetAuraDataBySpellName upvalue because I don't want to spam check if a function exists in such a frequent API call ---Custom UnitAura wrapper that can check spells by spellID or spell name for up to 5 spells at once ---@param uId string ---@param spellInput number|string|nil|unknown --required, accepts spellname or spellid ---@param spellInput2 number|string|nil|unknown? --optional 2nd spell, accepts spellname or spellid ---@param spellInput3 number|string|nil|unknown? --optional 3rd spell, accepts spellname or spellid ---@param spellInput4 number|string|nil|unknown? --optional 4th spell, accepts spellname or spellid ---@param spellInput5 number|string|nil|unknown? --optional 5th spell, accepts spellname or spellid function DBM:UnitAura(uId, spellInput, spellInput2, spellInput3, spellInput4, spellInput5) if not uId then return end if private.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 = 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 if newUnitAuraAPIs then if type(spellInput) == "string" and not spellInput2 then--A simple single spellName check should use more efficent direct blizzard method local spellTable = GetAuraDataBySpellName(uId, 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, or a single spell id check on non player unit (C_UnitAuras.GetPlayerAuraBySpellID is unavailable) for i = 1, 60 do local spellTable = UnitAura(uId, i) if not spellTable then return end if spellInput == spellTable.name or spellInput == spellTable.spellId or spellInput2 == spellTable.name or spellInput2 == spellTable.spellId or spellInput3 == spellTable.name or spellInput3 == spellTable.spellId or spellInput4 == spellTable.name or spellInput4 == spellTable.spellId or spellInput5 == spellTable.name or spellInput5 == spellTable.spellId then 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 end end end else 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 --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) return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod, value1, value2, value3 end end end end end ---Custom UnitDebuff wrapper that can check spells by spellID or spell name for up to 5 spells at once ---@param uId string ---@param spellInput number|string|nil|unknown --required, accepts spellname or spellid ---@param spellInput2 number|string|nil|unknown? --optional 2nd spell, accepts spellname or spellid ---@param spellInput3 number|string|nil|unknown? --optional 3rd spell, accepts spellname or spellid ---@param spellInput4 number|string|nil|unknown? --optional 4th spell, accepts spellname or spellid ---@param spellInput5 number|string|nil|unknown? --optional 5th spell, accepts spellname or spellid function DBM:UnitDebuff(uId, spellInput, spellInput2, spellInput3, spellInput4, spellInput5) if not uId then return end if private.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 = 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 if newUnitAuraAPIs then if type(spellInput) == "string" and not spellInput2 then--A simple single spellName check should use more efficent direct blizzard method local spellTable = GetAuraDataBySpellName(uId, spellInput, "HARMFUL") 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, or a single spell id check on non player unit (C_UnitAuras.GetPlayerAuraBySpellID is unavailable) for i = 1, 60 do local spellTable = UnitAura(uId, i, "HARMFUL") if not spellTable then return end if spellInput == spellTable.name or spellInput == spellTable.spellId or spellInput2 == spellTable.name or spellInput2 == spellTable.spellId or spellInput3 == spellTable.name or spellInput3 == spellTable.spellId or spellInput4 == spellTable.name or spellInput4 == spellTable.spellId or spellInput5 == spellTable.name or spellInput5 == spellTable.spellId then 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 end end end else 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, "HARMFUL") 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 --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) return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod, value1, value2, value3 end end end end end ---Custom UnitBuff wrapper that can check spells by spellID or spell name for up to 5 spells at once ---@param uId string ---@param spellInput number|string|nil|unknown --required, accepts spellname or spellid ---@param spellInput2 number|string|nil|unknown? --optional 2nd spell, accepts spellname or spellid ---@param spellInput3 number|string|nil|unknown? --optional 3rd spell, accepts spellname or spellid ---@param spellInput4 number|string|nil|unknown? --optional 4th spell, accepts spellname or spellid ---@param spellInput5 number|string|nil|unknown? --optional 5th spell, accepts spellname or spellid function DBM:UnitBuff(uId, spellInput, spellInput2, spellInput3, spellInput4, spellInput5) if not uId then return end if private.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 = 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 if newUnitAuraAPIs then if type(spellInput) == "string" and not spellInput2 then--A simple single spellName check should use more efficent direct blizzard method local spellTable = GetAuraDataBySpellName(uId, spellInput, "HELPFUL") 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, or a single spell id check on non player unit (C_UnitAuras.GetPlayerAuraBySpellID is unavailable) for i = 1, 60 do local spellTable = UnitAura(uId, i, "HELPFUL") if not spellTable then return end if spellInput == spellTable.name or spellInput == spellTable.spellId or spellInput2 == spellTable.name or spellInput2 == spellTable.spellId or spellInput3 == spellTable.name or spellInput3 == spellTable.spellId or spellInput4 == spellTable.name or spellInput4 == spellTable.spellId or spellInput5 == spellTable.name or spellInput5 == spellTable.spellId then 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 end end end else 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, "HELPFUL") 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 --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) return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod, value1, value2, value3 end end 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 private.isHardcoreServer and self.Options.AFKHealthWarning2 and GUID == UnitGUID("player") and not private.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 private.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", 3) 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 ---@param mod DBMMod 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 ---@param mod DBMMod 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 ---@param mod DBMMod 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 or IsTrialAccount() 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 ---@param mod DBMMod function DBM:SendCombatInfo(mod, target) if not dbmIsEnabled or IsTrialAccount() then return end return SendAddonMessage(DBMPrefix, (DBMSyncProtocol .. "\tCI\t%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target) end ---@param mod DBMMod function DBM:SendTimerInfo(mod, target) if not dbmIsEnabled or IsTrialAccount() 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 ---@param mod DBMMod function DBM:SendVariableInfo(mod, target) if not dbmIsEnabled or IsTrialAccount() 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 ------------------------------------ -- 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 local function getNumRealAlivePlayers() local alive = 0 local isInInstance = IsInInstance() local currentMapId = isInInstance and select(-1, 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(-1, 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(-1, 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 difficulties:RefreshCache() 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 private.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(difficulties.difficultyText .. (mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers())) elseif #inCombat > 0 and DBM.Options.AutoRespond then difficulties:RefreshCache() 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 private.isRetail and not mod.soloChallenge and IsInScenarioGroup() then sendWhisper(sender, chatPrefixShort .. L.AUTO_RESPOND_WHISPER_SCENARIO:format(playerName, difficulties.difficultyText .. (mod.combatInfo.name or ""), getNumAlivePlayers(), DBM:GetNumGroupMembers())) else sendWhisper(sender, chatPrefixShort .. L.AUTO_RESPOND_WHISPER:format(playerName, difficulties.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 private.testBuild then DisableEvent(RaidBossEmoteFrame, "RAID_BOSS_EMOTE") DisableEvent(RaidBossEmoteFrame, "RAID_BOSS_WHISPER") DisableEvent(RaidBossEmoteFrame, "CLEAR_BOSS_EMOTES") SOUNDKIT.UI_RAID_BOSS_WHISPER_WARNING = 999999--Since blizzard can still play the sound via RaidBossEmoteFrame_OnEvent (line 148) via encounter scripts in certain cases despite the frame having no registered events end if self.Options.HideGarrisonToasts or custom then DisableEvent(AlertFrame, "GARRISON_MISSION_FINISHED") DisableEvent(AlertFrame, "GARRISON_BUILDING_ACTIVATABLE") end if self.Options.HideGuildChallengeUpdates or custom then DisableEvent(AlertFrame, "GUILD_CHALLENGE_COMPLETED") end elseif toggle == 0 then if (self.Options.HideBossEmoteFrame2 or custom) and not private.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) for _, mod in ipairs(inCombat) do -- force combat end if anything is active because :Unschedule below breaks wipe detection leaving you in a weird state DBM:EndCombat(mod, true, true, "DBM:Disable() called") end DBMScheduler:Unschedule() 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 private.testBuild then DBM:AddMsg(L.UPDATEREMINDER_DISABLETEST) else DBM:AddMsg(L.UPDATEREMINDER_DISABLE) end end end ----------------------- -- Misc. Functions -- ----------------------- ---@param self DBMModOrDBM function DBM:AddMsg(text, prefix, useSound, allowHiddenChatFrame, isDebug) ---@diagnostic disable-next-line: undefined-field 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() and not allowHiddenChatFrame 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 if DBM.Options.DebugSound and isDebug then DBM:PlaySoundFile(567458)--"Ding" end if useSound then DBM:PlaySoundFile(DBM.Options.RaidWarningSound, nil, true) 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 ---@class DBMModTestMod: DBMMod 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, private.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 ", private.isRetail and "136194" or "136221", nil, nil, 3, CL.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, CL.INTERRUPT_ICON) testTimer5 = testMod:NewTimer(20, "%s ", "135826", nil, nil, 2, CL.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, CL.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 = FACTION_HORDE elseif bar.color and bar.color.r == 0 and bar.color.g == 0 and bar.color.b == 1 then prefix = 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 role if private.isRetail then local spec = GetSpecialization() if not spec then return end role = GetSpecializationRole(spec) if not role then return end local specID = GetLootSpecialization() local _, _, _, _, lootrole = GetSpecializationInfoByID(specID) --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 else--Cata if not currentSpecID then DBM:SetCurrentSpecInfo() end if currentSpecID and private.specRoleTable[currentSpecID] then if private.specRoleTable[currentSpecID]["Healer"] then role = "HEALER" elseif private.specRoleTable[currentSpecID]["Tank"] then role = "TANK" else role = "DAMAGER" end else--By some miracle, despite excessive redundancy, it still failed because classic spagetti code. role = "DAMAGER"--Set default role (which is what cataclysm does btw, it sets everyone to damager on raid join unless changed by our mod) end end if not InCombatLockdown() and not IsFalling() and ((IsPartyLFG() and (difficulties.difficultyIndex == 14 or difficulties.difficultyIndex == 15)) or not IsPartyLFG()) then if UnitGroupRolesAssigned("player") ~= role then UnitSetRole("player", role) end end end -- An anti spam function to throttle spammy events (e.g. SPELL_AURA_APPLIED on all group members) ---@param self DBMModOrDBM ---@param time number? time to wait between two events (optional, default 2.5 seconds) ---@param id any? 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 test:Trace(self, "AntiSpam", id, true) return true end test:Trace(self, "AntiSpam", id, false) 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,} ---@param self DBMModOrDBM function DBM:IconNumToString(number) return iconStrings[number] or number end ---@param self DBMModOrDBM function DBM:IconNumToTexture(number) return "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_" .. number .. ".blp:12:12|t" or number 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 } local requiresRecentKill = { [2238] = 2519--Fyrakk in Amirdrassil } ---@param self DBM local function checkOptions(self, id, mapID) --First, check if this specific cut scene should be blocked at all via the 3 primary rules local allowBlock = false if self.Options.HideMovieDuringFight and private.IsEncounterInProgress() then allowBlock = true end local isInstance, instanceType = IsInInstance() --HideMovieInstanceAnywhere means only dunegons and raids. NOT scenarios, and NOT garrisons. Delves will probably be added to this when api known if self.Options.HideMovieInstanceAnywhere and isInstance and instanceType ~= "scenario" and not (C_Garrison and C_Garrison:IsOnGarrisonMap()) then allowBlock = true end --HideMovieNonInstanceAnywhere means any outdoor area, which means anywhere non instanced or the garrison. --Scenarios are once again excluded, per deal with blizzard, since Legion --(scenarios ofen used for story events like broken shore and blizzard doesn't want addons skipping these) if self.Options.HideMovieNonInstanceAnywhere and instanceType ~= "scenario" and (not isInstance or (C_Garrison and C_Garrison:IsOnGarrisonMap())) then allowBlock = true end --Check for cinematics that should only be blocked if boss just died or was just pulled if mapID and requiresRecentKill[mapID] and allowBlock then local modID = requiresRecentKill[mapID] local mod = DBM:GetModByName(modID) if mod and mod.lastKillTime and (GetTime() - mod.lastKillTime) > 5 then allowBlock = false end end --Last check if seen yet and if seen once filter enabled, abort after flagging seen once if allowBlock and self.Options.HideMovieOnlyAfterSeen and not self.Options.MoviesSeen[id] then self.Options.MoviesSeen[id] = true allowBlock = false end return allowBlock end function DBM:PLAY_MOVIE(id) --Stop custom BG music during cut scenes regardless of block features self:TransitionToDungeonBGM(false, true) if id and not neverFilter[id] then self:Debug("PLAY_MOVIE fired for ID: " .. id, 2) if checkOptions(self, id) then MovieFrame:Hide()--can only just hide movie frame safely now, which means can't stop audio anymore :\ self:AddMsg(L.MOVIE_SKIPPED) end end end function DBM:CINEMATIC_START() self:Debug("CINEMATIC_START fired", 2) --Stop custom BG music during cut scenes regardless of block features self:TransitionToDungeonBGM(false, true) self.HudMap:SupressCanvas() local currentMapID = C_Map.GetBestMapForUnit("player") local currentSubZone = GetSubZoneText() or "" --Just abort if map is nil, don't want to touch it if can't map cinematic to an area if currentMapID and checkOptions(self, currentMapID .. currentSubZone, currentMapID) then CinematicFrame_CancelCinematic() self:AddMsg(L.MOVIE_SKIPPED) -- self:AddMsg(L.MOVIE_NOTSKIPPED) end end function DBM:CINEMATIC_STOP() self:Debug("CINEMATIC_STOP fired", 2) self.HudMap:UnSupressCanvas() end end ----------------------- -- Utility Methods -- ----------------------- ---@param season SeasonID? function DBM:IsSeasonal(season) if season and Enum.SeasonID then return Enum.SeasonID[season] == private.currentSeason else return not not private.currentSeason end end --Catch alls to basically allow encounter mods to use pre retail changes within mods ---@param self DBMModOrDBM function DBM:IsClassic() return not private.isRetail end bossModPrototype.IsClassic = DBM.IsClassic ---@param self DBMModOrDBM function DBM:IsRetail() return private.isRetail end bossModPrototype.IsRetail = DBM.IsRetail ---@param self DBMModOrDBM function DBM:IsCata() return private.isCata end bossModPrototype.IsCata = DBM.IsCata ---@param self DBMModOrDBM function DBM:IsPostCata() return private.isCata or private.isRetail end bossModPrototype.IsPostCata = DBM.IsPostCata function bossModPrototype:CheckBigWigs(name) if raid[name] and raid[name].bwversion then return raid[name].bwversion else return false 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. ---@param flag SpecFlags ---@return boolean 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 (private.isRetail or private.isCata) 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 private.isRetail and (role == "HEALER" or role == "TANK") or GetPartyAssignment("MAINTANK", uId, true) 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 private.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 ---@param self DBMModOrDBM 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 (private.isRetail or private.isCata) 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 private.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 ---@param self DBMModOrDBM function DBM:IsRanged(uId) if uId then local name = GetUnitName(uId, true) if (private.isRetail or private.isCata) 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 (private.isRetail or private.isCata) 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 (private.isRetail or private.isCata) 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 ---@param uId string? Used for querying external unit. If nil, queries "player" function bossModPrototype:UnitClass(uId) if uId then--Return unit requested local _, class = UnitClass(uId) return class end return playerClass--else return "player" end do -- 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 private.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 end ---@param uId string? Used for querying external unit. If nil, queries "player" ---@return boolean 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 private.isRetail and UnitGroupRolesAssigned(uId) == "DAMAGER" or not GetPartyAssignment("MAINTANK", uId, true) end if not currentSpecID then DBM:SetCurrentSpecInfo() end if not private.isRetail then return private.specRoleTable[currentSpecID]["Dps"] end local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID) return role == "DAMAGER" end ---@param self DBMModOrDBM ---@param uId string? Used for querying external unit. If nil, queries "player" ---@return boolean function DBM:IsHealer(uId) if uId then--External unit call. if not private.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 private.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 ---@param self DBMModOrDBM ---@param playerUnitID playerUUIDs|targetUIDs? unitID of requested unit. this or isName must be provided ---@param enemyUnitID enemyUIDs|targetUIDs? unitID of tanked unit we're checking. This or enemyGUID must be provided ---@param isName string? name of the requested unit. This or playerUnitID must be provided ---@param onlyRequested boolean? true if tight search, false if loose search that will return ALL tank specs ---@param enemyGUID string? guid of tanked unit we're checking. This or enemyUnitID must be provided ---@param includeTarget boolean? set to true to allow bosses target to be good enough if threat check fails ---@param onlyS3 boolean? true for tight threat check (status 3 securly tanked required). loose threat otherwise ---@return boolean 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, true) then return true end if not private.isClassic and not private.isBCC then--Allow boss checks in wrath and later --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 and 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 (private.isRetail and UnitGroupRolesAssigned(uId .. i) == "TANK" or GetPartyAssignment("MAINTANK", uId .. i, true)) and not UnitIsDeadOrGhost(uId .. i) then count = count + 1 end end return count end ---------------------------- -- Boss Health Function -- ---------------------------- do local bossNames, bossIcons = {}, {} ---This accepts both CID and GUID which makes switching to UnitPercentHealthFromGUID and UnitTokenFromGUID not as cut and dry ---@param self DBMModOrDBM ---@param cIdOrGUID number|string ---@param onlyHighest boolean? 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 bossNames[cIdOrGUID] = UnitName(uId) bossIcons[cIdOrGUID] = GetRaidTargetIndex(uId) return hp, uId, UnitName(uId) --Focus, does not exist in classic elseif private.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 bossNames[cIdOrGUID] = UnitName("focus") bossIcons[cIdOrGUID] = GetRaidTargetIndex("focus") return hp, "focus", UnitName("focus") else --Boss UnitIds if private.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 bossNames[cIdOrGUID] = UnitName(unitID) bossIcons[cIdOrGUID] = GetRaidTargetIndex(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 bossNames[cIdOrGUID] = UnitName(unitId) bossIcons[cIdOrGUID] = GetRaidTargetIndex(unitId) return hp, unitId, UnitName(unitId) end end if not private.isRetail then --Scan a few nameplates if we don't have raid boss uIDs, but not worth trying all of them for i = 1, 20 do local unitId = "nameplate" .. 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 bossNames[cIdOrGUID] = UnitName(unitId) bossIcons[cIdOrGUID] = GetRaidTargetIndex(unitId) return hp, unitId, UnitName(unitId) end 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 ---Used to instruct boss mod to record the health % of highest health boss when multiple bosses 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 function DBM:GetCachedBossHealth() return bossHealth, bossNames, bossIcons end end --------------- -- Options -- --------------- ---@param name any Option name must be string, but language server gets confused if it's not set to any ---@param default SpecFlags|boolean? ---@param cat string? category type: ie "timer", "announce", "misc", "sound", etc ---@param func any? Custom function to call when option is changed ---@param extraOption string|number? Used for attached options such as timer color or special warning sound ---@param extraOptionTwo string|number? Used for attached options such as countdown voice or special warning note ---@param spellId any? spellId to group with other options for same spell ---@param optionSubType string? ie "gtfo", "adds", "achievement", "stage", etc ---@param waCustomName string? used to inject custom weak aura spellId key text function bossModPrototype:AddBoolOption(name, default, cat, func, extraOption, extraOptionTwo, spellId, optionSubType, 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 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 optionSubType and optionSubType == "achievement" then spellId = "at" .. spellId--"at" for achievement timer end local optionTypeMatch = optionSubType or "" if not optionTypeMatch:find("stage") then self:GroupSpells(spellId, name) end end end self:SetOptionCategory(name, cat, optionSubType, waCustomName) if func then self.optionFuncs = self.optionFuncs or {} self.optionFuncs[name] = func end end ---@param name any ---@param default SpecFlags|boolean? ---@param defaultSound number|string? Can be number for built in spec warn sound 1-4 or string for custom sound path ---@param cat string? category type: ie "timer", "announce", "misc", "sound", etc ---@param spellId any? spellId to group with other options for same spell ---@param optionType string? ---@param waCustomName string? used to inject custom weak aura spellId key text 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 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 ---@param auraspellId number must match debuff ID so EnablePrivateAuraSound function can call right option key and right debuff ID ---@param default SpecFlags|boolean? ---@param groupSpellId number? is used if a diff option key is used in all other options with spell (will be quite common) ---@param defaultSound number? 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 type(default) == "string" then default = self:GetRoleFlagValue(default) end self.Options["PrivateAuraSound" .. auraspellId] = (default == nil) or default --LuaLS is just stupid here. There is no rule that says self.Options.Variable has to be a bool. Entire SWSound variable scope is always a number ---@diagnostic disable-next-line: assign-type-mismatch self.Options["PrivateAuraSound" .. auraspellId .. "SWSound"] = defaultSound or 1 self.localization.options["PrivateAuraSound" .. auraspellId] = L.AUTO_PRIVATEAURA_OPTION_TEXT:format(auraspellId) self:GroupSpellsPA(groupSpellId or auraspellId, "PrivateAuraSound" .. auraspellId) self:SetOptionCategory("PrivateAuraSound" .. auraspellId, "paura", nil, nil, true) end ---@meta ---@alias iconTypes ---|0: Player icon using no sorting. Most common in boss mods ---|1: Player icon using melee > ranged with alphabetical sorting on multiple melee ---|2: Player icon using melee > ranged with raid roster index sorting on multiple melee ---|3: Player icon using ranged > melee with alphabetical sorting on multiple ranged ---|4: Player icon using ranged > melee with raid roster index sorting on multiple ranged ---|5: NPC icon using feature that chooses ideal setter. Always use 5 for NPCS ---|6: Player icon using only alphabetical sorting ---|7: Player icon using only raid roster index sorting ---|8: Player icon using tank > non tank with alphabetical sorting on multiple melee ---|9: Player icon using tank > non tank with raid roster index sorting on multiple melee ---@param default SpecFlags|boolean? ---@param iconType iconTypes|number? ---@param iconsUsed table? table defining used icons such as {1, 2, 3} ---@param conflictWarning boolean? set to true if this mod has 2 or more icon options that use the same icons function bossModPrototype:AddSetIconOption(name, spellId, default, iconType, iconsUsed, conflictWarning, groupSpellId) self.DefaultOptions[name] = (default == nil) or default if 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 notice about outdated bool and nil support --Will be removed before TWW iconType = iconType or 0 if type(iconType) ~= "number" then error("DBM iconType must be a number. If you are seeing this error your content mods are probabably out of date") 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" 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 ---Still used for situations we may use static arrows to point for a specific way to move. Legacy arrows also supported toward/away from specific player units ---@meta ---@alias arrowTypes ---|1: Shows an arrow pointing toward player target ---|2: Shows an arrow pointing away from player target ---|3: Shows an arrow pointing toward specific location ---@param name string Option name ---@param spellId number if used, auto localizes using spell or journal id. if left blank uses generic description ---@param default SpecFlags|boolean? ---@param isRunTo arrowTypes|number function bossModPrototype:AddArrowOption(name, spellId, default, isRunTo) if isRunTo == true then isRunTo = 2 end--Support legacy self.DefaultOptions[name] = (default == nil) or default if 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 ---Legacy object at this point. Range checks aren't added to new modules due to no longer being usable inside raids. they should NOT be removed from old modules in event blizzard ever adds built in functionality we can automate ---@param range number|string Non optional, should be number if fixed ranged or string with custom string such as "various" or "10/6" ---@param spellId number? if used, auto localizes using spell or journal id. if left blank uses generic description ---@param default SpecFlags|boolean? function bossModPrototype:AddRangeFrameOption(range, spellId, default) self.DefaultOptions["RangeFrame"] = (default == nil) or default if 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 ---Legacy object at this point. HUD checks aren't added to new modules due to no longer being usable inside raids. ---@param name string Option name ---@param spellId number? if used, auto localizes using spell or journal id. if left blank uses generic description ---@param default SpecFlags|boolean? function bossModPrototype:AddHudMapOption(name, spellId, default) self.DefaultOptions[name] = (default == nil) or default if 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 ---@param name string Option name ---@param spellId number if used, auto localizes using spell or journal id. if left blank uses generic description ---@param default SpecFlags|boolean? ---@param forceDBM boolean? Used in very rare cases we need to use nameplate feature without a clean place to use enable/disable callbacks for 3rd party NP addons 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 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 ---@param spellId number? if used, auto localizes using spell or journal id. if left blank uses generic description ---@param default SpecFlags|boolean? 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 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 ---@meta ---@alias gossipTypes ---|"Action": Auto select gossip choice(s) to perform actions (such as using transports) ---|"Encounter": Auto select gossip choice to start encounter ---|"Buff": Auto select gossip choice(s) for npc or profession buffs ---@param default SpecFlags|boolean? ---@param gossipType gossipTypes|string function bossModPrototype:AddGossipOption(default, gossipType, optionVersion) local oVersion = "" if optionVersion then oVersion = tostring(optionVersion) end self.DefaultOptions["AutoGossip" .. gossipType .. oVersion] = (default == nil) or default if type(default) == "string" then default = self:GetRoleFlagValue(default) end self.Options["AutoGossipAction" .. gossipType .. oVersion] = (default == nil) or default if gossipType == "Action" then self.localization.options["AutoGossip" .. gossipType .. oVersion] = L.AUTO_GOSSIP_PERFORM_ACTION elseif gossipType == "Encounter" then self.localization.options["AutoGossip" .. gossipType .. oVersion] = L.AUTO_GOSSIP_START_ENCOUNTER else--Type 1 most common so the default fallback if left blank self.localization.options["AutoGossip" .. gossipType .. oVersion] = L.AUTO_GOSSIP_BUFFS end self:SetOptionCategory("AutoGossip" .. gossipType .. oVersion, "misc") end ---@param default SpecFlags|boolean? ---@param maxLevel number? set max level if you want to disable this readycheck from firing at a certain point function bossModPrototype:AddReadyCheckOption(questId, default, maxLevel) self.readyCheckQuestId = questId self.readyCheckMaxLevel = maxLevel or 999 self.DefaultOptions["ReadyCheck"] = (default == nil) or default if 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 ---@param default SpecFlags|boolean? function bossModPrototype:AddSpeedClearOption(name, default) self.DefaultOptions["SpeedClearTimer"] = (default == nil) or default if 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 ---@param customName string? Used to inject custom weak aura spellId key text 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 ---Duplicate function just for private auras to do literally same thing as GroupSpells without ability to pass extra arg function bossModPrototype:GroupSpellsPA(...) 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].hasPrivate = true--This single line is basically why GroupSpellsPA had to duplicate GroupSpells 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 ---@param name any ---@param cat string category type: ie "timer", "announce", "misc", "sound", etc ---@param optionSubType string? ie "gtfo", "adds", "achievement", "stage", etc ---@param waCustomName string? used to inject custom weak aura spellId key text ---@param hasPrivate boolean? used to mark option as private aura option so it displays PA icon in GUI function bossModPrototype:SetOptionCategory(name, cat, optionSubType, waCustomName, hasPrivate) optionSubType = optionSubType or "" for _, options in pairs(self.optionCategories) do removeEntry(options, name) end if self.addon and self.groupSpells[name] and not (optionSubType == "gtfo" or optionSubType == "adds" or optionSubType == "addscount" or optionSubType == "addscustom" or optionSubType:find("stage") or cat == "icon" and DBM.Options.GroupOptionsExcludeIcon) 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 if hasPrivate and not self.groupOptions[sSpell].hasPrivate then self.groupOptions[sSpell].hasPrivate = true 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 -- -------------- ---@meta ---@alias combatTypes ---|"combat": Default Option. Triggers Combat on ENCOUNTER_START, INSTANCE_ENCOUNTER_ENGAGE_UNIT, UNIT_HEALTH, PLAYER_REGEN_DISABLED ---|"yell": Triggers Combat on CHAT_MSG_MONSTER_YELL ---|"say": Triggers Combat on CHAT_MSG_SAY or CHAT_MSG_MONSTER_SAY ---|"emote": Triggers Combat on CHAT_MSG_EMOTE or CHAT_MSG_MONSTER_EMOTE ---|"yell_regex": Triggers Combat on CHAT_MSG_MONSTER_YELL using regex matching ---|"say_regex": Triggers Combat on CHAT_MSG_SAY or CHAT_MSG_MONSTER_SAY using regex matching ---|"emote_regex": Triggers Combat on CHAT_MSG_EMOTE or CHAT_MSG_MONSTER_EMOTE using regex matching ---|"combat_yell": Same as combat, but also uses CHAT_MSG_MONSTER_YELL exact matching ---|"combat_say": Same as combat, but also uses CHAT_MSG_SAY or CHAT_MSG_MONSTER_SAY exact matching ---|"combat_emote": Same as combat, but also uses CHAT_MSG_EMOTE or CHAT_MSG_MONSTER_EMOTE exact matching ---|"combat_yellfind": Same as combat, but also uses CHAT_MSG_MONSTER_YELL loose matching ---|"combat_sayfind": Same as combat, but also uses CHAT_MSG_SAY or CHAT_MSG_MONSTER_SAY loose matching ---|"combat_emotefind": Same as combat, but also uses CHAT_MSG_EMOTE or CHAT_MSG_MONSTER_EMOTE loose matching ---|"scenario": Tells mod to treat an entire scenario as combat ---@param cType combatTypes function bossModPrototype:RegisterCombat(cType, ...) if cType then cType = cType:lower() end ---@class CombatInfo 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 ---@class CombatInfo local combatInfo = self.combatInfo combatInfo.killType = msgType combatInfo.killMsgs = {} for i = 1, select("#", ...) do local v = select(i, ...) 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 ---@class CombatInfo local combatInfo = self.combatInfo combatInfo.noCombatInVehicle = not flag end ---Used to set creature IDs this mod will scan for Boss Health and legacy or backup combat detection methods 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 ---Used to set Encounter IDs this mod will pass to ENCOUNTER_START/ENCOUNTER_END/BOSS_KILL function bossModPrototype:SetEncounterID(...) self.encounterId = ... if select("#", ...) > 1 then self.multiEncounterPullDetection = {...} if self.combatInfo then self.combatInfo.multiEncounterPullDetection = self.multiEncounterPullDetection end end end ---Used to disable ENCOUNTER_START from detecting boss combat function bossModPrototype:DisableESCombatDetection() self.noESDetection = true if self.combatInfo then self.combatInfo.noESDetection = true end end ---Used to disable ENCOUNTER_END for kill detection function bossModPrototype:DisableEEKillDetection() self.noEEDetection = true if self.combatInfo then self.combatInfo.noEEDetection = true end end ---Used to disable BOSS_KILL for kill detection function bossModPrototype:DisableBKKillDetection() self.noBKDetection = true if self.combatInfo then self.combatInfo.noBKDetection = true end end ---Used to disable INSTANCE_ENCOUNTER_ENGAGE_UNIT from detecting boss combat function bossModPrototype:DisableIEEUCombatDetection() self.noIEEUDetection = true if self.combatInfo then self.combatInfo.noIEEUDetection = true end end ---Used to prevent engaging a boss that's friendly function bossModPrototype:DisableFriendlyDetection() self.noFriendlyEngagement = true if self.combatInfo then self.combatInfo.noFriendlyEngagement = true end end ---Used to disable using PLAYER_REGEN_DISABLED from detecting boss combat 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 ---Used to permit mod from sending syncs for world bosses. function bossModPrototype:EnableWBEngageSync() self.WBEsync = true if self.combatInfo then self.combatInfo.WBEsync = true end end ---Used when a bosses death condition should be ignored (maybe they die repeatedly for example) function bossModPrototype:DisableBossDeathKill() self.noBossDeathKill = true if self.combatInfo then self.combatInfo.noBossDeathKill = true end end ---Used when a boss is scripted in a hacky way that their creature Id changes mid fight, and we want to treat multiple IDs as a single boss function bossModPrototype:SetMultiIDSingleBoss() self.multiIDSingleBoss = true end ---Used for knowing if a specific mod is engaged function bossModPrototype:IsInCombat() return self.inCombat end ---Used for checking if any person in group is in any kind of combat ---@param self DBMModOrDBM function DBM:GroupInCombat() local combatFound = false --Any Boss engaged if private.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 bossModPrototype.GroupInCombat = DBM.GroupInCombat function bossModPrototype:IsAlive() return not UnitIsDeadOrGhost("player") end ---Sets minimum amount of time before a pull is concidered valid. ---@param t number function bossModPrototype:SetMinCombatTime(t) self.minCombatTime = t end ---Needs to be called after RegisterCombat ---
Sets time out of combat required before a module should declare a wipe ---@param t number function bossModPrototype:SetWipeTime(t) if not self.combatInfo then error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2) end ---@class CombatInfo local combatInfo = self.combatInfo combatInfo.wipeTimer = t end ---Used to specify amount of time before allowing a boss to be pulled again. ---@param t number? used to specify recombat time after a kill. ---@param t2 number? used to specify recombat time after a wipe function bossModPrototype:SetReCombatTime(t, t2) self.reCombatTime = t self.reCombatTime2 = t2 end function bossModPrototype:SetOOCBWComms() tinsert(oocBWComms, self) end ----------------------- -- Synchronization -- ----------------------- do ---@param self DBMMod local function prepareSync(self, 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 ---Send boss mod communication ---@param event string ---@param ... any function bossModPrototype:SendSync(event, ...) prepareSync(self, event, ...) end ---Variant of SendSync used to prevent comms spam ---@param throttle number ---@param event string ---@param ... any function bossModPrototype:SendThrottledSync(throttle, event, ...) if self:AntiSpam(throttle, "SyncSpam"..event) then prepareSync(self, event, ...) end end end function bossModPrototype:SendBigWigsSync(msg, extra) if not dbmIsEnabled or IsTrialAccount() 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 ---@param revision number|string Either a number in the format "202101010000" (year, month, day, hour, minute) or string "20240723001246" to be auto set by packager 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 ---Will block boss mod communication from other users if their revision older than defined revision ---
string revisions are date integer format (new), number revisions are legacy revisions (that should be updated) function bossModPrototype:SetMinSyncRevision(revision) self.minSyncRevision = (type(revision or "") == "number") and revision or parseCurseDate(revision) end ---Used for chat message that specific module is out of date and has key fixes function bossModPrototype:SetHotfixNoticeRev(revision) self.hotfixNoticeRev = (type(revision or "") == "number") and revision or parseCurseDate(revision) end ------------- -- Icons -- ------------- do ---@param mod DBMMod function DBM:ElectIconSetter(mod) --elect icon person if mod.findFastestComputer and not self.Options.DontSetIcons then if mod:IsDungeon() or 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 end -- Expose some file-local data to private for testing purposes only. test:RegisterLocalHook("LastInstanceMapID", function(val) local old = LastInstanceMapID LastInstanceMapID = val return old end) test:RegisterLocalHook("GetTime", function(val) local old = GetTime GetTime = val return old end) test:RegisterLocalHook("UnitDetailedThreatSituation", function(val) local old = UnitDetailedThreatSituation UnitDetailedThreatSituation = val return old end) test:RegisterLocalHook("UnitAffectingCombat", function(val) local old = UnitAffectingCombat UnitAffectingCombat = val return old end) test:RegisterLocalHook("UnitGUID", function(val) local old = UnitGUID UnitGUID = val return old end) private.mainFrame = mainFrame