local MAJOR_VERSION = "LibGetFrame-1.0" local MINOR_VERSION = 63 if not LibStub then error(MAJOR_VERSION .. " requires LibStub.") end local lib = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION) if not lib then return end lib.callbacks = lib.callbacks or LibStub("CallbackHandler-1.0"):New(lib) lib.timer = lib.timer or LibStub("AceTimer-3.0") local callbacks = lib.callbacks local GetPlayerInfoByGUID, UnitExists, UnitIsUnit, SecureButton_GetUnit, IsAddOnLoaded = GetPlayerInfoByGUID, UnitExists, UnitIsUnit, SecureButton_GetUnit, IsAddOnLoaded local tinsert, CopyTable, wipe = tinsert, CopyTable, wipe function lib.Mixin(object, ...) for i = 1, select("#", ...) do local mixin = select(i, ...); for k, v in pairs(mixin) do object[k] = v; end end return object; end local maxDepth = 50 local defaultFramePriorities = { -- raid frames "^Vd1", -- vuhdo "^Vd2", -- vuhdo "^Vd3", -- vuhdo "^Vd4", -- vuhdo "^Vd5", -- vuhdo "^Vd", -- vuhdo "^HealBot_HealUnit", -- healbot "^hbPet_HealUnit", -- healbot "^HealBot", -- healbot "^GridLayout", -- grid "^Grid2Layout", -- grid2 "^NugRaid%d+UnitButton%d+", -- Aptechka "^PlexusLayout", -- plexus "^ElvUF_Raid%d*Group", -- elv "^ElvUF_RaidGroup", -- elv "^oUF_bdGrid", -- bdgrid "^oUF_.-Raid", -- generic oUF "^LimeGroup", -- lime "^InvenRaidFrames3Group%dUnitButton", -- InvenRaidFrames3 "^SUFHeaderraid", -- suf "^LUFHeaderraid", -- luf "^AshToAshUnit%d+Unit%d+", -- AshToAsh "^Cell", -- Cell -- party frames "^AleaUI_GroupHeader", -- Alea "^SUFHeaderparty", -- suf "^LUFHeaderparty", -- luf "^ElvUF_PartyGroup", -- elv "^oUF_.-Party", -- generic oUF "^PitBull4_Groups_Party", -- pitbull4 "^CompactRaid", -- blizz "^CompactParty", -- blizz "^PartyFrame", -- blizz "^PartyMemberFrame", -- blizz -- player frame "^InvenUnitFrames_Player", "^SUFUnitplayer", "^LUFUnitplayer", "^PitBull4_Frames_Player", "^ElvUF_Player", "^oUF_.-Player", "^PlayerFrame", } local getDefaultFramePriorities = function() return CopyTable(defaultFramePriorities) end lib.getDefaultFramePriorities = getDefaultFramePriorities local defaultPlayerFrames = { "^InvenUnitFrames_Player", "SUFUnitplayer", "LUFUnitplayer", "PitBull4_Frames_Player", "ElvUF_Player", "oUF_.-Player", "oUF_PlayerPlate", "PlayerFrame", } local getDefaultPlayerFrames = function() return CopyTable(defaultPlayerFrames) end lib.getDefaultPlayerFrames = getDefaultPlayerFrames local defaultTargetFrames = { "^InvenUnitFrames_Target", "SUFUnittarget", "LUFUnittarget", "PitBull4_Frames_Target", "ElvUF_Target", "oUF_.-Target", "TargetFrame", "^hbExtra_HealUnit", } local getDefaultTargetFrames = function() return CopyTable(defaultTargetFrames) end lib.getDefaultTargetFrames = getDefaultTargetFrames local defaultTargettargetFrames = { "^InvenUnitFrames_TargetTarget", "SUFUnittargetarget", "LUFUnittargetarget", "PitBull4_Frames_Target's target", "ElvUF_TargetTarget", "oUF_.-TargetTarget", "oUF_ToT", "TargetTargetFrame", } local getDefaultTargettargetFrames = function() return CopyTable(defaultTargettargetFrames) end lib.getDefaultTargettargetFrames = getDefaultTargettargetFrames local defaultPartyFrames = { "^InvenUnitFrames_Party%d", "^AleaUI_GroupHeader", "^SUFHeaderparty", "^LUFHeaderparty", "^ElvUF_PartyGroup", "^oUF_.-Party", "^PitBull4_Groups_Party", "^PartyFrame", "^CompactParty", "^PartyMemberFrame", } local getDefaultPartyFrames = function() return CopyTable(defaultPartyFrames) end lib.getDefaultPartyFrames = getDefaultPartyFrames local defaultPartyTargetFrames = { "SUFChildpartytarget%d", } local getDefaultPartyTargetFrames = function() return CopyTable(defaultPartyTargetFrames) end lib.getDefaultPartyTargetFrames = getDefaultPartyTargetFrames local defaultFocusFrames = { "^InvenUnitFrames_Focus", "ElvUF_FocusTarget", "LUFUnitfocus", "FocusFrame", "^hbExtra_HealUnit", } local getDefaultFocusFrames = function() return CopyTable(defaultFocusFrames) end lib.getDefaultFocusFrames = getDefaultFocusFrames local defaultRaidFrames = { "^Vd", "^HealBot_HealUnit", "^hbPet_HealUnit", "^HealBot", "^GridLayout", "^Grid2Layout", "^PlexusLayout", "^InvenRaidFrames3Group%dUnitButton", "^ElvUF_Raid%d*Group", "^ElvUF_RaidGroup", "^oUF_.-Raid", "^AshToAsh", "^Cell", "^LimeGroup", "^SUFHeaderraid", "^LUFHeaderraid", "^CompactRaid", "^RaidPullout", } local getDefaultRaidFrames = function() return CopyTable(defaultRaidFrames) end lib.getDefaultRaidFrames = getDefaultRaidFrames -- local CacheMonitorMixin = {} function CacheMonitorMixin:Init(makeDiff) self.data = {} self.cache = {} if makeDiff then self.makeDiff = makeDiff self.added = {} self.updated = {} self.removed = {} end end -- fill cache, added, updated function CacheMonitorMixin:Add(key, ...) local args = select("#", ...) if args > 1 then if self.makeDiff then if type(self.data[key]) == "table" then for i = 1, args do local arg = select(i, ...) if self.data[key][i] ~= arg then self.updated[key] = self.data[key] break end end else self.added[key] = true end end self.cache[key] = {...} else local value = ... if self.makeDiff then if self.data[key] ~= value then if self.data[key] == nil then self.added[key] = true else self.updated[key] = self.data[key] end end end self.cache[key] = value end end function CacheMonitorMixin:CalcRemoved() if not self.makeDiff then return end for key, value in pairs(self.data) do if self.cache[key] == nil then self.removed[key] = value end end end function CacheMonitorMixin:WriteCache() local tmp = self.data self.data = self.cache self.cache = tmp wipe(self.cache) end function CacheMonitorMixin:Reset() if self.makeDiff then wipe(self.updated) wipe(self.removed) wipe(self.added) end end -- local FrameToFrameName = {} -- frame adress => frame name local FrameToUnit = {} -- frame adress => unitToken lib.Mixin(FrameToFrameName, CacheMonitorMixin) lib.Mixin(FrameToUnit, CacheMonitorMixin) FrameToFrameName:Init() FrameToUnit:Init(true) local profiling = false local profileData local function doNothing() end local StartProfiling = doNothing local StopProfiling = doNothing local function _StartProfiling(id) if not profileData[id] then profileData[id] = {} profileData[id].count = 1 profileData[id].start = debugprofilestop() profileData[id].elapsed = 0 profileData[id].spike = 0 return end if profileData[id].count == 0 then profileData[id].count = 1 profileData[id].start = debugprofilestop() else profileData[id].count = profileData[id].count + 1 end end local function _StopProfiling(id) profileData[id].count = profileData[id].count - 1 if profileData[id].count == 0 then local elapsed = debugprofilestop() - profileData[id].start profileData[id].elapsed = profileData[id].elapsed + elapsed if elapsed > profileData[id].spike then profileData[id].spike = elapsed end end end function lib.StartProfile() if profiling then print(MAJOR_VERSION, " (StartProfile) Profiling already started") return false end profiling = true profileData = {} StartProfiling = _StartProfiling StopProfiling = _StopProfiling end function lib.StopProfile() if not profiling then print(MAJOR_VERSION, " (StopProfile) Profiling not running") return false end profiling = false StartProfiling = doNothing StopProfiling = doNothing end function lib.GetProfileData() return profileData or {} end -- if frame doesn't have a name, try to use the key from it's parent local function recurseGetName(frame) local name = frame.GetName and frame:GetName() or nil if name then return name end local parent = frame.GetParent and frame:GetParent() if parent then local parentKey for key, child in pairs(parent) do if child == frame then parentKey = key break end end if parentKey then return (recurseGetName(parent) or "") .. "." .. parentKey end end end --local notAUnitFrameTypeAttribute = { -- cancelaura = true --} local function ScanFrames(depth, frame, ...) coroutine.yield() if not frame then return end if depth < maxDepth then local frameType = frame:GetObjectType() if frameType == "Frame" or frameType == "Button" then ScanFrames(depth + 1, frame:GetChildren()) end if frameType == "Button" then local typeAttribute = frame:GetAttribute("type") --if not notAUnitFrameTypeAttribute[typeAttribute] then local unit = SecureButton_GetUnit(frame) if unit and frame:IsVisible() then local name = recurseGetName(frame) if name then FrameToFrameName:Add(frame, name) FrameToUnit:Add(frame, unit) end end --end end end ScanFrames(depth, ...) end local status = "ready" local co local coroutineFrame = CreateFrame("Frame") coroutineFrame:Hide() local function doScanForUnitFrames() if not coroutineFrame:IsShown() then status = "scanning" co = coroutine.create(ScanFrames) coroutineFrame:Show() end end coroutineFrame:SetScript("OnUpdate", function() local start = debugprofilestop() -- Limit to 5ms per frame StartProfiling("scan frames") while debugprofilestop() - start < 5 and coroutine.status(co) ~= "dead" do coroutine.resume(co, 0, UIParent) end StopProfiling("scan frames") if coroutine.status(co) == "dead" then StartProfiling("callbacks") FrameToFrameName:WriteCache() FrameToUnit:CalcRemoved() FrameToUnit:WriteCache() StartProfiling("callback GETFRAME_REFRESH") callbacks:Fire("GETFRAME_REFRESH") StopProfiling("callback GETFRAME_REFRESH") -- FrameToUnit if next(FrameToUnit.added) then StartProfiling("callback FRAME_UNIT_ADDED") for frame in pairs(FrameToUnit.added) do callbacks:Fire("FRAME_UNIT_ADDED", frame, FrameToUnit.data[frame]) end StopProfiling("callback FRAME_UNIT_ADDED") end if next(FrameToUnit.updated) then StartProfiling("callback FRAME_UNIT_UPDATE") for frame, previousUnit in pairs(FrameToUnit.updated) do callbacks:Fire("FRAME_UNIT_UPDATE", frame, FrameToUnit.data[frame], previousUnit) end StopProfiling("callback FRAME_UNIT_UPDATE") end if next(FrameToUnit.removed) then StartProfiling("callback FRAME_UNIT_REMOVED") for frame, unit in pairs(FrameToUnit.removed) do callbacks:Fire("FRAME_UNIT_REMOVED", frame, unit) end StopProfiling("callback FRAME_UNIT_REMOVED") end coroutineFrame:Hide() FrameToFrameName:Reset() FrameToUnit:Reset() StopProfiling("callbacks") if status == "scan_queued" then doScanForUnitFrames("queued") else status = "ready" end end end) local function ScanForUnitFrames(noDelay) if status == "ready" then if noDelay then doScanForUnitFrames() else status = "scan_delay" lib.timer:ScheduleTimer(function() doScanForUnitFrames() end, 1) end elseif status == "scanning" then status = "scan_queued" end end function lib.ScanForUnitFrames() ScanForUnitFrames(true) end local function isFrameFiltered(name, ignoredFrames) for _, filter in pairs(ignoredFrames) do if name:find(filter) then return true end end return false end local function GetUnitFrames(target, ignoredFrames) if not UnitExists(target) then if type(target) ~= "string" then return end local B = tonumber(target:sub(5, 5), 16) if B and B % 8 == 0 then target = select(6, GetPlayerInfoByGUID(target)) else target = target:gsub(" .*", "") end if not UnitExists(target) then return end end local frames for frame, frameName in pairs(FrameToFrameName.data) do local unit = SecureButton_GetUnit(frame) if unit and UnitIsUnit(unit, target) and not isFrameFiltered(frameName, ignoredFrames) then frames = frames or {} frames[frame] = frameName end end return frames end local function ElvuiWorkaround(frame) if IsAddOnLoaded("ElvUI") and frame and frame:GetName() and frame:GetName():find("^ElvUF_") and frame.Health then return frame.Health else return frame end end local function CellGetUnitFrames(target, frames, framePriorities) if not IsAddOnLoaded("Cell") or not Cell.GetUnitFramesForLGF then return frames end return Cell.GetUnitFramesForLGF(target, frames, framePriorities) end local defaultOptions = { framePriorities = defaultFramePriorities, ignorePlayerFrame = true, ignoreTargetFrame = true, ignoreTargettargetFrame = true, ignorePartyFrame = false, ignorePartyTargetFrame = true, ignoreFocusFrame = true, ignoreRaidFrame = false, playerFrames = defaultPlayerFrames, targetFrames = defaultTargetFrames, targettargetFrames = defaultTargettargetFrames, partyFrames = defaultPartyFrames, partyTargetFrames = defaultPartyTargetFrames, focusFrames = defaultFocusFrames, raidFrames = defaultRaidFrames, ignoreFrames = { "PitBull4_Frames_Target's target's target", "ElvUF_PartyGroup%dUnitButton%dTarget", "RavenButton", "RavenOverlay", "AshToAshUnit%d+ShadowGroupHeaderUnitButton%d+", "InvenUnitFrames_TargetTargetTarget", "CellQuickCastButton", }, skipCellOverrides = false, returnAll = false, } local getDefaultOptions = function() return CopyTable(defaultOptions) end lib.getDefaultOptions = getDefaultOptions local IterateGroupMembers = function(reversed, forceParty) local unit = (not forceParty and GetNumRaidMembers() > 0) and 'raid' or 'party' local numGroupMembers = unit == 'party' and GetNumPartyMembers() or GetNumRaidMembers() local i = reversed and numGroupMembers or (unit == 'party' and 0 or 1) return function() local ret if i == 0 and unit == 'party' then ret = 'player' elseif i <= numGroupMembers and i > 0 then ret = unit .. i end i = i + (reversed and -1 or 1) return ret end end local unitPetState = {} -- track if unit's pet exists local saveGetUnitFrame local function fixGetUnitFrameIntegrity() lib.GetUnitFrame = saveGetUnitFrame lib.GetFrame = saveGetUnitFrame if WeakAuras and WeakAuras.GetUnitFrame then WeakAuras.GetUnitFrame = saveGetUnitFrame end end local GetFramesCacheListener local function Init(noDelay) GetFramesCacheListener = CreateFrame("Frame") GetFramesCacheListener:RegisterEvent("PLAYER_REGEN_DISABLED") GetFramesCacheListener:RegisterEvent("PLAYER_REGEN_ENABLED") GetFramesCacheListener:RegisterEvent("PLAYER_ENTERING_WORLD") GetFramesCacheListener:RegisterEvent("RAID_ROSTER_UPDATE") GetFramesCacheListener:RegisterEvent("PARTY_MEMBERS_CHANGED") GetFramesCacheListener:RegisterEvent("UNIT_PET") GetFramesCacheListener:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT") GetFramesCacheListener:SetScript("OnEvent", function(self, event, unit, ...) fixGetUnitFrameIntegrity() if event == "RAID_ROSTER_UPDATE" or "PARTY_MEMBERS_CHANGED" then wipe(unitPetState) for member in IterateGroupMembers() do unitPetState[member] = UnitExists(member .. "pet") and true or nil end end if event == "UNIT_PET" then if not (UnitIsUnit("player", unit) or UnitInParty(unit) or UnitInRaid(unit)) then return end -- skip if unit's pet existance has not changed local exists = UnitExists(unit .. "pet") and true or nil if unitPetState[unit] == exists then return else unitPetState[unit] = exists end end ScanForUnitFrames(false) end) ScanForUnitFrames(noDelay) end function lib.GetUnitFrame(target, opt) if type(GetFramesCacheListener) ~= "table" then Init(true) end opt = opt or {} setmetatable(opt, { __index = defaultOptions }) if not target then return end local ignoredFrames = CopyTable(opt.ignoreFrames) if opt.ignorePlayerFrame then for _, v in pairs(opt.playerFrames) do tinsert(ignoredFrames, v) end end if opt.ignoreTargetFrame then for _, v in pairs(opt.targetFrames) do tinsert(ignoredFrames, v) end end if opt.ignoreTargettargetFrame then for _, v in pairs(opt.targettargetFrames) do tinsert(ignoredFrames, v) end end if opt.ignorePartyFrame then for _, v in pairs(opt.partyFrames) do tinsert(ignoredFrames, v) end end if opt.ignorePartyTargetFrame then for _, v in pairs(opt.partyTargetFrames) do tinsert(ignoredFrames, v) end end if opt.ignoreFocusFrame then for _, v in pairs(opt.focusFrames) do tinsert(ignoredFrames, v) end end if opt.ignoreRaidFrame then for _, v in pairs(opt.raidFrames) do tinsert(ignoredFrames, v) end end local frames = GetUnitFrames(target, ignoredFrames) if not (opt.ignoreRaidFrame or opt.skipCellOverrides) then frames = CellGetUnitFrames(target, frames, opt.framePriorities) end if not frames then return end if not opt.returnAll then for i = 1, #opt.framePriorities do for frame, frameName in pairs(frames) do if frameName:find(opt.framePriorities[i]) then return ElvuiWorkaround(frame) end end end local next = next return ElvuiWorkaround(next(frames)) else for frame in pairs(frames) do frames[frame] = ElvuiWorkaround(frame) end return frames end end saveGetUnitFrame = lib.GetUnitFrame lib.GetFrame = lib.GetUnitFrame -- compatibility -- nameplates function lib.GetUnitNameplate(unit) if not unit then return end local nameplate = C_NamePlate.GetNamePlateForUnit(unit) if nameplate then -- credit to Exality for https://wago.io/explosiveorbs -- elvui bunny if nameplate.UnitFrame and nameplate.UnitFrame.Health and nameplate.UnitFrame.Health:IsShown() then return nameplate.UnitFrame.Health elseif nameplate.UnitFrame and nameplate.UnitFrame.Name and nameplate.UnitFrame.Name:IsShown() then return nameplate.UnitFrame.Name -- elvui someday elseif nameplate.unitFrame and nameplate.unitFrame.Health and nameplate.unitFrame.Health:IsShown() then return nameplate.unitFrame.Health elseif nameplate.unitFrame and nameplate.unitFrame.Name and nameplate.unitFrame.Name:IsShown() then return nameplate.unitFrame.Name elseif nameplate.unitFramePlater and nameplate.unitFramePlater.healthBar then -- plater -- fallback to default nameplate in case plater is not on screen and uses blizzard default (module disabled, force-blizzard functionality) return nameplate.unitFramePlater.PlaterOnScreen and nameplate.unitFramePlater.healthBar or (nameplate.UnitFrame and nameplate.UnitFrame.healthBar) or nameplate elseif nameplate.kui and nameplate.kui.HealthBar then -- kui return nameplate.kui.HealthBar elseif nameplate.extended and nameplate.extended.visual and nameplate.extended.visual.healthbar then -- tidyplates return nameplate.extended.visual.healthbar elseif nameplate.TPFrame and nameplate.TPFrame.visual and nameplate.TPFrame.visual.healthbar then -- tidyplates: threat plates return nameplate.TPFrame.visual.healthbar elseif nameplate.unitFrame and nameplate.unitFrame.Health then -- bdui nameplates return nameplate.unitFrame.Health elseif nameplate.ouf and nameplate.ouf.Health then -- bdNameplates return nameplate.ouf.Health elseif nameplate.slab and nameplate.slab.components and nameplate.slab.components.healthBar and nameplate.slab.components.healthBar.frame then -- Slab return nameplate.slab.components.healthBar.frame elseif nameplate.UnitFrame and nameplate.UnitFrame.healthBar then -- default return nameplate.UnitFrame.healthBar else return nameplate end end end