Merge pull request #35 from vatichild/feat/sort-engine-performance

Feat/sort engine performance
This commit is contained in:
Vati
2026-01-24 05:08:47 +04:00
committed by GitHub
8 changed files with 546 additions and 180 deletions
+80 -66
View File
@@ -482,20 +482,23 @@ function Utils:DeepCopy(orig)
return copy
end
-- Get class color
-- Class colors (cached at module level to avoid table creation per call)
local CLASS_COLORS = {
WARRIOR = {r = 0.78, g = 0.61, b = 0.43},
PALADIN = {r = 0.96, g = 0.55, b = 0.73},
HUNTER = {r = 0.67, g = 0.83, b = 0.45},
ROGUE = {r = 1.00, g = 0.96, b = 0.41},
PRIEST = {r = 1.00, g = 1.00, b = 1.00},
SHAMAN = {r = 0.00, g = 0.44, b = 0.87},
MAGE = {r = 0.41, g = 0.80, b = 0.94},
WARLOCK = {r = 0.58, g = 0.51, b = 0.79},
DRUID = {r = 1.00, g = 0.49, b = 0.04},
_default = {r = 0.5, g = 0.5, b = 0.5},
}
-- Get class color (uses cached table)
function Utils:GetClassColor(class)
local colors = {
WARRIOR = {r = 0.78, g = 0.61, b = 0.43},
PALADIN = {r = 0.96, g = 0.55, b = 0.73},
HUNTER = {r = 0.67, g = 0.83, b = 0.45},
ROGUE = {r = 1.00, g = 0.96, b = 0.41},
PRIEST = {r = 1.00, g = 1.00, b = 1.00},
SHAMAN = {r = 0.00, g = 0.44, b = 0.87},
MAGE = {r = 0.41, g = 0.80, b = 0.94},
WARLOCK = {r = 0.58, g = 0.51, b = 0.79},
DRUID = {r = 1.00, g = 0.49, b = 0.04},
}
return colors[class] or {r = 0.5, g = 0.5, b = 0.5}
return CLASS_COLORS[class] or CLASS_COLORS._default
end
-- Format time ago
@@ -536,52 +539,65 @@ local function IsGreenColor(r, g, b)
return r < 0.4 and g > 0.7 and b < 0.4
end
-- Patterns that indicate an item has special functionality (not junk)
local SPECIAL_TEXT_PATTERNS = {
-- Use effects
"use:",
"use :",
-- Equip effects
"equip:",
"equip :",
-- Proc effects
"chance on hit:",
"chance on hit :",
"chance to",
"chance on",
-- Stat effects
"increases",
"improves",
"restores",
"regenerate",
"generates",
"absorbs",
"reduces",
"grants",
"gives",
-- Learning
"teaches",
"learn",
-- Special actions
"creates",
"summons",
"teleports",
"opens",
"activates",
-- Resistance/stats
"resistance",
"armor",
"damage",
"healing",
"mana",
"health",
"spirit",
"intellect",
"stamina",
"strength",
"agility",
-- Fast pattern lookup table for special text detection (Lua 5.0 optimized)
-- Uses prefix-based lookup to avoid iterating all patterns
local SPECIAL_TEXT_PREFIXES = {
["use"] = true, -- use:, use :
["equ"] = true, -- equip:, equip :
["cha"] = true, -- chance on hit, chance to
["inc"] = true, -- increases
["imp"] = true, -- improves
["res"] = true, -- restores, resistance
["reg"] = true, -- regenerate
["gen"] = true, -- generates
["abs"] = true, -- absorbs
["red"] = true, -- reduces
["gra"] = true, -- grants
["giv"] = true, -- gives
["tea"] = true, -- teaches
["lea"] = true, -- learn
["cre"] = true, -- creates
["sum"] = true, -- summons
["tel"] = true, -- teleports
["ope"] = true, -- opens
["act"] = true, -- activates
["arm"] = true, -- armor
["dam"] = true, -- damage
["hea"] = true, -- healing, health
["man"] = true, -- mana
["spi"] = true, -- spirit
["int"] = true, -- intellect
["sta"] = true, -- stamina
["str"] = true, -- strength
["agi"] = true, -- agility
}
-- Full patterns for validation after prefix match (only checked if prefix matches)
local SPECIAL_TEXT_PATTERNS = {
"use:", "equip:", "chance on", "chance to",
"increases", "improves", "restores", "regenerate", "generates",
"absorbs", "reduces", "grants", "gives", "teaches", "learn",
"creates", "summons", "teleports", "opens", "activates",
"resistance", "armor", "damage", "healing", "mana", "health",
"spirit", "intellect", "stamina", "strength", "agility",
}
-- Fast check if text contains special patterns (uses prefix lookup first)
local function HasSpecialTextPattern(textLower)
-- Quick prefix check (3 chars) to avoid full pattern scan
local prefix = string.sub(textLower, 1, 3)
if not SPECIAL_TEXT_PREFIXES[prefix] then
return false
end
-- Prefix matched - do full pattern check
for _, pattern in ipairs(SPECIAL_TEXT_PATTERNS) do
if string.find(textLower, pattern) then
return true
end
end
return false
end
-- Check if an item's tooltip contains yellow or green description text
-- This indicates the item has a use effect, equip effect, or special property
-- Returns: hasSpecialText (boolean), textType ("yellow", "green", or nil)
@@ -644,16 +660,14 @@ function Utils:HasSpecialTooltipText(bagID, slotID, itemLink)
-- Check for yellow/gold text (Use:, Equip:, Chance on hit:, etc.)
if IsYellowColor(r, g, b) then
-- Check if it matches any special text pattern
for _, pattern in ipairs(SPECIAL_TEXT_PATTERNS) do
if string.find(textLower, pattern) then
addon:Debug("HasSpecialTooltipText: YELLOW match '%s' in: %s", pattern, text)
-- Cache positive result
if cacheKey then
tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "yellow" }
end
return true, "yellow"
-- Use optimized prefix-based pattern check
if HasSpecialTextPattern(textLower) then
addon:Debug("HasSpecialTooltipText: YELLOW match in: %s", text)
-- Cache positive result
if cacheKey then
tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "yellow" }
end
return true, "yellow"
end
end
+63 -15
View File
@@ -14,8 +14,55 @@ local cacheValid = false
local eventPending = false
local dirtySlots = {} -- Track specific slots that changed: dirtySlots[bagID][slotID] = true
-- Clear the bag cache
--=====================================================
-- Item Data Pool (Baganator-inspired memory optimization)
-- Reuses item data tables instead of creating new ones
--=====================================================
local itemDataPool = {}
local ITEM_DATA_POOL_MAX = 200 -- Limit pool size to prevent unbounded growth
-- Acquire item data table from pool
local function AcquireItemData()
return table.remove(itemDataPool) or {}
end
-- Release item data table back to pool
local function ReleaseItemData(data)
if data and table.getn(itemDataPool) < ITEM_DATA_POOL_MAX then
-- Clear all fields for reuse
data.link = nil
data.texture = nil
data.count = nil
data.quality = nil
data.name = nil
data.iLevel = nil
data.type = nil
data.class = nil
data.subclass = nil
data.equipSlot = nil
data.locked = nil
table.insert(itemDataPool, data)
end
end
-- Release all item data in a bag cache entry
local function ReleaseBagCacheData(bagData)
if bagData and bagData.slots then
for slotID, itemData in pairs(bagData.slots) do
if itemData then
ReleaseItemData(itemData)
end
end
end
end
-- Clear the bag cache (releases pooled data)
function BagScanner:ClearCache()
if bagCache then
for bagID, bagData in pairs(bagCache) do
ReleaseBagCacheData(bagData)
end
end
bagCache = nil
cacheValid = false
dirtySlots = {}
@@ -142,7 +189,7 @@ function BagScanner:ScanBag(bagID)
return bag
end
-- Scan a single slot
-- Scan a single slot (uses item data pooling)
function BagScanner:ScanSlot(bagID, slot)
-- Validate parameters to prevent API errors
if bagID == nil or slot == nil or slot < 1 then
@@ -164,19 +211,20 @@ function BagScanner:ScanSlot(bagID, slot)
name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink)
end
return {
link = itemLink,
texture = texture,
count = itemCount or 1,
quality = quality or itemQuality or 0,
name = name,
iLevel = iLevel,
type = itemType,
class = itemCategory,
subclass = itemSubType,
equipSlot = itemEquipLoc,
locked = locked,
}
-- Use pooled item data table instead of creating new one
local itemData = AcquireItemData()
itemData.link = itemLink
itemData.texture = texture
itemData.count = itemCount or 1
itemData.quality = quality or itemQuality or 0
itemData.name = name
itemData.iLevel = iLevel
itemData.type = itemType
itemData.class = itemCategory
itemData.subclass = itemSubType
itemData.equipSlot = itemEquipLoc
itemData.locked = locked
return itemData
end
-- Save current bags to database
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: Guda
## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW)
## Author: Vati
## Version: 1.6.6
## Version: 1.6.7
## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB
+78 -29
View File
@@ -12,6 +12,11 @@ SortEngine.sortingInProgress = false
-- Performance: Max items to move per cycle
-- Baganator uses 5 for manual transfers, but sorting needs more for smooth operation
local MAX_MOVES_PER_CYCLE = 20
-- Bank uses fewer moves per cycle to avoid lock conflicts (bank ops are slower)
local MAX_BANK_MOVES_PER_CYCLE = 15
-- Current sort context (set by ExecuteSort, used by ApplySort)
local currentSortType = "bags"
-- Transfer status constants (like Baganator's SortStatus)
local TransferStatus = {
@@ -131,10 +136,22 @@ local GEM_PATTERNS = {
--===========================================================================
-- Property cache to prevent race conditions during rapid moves
-- With size limits to prevent unbounded memory growth (Baganator pattern)
local propertyCache = {}
local propertyCacheSize = 0
local PROPERTY_CACHE_MAX = 500 -- Prevent memory leaks from unbounded cache
function SortEngine:ClearCache()
propertyCache = {}
propertyCacheSize = 0
end
-- Get cache statistics for performance monitoring
function SortEngine:GetCacheStats()
return {
size = propertyCacheSize,
maxSize = PROPERTY_CACHE_MAX,
}
end
local function GetItemProperties(bagID, slotID, itemLink)
@@ -148,6 +165,12 @@ local function GetItemProperties(bagID, slotID, itemLink)
return propertyCache[cacheKey]
end
-- Evict cache if it exceeds max size to prevent memory leaks
if propertyCacheSize >= PROPERTY_CACHE_MAX then
propertyCache = {}
propertyCacheSize = 0
end
local props = {
isQuest = false,
isQuestStarter = false,
@@ -231,6 +254,7 @@ local function GetItemProperties(bagID, slotID, itemLink)
end
propertyCache[cacheKey] = props
propertyCacheSize = propertyCacheSize + 1
return props
end
@@ -990,12 +1014,15 @@ local function BuildTargetPositions(bagIDs, itemCount)
})
end
table.sort(sortedBags, function(a, b)
if a.priority ~= b.priority then
return a.priority > b.priority
end
return a.bagID < b.bagID
end)
-- Only sort if we have more than one element
if table.getn(sortedBags) > 1 then
table.sort(sortedBags, function(a, b)
if a.priority ~= b.priority then
return a.priority > b.priority
end
return a.bagID < b.bagID
end)
end
-- Build positions in priority order
for _, bagInfo in ipairs(sortedBags) do
@@ -1034,6 +1061,9 @@ local function ApplySort(bagIDs, items, targetPositions)
local moveToEmpty = {}
local swapOccupied = {}
-- Use bank-specific move limit (bank has 50% more slots)
local maxMoves = (currentSortType == "bank") and MAX_BANK_MOVES_PER_CYCLE or MAX_MOVES_PER_CYCLE
-- Build move queues
for i, item in ipairs(items) do
local target = targetPositions[i]
@@ -1071,7 +1101,7 @@ local function ApplySort(bagIDs, items, targetPositions)
for _, move in ipairs(moveToEmpty) do
-- Limit moves per cycle to prevent slot locking (like Baganator)
if moveCount >= MAX_MOVES_PER_CYCLE then
if moveCount >= maxMoves then
break
end
@@ -1089,7 +1119,7 @@ local function ApplySort(bagIDs, items, targetPositions)
-- Execute swaps with occupied slots (if we haven't hit the limit)
for _, move in ipairs(swapOccupied) do
-- Limit moves per cycle
if moveCount >= MAX_MOVES_PER_CYCLE then
if moveCount >= maxMoves then
break
end
@@ -1106,6 +1136,11 @@ local function ApplySort(bagIDs, items, targetPositions)
end
end
-- Debug: report locked items if any
if lockedCount > 0 then
addon:DebugSort("ApplySort: %d moves completed, %d items were locked", moveCount, lockedCount)
end
return moveCount
end
@@ -1137,12 +1172,15 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
end
end
table.sort(ordered, function(a, b)
if a.priority ~= b.priority then
return a.priority < b.priority -- lowest first
end
return a.bagID > b.bagID -- higher bagID later (treated as further to the right)
end)
-- Only sort if we have more than one element
if table.getn(ordered) > 1 then
table.sort(ordered, function(a, b)
if a.priority ~= b.priority then
return a.priority < b.priority -- lowest first
end
return a.bagID > b.bagID -- higher bagID later (treated as further to the right)
end)
end
-- Collect tail slots from end to start, spilling to previous bags as needed.
local tailSlots = {}
@@ -1160,17 +1198,19 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
-- STABILITY FIX: Sort the collected tail slots to match the ascending scan order.
-- This ensures that identical items don't swap places every pass.
-- Ascending order: Priority DESC, BagID ASC, Slot ASC (matching BuildTargetPositions)
table.sort(tailSlots, function(a, b)
local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0
local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0
if aPrio ~= bPrio then
return aPrio > bPrio
end
if a.bag ~= b.bag then
return a.bag < b.bag
end
return a.slot < b.slot
end)
if table.getn(tailSlots) > 1 then
table.sort(tailSlots, function(a, b)
local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0
local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0
if aPrio ~= bPrio then
return aPrio > bPrio
end
if a.bag ~= b.bag then
return a.bag < b.bag
end
return a.slot < b.slot
end)
end
return tailSlots
end
@@ -1622,6 +1662,9 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
return false, "sorting in progress"
end
-- Set current sort context so ApplySort uses correct move limits
currentSortType = sortType or "bags"
-- Clear property cache at the start of a sort operation
self:ClearCache()
@@ -1630,6 +1673,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
-- Check if already sorted
if analysis.alreadySorted then
currentSortType = "bags" -- Reset to default
return false, "already sorted"
end
@@ -1643,7 +1687,8 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
local passCount = 0
local maxPasses = math.max(analysis.passes, 1) -- Use estimated passes, minimum 1
local safetyLimit = math.max(maxPasses * 3, 10) -- Reasonable upper bound
-- Bank has 50% more slots (240 vs 160), so needs higher safety limit
local safetyLimit = (sortType == "bank") and math.max(maxPasses * 4, 15) or math.max(maxPasses * 3, 10)
local totalMoves = 0
local noProgressPasses = 0
@@ -1671,6 +1716,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
frame:SetScript("OnUpdate", nil)
SortEngine.sortingInProgress = false
SortEngine:UpdateSortButtonState(false)
currentSortType = "bags" -- Reset sort context
-- Clear caches after sorting to ensure fresh detection
if sortType == "bank" then
addon.Modules.BankScanner:ClearCache()
@@ -1696,6 +1742,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
frame:SetScript("OnUpdate", nil)
SortEngine.sortingInProgress = false
SortEngine:UpdateSortButtonState(false)
currentSortType = "bags" -- Reset sort context
-- Clear caches after sorting to ensure fresh detection
if sortType == "bank" then
addon.Modules.BankScanner:ClearCache()
@@ -1728,6 +1775,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
frame:SetScript("OnUpdate", nil)
SortEngine.sortingInProgress = false
SortEngine:UpdateSortButtonState(false)
currentSortType = "bags" -- Reset sort context
-- Clear caches after sorting to ensure fresh detection
if sortType == "bank" then
addon.Modules.BankScanner:ClearCache()
@@ -1749,9 +1797,10 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
sortType, passCount, moveCount, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, remainingRatio * 100)
-- PROGRESSIVE DELAY: Short delay to let server process moves
-- Reduced from 0.9-3.4s to 0.2-0.5s for smoother sorting
local baseDelay = 0.2
local complexityDelay = math.min(currentAnalysis.itemsOutOfPlace * 0.01, 0.3) -- max 0.3 seconds
-- Bank needs longer delays because bank operations take longer to complete
local baseDelay = (sortType == "bank") and 0.35 or 0.2
local maxComplexityDelay = (sortType == "bank") and 0.5 or 0.3
local complexityDelay = math.min(currentAnalysis.itemsOutOfPlace * 0.01, maxComplexityDelay)
local totalDelay = baseDelay + complexityDelay
addon:DebugSort("Waiting %.1f seconds before next pass...", totalDelay)
+126 -3
View File
@@ -182,6 +182,111 @@ function BagFrame:UpdateLockStates()
Guda_UpdateLockStates(bagParents)
end
-- Update a single slot without full frame redraw (used for manual item moves)
-- Returns true if successful, false if full redraw is needed
function BagFrame:UpdateSingleSlot(bagID, slotID)
if not Guda_BagFrame:IsShown() then return false end
if currentViewChar then return false end -- Can't do single-slot for other characters
-- Find the button for this slot in itemButtons
local targetButton = nil
for _, button in ipairs(itemButtons) do
if button.bagID == bagID and button.slotID == slotID then
targetButton = button
break
end
end
if not targetButton then return false end
-- Get fresh item data for this slot
local itemLink = GetContainerItemLink(bagID, slotID)
local itemData = nil
if itemLink then
local texture, itemCount, locked, slotQuality = GetContainerItemInfo(bagID, slotID)
local itemID = nil
local _, _, idStr = string.find(itemLink, "item:(%d+)")
if idStr then itemID = tonumber(idStr) end
if itemID then
local name, link, quality, iLevel, _, itemType, stackCount, subType, _, equipLoc = GetItemInfo(itemID)
itemData = {
link = itemLink,
texture = texture,
count = itemCount or 1,
quality = quality or slotQuality or 0,
name = name,
iLevel = iLevel,
type = itemType,
subclass = subType,
equipLoc = equipLoc,
stackSize = stackCount or 1,
locked = locked,
}
end
end
-- Update the button
local matchesFilter = self:PassesSearchFilter(itemData)
Guda_ItemButton_SetItem(targetButton, bagID, slotID, itemData, false, nil, matchesFilter, false)
return true
end
-- Update changed slots in a bag by comparing with cached data
-- Returns number of slots updated, or -1 if full redraw is needed
function BagFrame:UpdateChangedSlots(bagID)
if not Guda_BagFrame:IsShown() then return -1 end
if currentViewChar then return -1 end
local numSlots = GetContainerNumSlots(bagID)
if not numSlots or numSlots == 0 then return -1 end
local updatedCount = 0
for slotID = 1, numSlots do
-- Find button for this slot
local targetButton = nil
for _, button in ipairs(itemButtons) do
if button.bagID == bagID and button.slotID == slotID then
targetButton = button
break
end
end
if not targetButton then
-- Button not found, need full redraw
return -1
end
-- Compare current item with button's cached data
local currentLink = GetContainerItemLink(bagID, slotID)
local cachedLink = targetButton.itemData and targetButton.itemData.link or nil
-- Check if slot changed (different item or count)
local needsUpdate = false
if currentLink ~= cachedLink then
needsUpdate = true
elseif currentLink then
local _, currentCount = GetContainerItemInfo(bagID, slotID)
local cachedCount = targetButton.itemData and targetButton.itemData.count or 0
if currentCount ~= cachedCount then
needsUpdate = true
end
end
if needsUpdate then
if self:UpdateSingleSlot(bagID, slotID) then
updatedCount = updatedCount + 1
else
return -1 -- Update failed, need full redraw
end
end
end
return updatedCount
end
-- Update bagline layout (hover option)
function BagFrame:UpdateBaglineLayout()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
@@ -2587,12 +2692,30 @@ function BagFrame:Initialize()
if currentViewChar then return end
if not Guda_BagFrame:IsShown() then return end
-- Invalidate only the specific bag that changed (arg1 = bagID)
if arg1 and arg1 >= 0 and arg1 <= 4 then
-- Only handle player bags (0-4)
if not arg1 or arg1 < 0 or arg1 > 4 then return end
-- Check if sorting is in progress - use full redraw with throttle
local isSorting = addon.Modules.SortEngine and addon.Modules.SortEngine.sortingInProgress
if not isSorting then
-- Try incremental update for manual item moves
addon.Modules.BagScanner:InvalidateBag(arg1)
if addon.Modules.ItemDetection then
addon.Modules.ItemDetection:ClearCache()
end
-- Try to update only changed slots in this bag
local result = BagFrame:UpdateChangedSlots(arg1)
if result >= 0 then
-- Success - updated slots without full redraw
return
end
-- Fall through to full redraw if incremental update failed
end
-- Use standard delay, will auto-extend if sorting is in progress
-- Sorting in progress or incremental update failed - use throttled full redraw
addon.Modules.BagScanner:InvalidateBag(arg1)
ScheduleBagFrameUpdate(0.1)
end)
+83 -19
View File
@@ -130,6 +130,60 @@ function BankFrame:UpdateLockStates()
Guda_UpdateLockStates(bankBagParents)
end
-- Update a single slot without full frame redraw (used for manual item moves)
function BankFrame:UpdateSingleSlot(bagID, slotID)
if not Guda_BankFrame:IsShown() then return false end
if currentViewChar then return false end -- Can't do single-slot for other characters
-- Find the button for this slot
local bankBagParent = bankBagParents[bagID]
if not bankBagParent or not bankBagParent.itemButtons then return false end
local targetButton = nil
for button in pairs(bankBagParent.itemButtons) do
if button.bagID == bagID and button.slotID == slotID then
targetButton = button
break
end
end
if not targetButton then return false end
-- Get fresh item data for this slot
local itemLink = GetContainerItemLink(bagID, slotID)
local itemData = nil
if itemLink then
local texture, itemCount, locked, quality = GetContainerItemInfo(bagID, slotID)
local itemID = nil
local _, _, idStr = string.find(itemLink, "item:(%d+)")
if idStr then itemID = tonumber(idStr) end
if itemID then
local name, link, itemQuality, iLevel, _, itemType, stackCount, subType, _, equipLoc = GetItemInfo(itemID)
itemData = {
link = itemLink,
texture = texture,
count = itemCount or 1,
quality = quality or itemQuality or 0, -- Prefer GetContainerItemInfo, fallback to GetItemInfo
name = name,
iLevel = iLevel,
type = itemType,
subclass = subType,
equipLoc = equipLoc,
stackSize = stackCount or 1,
locked = locked,
}
end
end
-- Update the button
local matchesFilter = self:PassesSearchFilter(itemData)
Guda_ItemButton_SetItem(targetButton, bagID, slotID, itemData, true, nil, matchesFilter, isReadOnlyMode)
return true
end
-- Deferred update state for frame budgeting
local bankPendingUpdate = false
local bankUpdateDebounceFrame = nil
@@ -173,9 +227,8 @@ function BankFrame:Update()
-- Check if we have any displayed items
local hasDisplayedItems = false
for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then
local children = { bankBagParent:GetChildren() }
for _, button in ipairs(children) do
if bankBagParent and bankBagParent.itemButtons then
for button in pairs(bankBagParent.itemButtons) do
if button.hasItem and button:IsShown() then
hasDisplayedItems = true
break
@@ -194,9 +247,8 @@ function BankFrame:Update()
-- Mark all existing buttons as not in use (we'll mark active ones during display)
for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then
local buttons = { bankBagParent:GetChildren() }
for _, button in ipairs(buttons) do
if bankBagParent and bankBagParent.itemButtons then
for button in pairs(bankBagParent.itemButtons) do
if button.hasItem ~= nil then
button.inUse = false
end
@@ -274,9 +326,8 @@ function BankFrame:Update()
-- Clean up unused buttons AFTER display is complete (prevents drag/drop issues)
for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then
local buttons = { bankBagParent:GetChildren() }
for _, button in ipairs(buttons) do
if bankBagParent and bankBagParent.itemButtons then
for button in pairs(bankBagParent.itemButtons) do
if button.hasItem ~= nil and not button.inUse then
button:Hide()
button:ClearAllPoints()
@@ -1629,11 +1680,25 @@ function BankFrame:Initialize()
if not addon.Modules.BankScanner:IsBankOpen() then return end
if currentViewChar then return end
-- Check if sorting is in progress - use full redraw with throttle
local isSorting = addon.Modules.SortEngine and addon.Modules.SortEngine.sortingInProgress
if event == "PLAYERBANKSLOTS_CHANGED" and arg1 then
-- Invalidate the entire main bank bag to ensure fresh data
-- (MarkSlotDirty was causing timing issues with item data)
-- arg1 is the slot number (1-28 for main bank)
-- Try single-slot update if not sorting
if not isSorting then
-- Invalidate cache for fresh data
addon.Modules.BankScanner:InvalidateBag(-1)
if addon.Modules.ItemDetection then
addon.Modules.ItemDetection:ClearCache()
end
-- Try single-slot update
if BankFrame:UpdateSingleSlot(-1, arg1) then
return -- Success, no full redraw needed
end
end
-- Fallback: full redraw (sorting or single-slot failed)
addon.Modules.BankScanner:InvalidateBag(-1)
-- Clear ItemDetection cache to ensure fresh detection after item swap
if addon.Modules.ItemDetection then
addon.Modules.ItemDetection:ClearCache()
end
@@ -1927,13 +1992,12 @@ end
-- Highlight all item slots belonging to a specific bank bag by dimming others
function Guda_BankFrame_HighlightBagSlots(bagID)
-- Buttons are parented under per-bag parents, not directly under the item container
-- Use itemButtons tracking instead of GetChildren to avoid allocations
local highlightCount, dimCount = 0, 0
for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then
local children = { bankBagParent:GetChildren() }
for _, button in ipairs(children) do
if bankBagParent and bankBagParent.itemButtons then
for button in pairs(bankBagParent.itemButtons) do
if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then
if button.bagID == bagID then
button:SetAlpha(1.0)
@@ -1957,10 +2021,10 @@ function Guda_BankFrame_ClearHighlightedSlots()
-- Restore alpha to search-filter state (pfUI style). If no search, full opacity.
local searchActive = BankFrame and BankFrame.IsSearchActive and BankFrame:IsSearchActive()
-- Use itemButtons tracking instead of GetChildren to avoid allocations
for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then
local children = { bankBagParent:GetChildren() }
for _, button in ipairs(children) do
if bankBagParent and bankBagParent.itemButtons then
for button in pairs(bankBagParent.itemButtons) do
if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then
if searchActive and BankFrame and BankFrame.PassesSearchFilter then
local matches = BankFrame:PassesSearchFilter(button.itemData)
+61 -19
View File
@@ -135,44 +135,77 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems,
table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData})
end
-- Initialize empty category tables
--=====================================================
-- Category Table Pooling (memory optimization)
-- Reuses category tables instead of creating new ones
--=====================================================
local categoriesCache = nil
local specialItemsCache = nil
-- Clear a table without creating a new one (Lua 5.0 compatible)
local function WipeTable(t)
if not t then return end
for k in pairs(t) do
t[k] = nil
end
end
-- Initialize empty category tables (reuses cached tables)
function Guda_InitCategories()
-- Refresh category list from CategoryManager (enabled categories only for display)
Guda_RefreshCategoryList()
-- Create tables for ALL categories (not just enabled) so items can be categorized
-- Items in disabled categories simply won't be displayed
local categories = {}
-- Reuse or create categories table
if not categoriesCache then
categoriesCache = {}
end
-- Clear existing category arrays (don't recreate the main table)
for cat, items in pairs(categoriesCache) do
WipeTable(items)
end
if addon.Modules.CategoryManager then
-- Get full category order (all categories, not just enabled)
local allCategories = addon.Modules.CategoryManager:GetCategoryOrder()
for _, cat in ipairs(allCategories) do
categories[cat] = {}
if not categoriesCache[cat] then
categoriesCache[cat] = {}
end
end
else
-- Fallback: use the display list
for _, cat in ipairs(Guda_CategoryList) do
categories[cat] = {}
if not categoriesCache[cat] then
categoriesCache[cat] = {}
end
end
end
-- Always ensure Miscellaneous exists as fallback
if not categories["Miscellaneous"] then
categories["Miscellaneous"] = {}
if not categoriesCache["Miscellaneous"] then
categoriesCache["Miscellaneous"] = {}
end
-- Always ensure Keyring exists (handled specially in BagFrame)
if not categories["Keyring"] then
categories["Keyring"] = {}
if not categoriesCache["Keyring"] then
categoriesCache["Keyring"] = {}
end
local specialItems = {
Hearthstone = {},
Mount = {},
Tools = {}
}
return categories, specialItems
-- Reuse or create specialItems table
if not specialItemsCache then
specialItemsCache = {
Hearthstone = {},
Mount = {},
Tools = {}
}
else
WipeTable(specialItemsCache.Hearthstone)
WipeTable(specialItemsCache.Mount)
WipeTable(specialItemsCache.Tools)
end
return categoriesCache, specialItemsCache
end
-- Sort items within a category
@@ -254,17 +287,26 @@ function Guda_GetBagParent(framePrefix, parentsTable, bagID, containerName)
if parentsTable[bagID].SetID then
parentsTable[bagID]:SetID(bagID)
end
-- Track item buttons to avoid GetChildren() table allocation
parentsTable[bagID].itemButtons = {}
end
return parentsTable[bagID]
end
-- Register an item button with its parent for tracking
function Guda_RegisterItemButton(parent, button)
if parent and parent.itemButtons and button then
parent.itemButtons[button] = true
end
end
-- Update lock/desaturation states for a table of parent frames
-- Uses itemButtons tracking to avoid GetChildren() table allocation
function Guda_UpdateLockStates(parentsTable)
if not parentsTable then return end
for _, parent in pairs(parentsTable) do
if parent then
local buttons = { parent:GetChildren() }
for _, button in ipairs(buttons) do
if parent and parent.itemButtons then
for button in pairs(parent.itemButtons) do
if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then
-- GetContainerItemInfo returns: texture, itemCount, locked, quality, readable
-- The 3rd return value is the lock state (boolean or nil)
+54 -28
View File
@@ -23,37 +23,45 @@ local function IsQuestItem(bagID, slotID, isBank, itemData)
end
--=====================================================
-- Junk Icon Helper Functions (reusable for all views)
-- Shows a vendor sell icon on junk items
-- Junk Icon Pool (Baganator-inspired memory optimization)
-- Uses frame pooling to avoid creating new frames per button
--=====================================================
local junkIconPool = {}
-- Create junk icon for a button (parented to UIParent for full opacity)
local function CreateJunkIcon(button)
if button.junkIcon then return button.junkIcon end
-- Get a junk icon from pool or create new one
local function AcquireJunkIcon()
local icon = table.remove(junkIconPool)
if not icon then
icon = CreateFrame("Frame", nil, UIParent)
icon:SetFrameStrata("HIGH")
icon:SetWidth(14)
icon:SetHeight(14)
local junkFrame = CreateFrame("Frame", nil, UIParent)
junkFrame:SetFrameStrata("HIGH")
junkFrame:SetWidth(14)
junkFrame:SetHeight(14)
local texture = junkFrame:CreateTexture(nil, "OVERLAY")
texture:SetAllPoints(junkFrame)
texture:SetTexture("Interface\\GossipFrame\\VendorGossipIcon")
texture:SetTexCoord(0, 1, 0, 1)
junkFrame:Hide()
button.junkIcon = junkFrame
return junkFrame
local texture = icon:CreateTexture(nil, "OVERLAY")
texture:SetAllPoints(icon)
texture:SetTexture("Interface\\GossipFrame\\VendorGossipIcon")
texture:SetTexCoord(0, 1, 0, 1)
icon.texture = texture
end
return icon
end
-- Update junk icon visibility and position
local function UpdateJunkIcon(button, isJunk, iconSize)
-- Create icon if it doesn't exist
if not button.junkIcon then
CreateJunkIcon(button)
-- Release a junk icon back to the pool
local function ReleaseJunkIcon(icon)
if icon then
icon:Hide()
icon:ClearAllPoints()
table.insert(junkIconPool, icon)
end
end
-- Update junk icon visibility and position (uses pooling)
local function UpdateJunkIcon(button, isJunk, iconSize)
if isJunk then
-- Acquire from pool if needed
if not button.junkIcon then
button.junkIcon = AcquireJunkIcon()
end
-- Scale icon size based on button size
local junkIconSize = math.max(10, math.min(14, iconSize * 0.30))
button.junkIcon:SetWidth(junkIconSize)
@@ -63,14 +71,19 @@ local function UpdateJunkIcon(button, isJunk, iconSize)
button.junkIcon:SetAlpha(1.0)
button.junkIcon:Show()
else
button.junkIcon:Hide()
-- Release back to pool when not needed
if button.junkIcon then
ReleaseJunkIcon(button.junkIcon)
button.junkIcon = nil
end
end
end
-- Hide junk icon (for cleanup/reset)
-- Hide junk icon (releases to pool)
local function HideJunkIcon(button)
if button.junkIcon then
button.junkIcon:Hide()
ReleaseJunkIcon(button.junkIcon)
button.junkIcon = nil
end
end
@@ -251,6 +264,11 @@ function Guda_GetItemButton(parent)
buttonPool[nextButtonID] = button
nextButtonID = nextButtonID + 1
-- Register button with parent for tracking (avoids GetChildren() allocation)
if Guda_RegisterItemButton then
Guda_RegisterItemButton(parent, button)
end
return button
end
@@ -337,8 +355,7 @@ function Guda_ItemButton_OnLoad(self)
self.questIcon = iconFrame
end
-- Create junk icon overlay (vendor sell icon in top-left corner)
CreateJunkIcon(self)
-- Note: Junk icon is acquired from pool on-demand in UpdateJunkIcon, not pre-created
-- Ensure the item button sits above its container backdrop and is mouse-enabled
local parent = self:GetParent()
@@ -976,12 +993,21 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
isLocked = locked
end
-- Fall back to itemData.quality if live query returned nil (timing issue on bank open)
if itemQuality == nil and itemData and itemData.quality then
itemQuality = itemData.quality
end
-- Ensure itemData is populated for live items (needed for ItemDetection)
if itemLink then
local itemName, _, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture = GetItemInfo(itemLink)
-- For bank main bag, get quality from GetItemInfo
if self.isBank and bagID == -1 then
itemQuality = itemRarity
-- Fall back to itemData.quality if GetItemInfo returned nil
if itemQuality == nil and itemData and itemData.quality then
itemQuality = itemData.quality
end
end
if not itemData then
-- Create new itemData