--[[ |
Name: LibGroupTalents-1.0 |
Revision: $Rev: 62 $ |
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: Specialization Tree, spent1, spent2, spent3, non-localized name of specialization tree |
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, then the three non-localized tree names (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 ID, tree name, tree description, tree icon, points spent, non-localized tree name, preview points spent (or 0 if not player), isSpecialization |
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 |
GetPrimaryTalentTree(unit[, group]) -- Returns: Primary Talent tree for the unit |
GetMajorTalentTreeBonuses(unit[, tab]) -- Returns: Major talent tree bonus |
GetMinorTalentTreeBonuses(unit[, tab]) -- Returns: Minor talent tree bonus |
GetTalentTreeEarlySpells(unit[, tab]) -- Returns: Spells that are learned early? |
GetTalentTreeMasterySpells(unit[, tab]) -- Returns: Mastery bonus spell |
GetTalentTreeRoles(unit[, tab ]) -- Returns: Blizzard roles for a talent tree |
|
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: 62 $"):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, data[weight].nlname |
end |
|
return weight, c1, c2, c3, nil |
end |
return nil, 0, 0, 0, nil |
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,%s", str, i, table.concat(t, "-"), r.primary[i] or "") |
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() |
local primary = 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 |
|
if (#partN >= 3) then |
primary[specNumber] = tonumber(partN[3]) |
else |
-- Don't have stored primary.. we'll infer it |
if (#specTalents[1] > 0 and #specTalents[1] > #specTalents[2] and #specTalents[1] > #specTalents[3]) then |
primary[specNumber] = 1 |
elseif (#specTalents[2] > 0 and #specTalents[2] > #specTalents[1] and #specTalents[2] > #specTalents[3]) then |
primary[specNumber] = 2 |
elseif (#specTalents[3] > 0 and #specTalents[3] > #specTalents[1] and #specTalents[3] > #specTalents[2]) then |
primary[specNumber] = 3 |
end |
end |
else |
del(specTalents) |
talents = del(talents) |
primary = del(primary) |
retInfo = "Invalid talent specs in tree "..i |
break |
end |
end |
end |
|
if (talents) then |
r.talents = talents |
r.active = active |
r.numActive = numActive |
r.primary = primary |
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 |
|
-- AddTreeBonuses |
local function AddTreeBonuses(data, tree, tab, bonusIndex, bonuskey, ...) |
local count = select("#", ...) |
if (count > 0) then |
for i = 1,count do |
local spellId = select(i, ...) |
if (spellId) then |
local name, rank, icon = GetSpellInfo(spellId) |
local entry = new() |
entry.spellId = spellId |
--[===[@debug@ |
entry.name = name |
entry.icon = icon |
--@end-debug@]===] |
entry.tier = 0 |
entry.bonus = bonuskey |
bonusIndex = bonusIndex + 1 |
entry.bonusIndex = bonusIndex |
entry.treeIndex = tab |
|
local bonus = tree.bonus |
if (not bonus) then |
bonus = new() |
tree.bonus = bonus |
end |
tinsert(bonus, entry) |
|
data.list[name] = entry |
end |
end |
end |
return bonusIndex |
end |
|
-- GetClassTalentData |
-- Builds an internal table for talent name -> tree/index lookups. |
local 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.tabid, tree.name, tree.desc, tree.icon, _, tree.nlname = 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 |
|
local bonusIndex = 0 |
bonusIndex = AddTreeBonuses(data, tree, tab, bonusIndex, "major", GetMajorTalentTreeBonuses(tab, isnotplayer)) |
bonusIndex = AddTreeBonuses(data, tree, tab, bonusIndex, "minor", GetMinorTalentTreeBonuses(tab, isnotplayer)) |
bonusIndex = AddTreeBonuses(data, tree, tab, bonusIndex, "early", GetTalentTreeEarlySpells(tab, isnotplayer)) |
bonusIndex = AddTreeBonuses(data, tree, tab, bonusIndex, "mastery", GetTalentTreeMasterySpells(tab, isnotplayer)) |
|
-- Store Blizzards talent tree roles per tab. These are fixed values per spec, rather than dependant on player's talent choices |
local role1, role2 = GetTalentTreeRoles(tab, isnotplayer) |
if (role1) then |
if (role2) then |
tree.roles = role1..","..role2 |
else |
tree.roles = role1 |
end |
end |
end |
|
if (next(data)) then |
lib.classTalentData[class] = data |
|
-- Now process any stored strings we received without class data for that class |
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, info[1].nlname, info[2].nlname, info[3].nlname |
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, primary |
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 |
|
local p = GetPrimaryTalentTree(isnotplayer, nil, group) |
if (p) then |
if (not primary) then |
primary = new() |
end |
primary[group] = p |
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, primary, 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, primary, 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.primary = primary |
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, glyphTooltipIndex, 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 naturalReaction = GetSpellInfo(57878) -- Natural Reaction |
local survivalInstincts = GetSpellInfo(61336) -- Survival Instincts |
local thickHide = GetSpellInfo(16929) -- Thick Hide |
|
-- 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 |
local _, t1, t2, t3 = TalentWeight(r.talents[r.active], class) |
|
if ((t1 + t2 + t3) > 0) then |
if (class == "PRIEST") then |
role = ((t1 + t2) > t3) and "healer" or "caster" |
elseif (class == "WARRIOR") then |
role = ((t1 + t2) > t3) and "melee" or "tank" |
elseif (class == "DEATHKNIGHT") then |
role = ((t2 + t3) > t1) 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, naturalReaction) and self:GUIDHasTalent(guid, survivalInstincts) and self:GUIDHasTalent(guid, thickHide)) 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 ~= 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 |
if (info.bonus) then |
local primary = r.primary and r.primary[r.active or group or 1] |
if (primary) then |
if (r.level >= 80 or info.bonus ~= "master") then |
return primary == info.treeIndex and 1 or nil |
end |
end |
else |
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 |
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 |
if (info.bonus) then |
return 1, info.icon, info.treeIndex |
else |
return info.maxRank, info.icon, info.treeIndex, info.column, info.tier, info.index |
end |
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].tabid, ctd[tab].name, ctd[tab].desc, ctd[tab].icon, tab == 1 and c1 or tab == 2 and c2 or c3, ctd[tab].nlname, 0, ctd[tab].name == spec |
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 |
|
-- GetPrimaryTalentTree |
function lib:GetPrimaryTalentTree(unit, group) |
if (UnitIsUnit(unit, "player")) then |
return GetPrimaryTalentTree(nil, nil, group) |
else |
local guid = unit and UnitGUID(unit) |
local r = guid and self.roster[guid] |
if (r) then |
return r.primary and r.primary[group or r.active or 1] |
end |
end |
end |
|
-- getTalentTreeBonuses |
local function getTalentTreeBonuses(bonusKey, unit, tab) |
local _, class = UnitClass(unit) |
local ctd = self.classTalentData[class] |
if (ctd and tab >= 1 and tab <= #ctd) then |
local bonus = ctd[tab] and ctd[tab].bonus |
if (bonus) then |
local ret |
for i,entry in ipairs(bonus) do |
if (entry.bonus == bonusKey) then |
if (not ret) then |
ret = {} |
end |
tinsert(ret, entry.spellId) |
end |
end |
if (ret) then |
return unpack(ret) |
end |
end |
end |
end |
|
-- GetMajorTalentTreeBonuses |
function lib:GetMajorTalentTreeBonuses(unit, tab) |
if (UnitIsUnit(unit, "player")) then |
return GetMajorTalentTreeBonuses(tab) |
elseif tab then |
return getTalentTreeBonuses("major", unit, tab) |
end |
end |
|
-- GetMinorTalentTreeBonuses |
function lib:GetMinorTalentTreeBonuses(unit, tab) |
if (UnitIsUnit(unit, "player")) then |
return GetMinorTalentTreeBonuses(tab) |
elseif tab then |
return getTalentTreeBonuses("minor", unit, tab) |
end |
end |
|
-- GetTalentTreeEarlySpells |
function lib:GetTalentTreeEarlySpells(unit, tab) |
if (UnitIsUnit(unit, "player")) then |
return GetTalentTreeEarlySpells(tab) |
elseif tab then |
return getTalentTreeBonuses("early", unit, tab) |
end |
end |
|
-- GetTalentTreeMasterySpells |
function lib:GetTalentTreeMasterySpells(unit, tab) |
if (UnitIsUnit(unit, "player")) then |
return GetTalentTreeMasterySpells(tab) |
elseif tab then |
return getTalentTreeBonuses("master", unit, tab) |
end |
end |
|
-- GetTalentTreeRoles |
function lib:GetTalentTreeRoles(unit, tab) |
-- self.talentTree, self.inspect, self.pet |
if (UnitIsUnit(unit, "player")) then |
return GetTalentTreeRoles(tab) |
elseif tab then |
local _, class = UnitClass(unit) |
local ctd = self.classTalentData[class] |
if (ctd and tab >= 1 and tab <= #ctd) then |
local roles = ctd[tab] and ctd[tab].roles |
if (roles) then |
if (strfind(roles, ",")) then |
local role1, role2 = strsplit(",", roles) |
return role1, role2 |
end |
return roles |
end |
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 |