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.
996 lines
32 KiB
996 lines
32 KiB
-- vim: ts=2 sw=2 ai et fenc=utf8
|
|
|
|
--[[
|
|
-- These events can be registered for using the regular CallbackHandler ways.
|
|
--
|
|
-- "GroupInSpecT_Update", guid, unit, info
|
|
-- "GroupInSpecT_Remove, guid
|
|
-- "GroupInSpecT_InspectReady", guid, unit
|
|
--
|
|
-- Where <info> is a table containing some or all of the following:
|
|
-- .guid
|
|
-- .name
|
|
-- .realm
|
|
-- .race
|
|
-- .race_localized
|
|
-- .class
|
|
-- .class_localized
|
|
-- .class_id
|
|
-- .gender -- 2 = male, 3 = female
|
|
-- .global_spec_id
|
|
-- .spec_index
|
|
-- .spec_name_localized
|
|
-- .spec_description
|
|
-- .spec_icon
|
|
-- .spec_background
|
|
-- .spec_role
|
|
-- .spec_role_detailed
|
|
-- .spec_group -- active spec group (1/2/nil)
|
|
-- .talents = {
|
|
-- [<talent_id>] = {
|
|
-- .tier
|
|
-- .column
|
|
-- .name_localized
|
|
-- .icon
|
|
-- .talent_id
|
|
-- .spell_id
|
|
-- }
|
|
-- ...
|
|
-- }
|
|
-- .pvp_talents = {
|
|
-- [<talent_id>] = {
|
|
-- .name_localized
|
|
-- .icon
|
|
-- .talent_id
|
|
-- .spell_id
|
|
-- }
|
|
-- ...
|
|
-- }
|
|
-- .lku -- last known unit id
|
|
-- .not_visible
|
|
--
|
|
-- Functions for external use:
|
|
--
|
|
-- lib:Rescan (guid or nil)
|
|
-- Force a rescan of the given group member GUID, or of all current group members if nil.
|
|
--
|
|
-- lib:QueuedInspections ()
|
|
-- Returns an array of GUIDs of outstanding inspects.
|
|
--
|
|
-- lib:StaleInspections ()
|
|
-- Returns an array of GUIDs for which the data has become stale and is
|
|
-- awaiting an update (no action required, the refresh happens internally).
|
|
-- Due to Blizzard exposing no events on (re/un)talent, there will be
|
|
-- frequent marking of inspect data as being stale.
|
|
--
|
|
-- lib:GetCachedInfo (guid)
|
|
-- Returns the cached info for the given GUID, if available, nil otherwise.
|
|
-- Information is cached for current group members only.
|
|
--
|
|
-- lib:GroupUnits ()
|
|
-- Returns an array with the set of unit ids for the current group.
|
|
--]]
|
|
|
|
local MAJOR, MINOR = "LibGroupInSpecT-1.1", 91
|
|
|
|
if not LibStub then error(MAJOR.." requires LibStub") end
|
|
local lib = LibStub:NewLibrary (MAJOR, MINOR)
|
|
if not lib then return end
|
|
|
|
lib.events = lib.events or LibStub ("CallbackHandler-1.0"):New (lib)
|
|
if not lib.events then error(MAJOR.." requires CallbackHandler") end
|
|
|
|
|
|
local UPDATE_EVENT = "GroupInSpecT_Update"
|
|
local REMOVE_EVENT = "GroupInSpecT_Remove"
|
|
local INSPECT_READY_EVENT = "GroupInSpecT_InspectReady"
|
|
local QUEUE_EVENT = "GroupInSpecT_QueueChanged"
|
|
|
|
local COMMS_PREFIX = "LGIST11"
|
|
local COMMS_FMT = "1"
|
|
local COMMS_DELIM = "\a"
|
|
|
|
local INSPECT_DELAY = 1.5
|
|
local INSPECT_TIMEOUT = 10 -- If we get no notification within 10s, give up on unit
|
|
|
|
local MAX_ATTEMPTS = 2
|
|
|
|
--[===[@debug@
|
|
lib.debug = false
|
|
local function debug (...)
|
|
if lib.debug then -- allow programmatic override of debug output by client addons
|
|
print (...)
|
|
end
|
|
end
|
|
--@end-debug@]===]
|
|
|
|
function lib.events:OnUsed(target, eventname)
|
|
if eventname == INSPECT_READY_EVENT then
|
|
target.inspect_ready_used = true
|
|
end
|
|
end
|
|
|
|
function lib.events:OnUnused(target, eventname)
|
|
if eventname == INSPECT_READY_EVENT then
|
|
target.inspect_ready_used = nil
|
|
end
|
|
end
|
|
|
|
-- Frame for events
|
|
local frame = _G[MAJOR .. "_Frame"] or CreateFrame ("Frame", MAJOR .. "_Frame")
|
|
lib.frame = frame
|
|
frame:Hide()
|
|
frame:UnregisterAllEvents ()
|
|
frame:RegisterEvent ("PLAYER_LOGIN")
|
|
frame:RegisterEvent ("PLAYER_LOGOUT")
|
|
if not frame.OnEvent then
|
|
frame.OnEvent = function(this, event, ...)
|
|
local eventhandler = lib[event]
|
|
return eventhandler and eventhandler (lib, ...)
|
|
end
|
|
frame:SetScript ("OnEvent", frame.OnEvent)
|
|
end
|
|
|
|
|
|
-- Hide our run-state in an easy-to-dump object
|
|
lib.state = {
|
|
mainq = {}, staleq = {}, -- inspect queues
|
|
t = 0,
|
|
last_inspect = 0,
|
|
current_guid = nil,
|
|
throttle = 0,
|
|
tt = 0,
|
|
debounce_send_update = 0,
|
|
}
|
|
lib.cache = {}
|
|
lib.static_cache = {}
|
|
|
|
|
|
-- Note: if we cache NotifyInspect, we have to hook before we cache it!
|
|
if not lib.hooked then
|
|
hooksecurefunc("NotifyInspect", function (...) return lib:NotifyInspect (...) end)
|
|
lib.hooked = true
|
|
end
|
|
function lib:NotifyInspect(unit)
|
|
self.state.last_inspect = GetTime()
|
|
end
|
|
|
|
|
|
-- Get local handles on the key API functions
|
|
local CanInspect = _G.CanInspect
|
|
local ClearInspectPlayer = _G.ClearInspectPlayer
|
|
local GetClassInfo = _G.GetClassInfo
|
|
local GetNumSubgroupMembers = _G.GetNumSubgroupMembers
|
|
local GetNumSpecializationsForClassID = _G.GetNumSpecializationsForClassID
|
|
local GetPlayerInfoByGUID = _G.GetPlayerInfoByGUID
|
|
local GetInspectSelectedPvpTalent = _G.C_SpecializationInfo.GetInspectSelectedPvpTalent
|
|
local GetInspectSpecialization = _G.GetInspectSpecialization
|
|
local GetSpecialization = _G.GetSpecialization
|
|
local GetSpecializationInfo = _G.GetSpecializationInfo
|
|
local GetSpecializationInfoForClassID = _G.GetSpecializationInfoForClassID
|
|
local GetSpecializationRoleByID = _G.GetSpecializationRoleByID
|
|
local GetSpellInfo = _G.GetSpellInfo
|
|
local GetPvpTalentInfoByID = _G.GetPvpTalentInfoByID
|
|
local GetPvpTalentSlotInfo = _G.C_SpecializationInfo.GetPvpTalentSlotInfo
|
|
local GetTalentInfo = _G.GetTalentInfo
|
|
local GetTalentInfoByID = _G.GetTalentInfoByID
|
|
local IsInRaid = _G.IsInRaid
|
|
--local NotifyInspect = _G.NotifyInspect -- Don't cache, as to avoid missing future hooks
|
|
local GetNumClasses = _G.GetNumClasses
|
|
local UnitExists = _G.UnitExists
|
|
local UnitGUID = _G.UnitGUID
|
|
local UnitInParty = _G.UnitInParty
|
|
local UnitInRaid = _G.UnitInRaid
|
|
local UnitIsConnected = _G.UnitIsConnected
|
|
local UnitIsPlayer = _G.UnitIsPlayer
|
|
local UnitIsUnit = _G.UnitIsUnit
|
|
local UnitName = _G.UnitName
|
|
local SendAddonMessage = _G.C_ChatInfo.SendAddonMessage
|
|
local RegisterAddonMessagePrefix = _G.C_ChatInfo.RegisterAddonMessagePrefix
|
|
|
|
local MAX_TALENT_TIERS = _G.MAX_TALENT_TIERS
|
|
local NUM_TALENT_COLUMNS = _G.NUM_TALENT_COLUMNS
|
|
local NUM_PVP_TALENT_SLOTS = 4
|
|
|
|
|
|
local global_spec_id_roles_detailed = {
|
|
-- Death Knight
|
|
[250] = "tank", -- Blood
|
|
[251] = "melee", -- Frost
|
|
[252] = "melee", -- Unholy
|
|
-- Demon Hunter
|
|
[577] = "melee", -- Havoc
|
|
[581] = "tank", -- Vengeance
|
|
-- Druid
|
|
[102] = "ranged", -- Balance
|
|
[103] = "melee", -- Feral
|
|
[104] = "tank", -- Guardian
|
|
[105] = "healer", -- Restoration
|
|
-- Hunter
|
|
[253] = "ranged", -- Beast Mastery
|
|
[254] = "ranged", -- Marksmanship
|
|
[255] = "melee", -- Survival
|
|
-- Mage
|
|
[62] = "ranged", -- Arcane
|
|
[63] = "ranged", -- Fire
|
|
[64] = "ranged", -- Frost
|
|
-- Monk
|
|
[268] = "tank", -- Brewmaster
|
|
[269] = "melee", -- Windwalker
|
|
[270] = "healer", -- Mistweaver
|
|
-- Paladin
|
|
[65] = "healer", -- Holy
|
|
[66] = "tank", -- Protection
|
|
[70] = "melee", -- Retribution
|
|
-- Priest
|
|
[256] = "healer", -- Discipline
|
|
[257] = "healer", -- Holy
|
|
[258] = "ranged", -- Shadow
|
|
-- Rogue
|
|
[259] = "melee", -- Assassination
|
|
[260] = "melee", -- Combat
|
|
[261] = "melee", -- Subtlety
|
|
-- Shaman
|
|
[262] = "ranged", -- Elemental
|
|
[263] = "melee", -- Enhancement
|
|
[264] = "healer", -- Restoration
|
|
-- Warlock
|
|
[265] = "ranged", -- Affliction
|
|
[266] = "ranged", -- Demonology
|
|
[267] = "ranged", -- Destruction
|
|
-- Warrior
|
|
[71] = "melee", -- Arms
|
|
[72] = "melee", -- Fury
|
|
[73] = "tank", -- Protection
|
|
}
|
|
|
|
local class_fixed_roles = {
|
|
HUNTER = "DAMAGER",
|
|
MAGE = "DAMAGER",
|
|
ROGUE = "DAMAGER",
|
|
WARLOCK = "DAMAGER",
|
|
}
|
|
|
|
local class_fixed_roles_detailed = {
|
|
MAGE = "ranged",
|
|
ROGUE = "melee",
|
|
WARLOCK = "ranged",
|
|
}
|
|
|
|
-- Inspects only work after being fully logged in, so track that
|
|
function lib:PLAYER_LOGIN ()
|
|
self.state.logged_in = true
|
|
|
|
self:CacheGameData ()
|
|
|
|
frame:RegisterEvent ("INSPECT_READY")
|
|
frame:RegisterEvent ("GROUP_ROSTER_UPDATE")
|
|
frame:RegisterEvent ("PLAYER_ENTERING_WORLD")
|
|
frame:RegisterEvent ("UNIT_LEVEL")
|
|
frame:RegisterEvent ("PLAYER_TALENT_UPDATE")
|
|
frame:RegisterEvent ("PLAYER_SPECIALIZATION_CHANGED")
|
|
frame:RegisterEvent ("UNIT_SPELLCAST_SUCCEEDED")
|
|
frame:RegisterEvent ("UNIT_NAME_UPDATE")
|
|
frame:RegisterEvent ("UNIT_AURA")
|
|
frame:RegisterEvent ("CHAT_MSG_ADDON")
|
|
RegisterAddonMessagePrefix (COMMS_PREFIX)
|
|
|
|
local guid = UnitGUID ("player")
|
|
local info = self:BuildInfo ("player")
|
|
self.events:Fire (UPDATE_EVENT, guid, "player", info)
|
|
end
|
|
|
|
function lib:PLAYER_LOGOUT ()
|
|
self.state.logged_in = false
|
|
end
|
|
|
|
|
|
-- Simple timer
|
|
do
|
|
lib.state.t = 0
|
|
if not frame.OnUpdate then -- ticket #4 if the OnUpdate code every changes we should stop borrowing the existing handler
|
|
frame.OnUpdate = function(this, elapsed)
|
|
lib.state.t = lib.state.t + elapsed
|
|
lib.state.tt = lib.state.tt + elapsed
|
|
if lib.state.t > INSPECT_DELAY then
|
|
lib:ProcessQueues ()
|
|
lib.state.t = 0
|
|
end
|
|
-- Unthrottle, essentially allowing 1 msg every 3 seconds, but with substantial burst capacity
|
|
if lib.state.tt > 3 and lib.state.throttle > 0 then
|
|
lib.state.throttle = lib.state.throttle - 1
|
|
lib.state.tt = 0
|
|
end
|
|
if lib.state.debounce_send_update > 0 then
|
|
local debounce = lib.state.debounce_send_update - elapsed
|
|
lib.state.debounce_send_update = debounce
|
|
if debounce <= 0 then lib:SendLatestSpecData () end
|
|
end
|
|
end
|
|
frame:SetScript("OnUpdate", frame.OnUpdate) -- this is good regardless of the handler check above because otherwise a new anonymous function is created every time the OnUpdate code runs
|
|
end
|
|
end
|
|
|
|
|
|
-- Internal library functions
|
|
|
|
-- Caches to deal with API shortcomings as well as performance
|
|
lib.static_cache.global_specs = {} -- [gspec] -> { .idx, .name_localized, .description, .icon, .background, .role }
|
|
lib.static_cache.class_to_class_id = {} -- [CLASS] -> class_id
|
|
|
|
-- The talents cache can no longer be pre-fetched on login, but is now constructed class-by-class as we inspect people.
|
|
-- This probably means we want to only ever access it through the GetCachedTalentInfo() helper function below.
|
|
lib.static_cache.talents = {} -- [talent_id] -> { .spell_id, .talent_id, .name_localized, .icon, .tier, .column }
|
|
lib.static_cache.pvp_talents = {} -- [talent_id] -> { .spell_id, .talent_id, .name_localized, .icon }
|
|
|
|
function lib:GetCachedTalentInfo (class_id, tier, col, group, is_inspect, unit)
|
|
local talent_id, name, icon, sel, _, spell_id = GetTalentInfo (tier, col, group, is_inspect, unit)
|
|
if not talent_id then
|
|
--[===[@debug@
|
|
debug ("GetCachedTalentInfo("..tostring(class_id)..","..tier..","..col..","..group..","..tostring(is_inspect)..","..tostring(unit)..") returned nil") --@end-debug@]===]
|
|
return {}
|
|
end
|
|
local class_talents = self.static_cache.talents
|
|
if not class_talents[talent_id] then
|
|
class_talents[talent_id] = {
|
|
spell_id = spell_id,
|
|
talent_id = talent_id,
|
|
name_localized = name,
|
|
icon = icon,
|
|
tier = tier,
|
|
column = col,
|
|
}
|
|
end
|
|
return class_talents[talent_id], sel
|
|
end
|
|
|
|
function lib:GetCachedTalentInfoByID (talent_id)
|
|
local class_talents = self.static_cache.talents
|
|
if talent_id and not class_talents[talent_id] then
|
|
local _, name, icon, _, _, spell_id, _, row, col = GetTalentInfoByID (talent_id)
|
|
if not name then
|
|
--[===[@debug@
|
|
debug ("GetCachedTalentInfoByID("..tostring(talent_id)..") returned nil") --@end-debug@]===]
|
|
return nil
|
|
end
|
|
class_talents[talent_id] = {
|
|
spell_id = spell_id,
|
|
talent_id = talent_id,
|
|
name_localized = name,
|
|
icon = icon,
|
|
tier = row,
|
|
column = col,
|
|
}
|
|
end
|
|
return class_talents[talent_id]
|
|
end
|
|
|
|
function lib:GetCachedPvpTalentInfoByID (talent_id)
|
|
local pvp_talents = self.static_cache.pvp_talents
|
|
if talent_id and not pvp_talents[talent_id] then
|
|
local _, name, icon, _, _, spell_id = GetPvpTalentInfoByID (talent_id)
|
|
if not name then
|
|
--[===[@debug@
|
|
debug ("GetCachedPvpTalentInfo("..tostring(talent_id)..") returned nil") --@end-debug@]===]
|
|
return nil
|
|
end
|
|
pvp_talents[talent_id] = {
|
|
spell_id = spell_id,
|
|
talent_id = talent_id,
|
|
name_localized = name,
|
|
icon = icon,
|
|
}
|
|
end
|
|
return pvp_talents[talent_id]
|
|
end
|
|
|
|
function lib:CacheGameData ()
|
|
local gspecs = self.static_cache.global_specs
|
|
gspecs[0] = {} -- Handle no-specialization case
|
|
for class_id = 1, GetNumClasses () do
|
|
for idx = 1, GetNumSpecializationsForClassID (class_id) do
|
|
local gspec_id, name, description, icon, background = GetSpecializationInfoForClassID (class_id, idx)
|
|
gspecs[gspec_id] = {}
|
|
local gspec = gspecs[gspec_id]
|
|
gspec.idx = idx
|
|
gspec.name_localized = name
|
|
gspec.description = description
|
|
gspec.icon = icon
|
|
gspec.background = background
|
|
gspec.role = GetSpecializationRoleByID (gspec_id)
|
|
end
|
|
|
|
local _, class = GetClassInfo (class_id)
|
|
self.static_cache.class_to_class_id[class] = class_id
|
|
end
|
|
end
|
|
|
|
|
|
function lib:GuidToUnit (guid)
|
|
local info = self.cache[guid]
|
|
if info and info.lku and UnitGUID (info.lku) == guid then return info.lku end
|
|
|
|
for i,unit in ipairs (self:GroupUnits ()) do
|
|
if UnitExists (unit) and UnitGUID (unit) == guid then
|
|
if info then info.lku = unit end
|
|
return unit
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function lib:Query (unit)
|
|
if not UnitIsPlayer (unit) then return end -- NPC
|
|
|
|
if UnitIsUnit (unit, "player") then
|
|
self.events:Fire (UPDATE_EVENT, UnitGUID("player"), "player", self:BuildInfo ("player"))
|
|
return
|
|
end
|
|
|
|
local mainq, staleq = self.state.mainq, self.state.staleq
|
|
|
|
local guid = UnitGUID (unit)
|
|
if not mainq[guid] then
|
|
mainq[guid] = 1
|
|
staleq[guid] = nil
|
|
self.frame:Show () -- Start timer if not already running
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
end
|
|
|
|
|
|
function lib:Refresh (unit)
|
|
local guid = UnitGUID (unit)
|
|
if not guid then return end
|
|
--[===[@debug@
|
|
debug ("Refreshing "..unit) --@end-debug@]===]
|
|
if not self.state.mainq[guid] then
|
|
self.state.staleq[guid] = 1
|
|
self.frame:Show ()
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
end
|
|
|
|
|
|
function lib:ProcessQueues ()
|
|
if not self.state.logged_in then return end
|
|
if InCombatLockdown () then return end -- Never inspect while in combat
|
|
if UnitIsDead ("player") then return end -- You can't inspect while dead, so don't even try
|
|
if InspectFrame and InspectFrame:IsShown () then return end -- Don't mess with the UI's inspections
|
|
|
|
local mainq = self.state.mainq
|
|
local staleq = self.state.staleq
|
|
|
|
if not next (mainq) and next(staleq) then
|
|
--[===[@debug@
|
|
debug ("Main queue empty, swapping main and stale queues") --@end-debug@]===]
|
|
self.state.mainq, self.state.staleq = self.state.staleq, self.state.mainq
|
|
mainq, staleq = staleq, mainq
|
|
end
|
|
|
|
if (self.state.last_inspect + INSPECT_TIMEOUT) < GetTime () then
|
|
-- If there was an inspect going, it's timed out, so either retry or move it to stale queue
|
|
local guid = self.state.current_guid
|
|
if guid then
|
|
--[===[@debug@
|
|
debug ("Inspect timed out for "..guid) --@end-debug@]===]
|
|
|
|
local count = mainq and mainq[guid] or (MAX_ATTEMPTS + 1)
|
|
if not self:GuidToUnit (guid) then
|
|
--[===[@debug@
|
|
debug ("No longer applicable, removing from queues") --@end-debug@]===]
|
|
mainq[guid], staleq[guid] = nil, nil
|
|
elseif count > MAX_ATTEMPTS then
|
|
--[===[@debug@
|
|
debug ("Excessive retries, moving to stale queue") --@end-debug@]===]
|
|
mainq[guid], staleq[guid] = nil, 1
|
|
else
|
|
mainq[guid] = count + 1
|
|
end
|
|
self.state.current_guid = nil
|
|
end
|
|
end
|
|
|
|
if self.state.current_guid then return end -- Still waiting on our inspect data
|
|
|
|
for guid,count in pairs (mainq) do
|
|
local unit = self:GuidToUnit (guid)
|
|
if not unit then
|
|
--[===[@debug@
|
|
debug ("No longer applicable, removing from queues") --@end-debug@]===]
|
|
mainq[guid], staleq[guid] = nil, nil
|
|
elseif not CanInspect (unit) or not UnitIsConnected (unit) then
|
|
--[===[@debug@
|
|
debug ("Cannot inspect "..unit..", aka "..(UnitName(unit) or "nil")..", moving to stale queue") --@end-debug@]===]
|
|
mainq[guid], staleq[guid] = nil, 1
|
|
else
|
|
--[===[@debug@
|
|
debug ("Inspecting "..unit..", aka "..(UnitName(unit) or "nil")) --@end-debug@]===]
|
|
mainq[guid] = count + 1
|
|
self.state.current_guid = guid
|
|
NotifyInspect (unit)
|
|
break
|
|
end
|
|
end
|
|
|
|
if not next (mainq) and not next (staleq) and self.state.throttle == 0 and self.state.debounce_send_update <= 0 then
|
|
frame:Hide() -- Cancel timer, nothing queued and no unthrottling to be done
|
|
end
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
|
|
|
|
function lib:UpdatePlayerInfo (guid, unit, info)
|
|
info.class_localized, info.class, info.race_localized, info.race, info.gender, info.name, info.realm = GetPlayerInfoByGUID (guid)
|
|
local class = info.class
|
|
if info.realm and info.realm == "" then info.realm = nil end
|
|
info.class_id = class and self.static_cache.class_to_class_id[class]
|
|
if not info.spec_role then info.spec_role = class and class_fixed_roles[class] end
|
|
if not info.spec_role_detailed then info.spec_role_detailed = class and class_fixed_roles_detailed[class] end
|
|
info.lku = unit
|
|
end
|
|
|
|
|
|
function lib:BuildInfo (unit)
|
|
local guid = UnitGUID (unit)
|
|
if not guid then return end
|
|
|
|
local cache = self.cache
|
|
local info = cache[guid] or {}
|
|
cache[guid] = info
|
|
info.guid = guid
|
|
|
|
self:UpdatePlayerInfo (guid, unit, info)
|
|
-- On a cold login, GetPlayerInfoByGUID() doesn't seem to be usable, so mark as stale
|
|
local class = info.class
|
|
if not class and not self.state.mainq[guid] then
|
|
self.state.staleq[guid] = 1
|
|
self.frame:Show ()
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
|
|
local is_inspect = not UnitIsUnit (unit, "player")
|
|
local spec = GetSpecialization ()
|
|
local gspec_id = is_inspect and GetInspectSpecialization (unit) or spec and GetSpecializationInfo (spec)
|
|
|
|
local gspecs = self.static_cache.global_specs
|
|
if not gspec_id or not gspecs[gspec_id] then -- not a valid spec_id
|
|
info.global_spec_id = nil
|
|
else
|
|
info.global_spec_id = gspec_id
|
|
local spec_info = gspecs[gspec_id]
|
|
info.spec_index = spec_info.idx
|
|
info.spec_name_localized = spec_info.name_localized
|
|
info.spec_description = spec_info.description
|
|
info.spec_icon = spec_info.icon
|
|
info.spec_background = spec_info.background
|
|
info.spec_role = spec_info.role
|
|
info.spec_role_detailed = global_spec_id_roles_detailed[gspec_id]
|
|
end
|
|
|
|
if not info.spec_role then info.spec_role = class and class_fixed_roles[class] end
|
|
if not info.spec_role_detailed then info.spec_role_detailed = class and class_fixed_roles_detailed[class] end
|
|
|
|
info.talents = info.talents or {}
|
|
info.pvp_talents = info.pvp_talents or {}
|
|
|
|
-- Only scan talents when we have player data
|
|
if info.spec_index then
|
|
info.spec_group = GetActiveSpecGroup (is_inspect)
|
|
wipe (info.talents)
|
|
for tier = 1, MAX_TALENT_TIERS do
|
|
for col = 1, NUM_TALENT_COLUMNS do
|
|
local talent, sel = self:GetCachedTalentInfo (info.class_id, tier, col, info.spec_group, is_inspect, unit)
|
|
if sel then
|
|
info.talents[talent.talent_id] = talent
|
|
end
|
|
end
|
|
end
|
|
|
|
wipe (info.pvp_talents)
|
|
if is_inspect then
|
|
for index = 1, NUM_PVP_TALENT_SLOTS do
|
|
local talent_id = GetInspectSelectedPvpTalent (unit, index)
|
|
if talent_id then
|
|
info.pvp_talents[talent_id] = self:GetCachedPvpTalentInfoByID (talent_id)
|
|
end
|
|
end
|
|
else
|
|
-- C_SpecializationInfo.GetAllSelectedPvpTalentIDs will sometimes return a lot of extra talents
|
|
for index = 1, NUM_PVP_TALENT_SLOTS do
|
|
local slot_info = GetPvpTalentSlotInfo (index)
|
|
local talent_id = slot_info and slot_info.selectedTalentID
|
|
if talent_id then
|
|
info.pvp_talents[talent_id] = self:GetCachedPvpTalentInfoByID (talent_id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
info.glyphs = info.glyphs or {} -- kept for addons that still refer to this
|
|
|
|
if is_inspect and not UnitIsVisible (unit) and UnitIsConnected (unit) then info.not_visible = true end
|
|
|
|
return info
|
|
end
|
|
|
|
|
|
function lib:INSPECT_READY (guid)
|
|
local unit = self:GuidToUnit (guid)
|
|
local finalize = false
|
|
if unit then
|
|
if guid == self.state.current_guid then
|
|
self.state.current_guid = nil -- Got what we asked for
|
|
finalize = true
|
|
--[===[@debug@
|
|
debug ("Got inspection data for requested guid "..guid) --@end-debug@]===]
|
|
end
|
|
|
|
local mainq, staleq = self.state.mainq, self.state.staleq
|
|
mainq[guid], staleq[guid] = nil, nil
|
|
|
|
local gspec_id = GetInspectSpecialization (unit)
|
|
if not self.static_cache.global_specs[gspec_id] then -- Bah, got garbage, flag as stale and try again
|
|
staleq[guid] = 1
|
|
return
|
|
end
|
|
|
|
self.events:Fire (UPDATE_EVENT, guid, unit, self:BuildInfo (unit))
|
|
self.events:Fire (INSPECT_READY_EVENT, guid, unit)
|
|
end
|
|
if finalize then
|
|
ClearInspectPlayer ()
|
|
end
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
|
|
|
|
function lib:PLAYER_ENTERING_WORLD ()
|
|
if self.commScope == "INSTANCE_CHAT" then
|
|
-- Handle moving directly from one LFG to another
|
|
self.commScope = nil
|
|
self:UpdateCommScope ()
|
|
end
|
|
end
|
|
|
|
|
|
-- Group handling parts
|
|
|
|
local members = {}
|
|
function lib:GROUP_ROSTER_UPDATE ()
|
|
local group = self.cache
|
|
local units = self:GroupUnits ()
|
|
-- Find new members
|
|
for i,unit in ipairs (self:GroupUnits ()) do
|
|
local guid = UnitGUID (unit)
|
|
if guid then
|
|
members[guid] = true
|
|
if not group[guid] then
|
|
self:Query (unit)
|
|
-- Update with what we have so far (guid, unit, name/class/race?)
|
|
self.events:Fire (UPDATE_EVENT, guid, unit, self:BuildInfo (unit))
|
|
end
|
|
end
|
|
end
|
|
-- Find removed members
|
|
for guid in pairs (group) do
|
|
if not members[guid] then
|
|
group[guid] = nil
|
|
self.events:Fire (REMOVE_EVENT, guid, nil)
|
|
end
|
|
end
|
|
wipe (members)
|
|
self:UpdateCommScope ()
|
|
end
|
|
|
|
|
|
function lib:DoPlayerUpdate ()
|
|
self:Query ("player")
|
|
self.state.debounce_send_update = 2.5 -- Hold off 2.5sec before sending update
|
|
self.frame:Show ()
|
|
end
|
|
|
|
|
|
function lib:SendLatestSpecData ()
|
|
local scope = self.commScope
|
|
if not scope then return end
|
|
|
|
local guid = UnitGUID ("player")
|
|
local info = self.cache[guid]
|
|
if not info then return end
|
|
|
|
-- fmt, guid, global_spec_id, talent1 -> MAX_TALENT_TIERS, pvptalent1 -> NUM_PVP_TALENT_SLOTS
|
|
-- sequentially, allow no gaps for missing talents we decode by index on the receiving end.
|
|
local datastr = COMMS_FMT..COMMS_DELIM..guid..COMMS_DELIM..(info.global_spec_id or 0)
|
|
local talentCount = 1
|
|
for k in pairs(info.talents) do
|
|
datastr = datastr..COMMS_DELIM..k
|
|
talentCount = talentCount + 1
|
|
end
|
|
for i=talentCount,MAX_TALENT_TIERS do
|
|
datastr = datastr..COMMS_DELIM..0
|
|
end
|
|
talentCount = 1
|
|
for k in pairs(info.pvp_talents) do
|
|
datastr = datastr..COMMS_DELIM..k
|
|
talentCount = talentCount + 1
|
|
end
|
|
for i=talentCount,NUM_PVP_TALENT_SLOTS do
|
|
datastr = datastr..COMMS_DELIM..0
|
|
end
|
|
|
|
--[===[@debug@
|
|
debug ("Sending LGIST update to "..scope) --@end-debug@]===]
|
|
SendAddonMessage(COMMS_PREFIX, datastr, scope)
|
|
end
|
|
|
|
|
|
function lib:UpdateCommScope ()
|
|
local scope = (IsInGroup (LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT") or (IsInRaid () and "RAID") or (IsInGroup (LE_PARTY_CATEGORY_HOME) and "PARTY")
|
|
if self.commScope ~= scope then
|
|
self.commScope = scope
|
|
self:DoPlayerUpdate ()
|
|
end
|
|
end
|
|
|
|
|
|
-- Indicies for various parts of the split data msg
|
|
local msg_idx = {}
|
|
msg_idx.fmt = 1
|
|
msg_idx.guid = msg_idx.fmt + 1
|
|
msg_idx.global_spec_id = msg_idx.guid + 1
|
|
msg_idx.talents = msg_idx.global_spec_id + 1
|
|
msg_idx.end_talents = msg_idx.talents + MAX_TALENT_TIERS
|
|
msg_idx.pvp_talents = msg_idx.end_talents + 1
|
|
msg_idx.end_pvp_talents = msg_idx.pvp_talents + NUM_PVP_TALENT_SLOTS - 1
|
|
|
|
function lib:CHAT_MSG_ADDON (prefix, datastr, scope, sender)
|
|
if prefix ~= COMMS_PREFIX or scope ~= self.commScope then return end
|
|
--[===[@debug@
|
|
debug ("Incoming LGIST update from "..(scope or "nil").."/"..(sender or "nil")..": "..(datastr:gsub(COMMS_DELIM,";") or "nil")) --@end-debug@]===]
|
|
|
|
local data = { strsplit (COMMS_DELIM,datastr) }
|
|
local fmt = data[msg_idx.fmt]
|
|
if fmt ~= COMMS_FMT then return end -- Unknown format, ignore
|
|
|
|
local guid = data[msg_idx.guid]
|
|
|
|
local senderguid = UnitGUID(sender)
|
|
if senderguid and senderguid ~= guid then return end
|
|
|
|
local info = guid and self.cache[guid]
|
|
if not info then return end -- Never allow random message to create new group member entries!
|
|
|
|
local unit = self:GuidToUnit (guid)
|
|
if not unit then return end
|
|
if UnitIsUnit (unit, "player") then return end -- we're already up-to-date, comment out for solo debugging
|
|
|
|
self.state.throttle = self.state.throttle + 1
|
|
self.frame:Show () -- Ensure we're unthrottling
|
|
if self.state.throttle > 40 then return end -- If we ever hit this, someone's being "funny"
|
|
|
|
info.class_localized, info.class, info.race_localized, info.race, info.gender, info.name, info.realm = GetPlayerInfoByGUID (guid)
|
|
if info.realm and info.realm == "" then info.realm = nil end
|
|
info.class_id = self.static_cache.class_to_class_id[info.class]
|
|
|
|
local gspecs = self.static_cache.global_specs
|
|
|
|
local gspec_id = data[msg_idx.global_spec_id] and tonumber (data[msg_idx.global_spec_id])
|
|
if not gspec_id or not gspecs[gspec_id] then return end -- Malformed message, avoid throwing errors by using this nil
|
|
|
|
info.global_spec_id = gspec_id
|
|
info.spec_index = gspecs[gspec_id].idx
|
|
info.spec_name_localized = gspecs[gspec_id].name_localized
|
|
info.spec_description = gspecs[gspec_id].description
|
|
info.spec_icon = gspecs[gspec_id].icon
|
|
info.spec_background = gspecs[gspec_id].background
|
|
info.spec_role = gspecs[gspec_id].role
|
|
info.spec_role_detailed = global_spec_id_roles_detailed[gspec_id]
|
|
|
|
local need_inspect = nil -- shouldn't be needed, but just in case
|
|
info.talents = wipe (info.talents or {})
|
|
for i = msg_idx.talents, msg_idx.end_talents do
|
|
local talent_id = tonumber (data[i]) or 0
|
|
if talent_id > 0 then
|
|
local talent = self:GetCachedTalentInfoByID (talent_id)
|
|
if talent then
|
|
info.talents[talent_id] = talent
|
|
else
|
|
need_inspect = 1
|
|
end
|
|
end
|
|
end
|
|
|
|
info.pvp_talents = wipe (info.pvp_talents or {})
|
|
for i = msg_idx.pvp_talents, msg_idx.end_pvp_talents do
|
|
local talent_id = tonumber (data[i]) or 0
|
|
if talent_id > 0 then
|
|
local talent = self:GetCachedPvpTalentInfoByID (talent_id)
|
|
if talent then
|
|
info.pvp_talents[talent_id] = talent
|
|
else
|
|
need_inspect = 1
|
|
end
|
|
end
|
|
end
|
|
|
|
info.glyphs = info.glyphs or {} -- kept for addons that still refer to this
|
|
|
|
local mainq, staleq = self.state.mainq, self.state.staleq
|
|
local want_inspect = not need_inspect and self.inspect_ready_used and (mainq[guid] or staleq[guid]) and 1 or nil
|
|
mainq[guid], staleq[guid] = need_inspect, want_inspect
|
|
if need_inspect or want_inspect then self.frame:Show () end
|
|
|
|
--[===[@debug@
|
|
debug ("Firing LGIST update event for unit "..unit..", GUID "..guid..", inspect "..tostring(not not need_inspect)) --@end-debug@]===]
|
|
self.events:Fire (UPDATE_EVENT, guid, unit, info)
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
|
|
|
|
function lib:UNIT_LEVEL (unit)
|
|
if UnitInRaid (unit) or UnitInParty (unit) then
|
|
self:Refresh (unit)
|
|
end
|
|
if UnitIsUnit (unit, "player") then
|
|
self:DoPlayerUpdate ()
|
|
end
|
|
end
|
|
|
|
|
|
function lib:PLAYER_TALENT_UPDATE ()
|
|
self:DoPlayerUpdate ()
|
|
end
|
|
|
|
|
|
function lib:PLAYER_SPECIALIZATION_CHANGED (unit)
|
|
-- This event seems to fire a lot, and for no particular reason *sigh*
|
|
-- if UnitInRaid (unit) or UnitInParty (unit) then
|
|
-- self:Refresh (unit)
|
|
-- end
|
|
if unit and UnitIsUnit (unit, "player") then
|
|
self:DoPlayerUpdate ()
|
|
end
|
|
end
|
|
|
|
|
|
function lib:UNIT_NAME_UPDATE (unit)
|
|
local group = self.cache
|
|
local guid = UnitGUID (unit)
|
|
local info = guid and group[guid]
|
|
if info then
|
|
self:UpdatePlayerInfo (guid, unit, info)
|
|
if info.name ~= UNKNOWN then
|
|
self.events:Fire (UPDATE_EVENT, guid, unit, info)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Always get a UNIT_AURA when a unit's UnitIsVisible() changes
|
|
function lib:UNIT_AURA (unit)
|
|
local group = self.cache
|
|
local guid = UnitGUID (unit)
|
|
local info = guid and group[guid]
|
|
if info then
|
|
if not UnitIsUnit (unit, "player") then
|
|
if UnitIsVisible (unit) then
|
|
if info.not_visible then
|
|
info.not_visible = nil
|
|
--[===[@debug@
|
|
debug (unit..", aka "..(UnitName(unit) or "nil")..", is now visible") --@end-debug@]===]
|
|
if not self.state.mainq[guid] then
|
|
self.state.staleq[guid] = 1
|
|
self.frame:Show ()
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
end
|
|
elseif UnitIsConnected (unit) then
|
|
--[===[@debug@
|
|
if not info.not_visible then
|
|
debug (unit..", aka "..(UnitName(unit) or "nil")..", is no longer visible")
|
|
end
|
|
--@end-debug@]===]
|
|
info.not_visible = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function lib:UNIT_SPELLCAST_SUCCEEDED (unit, _, spell_id)
|
|
if spell_id == 200749 then -- Activating Specialization
|
|
self:Query (unit) -- Definitely changed, so high prio refresh
|
|
end
|
|
end
|
|
|
|
|
|
-- External library functions
|
|
|
|
function lib:QueuedInspections ()
|
|
local q = {}
|
|
for guid in pairs (self.state.mainq) do
|
|
table.insert (q, guid)
|
|
end
|
|
return q
|
|
end
|
|
|
|
|
|
function lib:StaleInspections ()
|
|
local q = {}
|
|
for guid in pairs (self.state.staleq) do
|
|
table.insert (q, guid)
|
|
end
|
|
return q
|
|
end
|
|
|
|
|
|
function lib:IsInspectQueued (guid)
|
|
return guid and ((self.state.mainq[guid] or self.state.staleq[guid]) and true)
|
|
end
|
|
|
|
|
|
function lib:GetCachedInfo (guid)
|
|
local group = self.cache
|
|
return guid and group[guid]
|
|
end
|
|
|
|
|
|
function lib:Rescan (guid)
|
|
local mainq, staleq = self.state.mainq, self.state.staleq
|
|
if guid then
|
|
local unit = self:GuidToUnit (guid)
|
|
if unit then
|
|
if UnitIsUnit (unit, "player") then
|
|
self.events:Fire (UPDATE_EVENT, guid, "player", self:BuildInfo ("player"))
|
|
elseif not mainq[guid] then
|
|
staleq[guid] = 1
|
|
end
|
|
end
|
|
else
|
|
for i,unit in ipairs (self:GroupUnits ()) do
|
|
if UnitExists (unit) then
|
|
if UnitIsUnit (unit, "player") then
|
|
self.events:Fire (UPDATE_EVENT, UnitGUID("player"), "player", self:BuildInfo ("player"))
|
|
else
|
|
local guid = UnitGUID (unit)
|
|
if guid and not mainq[guid] then
|
|
staleq[guid] = 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self.frame:Show () -- Start timer if not already running
|
|
|
|
-- Evict any stale entries
|
|
self:GROUP_ROSTER_UPDATE ()
|
|
self.events:Fire (QUEUE_EVENT)
|
|
end
|
|
|
|
|
|
local unitstrings = {
|
|
raid = { "player" }, -- This seems to be needed under certain circumstances. Odd.
|
|
party = { "player" }, -- Player not part of partyN
|
|
player = { "player" }
|
|
}
|
|
for i = 1,40 do table.insert (unitstrings.raid, "raid"..i) end
|
|
for i = 1,4 do table.insert (unitstrings.party, "party"..i) end
|
|
|
|
|
|
-- Returns an array with the set of unit ids for the current group
|
|
function lib:GroupUnits ()
|
|
local units
|
|
if IsInRaid () then
|
|
units = unitstrings.raid
|
|
elseif GetNumSubgroupMembers () > 0 then
|
|
units = unitstrings.party
|
|
else
|
|
units = unitstrings.player
|
|
end
|
|
return units
|
|
end
|
|
|
|
|
|
-- If demand-loaded, we need to synthesize a login event
|
|
if IsLoggedIn () then lib:PLAYER_LOGIN () end
|
|
|