--------------------------------------------------------------------- -- File: Cell\Libs\LibGroupInfo.lua -- Author: enderneko (enderneko-dev@outlook.com) -- Created : 2022-07-29 15:04 +08:00 -- Modified: 2024-06-18 10:50 +08:00 --------------------------------------------------------------------- local MAJOR, MINOR = "LibGroupInfo", 6 local lib = LibStub:NewLibrary(MAJOR, MINOR) if not lib then return end -- already loaded lib.callbacks = LibStub("CallbackHandler-1.0"):New(lib) if not lib.callbacks then error(MAJOR.." requires CallbackHandler") end local UPDATE_EVENT = "GroupInfo_Update" -- guid, unit, cache[guid] local UPDATE_BASE_EVENT = "GroupInfo_UpdateBase" -- guid, unit, cache[guid] local QUEUE_EVENT = "GroupInfo_QueueStatus" local PLAYER_GUID local RETRY_INTERVAL = 1.5 local MAX_ATTEMPTS = 3 local IS_RETAIL = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE local IS_WRATH = WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC local debugMode = false local function Print(...) if debugMode then print(...) end end -- store inspect data local cache = { -- [guid] = { -- unit = (string), -- name = (string), -- realm = (string), -- class = (string, EN uppercase), -- level = (number), -- race = (string, EN), -- gender = ("unknown", "male", "female"), -- faction = ("Alliance", "Horde", "Neutral", nil), -- assignedRole = ("TANK", "HEALER", "DAMAGER", "NONE"), -- specId = (number), -- specName = (string), -- specRole = ("TANK", "MELEE", "RANGED", "DAMAGER", "HEALER"), -- specIcon = (number), -- inspected = (boolean), -- } } lib.cache = cache function lib:GetCachedInfo(guid) return guid and cache[guid] end function lib:GuidToUnit(guid) if cache[guid] then return cache[guid].unit end end -- static data local genders = {"unknown", "male", "female"} local specData = {} local specRoles = { -- Death Knight [250] = "TANK", -- Blood [251] = "MELEE", -- Frost [252] = "MELEE", -- Unholy [1455] = "DAMAGER", -- Demon Hunter [577] = "MELEE", -- Havoc [581] = "TANK", -- Vengeance [1456] = "DAMAGER", -- Druid [102] = "RANGED", -- Balance [103] = "MELEE", -- Feral [104] = "TANK", -- Guardian [105] = "HEALER", -- Restoration [1447] = "DAMAGER", -- Evoker [1467] = "RANGED", -- Devastation [1468] = "HEALER", -- Preservation [1473] = "RANGED", -- Augmentation [1465] = "DAMAGER", -- Hunter [253] = "RANGED", -- Beast Mastery [254] = "RANGED", -- Marksmanship [255] = "MELEE", -- Survival [1448] = "DAMAGER", -- Mage [62] = "RANGED", -- Arcane [63] = "RANGED", -- Fire [64] = "RANGED", -- Frost [1449] = "DAMAGER", -- Monk [268] = "TANK", -- Brewmaster [269] = "MELEE", -- Windwalker [270] = "HEALER", -- Mistweaver [1450] = "DAMAGER", -- Paladin [65] = "HEALER", -- Holy [66] = "TANK", -- Protection [70] = "MELEE", -- Retribution [1451] = "DAMAGER", -- Priest [256] = "HEALER", -- Discipline [257] = "HEALER", -- Holy [258] = "RANGED", -- Shadow [1452] = "DAMAGER", -- Rogue [259] = "MELEE", -- Assassination [260] = "MELEE", -- Combat [261] = "MELEE", -- Subtlety [1453] = "DAMAGER", -- Shaman [262] = "RANGED", -- Elemental [263] = "MELEE", -- Enhancement [264] = "HEALER", -- Restoration [1444] = "DAMAGER", -- Warlock [265] = "RANGED", -- Affliction [266] = "RANGED", -- Demonology [267] = "RANGED", -- Destruction [1454] = "DAMAGER", -- Warrior [71] = "MELEE", -- Arms [72] = "MELEE", -- Fury [73] = "TANK", -- Protection [1446] = "DAMAGER", } lib.specData = specData lib.specRoles = specRoles -- functions local NotifyInspect = NotifyInspect local UnitGUID = UnitGUID local UnitClassBase = UnitClassBase local UnitIsUnit = UnitIsUnit local UnitIsDead = UnitIsDead local UnitIsConnected = UnitIsConnected local UnitIsVisible = UnitIsVisible local CanInspect = CanInspect local GetSpecialization = GetSpecialization local GetSpecializationInfo = GetSpecializationInfo local GetInspectSpecialization = GetInspectSpecialization local UnitNameUnmodified = UnitNameUnmodified local GetNormalizedRealmName = GetNormalizedRealmName local UnitLevel = UnitLevel local UnitRace = UnitRace local UnitSex = UnitSex local UnitFactionGroup = UnitFactionGroup local IsInInstance = IsInInstance local IsInRaid = IsInRaid local IsInGroup = IsInGroup local GetNumGroupMembers = GetNumGroupMembers local UnitInParty = UnitInParty local UnitInRaid = UnitInRaid local UnitGroupRolesAssigned = UnitGroupRolesAssigned local GetNumTalentTabs = GetNumTalentTabs local GetTalentTabInfo = GetTalentTabInfo -- event frame local frame = CreateFrame("Frame", MAJOR.."Frame") frame:Hide() frame:RegisterEvent("PLAYER_LOGIN") -- frame:RegisterEvent("PLAYER_LOGOUT") frame:SetScript("OnEvent", function(self, event, ...) self[event](self, ...) end) -- prepare spec data (name, icon, role) local function CacheSpecData() for classId = 1, GetNumClasses() do for specIndex = 1, GetNumSpecializationsForClassID(classId) do local id, name, description, icon, role = GetSpecializationInfoForClassID(classId, specIndex) specData[id] = { ["name"] = name, ["icon"] = icon, ["role"] = specRoles[id], } end -- initials if IS_RETAIL then local id, name, description, icon, role = GetSpecializationInfoForClassID(classId, 5) specData[id] = { ["name"] = name, ["icon"] = icon, ["role"] = specRoles[id], } end end end local function UpdateBaseInfo(unit, guid) if not guid then return end if not cache[guid] then cache[guid] = {} end if IS_WRATH then if not cache[guid]["talents"] then cache[guid]["talents"] = {} end end -- general cache[guid].unit = unit cache[guid].name, cache[guid].realm = UnitNameUnmodified(unit) if not cache[guid].realm then cache[guid].realm = GetNormalizedRealmName() end cache[guid].class = UnitClassBase(unit) cache[guid].level = UnitLevel(unit) cache[guid].race = select(2, UnitRace(unit)) cache[guid].gender = genders[UnitSex(unit)] cache[guid].faction = UnitFactionGroup(unit) cache[guid].assignedRole = UnitGroupRolesAssigned(unit) --! fire lib.callbacks:Fire(UPDATE_BASE_EVENT, guid, unit, cache[guid]) return guid end local function BuildAndNotify(unit) Print("|cffff7777LGI:BuildAndNotify|r", unit) local guid = UnitGUID(unit) UpdateBaseInfo(unit, guid) local specId, role if UnitIsUnit(unit, "player") then local specIndex = GetSpecialization() specId, _, _, _, role = GetSpecializationInfo(specIndex) else specId = GetInspectSpecialization(unit) role = select(5, GetSpecializationInfoByID(specId)) -- if not (UnitIsConnected(unit) or UnitIsVisible(unit)) then -- cache[guid].notVisible = true -- else -- cache[guid].notVisible = nil -- end end cache[guid].role = role -- spec if specId and specData[specId] then cache[guid].specId = specId cache[guid].specName = specData[specId].name cache[guid].specRole = specData[specId].role cache[guid].specIcon = specData[specId].icon cache[guid].inspected = true else cache[guid].specId = 0 cache[guid].specName = nil cache[guid].specRole = nil cache[guid].specIcon = nil cache[guid].inspected = nil end --! fire lib.callbacks:Fire(UPDATE_EVENT, guid, unit, cache[guid]) end local function BuildAndNotify_Wrath(unit) Print("|cffff7777LGI:BuildAndNotify_Wrath|r", unit) local guid = UnitGUID(unit) UpdateBaseInfo(unit, guid) -- spec local isInspect = not UnitIsUnit(unit, "player") local maxPoints = 0 if isInspect then for i = 1, GetNumTalentTabs(true) do local name, texture, pointsSpent, fileName = GetTalentTabInfo(i, true, false) cache[guid]["talents"][fileName] = { ["points"] = pointsSpent, ["name"] = name, ["icon"] = texture, } if pointsSpent > maxPoints then maxPoints = pointsSpent cache[guid].specName = name cache[guid].specIcon = texture end end else for i = 1, GetNumTalentTabs() do local name, texture, pointsSpent, fileName = GetTalentTabInfo(i) cache[guid]["talents"][fileName] = { ["points"] = pointsSpent, ["name"] = name, ["icon"] = texture, } if pointsSpent > maxPoints then maxPoints = pointsSpent cache[guid].specName = name cache[guid].specIcon = texture end end end --! fire lib.callbacks:Fire(UPDATE_EVENT, guid, unit, cache[guid]) end local function Query(unit) -- if InCombatLockdown() then return end if UnitIsDead("player") then return end if IsInGroup() and not (UnitInParty(unit) or UnitInRaid(unit)) then return end if IS_RETAIL then BuildAndNotify(unit) else BuildAndNotify_Wrath(unit) end end --------------------------------------------------------------------- -- login & reload & enter/leave instance --------------------------------------------------------------------- function frame:PLAYER_LOGIN() PLAYER_GUID = UnitGUID("player") if IS_RETAIL then cache[PLAYER_GUID] = {} CacheSpecData() frame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED") else cache[PLAYER_GUID] = {["talents"]={}} -- frame:RegisterEvent("UNIT_AURA") end frame:RegisterEvent("PLAYER_ENTERING_WORLD") frame:RegisterEvent("PLAYER_REGEN_ENABLED") frame:RegisterEvent("PLAYER_REGEN_DISABLED") frame:RegisterEvent("INSPECT_READY") frame:RegisterEvent("GROUP_ROSTER_UPDATE") frame:RegisterEvent("UNIT_LEVEL") frame:RegisterEvent("UNIT_NAME_UPDATE") -- frame:RegisterEvent("UNIT_PHASE") -- frame:RegisterEvent("PARTY_MEMBER_ENABLE") end local inInstance function frame:PLAYER_ENTERING_WORLD(isLogin, isReload) local isIn, iType = IsInInstance() local shouldUpdate if isIn then -- enter inInstance = true shouldUpdate = true elseif inInstance then -- leave inInstance = nil shouldUpdate = true elseif isLogin or isReload then -- login/reload shouldUpdate = true end if shouldUpdate then frame:Hide() wipe(lib.queue) wipe(lib.queueGUIDs) for _, t in pairs(cache) do t.inspected = nil end -- update self Query("player") -- update group frame:GROUP_ROSTER_UPDATE(true) end end --------------------------------------------------------------------- -- inspection queue --------------------------------------------------------------------- local queue = {} lib.queue = queue local queueGUIDs = {} lib.queueGUIDs = queueGUIDs local elapsedTime = 0 frame:SetScript("OnUpdate", function(self, elapsed) elapsedTime = elapsedTime + elapsed if elapsedTime >= 0.25 then elapsedTime = 0 local guid = queue[1] if guid then if queueGUIDs[guid] then if queueGUIDs[guid].status == "waiting" then queueGUIDs[guid].status = "requesting" queueGUIDs[guid].attempts = queueGUIDs[guid].attempts + 1 queueGUIDs[guid].lastRequest = GetTime() Print("|cffffff33LGI:INSPECT_REQUESTING|r", guid, queueGUIDs[guid].unit) lib.callbacks:Fire(QUEUE_EVENT, guid, queueGUIDs[guid].unit, "INSPECT_REQUESTING") NotifyInspect(queueGUIDs[guid].unit) elseif queueGUIDs[guid].status == "requesting" then -- give it another shot if queueGUIDs[guid].attempts < MAX_ATTEMPTS then if GetTime() - queueGUIDs[guid].lastRequest >= RETRY_INTERVAL then queueGUIDs[guid].attempts = queueGUIDs[guid].attempts + 1 queueGUIDs[guid].lastRequest = GetTime() Print("|cffffff33LGI:INSPECT_RETRYING|r", guid, queueGUIDs[guid].unit) lib.callbacks:Fire(QUEUE_EVENT, guid, queueGUIDs[guid].unit, "INSPECT_RETRYING") NotifyInspect(queueGUIDs[guid].unit) end else -- reach max attempts Print("|cffffff33LGI:INSPECT_FAILED|r", guid, queueGUIDs[guid].unit) lib.callbacks:Fire(QUEUE_EVENT, guid, queueGUIDs[guid].unit, "INSPECT_FAILED") tremove(queue, 1) queueGUIDs[guid] = nil end end else -- INSPECT_READY tremove(queue, 1) end else -- none left frame:Hide() wipe(queue) wipe(queueGUIDs) end end end) local function AddToQueue(unit, guid) if IS_WRATH then if not UnitIsConnected(unit) or not CheckInteractDistance(unit, 1) or not CanInspect(unit) then UpdateBaseInfo(unit, guid) return end else if not UnitIsConnected(unit) or not CanInspect(unit) then UpdateBaseInfo(unit, guid) return end end Print("|cffffff33LGI:AddToQueue|r", guid, unit) lib.callbacks:Fire(QUEUE_EVENT, guid, unit, "INSPECT_WAITING") queueGUIDs[guid] = { ["unit"] = unit, ["attempts"] = 0, ["status"] = "waiting", } tinsert(queue, guid) if not InCombatLockdown() then frame:Show() end end --------------------------------------------------------------------- -- INSPECT_READY: ready to query --------------------------------------------------------------------- function frame:INSPECT_READY(guid) if queueGUIDs[guid] then Print("|cffffff33LGI:INSPECT_READY|r", guid, queueGUIDs[guid].unit) lib.callbacks:Fire(QUEUE_EVENT, guid, queueGUIDs[guid].unit, "INSPECT_READY") Query(queueGUIDs[guid].unit) queueGUIDs[guid] = nil end end --------------------------------------------------------------------- -- GROUP_ROSTER_UPDATE: update queue --------------------------------------------------------------------- local wasInGroup local function IterateAllUnits() cache[PLAYER_GUID].unit = "player" local currentMembers = {[PLAYER_GUID] = true} if IsInRaid() then wasInGroup = true for i = 1, GetNumGroupMembers() do local unit = "raid"..i local guid = UnitGUID(unit) currentMembers[guid] = true if not (UnitIsUnit(unit, "player") or (cache[guid] and cache[guid].inspected) or queueGUIDs[guid]) then AddToQueue(unit, guid) end end cache[PLAYER_GUID].unit = "raid"..UnitInRaid("player") elseif IsInGroup() then wasInGroup = true for i = 1, GetNumGroupMembers()-1 do local unit = "party"..i local guid = UnitGUID(unit) currentMembers[guid] = true if not ((cache[guid] and cache[guid].inspected) or queueGUIDs[guid]) then AddToQueue(unit, guid) end end elseif wasInGroup then wasInGroup = nil for guid in pairs(cache) do if guid ~= PLAYER_GUID then cache[guid] = nil end end frame:Hide() wipe(queueGUIDs) wipe(queue) end -- remove not in group if wasInGroup then for guid in pairs(cache) do if not currentMembers[guid] then cache[guid] = nil queueGUIDs[guid] = nil end end end end local timer function frame:GROUP_ROSTER_UPDATE(immediate) if timer then timer:Cancel() end if immediate then IterateAllUnits() else timer = C_Timer.NewTimer(1, IterateAllUnits) end end local forceUpdateAvailable = true function lib:ForceUpdate() if not forceUpdateAvailable then return end forceUpdateAvailable = false C_Timer.After(10, function() forceUpdateAvailable = true end) frame:PLAYER_ENTERING_WORLD(true) end --------------------------------------------------------------------- -- other events: update --------------------------------------------------------------------- function frame:PLAYER_SPECIALIZATION_CHANGED(unit) if not UnitIsPlayer(unit) then return end if strfind(unit, "target") or strfind(unit, "nameplate") then return end if UnitIsUnit(unit, "player") then Query(unit) else local guid = UnitGUID(unit) if cache[guid] then cache[guid].inspected = nil end if queueGUIDs[guid] then queueGUIDs[guid].attempts = 0 -- reset attempts if exists in queue else AddToQueue(unit, guid) end end end function frame:UNIT_NAME_UPDATE(unit) frame:PLAYER_SPECIALIZATION_CHANGED(unit) end -- function frame:UNIT_PHASE(unit) -- frame:PLAYER_SPECIALIZATION_CHANGED(unit) -- end -- function frame:PARTY_MEMBER_ENABLE(unit) -- frame:PLAYER_SPECIALIZATION_CHANGED(unit) -- end function frame:UNIT_LEVEL(unit) local guid = UnitGUID(unit) if cache[guid] then cache[guid].level = UnitLevel(unit) end end -- local lastUpdate = {} -- function frame:UNIT_AURA(unit) -- print(unit) -- if InCombatLockdown() then return end -- if not (strfind(unit, "^party") or strfind(unit, "^raid")) then return end -- if not UnitIsPlayer(unit) then return end -- local guid = UnitGUID(unit) -- if not lastUpdate[guid] or GetTime() - lastUpdate[guid] > 600 then -- lastUpdate[guid] = GetTime() -- AddToQueue(unit, guid) -- end -- end --------------------------------------------------------------------- -- combat check --------------------------------------------------------------------- function frame:PLAYER_REGEN_ENABLED() if #queue ~= 0 then frame:Show() end end function frame:PLAYER_REGEN_DISABLED() frame:Hide() end