--[[ *** DataStore_Skills *** Written by : Thaoky, EU-Marécages de Zangar July 6th, 2009 --]] if not DataStore then return end local addonName = "DataStore_Skills" _G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0") local addon = _G[addonName] local THIS_ACCOUNT = "Default" local BI = LibStub("LibBabble-Inventory-3.0"):GetLookupTable() local L = LibStub("AceLocale-3.0"):GetLocale("DataStore_Skills") local AddonDB_Defaults = { global = { Characters = { ['*'] = { -- ["Account.Realm.Name"] lastUpdate = nil, Skills = { ['*'] = { -- "Professions", "Secondary Skills", "Class Skills", etc... ['*'] = nil } }, } } } } -- ** Mixins ** local function _GetPrimaryProfessions(character) return character.Skills[L["Professions"]] end -- CoA: a character can know ALL professions at once (no retail 2-primary limit), -- and the two custom professions Woodcutting/Woodworking may sit under a different -- skill-window header. To render "all professions" reliably we don't trust the -- "Professions" category header alone: we collect every known crafting/gathering -- profession by name across all categories. English skill names are the keys; on -- non-English clients these names won't match and the list falls back to whatever -- sits under L["Professions"] via _GetPrimaryProfessions (caller handles that). local PRIMARY_PROFESSION_NAMES = { "Alchemy", "Blacksmithing", "Enchanting", "Engineering", "Inscription", "Jewelcrafting", "Leatherworking", "Tailoring", "Skinning", "Mining", "Herbalism", "Woodcutting", "Woodworking", } -- Returns an ordered array of { name = , rank = , maxRank = } -- for every primary profession the character actually knows. Never returns nil. local function _GetPrimaryProfessionList(character) local result = {} local skills = character.Skills if not skills then return result end for _, profName in ipairs(PRIMARY_PROFESSION_NAMES) do for _, category in pairs(skills) do local skill = category[profName] if skill then local rank, maxRank = strsplit("|", skill) result[#result + 1] = { name = profName, rank = tonumber(rank) or 0, maxRank = tonumber(maxRank) or 0, } break -- found this profession, move to the next name end end end return result end local function _GetSecondaryProfessions(character) return character.Skills[L["Secondary Skills"]] end local function _GetSkillInfo(character, name) for categoryName, category in pairs(character.Skills) do for skillName, skill in pairs(category) do if skillName == name then local skillRank, skillMaxRank = strsplit("|", skill) return tonumber(skillRank) or 0, tonumber(skillMaxRank) or 0 end end end return 0, 0 end local function _GetSkillInfoByCategory(character, category, name) local skillRank, skillMaxRank local skill = character.Skills[category][name] if skill then skillRank, skillMaxRank = strsplit("|", skill) end return tonumber(skillRank) or 0, tonumber(skillMaxRank) or 0 end -- a few shortcuts for commonly used skills local function _GetFirstAidRank(character) return _GetSkillInfoByCategory(character, L["Secondary Skills"], BI["First Aid"]) end local function _GetCookingRank(character) return _GetSkillInfoByCategory(character, L["Secondary Skills"], BI["Cooking"]) end local function _GetFishingRank(character) return _GetSkillInfoByCategory(character, L["Secondary Skills"], BI["Fishing"]) end local function _GetRidingRank(character) return _GetSkillInfoByCategory(character, L["Secondary Skills"], L["Riding"]) end -- CoA (Ascension Vol'jin) adds two custom professions on top of the vanilla 15: -- Woodcutting (gathering, base spell 13977860) and Woodworking (crafting, -- ranks 1005008-1005011). They register as normal skill lines in the in-game -- skill UI under their English names "Woodcutting" / "Woodworking" (confirmed -- against coa-professionmenu, which reads them via GetSkillLineInfo by name). -- ScanSkills() already buckets them by whatever category header they sit under, -- so a name-based lookup across all categories retrieves them regardless of the -- header. On non-CoA realms the skill simply doesn't exist and these return 0, 0. local function _GetWoodcuttingRank(character) return _GetSkillInfo(character, "Woodcutting") end local function _GetWoodworkingRank(character) return _GetSkillInfo(character, "Woodworking") end local PublicMethods = { GetPrimaryProfessions = _GetPrimaryProfessions, GetPrimaryProfessionList = _GetPrimaryProfessionList, GetSecondaryProfessions = _GetSecondaryProfessions, GetSkillInfo = _GetSkillInfo, GetSkillInfoByCategory = _GetSkillInfoByCategory, GetFirstAidRank = _GetFirstAidRank, GetCookingRank = _GetCookingRank, GetFishingRank = _GetFishingRank, GetRidingRank = _GetRidingRank, GetWoodcuttingRank = _GetWoodcuttingRank, GetWoodworkingRank = _GetWoodworkingRank, } function addon:OnInitialize() addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults) DataStore:RegisterModule(addonName, addon, PublicMethods) DataStore:SetCharacterBasedMethod("GetPrimaryProfessions") DataStore:SetCharacterBasedMethod("GetPrimaryProfessionList") DataStore:SetCharacterBasedMethod("GetSecondaryProfessions") DataStore:SetCharacterBasedMethod("GetSkillInfo") DataStore:SetCharacterBasedMethod("GetSkillInfoByCategory") DataStore:SetCharacterBasedMethod("GetFirstAidRank") DataStore:SetCharacterBasedMethod("GetCookingRank") DataStore:SetCharacterBasedMethod("GetFishingRank") DataStore:SetCharacterBasedMethod("GetRidingRank") DataStore:SetCharacterBasedMethod("GetWoodcuttingRank") DataStore:SetCharacterBasedMethod("GetWoodworkingRank") end function addon:OnEnable() -- CoA fix: the old build only scanned in PLAYER_ALIVE while the player was a -- ghost (see below), so a living character that never died was never scanned -- and every profession rank rendered as 0 ("Skills shows no data"). We now scan -- on login and whenever the skill window changes so every character populates. addon:RegisterEvent("PLAYER_ALIVE") addon:RegisterEvent("PLAYER_ENTERING_WORLD", "ScanOnLogin") addon:RegisterEvent("SKILL_LINES_CHANGED", "ScanOnLogin") addon:RegisterEvent("CHAT_MSG_SKILL") end function addon:OnDisable() addon:UnregisterEvent("PLAYER_ALIVE") addon:UnregisterEvent("PLAYER_ENTERING_WORLD") addon:UnregisterEvent("SKILL_LINES_CHANGED") addon:UnregisterEvent("CHAT_MSG_SKILL") end -- *** Scanning functions *** local headersState = {} local headerCount local function SaveHeaders() headerCount = 0 -- use a counter to avoid being bound to header names, which might not be unique. for i = GetNumSkillLines(), 1, -1 do -- 1st pass, expand all categories local _, isHeader, isExpanded = GetSkillLineInfo(i) if isHeader then headerCount = headerCount + 1 if not isExpanded then ExpandSkillHeader(i) headersState[headerCount] = true end end end end local function RestoreHeaders() headerCount = 0 for i = GetNumSkillLines(), 1, -1 do local _, isHeader = GetSkillLineInfo(i) if isHeader then headerCount = headerCount + 1 if headersState[headerCount] then CollapseSkillHeader(i) end end end wipe(headersState) end local function ScanSkills() local skills = addon.ThisCharacter.Skills wipe(skills) SaveHeaders() local category for i = 1, GetNumSkillLines() do local skillName, isHeader, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(i) if isHeader then category = skillName else if category and skillName then skills[category][skillName] = skillRank .. "|" .. skillMaxRank end end end RestoreHeaders() addon.ThisCharacter.lastUpdate = time() end -- *** EVENT HANDLERS *** function addon:PLAYER_ALIVE() -- print("DataStore_Skills.lua") -- DEBUG 2025 07 21 if not UnitIsGhost("player") then return end -- only scan if player released spirit and went to graveyard ScanSkills() end -- CoA fix: scan once shortly after login / when skills change. SKILL_LINES_CHANGED -- can fire many times in a burst (once per skill line during login), so throttle to -- a single deferred scan instead of scanning on every event. local scanScheduled function addon:ScanOnLogin() if scanScheduled then return end scanScheduled = true -- defer so the skill UI / API is fully populated before we read GetNumSkillLines() addon:ScheduleTimer(function() scanScheduled = nil ScanSkills() end, 2) end -- this turns -- "Your skill in %s has increased to %d." -- into -- "Your skill in (.+) has increased to (%d+)." local arg1pattern, arg2pattern if GetLocale() == "deDE" then -- ERR_SKILL_UP_SI = "Eure Fertigkeit '%1$s' hat sich auf %2$d erhöht."; arg1pattern = "'%%1%$s'" arg2pattern = "%%2%$d" else arg1pattern = "%%s" arg2pattern = "%%d" end local skillUpMsg = gsub(ERR_SKILL_UP_SI, arg1pattern, "(.+)") skillUpMsg = gsub(skillUpMsg, arg2pattern, "(%%d+)") function addon:CHAT_MSG_SKILL(self, msg) -- This code is a bit more complex than calling ScanSkills again. -- The purpose is to avoid triggering events when expanding/collapsing headers, as this may result in heavy processing in other addons if not msg then return end local updatedSkill, value = msg:match(skillUpMsg) if updatedSkill and value then for _, category in pairs(addon.ThisCharacter.Skills) do for skillName, skillData in pairs(category) do if skillName == updatedSkill then local _, skillMaxRank = strsplit("|", skillData) category[skillName] = tonumber(value) .. "|" .. skillMaxRank addon.ThisCharacter.lastUpdate = time() end end end end end