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.

621 lines
19 KiB

---------------------------------------------------------------------
-- 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