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