Files
florian.berthold f4f3de929b
release / release (push) Successful in 5s
coa.28: fix login scan in 9 DataStore modules (the 'data not saved' root cause)
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'.
2026-05-29 23:55:29 +02:00

300 lines
9.6 KiB
Lua

--[[ *** 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 = <skillName>, rank = <n>, maxRank = <n> }
-- 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 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
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