f4f3de929b
release / release (push) Successful in 5s
Quests, Achievements, Reputations, Pets, Stats, Skills, Crafts, Spells, Talents all had the ghost-gated PLAYER_ALIVE scan (DEBUG 2025-07-21 leftover): they only scanned when the player died and released spirit, so their data never populated on a normal login. Now scan once per session at login (addon.coaScannedThisSession guard), matching the earlier DataStore_Characters/_Inventory fix. This is why reputations/recipes/quests/pets/etc were 'not saved'.
429 lines
13 KiB
Lua
429 lines
13 KiB
Lua
--[[ *** DataStore_Talents ***
|
|
Written by : Thaoky, EU-Marécages de Zangar
|
|
June 23rd, 2009
|
|
--]]
|
|
if not DataStore then return end
|
|
|
|
local addonName = "DataStore_Talents"
|
|
|
|
_G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0")
|
|
|
|
local addon = _G[addonName]
|
|
|
|
local THIS_ACCOUNT = "Default"
|
|
|
|
-- TODO:
|
|
-- add support for hunter pets' talent trees
|
|
|
|
local NUM_GLYPH_SLOTS = 6
|
|
|
|
local AddonDB_Defaults = {
|
|
global = {
|
|
Characters = {
|
|
['*'] = { -- ["Account.Realm.Name"]
|
|
lastUpdate = nil,
|
|
ActiveTalents = nil, -- 1 for primary, 2 for secondary
|
|
Class = nil, -- englishClass
|
|
PointsSpent = nil, -- "51,5,15 ... " 3 numbers for primary spec, 3 for secondary, comma separated
|
|
TalentTrees = {
|
|
['*'] = { -- "Fire|2" = Mage Fire tree, secondary
|
|
['*'] = 0
|
|
}
|
|
},
|
|
Glyphs = {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
-- This table saved reference data required to rebuild a talent tree for a class when logged in under another class.
|
|
-- The API does not provide that ability, but saving and reusing is fine
|
|
local ReferenceDB_Defaults = {
|
|
global = {
|
|
['*'] = { -- "englishClass" like "MAGE", "DRUID" etc..
|
|
Order = nil,
|
|
Version = nil, -- build number under which this class ref was saved
|
|
Locale = nil, -- locale under which this class ref was saved
|
|
Trees = {
|
|
['*'] = { -- tree name
|
|
icon = nil,
|
|
background = nil,
|
|
talents = {}, -- name, icon, max rank etc..for talent x in this tree
|
|
prereqs = {} -- prerequisites
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
local TALENT_ICON_PATH = "Interface\\Icons\\"
|
|
local BACKGROUND_PATH = "Interface\\TalentFrame\\"
|
|
|
|
-- *** Utility functions ***
|
|
local function GetVersion()
|
|
local _, version = GetBuildInfo()
|
|
return tonumber(version)
|
|
end
|
|
|
|
-- *** Scanning functions ***
|
|
local LocaleExceptions = {} --- see ScanTalentReference() for an explanation on the purpose of this table
|
|
|
|
if GetLocale() == "enUS" then
|
|
LocaleExceptions["Elemental Combat"] = "Elemental"
|
|
LocaleExceptions["Shadow Magic"] = "Shadow"
|
|
elseif GetLocale() == "frFR" then
|
|
LocaleExceptions["Combat élémentaire"] = "Elémentaire"
|
|
LocaleExceptions["Arcanes"] = "Arcane"
|
|
LocaleExceptions["Magie de l'ombre"] = "Ombre"
|
|
elseif GetLocale() == "deDE" then
|
|
LocaleExceptions["Wiederherstellung"] = "Wiederherst."
|
|
LocaleExceptions["Elementarkampf"] = "Elementar"
|
|
LocaleExceptions["Verstärkung"] = "Verstärk."
|
|
LocaleExceptions["Schattenmagie"] = "Schatten"
|
|
elseif GetLocale() == "zhTW" then
|
|
LocaleExceptions["生存技能"] = "生存"
|
|
LocaleExceptions["暗影魔法"] = "暗影"
|
|
LocaleExceptions["元素戰鬥"] = "元素"
|
|
end
|
|
|
|
local function ScanTalents()
|
|
|
|
local level = UnitLevel("player")
|
|
if not level or level < 10 then return end -- don't scan anything for low level characters
|
|
|
|
local char = addon.ThisCharacter
|
|
local _, englishClass = UnitClass("player")
|
|
|
|
char.ActiveTalents = GetActiveTalentGroup() -- returns 1 or 2
|
|
char.Class = englishClass
|
|
|
|
wipe(char.TalentTrees)
|
|
|
|
local points = {}
|
|
|
|
for specNum = 1, 2 do -- primary and secondary specs
|
|
for tabNum = 1, GetNumTalentTabs() do -- all tabs
|
|
local name, _, pointsSpent = GetTalentTabInfo( tabNum, nil, nil, specNum );
|
|
table.insert(points, pointsSpent)
|
|
|
|
for talentNum = 1, GetNumTalents(tabNum) do -- all talents
|
|
local _, _, _, _, currentRank = GetTalentInfo( tabNum, talentNum, nil, nil, specNum )
|
|
|
|
char.TalentTrees[name .."|" .. specNum][talentNum] = currentRank
|
|
end
|
|
end
|
|
end
|
|
|
|
char.PointsSpent = table.concat(points, ",")
|
|
char.lastUpdate = time()
|
|
end
|
|
|
|
local function ScanTalentReference()
|
|
|
|
local level = UnitLevel("player")
|
|
if not level or level < 10 then return end -- don't scan anything for low level characters
|
|
|
|
local _, class = UnitClass("player") -- we need the englishClass
|
|
local ref = addon.ref.global[class]
|
|
|
|
-- see if we already have data for this version
|
|
if ref.Version then -- if we already have a version ..
|
|
if ref.Version == GetVersion() then -- .. and it's the current build ..
|
|
return -- ..then exit
|
|
end
|
|
end
|
|
|
|
ref.Version = GetVersion()
|
|
ref.Locale = GetLocale()
|
|
local order = {} -- order of the talent tabs
|
|
|
|
-- first talent tree, gather reference + user specific
|
|
for tabNum = 1, GetNumTalentTabs() do
|
|
local talentTabName, _, _, fileName = GetTalentTabInfo( tabNum, nil, nil, 1 );
|
|
order[tabNum] = talentTabName
|
|
|
|
local ti = ref.Trees[talentTabName] -- ti for talent info
|
|
|
|
ti.background = fileName
|
|
|
|
for talentNum = 1, GetNumTalents(tabNum) do
|
|
local nameTalent, iconPath, tier, column, _, maximumRank = GetTalentInfo(tabNum, talentNum, nil, nil, 1 )
|
|
|
|
-- all paths start with this prefix, let's hope blue does not change this :)
|
|
-- saves a lot of memory not to keep the full path for each talent (about 16k in total for all classes)
|
|
iconPath = string.gsub(iconPath, TALENT_ICON_PATH, "")
|
|
|
|
local link = GetTalentLink(tabNum, talentNum)
|
|
local id = tonumber(link:match("talent:(%d+)"))
|
|
|
|
ti.talents[talentNum] = id .. "|" .. nameTalent .. "|" .. iconPath .. "|" .. tier .. "|" .. column .. "|" .. maximumRank
|
|
|
|
prereqTier, prereqColumn = GetTalentPrereqs(tabNum, talentNum) -- talent prerequisites
|
|
if prereqTier and prereqColumn then
|
|
ti.prereqs[talentNum] = prereqTier .. "|" .. prereqColumn
|
|
end
|
|
end
|
|
end
|
|
|
|
-- save the order of talent tabs, this is necessary because the order of talent tabs is not the same as that of spell tabs in all languages/classes
|
|
-- it is fine in enUS, but not in frFR (druid at least did not match)
|
|
ref["Order"] = table.concat(order, ",")
|
|
|
|
for i = 2, 4 do
|
|
local name, icon = GetSpellTabInfo(i) -- skip spell tab 1, it's the general tab
|
|
|
|
-- the icon may be nil on a low level char.
|
|
-- Example : rogue lv 2
|
|
-- GetSpellTabInfo(1) returns the General tab
|
|
-- GetSpellTabInfo(2) returns the Assassination tab
|
|
-- GetSpellTabInfo(3) returns the Combat tab
|
|
-- GetSpellTabInfo(4) returns nil, instead of Subtelty
|
|
if name and icon then
|
|
-- in addition to having different order between spell tabs & talent tabs, the names may not match. Most of the time they do, but in case they don't, use the excpetion table
|
|
name = LocaleExceptions[name] or name
|
|
|
|
|
|
local ti = ref.Trees[name] -- ti for talent info
|
|
ti.icon = string.gsub(icon, TALENT_ICON_PATH, "")
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ScanGlyphs()
|
|
-- GLYPHTYPE_MAJOR = 1;
|
|
-- GLYPHTYPE_MINOR = 2;
|
|
|
|
-- 1
|
|
-- 3 5
|
|
-- 6 4
|
|
-- 2
|
|
|
|
local glyphs = addon.ThisCharacter.Glyphs
|
|
wipe(glyphs)
|
|
|
|
local enabled, glyphType, spell, icon, glyphID
|
|
local link, index
|
|
|
|
for specNum = 1, 2 do
|
|
for i = 1, NUM_GLYPH_SLOTS do
|
|
index = ((specNum - 1) * NUM_GLYPH_SLOTS) + i
|
|
|
|
enabled, glyphType, spell, icon = GetGlyphSocketInfo(i, specNum)
|
|
link = GetGlyphLink(i, specNum)
|
|
if link then
|
|
_, glyphID = link:match("glyph:(%d+):(%d+)")
|
|
end
|
|
|
|
glyphID = glyphID or 0
|
|
glyphType = glyphType or 0
|
|
enabled = enabled or 0
|
|
spell = spell or ""
|
|
icon = icon or ""
|
|
|
|
glyphs[index] = enabled .."|" .. glyphType .. "|" .. spell .. "|" .. icon .. "|" .. glyphID
|
|
end
|
|
end
|
|
|
|
addon.ThisCharacter.lastUpdate = time()
|
|
end
|
|
|
|
|
|
-- ** Mixins **
|
|
local function _GetReferenceTable()
|
|
return addon.ref.global
|
|
end
|
|
|
|
local function _GetClassReference(class)
|
|
-- CoA: custom classes (MONK, BARBARIAN, …) have no vanilla reference table; return nil
|
|
-- instead of asserting/crashing so callers can degrade gracefully.
|
|
if type(class) ~= "string" then return end
|
|
return addon.ref.global[class]
|
|
end
|
|
|
|
local function _GetTreeReference(class, tree)
|
|
-- CoA: custom classes (MONK, etc.) may have no/partial talent reference data, so a
|
|
-- tree lookup can arrive with a nil tree name. Degrade to nil instead of asserting.
|
|
if type(class) ~= "string" or type(tree) ~= "string" then return end
|
|
local c = addon.ref.global[class]
|
|
if not c or not c.Trees then return end
|
|
return c.Trees[tree]
|
|
end
|
|
|
|
local function _IsClassKnown(class)
|
|
class = class or "" -- if by any chance nil is passed, trap it to make sure the function does not fail, but returns nil anyway
|
|
|
|
local ref = _GetClassReference(class)
|
|
if ref and ref.Order then -- CoA: ref is nil for custom classes; was an unguarded index crash
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function _ImportClassReference(class, data)
|
|
-- CoA: data arrives over Comm/AccountSharing; a peer with no reference for a custom
|
|
-- class can send nil, which used to crash the import. Skip silently instead.
|
|
if type(class) ~= "string" or type(data) ~= "table" then return end
|
|
|
|
addon.ref.global[class] = data
|
|
end
|
|
|
|
local function _GetClassTrees(class)
|
|
-- CoA: ref is nil for custom classes; guard so the `for tree in DS:GetClassTrees()`
|
|
-- loops in Talents.lua get an empty iterator instead of an index-nil crash.
|
|
local ref = _GetClassReference(class)
|
|
local order = ref and ref.Order
|
|
if order then
|
|
return order:gmatch("([^,]+)")
|
|
end
|
|
return function() return nil end -- empty iterator so callers can loop safely
|
|
end
|
|
|
|
local function _GetTreeInfo(class, tree)
|
|
local t = _GetTreeReference(class, tree)
|
|
|
|
if t then
|
|
return TALENT_ICON_PATH..t.icon, BACKGROUND_PATH .. t.background
|
|
end
|
|
end
|
|
|
|
local function _GetTreeNameByID(class, id)
|
|
-- returns the name of tree "id" for a given class
|
|
-- CoA: _GetClassTrees now yields an empty iterator for custom classes, so no assert needed
|
|
local index = 1
|
|
for name in _GetClassTrees(class) do
|
|
if index == id then
|
|
return name
|
|
end
|
|
index = index + 1
|
|
end
|
|
end
|
|
|
|
local function _GetTalentLink(id, rank, name)
|
|
return format("|cff4e96f7|Htalent:%s:%s|h[%s]|h|r", id, (rank-1), name)
|
|
end
|
|
|
|
local function _GetNumTalents(class, tree)
|
|
-- returns the number of talents in a given tree
|
|
local t = _GetTreeReference(class, tree)
|
|
|
|
if t then
|
|
return #t.talents
|
|
end
|
|
end
|
|
|
|
local function _GetTalentInfo(class, tree, index)
|
|
local t = _GetTreeReference(class, tree)
|
|
local talentInfo = t.talents[index]
|
|
|
|
if not talentInfo then return end
|
|
|
|
local id, name, icon, tier, column, maximumRank = strsplit("|", talentInfo)
|
|
|
|
return tonumber(id), name, TALENT_ICON_PATH..icon, tonumber(tier), tonumber(column), tonumber(maximumRank)
|
|
end
|
|
|
|
local function _GetTalentRank(character, tree, specNum, index)
|
|
return character.TalentTrees[tree .. "|" .. specNum][index]
|
|
end
|
|
|
|
local function _GetActiveTalents(character)
|
|
return character.ActiveTalents
|
|
end
|
|
|
|
local function _GetNumPointsSpent(character, tree, specNum)
|
|
local index = 1
|
|
for treeName in _GetClassTrees(character.Class) do
|
|
if treeName == tree then
|
|
break
|
|
end
|
|
index = index + 1
|
|
end
|
|
|
|
if index == 4 then return end -- = 4 means tree was not found
|
|
|
|
index = index + ((specNum-1) * 3)
|
|
|
|
return select(index, strsplit(",", character.PointsSpent)) or 0
|
|
end
|
|
|
|
local function _GetTalentPrereqs(class, tree, index)
|
|
local t = _GetTreeReference(class, tree)
|
|
local prereq = t.prereqs[index]
|
|
|
|
if prereq then
|
|
local prereqTier, prereqColumn = strsplit("|", prereq)
|
|
return tonumber(prereqTier), tonumber(prereqColumn)
|
|
end
|
|
end
|
|
|
|
local function _GetGlyphInfo(character, specNum, index)
|
|
index = ((specNum - 1) * NUM_GLYPH_SLOTS) + index -- 4th glyph = 4 for spec 1, 10 for spec 2
|
|
local glyph = character.Glyphs[index]
|
|
if glyph then
|
|
local enabled, glyphType, spell, icon, glyphID = strsplit("|", glyph)
|
|
return tonumber(enabled), tonumber(glyphType), tonumber(spell), icon, tonumber(glyphID)
|
|
end
|
|
end
|
|
|
|
local function _GetGlyphLink(id, spell, glyphID)
|
|
local name = GetSpellInfo(spell)
|
|
return format("|cff66bbff|Hglyph:2%s:%s|h[%s]|h|r", id, glyphID, name)
|
|
end
|
|
|
|
local PublicMethods = {
|
|
GetReferenceTable = _GetReferenceTable,
|
|
GetClassReference = _GetClassReference,
|
|
GetTreeReference = _GetTreeReference,
|
|
IsClassKnown = _IsClassKnown,
|
|
ImportClassReference = _ImportClassReference,
|
|
GetClassTrees = _GetClassTrees,
|
|
GetTreeInfo = _GetTreeInfo,
|
|
GetTreeNameByID = _GetTreeNameByID,
|
|
GetTalentLink = _GetTalentLink,
|
|
GetNumTalents = _GetNumTalents,
|
|
GetTalentInfo = _GetTalentInfo,
|
|
GetTalentRank = _GetTalentRank,
|
|
GetActiveTalents = _GetActiveTalents,
|
|
GetNumPointsSpent = _GetNumPointsSpent,
|
|
GetTalentPrereqs = _GetTalentPrereqs,
|
|
GetGlyphInfo = _GetGlyphInfo,
|
|
GetGlyphLink = _GetGlyphLink,
|
|
}
|
|
|
|
function addon:OnInitialize()
|
|
addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults)
|
|
addon.ref = LibStub("AceDB-3.0"):New(addonName .. "RefDB", ReferenceDB_Defaults)
|
|
|
|
DataStore:RegisterModule(addonName, addon, PublicMethods)
|
|
DataStore:SetCharacterBasedMethod("GetTalentRank")
|
|
DataStore:SetCharacterBasedMethod("GetActiveTalents")
|
|
DataStore:SetCharacterBasedMethod("GetNumPointsSpent")
|
|
DataStore:SetCharacterBasedMethod("GetGlyphInfo")
|
|
end
|
|
|
|
function addon:OnEnable()
|
|
addon:RegisterEvent("PLAYER_ALIVE")
|
|
addon:RegisterEvent("PLAYER_TALENT_UPDATE", ScanTalents)
|
|
addon:RegisterEvent("GLYPH_ADDED", ScanGlyphs)
|
|
addon:RegisterEvent("GLYPH_REMOVED", ScanGlyphs)
|
|
addon:RegisterEvent("GLYPH_UPDATED", ScanGlyphs)
|
|
end
|
|
|
|
function addon:OnDisable()
|
|
addon:UnregisterEvent("PLAYER_ALIVE")
|
|
addon:UnregisterEvent("PLAYER_TALENT_UPDATE")
|
|
addon:UnregisterEvent("GLYPH_ADDED")
|
|
addon:UnregisterEvent("GLYPH_REMOVED")
|
|
addon:UnregisterEvent("GLYPH_UPDATED")
|
|
end
|
|
|
|
-- *** EVENT HANDLERS ***
|
|
function addon:PLAYER_ALIVE()
|
|
-- print("DataStore_Talents.lua") -- DEBUG 2025 07 21
|
|
if addon.coaScannedThisSession then return end -- CoA: scan once at login (was ghost-gated, so data never saved on a normal login - the cause of "data not saved")
|
|
addon.coaScannedThisSession = true
|
|
|
|
ScanTalents()
|
|
ScanTalentReference()
|
|
ScanGlyphs()
|
|
end
|