You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
9061 lines
384 KiB
9061 lines
384 KiB
-- *********************************************************
|
|
-- ** Deadly Boss Mods - Core **
|
|
-- ** https://deadlybossmods.com **
|
|
-- ** https://patreon.com/deadlybossmods **
|
|
-- *********************************************************
|
|
--
|
|
-- This addon is written and copyrighted by:
|
|
-- * Paul Emmerich (Tandanu @ EU-Aegwynn) (DBM-Core)
|
|
-- * Martin Verges (Nitram @ EU-Azshara) (DBM-GUI)
|
|
-- * Adam Williams (Omegal @ US-Whisperwind) (Primary boss mod author & DBM maintainer)
|
|
--
|
|
-- The localizations are written by:
|
|
-- * enGB/enUS: Omegal Twitter @MysticalOS
|
|
-- * deDE: Ebmor
|
|
-- * ruRU: TOM_RUS https://curseforge.com/profiles/TOM_RUS/
|
|
-- * zhTW: Whyv ultrashining@gmail.com
|
|
-- * koKR: Elnarfim ---
|
|
-- * zhCN: Mini Dragon projecteurs@gmail.com
|
|
--
|
|
--
|
|
-- Special thanks to:
|
|
-- * Arta
|
|
-- * Tennberg (a lot of fixes in the enGB/enUS localization)
|
|
-- * nBlueWiz (a lot of previous fixes in the koKR localization as well as boss mod work) Contact: everfinale@gmail.com
|
|
--
|
|
---@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<integer, CombatInfo[]>
|
|
-- 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<DBMEvent, DBMModOrDBM[]>
|
|
local registeredEvents = {}
|
|
---@type table<string, table<integer, integer>>
|
|
local registeredSpellIds = {}
|
|
---@type table<string, table<integer, integer>>
|
|
local unfilteredCLEUEvents = {}
|
|
---@type table<string, integer>
|
|
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 "<no target>")
|
|
if whisperTarget then
|
|
C_ChatInfo.SendAddonMessageLogged(DBMPrefix, (DBMSyncProtocol .. "\tUW\t0\t%s"):format(text), "WHISPER", whisperTarget)
|
|
else
|
|
sendLoggedSync(DBMSyncProtocol, "U", ("0\t%s"):format(text))
|
|
end
|
|
end
|
|
return
|
|
end
|
|
if sender and ignore[sender] then return end
|
|
text = text:sub(1, 16)
|
|
text = text:gsub("%%t", UnitName("target") or "<no target>")
|
|
if time < 3 then
|
|
self:AddMsg(L.PIZZA_ERROR_USAGE)
|
|
return
|
|
end
|
|
DBT:CreateBar(time, text, 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
|
|
---<br>I'm not even sure why a boss check is in it, since GetBossUnitId existed (and is now deprecated)
|
|
---<br>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
|
|
---<br>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
|
|
---<br>This is very touchy though and will fail if everyone isn't in same SUB zone (ie same room/area)
|
|
---<br>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
|
|
----<type>:<subtype>:<realmID>:<mapID>:<serverID>:<dbID>:<creationbits>
|
|
--Player/Item
|
|
----<type>:<realmID>:<dbID>
|
|
---@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
|
|
---<br>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
|
|
---<br>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
|
|
---<br>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
|
|
---<br> 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
|
|
|