/trunk/RaidToolkit/libs
## Interface: 30300 |
## LoadOnDemand: 1 |
## Title: Lib: GroupTalents-1.0 |
## Notes: Library to help with querying unit talents. |
## Author: Zek |
## Version: $Rev: 51 $ |
## OptionalDeps: Ace3, LibTalentQuery-1.0 |
## X-Category: Library |
## X-ReleaseDate: $Date$ |
## X-Website: http://wowace.com/wiki/LibGroupTalents-1.0 |
## X-License: MIT |
## X-Curse-Packaged-Version: 3.3 Release 3 |
## X-Curse-Project-Name: LibGroupTalents-1.0 |
## X-Curse-Project-ID: libgrouptalents-1-0 |
## X-Curse-Repository-ID: wow/libgrouptalents-1-0/mainline |
lib.xml |
------------------------------------------------------------------------ |
r56 | greltok | 2010-09-30 14:49:58 -0500 (Thu, 30 Sep 2010) | 1 line |
Changed paths: |
A /tags/3.3 Release 3 (from /trunk:55) |
Tagging as 3.3 Release 3 |
------------------------------------------------------------------------ |
r55 | greltok | 2010-09-23 23:25:05 -0500 (Thu, 23 Sep 2010) | 1 line |
Changed paths: |
M /trunk/LibGroupTalents-1.0.lua |
Ticket 6 - Use full name where needed. |
------------------------------------------------------------------------ |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\..\FrameXML\UI.xsd"> |
<Script file="LibGroupTalents-1.0.lua"/> |
</Ui> |
--[[ |
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 |
## Interface: 30300 |
## X-Curse-Packaged-Version: Release-r960 |
## Interface: 40000 |
## X-Curse-Packaged-Version: Release-r971 |
## X-Curse-Project-Name: Ace3 |
## X-Curse-Project-ID: ace3 |
## X-Curse-Repository-ID: wow/ace3/mainline |
Ace3 Release - Revision r971 (October 12th, 2010) |
------------------------------------------------- |
- Small fixes and adjustments for the 4.0 Content Patch. |
- AceGUI-3.0: ScrollFrame: Allow for a small margin of error when determining if the scroll bar should be shown. |
- AceGUI-3.0: Added new widget APIs: GetText for EditBox and DisableButton for MultiLineEditBox |
Ace3 Release - Revision r960 (July 20th, 2010) |
---------------------------------------------- |
- AceGUI-3.0: Label: Reset Image Size and TexCoords on Acquire (Ticket #110) |
-- as well as associate it with a slash command. |
-- @class file |
-- @name AceConfig-3.0 |
-- @release $Id: AceConfig-3.0.lua 877 2009-11-02 15:56:50Z nevcairiel $ |
-- @release $Id: AceConfig-3.0.lua 969 2010-10-07 02:11:48Z shefki $ |
--[[ |
AceConfig-3.0 |
local cfgreg = LibStub("AceConfigRegistry-3.0") |
local cfgcmd = LibStub("AceConfigCmd-3.0") |
local cfgdlg = LibStub("AceConfigDialog-3.0") |
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0") |
--TODO: local cfgdlg = LibStub("AceConfigDialog-3.0", true) |
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0", true) |
-- Lua APIs |
local pcall, error, type, pairs = pcall, error, type, pairs |
-- You can supply a slash command (or a table of slash commands) to register with AceConfigCmd directly. |
-- @paramsig appName, options [, slashcmd] |
-- @param appName The application name for the config table. |
-- @param options The option table (or a function to generate one on demand) |
-- @param options The option table (or a function to generate one on demand). http://www.wowace.com/addons/ace3/pages/ace-config-3-0-options-tables/ |
-- @param slashcmd A slash command to register for the option table, or a table of slash commands. |
-- @usage |
-- local AceConfig = LibStub("AceConfig-3.0") |
--- AceConfigDialog-3.0 generates AceGUI-3.0 based windows based on option tables. |
-- @class file |
-- @name AceConfigDialog-3.0 |
-- @release $Id: AceConfigDialog-3.0.lua 958 2010-07-03 10:22:29Z nevcairiel $ |
-- @release $Id: AceConfigDialog-3.0.lua 967 2010-09-25 08:20:55Z nevcairiel $ |
local LibStub = LibStub |
local MAJOR, MINOR = "AceConfigDialog-3.0", 49 |
local MAJOR, MINOR = "AceConfigDialog-3.0", 50 |
local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR) |
if not AceConfigDialog then return end |
end |
local OrderA, OrderB = tempOrders[a] or 100, tempOrders[b] or 100 |
if OrderA == OrderB then |
local NameA = (type(tempNames[a] == "string") and tempNames[a]) or "" |
local NameB = (type(tempNames[b] == "string") and tempNames[b]) or "" |
local NameA = (type(tempNames[a]) == "string") and tempNames[a] or "" |
local NameB = (type(tempNames[b]) == "string") and tempNames[b] or "" |
return NameA:upper() < NameB:upper() |
end |
if OrderA < 0 then |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="AceTab-3.0.lua"/> |
<Script file="AceConfigTab-3.0.lua"/> |
</Ui> |
--[[ $Id: CallbackHandler-1.0.lua 895 2009-12-06 16:28:55Z nevcairiel $ ]] |
local MAJOR, MINOR = "CallbackHandler-1.0", 5 |
--[[ $Id: CallbackHandler-1.0.lua 965 2010-08-09 00:47:52Z mikk $ ]] |
local MAJOR, MINOR = "CallbackHandler-1.0", 6 |
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR) |
if not CallbackHandler then return end -- No upgrade needed |
regfunc = function(...) self[method](self,...) end |
end |
else |
-- function ref with self=object or self="addonId" |
if type(self)~="table" and type(self)~="string" then |
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string expected.", 2) |
-- function ref with self=object or self="addonId" or self=thread |
if type(self)~="table" and type(self)~="string" and type(self)~="thread" then |
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2) |
end |
if select("#",...)>=1 then -- this is not the same as testing for arg==nil! |
local Type, Version = "MultiLineEditBox", 22 |
local Type, Version = "MultiLineEditBox", 23 |
local AceGUI = LibStub and LibStub("AceGUI-3.0", true) |
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end |
-- GLOBALS: ACCEPT, ChatFontNormal |
--[[----------------------------------------------------------------------------- |
Support functions |
-------------------------------------------------------------------------------]] |
local function Layout(self) |
self:SetHeight(self.numlines * 14 + (self.disablebutton and 19 or 41) + self.labelHeight) |
if self.labelHeight == 0 then |
self.scrollBar:SetPoint("TOP", self.frame, "TOP", 0, -23) |
else |
self.scrollBar:SetPoint("TOP", self.label, "BOTTOM", 0, -19) |
end |
if self.disablebutton then |
self.scrollBar:SetPoint("BOTTOM", self.frame, "BOTTOM", 0, 21) |
self.scrollBG:SetPoint("BOTTOMLEFT", 0, 4) |
else |
self.scrollBar:SetPoint("BOTTOM", self.button, "TOP", 0, 18) |
self.scrollBG:SetPoint("BOTTOMLEFT", self.button, "TOPLEFT") |
end |
end |
--[[----------------------------------------------------------------------------- |
Scripts |
-------------------------------------------------------------------------------]] |
local function OnClick(self) -- Button |
Methods |
-------------------------------------------------------------------------------]] |
local methods = { |
["GetText"] = function(self) |
return self.editBox:GetText() |
end, |
["OnAcquire"] = function(self) |
self.editBox:SetText("") |
self:SetDisabled(false) |
self:SetWidth(200) |
self:DisableButton(false) |
self:SetNumLines() |
self.entered = nil |
self:SetMaxLetters(0) |
end, |
["OnRelease"] = function(self) |
self.frame:ClearAllPoints() |
self.frame:Hide() |
end, |
-- ["OnRelease"] = nil, |
["SetDisabled"] = function(self, disabled) |
local editBox = self.editBox |
self.label:SetText(text) |
if self.labelHeight ~= 10 then |
self.labelHeight = 10 |
self.scrollBar:SetPoint("TOP", self.label, "BOTTOM", 0, -19) |
self:SetHeight(self.frame.height + 10) |
self.label:Show() |
end |
elseif self.labelHeight ~= 0 then |
self.labelHeight = 0 |
self.label:Hide() |
self.scrollBar:SetPoint("TOP", self.frame, "TOP", 0, -23) |
self:SetHeight(self.frame.height - 10) |
end |
Layout(self) |
end, |
["SetNumLines"] = function(self, value) |
if not value or value < 4 then |
value = 4 |
end |
self:SetHeight(value * 14 + 41 + self.labelHeight) |
self.numlines = value |
Layout(self) |
end, |
["SetText"] = function(self, text) |
self.editBox:SetText(text) |
end, |
["GetText"] = function(self) |
return self.editBox:GetText() |
end, |
["SetMaxLetters"] = function (self, num) |
self.editBox:SetMaxLetters(num or 0) |
end, |
["DisableButton"] = function(self, disabled) |
self.disablebutton = disabled |
if disabled then |
self.button:Hide() |
else |
self.button:Show() |
end |
Layout(self) |
end |
} |
label:SetHeight(10) |
local button = CreateFrame("Button", ("%s%dButton"):format(Type, widgetNum), frame, "UIPanelButtonTemplate2") |
button:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 4) |
button:SetPoint("BOTTOMLEFT", 0, 4) |
button:SetHeight(22) |
button:SetWidth(label:GetStringWidth() + 24) |
button:SetText(ACCEPT) |
frame = frame, |
label = label, |
labelHeight = 10, |
numlines = 4, |
scrollBar = scrollBar, |
scrollBG = scrollBG, |
scrollFrame = scrollFrame, |
type = Type |
} |
end |
button.obj, editBox.obj, scrollFrame.obj = widget, widget, widget |
AceGUI:RegisterAsWidget(widget) |
return widget |
return AceGUI:RegisterAsWidget(widget) |
end |
AceGUI:RegisterWidgetType(Type, Constructor, Version) |
--[[----------------------------------------------------------------------------- |
EditBox Widget |
-------------------------------------------------------------------------------]] |
local Type, Version = "EditBox", 21 |
local Type, Version = "EditBox", 22 |
local AceGUI = LibStub and LibStub("AceGUI-3.0", true) |
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end |
HideButton(self) |
end, |
["GetText"] = function(self, text) |
return self.editbox:GetText() |
end, |
["SetLabel"] = function(self, text) |
if text and text ~= "" then |
self.label:SetText(text) |
["DisableButton"] = function(self, disabled) |
self.disablebutton = disabled |
if disabled then |
HideButton(self) |
end |
end, |
["SetMaxLetters"] = function (self, num) |
ScrollFrame Container |
Plain container that scrolls its content and doesn't grow in height. |
-------------------------------------------------------------------------------]] |
local Type, Version = "ScrollFrame", 20 |
local Type, Version = "ScrollFrame", 21 |
local AceGUI = LibStub and LibStub("AceGUI-3.0", true) |
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end |
-- Lua APIs |
local pairs, assert, type = pairs, assert, type |
local min, max, floor = math.min, math.max, math.floor |
local min, max, floor, abs = math.min, math.max, math.floor, math.abs |
-- WoW APIs |
local CreateFrame, UIParent = CreateFrame, UIParent |
local status = self.status or self.localstatus |
local height, viewheight = self.scrollframe:GetHeight(), self.content:GetHeight() |
if height > viewheight then |
self.scrollbar:Hide() |
else |
self.scrollbar:Show() |
if self.scrollBarShown then |
local diff = height - viewheight |
local delta = 1 |
if value < 0 then |
local height, viewheight = self.scrollframe:GetHeight(), self.content:GetHeight() |
local offset = status.offset or 0 |
local curvalue = self.scrollbar:GetValue() |
if viewheight < height then |
-- Give us a margin of error of 2 pixels to stop some conditions that i would blame on floating point inaccuracys |
-- No-one is going to miss 2 pixels at the bottom of the frame, anyhow! |
if viewheight < height + 2 then |
if self.scrollBarShown then |
self.scrollBarShown = nil |
self.scrollbar:Hide() |
------------------------------------------------------------------------ |
r972 | nevcairiel | 2010-10-12 18:56:37 +0000 (Tue, 12 Oct 2010) | 1 line |
Changed paths: |
A /tags/Release-r971 (from /trunk:971) |
Tag r971 as release |
------------------------------------------------------------------------ |
r971 | nevcairiel | 2010-10-12 18:55:55 +0000 (Tue, 12 Oct 2010) | 1 line |
Changed paths: |
M /trunk/changelog.txt |
Update changelog |
------------------------------------------------------------------------ |
r970 | nevcairiel | 2010-10-12 18:17:30 +0000 (Tue, 12 Oct 2010) | 1 line |
Changed paths: |
M /trunk/Ace3.toc |
Bump toc |
------------------------------------------------------------------------ |
r969 | shefki | 2010-10-07 02:11:48 +0000 (Thu, 07 Oct 2010) | 2 lines |
Changed paths: |
M /trunk/AceConfig-3.0/AceConfig-3.0.lua |
Add link to documentation of the contents of the options table. |
------------------------------------------------------------------------ |
r968 | nevcairiel | 2010-10-06 06:44:54 +0000 (Wed, 06 Oct 2010) | 1 line |
Changed paths: |
M /trunk/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua |
AceGUI-3.0: ScrollFrame: Allow for a small margin of error when determining if the scroll bar should be shown. |
------------------------------------------------------------------------ |
r967 | nevcairiel | 2010-09-25 08:20:55 +0000 (Sat, 25 Sep 2010) | 1 line |
Changed paths: |
M /trunk/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua |
AceConfigDialog-3.0: Fix a typo that broke options sorting by name in case of same order (Ticket #172) |
------------------------------------------------------------------------ |
r966 | nevcairiel | 2010-09-18 06:34:04 +0000 (Sat, 18 Sep 2010) | 1 line |
Changed paths: |
D /trunk/AceTab-3.0/AceConfigTab-3.0.lua |
M /trunk/AceTab-3.0/AceTab-3.0.xml |
Remove AceConfigTab as it was never functional and breaks tab completion on 4.0 |
------------------------------------------------------------------------ |
r965 | mikk | 2010-08-09 00:47:52 +0000 (Mon, 09 Aug 2010) | 1 line |
Changed paths: |
M /trunk/CallbackHandler-1.0/CallbackHandler-1.0.lua |
Upgrade CallbackHandler to rev6 (hrmph... should this be an extern? i guess that might cause packager funz though..) |
------------------------------------------------------------------------ |
--[[ |
Name: LibTalentQuery-1.0 |
Revision: $Rev: 84 $ |
Author: Rich Martel (richmartel@gmail.com) |
Documentation: http://wowace.com/wiki/LibTalentQuery-1.0 |
SVN: svn://svn.wowace.com/wow/libtalentquery-1-0/mainline/trunk |
Description: Library to help with querying unit talents |
Dependancies: LibStub, CallbackHandler-1.0 |
License: LGPL v2.1 |
Example Usage: |
local TalentQuery = LibStub:GetLibrary("LibTalentQuery-1.0") |
TalentQuery.RegisterCallback(self, "TalentQuery_Ready") |
local raidTalents = {} |
... |
TalentQuery:Query(unit) |
... |
function MyAddon:TalentQuery_Ready(e, name, realm, unitid) |
local isnotplayer = not UnitIsUnit(unitid, "player") |
local spec = {} |
for tab = 1, GetNumTalentTabs(isnotplayer) do |
local treename, _, pointsspent = GetTalentTabInfo(tab, isnotplayer) |
tinsert(spec, pointsspent) |
end |
raidTalents[UnitGUID(unitid)] = spec |
end |
]] |
local MAJOR, MINOR = "LibTalentQuery-1.0", 90000 + tonumber(("$Rev: 84 $"):match("(%d+)")) |
local lib = LibStub:NewLibrary(MAJOR, MINOR) |
if not lib then return end |
local INSPECTDELAY = 1 |
local INSPECTTIMEOUT = 5 |
if not lib.events then |
lib.events = LibStub("CallbackHandler-1.0"):New(lib) |
end |
local validateTrees |
local enteredWorld = IsLoggedIn() |
local frame = lib.frame |
if not frame then |
frame = CreateFrame("Frame", MAJOR .. "_Frame") |
lib.frame = frame |
end |
frame:UnregisterAllEvents() |
frame:RegisterEvent("INSPECT_TALENT_READY") |
frame:RegisterEvent("PLAYER_ENTERING_WORLD") |
frame:RegisterEvent("PLAYER_LEAVING_WORLD") |
frame:RegisterEvent("PLAYER_LOGIN") |
frame:SetScript("OnEvent", function(this, event, ...) |
return lib[event](lib, ...) |
end) |
do |
local lastUpdateTime = 0 |
frame:SetScript("OnUpdate", function(this, elapsed) |
lastUpdateTime = lastUpdateTime + elapsed |
if lastUpdateTime > INSPECTDELAY then |
lib:CheckInspectQueue() |
lastUpdateTime = 0 |
end |
end) |
frame:Hide() |
end |
local inspectQueue = lib.inspectQueue or {} |
lib.inspectQueue = inspectQueue |
local garbageQueue = lib.garbageQueue or {} -- Added a second queue to things. Inspects that initially fail are now |
lib.garbageQueue = garbageQueue -- thrown into second queue will will be processed once main queue is empty |
if next(inspectQueue) then |
frame:Show() |
end |
local UnitIsPlayer = _G.UnitIsPlayer |
local UnitName = _G.UnitName |
local UnitExists = _G.UnitExists |
local UnitGUID = _G.UnitGUID |
local GetNumRaidMembers = _G.GetNumRaidMembers |
local GetNumPartyMembers = _G.GetNumPartyMembers |
local UnitIsVisible = _G.UnitIsVisible |
local UnitIsConnected = _G.UnitIsConnected |
local UnitCanAttack = _G.UnitCanAttack |
local CanInspect = _G.CanInspect |
local function UnitFullName(unit) |
local name, realm = UnitName(unit) |
local namerealm = realm and realm ~= "" and name .. "-" .. realm or name |
return namerealm |
end |
-- GuidToUnitID |
local function GuidToUnitID(guid) |
local prefix, min, max = "raid", 1, GetNumRaidMembers() |
if max == 0 then |
prefix, min, max = "party", 0, GetNumPartyMembers() |
end |
-- Prioritise getting direct units first because other players targets |
-- can change between notify and event which can bugger things up |
for i = min, max do |
local unit = i == 0 and "player" or prefix .. i |
if (UnitGUID(unit) == guid) then |
return unit |
end |
end |
-- This properly detects target units |
if (UnitGUID("target") == guid) then |
return "target" |
elseif (UnitGUID("focus") == guid) then |
return "focus" |
elseif (UnitGUID("mouseover") == guid) then |
return "mouseover" |
end |
for i = min, max + 3 do |
local unit |
if i == 0 then |
unit = "player" |
elseif i == max + 1 then |
unit = "target" |
elseif i == max + 2 then |
unit = "focus" |
elseif i == max + 3 then |
unit = "mouseover" |
else |
unit = prefix .. i |
end |
if (UnitGUID(unit .. "target") == guid) then |
return unit .. "target" |
elseif (i <= max and UnitGUID(unit.."pettarget") == guid) then |
return unit .. "pettarget" |
end |
end |
return nil |
end |
-- Query |
function lib:Query(unit) |
if (UnitLevel(unit) < 10 or UnitName(unit) == UNKNOWN) then |
return |
end |
self.lastQueuedInspectReceived = nil |
if UnitIsUnit(unit, "player") then |
self.events:Fire("TalentQuery_Ready", UnitName("player"), nil, "player") |
else |
if type(unit) ~= "string" then |
error(("Bad argument #2 to 'Query'. Expected %q, received %q (%s)"):format("string", type(unit), tostring(unit)), 2) |
elseif not UnitExists(unit) or not UnitIsPlayer(unit) then |
error(("Bad argument #2 to 'Query'. %q is not a valid player unit"):format(tostring(unit)), 2) |
elseif not UnitExists(unit) or not UnitIsPlayer(unit) then |
error(("Bad argument #2 to 'Query'. %q does not require a server query before reading talents"):format("player"), 2) |
else |
local name = UnitFullName(unit) |
if (not inspectQueue[name]) then |
inspectQueue[name] = UnitGUID(unit) |
garbageQueue[name] = nil |
end |
frame:Show() |
end |
end |
end |
-- CheckInspectQueue |
-- Originally, it would wait until no pending NotifyInspect() were expected, and then do it's own. |
-- It was also only bother looking at ready results if it had triggered the Notify for that occasion. |
-- For the changes I've done, no assumption is made about which mod is performing NotifyInspect(). |
-- We note the name, unit, time of any inspects done whether from this queue or any other source, |
-- we remove from our queue any we were expecting, and use a seperate event in case extra talent |
-- info is any time wanted (opportunistic refreshes etc) - Zeksie, 20th May 2009 |
function lib:CheckInspectQueue() |
if (_G.InspectFrame and _G.InspectFrame:IsShown()) then |
return |
end |
if (not self.lastInspectTime or self.lastInspectTime < GetTime() - INSPECTTIMEOUT) then |
self.lastInspectPending = 0 |
end |
if (self.lastInspectPending > 0 or not enteredWorld) then |
return |
end |
if (self.lastQueuedInspectReceived and self.lastQueuedInspectReceived < GetTime() - 60) then |
-- No queued results received for a minute, so purge the queue as invalid and move on with our lives |
self.lastQueuedInspectReceived = nil |
inspectQueue = {} |
lib.inspectQueue = inspectQueue |
garbageQueue = {} |
lib.garbageQueue = garbageQueue |
frame:Hide() |
return |
end |
for name,guid in pairs(inspectQueue) do |
local unit = GuidToUnitID(guid) |
if (not unit) then |
inspectQueue[name] = nil |
else |
if (UnitIsVisible(unit) and UnitIsConnected(unit) and not UnitCanAttack("player", unit) and not UnitCanAttack(unit, "player") and CanInspect(unit) and UnitClass(unit)) then |
NotifyInspect(unit) |
break |
else |
garbageQueue[name] = guid -- Not available, throw into secondary queue and continue |
inspectQueue[name] = nil |
end |
end |
end |
if (not next(inspectQueue)) then |
if (next(garbageQueue)) then |
-- Retry initially failed inspects |
lib.inspectQueue = garbageQueue |
inspectQueue = lib.inspectQueue |
lib.garbageQueue = {} |
garbageQueue = lib.garbageQueue |
else |
frame:Hide() |
end |
end |
end |
-- NotifyInspect |
if not lib.NotifyInspect then -- don't hook twice |
hooksecurefunc("NotifyInspect", function(...) return lib:NotifyInspect(...) end) |
end |
function lib:NotifyInspect(unit) |
if (not (UnitExists(unit) and UnitIsVisible(unit) and UnitIsConnected(unit) and CheckInteractDistance(unit, 4))) then |
return |
end |
self.lastInspectUnit = unit |
self.lastInspectGUID = UnitGUID(unit) |
self.lastInspectTime = GetTime() |
self.lastInspectName = UnitFullName(unit) |
self.lastInspectPending = self.lastInspectPending + 1 |
local isnotplayer = not UnitIsUnit("player", unit) |
self.lastInspectTree = GetTalentTabInfo(1, isnotplayer) -- Talent tree names are available immediately |
end |
-- Reset |
function lib:Reset() |
self.lastInspectPending = 0 |
self.lastInspectUnit = nil |
self.lastInspectTime = nil |
self.lastInspectName = nil |
self.lastInspectGUID = nil |
self.lastInspectTree = nil |
end |
-- INSPECT_TALENT_READY |
function lib:INSPECT_TALENT_READY() |
self.lastInspectPending = self.lastInspectPending - 1 |
-- Results are valid only when we have received as many events as we have posted notifies |
if (self.lastInspectName and self.lastInspectPending == 0) then |
-- Check unit ID is still pointing to same actual unit |
if (UnitGUID(self.lastInspectUnit) == self.lastInspectGUID) then |
local guid = inspectQueue[self.lastInspectName] |
inspectQueue[self.lastInspectName] = nil |
local name, realm = strsplit("-", self.lastInspectName) |
self.lastQueuedInspectReceived = GetTime() |
-- Notify of expected talent results |
local isnotplayer = not UnitIsUnit("player", self.lastInspectName) |
local group = GetActiveTalentGroup(isnotplayer) |
local tree1, _, spent1 = GetTalentTabInfo(1, isnotplayer, nil, group) |
if (tree1 ~= self.lastInspectTree) then |
-- Expected talent tree name to be the same as it was when we triggered the NotifyInspect() |
garbageQueue[self.lastInspectName] = self.lastInspectGUID |
self:Reset() |
self:CheckInspectQueue() |
return |
elseif (validateTrees) then |
-- Double checking here. Check the tree name matches what we expect for this class |
local _, class = UnitClass(self.lastInspectUnit) |
if (tree1 ~= validateTrees[class]) then |
garbageQueue[self.lastInspectName] = self.lastInspectGUID |
self:Reset() |
self:CheckInspectQueue() |
return |
end |
end |
local tree2, _, spent2 = GetTalentTabInfo(2, isnotplayer, nil, group) |
local tree3, _, spent3 = GetTalentTabInfo(3, isnotplayer, nil, group) |
if ((spent1 or 0) + (spent2 or 0) + (spent3 or 0) > 0) then |
if (guid) then |
-- It was in our queue |
self.events:Fire("TalentQuery_Ready", name, realm, self.lastInspectUnit) |
else |
-- Also notify of non-expected ones, as it's entirely useful to refresh them if they're there |
-- It is up to the receiving applicating to determine whether they want to receive the information |
self.events:Fire("TalentQuery_Ready_Outsider", name, realm, self.lastInspectUnit) |
end |
else |
-- Tree came back with zero points spent, probably an issue while logging in |
garbageQueue[self.lastInspectName] = guid |
end |
end |
self:Reset() |
self:CheckInspectQueue() |
end |
end |
function lib:PLAYER_ENTERING_WORLD() |
-- We can't inspect other's talents until now |
-- We just get 0/0/0 back even though we get an INSPECT_TALENT_READY event |
enteredWorld = true |
end |
function lib:PLAYER_LEAVING_WORLD() |
enteredWorld = nil |
end |
function lib:PLAYER_LOGIN() |
validateTrees = { |
DRUID = "Balance", |
PRIEST = "Discipline", |
ROGUE = "Assassination", |
HUNTER = "Beast Mastery", |
WARLOCK = "Affliction", |
WARRIOR = "Arms", |
DEATHKNIGHT = "Blood", |
PALADIN = "Holy", |
SHAMAN = "Elemental", |
MAGE = "Arcane", |
} |
if (GetLocale() ~= "enUS" and GetLocale() ~= "enGB") then |
-- LibBabble-TalentTree-3.0 only loaded if present and not enUS |
local LBT = LibStub("LibBabble-TalentTree-3.0", true) |
if (not LBT) then |
LoadAddOn("LibBabble-TalentTree-3.0") |
LBT = LibStub("LibBabble-TalentTree-3.0", true) |
end |
LBT = LBT and LBT:GetLookupTable() |
if (LBT) then |
for class,tree1 in pairs(validateTrees) do |
validateTrees[class] = LBT[tree1] |
end |
else |
validateTrees = nil |
end |
end |
self.PLAYER_LOGIN = nil |
end |
lib:Reset() |
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info |
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke |
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS! |
local LibStub = _G[LIBSTUB_MAJOR] |
if not LibStub or LibStub.minor < LIBSTUB_MINOR then |
LibStub = LibStub or {libs = {}, minors = {} } |
_G[LIBSTUB_MAJOR] = LibStub |
LibStub.minor = LIBSTUB_MINOR |
function LibStub:NewLibrary(major, minor) |
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)") |
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.") |
local oldminor = self.minors[major] |
if oldminor and oldminor >= minor then return nil end |
self.minors[major], self.libs[major] = minor, self.libs[major] or {} |
return self.libs[major], oldminor |
end |
function LibStub:GetLibrary(major, silent) |
if not self.libs[major] and not silent then |
error(("Cannot find a library instance of %q."):format(tostring(major)), 2) |
end |
return self.libs[major], self.minors[major] |
end |
function LibStub:IterateLibraries() return pairs(self.libs) end |
setmetatable(LibStub, { __call = LibStub.GetLibrary }) |
end |
## Interface: 20400 |
## Title: Lib: LibStub |
## Notes: Universal Library Stub |
## Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel |
## X-Website: http://jira.wowace.com/browse/LS |
## X-Category: Library |
## X-License: Public Domain |
## X-Curse-Packaged-Version: 3.3 Release 2 |
## X-Curse-Project-Name: LibTalentQuery-1.0 |
## X-Curse-Project-ID: libtalentquery-1-0 |
## X-Curse-Repository-ID: wow/libtalentquery-1-0/mainline |
LibStub.lua |
## Interface: 30300 |
## LoadOnDemand: 1 |
## Title: Lib: TalentQuery 1.0 |
## Notes: Library to help with querying unit talents. |
## Author: Peragor |
## Version: $Rev: 82 $ |
## X-Category: Library |
## X-ReleaseDate: $Date: 2009-12-09 11:08:46 +0000 (Wed, 09 Dec 2009) $ |
## X-Website: http://wowace.com/wiki/LibTalentQuery-1.0 |
## X-License: LGPL v2.1 |
## X-Curse-Packaged-Version: 3.3 Release 2 |
## X-Curse-Project-Name: LibTalentQuery-1.0 |
## X-Curse-Project-ID: libtalentquery-1-0 |
## X-Curse-Repository-ID: wow/libtalentquery-1-0/mainline |
LibStub\LibStub.lua |
CallbackHandler-1.0\CallbackHandler-1.0.lua |
lib.xml |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\..\FrameXML\UI.xsd"> |
<Script file="LibTalentQuery-1.0.lua"/> |
</Ui> |
--[[ $Id: CallbackHandler-1.0.lua 60697 2008-02-09 16:51:20Z nevcairiel $ ]] |
local MAJOR, MINOR = "CallbackHandler-1.0", 3 |
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR) |
if not CallbackHandler then return end -- No upgrade needed |
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end} |
local type = type |
local pcall = pcall |
local pairs = pairs |
local assert = assert |
local concat = table.concat |
local loadstring = loadstring |
local next = next |
local select = select |
local type = type |
local xpcall = xpcall |
local function errorhandler(err) |
return geterrorhandler()(err) |
end |
local function CreateDispatcher(argCount) |
local code = [[ |
local next, xpcall, eh = ... |
local method, ARGS |
local function call() method(ARGS) end |
local function dispatch(handlers, ...) |
local index |
index, method = next(handlers) |
if not method then return end |
local OLD_ARGS = ARGS |
ARGS = ... |
repeat |
xpcall(call, eh) |
index, method = next(handlers, index) |
until not method |
ARGS = OLD_ARGS |
end |
return dispatch |
]] |
local ARGS, OLD_ARGS = {}, {} |
for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end |
code = code:gsub("OLD_ARGS", concat(OLD_ARGS, ", ")):gsub("ARGS", concat(ARGS, ", ")) |
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler) |
end |
local Dispatchers = setmetatable({}, {__index=function(self, argCount) |
local dispatcher = CreateDispatcher(argCount) |
rawset(self, argCount, dispatcher) |
return dispatcher |
end}) |
-------------------------------------------------------------------------- |
-- CallbackHandler:New |
-- |
-- target - target object to embed public APIs in |
-- RegisterName - name of the callback registration API, default "RegisterCallback" |
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback" |
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API. |
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName, OnUsed, OnUnused) |
-- TODO: Remove this after beta has gone out |
assert(not OnUsed and not OnUnused, "ACE-80: OnUsed/OnUnused are deprecated. Callbacks are now done to registry.OnUsed and registry.OnUnused") |
RegisterName = RegisterName or "RegisterCallback" |
UnregisterName = UnregisterName or "UnregisterCallback" |
if UnregisterAllName==nil then -- false is used to indicate "don't want this method" |
UnregisterAllName = "UnregisterAllCallbacks" |
end |
-- we declare all objects and exported APIs inside this closure to quickly gain access |
-- to e.g. function names, the "target" parameter, etc |
-- Create the registry object |
local events = setmetatable({}, meta) |
local registry = { recurse=0, events=events } |
-- registry:Fire() - fires the given event/message into the registry |
function registry:Fire(eventname, ...) |
if not rawget(events, eventname) or not next(events[eventname]) then return end |
local oldrecurse = registry.recurse |
registry.recurse = oldrecurse + 1 |
Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...) |
registry.recurse = oldrecurse |
if registry.insertQueue and oldrecurse==0 then |
-- Something in one of our callbacks wanted to register more callbacks; they got queued |
for eventname,callbacks in pairs(registry.insertQueue) do |
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten. |
for self,func in pairs(callbacks) do |
events[eventname][self] = func |
-- fire OnUsed callback? |
if first and registry.OnUsed then |
registry.OnUsed(registry, target, eventname) |
first = nil |
end |
end |
end |
registry.insertQueue = nil |
end |
end |
-- Registration of a callback, handles: |
-- self["method"], leads to self["method"](self, ...) |
-- self with function ref, leads to functionref(...) |
-- "addonId" (instead of self) with function ref, leads to functionref(...) |
-- all with an optional arg, which, if present, gets passed as first argument (after self if present) |
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]]) |
if type(eventname) ~= "string" then |
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2) |
end |
method = method or eventname |
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten. |
if type(method) ~= "string" and type(method) ~= "function" then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2) |
end |
local regfunc |
if type(method) == "string" then |
-- self["method"] calling style |
if type(self) ~= "table" then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2) |
elseif self==target then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2) |
elseif type(self[method]) ~= "function" then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2) |
end |
if select("#",...)>=1 then -- this is not the same as testing for arg==nil! |
local arg=select(1,...) |
regfunc = function(...) self[method](self,arg,...) end |
else |
regfunc = function(...) self[method](self,...) end |
end |
else |
-- function ref with self=object or self="addonId" |
if type(self)~="table" and type(self)~="string" then |
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string expected.", 2) |
end |
if select("#",...)>=1 then -- this is not the same as testing for arg==nil! |
local arg=select(1,...) |
regfunc = function(...) method(arg,...) end |
else |
regfunc = method |
end |
end |
if events[eventname][self] or registry.recurse<1 then |
-- if registry.recurse<1 then |
-- we're overwriting an existing entry, or not currently recursing. just set it. |
events[eventname][self] = regfunc |
-- fire OnUsed callback? |
if registry.OnUsed and first then |
registry.OnUsed(registry, target, eventname) |
end |
else |
-- we're currently processing a callback in this registry, so delay the registration of this new entry! |
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency |
registry.insertQueue = registry.insertQueue or setmetatable({},meta) |
registry.insertQueue[eventname][self] = regfunc |
end |
end |
-- Unregister a callback |
target[UnregisterName] = function(self, eventname) |
if not self or self==target then |
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2) |
end |
if type(eventname) ~= "string" then |
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2) |
end |
if rawget(events, eventname) and events[eventname][self] then |
events[eventname][self] = nil |
-- Fire OnUnused callback? |
if registry.OnUnused and not next(events[eventname]) then |
registry.OnUnused(registry, target, eventname) |
end |
end |
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then |
registry.insertQueue[eventname][self] = nil |
end |
end |
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds |
if UnregisterAllName then |
target[UnregisterAllName] = function(...) |
if select("#",...)<1 then |
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2) |
end |
if select("#",...)==1 and ...==target then |
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2) |
end |
for i=1,select("#",...) do |
local self = select(i,...) |
if registry.insertQueue then |
for eventname, callbacks in pairs(registry.insertQueue) do |
if callbacks[self] then |
callbacks[self] = nil |
end |
end |
end |
for eventname, callbacks in pairs(events) do |
if callbacks[self] then |
callbacks[self] = nil |
-- Fire OnUnused callback? |
if registry.OnUnused and not next(callbacks) then |
registry.OnUnused(registry, target, eventname) |
end |
end |
end |
end |
end |
end |
return registry |
end |
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it |
-- try to upgrade old implicit embeds since the system is selfcontained and |
-- relies on closures to work. |
------------------------------------------------------------------------ |
r85 | zeksie | 2010-08-27 16:58:05 +0000 (Fri, 27 Aug 2010) | 1 line |
Changed paths: |
A /tags/3.3 Release 2 (from /trunk:84) |
Tagging as 3.3 Release 2 |
------------------------------------------------------------------------ |
r84 | greltok | 2010-08-24 07:38:56 +0000 (Tue, 24 Aug 2010) | 2 lines |
Changed paths: |
M /trunk/LibTalentQuery-1.0.lua |
Now handles "mouseover" when converting from guid to unitid. |
Reported in Curse comments by kliegs. |
------------------------------------------------------------------------ |
--[[ |
Name: LibDeformat-3.0 |
Author(s): ckknight (ckknight@gmail.com) |
Website: http://www.wowace.com/projects/libdeformat-3-0/ |
Description: A library to convert a post-formatted string back to its original arguments given its format string. |
License: MIT |
]] |
local LibDeformat = LibStub:NewLibrary("LibDeformat-3.0", 1) |
if not LibDeformat then |
return |
end |
-- this function does nothing and returns nothing |
local function do_nothing() |
end |
-- a dictionary of format to match entity |
local FORMAT_SEQUENCES = { |
["s"] = ".+", |
["c"] = ".", |
["%d*d"] = "%%-?%%d+", |
["[fg]"] = "%%-?%%d+%%.?%%d*", |
["%%%.%d[fg]"] = "%%-?%%d+%%.?%%d*", |
} |
-- a set of format sequences that are string-based, i.e. not numbers. |
local STRING_BASED_SEQUENCES = { |
["s"] = true, |
["c"] = true, |
} |
local cache = setmetatable({}, {__mode='k'}) |
-- generate the deformat function for the pattern, or fetch from the cache. |
local function get_deformat_function(pattern) |
local func = cache[pattern] |
if func then |
return func |
end |
-- escape the pattern, so that string.match can use it properly |
local unpattern = '^' .. pattern:gsub("([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") .. '$' |
-- a dictionary of index-to-boolean representing whether the index is a number rather than a string. |
local number_indexes = {} |
-- (if the pattern is a numbered format,) a dictionary of index-to-real index. |
local index_translation = nil |
-- the highest found index, also the number of indexes found. |
local highest_index |
if not pattern:find("%%1%$") then |
-- not a numbered format |
local i = 0 |
while true do |
i = i + 1 |
local first_index |
local first_sequence |
for sequence in pairs(FORMAT_SEQUENCES) do |
local index = unpattern:find("%%%%" .. sequence) |
if index and (not first_index or index < first_index) then |
first_index = index |
first_sequence = sequence |
end |
end |
if not first_index then |
break |
end |
unpattern = unpattern:gsub("%%%%" .. first_sequence, "(" .. FORMAT_SEQUENCES[first_sequence] .. ")", 1) |
number_indexes[i] = not STRING_BASED_SEQUENCES[first_sequence] |
end |
highest_index = i - 1 |
else |
-- a numbered format |
local i = 0 |
while true do |
i = i + 1 |
local found_sequence |
for sequence in pairs(FORMAT_SEQUENCES) do |
if unpattern:find("%%%%" .. i .. "%%%$" .. sequence) then |
found_sequence = sequence |
break |
end |
end |
if not found_sequence then |
break |
end |
unpattern = unpattern:gsub("%%%%" .. i .. "%%%$" .. found_sequence, "(" .. FORMAT_SEQUENCES[found_sequence] .. ")", 1) |
number_indexes[i] = not STRING_BASED_SEQUENCES[found_sequence] |
end |
highest_index = i - 1 |
i = 0 |
index_translation = {} |
pattern:gsub("%%(%d)%$", function(w) |
i = i + 1 |
index_translation[i] = tonumber(w) |
end) |
end |
if highest_index == 0 then |
cache[pattern] = do_nothing |
else |
--[=[ |
-- resultant function looks something like this: |
local unpattern = ... |
return function(text) |
local a1, a2 = text:match(unpattern) |
if not a1 then |
return nil, nil |
end |
return a1+0, a2 |
end |
-- or if it were a numbered pattern, |
local unpattern = ... |
return function(text) |
local a2, a1 = text:match(unpattern) |
if not a1 then |
return nil, nil |
end |
return a1+0, a2 |
end |
]=] |
local t = {} |
t[#t+1] = [=[ |
return function(text) |
local ]=] |
for i = 1, highest_index do |
if i ~= 1 then |
t[#t+1] = ", " |
end |
t[#t+1] = "a" |
if not index_translation then |
t[#t+1] = i |
else |
t[#t+1] = index_translation[i] |
end |
end |
t[#t+1] = [=[ = text:match(]=] |
t[#t+1] = ("%q"):format(unpattern) |
t[#t+1] = [=[) |
if not a1 then |
return ]=] |
for i = 1, highest_index do |
if i ~= 1 then |
t[#t+1] = ", " |
end |
t[#t+1] = "nil" |
end |
t[#t+1] = "\n" |
t[#t+1] = [=[ |
end |
]=] |
t[#t+1] = "return " |
for i = 1, highest_index do |
if i ~= 1 then |
t[#t+1] = ", " |
end |
t[#t+1] = "a" |
t[#t+1] = i |
if number_indexes[i] then |
t[#t+1] = "+0" |
end |
end |
t[#t+1] = "\n" |
t[#t+1] = [=[ |
end |
]=] |
t = table.concat(t, "") |
-- print(t) |
cache[pattern] = assert(loadstring(t))() |
end |
return cache[pattern] |
end |
--- Return the arguments of the given format string as found in the text. |
-- @param text The resultant formatted text. |
-- @param pattern The pattern used to create said text. |
-- @return a tuple of values, either strings or numbers, based on the pattern. |
-- @usage LibDeformat.Deformat("Hello, friend", "Hello, %s") == "friend" |
-- @usage LibDeformat.Deformat("Hello, friend", "Hello, %1$s") == "friend" |
-- @usage LibDeformat.Deformat("Cost: $100", "Cost: $%d") == 100 |
-- @usage LibDeformat.Deformat("Cost: $100", "Cost: $%1$d") == 100 |
-- @usage LibDeformat.Deformat("Alpha, Bravo", "%s, %s") => "Alpha", "Bravo" |
-- @usage LibDeformat.Deformat("Alpha, Bravo", "%1$s, %2$s") => "Alpha", "Bravo" |
-- @usage LibDeformat.Deformat("Alpha, Bravo", "%2$s, %1$s") => "Bravo", "Alpha" |
-- @usage LibDeformat.Deformat("Hello, friend", "Cost: $%d") == nil |
-- @usage LibDeformat("Hello, friend", "Hello, %s") == "friend" |
function LibDeformat.Deformat(text, pattern) |
if type(text) ~= "string" then |
error(("Argument #1 to `Deformat' must be a string, got %s (%s)."):format(type(text), text), 2) |
elseif type(pattern) ~= "string" then |
error(("Argument #2 to `Deformat' must be a string, got %s (%s)."):format(type(pattern), pattern), 2) |
end |
return get_deformat_function(pattern)(text) |
end |
--[===[@debug@ |
function LibDeformat.Test() |
local function tuple(success, ...) |
if success then |
return true, { n = select('#', ...), ... } |
else |
return false, ... |
end |
end |
local function check(text, pattern, ...) |
local success, results = tuple(pcall(LibDeformat.Deformat, text, pattern)) |
if not success then |
return false, results |
end |
if select('#', ...) ~= results.n then |
return false, ("Differing number of return values. Expected: %d. Actual: %d."):format(select('#', ...), results.n) |
end |
for i = 1, results.n do |
local expected = select(i, ...) |
local actual = results[i] |
if type(expected) ~= type(actual) then |
return false, ("Return #%d differs by type. Expected: %s (%s). Actual: %s (%s)"):format(type(expected), expected, type(actual), actual) |
elseif expected ~= actual then |
return false, ("Return #%d differs. Expected: %s. Actual: %s"):format(expected, actual) |
end |
end |
return true |
end |
local function test(text, pattern, ...) |
local success, problem = check(text, pattern, ...) |
if not success then |
print(("Problem with (%q, %q): %s"):format(text, pattern, problem or "")) |
end |
end |
test("Hello, friend", "Hello, %s", "friend") |
test("Hello, friend", "Hello, %1$s", "friend") |
test("Cost: $100", "Cost: $%d", 100) |
test("Cost: $100", "Cost: $%1$d", 100) |
test("Alpha, Bravo", "%s, %s", "Alpha", "Bravo") |
test("Alpha, Bravo", "%1$s, %2$s", "Alpha", "Bravo") |
test("Alpha, Bravo", "%2$s, %1$s", "Bravo", "Alpha") |
test("Alpha, Bravo, Charlie, Delta, Echo", "%s, %s, %s, %s, %s", "Alpha", "Bravo", "Charlie", "Delta", "Echo") |
test("Alpha, Bravo, Charlie, Delta, Echo", "%1$s, %2$s, %3$s, %4$s, %5$s", "Alpha", "Bravo", "Charlie", "Delta", "Echo") |
test("Alpha, Bravo, Charlie, Delta, Echo", "%5$s, %4$s, %3$s, %2$s, %1$s", "Echo", "Delta", "Charlie", "Bravo", "Alpha") |
test("Alpha, Bravo, Charlie, Delta, Echo", "%2$s, %3$s, %4$s, %5$s, %1$s", "Echo", "Alpha", "Bravo", "Charlie", "Delta") |
test("Alpha, Bravo, Charlie, Delta, Echo", "%3$s, %4$s, %5$s, %1$s, %2$s", "Delta", "Echo", "Alpha", "Bravo", "Charlie") |
test("Alpha, Bravo, Charlie, Delta", "%s, %s, %s, %s, %s", nil, nil, nil, nil, nil) |
test("Hello, friend", "Cost: $%d", nil) |
print("LibDeformat-3.0: Tests completed.") |
end |
--@end-debug@]===] |
setmetatable(LibDeformat, { __call = function(self, ...) return self.Deformat(...) end }) |
## Interface: 30300 |
## Title: Lib: Deformat-3.0 |
## Notes: A library to convert a post-formatted string back to its original arguments given its format string. |
## Author: ckknight |
## X-eMail: ckknight@gmail.com |
## X-Category: Library |
## X-License: MIT |
## LoadOnDemand: 1 |
## X-Curse-Packaged-Version: v1 |
## X-Curse-Project-Name: LibDeformat-3.0 |
## X-Curse-Project-ID: libdeformat-3-0 |
## X-Curse-Repository-ID: wow/libdeformat-3-0/mainline |
LibStub\LibStub.lua |
lib.xml |
tag v1 |
0cb38e41ea596a557a070cd7b8157e81d3ef2d0b |
Cameron Knight <ckknight@Cameron-Knights-MacBook-Pro.local> |
2010-01-04 14:45:16 -0600 |
Tagging as v1 |
-------------------- |
Cameron Knight: |
- Fix folder name in .pkgmeta |
- Add tests to main LibDeformat-3.0.lua, accessible only in debug mode and LibDeformat.Test() must be called to run the tests. Remove other tests, as they're not really useful anymore. |
- Add some comments and move some code around. |
- change @example to @usage |
- Initial commit |
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info |
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke |
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS! |
local LibStub = _G[LIBSTUB_MAJOR] |
if not LibStub or LibStub.minor < LIBSTUB_MINOR then |
LibStub = LibStub or {libs = {}, minors = {} } |
_G[LIBSTUB_MAJOR] = LibStub |
LibStub.minor = LIBSTUB_MINOR |
function LibStub:NewLibrary(major, minor) |
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)") |
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.") |
local oldminor = self.minors[major] |
if oldminor and oldminor >= minor then return nil end |
self.minors[major], self.libs[major] = minor, self.libs[major] or {} |
return self.libs[major], oldminor |
end |
function LibStub:GetLibrary(major, silent) |
if not self.libs[major] and not silent then |
error(("Cannot find a library instance of %q."):format(tostring(major)), 2) |
end |
return self.libs[major], self.minors[major] |
end |
function LibStub:IterateLibraries() return pairs(self.libs) end |
setmetatable(LibStub, { __call = LibStub.GetLibrary }) |
end |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="LibDeformat-3.0.lua" /> |
</Ui> |