From b85eb2ce53e0e9da11e64bde8d8388228d5532b5 Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Mon, 2 Nov 2026 00:00:41 +0000 Subject: [PATCH] Cheaper window open, lock-only button refresh, re-sort only changed groups - UiFrame_OnShow requests a full categorization pass only if the last one was more than 60 seconds ago. - Lock-only cache updates refresh just the affected item buttons (AssignItemToItemButton) instead of running UpdateWindow(). - SortGroups() reuses a group's previous sorted order when none of its items were re-categorized and it holds exactly the same items; any full categorization pass invalidates all sorted orders. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH --- Changelog.md | 8 +++ Components/Inventory.Cache.lua | 7 ++ Components/Inventory.Layout.lua | 116 +++++++++++++++++++++++++++++++- Components/Inventory.Ui.lua | 5 +- 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index c492ea4..73f063f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,13 @@ # Bagshui Changelog +## Unreleased + +### Performance + +- Opening a window re-checks every item's category at most once a minute, so opening and closing the bags or bank repeatedly is cheaper. (Categories based on Outfitter/ItemRack sets, the AtlasLoot wishlist or transmog catch up within a minute.) +- An item locking or unlocking (picked up, or moving between the bags and bank) now only refreshes that item's button instead of redrawing the whole window. +- Only groups whose items changed are re-sorted; the others keep their previous order. + ## 1.6.7 - 2026-11-01 ### Performance diff --git a/Components/Inventory.Cache.lua b/Components/Inventory.Cache.lua index 299d716..cdc1a92 100644 --- a/Components/Inventory.Cache.lua +++ b/Components/Inventory.Cache.lua @@ -77,6 +77,10 @@ Bagshui:AddComponent(function() local shadowId, prevShadowId -- Reset tracking tables. + if not self.lockOnlyChangedItems then + self.lockOnlyChangedItems = {} + end + BsUtil.TableClear(self.lockOnlyChangedItems) BsUtil.TableClear(self.partialStacks) BsUtil.TableClear(self.emptyGenericContainerSlots) @@ -267,6 +271,7 @@ bagInfo.numSlots > 0 and (not bagInfo.name or not bagInfo.type) and not self.forceFullCacheUpdate then item.locked = nowLocked + self.lockOnlyChangedItems[item] = true -- Process item if there are changes. elseif preItemLink ~= nowItemLink -- Item has changed. @@ -555,6 +560,8 @@ item._proposedStockState ~= BS_ITEM_STOCK_STATE.DOWN self.cacheChanged = (majorChanges or cosmeticChanges or minorChanges) -- Lock-only (cosmetic) changes redraw the window but don't need every item re-categorized. self.cacheNeedsCategorize = (majorChanges or minorChanges or itemChanges) + -- Nothing but items locking/unlocking: Update() can refresh just those buttons. + self.cacheOnlyLockChanges = (cosmeticChanges and not self.cacheNeedsCategorize) and true or false -- Make sure we don't do a full cache rebuild again. self.initialInventoryUpdateNeeded = false diff --git a/Components/Inventory.Layout.lua b/Components/Inventory.Layout.lua index d839646..215b29e 100644 --- a/Components/Inventory.Layout.lua +++ b/Components/Inventory.Layout.lua @@ -60,6 +60,9 @@ Bagshui:AddComponent(function() -- Before doing anything else, we need to know if we're online. self:UpdateOnlineStatus() + -- Was a full redraw already requested before the cache update? + local windowUpdateWasNeeded = self.windowUpdateNeeded + -- Always update the cache if we get this far. self:UpdateCache() @@ -70,6 +73,25 @@ Bagshui:AddComponent(function() return end + -- Only items locking/unlocking (picked up, or moving between bags and bank): refresh those + -- buttons so they turn gray and back, instead of redrawing the whole window. + if + self.cacheOnlyLockChanges + and not windowUpdateWasNeeded + and not self.forceResort + and not self.resortNeeded + and not self.lookupTablesStale + and not self.editMode + then + self:RefreshLockChangedItemButtons() + self.windowUpdateNeeded = false + self.windowUpdateBlocked = false + if update_cascadeInventory then + update_cascadeInventory:Update() + end + return + end + -- Nothing changed and nothing is waiting for a redraw -- skip the entire pipeline. -- This is the primary optimization for spurious BAG_UPDATE events. if not self.cacheChanged and not self.windowUpdateNeeded and not self.forceResort and not self.lookupTablesStale then @@ -139,6 +161,21 @@ Bagshui:AddComponent(function() self:Update(cascade) end + --- Re-apply the look of item buttons whose item only locked or unlocked in the last cache update + --- (gray while locked, badges dimmed). Same as what a full redraw does for those buttons. + function Inventory:RefreshLockChangedItemButtons() + local changed = self.lockOnlyChangedItems + if not changed then + return + end + for _, button in ipairs(self.ui.buttons.itemSlots) do + local buttonInfo = button.bagshuiData + if buttonInfo and buttonInfo.item and changed[buttonInfo.item] and button:IsVisible() then + self.ui:AssignItemToItemButton(button, buttonInfo.item, buttonInfo.groupId) + end + end + end + --- Refresh only item slot colors without all the overhead of calculating the window layout. --- High-performance -- much better than `UpdateWindow()`/`ForceUpdateWindow()`, so it should be used for --- changing highlights or opacity (searching, bag bar mouseover, Edit Mode highlighting, etc.). @@ -416,6 +453,9 @@ Bagshui:AddComponent(function() end end + -- Sorted-order cache helpers, defined below SortGroups()'s cache tables. + local MarkGroupForResort, InvalidateAllSortedGroups + --- Assign categories and groups to all items in the inventory cache. --- **Should not be called directly!** Use `Inventory:Update()` to coordinate the process. --- @@ -447,6 +487,9 @@ Bagshui:AddComponent(function() if fullPass then self.categorizeEpoch = (self.categorizeEpoch or 0) + 1 self.fullCategorizeNeeded = false + self.lastFullCategorizeTime = _G.GetTime() + -- Every group gets re-sorted after a full pass (see SortGroups()). + InvalidateAllSortedGroups() end -- Perform the actual categorization. @@ -461,6 +504,7 @@ Bagshui:AddComponent(function() -- items are always categorized so they never show up with an unknown category in the -- Bagshui tooltip; unchanged items keep the category from the last pass. if fullPass or self:CategoryCacheStale(self.inventory[bagNum][slotNum]) then + local previousGroupId = self.inventory[bagNum][slotNum].bagshuiGroupId BsCategories:Categorize( self.inventory[bagNum][slotNum], nil, -- Categorization as any alternate character is not currently enabled. @@ -468,6 +512,9 @@ Bagshui:AddComponent(function() self.categoryIdsGroupedBySequence ) self:StoreCategorySignature(self.inventory[bagNum][slotNum]) + -- The group this item left and the group it's in now both need re-sorting. + MarkGroupForResort(previousGroupId) + MarkGroupForResort(self.inventory[bagNum][slotNum].bagshuiGroupId) end -- Fall back to the default group if one wasn't assigned. @@ -500,6 +547,69 @@ Bagshui:AddComponent(function() self.errorText = BsCategories:GetErrors(self.activeCategoryIds) end + -- Sorted-order cache for groups (see SortGroups()). A group's cached order is reused when none of + -- its items were re-categorized since it was sorted (an item that changed in any way the sort can + -- see is re-categorized, see CategoryCacheStale()) and it still holds exactly the same items. + -- `groupSortVersions[groupId]` goes up whenever an item enters or leaves a group or changes; + -- `sortEpoch` goes up after every full categorization pass (rules, settings, sort orders...). + local groupSortVersions = {} + local sortEpoch = 0 + -- { [groupItems table] = { list = {...}, set = { [item] = true }, count, version, epoch } } + local sortedGroupCache = setmetatable({}, { __mode = "k" }) + + MarkGroupForResort = function(groupId) + if groupId and groupId ~= "" then + groupSortVersions[groupId] = (groupSortVersions[groupId] or 0) + 1 + end + end + + InvalidateAllSortedGroups = function() + sortEpoch = sortEpoch + 1 + end + + --- Put a group's items back in their last sorted order if nothing relevant changed. + ---@return boolean restored + local function RestoreSortedGroup(items, groupId) + local cached = sortedGroupCache[items] + local count = table.getn(items) + if + not cached + or cached.epoch ~= sortEpoch + or cached.version ~= (groupSortVersions[groupId] or 0) + or cached.count ~= count + then + return false + end + for i = 1, count do + if not cached.set[items[i]] then + return false + end + end + for i = 1, count do + items[i] = cached.list[i] + end + return true + end + + --- Remember a group's sorted order. + local function StoreSortedGroup(items, groupId) + local cached = sortedGroupCache[items] + if not cached then + cached = { list = {}, set = {} } + sortedGroupCache[items] = cached + end + BsUtil.TableClear(cached.list) + BsUtil.TableClear(cached.set) + local count = table.getn(items) + for i = 1, count do + table.insert(cached.list, items[i]) + cached.set[items[i]] = true + end + cached.count = count + cached.epoch = sortEpoch + cached.version = groupSortVersions[groupId] or 0 + end + -- What each item's last categorization was based on: { [item] = signature }. -- Weak keys so replaced item tables don't linger; kept out of the item tables so nothing extra -- ends up in SavedVariables. @@ -551,7 +661,11 @@ Bagshui:AddComponent(function() -- **Should not be called directly!** Use `Inventory:Update()` to coordinate the process. function Inventory:SortGroups() for groupId, groupConfig in pairs(self.activeGroups) do - BsSortOrders:SortGroup(self.groupItems[groupId], groupConfig, self.settings.defaultSortOrder) + -- Groups whose items didn't change keep their previous sorted order (see RestoreSortedGroup()). + if not RestoreSortedGroup(self.groupItems[groupId], groupId) then + BsSortOrders:SortGroup(self.groupItems[groupId], groupConfig, self.settings.defaultSortOrder) + StoreSortedGroup(self.groupItems[groupId], groupId) + end end end diff --git a/Components/Inventory.Ui.lua b/Components/Inventory.Ui.lua index 6fe596a..02e097a 100644 --- a/Components/Inventory.Ui.lua +++ b/Components/Inventory.Ui.lua @@ -1113,7 +1113,10 @@ Bagshui:AddComponent(function() -- Re-check every item's category when the window opens, so categories based on things -- Bagshui gets no event for (Outfitter/ItemRack sets, AtlasLoot wishlist, transmog) catch up. - self.fullCategorizeNeeded = true + -- At most once a minute, so opening and closing the window repeatedly stays cheap. + if _G.GetTime() - (self.lastFullCategorizeTime or -1000) > 60 then + self.fullCategorizeNeeded = true + end self.online = true self.editMode = false