coa.17: comprehensive partial-data hardening + DataStore_Characters login scan + Skills strip cap
release / release (push) Successful in 5s

- Hardening sweep across DataStore_* (softened crash-asserts in Talents/Containers/Quests
  to graceful nil) + Altoholic frames (guarded remaining getter results).
- DataStore_Characters: scan on login (was ghost-gated -> name/level/class never populated;
  the core 'no character data' cause).
- Skills tab: cap inline professions at 6 (+N) so the strip stops overflowing into Cooking.
This commit is contained in:
2026-05-29 19:53:03 +02:00
parent b8d619c3bb
commit 0a56cbe560
12 changed files with 68 additions and 50 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
## Author: Thaoky, Telkar-RG ## Author: Thaoky, Telkar-RG
## X-Edited-By: Exiles (Sub-Net) — florian.berthold@sub-net.at ## X-Edited-By: Exiles (Sub-Net) — florian.berthold@sub-net.at
## Version: 3.3.002b-coa.16 ## Version: 3.3.002b-coa.17
## X-Category: Inventory, Tradeskill, Mail ## X-Category: Inventory, Tradeskill, Mail
## X-Localizations: enUS, frFR, zhCN, zhTW, deDE, koKR, esES, esMX, ruRU ## X-Localizations: enUS, frFR, zhCN, zhTW, deDE, koKR, esES, esMX, ruRU
## X-Website: http://wow.curse.com/downloads/wow-addons/details/altoholic.aspx ## X-Website: http://wow.curse.com/downloads/wow-addons/details/altoholic.aspx
+9 -9
View File
@@ -118,15 +118,15 @@ function ns:Update()
_G[entry..i.."BankSlotsNormalText"]:SetText(L["Bank not visited yet"]) _G[entry..i.."BankSlotsNormalText"]:SetText(L["Bank not visited yet"])
else else
_G[entry..i.."BankSlotsNormalText"]:SetText(format("%s/%s|r/%s|r/%s|r/%s|r/%s|r/%s|r/%s |r(%s|r)", _G[entry..i.."BankSlotsNormalText"]:SetText(format("%s/%s|r/%s|r/%s|r/%s|r/%s|r/%s|r/%s |r(%s|r)",
DS:GetContainerSize(character, 100), DS:GetContainerSize(character, 100) or 0, -- CoA: empty/unscanned bank bags return nil size
WHITE .. DS:GetContainerSize(character, 5), WHITE .. (DS:GetContainerSize(character, 5) or 0),
WHITE .. DS:GetContainerSize(character, 6), WHITE .. (DS:GetContainerSize(character, 6) or 0),
WHITE .. DS:GetContainerSize(character, 7), WHITE .. (DS:GetContainerSize(character, 7) or 0),
WHITE .. DS:GetContainerSize(character, 8), WHITE .. (DS:GetContainerSize(character, 8) or 0),
WHITE .. DS:GetContainerSize(character, 9), WHITE .. (DS:GetContainerSize(character, 9) or 0),
WHITE .. DS:GetContainerSize(character, 10), WHITE .. (DS:GetContainerSize(character, 10) or 0),
WHITE .. DS:GetContainerSize(character, 11), WHITE .. (DS:GetContainerSize(character, 11) or 0),
CYAN .. DS:GetNumBankSlots(character))) CYAN .. (DS:GetNumBankSlots(character) or 0)))
end end
elseif (lineType == INFO_TOTAL_LINE) then elseif (lineType == INFO_TOTAL_LINE) then
_G[entry..i.."Collapse"]:Hide() _G[entry..i.."Collapse"]:Hide()
+3 -3
View File
@@ -177,8 +177,8 @@ local function UpdateSpread()
local slotID = bagIndices[line].from - 3 + j local slotID = bagIndices[line].from - 3 + j
local itemID, itemLink, itemCount = DS:GetSlotInfo(container, slotID) local itemID, itemLink, itemCount = DS:GetSlotInfo(container, slotID)
if (slotID <= containerSize) then if (slotID <= (containerSize or 0)) then -- CoA: containerSize nil for unscanned bag on partial-data alt
if itemID then if itemID then
Altoholic:SetItemButtonTexture(itemName, GetItemIcon(itemID)); Altoholic:SetItemButtonTexture(itemName, GetItemIcon(itemID));
@@ -278,7 +278,7 @@ local function UpdateAllInOne()
local container = DS:GetContainer(character, containerID) local container = DS:GetContainer(character, containerID)
local _, _, containerSize = DS:GetContainerInfo(character, containerID) local _, _, containerSize = DS:GetContainerInfo(character, containerID)
for slotID = 1, containerSize do for slotID = 1, (containerSize or 0) do -- CoA: containerSize nil for unscanned bag on partial-data alt
local itemID, itemLink, itemCount = DS:GetSlotInfo(container, slotID) local itemID, itemLink, itemCount = DS:GetSlotInfo(container, slotID)
if itemID then if itemID then
currentSlotIndex = currentSlotIndex + 1 currentSlotIndex = currentSlotIndex + 1
+3 -3
View File
@@ -81,9 +81,9 @@ local SecondaryLevelSort = {-- sort functions for the alts
end end
end, end,
["level"] = function(a, b) ["level"] = function(a, b)
local levelA = select(4, DataStore:GetGuildMemberInfo(a)) local levelA = select(4, DataStore:GetGuildMemberInfo(a)) or 0 -- CoA: nil level on partial guild data crashed table.sort
local levelB = select(4, DataStore:GetGuildMemberInfo(b)) local levelB = select(4, DataStore:GetGuildMemberInfo(b)) or 0
if viewSortOrder then if viewSortOrder then
return levelA < levelB return levelA < levelB
else else
+9 -7
View File
@@ -25,9 +25,9 @@ local PrimaryLevelSort = { -- sort functions for the mains
end end
end, end,
["level"] = function(a, b) ["level"] = function(a, b)
local levelA = select(4, DataStore:GetGuildMemberInfo(a.name)) local levelA = select(4, DataStore:GetGuildMemberInfo(a.name)) or 0 -- CoA: nil level on partial guild data crashed table.sort
local levelB = select(4, DataStore:GetGuildMemberInfo(b.name)) local levelB = select(4, DataStore:GetGuildMemberInfo(b.name)) or 0
if viewSortOrder then if viewSortOrder then
return levelA < levelB return levelA < levelB
else else
@@ -76,9 +76,9 @@ local SecondaryLevelSort = {-- sort functions for the alts
end end
end, end,
["level"] = function(a, b) ["level"] = function(a, b)
local levelA = select(4, DataStore:GetGuildMemberInfo(a)) local levelA = select(4, DataStore:GetGuildMemberInfo(a)) or 0 -- CoA: nil level on partial guild data crashed table.sort
local levelB = select(4, DataStore:GetGuildMemberInfo(b)) local levelB = select(4, DataStore:GetGuildMemberInfo(b)) or 0
if viewSortOrder then if viewSortOrder then
return levelA < levelB return levelA < levelB
else else
@@ -199,6 +199,7 @@ local function DisplayProfessionLink(frameName, member, index)
local icon = addon:TextureToFontstring(addon:GetSpellIcon(tonumber(spellID)), 18, 18) .. " " local icon = addon:TextureToFontstring(addon:GetSpellIcon(tonumber(spellID)), 18, 18) .. " "
if link then if link then
local curRank, maxRank = DataStore:GetProfessionInfo(link) local curRank, maxRank = DataStore:GetProfessionInfo(link)
curRank, maxRank = curRank or 0, maxRank or 0 -- CoA: GetProfessionInfo returns nil if the link doesn't match the trade pattern
local ts = addon.TradeSkills local ts = addon.TradeSkills
text:SetText(icon .. ts:GetColor(curRank) .. curRank .. "/" .. maxRank) text:SetText(icon .. ts:GetColor(curRank) .. curRank .. "/" .. maxRank)
else else
@@ -350,7 +351,8 @@ function ns:OnEnter(self)
if not spellID or not link then return end if not spellID or not link then return end
local curRank, maxRank = DataStore:GetProfessionInfo(link) local curRank, maxRank = DataStore:GetProfessionInfo(link)
curRank, maxRank = curRank or 0, maxRank or 0 -- CoA: nil ranks when link doesn't match trade pattern; guard concat below
AltoTooltip:ClearLines(); AltoTooltip:ClearLines();
AltoTooltip:SetOwner(self, "ANCHOR_RIGHT"); AltoTooltip:SetOwner(self, "ANCHOR_RIGHT");
+2 -1
View File
@@ -118,7 +118,8 @@ function ns:Update()
local professions = Characters:GetField(line, "professions") local professions = Characters:GetField(line, "professions")
local profText = "" local profText = ""
if professions then if professions then
for _, p in ipairs(professions) do for idx, p in ipairs(professions) do
if idx > 6 then profText = profText .. YELLOW .. "+" .. (#professions - 6) .. "|r"; break end -- CoA: cap inline profs so the strip fits its 325px cell
local rank = p.rank or 0 local rank = p.rank or 0
local profIcon = "" local profIcon = ""
if p.spellID then if p.spellID then
+1 -1
View File
@@ -380,7 +380,7 @@ local function GetItemCount(searchedID)
for tabID = 1, 6 do for tabID = 1, 6 do
local tabCount = DataStore:GetGuildBankTabItemCount(guildKey, tabID, searchedID) local tabCount = DataStore:GetGuildBankTabItemCount(guildKey, tabID, searchedID)
if tabCount > 0 then if tabCount > 0 then
table.insert(tabCounters, format("%s: %s", WHITE .. DataStore:GetGuildBankTabName(guildKey, tabID), TEAL..tabCount)) table.insert(tabCounters, format("%s: %s", WHITE .. (DataStore:GetGuildBankTabName(guildKey, tabID) or ""), TEAL..tabCount)) -- CoA: tab name nil on partial guild-bank data
end end
end end
@@ -86,10 +86,15 @@ local function OnPlayerMoney()
addon.ThisCharacter.money = GetMoney(); addon.ThisCharacter.money = GetMoney();
end end
local hasScannedThisSession
local function OnPlayerAlive() local function OnPlayerAlive()
-- print("DataStore_Characters.lua") -- DEBUG 2025 07 21 -- CoA: scan once at login. PLAYER_ALIVE also fires on resurrect / Feign-Death cancel
if not UnitIsGhost("player") then return end -- only scan if player released spirit and went to graveyard -- (unchanged data), so skip those. The previous "only when ghost" gate skipped LOGIN
-- too, so name/level/class/money/XP never populated on a normal login - the root of
-- "no character data". (Same trap as DataStore_Inventory / _Skills; see commit fdcb25a.)
if hasScannedThisSession then return end
hasScannedThisSession = true
local character = addon.ThisCharacter local character = addon.ThisCharacter
character.name = UnitName("player") -- to simplify processing a bit, the name is saved in the table too, in addition to being part of the key character.name = UnitName("player") -- to simplify processing a bit, the name is saved in the table too, in addition to being part of the key
@@ -263,7 +268,7 @@ local function _GetGuildInfo(character)
end end
local function _GetPlayTime(character) local function _GetPlayTime(character)
return character.played return character.played or 0 -- CoA: nil on partial-data alt; callers do arithmetic (AccountSummary)
end end
local function _GetLocation(character) local function _GetLocation(character)
+10 -7
View File
@@ -678,25 +678,28 @@ local BagTypeStrings = {
local function _GetContainerInfo(character, containerID) local function _GetContainerInfo(character, containerID)
local bag = _GetContainer(character, containerID) local bag = _GetContainer(character, containerID)
if type(bag) ~= "table" then return end -- CoA: unscanned bag on partial-data alt; was an index-nil crash
return bag.icon, bag.link, bag.size, bag.freeslots, BagTypeStrings[bag.bagtype] return bag.icon, bag.link, bag.size, bag.freeslots, BagTypeStrings[bag.bagtype]
end end
local function _GetContainerSize(character, containerID) local function _GetContainerSize(character, containerID)
-- containerID can be number or string -- containerID can be number or string
return character.Containers["Bag" .. containerID].size local bag = character.Containers["Bag" .. containerID] -- CoA: nil for unscanned bag on partial-data alt
return bag and bag.size
end end
local function _GetSlotInfo(bag, slotID) local function _GetSlotInfo(bag, slotID)
assert(type(bag) == "table") -- this is the pointer to a bag table, obtained through addon:GetContainer() -- CoA: partial-data alts can have an unscanned/nil bag pointer (GetContainer returns nil
assert(type(slotID) == "number") -- for a "BagN" the Containers module never scanned); return empties instead of asserting.
if type(bag) ~= "table" or type(slotID) ~= "number" then return end
-- return itemID, itemLink, itemCount -- return itemID, itemLink, itemCount
return bag.ids[slotID], bag.links[slotID], bag.counts[slotID] or 1 return bag.ids and bag.ids[slotID], bag.links and bag.links[slotID], (bag.counts and bag.counts[slotID]) or 1
end end
local function _GetContainerCooldownInfo(bag, slotID) local function _GetContainerCooldownInfo(bag, slotID)
assert(type(bag) == "table") -- this is the pointer to a bag table, obtained through addon:GetContainer() -- CoA: partial-data alts can have an unscanned/nil bag pointer; degrade to nil gracefully.
assert(type(slotID) == "number") if type(bag) ~= "table" or type(slotID) ~= "number" or type(bag.cooldowns) ~= "table" then return end
local cd = bag.cooldowns[slotID] local cd = bag.cooldowns[slotID]
if cd then if cd then
@@ -868,7 +871,7 @@ end
local function _GetGuildBankTabItemCount(guild, tabID, searchedID) local function _GetGuildBankTabItemCount(guild, tabID, searchedID)
local count = 0 local count = 0
local container = guild.Tabs[tabID] local container = guild.Tabs[tabID]
if type(container) ~= "table" or type(container.ids) ~= "table" then return count end -- CoA: unscanned guild bank tab; was a pairs(nil) crash on item tooltips
for slotID, id in pairs(container.ids) do for slotID, id in pairs(container.ids) do
if (id == searchedID) then if (id == searchedID) then
count = count + (container.counts[slotID] or 1) count = count + (container.counts[slotID] or 1)
+4 -1
View File
@@ -625,6 +625,9 @@ local function _GetProfessionInfo(profession)
end end
local function _GetNumCraftLines(profession) local function _GetNumCraftLines(profession)
-- CoA: profession is nil for an unscanned/custom profession on a partial-data alt;
-- callers use this as a numeric `for` limit, so return 0 instead of crashing on #nil.Crafts
if type(profession) ~= "table" or type(profession.Crafts) ~= "table" then return 0 end
return #profession.Crafts return #profession.Crafts
end end
@@ -709,7 +712,7 @@ local function _GetNumRecipesByColor(profession)
for i = 1, _GetNumCraftLines(profession) do for i = 1, _GetNumCraftLines(profession) do
local isHeader, color = _GetCraftLineInfo(profession, i) local isHeader, color = _GetCraftLineInfo(profession, i)
if not isHeader then if not isHeader and color and counts[color] then -- CoA: custom-profession craft lines can carry an out-of-range/nil color
counts[color] = counts[color] + 1 counts[color] = counts[color] + 1
end end
end end
+4 -2
View File
@@ -274,8 +274,10 @@ local function _GetQuestLogRewardInfo(character, index, rewardIndex)
end end
local function _GetQuestInfo(link) local function _GetQuestInfo(link)
assert(type(link) == "string") -- CoA: GetQuestLogInfo can hand back a nil link for a partial-data alt; degrade to nil
-- returns instead of asserting (callers already nil-check the returned name/level).
if type(link) ~= "string" then return end
local questID, questLevel = link:match("quest:(%d+):(-?%d+)") local questID, questLevel = link:match("quest:(%d+):(-?%d+)")
local questName = link:match("%[(.+)%]") local questName = link:match("%[(.+)%]")
+13 -11
View File
@@ -234,7 +234,9 @@ local function _GetReferenceTable()
end end
local function _GetClassReference(class) local function _GetClassReference(class)
assert(type(class) == "string") -- 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] return addon.ref.global[class]
end end
@@ -251,27 +253,28 @@ 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 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) local ref = _GetClassReference(class)
if ref.Order then -- if the Order field is not nil, we have data for this class if ref and ref.Order then -- CoA: ref is nil for custom classes; was an unguarded index crash
return true return true
end end
end end
local function _ImportClassReference(class, data) local function _ImportClassReference(class, data)
assert(type(class) == "string") -- CoA: data arrives over Comm/AccountSharing; a peer with no reference for a custom
assert(type(data) == "table") -- 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 addon.ref.global[class] = data
end end
local function _GetClassTrees(class) local function _GetClassTrees(class)
assert(type(class) == "string") -- 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 ref = _GetClassReference(class)
local order = ref.Order local order = ref and ref.Order
if order then if order then
return order:gmatch("([^,]+)") return order:gmatch("([^,]+)")
end end
-- to do, add a return value that does not require validity testing by the caller return function() return nil end -- empty iterator so callers can loop safely
end end
local function _GetTreeInfo(class, tree) local function _GetTreeInfo(class, tree)
@@ -284,8 +287,7 @@ end
local function _GetTreeNameByID(class, id) local function _GetTreeNameByID(class, id)
-- returns the name of tree "id" for a given class -- returns the name of tree "id" for a given class
assert(type(class) == "string") -- CoA: _GetClassTrees now yields an empty iterator for custom classes, so no assert needed
local index = 1 local index = 1
for name in _GetClassTrees(class) do for name in _GetClassTrees(class) do
if index == id then if index == id then