--[[ Name: LibDogTag-Unit-3.0 Revision: @project-revision@ Website: https://www.wowace.com/projects/libdogtag-unit-3-0 Description: A library to provide a markup syntax - unit-specific tags ]] local MAJOR_VERSION = "LibDogTag-Unit-3.0" local MINOR_VERSION = tonumber(("@project-date-integer@"):match("%d+")) or 33333333333333 if MINOR_VERSION > _G.DogTag_Unit_MINOR_VERSION then _G.DogTag_Unit_MINOR_VERSION = MINOR_VERSION end local select, type, pairs, ipairs, next, setmetatable = select, type, pairs, ipairs, next, setmetatable local UnitIsUnit, UnitName, UnitGUID, UnitMana, UnitExists = UnitIsUnit, UnitName, UnitGUID, UnitMana, UnitExists DogTag_Unit_funcs[#DogTag_Unit_funcs+1] = function(DogTag_Unit, DogTag) local L = DogTag_Unit.L local newList = DogTag.newList local del = DogTag.del local PartyChangedEvent = "PARTY_MEMBERS_CHANGED" if UnitIsGroupLeader then -- Changed in wow 5.0 PartyChangedEvent = "GROUP_ROSTER_UPDATE" end local frame if DogTag_Unit.oldLib and DogTag_Unit.oldLib.frame then frame = DogTag_Unit.oldLib.frame frame:UnregisterAllEvents() frame:SetScript("OnEvent", nil) frame:SetScript("OnUpdate", nil) frame:Show() else frame = CreateFrame("Frame") end DogTag_Unit.frame = frame local usedEvents = {} DogTag:AddEventHandler("Unit", "EventRequested", function(_, event) if not usedEvents[event] then usedEvents[event] = true pcall(frame.RegisterEvent, frame, event) end end) local normalUnitsWackyDependents = {} local function fireEventForDependents(event, unit, ...) local wackyDependents = normalUnitsWackyDependents[unit] if wackyDependents then unit = next(wackyDependents, nil) while unit ~= nil do local nextUnit = next(wackyDependents, unit) DogTag:FireEvent(event, unit, ...) unit = nextUnit end end end frame:SetScript("OnEvent", function(this, event, unit, ...) fireEventForDependents(event, unit, ...) if unit == "target" then if UnitIsUnit("mouseover", "target") then DogTag:FireEvent(event, "mouseover", ...) fireEventForDependents(event, "mouseover", ...) end DogTag:FireEvent(event, "playertarget", ...) fireEventForDependents(event, "playertarget", ...) elseif unit == "pet" then DogTag:FireEvent(event, "playerpet", ...) fireEventForDependents(event, "playerpet", ...) elseif type(unit) == "string" then local num = unit:match("^partypet(%d)$") if num then DogTag:FireEvent(event, "party" .. num .. "pet", ...) fireEventForDependents(event, "party" .. num .. "pet", ...) end else -- event must not be a unit event, so we can unregister it -- as the only purpose of this is to replay unit events -- with different unit IDs. AFAIK there are no unit events -- that are ever fired without a unitid, so this shouldn't -- have any false positives. frame:UnregisterEvent(event) end end) local function GetNameServer(unit) local name, realm = UnitName(unit) if name then if realm and realm ~= "" then return name .. "-" .. realm else return name end end end DogTag_Unit.GetNameServer = GetNameServer local UnitToLocale = {player = L["Player"], target = L["Target"], pet = L["%s's pet"]:format(L["Player"]), focus = L["Focus-target"], mouseover = L["Mouse-over"]} setmetatable(UnitToLocale, {__index=function(self, unit) if unit:find("pet$") then local nonPet = unit:sub(1, -4) self[unit] = L["%s's pet"]:format(self[nonPet]) return self[unit] elseif not unit:find("target$") then if unit:find("^party%d$") then local num = unit:match("^party(%d)$") self[unit] = L["Party member #%d"]:format(num) return self[unit] elseif unit:find("^raid%d%d?$") then local num = unit:match("^raid(%d%d?)$") self[unit] = L["Raid member #%d"]:format(num) return self[unit] elseif unit:find("^arena%d$") then local num = unit:match("^arena(%d)$") self[unit] = L["Arena enemy #%d"]:format(num) return self[unit] elseif unit:find("^boss%d$") then local num = unit:match("^boss(%d)$") self[unit] = L["Boss #%d"]:format(num) return self[unit] elseif unit:find("^nameplate%d$") then local num = unit:match("^nameplate(%d)$") self[unit] = L["Nameplate #%d"]:format(num) return self[unit] elseif unit:find("^partypet%d$") then local num = unit:match("^partypet(%d)$") self[unit] = UnitToLocale["party" .. num .. "pet"] return self[unit] elseif unit:find("^raidpet%d%d?$") then local num = unit:match("^raidpet(%d%d?)$") self[unit] = UnitToLocale["raid" .. num .. "pet"] return self[unit] elseif unit:find("^arenapet%d$") then local num = unit:match("^arenapet(%d)$") self[unit] = UnitToLocale["arena" .. num .. "pet"] return self[unit] end self[unit] = unit return unit end local nonTarget = unit:sub(1, -7) self[unit] = L["%s's target"]:format(self[nonTarget]) return self[unit] end}) DogTag_Unit.UnitToLocale = UnitToLocale -- [""] = true added 8/26 by Cybeloras. TellMeWhen icons (which implement DogTag) don't always check a unit, in which case they fall back on "", not "player". -- Falling back on "player" (in TellMeWhen) is counter-intuitive. Falling back on "" doesn't seem to cause any issues. local IsLegitimateUnit = { [""] = true, player = true, target = true, focus = true, pet = true, playerpet = true, mouseover = true, npc = true, NPC = true, vehicle = true } DogTag_Unit.IsLegitimateUnit = IsLegitimateUnit local IsNormalUnit = { player = true, target = true, focus = true, pet = true, playerpet = true, mouseover = true } local WACKY_UNITS = { targettarget = true, playertargettarget = true, targettargettarget = true, playertargettargettarget = true, pettarget = true, playerpettarget = true, pettargettarget = true, playerpettargettarget = true } DogTag_Unit.IsNormalUnit = IsNormalUnit for i = 1, MAX_PARTY_MEMBERS do IsLegitimateUnit["party" .. i] = true IsLegitimateUnit["partypet" .. i] = true IsLegitimateUnit["party" .. i .. "pet"] = true IsNormalUnit["party" .. i] = true IsNormalUnit["partypet" .. i] = true IsNormalUnit["party" .. i .. "pet"] = true WACKY_UNITS["party" .. i .. "target"] = true WACKY_UNITS["partypet" .. i .. "target"] = true WACKY_UNITS["party" .. i .. "pettarget"] = true WACKY_UNITS["party" .. i .. "targettarget"] = true WACKY_UNITS["partypet" .. i .. "targettarget"] = true WACKY_UNITS["party" .. i .. "pettargettarget"] = true end for i = 1, MAX_RAID_MEMBERS do IsLegitimateUnit["raid" .. i] = true IsNormalUnit["raid" .. i] = true IsLegitimateUnit["raidpet" .. i] = true IsLegitimateUnit["raid" .. i .. "pet"] = true WACKY_UNITS["raid" .. i .. "target"] = true WACKY_UNITS["raidpet" .. i] = true WACKY_UNITS["raid" .. i .. "pet"] = true WACKY_UNITS["raidpet" .. i .. "target"] = true WACKY_UNITS["raid" .. i .. "pettarget"] = true end for i = 1, 40 do -- There is no const for max nameplate units, -- but it currently appears to be 40 based on in-game testing -- (i.e. "pull everything in stockades") -- (at some point in the past it used to be 30). IsLegitimateUnit["nameplate" .. i] = true IsNormalUnit["nameplate" .. i] = true end for i = 1, MAX_BOSS_FRAMES do IsLegitimateUnit["boss" .. i] = true IsNormalUnit["boss" .. i] = true WACKY_UNITS["boss" .. i .. "target"] = true WACKY_UNITS["boss" .. i .. "targettarget"] = true end for i = 1, 5 do IsLegitimateUnit["arena" .. i] = true IsLegitimateUnit["arenapet" .. i] = true IsLegitimateUnit["arena" .. i .. "pet"] = true IsNormalUnit["arena" .. i] = true IsNormalUnit["arenapet" .. i] = true IsNormalUnit["arena" .. i .. "pet"] = true WACKY_UNITS["arena" .. i .. "target"] = true WACKY_UNITS["arenapet" .. i .. "target"] = true WACKY_UNITS["arena" .. i .. "pettarget"] = true WACKY_UNITS["arena" .. i .. "targettarget"] = true WACKY_UNITS["arenapet" .. i .. "targettarget"] = true WACKY_UNITS["arena" .. i .. "pettargettarget"] = true end setmetatable(IsLegitimateUnit, { __index = function(self, key) if type(key) ~= "string" then return false end if key:match("target$") then self[key] = self[key:sub(1, -7)] return self[key] end self[key] = false return false end, __call = function(self, key) return self[key] end}) -- Setting these on the DogTag lib root for backwards-compat with addons that are peeking at internals, e.g ThreatPlates: -- https://github.com/Backupiseasy/ThreatPlates/blob/79c933e25dbc3bb0b99b63f34eec97d0be9dc64d/Init.lua#L127 -- However, they could get nulled out if a newer version of LibDogTag-3.0 is loaded later. DogTag.IsNormalUnit = IsNormalUnit DogTag.IsLegitimateUnit = IsLegitimateUnit DogTag.UnitToLocale = UnitToLocale local unitToGUID = {} local guidToUnits = {} local wackyUnitToBestUnit = {} local function getBestUnit(guid) if not guid then return nil end local guidToUnits__guid = guidToUnits[guid] if not guidToUnits__guid then return nil end for unit in pairs(guidToUnits__guid) do if IsNormalUnit[unit] and unit ~= "mouseover" then return unit end end return nil end local function calculateBestUnit(unit) local bestUnit = getBestUnit(UnitGUID(unit)) local oldBestUnit = wackyUnitToBestUnit[unit] if bestUnit == oldBestUnit then return end wackyUnitToBestUnit[unit] = bestUnit local normalUnitsWackyDependents__oldBestUnit = normalUnitsWackyDependents[oldBestUnit] if normalUnitsWackyDependents__oldBestUnit then normalUnitsWackyDependents__oldBestUnit[unit] = nil if not next(normalUnitsWackyDependents__oldBestUnit) then normalUnitsWackyDependents[oldBestUnit] = del(normalUnitsWackyDependents__oldBestUnit) end end if bestUnit then local normalUnitsWackyDependents__bestUnit = normalUnitsWackyDependents[bestUnit] if not normalUnitsWackyDependents__bestUnit then normalUnitsWackyDependents__bestUnit = newList() normalUnitsWackyDependents[bestUnit] = normalUnitsWackyDependents__bestUnit end normalUnitsWackyDependents__bestUnit[unit] = true end end local function refreshGUID(unit) local guid = UnitGUID(unit) local oldGuid = unitToGUID[unit] if guid == oldGuid then return end unitToGUID[unit] = guid if oldGuid then local guidToUnits_oldGuid = guidToUnits[oldGuid] if guidToUnits_oldGuid then guidToUnits_oldGuid[unit] = nil if not next(guidToUnits_oldGuid) then guidToUnits[oldGuid] = del(guidToUnits_oldGuid) end end end if guid then local guidToUnits_guid = guidToUnits[guid] if not guidToUnits_guid then guidToUnits_guid = newList() guidToUnits[guid] = guidToUnits_guid end guidToUnits_guid[unit] = true end for wackyUnit in pairs(WACKY_UNITS) do if wackyUnitToBestUnit[wackyUnit] == unit or (guid and unitToGUID[wackyUnit] == guid) then calculateBestUnit(wackyUnit) end end end local function PARTY_MEMBERS_CHANGED() for unit in pairs(IsNormalUnit) do local guid = unitToGUID[unit] refreshGUID(unit) local newGUID = unitToGUID[unit] if guid ~= newGUID then DogTag:FireEvent("UnitChanged", unit) end end end DogTag:AddEventHandler("Unit", PartyChangedEvent, PARTY_MEMBERS_CHANGED) DogTag:AddEventHandler("Unit", "PLAYER_ENTERING_WORLD", PARTY_MEMBERS_CHANGED) PARTY_MEMBERS_CHANGED() DogTag:AddEventHandler("Unit", "PLAYER_LOGIN", PARTY_MEMBERS_CHANGED) local function doNothing() end local function IterateUnitsWithGUID(guid) local t = guidToUnits[guid] if not t then return doNothing else return pairs(t) end end DogTag_Unit.IterateUnitsWithGUID = IterateUnitsWithGUID local function searchForNameTag(ast) if type(ast) ~= "table" then return false end if ast[1] == "tag" and ast[2]:lower() == "name" then return true end for i = 2, #ast do if searchForNameTag(ast[i]) then return true end end if ast.kwarg then for k, v in pairs(ast.kwarg) do if searchForNameTag(v) then return true end end end return false end DogTag:AddCompilationStep("Unit", "start", function(t, ast, kwargTypes, extraKwargs) -- Since DogTag_Unit isn't upvalued in the same way that DogTag is -- in the function compilation, we instead copy the functions from DogTag_Unit -- that we need onto DogTag so we can used them in here. -- This copy is necessary on every compilation because if a newer version -- of LibDogTag-3.0 is loaded, it will wipe the DogTag lib object. DogTag.IsLegitimateUnit = DogTag_Unit.IsLegitimateUnit DogTag.UnitToLocale = DogTag_Unit.UnitToLocale if kwargTypes["unit"] then t[#t+1] = [=[if not DogTag.IsLegitimateUnit[]=] t[#t+1] = extraKwargs["unit"][1] t[#t+1] = [=[] then]=] t[#t+1] = "\n" t[#t+1] = [=[return ("Bad unit: %q"):format(]=] t[#t+1] = extraKwargs["unit"][1] t[#t+1] = [=[ or tostring(]=] t[#t+1] = extraKwargs["unit"][1] t[#t+1] = [=[)), nil;]=] t[#t+1] = "\n" t[#t+1] = [=[end;]=] t[#t+1] = "\n" -- I really don't see this point to this. -- It just prevents users from using custom tags that override the unit specified by the kwargs passed to AddFontString. -- So I commented it out. t[#t+1] = [=[if ]=] t[#t+1] = extraKwargs["unit"][1] t[#t+1] = [=[ ~= "player" and not UnitExists(]=] t[#t+1] = extraKwargs["unit"][1] t[#t+1] = [=[) then]=] t[#t+1] = "\n" t[#t+1] = [=[return ]=] if searchForNameTag(ast) then t[#t+1] = [=[DogTag.UnitToLocale[]=] t[#t+1] = extraKwargs["unit"][1] t[#t+1] = [=[]]=] else t[#t+1] = [=[nil]=] end t[#t+1] = [=[, nil;]=] t[#t+1] = "\n" t[#t+1] = [=[end;]=] t[#t+1] = "\n" end end) DogTag:AddCompilationStep("Unit", "tag", function(ast, t, tag, tagData, kwargs, extraKwargs, compiledKwargs) if compiledKwargs["unit"] and kwargs["unit"] ~= extraKwargs then if type(kwargs["unit"]) ~= "table" then if not IsLegitimateUnit[kwargs["unit"]] then t[#t+1] = [=[do]=] t[#t+1] = "\n" t[#t+1] = [=[return ]=] t[#t+1] = [=[("Bad unit: %q"):format(tostring(]=] t[#t+1] = compiledKwargs["unit"][1] t[#t+1] = [=[));]=] t[#t+1] = "\n" t[#t+1] = [=[end;]=] t[#t+1] = "\n" end else t[#t+1] = [=[if ]=] t[#t+1] = compiledKwargs["unit"][1] t[#t+1] = [=[ and not DogTag.IsLegitimateUnit[]=] t[#t+1] = compiledKwargs["unit"][1] t[#t+1] = [=[] then]=] t[#t+1] = "\n" t[#t+1] = [=[return ]=] t[#t+1] = [=[("Bad unit: %q"):format(tostring(]=] t[#t+1] = compiledKwargs["unit"][1] t[#t+1] = [=[));]=] t[#t+1] = "\n" t[#t+1] = [=[end;]=] t[#t+1] = "\n" end end if tag == "IsUnit" then if type(kwargs["other"]) ~= "table" then if not IsLegitimateUnit[kwargs["other"]] then t[#t+1] = [=[do]=] t[#t+1] = "\n" t[#t+1] = [=[return ]=] t[#t+1] = [=[("Bad unit: %q"):format(tostring(]=] t[#t+1] = compiledKwargs["other"][1] t[#t+1] = [=[));]=] t[#t+1] = "\n" t[#t+1] = [=[end;]=] t[#t+1] = "\n" end else t[#t+1] = [=[if not DogTag.IsLegitimateUnit[]=] t[#t+1] = compiledKwargs["other"][1] t[#t+1] = [=[] then]=] t[#t+1] = "\n" t[#t+1] = [=[return ]=] t[#t+1] = [=[("Bad unit: %q"):format(tostring(]=] t[#t+1] = compiledKwargs["other"][1] t[#t+1] = [=[));]=] t[#t+1] = "\n" t[#t+1] = [=[end;]=] t[#t+1] = "\n" end end end) DogTag:AddCompilationStep("Unit", "tagevents", function(ast, t, u, tag, tagData, kwargs, extraKwargs, compiledKwargs, events, returns) if compiledKwargs["unit"] and kwargs["unit"] ~= extraKwargs and kwargs["unit"] ~= "player" then t[#t+1] = [=[if ]=] t[#t+1] = compiledKwargs["unit"][1] t[#t+1] = [=[ and UnitExists(]=] t[#t+1] = compiledKwargs["unit"][1] t[#t+1] = [=[) then]=] t[#t+1] = "\n" u[#u+1] = [=[end;]=] u[#u+1] = "\n" if not returns["boolean"] then returns["nil"] = true end end end) DogTag:AddEventHandler("Unit", "PLAYER_TARGET_CHANGED", function(event, ...) refreshGUID("target") DogTag:FireEvent("UnitChanged", "target") DogTag:FireEvent("UnitChanged", "playertarget") end) DogTag:AddEventHandler("Unit", "PLAYER_FOCUS_CHANGED", function(event, ...) refreshGUID("focus") DogTag:FireEvent("UnitChanged", "focus") end) DogTag:AddEventHandler("Unit", "UNIT_TARGET", function(event, unit) DogTag:FireEvent("UnitChanged", unit .. "target") end) DogTag:AddEventHandler("Unit", "UNIT_TARGETABLE_CHANGED", function(event, unit) refreshGUID(unit) DogTag:FireEvent("UnitChanged", unit) end) DogTag:AddEventHandler("Unit", "NAME_PLATE_UNIT_ADDED", function(event, unit) refreshGUID(unit) DogTag:FireEvent("UnitChanged", unit) end) DogTag:AddEventHandler("Unit", "NAME_PLATE_UNIT_REMOVED", function(event, unit) refreshGUID(unit) DogTag:FireEvent("UnitChanged", unit) end) DogTag:AddEventHandler("Unit", "UNIT_PET", function(event, unit) if unit == "player" then unit = "" end local unit_pet = unit .. "pet" refreshGUID(unit_pet) DogTag:FireEvent("UnitChanged", unit_pet) end) DogTag:AddEventHandler("Unit", "UPDATE_MOUSEOVER_UNIT", function(event, ...) refreshGUID("mouseover") DogTag:FireEvent("UnitChanged", "mouseover") end) DogTag:AddEventHandler("Unit", "INSTANCE_ENCOUNTER_ENGAGE_UNIT", function(event, ...) for i = 1, MAX_BOSS_FRAMES do refreshGUID("boss"..i) DogTag:FireEvent("UnitChanged", "boss"..i) end end) -- These 4 tables are safe to upvalue from DogTag -- because the same tables are reused across DogTag upgrades: local fsToKwargs = DogTag.fsToKwargs local fsToNSList = DogTag.fsToNSList local fsNeedUpdate = DogTag.fsNeedUpdate local fsNeedQuickUpdate = DogTag.fsNeedQuickUpdate local nsListHasUnit = setmetatable({}, { __index = function(self, key) for _, ns in ipairs(DogTag.unpackNamespaceList[key]) do if ns == "Unit" then self[key] = true return true end end self[key] = false return false end }) local nextRefreshGUIDsTime = 0 DogTag:AddTimerHandler("Unit", function(num, currentTime) if nextRefreshGUIDsTime > currentTime then return end nextRefreshGUIDsTime = currentTime + 15 PARTY_MEMBERS_CHANGED() end, 1) local nextUpdateWackyUnitsTime = 0 DogTag:AddTimerHandler("Unit", function(num, currentTime) local mouseoverGUID = UnitGUID("mouseover") if mouseoverGUID ~= unitToGUID["mouseover"] then unitToGUID["mouseover"] = mouseoverGUID DogTag:FireEvent("UnitChanged", "mouseover") end if currentTime >= nextUpdateWackyUnitsTime then local checkYield = DogTag.checkYield for unit in pairs(WACKY_UNITS) do local oldGUID = unitToGUID[unit] refreshGUID(unit) local newGUID = unitToGUID[unit] if oldGUID ~= newGUID then DogTag:FireEvent("UnitChanged", unit) -- This loop is where things get hung up all the time, -- so we should check for a yield right here. checkYield() end end nextUpdateWackyUnitsTime = currentTime + 0.5 DogTag:FireEvent("UpdateWackyUnits") for fs, nsList in pairs(fsToNSList) do if nsListHasUnit[nsList] then local kwargs = fsToKwargs[fs] local unit = kwargs and kwargs["unit"] if unit and not IsNormalUnit[unit] and not wackyUnitToBestUnit[unit] then fsNeedUpdate[fs] = true end end end end end) DogTag:AddTimerHandler("Unit", function(num, currentTime) local exists = not not UnitExists("mouseover") if not exists then for fs, nsList in pairs(fsToNSList) do if nsListHasUnit[nsList] and not fs.__DT_dontcancelupdatesforMO then local kwargs = fsToKwargs[fs] if kwargs and kwargs["unit"] == "mouseover" then fsNeedUpdate[fs] = nil fsNeedQuickUpdate[fs] = nil end end end end end, 9) DogTag:AddCompilationStep("Unit", "tagevents", function(ast, t, u, tag, tagData, kwargs, extraKwargs, compiledKwargs, events, returns) if compiledKwargs["unit"] then events["UnitChanged#$unit"] = true events[PartyChangedEvent] = true events["PLAYER_ENTERING_WORLD"] = true local kwargs_unit = kwargs["unit"] if (type(kwargs_unit) ~= "table" or kwargs_unit[1] ~= "kwarg" or kwargs_unit[2] ~= "unit") and kwargs_unit ~= extraKwargs and (type(kwargs_unit) ~= "string" or not IsNormalUnit[kwargs_unit]) then events["UpdateWackyUnits"] = true end end end) end