From 0a56cbe560250ae3a61a8bafd32ae71f8394b8f1 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Fri, 29 May 2026 19:53:03 +0200 Subject: [PATCH] coa.17: comprehensive partial-data hardening + DataStore_Characters login scan + Skills strip cap - 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. --- Altoholic/Altoholic.toc | 2 +- Altoholic/Frames/BagUsage.lua | 18 +++++++------- Altoholic/Frames/Containers.lua | 6 ++--- Altoholic/Frames/GuildMembers.lua | 6 ++--- Altoholic/Frames/GuildProfessions.lua | 16 +++++++------ Altoholic/Frames/Skills.lua | 3 ++- Altoholic/Tooltip.lua | 2 +- DataStore_Characters/DataStore_Characters.lua | 13 ++++++---- DataStore_Containers/DataStore_Containers.lua | 17 +++++++------ DataStore_Crafts/DataStore_Crafts.lua | 5 +++- DataStore_Quests/DataStore_Quests.lua | 6 +++-- DataStore_Talents/DataStore_Talents.lua | 24 ++++++++++--------- 12 files changed, 68 insertions(+), 50 deletions(-) diff --git a/Altoholic/Altoholic.toc b/Altoholic/Altoholic.toc index 850bbe9..a225962 100644 --- a/Altoholic/Altoholic.toc +++ b/Altoholic/Altoholic.toc @@ -13,7 +13,7 @@ ## Author: Thaoky, Telkar-RG ## 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-Localizations: enUS, frFR, zhCN, zhTW, deDE, koKR, esES, esMX, ruRU ## X-Website: http://wow.curse.com/downloads/wow-addons/details/altoholic.aspx diff --git a/Altoholic/Frames/BagUsage.lua b/Altoholic/Frames/BagUsage.lua index 988a59e..8c29a9b 100644 --- a/Altoholic/Frames/BagUsage.lua +++ b/Altoholic/Frames/BagUsage.lua @@ -118,15 +118,15 @@ function ns:Update() _G[entry..i.."BankSlotsNormalText"]:SetText(L["Bank not visited yet"]) else _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), - WHITE .. DS:GetContainerSize(character, 5), - WHITE .. DS:GetContainerSize(character, 6), - WHITE .. DS:GetContainerSize(character, 7), - WHITE .. DS:GetContainerSize(character, 8), - WHITE .. DS:GetContainerSize(character, 9), - WHITE .. DS:GetContainerSize(character, 10), - WHITE .. DS:GetContainerSize(character, 11), - CYAN .. DS:GetNumBankSlots(character))) + DS:GetContainerSize(character, 100) or 0, -- CoA: empty/unscanned bank bags return nil size + WHITE .. (DS:GetContainerSize(character, 5) or 0), + WHITE .. (DS:GetContainerSize(character, 6) or 0), + WHITE .. (DS:GetContainerSize(character, 7) or 0), + WHITE .. (DS:GetContainerSize(character, 8) or 0), + WHITE .. (DS:GetContainerSize(character, 9) or 0), + WHITE .. (DS:GetContainerSize(character, 10) or 0), + WHITE .. (DS:GetContainerSize(character, 11) or 0), + CYAN .. (DS:GetNumBankSlots(character) or 0))) end elseif (lineType == INFO_TOTAL_LINE) then _G[entry..i.."Collapse"]:Hide() diff --git a/Altoholic/Frames/Containers.lua b/Altoholic/Frames/Containers.lua index f57b3ae..afc46be 100644 --- a/Altoholic/Frames/Containers.lua +++ b/Altoholic/Frames/Containers.lua @@ -177,8 +177,8 @@ local function UpdateSpread() local slotID = bagIndices[line].from - 3 + j 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 Altoholic:SetItemButtonTexture(itemName, GetItemIcon(itemID)); @@ -278,7 +278,7 @@ local function UpdateAllInOne() local container = DS:GetContainer(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) if itemID then currentSlotIndex = currentSlotIndex + 1 diff --git a/Altoholic/Frames/GuildMembers.lua b/Altoholic/Frames/GuildMembers.lua index c98f917..5a49210 100644 --- a/Altoholic/Frames/GuildMembers.lua +++ b/Altoholic/Frames/GuildMembers.lua @@ -81,9 +81,9 @@ local SecondaryLevelSort = {-- sort functions for the alts end end, ["level"] = function(a, b) - local levelA = select(4, DataStore:GetGuildMemberInfo(a)) - local levelB = select(4, DataStore:GetGuildMemberInfo(b)) - + 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)) or 0 + if viewSortOrder then return levelA < levelB else diff --git a/Altoholic/Frames/GuildProfessions.lua b/Altoholic/Frames/GuildProfessions.lua index 5d40473..b98653f 100644 --- a/Altoholic/Frames/GuildProfessions.lua +++ b/Altoholic/Frames/GuildProfessions.lua @@ -25,9 +25,9 @@ local PrimaryLevelSort = { -- sort functions for the mains end end, ["level"] = function(a, b) - local levelA = select(4, DataStore:GetGuildMemberInfo(a.name)) - local levelB = select(4, DataStore:GetGuildMemberInfo(b.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)) or 0 + if viewSortOrder then return levelA < levelB else @@ -76,9 +76,9 @@ local SecondaryLevelSort = {-- sort functions for the alts end end, ["level"] = function(a, b) - local levelA = select(4, DataStore:GetGuildMemberInfo(a)) - local levelB = select(4, DataStore:GetGuildMemberInfo(b)) - + 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)) or 0 + if viewSortOrder then return levelA < levelB else @@ -199,6 +199,7 @@ local function DisplayProfessionLink(frameName, member, index) local icon = addon:TextureToFontstring(addon:GetSpellIcon(tonumber(spellID)), 18, 18) .. " " if link then 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 text:SetText(icon .. ts:GetColor(curRank) .. curRank .. "/" .. maxRank) else @@ -350,7 +351,8 @@ function ns:OnEnter(self) if not spellID or not link then return end 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:SetOwner(self, "ANCHOR_RIGHT"); diff --git a/Altoholic/Frames/Skills.lua b/Altoholic/Frames/Skills.lua index 9b20507..b8c8a24 100644 --- a/Altoholic/Frames/Skills.lua +++ b/Altoholic/Frames/Skills.lua @@ -118,7 +118,8 @@ function ns:Update() local professions = Characters:GetField(line, "professions") local profText = "" 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 profIcon = "" if p.spellID then diff --git a/Altoholic/Tooltip.lua b/Altoholic/Tooltip.lua index 6726406..1b4809e 100644 --- a/Altoholic/Tooltip.lua +++ b/Altoholic/Tooltip.lua @@ -380,7 +380,7 @@ local function GetItemCount(searchedID) for tabID = 1, 6 do local tabCount = DataStore:GetGuildBankTabItemCount(guildKey, tabID, searchedID) 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 diff --git a/DataStore_Characters/DataStore_Characters.lua b/DataStore_Characters/DataStore_Characters.lua index af8ef98..97d4d21 100644 --- a/DataStore_Characters/DataStore_Characters.lua +++ b/DataStore_Characters/DataStore_Characters.lua @@ -86,10 +86,15 @@ local function OnPlayerMoney() addon.ThisCharacter.money = GetMoney(); end +local hasScannedThisSession local function OnPlayerAlive() - -- print("DataStore_Characters.lua") -- DEBUG 2025 07 21 - if not UnitIsGhost("player") then return end -- only scan if player released spirit and went to graveyard - + -- CoA: scan once at login. PLAYER_ALIVE also fires on resurrect / Feign-Death cancel + -- (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 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 local function _GetPlayTime(character) - return character.played + return character.played or 0 -- CoA: nil on partial-data alt; callers do arithmetic (AccountSummary) end local function _GetLocation(character) diff --git a/DataStore_Containers/DataStore_Containers.lua b/DataStore_Containers/DataStore_Containers.lua index 37cd2ad..3888724 100644 --- a/DataStore_Containers/DataStore_Containers.lua +++ b/DataStore_Containers/DataStore_Containers.lua @@ -678,25 +678,28 @@ local BagTypeStrings = { local function _GetContainerInfo(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] end local function _GetContainerSize(character, containerID) -- 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 local function _GetSlotInfo(bag, slotID) - assert(type(bag) == "table") -- this is the pointer to a bag table, obtained through addon:GetContainer() - assert(type(slotID) == "number") + -- CoA: partial-data alts can have an unscanned/nil bag pointer (GetContainer returns nil + -- 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 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 local function _GetContainerCooldownInfo(bag, slotID) - assert(type(bag) == "table") -- this is the pointer to a bag table, obtained through addon:GetContainer() - assert(type(slotID) == "number") + -- CoA: partial-data alts can have an unscanned/nil bag pointer; degrade to nil gracefully. + if type(bag) ~= "table" or type(slotID) ~= "number" or type(bag.cooldowns) ~= "table" then return end local cd = bag.cooldowns[slotID] if cd then @@ -868,7 +871,7 @@ end local function _GetGuildBankTabItemCount(guild, tabID, searchedID) local count = 0 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 if (id == searchedID) then count = count + (container.counts[slotID] or 1) diff --git a/DataStore_Crafts/DataStore_Crafts.lua b/DataStore_Crafts/DataStore_Crafts.lua index ad65af2..6c9d88e 100644 --- a/DataStore_Crafts/DataStore_Crafts.lua +++ b/DataStore_Crafts/DataStore_Crafts.lua @@ -625,6 +625,9 @@ local function _GetProfessionInfo(profession) end 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 end @@ -709,7 +712,7 @@ local function _GetNumRecipesByColor(profession) for i = 1, _GetNumCraftLines(profession) do 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 end end diff --git a/DataStore_Quests/DataStore_Quests.lua b/DataStore_Quests/DataStore_Quests.lua index 73b0cfd..ca68a76 100644 --- a/DataStore_Quests/DataStore_Quests.lua +++ b/DataStore_Quests/DataStore_Quests.lua @@ -274,8 +274,10 @@ local function _GetQuestLogRewardInfo(character, index, rewardIndex) end 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 questName = link:match("%[(.+)%]") diff --git a/DataStore_Talents/DataStore_Talents.lua b/DataStore_Talents/DataStore_Talents.lua index 430231b..f979fb2 100644 --- a/DataStore_Talents/DataStore_Talents.lua +++ b/DataStore_Talents/DataStore_Talents.lua @@ -234,7 +234,9 @@ local function _GetReferenceTable() end 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] 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 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 end end local function _ImportClassReference(class, data) - assert(type(class) == "string") - assert(type(data) == "table") - + -- 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) - 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 order = ref.Order + local order = ref and ref.Order if order then return order:gmatch("([^,]+)") 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 local function _GetTreeInfo(class, tree) @@ -284,8 +287,7 @@ end local function _GetTreeNameByID(class, id) -- 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 for name in _GetClassTrees(class) do if index == id then