From b3bd7134560afa2bd6d84ef23768622f97635b56 Mon Sep 17 00:00:00 2001 From: Uga Date: Sat, 1 Aug 2026 20:08:51 +0200 Subject: [PATCH 1/5] Don't wipe stored equipment when the item cache is cold ScanInventorySlot() cleared a slot whenever GetInventoryItemLink() returned nil, without distinguishing an empty slot from an item whose link cannot be built yet because it is not in the client cache. On the first login after a patch this erased the character's whole stored inventory, which then got written to SavedVariables on logout, leaving the alt's grid column blank until the next login. Falls back to GetInventoryItemID(), which reads local inventory data and stays valid while the cache is cold. Also runs the delayed rescan on every version, not just retail, so an unresolved link is picked up once the cache warms up. --- DataStore_Inventory/DataStore_Inventory.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/DataStore_Inventory/DataStore_Inventory.lua b/DataStore_Inventory/DataStore_Inventory.lua index 3fd5250..b4459ae 100644 --- a/DataStore_Inventory/DataStore_Inventory.lua +++ b/DataStore_Inventory/DataStore_Inventory.lua @@ -91,13 +91,19 @@ local function ScanInventorySlot(slot) local link = GetInventoryItemLink("player", slot) local currentContent = inventory[slot] - - if link then + + if link then if IsEnchanted(link) then -- if there's an enchant, save the full link inventory[slot] = link else -- .. otherwise, only save the id inventory[slot] = tonumber(link:match("item:(%d+)")) end + elseif GetInventoryItemID("player", slot) then + -- The slot holds an item, but its link cannot be built yet because the item is not in the + -- client cache (typically the first login after a patch, when the cache was invalidated). + -- Save the id rather than wiping a slot that is not actually empty. A later rescan, once + -- the cache is warm, replaces it with the full link if the item is enchanted. + inventory[slot] = GetInventoryItemID("player", slot) else inventory[slot] = nil end @@ -332,10 +338,12 @@ local function OnPlayerAlive() if isRetail then ScanTransmogSets() - - -- Scan again after 5 seconds, no less, to ensure that item info has been properly updated. - C_Timer.After(5, ScanInventory) end + + -- Scan again after 5 seconds, no less, to ensure that item info has been properly updated. + -- Needed on every version, not just retail : the first scan runs early enough that the item + -- cache may still be cold, which is what leaves slots unresolved right after a patch. + C_Timer.After(5, ScanInventory) end local function OnPlayerEquipmentChanged(event, slot) From a2f307c879fa8dd91673b3bf89c2213a9e1b9806 Mon Sep 17 00:00:00 2001 From: Uga Date: Sat, 1 Aug 2026 20:08:51 +0200 Subject: [PATCH 2/5] Compute the item level manually when the API returns zeroes ScanAverageItemLevel() returned as soon as GetAverageItemLevel() existed, whether or not it answered anything usable. On Classic Era the function is there but returns zeroes, so the guard rejected the values, the early return skipped the manual calculation, and averageItemLvl was never set at all - which is why every AiL in the guild pane reads 0.0 while the same equipment shows up fine under Grids. The early return now happens only when the API actually produced an item level, so an unusable answer falls through to the loop over the 18 equipped slots that already exists for versions without the function. Co-Authored-By: Claude Opus 5 --- DataStore_Inventory/DataStore_Inventory.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/DataStore_Inventory/DataStore_Inventory.lua b/DataStore_Inventory/DataStore_Inventory.lua index b4459ae..9302edc 100644 --- a/DataStore_Inventory/DataStore_Inventory.lua +++ b/DataStore_Inventory/DataStore_Inventory.lua @@ -43,18 +43,20 @@ local function ScanAverageItemLevel() char.lastUpdate = time() char.overallAIL = 0 - -- GetAverageItemLevel only exists in retail + -- GetAverageItemLevel does not exist on every version, and where it does it may still return + -- zeroes (Classic Era). Only trust it when it answers with usable values, fall through to the + -- manual calculation otherwise, or the character ends up with no item level at all. if type(GetAverageItemLevel) == "function" then local overallAiL, AiL = GetAverageItemLevel() if overallAiL and AiL and overallAiL > 0 and AiL > 0 then char.overallAIL = overallAiL char.averageItemLvl = AiL + return end - return end - - -- if we get here, GetAverageItemLevel does not exist, we must calculate manually. + + -- if we get here, GetAverageItemLevel is missing or unusable, we must calculate manually. local totalItemLevel = 0 local itemCount = 0 From 5d1ec3141359b99dcca21eb0f929ec4a1d208bc8 Mon Sep 17 00:00:00 2001 From: Uga Date: Sat, 1 Aug 2026 20:08:52 +0200 Subject: [PATCH 3/5] Harden the guild item level broadcast against missing data GetAIL() passed the current character's item level straight to format("%d"), which errors on nil - so a character without a stored item level did not just broadcast a wrong value, it threw out of the login broadcast and no guild mate received anything. The alt branch right below already guarded for this. Three more nil paths on the same road, reachable now that item levels are stored again: GetMemberKey() gave up when GetNameOfMain() returned nil, even for our own character, where RequestGuildMemberEquipment() already falls back to the member name; a member known only through his item level has no Inventory table, which the slot lookup indexed blindly; and an equipment transfer for a member never seen before indexed Members[character] before creating it. Co-Authored-By: Claude Opus 5 --- DataStore_Inventory/API/GuildComm.lua | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/DataStore_Inventory/API/GuildComm.lua b/DataStore_Inventory/API/GuildComm.lua index ed9ab8a..a8bf17a 100644 --- a/DataStore_Inventory/API/GuildComm.lua +++ b/DataStore_Inventory/API/GuildComm.lua @@ -30,12 +30,15 @@ local function GetMemberKey(guild, member) -- Either it's a known alt ==> point to the characters table -- Or it's a guild member ==> point to the guild table local main = DataStore:GetNameOfMain(member) - - if main and main == UnitName("player") then + + -- a member whose main is unknown may still be ourselves, same fallback as RequestGuildMemberEquipment + main = main or member + + if main == UnitName("player") then local key = format("%s.%s.%s", DataStore.ThisAccount, DataStore.ThisRealm, member) local id = DataStore:GetCharacterID(key) - - return DataStore_Inventory_Characters[id] + + return id and DataStore_Inventory_Characters[id] end if not guild or not guild.Members then @@ -51,7 +54,11 @@ local function GetAIL(alts) local character = DataStore:GetCharacter() -- this character local ail = DataStore:GetAverageItemLevel(character) - TableInsert(out, format("%s:%d", UnitName("player"), ail)) + + -- a nil item level would error out of format(), taking the whole broadcast with it + if ail then + TableInsert(out, format("%s:%d", UnitName("player"), ail)) + end if strlen(alts) > 0 then for _, name in pairs( { strsplit("|", alts) }) do -- then all his alts @@ -105,7 +112,7 @@ local function _RequestGuildMemberEquipment(member) if not main then -- player is offline, check if his equipment is in the DB local thisGuild = GetThisGuild() - if thisGuild and thisGuild.Members[member] then -- player found + if thisGuild and thisGuild.Members and thisGuild.Members[member] then -- player found if thisGuild.Members[member].Inventory then -- equipment found AddonFactory:Broadcast("DATASTORE_PLAYER_EQUIPMENT_RECEIVED", player, member) return @@ -133,8 +140,9 @@ end local function _GetGuildMemberInventoryItem(guild, member, slotID) local character = GetMemberKey(guild, member) - - if character then + + -- a member known only through his item level broadcast has no inventory yet + if character and character.Inventory then return character.Inventory[slotID] end end @@ -182,6 +190,8 @@ local commCallbacks = { [MSG_EQUIPMENT_TRANSFER] = function(sender, character, equipment) local thisGuild = GetThisGuild() if thisGuild then + thisGuild.Members = thisGuild.Members or {} + thisGuild.Members[character] = thisGuild.Members[character] or {} thisGuild.Members[character].Inventory = equipment thisGuild.Members[character].lastUpdate = time() AddonFactory:Broadcast("DATASTORE_PLAYER_EQUIPMENT_RECEIVED", sender, character) From c678cb364584260285d0938df8945f4090a51090 Mon Sep 17 00:00:00 2001 From: Uga Date: Sat, 1 Aug 2026 20:08:53 +0200 Subject: [PATCH 4/5] Resolve our own characters in the guild pane without the network GetMemberKey() asked GetNameOfMain() who a guild member belongs to, and only took the local path when the answer was our own name. GetNameOfMain() answers from onlineMembers and the guild alts broadcast, so before that traffic arrives it does not recognize even the character currently logged in. The lookup then fell through to guild.Members[member], which holds nothing for a character that never broadcast anything about itself, and the whole method returned no value - which is what left the AiL column at 0.0 for the logged in character and for an alt in the same guild, while the very same item level was correctly stored and readable through GetAverageItemLevel(). The member name is now matched against our own characters on this realm and account first, and only what does not resolve locally is looked up in the guild table. Reading data we already have no longer depends on a broadcast having arrived, and our own data wins over a possibly stale copy received earlier. Co-Authored-By: Claude Opus 5 --- DataStore_Inventory/API/GuildComm.lua | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/DataStore_Inventory/API/GuildComm.lua b/DataStore_Inventory/API/GuildComm.lua index a8bf17a..4aa4980 100644 --- a/DataStore_Inventory/API/GuildComm.lua +++ b/DataStore_Inventory/API/GuildComm.lua @@ -26,25 +26,21 @@ local function GetThisGuild() end local function GetMemberKey(guild, member) - -- returns the appropriate key to address a guild member. - -- Either it's a known alt ==> point to the characters table + -- returns the appropriate key to address a guild member. + -- Either it's one of our own characters ==> point to the characters table -- Or it's a guild member ==> point to the guild table - local main = DataStore:GetNameOfMain(member) - - -- a member whose main is unknown may still be ourselves, same fallback as RequestGuildMemberEquipment - main = main or member - if main == UnitName("player") then - local key = format("%s.%s.%s", DataStore.ThisAccount, DataStore.ThisRealm, member) - local id = DataStore:GetCharacterID(key) + -- Resolve against our own characters first: our data is always fresher than anything received + -- over the guild channel. Do not ask GetNameOfMain() who this member belongs to - it answers + -- from the guild alts broadcast, so until that arrives it does not even recognize ourselves, + -- and reading data we already have should not depend on the network at all. + local key = format("%s.%s.%s", DataStore.ThisAccount, DataStore.ThisRealm, member) + local id = DataStore:GetCharacterID(key) + local character = id and DataStore_Inventory_Characters[id] - return id and DataStore_Inventory_Characters[id] - end + if character then return character end - if not guild or not guild.Members then - return nil - end - return guild.Members[member] + return guild and guild.Members and guild.Members[member] end local function GetAIL(alts) From 211b1f47a024b7bc88cd50e775c71d7a285eb102 Mon Sep 17 00:00:00 2001 From: Uga Date: Sat, 1 Aug 2026 20:08:54 +0200 Subject: [PATCH 5/5] Read our own guild data through DataStore's own lookups The previous attempt still resolved our characters by hand: it rebuilt the character key from DataStore.ThisAccount and ThisRealm, asked GetCharacterID() for an id, then indexed DataStore_Inventory_Characters directly. Three assumptions, none of them verified from inside this file, and the AiL column stayed at 0.0. Both getters now go through the same public methods any other consumer uses: DataStore:GetCharacter(name) to tell whether the member is one of ours, then GetInventoryItem() / GetAverageItemLevel() on the returned key. Those are the calls that are known to answer correctly for the logged in character, so the guild pane now reads exactly what the rest of the addon reads. GetMemberKey() is gone, replaced by a small helper that only looks up the guild table. Co-Authored-By: Claude Opus 5 --- DataStore_Inventory/API/GuildComm.lua | 43 +++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/DataStore_Inventory/API/GuildComm.lua b/DataStore_Inventory/API/GuildComm.lua index 4aa4980..523b807 100644 --- a/DataStore_Inventory/API/GuildComm.lua +++ b/DataStore_Inventory/API/GuildComm.lua @@ -25,21 +25,8 @@ local function GetThisGuild() return guildID and guilds[guildID] end -local function GetMemberKey(guild, member) - -- returns the appropriate key to address a guild member. - -- Either it's one of our own characters ==> point to the characters table - -- Or it's a guild member ==> point to the guild table - - -- Resolve against our own characters first: our data is always fresher than anything received - -- over the guild channel. Do not ask GetNameOfMain() who this member belongs to - it answers - -- from the guild alts broadcast, so until that arrives it does not even recognize ourselves, - -- and reading data we already have should not depend on the network at all. - local key = format("%s.%s.%s", DataStore.ThisAccount, DataStore.ThisRealm, member) - local id = DataStore:GetCharacterID(key) - local character = id and DataStore_Inventory_Characters[id] - - if character then return character end - +local function GetGuildMemberInfo(guild, member) + -- data received over the guild channel, for a member who broadcast something about himself return guild and guild.Members and guild.Members[member] end @@ -134,21 +121,33 @@ local function _RequestGuildMemberEquipment(member) DataStore:GuildWhisper(commPrefix, main, MSG_EQUIPMENT_REQUEST, member) end +--[[ + A guild member may be one of our own characters: the one currently logged in, or an alt in the + same guild. Those are answered from our own data, which is always there and always fresher than + anything the guild channel may have carried. Everyone else is answered from the guild table. + Note that we resolve the name through DataStore:GetCharacter() rather than asking + GetNameOfMain() who the member belongs to: that one answers from the guild alts broadcast, so + until that arrives it does not even recognize ourselves. +--]] local function _GetGuildMemberInventoryItem(guild, member, slotID) - local character = GetMemberKey(guild, member) + local character = DataStore:GetCharacter(member) -- this realm, this account + if character then + return DataStore:GetInventoryItem(character, slotID) + end -- a member known only through his item level broadcast has no inventory yet - if character and character.Inventory then - return character.Inventory[slotID] - end + local info = GetGuildMemberInfo(guild, member) + return info and info.Inventory and info.Inventory[slotID] end local function _GetGuildMemberAverageItemLevel(guild, member) - local character = GetMemberKey(guild, member) - + local character = DataStore:GetCharacter(member) -- this realm, this account if character then - return character.averageItemLvl + return (DataStore:GetAverageItemLevel(character)) end + + local info = GetGuildMemberInfo(guild, member) + return info and info.averageItemLvl end