1727 lines
46 KiB
Lua
1727 lines
46 KiB
Lua
--[[
|
|
Name: LibGroupTalents-1.0
|
|
Revision: $Rev: 55 $
|
|
Author: Zek
|
|
Documentation: http://wowace.com/wiki/LibGroupTalents-1.0
|
|
SVN: svn://svn.wowace.com/wow/libgrouptalents-1-0/mainline/trunk
|
|
Description: Talent Library abstraction layer to provide easy interface to the lower level system
|
|
Dependancies: LibStub, CallbackHandler-1.0, LibTalentQuery-1.0
|
|
License: GPL v3
|
|
|
|
Purpose:
|
|
LibGroupTalents-1.0 is intended to do the following basic functions usually handled at the mod level.
|
|
|
|
- Maintain a raid wide table of talents, automatically updated on roster changes, notifying you on talent receipts.
|
|
- Provide easy access to talent queries (spec weight, spec name, specific talent presence)
|
|
- Monitor talent changes in players, and notify of changes (respec, talent swap, update after out of sight, level up)
|
|
- Monitor player roles, and notify of changes (melee, tank, healer, caster)
|
|
- Communicate directly with itself to other users to update talents via addon channel when possible
|
|
|
|
Notes:
|
|
The LibTalentQuery-1.0 dependancy must be included before LibGroupTalents-1.0 in any lib.xml or mod side TOC declarations.
|
|
|
|
Functions:
|
|
UnitHasTalent(unit, talentName[, group])-- Returns: Points spent in talent or nil
|
|
GUIDHasTalent(guid, talentName[, group])-- As UnitHasTalent
|
|
GetUnitTalentSpec(unitid[, group]) -- Returns: Dominant Tree, spent1, spent2, spent3
|
|
GetGUIDTalentSpec(guid[, group]) -- As GetUnitTalentSpec
|
|
GetUnitTalents(unit, refresh) -- Returns: Raw talent information in form of table of 3 strings of points spent. The refresh arg will force a re-query of the unit's talents
|
|
GetGUIDTalents(guid, refresh) -- As GetUnitTalents
|
|
GetUnitRole(unit) -- Returns one of: "melee", "caster", "healer", "tank"
|
|
GetGUIDRole(guid) -- As GetUnitRole
|
|
RefreshTalentsByUnit(unit) -- Force a refresh of talents for the specific unit
|
|
RefreshTalentsByGUID(guid) -- Force a refresh of talents for the specific player GUID
|
|
GetTreeNames(class) -- Returns: The three talent tree names for that class (Note: These return values are only valid after a player of that class has been inspected)
|
|
GetTreeIcons(class) -- Returns: The three talent tree icons for that class (Note: As above)
|
|
GetTalentCount() -- Returns: Talent info got, Talent info missing
|
|
GetTalentMissingNames() -- Returns: Comma delimited list of player names we're missing talents for
|
|
GetClassTalentInfo(class, talentName) -- Returns: Max Rank, Icon, Tab, Tier, Column, Tree Index
|
|
GetUnitStorageString(unit) -- Returns: An encoded data string containing talent information for the player which can be stored by mods to set in later sessions using SetStorageString()
|
|
GetGUIDStorageString(guid) -- As GetUnitStorageString
|
|
SetStorageString(talentString) -- Returns: true on success (applicable). Any second return value indicates the data was invalid and should not be kept
|
|
GetUnitGlyphs(unit[, group]) -- Returns: Up to 6 spell IDs for the currently assigned Glyphs (Note: For the moment, we can only see the glyphs of players running LibGroupTalents-1.0)
|
|
GetGUIDGlyphs(guid[, group]) -- As GetUnitGlyphs
|
|
UnitHasGlyph(unit, glyph [, group]) -- Returns: true if the player has the glyph associated with spellID or spellName (Note: For the moment, we can only see the glyphs of players running LibGroupTalents-1.0)
|
|
GUIDHasGlyph(unit, glyph [, group]) -- As UnitHasGlyph
|
|
PurgeAndRescanTalents() -- Wipe current roster of all talents and rescan from start
|
|
|
|
Convenience Functions (Similar to Blizzard API functions, but callable with a unit ID):
|
|
GetActiveTalentGroup(unit) -- Returns: Active talent group for unit
|
|
GetNumTalentGroups(unit) -- Returns: Number of talent groups for unit
|
|
GetNumTalentTabs(unit) -- Returns: Number of talent tabs. Here's a clue; it's going to be 3...
|
|
GetTalentTabInfo(unit, tab[, group]) -- Returns: Tree Name, Tree Icon, Points Spent, Tree Background
|
|
GetNumTalents(unit, tab) -- Returns: Number of talents for specified tree
|
|
GetTalentInfo(unit, tab, index[, group])-- Returns: Talent Name, Icon, Tier, Column, Points Spent, Max Rank (Note that preview return values are not given unless called with "player")
|
|
GetUnspentTalentPoints(unit[, group]) -- Returns: Number of un-spent talent points for the unit
|
|
|
|
Events:
|
|
LibGroupTalents_Update(guid, unit, newSpec, n1, n2, n3 [, oldSpec, o1, o2, o3]) -- Received updated talents. If it's a respec, or old set is know, it passes the old info also (this is not sent if new talent scan is same as previous)
|
|
LibGroupTalents_UpdateComplete(guid1, guid2[, ...]) -- Sent when there are no more pending talent reads due (passes all GUIDs that were updated since last time this event was fired)
|
|
LibGroupTalents_Add(guid, unit, name, realm) -- Unit added to talent roster (Talents not necessarily available yet, but this is the mod's chance to feed talents using SetStorageString)
|
|
LibGroupTalents_Remove(guid, name, realm) -- Unit removed from talent roster (This is your last chance to store talents if required using GetUnitStorageString)
|
|
LibGroupTalents_RoleChange(guid, unit, newrole, oldrole) -- Roles are: "melee", "caster", "healer", "tank"
|
|
LibGroupTalents_GlyphUpdate(guid, unit) -- Fired when a player's glyphs change (Note: For the moment, we can only see the glyphs of players running LibGroupTalents-1.0)
|
|
|
|
]]
|
|
|
|
local TalentQuery = LibStub("LibTalentQuery-1.0")
|
|
|
|
local MAJOR, MINOR = "LibGroupTalents-1.0", tonumber(("$Rev: 55 $"):match("(%d+)"))
|
|
local lib = LibStub:NewLibrary(MAJOR, MINOR)
|
|
if not lib then return end
|
|
|
|
local ChatThrottleLib = _G.ChatThrottleLib
|
|
|
|
lib.roster = lib.roster or {}
|
|
lib.classTalentData = lib.classTalentData or {}
|
|
lib.batch = lib.batch or {}
|
|
lib.pendingStorageStrings = lib.pendingStorageStrings or {}
|
|
|
|
local function UnitFullName(unit)
|
|
local name, realm = UnitName(unit)
|
|
local namerealm = realm and realm ~= "" and name .. "-" .. realm or name
|
|
return namerealm
|
|
end
|
|
|
|
local function RosterInfoFullName(info)
|
|
local name, realm = info.name, info.realm
|
|
local namerealm = realm and realm ~= "" and name .. "-" .. realm or name
|
|
return namerealm
|
|
end
|
|
|
|
local specChangers = {}
|
|
for index,spellid in ipairs(_G.TALENT_ACTIVATION_SPELLS) do
|
|
specChangers[GetSpellInfo(spellid)] = index
|
|
end
|
|
|
|
local frame = lib.frame
|
|
if (not frame) then
|
|
frame = CreateFrame("Frame", "LibGroupTalents_Frame")
|
|
lib.frame = frame
|
|
end
|
|
frame:UnregisterAllEvents()
|
|
frame:RegisterEvent("RAID_ROSTER_UPDATE")
|
|
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
|
|
frame:RegisterEvent("UNIT_NAME_UPDATE")
|
|
frame:RegisterEvent("PLAYER_TALENT_UPDATE")
|
|
frame:RegisterEvent("UNIT_LEVEL")
|
|
frame:RegisterEvent("UNIT_AURA") -- Always get a UNIT_AURA when a unit's UnitIsVisible() changes
|
|
frame:RegisterEvent("CHAT_MSG_ADDON")
|
|
frame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
|
|
frame:RegisterEvent("PLAYER_LOGIN")
|
|
frame:RegisterEvent("GLYPH_ADDED")
|
|
frame:RegisterEvent("GLYPH_REMOVED")
|
|
frame:RegisterEvent("GLYPH_UPDATED")
|
|
|
|
frame:SetScript("OnEvent", function(self, event, ...)
|
|
return lib[event](lib, ...)
|
|
end)
|
|
|
|
if not lib.events then
|
|
lib.events = LibStub("CallbackHandler-1.0"):New(lib)
|
|
end
|
|
|
|
local next, select, pairs, type = next, select, pairs, type
|
|
local new, del, deepDel
|
|
do
|
|
local list = setmetatable({},{__mode='k'})
|
|
function new(...)
|
|
local t = next(list)
|
|
if t then
|
|
list[t] = nil
|
|
for i = 1, select('#', ...) do
|
|
t[i] = select(i, ...)
|
|
end
|
|
return t
|
|
else
|
|
return {...}
|
|
end
|
|
end
|
|
function del(t)
|
|
if (t) then
|
|
wipe(t)
|
|
t[''] = true
|
|
t[''] = nil
|
|
list[t] = true
|
|
end
|
|
end
|
|
function deepDel(t)
|
|
if (t) then
|
|
for k,v in pairs(t) do
|
|
if type(v) == "table" then
|
|
deepDel(v)
|
|
end
|
|
t[k] = nil
|
|
end
|
|
t[''] = true
|
|
t[''] = nil
|
|
list[t] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
do
|
|
local delay = 0
|
|
frame:SetScript("OnUpdate", function(self, elapsed)
|
|
if (lib.raidRosterUpdate) then
|
|
lib.raidRosterUpdate = nil
|
|
lib:OnRaidRosterUpdate()
|
|
end
|
|
|
|
if (lib.refreshCheckTimer) then
|
|
lib.refreshCheckTimer = lib.refreshCheckTimer - elapsed
|
|
if (lib.refreshCheckTimer < 0) then
|
|
lib.refreshCheckTimer = nil
|
|
lib:CheckForMissingTalents()
|
|
end
|
|
end
|
|
|
|
if (lib.talentTimers) then
|
|
delay = delay + elapsed
|
|
if (delay > 1) then
|
|
delay = 0
|
|
local now = GetTime()
|
|
local triggers
|
|
for guid,when in pairs(lib.talentTimers) do
|
|
if (now > when) then
|
|
-- Pass to second table to process, because RefreshTimers can affect this talentTimers table
|
|
-- So it's important we're not still iterating it at the time
|
|
if (not triggers) then
|
|
triggers = new()
|
|
end
|
|
triggers[guid] = true
|
|
lib.talentTimers[guid] = nil
|
|
if (not next(lib.talentTimers)) then
|
|
lib.talentTimers = del(lib.talentTimers)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if (triggers) then
|
|
for guid in pairs(triggers) do
|
|
lib:RefreshTalentsByGUID(guid)
|
|
end
|
|
del(triggers)
|
|
end
|
|
end
|
|
end
|
|
|
|
if (not lib.talentTimers and not lib.refreshCheckTimer) then
|
|
self:Hide()
|
|
end
|
|
end)
|
|
end
|
|
frame:Show()
|
|
lib.raidRosterUpdate = true
|
|
|
|
-- GetGUIDTalentsRaw
|
|
local function GetGUIDTalentsRaw(guid, group)
|
|
local r = guid and lib.roster[guid]
|
|
return r and r.talents and r.talents[group or r.active], r
|
|
end
|
|
|
|
-- PLAYER_LOGIN
|
|
function lib:PLAYER_LOGIN()
|
|
ChatThrottleLib = _G.ChatThrottleLib
|
|
lib.PLAYER_LOGIN = nil
|
|
end
|
|
|
|
-- RAID_ROSTER_UPDATE
|
|
function lib:RAID_ROSTER_UPDATE()
|
|
self.raidRosterUpdate = true
|
|
frame:Show()
|
|
end
|
|
lib.PARTY_MEMBERS_CHANGED = lib.RAID_ROSTER_UPDATE
|
|
|
|
-- UNIT_NAME_UPDATE
|
|
function lib:UNIT_NAME_UPDATE(unit)
|
|
local guid = unit and UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
if (r) then
|
|
local needsAdd = r.name == UNKNOWN
|
|
r.name, r.realm = UnitName(unit)
|
|
if (r.realm == "") then
|
|
r.realm = nil
|
|
end
|
|
r.class = select(2, UnitClass(unit))
|
|
r.level = UnitLevel(unit)
|
|
if (not r.talents) then
|
|
if (needsAdd) then
|
|
self.events:Fire("LibGroupTalents_Add", guid, unit, r.name, r.realm)
|
|
end
|
|
|
|
self:CheckForMissingTalents()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- AnyPending
|
|
local function AnyPending()
|
|
local checkUpdate
|
|
for guid,info in pairs(lib.roster) do
|
|
local namerealm = RosterInfoFullName(info)
|
|
if (UnitIsConnected(namerealm)) then
|
|
if (lib.wasOffline) then
|
|
lib.wasOffline[guid] = nil
|
|
end
|
|
if (not info.talents or info.refresh) then
|
|
return true
|
|
end
|
|
else
|
|
if (not lib.wasOffline) then
|
|
lib.wasOffline = new()
|
|
end
|
|
lib.wasOffline[guid] = true
|
|
end
|
|
end
|
|
if (lib.wasOffline and not next(lib.wasOffline)) then
|
|
lib.wasOffline = del(lib.wasOffline)
|
|
end
|
|
end
|
|
|
|
-- CheckForUpdateComplete
|
|
local function CheckForUpdateComplete()
|
|
-- When all pending updates are complete, send an event to notify nothing else is due
|
|
if (next(lib.batch)) then
|
|
if (not AnyPending()) then
|
|
lib.events:Fire("LibGroupTalents_UpdateComplete", unpack(lib.batch))
|
|
wipe(lib.batch)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- UNIT_LEVEL
|
|
function lib:UNIT_LEVEL(unit)
|
|
if (UnitInParty(unit) or UnitInRaid(unit)) then
|
|
local guid = UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
if (r) then
|
|
r.level = UnitLevel(unit)
|
|
self:RefreshTalentsByUnit(unit)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- UNIT_AURA
|
|
function lib:UNIT_AURA(unit)
|
|
local guid = UnitGUID(unit)
|
|
if (not UnitIsVisible(unit) or (self.wasOffline and self.wasOffline[guid])) then
|
|
if (not self.outOfSight) then
|
|
self.outOfSight = {}
|
|
end
|
|
self.outOfSight[guid] = true
|
|
self:RefreshTalentsByGUID(guid)
|
|
end
|
|
end
|
|
|
|
-- OnRaidRosterUpdate
|
|
function lib:OnRaidRosterUpdate()
|
|
local instanceType = select(2, IsInInstance())
|
|
if (instanceType == "pvp" or instanceType == "arena") then
|
|
self.distribution = "BATTLEGROUND"
|
|
else
|
|
if (GetNumRaidMembers() > 0) then
|
|
self.distribution = "RAID"
|
|
elseif (GetNumPartyMembers() > 0) then
|
|
self.distribution = "PARTY"
|
|
else
|
|
self.distribution = nil
|
|
end
|
|
end
|
|
if (self.distribution) then
|
|
if (self.sentHello ~= self.distribution) then
|
|
self.sentHello = self.distribution
|
|
self:SendCommMessage("HELLO "..MINOR, nil, self.distribution)
|
|
end
|
|
else
|
|
self.sentHello = nil
|
|
self.talentThrottle = del(self.talentThrottle)
|
|
self.wasOffline = del(self.wasOffline)
|
|
self.outOfSight = del(self.outOfSight)
|
|
wipe(self.pendingStorageStrings)
|
|
end
|
|
|
|
-- Now check for roster changes
|
|
local subtractions = new()
|
|
local additions = new()
|
|
local changes = new()
|
|
|
|
if (self.roster) then
|
|
for guid,info in pairs(self.roster) do
|
|
subtractions[guid] = info.level or 0
|
|
end
|
|
end
|
|
|
|
for unit in self:IterateRoster() do
|
|
local guid = UnitGUID(unit)
|
|
if (guid) then
|
|
local n = self.roster[guid]
|
|
if (not n) then
|
|
n = new()
|
|
self.roster[guid] = n
|
|
end
|
|
|
|
n.name, n.realm = UnitName(unit)
|
|
if (n.realm == "") then
|
|
n.realm = nil -- Fix this already..
|
|
end
|
|
n.level = UnitLevel(unit)
|
|
n.class = select(2, UnitClass(unit))
|
|
n.unit = unit
|
|
|
|
if (subtractions[guid]) then
|
|
if (subtractions[guid] ~= n.level) then
|
|
changes[guid] = unit -- Level changed, needs a rescan
|
|
end
|
|
|
|
subtractions[guid] = nil
|
|
else
|
|
if (n.name ~= UNKNOWN) then
|
|
self.events:Fire("LibGroupTalents_Add", guid, unit, n.name, n.realm)
|
|
end
|
|
additions[guid] = unit
|
|
end
|
|
end
|
|
end
|
|
|
|
if (next(additions)) then
|
|
for guid,unit in pairs(additions) do
|
|
self:GetUnitTalents(unit)
|
|
end
|
|
end
|
|
|
|
if (next(changes)) then
|
|
for guid,unit in pairs(changes) do
|
|
self:GetUnitTalents(unit)
|
|
end
|
|
end
|
|
|
|
if (next(subtractions)) then
|
|
for guid in pairs(subtractions) do
|
|
local r = self.roster[guid]
|
|
if (r) then
|
|
self.events:Fire("LibGroupTalents_Remove", guid, r.name, r.realm)
|
|
self.roster[guid] = deepDel(r)
|
|
|
|
local classStorageStrings = self.pendingStorageStrings[r.class]
|
|
if (classStorageStrings) then
|
|
classStorageStrings[guid] = del(classStorageStrings[guid])
|
|
if (not next(classStorageStrings)) then
|
|
self.pendingStorageStrings[r.class] = del(self.pendingStorageStrings[r.class])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
CheckForUpdateComplete()
|
|
end
|
|
|
|
del(additions)
|
|
del(subtractions)
|
|
del(changes)
|
|
|
|
self:CheckForMissingTalents()
|
|
end
|
|
|
|
-- ValidateUnit
|
|
local function ValidateUnit(r, guid)
|
|
local unit = r.unit
|
|
if (UnitGUID(unit) ~= guid) then
|
|
local name = r.name .. (r.realm and "-" or "") .. (r.realm or "")
|
|
local index = UnitInRaid(name)
|
|
if (index) then
|
|
r.unit = "raid"..index
|
|
return true
|
|
else
|
|
if (UnitGUID("player") == guid) then
|
|
r.unit = "player"
|
|
return true
|
|
|
|
elseif (UnitInParty(name)) then
|
|
for i = 1,4 do
|
|
if (UnitGUID("party"..i) == guid) then
|
|
r.unit = "party"..i
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- CountTree
|
|
local function CountTree(branch)
|
|
local count = 0
|
|
for i = 1,#branch do
|
|
count = count + branch:byte(i) - 48
|
|
end
|
|
return count
|
|
end
|
|
|
|
-- TalentWeight
|
|
local function TalentWeight(talents, class)
|
|
if (talents and #talents == 3 and class) then
|
|
local c1, c2, c3 = CountTree(talents[1]), CountTree(talents[2]), CountTree(talents[3])
|
|
|
|
local weight = 1
|
|
if (c2 > c1 and c2 > c3) then
|
|
weight = 2
|
|
elseif (c3 > c1 and c3 > c2) then
|
|
weight = 3
|
|
end
|
|
|
|
local data = lib.classTalentData[class]
|
|
if (data and data[weight]) then
|
|
return data[weight].name, c1, c2, c3
|
|
end
|
|
|
|
return weight, c1, c2, c3
|
|
end
|
|
return nil, 0, 0, 0
|
|
end
|
|
|
|
do
|
|
-- First segment: Player ID (from GUID), Name, level, class, activePage, TalentString
|
|
-- Subsequent: spec number, talentString()
|
|
|
|
-- crc
|
|
local function crc32(str)
|
|
local val = tonumber((select(2, GetBuildInfo()))) -- Use WoW build as CRC base
|
|
for i = 1,#str do
|
|
val = bit.band(val * 2 + str:byte(i), 0xFFFF)
|
|
end
|
|
return val
|
|
end
|
|
|
|
-- GetUnitStorageString
|
|
function lib:GetUnitStorageString(unit)
|
|
return self:GetGUIDStorageString(UnitGUID(unit))
|
|
end
|
|
|
|
-- GetGUIDStorageString
|
|
-- Make a storage string for mods to store talents.
|
|
-- Rules: 1) Your own realm only 2) Their talents are complete (nothing unspent)
|
|
function lib:GetGUIDStorageString(guid)
|
|
local r = self.roster[guid]
|
|
if (r) then
|
|
local id
|
|
local playerGUID = UnitGUID("player")
|
|
if (playerGUID:sub(1, 6) == guid:sub(1, 6)) then
|
|
-- Same realm code, so just trim it off. This is likely always true from what I've seen
|
|
id = format("%X", tonumber(guid:sub(7), 16))
|
|
else
|
|
id = guid:sub(4)
|
|
end
|
|
|
|
if (r.talents and r.active and not r.realm and (not r.unspent or not r.unspent[r.active])) then
|
|
if (r.level < 1) then
|
|
r.level = UnitLevel(r.name) or 0
|
|
end
|
|
local str = format("%s,%d,%s,%d,%d", id, r.level, r.class, r.active, r.numActive)
|
|
for i = 1,r.numActive do
|
|
local t = r.talents[i]
|
|
if (t) then
|
|
str = format("%s;%d,%s", str, i, table.concat(t, "-"))
|
|
end
|
|
end
|
|
return format("%s;%d", str, crc32(str))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- SetStorageString
|
|
function lib:SetStorageString(str, comms)
|
|
local ret, retInfo
|
|
if (str) then
|
|
local parts = new(strsplit(";", str))
|
|
if (#parts >= 2) then
|
|
local strCRC = tonumber(parts[#parts])
|
|
local temp = table.concat(parts, ";", 1, #parts - 1)
|
|
if (crc32(temp) == strCRC) then
|
|
local part1 = new(strsplit(",", parts[1]))
|
|
|
|
while true do
|
|
local guid
|
|
local id = part1[1]
|
|
if (id:len() < 12) then
|
|
-- Trimmed GUID, we'll prefix it with our own GUID's realm code
|
|
guid = format("%s%012X", UnitGUID("player"):sub(1, 6), tonumber(id, 16))
|
|
else
|
|
guid = format("0x0%015s", id)
|
|
end
|
|
|
|
local r = self.roster[guid]
|
|
if (not r) then
|
|
retInfo = format("Unexpected SetStorageString for ID %s", guid)
|
|
ret = true -- Still return true, we just didn't want this string yet
|
|
break
|
|
elseif (r.name == UNKNOWN) then
|
|
retInfo = format("Premature SetStorageString for ID %s", guid)
|
|
ret = true -- Still return true, we just didn't want this string yet
|
|
break
|
|
end
|
|
if (r.talents) then
|
|
-- We've already received talents for this player
|
|
ret = true -- Still return true, we just didn't want this string because we have their talents
|
|
break
|
|
end
|
|
|
|
if (not self.classTalentData[r.class]) then
|
|
-- Received a storage string for a class that we've not yet been able to scan
|
|
-- the talent trees for. We store this until we have that data
|
|
local classStorageStrings = self.pendingStorageStrings[r.class]
|
|
if (not classStorageStrings) then
|
|
classStorageStrings = new()
|
|
self.pendingStorageStrings[r.class] = classStorageStrings
|
|
end
|
|
classStorageStrings[guid] = str
|
|
ret = true
|
|
break
|
|
end
|
|
|
|
local level = tonumber(part1[2])
|
|
local class = part1[3]
|
|
local active = tonumber(part1[4])
|
|
local numActive = tonumber(part1[5])
|
|
|
|
if (r.level < 1) then
|
|
r.level = UnitLevel(r.name) or 0
|
|
end
|
|
if (level ~= r.level and r.level > 1) then
|
|
-- Won't accept talents for mismatched levels (but ignore errors reading the UnitLevel early)
|
|
retInfo = "Wrong level"
|
|
break
|
|
end
|
|
if (not r.class and class) then
|
|
-- If we don't have the class, but the storage string does, we'll take it
|
|
r.class = class
|
|
end
|
|
if (class ~= r.class) then
|
|
-- Class doesn't match, probably a char delete/remake or xrealm
|
|
retInfo = format("Wrong class: expected %q, got %q", tostring(r.class), tostring(class))
|
|
break
|
|
end
|
|
|
|
-- Now the talent trees
|
|
local talents = new()
|
|
for i = 2,#parts - 1 do
|
|
local partN = new(strsplit(",", parts[i]))
|
|
if (#partN == 2) then
|
|
local specNumber = tonumber(partN[1])
|
|
local specTalents = new(strsplit("-", partN[2]))
|
|
|
|
if (specNumber and #specTalents >= 3) then
|
|
talents[specNumber] = specTalents
|
|
else
|
|
del(specTalents)
|
|
talents = del(talents)
|
|
retInfo = "Invalid talent specs in tree "..i
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if (talents) then
|
|
r.talents = talents
|
|
r.active = active
|
|
r.numActive = numActive
|
|
if (comms ~= r.name) then
|
|
-- If comms part sends player name along with packet, then we'll skip the refresh later
|
|
-- which we'd normally do when Storage is set via app startup
|
|
r.refresh = true
|
|
else
|
|
r.refresh = nil
|
|
end
|
|
|
|
ValidateUnit(r, guid)
|
|
local newSpec, n1, n2, n3 = TalentWeight(r.talents[r.active], r.class)
|
|
self.events:Fire("LibGroupTalents_Update", guid, r.unit, newSpec, n1, n2, n3)
|
|
self:GetGUIDRole(guid, true)
|
|
ret = true
|
|
end
|
|
break
|
|
end
|
|
|
|
del(part1)
|
|
else
|
|
retInfo = "Invalid string"
|
|
end
|
|
end
|
|
del(parts)
|
|
end
|
|
|
|
return ret, retInfo
|
|
end
|
|
end
|
|
|
|
-- GetClassTalentData
|
|
-- Builds an internal table for talent name -> tree/index lookups.
|
|
function GetClassTalentData(unit)
|
|
local _, class = UnitClass(unit)
|
|
if (class) then
|
|
local data = lib.classTalentData[class]
|
|
if (not data) then
|
|
local isnotplayer = not UnitIsUnit("player", unit)
|
|
if (GetNumTalentTabs(isnotplayer) > 0) then
|
|
data = new()
|
|
|
|
for tab = 1, GetNumTalentTabs(isnotplayer) do
|
|
local tree = new()
|
|
local _
|
|
tree.name, tree.icon, _, tree.background = GetTalentTabInfo(tab, isnotplayer)
|
|
tinsert(data, tree)
|
|
|
|
tree.list = new()
|
|
for i = 1,GetNumTalents(tab, isnotplayer) do
|
|
local name, icon, tier, column, currentRank, maxRank = GetTalentInfo(tab, i, isnotplayer)
|
|
if (name) then
|
|
local entry = new()
|
|
entry.name = name
|
|
entry.icon = icon
|
|
entry.tier = tier
|
|
entry.column = column
|
|
entry.maxRank = maxRank
|
|
entry.index = i
|
|
entry.treeIndex = tab
|
|
tinsert(tree.list, entry)
|
|
if (not data.list) then
|
|
data.list = new()
|
|
end
|
|
data.list[name] = entry
|
|
end
|
|
end
|
|
end
|
|
|
|
if (next(data)) then
|
|
lib.classTalentData[class] = data
|
|
|
|
--for guid,r in pairs(lib.roster) do
|
|
-- if (r.class == class and r.talents) then
|
|
-- -- We picked up class talent data for a class after receiving talents for them via comms
|
|
-- -- So, we fire an Update event for any members of the class we already have so that
|
|
-- -- talents can now be interpreted correctly.
|
|
-- local spec, n1, n2, n3 = TalentWeight(r.talents[r.active], class)
|
|
-- lib.events:Fire("LibGroupTalents_Update", guid, unit, spec, n1, n2, n3)
|
|
-- end
|
|
--end
|
|
|
|
local classStorageStrings = lib.pendingStorageStrings[class]
|
|
if (classStorageStrings) then
|
|
local unitGUID = UnitGUID(unit)
|
|
for guid, str in pairs(classStorageStrings) do
|
|
if (guid ~= unitGUID) then
|
|
lib:SetStorageString(str)
|
|
end
|
|
end
|
|
lib.pendingStorageStrings[class] = del(lib.pendingStorageStrings[class])
|
|
end
|
|
else
|
|
deepDel(data)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GetTreeNames
|
|
function lib:GetTreeNames(class)
|
|
local info = self.classTalentData[class]
|
|
if (info) then
|
|
return info[1].name, info[2].name, info[3].name
|
|
end
|
|
end
|
|
|
|
-- GetTreeIcons
|
|
function lib:GetTreeIcons(class)
|
|
local info = self.classTalentData[class]
|
|
if (info) then
|
|
return info[1].icon, info[2].icon, info[3].icon
|
|
end
|
|
end
|
|
|
|
-- ReadTalentGroup
|
|
local function ReadTalentGroup(isnotplayer, group, class)
|
|
local numTabs = GetNumTalentTabs(isnotplayer)
|
|
if (numTabs and numTabs >= 3 and GetNumTalents(1, isnotplayer) > 0) then
|
|
local ctd = lib.classTalentData[class]
|
|
--[===[@debug@
|
|
assert(ctd and ctd[1] and ctd[2] and ctd[3])
|
|
assert(ctd[1].list and ctd[2].list and ctd[3].list)
|
|
--@end-debug@]===]
|
|
|
|
local n = new()
|
|
for tab = 1, numTabs do
|
|
local branchLength = GetNumTalents(tab, isnotplayer, nil, group)
|
|
if (branchLength ~= #ctd[tab].list) then
|
|
-- Tab tree size is not what we expected for this class
|
|
del(n)
|
|
return
|
|
end
|
|
|
|
local t = new()
|
|
local trim
|
|
for i = 1,branchLength do
|
|
local name, icon, tier, column, currentRank, maxRank = GetTalentInfo(tab, i, isnotplayer, nil, group)
|
|
tinsert(t, currentRank)
|
|
if (currentRank > 0) then
|
|
trim = i -- We strip off trailing zeros from talent strings to save storage space
|
|
end
|
|
end
|
|
|
|
tinsert(n, table.concat(t, nil, 1, trim or 0))
|
|
del(t)
|
|
end
|
|
|
|
return n
|
|
end
|
|
end
|
|
|
|
-- TalentQuery_Ready
|
|
function lib:TalentQuery_Ready_Outsider(e, name, realm, unit)
|
|
self:TalentQuery_Ready(e, name, realm, unit)
|
|
end
|
|
|
|
-- TalentQuery_Ready
|
|
function lib:TalentQuery_Ready(e, name, realm, unit)
|
|
GetClassTalentData(unit)
|
|
|
|
local guid = unit and UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
if (r) then
|
|
local namerealm = realm and realm ~= "" and name .. "-" .. realm or name
|
|
local isnotplayer = not UnitIsUnit(unit, "player")
|
|
|
|
if (GetTalentTabInfo(1, isnotplayer)) then
|
|
local active = GetActiveTalentGroup(isnotplayer)
|
|
local numActive = GetNumTalentGroups(isnotplayer)
|
|
local listUnspent, invalid
|
|
local talents = new()
|
|
|
|
for group = 1,numActive do
|
|
local n = ReadTalentGroup(isnotplayer, group, r.class)
|
|
if (n and #n >= 3) then
|
|
talents[group] = n
|
|
else
|
|
invalid = true
|
|
break
|
|
end
|
|
|
|
local unspent = GetUnspentTalentPoints(isnotplayer, nil, group)
|
|
if (unspent and unspent > 0) then
|
|
if (not listUnspent) then
|
|
listUnspent = new()
|
|
end
|
|
listUnspent[group] = unspent
|
|
end
|
|
end
|
|
|
|
if (isnotplayer and (invalid or (listUnspent and listUnspent[active] or 0) > 0)) then
|
|
-- Unit didn't have all their points spent in active group, so we'll have another look in 10 seconds
|
|
-- Don't need to check when it's "player" because we get PLAYER_TALENT_UPDATE event on changes
|
|
self:TriggerRefreshTalents(guid, 10)
|
|
end
|
|
|
|
if (not invalid) then
|
|
if (active > numActive) then
|
|
-- May be better to discard instead? We'll see
|
|
active = 1
|
|
end
|
|
self:OnReceiveTalents(guid, unit, talents, active, numActive, listUnspent)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
TalentQuery.RegisterCallback(lib, "TalentQuery_Ready")
|
|
TalentQuery.RegisterCallback(lib, "TalentQuery_Ready_Outsider")
|
|
|
|
-- GetUnitTalentSpec
|
|
function lib:GetUnitTalentSpec(unit, group)
|
|
return self:GetGUIDTalentSpec(UnitGUID(unit), group)
|
|
end
|
|
|
|
-- GetGUIDTalentSpec
|
|
function lib:GetGUIDTalentSpec(guid, group)
|
|
local talents, r = GetGUIDTalentsRaw(guid, group)
|
|
if (talents) then
|
|
return TalentWeight(talents, r.class)
|
|
end
|
|
end
|
|
|
|
-- CompareTalents
|
|
local function CompareTalents(tree1, tree2)
|
|
if ((tree1 ~= nil) ~= (tree2 ~= nil)) then
|
|
return
|
|
end
|
|
if (tree1 and tree2 and #tree1 == #tree2) then
|
|
for i = 1,#tree1 do
|
|
if (tree1[i] ~= tree2[i]) then
|
|
return
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- OnReceiveTalents
|
|
function lib:OnReceiveTalents(guid, unit, talents, active, numActive, listUnspent)
|
|
local r = self.roster[guid]
|
|
if (r) then
|
|
if (active ~= r.active or numActive ~= r.numActive or not CompareTalents(talents and talents[active], r.talents and r.talents[r.active])) then
|
|
local oldTalents
|
|
if (r.talents) then
|
|
oldTalents = r.talents[r.active]
|
|
end
|
|
del(r.unspent)
|
|
|
|
r.talents = talents
|
|
r.active = active
|
|
r.numActive = numActive
|
|
r.unspent = listUnspent
|
|
|
|
local newSpec, n1, n2, n3 = TalentWeight(r.talents[active], r.class)
|
|
|
|
local fired
|
|
if (oldTalents) then
|
|
-- For those cases when we didn't have the alternate talents for a player for whatever reason.
|
|
-- Maybe they just picked up dual talent spec, or gated to trainer to respec.
|
|
local oldSpec, o1, o2, o3 = TalentWeight(oldTalents, r.class)
|
|
|
|
if (o1 ~= n1 or o2 ~= n2 or o3 ~= n3) then
|
|
self.events:Fire("LibGroupTalents_Update", guid, unit, newSpec, n1, n2, n3, oldSpec, o1, o2, o3)
|
|
fired = true
|
|
end
|
|
end
|
|
|
|
if (not fired) then
|
|
self.events:Fire("LibGroupTalents_Update", guid, unit, newSpec, n1, n2, n3)
|
|
end
|
|
self:GetGUIDRole(guid, true)
|
|
|
|
tinsert(self.batch, guid)
|
|
CheckForUpdateComplete()
|
|
|
|
oldTalents = del(oldTalents)
|
|
return
|
|
end
|
|
end
|
|
del(talents)
|
|
end
|
|
|
|
-- OnReceiveGlyphs
|
|
function lib:OnReceiveGlyphs(guid, sender, glyphs)
|
|
local r = self.roster[guid]
|
|
if (r) then
|
|
if (ValidateUnit(r, guid)) then
|
|
local oldGlyphs
|
|
if (r.glyphs) then
|
|
oldGlyphs = r.glyphs[r.active]
|
|
r.glyphs = del(r.glyphs)
|
|
end
|
|
|
|
r.glyphs = glyphs
|
|
|
|
local newGlyphs = r.glyphs and r.glyphs[r.active]
|
|
if (newGlyphs ~= oldGlyphs) then
|
|
self.events:Fire("LibGroupTalents_GlyphUpdate", guid, r.unit)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
del(glyphs)
|
|
end
|
|
|
|
-- GetUnitGlyphs
|
|
function lib:GetUnitGlyphs(unit, group)
|
|
return self:GetGUIDGlyphs(UnitGUID(unit), group)
|
|
end
|
|
|
|
-- GetGUIDGlyphs
|
|
function lib:GetGUIDGlyphs(guid, group)
|
|
local r = self.roster[guid]
|
|
if (r) then
|
|
local g = r.glyphs and r.glyphs[group or r.active]
|
|
if (g) then
|
|
local temp = new(strsplit(",", g))
|
|
for i,str in ipairs(temp) do
|
|
temp[i] = tonumber(str)
|
|
end
|
|
local a, b, c, d, e, f = unpack(temp)
|
|
del(temp)
|
|
return a, b, c, d, e, f
|
|
end
|
|
end
|
|
end
|
|
|
|
-- UnitHasGlyph
|
|
function lib:UnitHasGlyph(unit, glyphID, group)
|
|
return lib:GUIDHasGlyph(UnitGUID(unit), glyphID, group)
|
|
end
|
|
|
|
-- GUIDHasGlyph
|
|
function lib:GUIDHasGlyph(guid, glyphID, group)
|
|
local ret
|
|
local r = self.roster[guid]
|
|
if (r) then
|
|
local g = r.glyphs and r.glyphs[group or r.active]
|
|
if (g) then
|
|
local temp = new(strsplit(",", g))
|
|
for i,str in ipairs(temp) do
|
|
local id = tonumber(str)
|
|
if (type(glyphID) == "number") then
|
|
if (glyphID == id) then
|
|
ret = true
|
|
break
|
|
end
|
|
else
|
|
if (glyphID == GetSpellInfo(id)) then
|
|
ret = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
del(temp)
|
|
end
|
|
end
|
|
return ret
|
|
end
|
|
|
|
-- GLYPH_ADDED
|
|
function lib:GLYPH_ADDED(index, a, b, c)
|
|
self:RefreshPlayerGlyphs()
|
|
end
|
|
|
|
-- GLYPH_REMOVED
|
|
function lib:GLYPH_REMOVED(index, a, b, c)
|
|
self:RefreshPlayerGlyphs()
|
|
end
|
|
|
|
-- GLYPH_UPDATED
|
|
function lib:GLYPH_UPDATED(index, a, b, c)
|
|
self:RefreshPlayerGlyphs()
|
|
end
|
|
|
|
-- RefreshPlayerGlyphs
|
|
function lib:RefreshPlayerGlyphs()
|
|
local guid = UnitGUID("player")
|
|
local r = self.roster[guid]
|
|
if (not r) then
|
|
return
|
|
end
|
|
|
|
local glyphs = new()
|
|
local any
|
|
for talentGroup = 1,GetNumTalentGroups() do
|
|
local list = new()
|
|
for i = 1,GetNumGlyphSockets() do
|
|
local enabled, glyphType, glyphSpell, icon = GetGlyphSocketInfo(i, talentGroup)
|
|
if (enabled and glyphType and glyphSpell) then
|
|
tinsert(list, glyphSpell)
|
|
any = true
|
|
end
|
|
end
|
|
glyphs[talentGroup] = table.concat(list, ",")
|
|
del(list)
|
|
end
|
|
|
|
local oldGlyphs = r.glyphs
|
|
if (any) then
|
|
r.glyphs = glyphs
|
|
else
|
|
del(glyphs)
|
|
end
|
|
|
|
local change = (oldGlyphs and oldGlyphs[r.active]) ~= (r.glyphs and r.glyphs[r.active])
|
|
if (change) then
|
|
self:SendMyGlyphs()
|
|
self.events:Fire("LibGroupTalents_GlyphUpdate", guid, "player")
|
|
end
|
|
|
|
del(oldGlyphs)
|
|
end
|
|
|
|
-- PLAYER_TALENT_UPDATE
|
|
function lib:PLAYER_TALENT_UPDATE()
|
|
self:TriggerRefreshTalents(UnitGUID("player"), 2)
|
|
end
|
|
|
|
-- UNIT_SPELLCAST_SUCCEEDED
|
|
function lib:UNIT_SPELLCAST_SUCCEEDED(unit, spell)
|
|
local newActiveGroup = specChangers[spell]
|
|
if (newActiveGroup) then
|
|
local guid = UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
if (r) then
|
|
if (newActiveGroup == r.active) then
|
|
-- We obviously didn't see them switch from this set
|
|
self:GetGUIDRole(guid, true)
|
|
return
|
|
end
|
|
|
|
if (r.talents) then
|
|
local oldSet = r.talents[r.active]
|
|
local newSet = r.talents[newActiveGroup]
|
|
if (oldSet and newSet) then
|
|
-- We have the other talent set, so no need to refresh anything. Just compare and notify
|
|
r.active = newActiveGroup
|
|
|
|
local oldSpec, o1, o2, o3 = TalentWeight(oldSet, r.class)
|
|
local newSpec, n1, n2, n3 = TalentWeight(newSet, r.class)
|
|
|
|
if (o1 ~= n1 or o2 ~= n2 or o3 ~= n3) then
|
|
self.events:Fire("LibGroupTalents_Update", guid, unit, newSpec, n1, n2, n3, oldSpec, o1, o2, o3)
|
|
else
|
|
self.events:Fire("LibGroupTalents_Update", guid, unit, newSpec, n1, n2, n3)
|
|
end
|
|
self:GetGUIDRole(guid, true)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- If we get this far, then someone probably gated to respec
|
|
self:RefreshTalentsByGUID(guid)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- TriggerRefreshTalents
|
|
function lib:TriggerRefreshTalents(guid, delay)
|
|
if (not self.talentTimers) then
|
|
self.talentTimers = new()
|
|
end
|
|
if (guid) then
|
|
self.talentTimers[guid] = GetTime() + delay
|
|
frame:Show()
|
|
end
|
|
end
|
|
|
|
-- RefreshTalentsByUnit
|
|
function lib:RefreshTalentsByUnit(unit)
|
|
local guid = UnitGUID(unit)
|
|
if (guid) then
|
|
self:RefreshTalentsByGUID(guid)
|
|
end
|
|
end
|
|
|
|
-- RefreshTalentsByGUID
|
|
function lib:RefreshTalentsByGUID(guid)
|
|
local r = self.roster[guid]
|
|
if (not r) then
|
|
return
|
|
end
|
|
if (not ValidateUnit(r, guid)) then
|
|
return
|
|
end
|
|
|
|
if (self.talentTimers) then
|
|
self.talentTimers[guid] = nil
|
|
end
|
|
|
|
if (not self.talentThrottle) then
|
|
self.talentThrottle = {}
|
|
end
|
|
for guidThrottle,when in pairs(self.talentThrottle) do
|
|
if (when < GetTime() - 5) then
|
|
self.talentThrottle[guidThrottle] = nil
|
|
elseif (guid == guidThrottle) then
|
|
return
|
|
end
|
|
end
|
|
self.talentThrottle[guid] = GetTime()
|
|
|
|
if (self.commQueried) then
|
|
self.commQueried[guid] = nil
|
|
if (not next(self.commQueried)) then
|
|
self.commQueried = del(self.commQueried)
|
|
end
|
|
end
|
|
|
|
r.refresh = true
|
|
self:CheckForMissingTalents()
|
|
|
|
if (UnitGUID("player") == guid) then
|
|
self:SendMyTalents()
|
|
end
|
|
end
|
|
|
|
-- CheckForMissingTalents
|
|
function lib:CheckForMissingTalents()
|
|
local any
|
|
for guid,info in pairs(self.roster) do
|
|
local namerealm = RosterInfoFullName(info)
|
|
if (not info.talents or (not UnitIsVisible(namerealm) and UnitExists(namerealm)) or info.refresh) then
|
|
any = true
|
|
info.refresh = nil
|
|
self:GetUnitTalents(info.unit, true)
|
|
end
|
|
end
|
|
|
|
if (any) then
|
|
lib.refreshCheckTimer = 15
|
|
frame:Show()
|
|
end
|
|
end
|
|
|
|
do
|
|
local survivalOfTheFittest = GetSpellInfo(33853) -- Survival of the Fittest
|
|
local protectorOfThePack = GetSpellInfo(57873) -- Protector of the Pack
|
|
local dkBladeBarrier = GetSpellInfo(49182) -- Blade Barrier
|
|
local dkToughness = GetSpellInfo(49042) -- Toughness
|
|
local dkAnticipation = GetSpellInfo(55129) -- Anticipation
|
|
|
|
-- GetUnitRole
|
|
function lib:GetUnitRole(unit, reset)
|
|
local guid = UnitGUID(unit)
|
|
if (guid) then
|
|
return self:GetGUIDRole(guid, reset)
|
|
end
|
|
end
|
|
|
|
-- GetGUIDRole
|
|
function lib:GetGUIDRole(guid, reset)
|
|
local r = guid and self.roster[guid]
|
|
if (not r) then
|
|
return
|
|
end
|
|
if (r.role and not reset) then
|
|
return r.role
|
|
end
|
|
if (not ValidateUnit(r, guid)) then
|
|
return
|
|
end
|
|
|
|
local class = r.class
|
|
local role
|
|
|
|
local unit = r.unit
|
|
if (class == "ROGUE" or class == "HUNTER") then
|
|
role = "melee"
|
|
elseif (class == "MAGE" or class == "WARLOCK") then
|
|
role = "caster"
|
|
elseif (r.talents and r.talents[r.active]) then
|
|
if (class == "DEATHKNIGHT") then
|
|
local score = self:GUIDHasTalent(guid, dkBladeBarrier) and 1 or 0
|
|
score = score + (self:GUIDHasTalent(guid, dkToughness) and 1 or 0)
|
|
score = score + (self:GUIDHasTalent(guid, dkAnticipation) and 1 or 0)
|
|
role = score >= 2 and "tank" or "melee" -- Has 2 of the 3 tanking talents at least
|
|
|
|
else
|
|
local specName, t1, t2, t3 = TalentWeight(r.talents[r.active], class)
|
|
|
|
if (class == "PRIEST") then
|
|
role = ((t1 + t2) > t3) and "healer" or "caster"
|
|
elseif (class == "WARRIOR") then
|
|
role = ((t1 + t2) > t3) and "melee" or "tank"
|
|
else
|
|
local heavy = (t1 > t2 and t1 > t3 and 1) or (t2 > t1 and t2 > t3 and 2) or (t3 > t1 and t3 > t2 and 3) or 0
|
|
if (class == "PALADIN") then
|
|
role = heavy == 1 and "healer" or heavy == 2 and "tank" or heavy == 3 and "melee"
|
|
|
|
elseif (class == "DRUID") then
|
|
if (heavy == 2) then
|
|
if (self:GUIDHasTalent(guid, survivalOfTheFittest) and self:GUIDHasTalent(guid, protectorOfThePack)) then
|
|
role = "tank"
|
|
else
|
|
role = "melee"
|
|
end
|
|
else
|
|
role = heavy == 1 and "caster" or "healer"
|
|
end
|
|
|
|
elseif (class == "SHAMAN") then
|
|
role = heavy == 1 and "caster" or heavy == 2 and "melee" or heavy == 3 and "healer"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local oldrole = r.role
|
|
r.role = role
|
|
|
|
if (role and role ~= oldrole) then
|
|
self.events:Fire("LibGroupTalents_RoleChange", guid, unit, role, oldrole)
|
|
end
|
|
return role
|
|
end
|
|
end
|
|
|
|
-- GetUnitTalents
|
|
function lib:GetUnitTalents(unit, refetch)
|
|
local guid = UnitGUID(unit)
|
|
if (not guid) then
|
|
return
|
|
end
|
|
return self:GetGUIDTalents(guid, refetch)
|
|
end
|
|
|
|
-- CanCommQuery
|
|
local function CanCommQuery(guid)
|
|
if (not lib.commQueried or not lib.commQueried[guid]) then
|
|
if (not lib.commQueried) then
|
|
lib.commQueried = new()
|
|
end
|
|
lib.commQueried[guid] = true
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- GetGUIDTalents
|
|
function lib:GetGUIDTalents(guid, refetch)
|
|
local r = self.roster[guid]
|
|
if (not r) then
|
|
return
|
|
end
|
|
if (not ValidateUnit(r, guid)) then
|
|
return
|
|
end
|
|
|
|
local unit = r.unit
|
|
local name, realm = UnitName(unit)
|
|
local activeTalents = r.talents and r.talents[r.active]
|
|
|
|
if (activeTalents) then
|
|
-- If someone is out of sight, we won't catch their talent swap spell cast, so we'll invalidate them here and recheck talents
|
|
if ((not UnitIsVisible(unit) and UnitIsConnected(unit)) or (self.outOfSight and self.outOfSight[guid])) then
|
|
if (not r.version) then
|
|
refetch = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if (not activeTalents or refetch) then
|
|
if (UnitIsUnit("player", unit)) then
|
|
self:RefreshPlayerGlyphs()
|
|
self:TalentQuery_Ready(nil, name, nil, unit)
|
|
|
|
elseif ((UnitInRaid(unit) or UnitInParty(unit)) and UnitIsConnected(unit)) then
|
|
TalentQuery:Query(unit)
|
|
|
|
local namerealm = UnitFullName(unit)
|
|
if (not r.talents and not r.requested) then
|
|
-- Don't need to query on a 'refetch' because they'll send changes anyway via comms
|
|
local skipGlyphs
|
|
if (not UnitIsVisible(unit) or not CanInspect(unit)) then
|
|
if (r.version) then
|
|
if (CanCommQuery(guid)) then
|
|
-- We request talents via comms for anyone that may be out of inspect range
|
|
self:SendCommMessage("REQUESTTALENTS", namerealm)
|
|
r.requested = true
|
|
skipGlyphs = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if (not r.glyphs and not skipGlyphs) then
|
|
if (r.version and r.version >= 15) then
|
|
if (CanCommQuery(guid)) then
|
|
-- They're in range to inspect, but we'll still want to ask for their glyphs
|
|
self:SendCommMessage("REQUESTGLYPHS", namerealm)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if (self.outOfSight) then
|
|
self.outOfSight[guid] = nil
|
|
end
|
|
end
|
|
|
|
return activeTalents
|
|
end
|
|
|
|
-- SendCommMessage
|
|
function lib:SendCommMessage(msg, target, channel)
|
|
if (msg) then
|
|
if (ChatThrottleLib) then
|
|
ChatThrottleLib:SendAddonMessage("NORMAL", MAJOR, msg, channel or "WHISPER", target)
|
|
else
|
|
SendAddonMessage(MAJOR, msg, channel or "WHISPER", target)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Throttle - Purposely local to here
|
|
-- Abuse prevention. Yes, who would abuse addon comms? Noone would make a macro to crash a mod user would they. Right?
|
|
-- Well, this one time, at band camp. Someone thought it was super funny to make a macro that DCd PallyPower users
|
|
local throttle
|
|
local function Throttle(sender, key)
|
|
if (not throttle) then
|
|
throttle = {}
|
|
end
|
|
local s = throttle[sender]
|
|
if (not s) then
|
|
s = {}
|
|
throttle[sender] = s
|
|
end
|
|
|
|
if ((s[key] or 0) < GetTime() - 4.5) then
|
|
-- Same message key only allowable once every 4.5 secs from 1 person (Respec cast time is 5 seconds)
|
|
s[key] = GetTime()
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- CHAT_MSG_ADDON
|
|
function lib:CHAT_MSG_ADDON(prefix, msg, channel, sender)
|
|
if (prefix == MAJOR) then
|
|
if (sender == UnitName("player")) then
|
|
return
|
|
elseif (not UnitInRaid(sender) and not UnitInParty(sender)) then
|
|
return
|
|
end
|
|
|
|
local guid = UnitGUID(sender)
|
|
if (not guid) then
|
|
return
|
|
end
|
|
local r = self.roster[guid]
|
|
if (not r) then
|
|
return
|
|
end
|
|
|
|
local cmd, str = msg:match("^(%a+) *(.*)$")
|
|
if (not cmd) then
|
|
return
|
|
end
|
|
|
|
if (cmd == "TALENTS") then
|
|
-- Talents come in form of:
|
|
local t = r.talents
|
|
r.talents = nil -- SetStorageString won't overwrite talents usually, but we want it to here, without providing a means to do it easily with an arg from a mod
|
|
if (not self:SetStorageString(str, sender)) then
|
|
r.talents = t
|
|
else
|
|
deepDel(t)
|
|
end
|
|
|
|
elseif (cmd == "GLYPHS") then
|
|
local invalid
|
|
local pages = new(strsplit(";", str))
|
|
local glyphs = new()
|
|
for page,info in ipairs(pages) do
|
|
local list = new(strsplit(",", info))
|
|
local tab = tonumber(tremove(list, 1))
|
|
if (tab) then
|
|
glyphs[tab] = table.concat(list, ",")
|
|
del(list)
|
|
else
|
|
invalid = true
|
|
del(glyphs)
|
|
del(list)
|
|
break
|
|
end
|
|
end
|
|
if (not invalid) then
|
|
self:OnReceiveGlyphs(guid, sender, glyphs)
|
|
end
|
|
del(pages)
|
|
|
|
elseif (cmd == "REQUESTTALENTS") then
|
|
if (Throttle(sender, "REQUESTTALENTS")) then
|
|
if ((r.version or 0) < 39) then
|
|
if (lib.sentToOld and lib.sentToOld[guid]) then
|
|
return
|
|
end
|
|
if (not lib.sentToOld) then
|
|
lib.sentToOld = new()
|
|
end
|
|
lib.sentToOld[guid] = time()
|
|
end
|
|
|
|
self:SendMyTalents(sender)
|
|
self:SendMyGlyphs(sender)
|
|
end
|
|
|
|
elseif (cmd == "REQUESTGLYPHS") then
|
|
if (Throttle(sender, "REQUESTGLYPHS")) then
|
|
self:SendMyGlyphs(sender)
|
|
end
|
|
|
|
elseif (cmd == "HELLO") then
|
|
r.version = tonumber(str)
|
|
if (channel ~= "WHISPER") then
|
|
if (lib.sentToOld) then
|
|
lib.sentToOld[guid] = nil
|
|
end
|
|
if (UnitIsConnected(sender) and Throttle(sender, "HELLO")) then
|
|
self:SendCommMessage("HELLO "..MINOR, sender)
|
|
self:SendMyGlyphs(sender)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- SendMy
|
|
local function SendMy(sender, str)
|
|
if (sender) then
|
|
if (UnitIsConnected(sender)) then
|
|
lib:SendCommMessage(str, sender)
|
|
end
|
|
else
|
|
for guid,info in pairs(lib.roster) do
|
|
if (info.version) then
|
|
local namerealm = RosterInfoFullName(info)
|
|
if (UnitIsConnected(namerealm)) then
|
|
lib:SendCommMessage(str, namerealm)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- SendMyTalents
|
|
function lib:SendMyTalents(sender)
|
|
if (sender or self:UserCount() > 0) then
|
|
local str = self:GetGUIDStorageString(UnitGUID("player"))
|
|
if (str) then
|
|
SendMy(sender, "TALENTS "..str)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- SendMyGlyphs
|
|
function lib:SendMyGlyphs(sender)
|
|
if (sender or self:UserCount() > 0) then
|
|
local r = self.roster[UnitGUID("player")]
|
|
if (r and r.glyphs) then
|
|
local str = "GLYPHS "
|
|
local i = 1
|
|
for tab,g in pairs(r.glyphs) do
|
|
local temp = format("%d,%s", tab, g)
|
|
str = str .. (i > 1 and ";" or "") .. temp
|
|
i = i + 1
|
|
end
|
|
SendMy(sender, str)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- UserCount
|
|
function lib:UserCount()
|
|
local count = 0
|
|
for guid,info in pairs(self.roster) do
|
|
if (info.version and not UnitIsUnit("player", RosterInfoFullName(info))) then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
-- UnitHasTalent
|
|
-- eg: lib:UnitHasTalent("player", GetSpellInfo(talentSpellID))
|
|
-- Returns: nil, or number of points spent into talent
|
|
-- If the talent group is not specified, then the active talent group is used
|
|
function lib:UnitHasTalent(unit, talentName, group)
|
|
return unit and self:GUIDHasTalent(UnitGUID(unit), talentName, group)
|
|
end
|
|
|
|
-- GUIDHasTalent
|
|
-- Returns: nil, or number of points spent into talent
|
|
function lib:GUIDHasTalent(guid, talentName, group)
|
|
local talents, r = GetGUIDTalentsRaw(guid, group)
|
|
if (talents and r.class) then
|
|
local data = self.classTalentData[r.class]
|
|
if (data) then
|
|
local info = data.list and data.list[talentName]
|
|
if (info) then
|
|
local str = talents[info.treeIndex]
|
|
if (str) then
|
|
local amount = (str:byte(info.index) or 48) - 48
|
|
return (amount or 0) > 0 and amount or nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GetClassTalentInfo
|
|
function lib:GetClassTalentInfo(class, talentName)
|
|
-- Returns: Max Rank, Icon, Tab, Tier, Column, Tree Index
|
|
local data = self.classTalentData[class]
|
|
if (data) then
|
|
local info = data.list and data.list[talentName]
|
|
if (info) then
|
|
return info.maxRank, info.icon, info.treeIndex, info.column, info.tier, info.index
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GetActiveTalentGroup
|
|
function lib:GetActiveTalentGroup(unit)
|
|
if (UnitIsUnit(unit, "player")) then
|
|
return GetActiveTalentGroup()
|
|
else
|
|
local guid = unit and UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
return r and r.active or nil
|
|
end
|
|
end
|
|
|
|
-- GetNumTalentGroups
|
|
function lib:GetNumTalentGroups(unit)
|
|
if (UnitIsUnit(unit, "player")) then
|
|
return GetNumTalentGroups()
|
|
else
|
|
local guid = unit and UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
return r and r.numActive or nil
|
|
end
|
|
end
|
|
|
|
-- GetTalentTabInfo
|
|
function lib:GetTalentTabInfo(unit, tab, group)
|
|
if (UnitIsUnit(unit, "player")) then
|
|
return GetTalentTabInfo(tab, nil, nil, group or GetActiveTalentGroup())
|
|
else
|
|
local guid = unit and UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
if (r and r.class) then
|
|
local ctd = self.classTalentData[r.class]
|
|
if (ctd and tab >= 1 and tab <= #ctd) then
|
|
local spec, c1, c2, c3 = self:GetGUIDTalentSpec(guid, group)
|
|
return ctd[tab].name, ctd[tab].icon, tab == 1 and c1 or tab == 2 and c2 or c3, ctd[tab].background, 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GetNumTalents
|
|
function lib:GetNumTalents(unit, tab)
|
|
if (UnitIsUnit(unit, "player")) then
|
|
return GetNumTalents(tab)
|
|
else
|
|
local _, class = UnitClass(unit)
|
|
if (class) then
|
|
local ctd = self.classTalentData[class]
|
|
if (ctd and tab >= 1 and tab <= #ctd) then
|
|
return #ctd[tab].list
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GetTalentInfo
|
|
function lib:GetTalentInfo(unit, tab, index, group)
|
|
if (UnitIsUnit(unit, "player")) then
|
|
return GetTalentInfo(tab, index, nil, nil, group or GetActiveTalentGroup())
|
|
else
|
|
local _, class = UnitClass(unit)
|
|
if (class) then
|
|
local ctd = self.classTalentData[class]
|
|
if (ctd and tab >= 1 and tab <= #ctd) then
|
|
local info = ctd[tab].list[index]
|
|
if (info) then
|
|
local spent = self:UnitHasTalent(unit, info.name, group)
|
|
return info.name, info.icon, info.tier, info.column, spent or 0, info.maxRank
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GetNumTalentTabs
|
|
function lib:GetNumTalentTabs()
|
|
return GetNumTalentTabs()
|
|
end
|
|
|
|
-- GetNumTalentTabs
|
|
function lib:GetUnspentTalentPoints(unit, group)
|
|
if (UnitIsUnit(unit, "player")) then
|
|
return GetUnspentTalentPoints(nil, nil, group)
|
|
else
|
|
local guid = unit and UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
if (r) then
|
|
return r.unspent and r.unspent[group or r.active or 1]
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GetTalentCount
|
|
function lib:GetTalentCount()
|
|
local count, missing = 0, 0
|
|
for guid,info in pairs(self.roster) do
|
|
if (info.talents) then
|
|
count = count + 1
|
|
else
|
|
missing = missing + 1
|
|
end
|
|
end
|
|
return count, missing
|
|
end
|
|
|
|
-- GetTalentMissingNames
|
|
function lib:GetTalentMissingNames()
|
|
local list = new()
|
|
for unit in self:IterateRoster() do
|
|
local guid = UnitGUID(unit)
|
|
local r = guid and self.roster[guid]
|
|
if (not r or not r.talents) then
|
|
tinsert(list, UnitFullName(unit))
|
|
end
|
|
end
|
|
local ret
|
|
if (next(list)) then
|
|
ret = table.concat(list, ",")
|
|
end
|
|
del(list)
|
|
return ret
|
|
end
|
|
|
|
-- PurgeAndRescanTalents
|
|
function lib:PurgeAndRescanTalents()
|
|
if (self.roster) then
|
|
wipe(self.pendingStorageStrings)
|
|
for guid,info in pairs(self.roster) do
|
|
info.talents = del(info.talents)
|
|
info.active = nil
|
|
info.numActive = nil
|
|
info.requested = nil
|
|
end
|
|
end
|
|
self:CheckForMissingTalents()
|
|
end
|
|
|
|
-- Roster iterator
|
|
do
|
|
local function iter(t)
|
|
local key = t.id
|
|
local ret
|
|
if (t.mode == "raid") then
|
|
if (key > t.r) then
|
|
del(t)
|
|
return nil
|
|
end
|
|
ret = "raid"..key
|
|
else
|
|
if (key > t.p) then
|
|
del(t)
|
|
return nil
|
|
end
|
|
ret = key == 0 and "player" or "party"..key
|
|
end
|
|
t.id = key + 1
|
|
return ret
|
|
end
|
|
|
|
-- IterateRoster
|
|
function lib:IterateRoster()
|
|
local t = new()
|
|
if (GetNumRaidMembers() > 0) then
|
|
t.mode = "raid"
|
|
t.id = 1
|
|
t.r = GetNumRaidMembers()
|
|
else
|
|
t.mode = "party"
|
|
t.id = 0
|
|
t.p = GetNumPartyMembers()
|
|
end
|
|
return iter, t
|
|
end
|
|
end |