From 77e504cfec0741ad3dbb418ca4c7ac60a308f177 Mon Sep 17 00:00:00 2001 From: Glyalith Date: Fri, 7 Aug 2026 15:26:32 -0600 Subject: [PATCH 1/2] fix(bags): track currencies per character instead of per profile Reported by a user wanting one currency on their main and another on the alt that farms it: every character showed the same set, from either the EUI dropdown or Blizzard's currency tab. The bag footer renders from the module's own currencyOrder table rather than Blizzard's tracked set, and that table is profile-level, so it is shared by every character on the profile. The input feeding it is not: a TokenFrame.OnTokenWatchChanged callback syncs Blizzard's currency tab, which IS per-character, into that shared table. A per-character source writing a shared store can only bleed, which is why ticking a currency on one character put it in everyone's bag and made Blizzard's own per-character tab look broken. (EUI never writes that tab; there is no SetCurrencyBackpack-family call in the module. It only ever reads it.) Tracked currencies now live under currencyOrderByChar, keyed by name-realm, which is the same key the profile system already uses for lastSpecByChar. Storage stays inside the Bags profile so it keeps riding the existing plumbing: defaults merge, logout strip, export. A profile exported to someone else simply carries keys their characters do not match, and they seed fresh. Each character seeds once from the legacy shared table, so an upgrading user keeps exactly what they had and only diverges as they change things. The legacy table is deliberately left in place rather than deleted or migrated: characters that have not logged in since the upgrade still need it to seed from, and nothing writes to it any more, so the seed stays stable. Fresh installs are unchanged and still seed from Blizzard's tracked set. The options dropdown goes through the module's accessor rather than reaching for db.profile.currencyOrder, so there is one definition of where this data lives and one place that owns the seeding. --- EllesmereUIBags/EUI_Bags_Options.lua | 18 +++++--- EllesmereUIBags/EllesmereUIBags.lua | 66 ++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/EllesmereUIBags/EUI_Bags_Options.lua b/EllesmereUIBags/EUI_Bags_Options.lua index 1a660d59..76d6d4a8 100644 --- a/EllesmereUIBags/EUI_Bags_Options.lua +++ b/EllesmereUIBags/EUI_Bags_Options.lua @@ -472,20 +472,28 @@ initFrame:SetScript("OnEvent", function(self) local cbDD, cbDDRefresh = EllesmereUI.BuildVisOptsCBDropdown( rightRgn, 210, rightRgn:GetFrameLevel() + 2, currencyItems, + -- Tracked currencies are per character (the module owns + -- the accessor and its first-use seeding; see + -- CurrencyOrder in EllesmereUIBags.lua). Reading + -- db.profile.currencyOrder here would edit the legacy + -- shared table that nothing renders any more. function(cID) - local co = db.profile.currencyOrder + local co = EllesmereUI._BagsCurrencyOrder + and EllesmereUI._BagsCurrencyOrder() return co and co[cID] and true or false end, function(cID, v) - if not db.profile.currencyOrder then db.profile.currencyOrder = {} end + local co = EllesmereUI._BagsCurrencyOrder + and EllesmereUI._BagsCurrencyOrder() + if not co then return end if v then local maxOrder = 0 - for _, ord in pairs(db.profile.currencyOrder) do + for _, ord in pairs(co) do if type(ord) == "number" and ord > maxOrder then maxOrder = ord end end - db.profile.currencyOrder[cID] = maxOrder + 1 + co[cID] = maxOrder + 1 else - db.profile.currencyOrder[cID] = nil + co[cID] = nil end if _G.EUI_Bags and _G.EUI_Bags.RefreshInventory then C_Timer.After(0.1, function() diff --git a/EllesmereUIBags/EllesmereUIBags.lua b/EllesmereUIBags/EllesmereUIBags.lua index 4555be7c..70ce9e4f 100644 --- a/EllesmereUIBags/EllesmereUIBags.lua +++ b/EllesmereUIBags/EllesmereUIBags.lua @@ -83,6 +83,52 @@ local EUI = EllesmereUI local _emptyP = {} local function BP() return (EUI._bagsDB and EUI._bagsDB.profile) or _emptyP end +-- Tracked currencies are PER CHARACTER, unlike every other bag setting. +-- +-- They used to live in one profile-level `currencyOrder` table, which meant +-- ticking a currency on one character put it in every character's bag. Worse, +-- the input feeding that table is Blizzard's own currency tab, which IS +-- per-character (see the TokenFrame.OnTokenWatchChanged sync below): a +-- per-character source writing a shared store can only ever bleed. Reported by +-- a user wanting one currency on their main and another on the alt that farms +-- it, which the shared table made impossible. +-- +-- Storage stays inside the Bags profile, keyed by character, so it still rides +-- the existing profile plumbing (defaults merge, logout strip, export). A +-- profile exported to someone else simply carries keys their characters do not +-- match, and they seed fresh. +local function BagsCharKey() + return (UnitName("player") or "?") .. " - " .. (GetRealmName() or "?") +end + +-- The per-character order table, seeded on this character's first use. +-- Returns nil only when the DB is not up yet (callers already handle that). +local function CurrencyOrder() + local p = BP() + if p == _emptyP then return nil end + local byChar = p.currencyOrderByChar + if type(byChar) ~= "table" then byChar = {}; p.currencyOrderByChar = byChar end + local key = BagsCharKey() + local t = byChar[key] + if type(t) ~= "table" then + t = {} + -- Seed from the legacy shared table so an upgrading user keeps exactly + -- what they had. The legacy table is deliberately NOT deleted: every + -- character seeds from it once, so a character that has not logged in + -- since the upgrade still inherits the old setup rather than an empty + -- bag footer. Nothing writes to it any more, so the seed is stable. + local legacy = p.currencyOrder + if type(legacy) == "table" then + for cID, order in pairs(legacy) do + if type(order) == "number" then t[cID] = order end + end + end + byChar[key] = t + end + return t +end +EUI._BagsCurrencyOrder = CurrencyOrder + -- Resolve the default bag-type view ("all" | "onebag" | "multibag"). Reads the -- new bagDefaultBagType key, falling back to the legacy bagDefaultOneBag boolean -- so existing users keep their OneBag default. (Cross-version profile imports are @@ -1667,7 +1713,7 @@ local function UpdateCurrencyDisplays(footerWidth) -- Build tracked list from internal order table (decoupled from Blizzard) local tracked = {} - local orderDB = BP().currencyOrder + local orderDB = CurrencyOrder() if orderDB and C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo then for cID, order in pairs(orderDB) do if type(order) == "number" then @@ -6656,14 +6702,16 @@ local function StartAddon() end end - -- Seed currencyOrder from Blizzard's tracked currencies on first load + -- Seed this character's tracked currencies from Blizzard's on first load if EllesmereUIDB and C_CurrencyInfo and C_CurrencyInfo.GetBackpackCurrencyInfo then - if not BP().currencyOrder then BP().currencyOrder = {} end - local co = BP().currencyOrder - -- Only seed if our table is empty (first install or fresh profile) + -- Only seed when this character's table is empty: first install, a + -- fresh profile, or a character whose legacy seed was empty too. + local co = CurrencyOrder() local hasAny = false - for _ in pairs(co) do hasAny = true; break end - if not hasAny then + if co then + for _ in pairs(co) do hasAny = true; break end + end + if co and not hasAny then local order = 0 for i = 1, 20 do local info = C_CurrencyInfo.GetBackpackCurrencyInfo(i) @@ -6695,8 +6743,8 @@ local function StartAddon() if EventRegistry and EventRegistry.RegisterCallback then EventRegistry:RegisterCallback("TokenFrame.OnTokenWatchChanged", function() if not EllesmereUIDB then return end - if not BP().currencyOrder then BP().currencyOrder = {} end - local co = BP().currencyOrder + local co = CurrencyOrder() + if not co then return end local blizzSet = ReadBlizzSet() -- Add newly checked currencies for cID in pairs(blizzSet) do From aeb4ba565c998d14ecb05636d0a5760c590ae92a Mon Sep 17 00:00:00 2001 From: Glyalith Date: Fri, 7 Aug 2026 15:38:56 -0600 Subject: [PATCH 2/2] fix(bags): store per-character currencies at the DB root, not in the profile Self-review catch on the previous commit, which put currencyOrderByChar inside the bag profile. Two ways that was the wrong home, both with existing precedent in this very module: ApplyProfileData wipes db.profile wholesale (for k in pairs(profile) do profile[k] = nil end) before copying an imported snapshot, so importing any shared profile would have erased EVERY character's currencies and replaced them with the exporter's keys. Profile exports would also have shipped a roster of the author's character names, realms and per-alt currency lists to anyone importing it. That is exactly the leak PRIVATE_ADDON_KEYS exists to plug for DataBars' gold ledger and QoL's upgrade calculator. The module already keeps its other per-character data at the EllesmereUIDB root (characterGold, bagPinnedItems, bagItemAssignments), so this joins them as bagCurrencyByChar and is cleared alongside them by the per-character reset in the options page. Root storage also removes a failure the profile version had: a spec-driven profile switch could hand a character a freshly created empty table mid-session, with nothing to seed it and a blank footer for the rest of the session. Root data does not move with profiles, so first use happens once per character, ever. The character key is now resolved once and cached. It is session-constant and was being rebuilt, two API calls plus a string, on a render path driven by CURRENCY_DISPLAY_UPDATE. It refuses to cache before UnitName and GetRealmName are both available, so an early call returns nil rather than pinning a stub key for the session. --- EllesmereUIBags/EUI_Bags_Options.lua | 1 + EllesmereUIBags/EllesmereUIBags.lua | 40 ++++++++++++++++++---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/EllesmereUIBags/EUI_Bags_Options.lua b/EllesmereUIBags/EUI_Bags_Options.lua index 76d6d4a8..ff464abf 100644 --- a/EllesmereUIBags/EUI_Bags_Options.lua +++ b/EllesmereUIBags/EUI_Bags_Options.lua @@ -835,6 +835,7 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUIDB.bagItemAssignments = nil EllesmereUIDB.characterGold = nil EllesmereUIDB.warbandGold = nil + EllesmereUIDB.bagCurrencyByChar = nil end EllesmereUI:InvalidatePageCache() end, diff --git a/EllesmereUIBags/EllesmereUIBags.lua b/EllesmereUIBags/EllesmereUIBags.lua index 70ce9e4f..8d3e8308 100644 --- a/EllesmereUIBags/EllesmereUIBags.lua +++ b/EllesmereUIBags/EllesmereUIBags.lua @@ -93,31 +93,43 @@ local function BP() return (EUI._bagsDB and EUI._bagsDB.profile) or _emptyP end -- a user wanting one currency on their main and another on the alt that farms -- it, which the shared table made impossible. -- --- Storage stays inside the Bags profile, keyed by character, so it still rides --- the existing profile plumbing (defaults merge, logout strip, export). A --- profile exported to someone else simply carries keys their characters do not --- match, and they seed fresh. +-- Stored at the EllesmereUIDB ROOT, not in the bag profile, which is where the +-- module already keeps its other per-character data (characterGold, +-- bagPinnedItems, bagItemAssignments). Profile data is the wrong home for it in +-- two ways: ApplyProfileData wipes db.profile wholesale before copying an +-- imported snapshot, so importing any shared profile would erase every +-- character's currencies, and profile exports would ship a roster of the +-- author's character names, realms and per-alt currency lists to whoever +-- imported it -- the leak PRIVATE_ADDON_KEYS exists to plug. +local _bagsCharKey -- session-constant; UpdateCurrencyDisplays is a render path local function BagsCharKey() - return (UnitName("player") or "?") .. " - " .. (GetRealmName() or "?") + if not _bagsCharKey then + local n, r = UnitName("player"), GetRealmName() + if not n or not r then return nil end -- too early; do not cache a stub + _bagsCharKey = n .. " - " .. r + end + return _bagsCharKey end -- The per-character order table, seeded on this character's first use. --- Returns nil only when the DB is not up yet (callers already handle that). +-- Returns nil only when the DB or the player identity is not up yet (callers +-- already handle that). local function CurrencyOrder() - local p = BP() - if p == _emptyP then return nil end - local byChar = p.currencyOrderByChar - if type(byChar) ~= "table" then byChar = {}; p.currencyOrderByChar = byChar end + if not EllesmereUIDB then return nil end local key = BagsCharKey() + if not key then return nil end + local byChar = EllesmereUIDB.bagCurrencyByChar + if type(byChar) ~= "table" then byChar = {}; EllesmereUIDB.bagCurrencyByChar = byChar end local t = byChar[key] if type(t) ~= "table" then t = {} -- Seed from the legacy shared table so an upgrading user keeps exactly -- what they had. The legacy table is deliberately NOT deleted: every - -- character seeds from it once, so a character that has not logged in - -- since the upgrade still inherits the old setup rather than an empty - -- bag footer. Nothing writes to it any more, so the seed is stable. - local legacy = p.currencyOrder + -- character seeds from it once, at ITS first login after the upgrade, + -- so a character that has not logged in yet still inherits the old + -- setup instead of an empty footer. Nothing writes to it any more, so + -- the seed stays stable. A per-character migration is never a one-shot. + local legacy = BP().currencyOrder if type(legacy) == "table" then for cID, order in pairs(legacy) do if type(order) == "number" then t[cID] = order end