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 return copy
end 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) function Utils:GetClassColor(class)
local colors = { return CLASS_COLORS[class] or CLASS_COLORS._default
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}
end end
-- Format time ago -- 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 return r < 0.4 and g > 0.7 and b < 0.4
end end
-- Patterns that indicate an item has special functionality (not junk) -- Fast pattern lookup table for special text detection (Lua 5.0 optimized)
local SPECIAL_TEXT_PATTERNS = { -- Uses prefix-based lookup to avoid iterating all patterns
-- Use effects local SPECIAL_TEXT_PREFIXES = {
"use:", ["use"] = true, -- use:, use :
"use :", ["equ"] = true, -- equip:, equip :
-- Equip effects ["cha"] = true, -- chance on hit, chance to
"equip:", ["inc"] = true, -- increases
"equip :", ["imp"] = true, -- improves
-- Proc effects ["res"] = true, -- restores, resistance
"chance on hit:", ["reg"] = true, -- regenerate
"chance on hit :", ["gen"] = true, -- generates
"chance to", ["abs"] = true, -- absorbs
"chance on", ["red"] = true, -- reduces
-- Stat effects ["gra"] = true, -- grants
"increases", ["giv"] = true, -- gives
"improves", ["tea"] = true, -- teaches
"restores", ["lea"] = true, -- learn
"regenerate", ["cre"] = true, -- creates
"generates", ["sum"] = true, -- summons
"absorbs", ["tel"] = true, -- teleports
"reduces", ["ope"] = true, -- opens
"grants", ["act"] = true, -- activates
"gives", ["arm"] = true, -- armor
-- Learning ["dam"] = true, -- damage
"teaches", ["hea"] = true, -- healing, health
"learn", ["man"] = true, -- mana
-- Special actions ["spi"] = true, -- spirit
"creates", ["int"] = true, -- intellect
"summons", ["sta"] = true, -- stamina
"teleports", ["str"] = true, -- strength
"opens", ["agi"] = true, -- agility
"activates",
-- Resistance/stats
"resistance",
"armor",
"damage",
"healing",
"mana",
"health",
"spirit",
"intellect",
"stamina",
"strength",
"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 -- 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 -- This indicates the item has a use effect, equip effect, or special property
-- Returns: hasSpecialText (boolean), textType ("yellow", "green", or nil) -- 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.) -- Check for yellow/gold text (Use:, Equip:, Chance on hit:, etc.)
if IsYellowColor(r, g, b) then if IsYellowColor(r, g, b) then
-- Check if it matches any special text pattern -- Use optimized prefix-based pattern check
for _, pattern in ipairs(SPECIAL_TEXT_PATTERNS) do if HasSpecialTextPattern(textLower) then
if string.find(textLower, pattern) then addon:Debug("HasSpecialTooltipText: YELLOW match in: %s", text)
addon:Debug("HasSpecialTooltipText: YELLOW match '%s' in: %s", pattern, text) -- Cache positive result
-- Cache positive result if cacheKey then
if cacheKey then tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "yellow" }
tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "yellow" }
end
return true, "yellow"
end end
return true, "yellow"
end end
end end
+63 -15
View File
@@ -14,8 +14,55 @@ local cacheValid = false
local eventPending = false local eventPending = false
local dirtySlots = {} -- Track specific slots that changed: dirtySlots[bagID][slotID] = true 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() function BagScanner:ClearCache()
if bagCache then
for bagID, bagData in pairs(bagCache) do
ReleaseBagCacheData(bagData)
end
end
bagCache = nil bagCache = nil
cacheValid = false cacheValid = false
dirtySlots = {} dirtySlots = {}
@@ -142,7 +189,7 @@ function BagScanner:ScanBag(bagID)
return bag return bag
end end
-- Scan a single slot -- Scan a single slot (uses item data pooling)
function BagScanner:ScanSlot(bagID, slot) function BagScanner:ScanSlot(bagID, slot)
-- Validate parameters to prevent API errors -- Validate parameters to prevent API errors
if bagID == nil or slot == nil or slot < 1 then 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) name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink)
end end
return { -- Use pooled item data table instead of creating new one
link = itemLink, local itemData = AcquireItemData()
texture = texture, itemData.link = itemLink
count = itemCount or 1, itemData.texture = texture
quality = quality or itemQuality or 0, itemData.count = itemCount or 1
name = name, itemData.quality = quality or itemQuality or 0
iLevel = iLevel, itemData.name = name
type = itemType, itemData.iLevel = iLevel
class = itemCategory, itemData.type = itemType
subclass = itemSubType, itemData.class = itemCategory
equipSlot = itemEquipLoc, itemData.subclass = itemSubType
locked = locked, itemData.equipSlot = itemEquipLoc
} itemData.locked = locked
return itemData
end end
-- Save current bags to database -- Save current bags to database
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: Guda ## Title: Guda
## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW) ## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW)
## Author: Vati ## Author: Vati
## Version: 1.6.6 ## Version: 1.6.7
## SavedVariables: Guda_DB ## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB ## SavedVariablesPerCharacter: Guda_CharDB
+78 -29
View File
@@ -12,6 +12,11 @@ SortEngine.sortingInProgress = false
-- Performance: Max items to move per cycle -- Performance: Max items to move per cycle
-- Baganator uses 5 for manual transfers, but sorting needs more for smooth operation -- Baganator uses 5 for manual transfers, but sorting needs more for smooth operation
local MAX_MOVES_PER_CYCLE = 20 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) -- Transfer status constants (like Baganator's SortStatus)
local TransferStatus = { local TransferStatus = {
@@ -131,10 +136,22 @@ local GEM_PATTERNS = {
--=========================================================================== --===========================================================================
-- Property cache to prevent race conditions during rapid moves -- Property cache to prevent race conditions during rapid moves
-- With size limits to prevent unbounded memory growth (Baganator pattern)
local propertyCache = {} local propertyCache = {}
local propertyCacheSize = 0
local PROPERTY_CACHE_MAX = 500 -- Prevent memory leaks from unbounded cache
function SortEngine:ClearCache() function SortEngine:ClearCache()
propertyCache = {} propertyCache = {}
propertyCacheSize = 0
end
-- Get cache statistics for performance monitoring
function SortEngine:GetCacheStats()
return {
size = propertyCacheSize,
maxSize = PROPERTY_CACHE_MAX,
}
end end
local function GetItemProperties(bagID, slotID, itemLink) local function GetItemProperties(bagID, slotID, itemLink)
@@ -148,6 +165,12 @@ local function GetItemProperties(bagID, slotID, itemLink)
return propertyCache[cacheKey] return propertyCache[cacheKey]
end end
-- Evict cache if it exceeds max size to prevent memory leaks
if propertyCacheSize >= PROPERTY_CACHE_MAX then
propertyCache = {}
propertyCacheSize = 0
end
local props = { local props = {
isQuest = false, isQuest = false,
isQuestStarter = false, isQuestStarter = false,
@@ -231,6 +254,7 @@ local function GetItemProperties(bagID, slotID, itemLink)
end end
propertyCache[cacheKey] = props propertyCache[cacheKey] = props
propertyCacheSize = propertyCacheSize + 1
return props return props
end end
@@ -990,12 +1014,15 @@ local function BuildTargetPositions(bagIDs, itemCount)
}) })
end end
table.sort(sortedBags, function(a, b) -- Only sort if we have more than one element
if a.priority ~= b.priority then if table.getn(sortedBags) > 1 then
return a.priority > b.priority table.sort(sortedBags, function(a, b)
end if a.priority ~= b.priority then
return a.bagID < b.bagID return a.priority > b.priority
end) end
return a.bagID < b.bagID
end)
end
-- Build positions in priority order -- Build positions in priority order
for _, bagInfo in ipairs(sortedBags) do for _, bagInfo in ipairs(sortedBags) do
@@ -1034,6 +1061,9 @@ local function ApplySort(bagIDs, items, targetPositions)
local moveToEmpty = {} local moveToEmpty = {}
local swapOccupied = {} 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 -- Build move queues
for i, item in ipairs(items) do for i, item in ipairs(items) do
local target = targetPositions[i] local target = targetPositions[i]
@@ -1071,7 +1101,7 @@ local function ApplySort(bagIDs, items, targetPositions)
for _, move in ipairs(moveToEmpty) do for _, move in ipairs(moveToEmpty) do
-- Limit moves per cycle to prevent slot locking (like Baganator) -- Limit moves per cycle to prevent slot locking (like Baganator)
if moveCount >= MAX_MOVES_PER_CYCLE then if moveCount >= maxMoves then
break break
end end
@@ -1089,7 +1119,7 @@ local function ApplySort(bagIDs, items, targetPositions)
-- Execute swaps with occupied slots (if we haven't hit the limit) -- Execute swaps with occupied slots (if we haven't hit the limit)
for _, move in ipairs(swapOccupied) do for _, move in ipairs(swapOccupied) do
-- Limit moves per cycle -- Limit moves per cycle
if moveCount >= MAX_MOVES_PER_CYCLE then if moveCount >= maxMoves then
break break
end end
@@ -1106,6 +1136,11 @@ local function ApplySort(bagIDs, items, targetPositions)
end end
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 return moveCount
end end
@@ -1137,12 +1172,15 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
end end
end end
table.sort(ordered, function(a, b) -- Only sort if we have more than one element
if a.priority ~= b.priority then if table.getn(ordered) > 1 then
return a.priority < b.priority -- lowest first table.sort(ordered, function(a, b)
end if a.priority ~= b.priority then
return a.bagID > b.bagID -- higher bagID later (treated as further to the right) return a.priority < b.priority -- lowest first
end) 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. -- Collect tail slots from end to start, spilling to previous bags as needed.
local tailSlots = {} local tailSlots = {}
@@ -1160,17 +1198,19 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
-- STABILITY FIX: Sort the collected tail slots to match the ascending scan order. -- STABILITY FIX: Sort the collected tail slots to match the ascending scan order.
-- This ensures that identical items don't swap places every pass. -- This ensures that identical items don't swap places every pass.
-- Ascending order: Priority DESC, BagID ASC, Slot ASC (matching BuildTargetPositions) -- Ascending order: Priority DESC, BagID ASC, Slot ASC (matching BuildTargetPositions)
table.sort(tailSlots, function(a, b) if table.getn(tailSlots) > 1 then
local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0 table.sort(tailSlots, function(a, b)
local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0 local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0
if aPrio ~= bPrio then local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0
return aPrio > bPrio if aPrio ~= bPrio then
end return aPrio > bPrio
if a.bag ~= b.bag then end
return a.bag < b.bag if a.bag ~= b.bag then
end return a.bag < b.bag
return a.slot < b.slot end
end) return a.slot < b.slot
end)
end
return tailSlots return tailSlots
end end
@@ -1622,6 +1662,9 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
return false, "sorting in progress" return false, "sorting in progress"
end 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 -- Clear property cache at the start of a sort operation
self:ClearCache() self:ClearCache()
@@ -1630,6 +1673,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
-- Check if already sorted -- Check if already sorted
if analysis.alreadySorted then if analysis.alreadySorted then
currentSortType = "bags" -- Reset to default
return false, "already sorted" return false, "already sorted"
end end
@@ -1643,7 +1687,8 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
local passCount = 0 local passCount = 0
local maxPasses = math.max(analysis.passes, 1) -- Use estimated passes, minimum 1 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 totalMoves = 0
local noProgressPasses = 0 local noProgressPasses = 0
@@ -1671,6 +1716,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
frame:SetScript("OnUpdate", nil) frame:SetScript("OnUpdate", nil)
SortEngine.sortingInProgress = false SortEngine.sortingInProgress = false
SortEngine:UpdateSortButtonState(false) SortEngine:UpdateSortButtonState(false)
currentSortType = "bags" -- Reset sort context
-- Clear caches after sorting to ensure fresh detection -- Clear caches after sorting to ensure fresh detection
if sortType == "bank" then if sortType == "bank" then
addon.Modules.BankScanner:ClearCache() addon.Modules.BankScanner:ClearCache()
@@ -1696,6 +1742,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
frame:SetScript("OnUpdate", nil) frame:SetScript("OnUpdate", nil)
SortEngine.sortingInProgress = false SortEngine.sortingInProgress = false
SortEngine:UpdateSortButtonState(false) SortEngine:UpdateSortButtonState(false)
currentSortType = "bags" -- Reset sort context
-- Clear caches after sorting to ensure fresh detection -- Clear caches after sorting to ensure fresh detection
if sortType == "bank" then if sortType == "bank" then
addon.Modules.BankScanner:ClearCache() addon.Modules.BankScanner:ClearCache()
@@ -1728,6 +1775,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
frame:SetScript("OnUpdate", nil) frame:SetScript("OnUpdate", nil)
SortEngine.sortingInProgress = false SortEngine.sortingInProgress = false
SortEngine:UpdateSortButtonState(false) SortEngine:UpdateSortButtonState(false)
currentSortType = "bags" -- Reset sort context
-- Clear caches after sorting to ensure fresh detection -- Clear caches after sorting to ensure fresh detection
if sortType == "bank" then if sortType == "bank" then
addon.Modules.BankScanner:ClearCache() addon.Modules.BankScanner:ClearCache()
@@ -1749,9 +1797,10 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort
sortType, passCount, moveCount, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, remainingRatio * 100) sortType, passCount, moveCount, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, remainingRatio * 100)
-- PROGRESSIVE DELAY: Short delay to let server process moves -- PROGRESSIVE DELAY: Short delay to let server process moves
-- Reduced from 0.9-3.4s to 0.2-0.5s for smoother sorting -- Bank needs longer delays because bank operations take longer to complete
local baseDelay = 0.2 local baseDelay = (sortType == "bank") and 0.35 or 0.2
local complexityDelay = math.min(currentAnalysis.itemsOutOfPlace * 0.01, 0.3) -- max 0.3 seconds local maxComplexityDelay = (sortType == "bank") and 0.5 or 0.3
local complexityDelay = math.min(currentAnalysis.itemsOutOfPlace * 0.01, maxComplexityDelay)
local totalDelay = baseDelay + complexityDelay local totalDelay = baseDelay + complexityDelay
addon:DebugSort("Waiting %.1f seconds before next pass...", totalDelay) addon:DebugSort("Waiting %.1f seconds before next pass...", totalDelay)
+126 -3
View File
@@ -182,6 +182,111 @@ function BagFrame:UpdateLockStates()
Guda_UpdateLockStates(bagParents) Guda_UpdateLockStates(bagParents)
end 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) -- Update bagline layout (hover option)
function BagFrame:UpdateBaglineLayout() function BagFrame:UpdateBaglineLayout()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter") local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
@@ -2587,12 +2692,30 @@ function BagFrame:Initialize()
if currentViewChar then return end if currentViewChar then return end
if not Guda_BagFrame:IsShown() then return end if not Guda_BagFrame:IsShown() then return end
-- Invalidate only the specific bag that changed (arg1 = bagID) -- Only handle player bags (0-4)
if arg1 and arg1 >= 0 and arg1 <= 4 then 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) 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 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) ScheduleBagFrameUpdate(0.1)
end) end)
+83 -19
View File
@@ -130,6 +130,60 @@ function BankFrame:UpdateLockStates()
Guda_UpdateLockStates(bankBagParents) Guda_UpdateLockStates(bankBagParents)
end 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 -- Deferred update state for frame budgeting
local bankPendingUpdate = false local bankPendingUpdate = false
local bankUpdateDebounceFrame = nil local bankUpdateDebounceFrame = nil
@@ -173,9 +227,8 @@ function BankFrame:Update()
-- Check if we have any displayed items -- Check if we have any displayed items
local hasDisplayedItems = false local hasDisplayedItems = false
for _, bankBagParent in pairs(bankBagParents) do for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then if bankBagParent and bankBagParent.itemButtons then
local children = { bankBagParent:GetChildren() } for button in pairs(bankBagParent.itemButtons) do
for _, button in ipairs(children) do
if button.hasItem and button:IsShown() then if button.hasItem and button:IsShown() then
hasDisplayedItems = true hasDisplayedItems = true
break break
@@ -194,9 +247,8 @@ function BankFrame:Update()
-- Mark all existing buttons as not in use (we'll mark active ones during display) -- Mark all existing buttons as not in use (we'll mark active ones during display)
for _, bankBagParent in pairs(bankBagParents) do for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then if bankBagParent and bankBagParent.itemButtons then
local buttons = { bankBagParent:GetChildren() } for button in pairs(bankBagParent.itemButtons) do
for _, button in ipairs(buttons) do
if button.hasItem ~= nil then if button.hasItem ~= nil then
button.inUse = false button.inUse = false
end end
@@ -274,9 +326,8 @@ function BankFrame:Update()
-- Clean up unused buttons AFTER display is complete (prevents drag/drop issues) -- Clean up unused buttons AFTER display is complete (prevents drag/drop issues)
for _, bankBagParent in pairs(bankBagParents) do for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then if bankBagParent and bankBagParent.itemButtons then
local buttons = { bankBagParent:GetChildren() } for button in pairs(bankBagParent.itemButtons) do
for _, button in ipairs(buttons) do
if button.hasItem ~= nil and not button.inUse then if button.hasItem ~= nil and not button.inUse then
button:Hide() button:Hide()
button:ClearAllPoints() button:ClearAllPoints()
@@ -1629,11 +1680,25 @@ function BankFrame:Initialize()
if not addon.Modules.BankScanner:IsBankOpen() then return end if not addon.Modules.BankScanner:IsBankOpen() then return end
if currentViewChar 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 if event == "PLAYERBANKSLOTS_CHANGED" and arg1 then
-- Invalidate the entire main bank bag to ensure fresh data -- arg1 is the slot number (1-28 for main bank)
-- (MarkSlotDirty was causing timing issues with item data) -- 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) addon.Modules.BankScanner:InvalidateBag(-1)
-- Clear ItemDetection cache to ensure fresh detection after item swap
if addon.Modules.ItemDetection then if addon.Modules.ItemDetection then
addon.Modules.ItemDetection:ClearCache() addon.Modules.ItemDetection:ClearCache()
end end
@@ -1927,13 +1992,12 @@ end
-- Highlight all item slots belonging to a specific bank bag by dimming others -- Highlight all item slots belonging to a specific bank bag by dimming others
function Guda_BankFrame_HighlightBagSlots(bagID) 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 local highlightCount, dimCount = 0, 0
for _, bankBagParent in pairs(bankBagParents) do for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then if bankBagParent and bankBagParent.itemButtons then
local children = { bankBagParent:GetChildren() } for button in pairs(bankBagParent.itemButtons) do
for _, button in ipairs(children) do
if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then
if button.bagID == bagID then if button.bagID == bagID then
button:SetAlpha(1.0) 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. -- Restore alpha to search-filter state (pfUI style). If no search, full opacity.
local searchActive = BankFrame and BankFrame.IsSearchActive and BankFrame:IsSearchActive() local searchActive = BankFrame and BankFrame.IsSearchActive and BankFrame:IsSearchActive()
-- Use itemButtons tracking instead of GetChildren to avoid allocations
for _, bankBagParent in pairs(bankBagParents) do for _, bankBagParent in pairs(bankBagParents) do
if bankBagParent then if bankBagParent and bankBagParent.itemButtons then
local children = { bankBagParent:GetChildren() } for button in pairs(bankBagParent.itemButtons) do
for _, button in ipairs(children) do
if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then
if searchActive and BankFrame and BankFrame.PassesSearchFilter then if searchActive and BankFrame and BankFrame.PassesSearchFilter then
local matches = BankFrame:PassesSearchFilter(button.itemData) 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}) table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData})
end 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() function Guda_InitCategories()
-- Refresh category list from CategoryManager (enabled categories only for display) -- Refresh category list from CategoryManager (enabled categories only for display)
Guda_RefreshCategoryList() Guda_RefreshCategoryList()
-- Create tables for ALL categories (not just enabled) so items can be categorized -- Reuse or create categories table
-- Items in disabled categories simply won't be displayed if not categoriesCache then
local categories = {} 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 if addon.Modules.CategoryManager then
-- Get full category order (all categories, not just enabled) -- Get full category order (all categories, not just enabled)
local allCategories = addon.Modules.CategoryManager:GetCategoryOrder() local allCategories = addon.Modules.CategoryManager:GetCategoryOrder()
for _, cat in ipairs(allCategories) do for _, cat in ipairs(allCategories) do
categories[cat] = {} if not categoriesCache[cat] then
categoriesCache[cat] = {}
end
end end
else else
-- Fallback: use the display list -- Fallback: use the display list
for _, cat in ipairs(Guda_CategoryList) do for _, cat in ipairs(Guda_CategoryList) do
categories[cat] = {} if not categoriesCache[cat] then
categoriesCache[cat] = {}
end
end end
end end
-- Always ensure Miscellaneous exists as fallback -- Always ensure Miscellaneous exists as fallback
if not categories["Miscellaneous"] then if not categoriesCache["Miscellaneous"] then
categories["Miscellaneous"] = {} categoriesCache["Miscellaneous"] = {}
end end
-- Always ensure Keyring exists (handled specially in BagFrame) -- Always ensure Keyring exists (handled specially in BagFrame)
if not categories["Keyring"] then if not categoriesCache["Keyring"] then
categories["Keyring"] = {} categoriesCache["Keyring"] = {}
end end
local specialItems = { -- Reuse or create specialItems table
Hearthstone = {}, if not specialItemsCache then
Mount = {}, specialItemsCache = {
Tools = {} Hearthstone = {},
} Mount = {},
return categories, specialItems Tools = {}
}
else
WipeTable(specialItemsCache.Hearthstone)
WipeTable(specialItemsCache.Mount)
WipeTable(specialItemsCache.Tools)
end
return categoriesCache, specialItemsCache
end end
-- Sort items within a category -- Sort items within a category
@@ -254,17 +287,26 @@ function Guda_GetBagParent(framePrefix, parentsTable, bagID, containerName)
if parentsTable[bagID].SetID then if parentsTable[bagID].SetID then
parentsTable[bagID]:SetID(bagID) parentsTable[bagID]:SetID(bagID)
end end
-- Track item buttons to avoid GetChildren() table allocation
parentsTable[bagID].itemButtons = {}
end end
return parentsTable[bagID] return parentsTable[bagID]
end 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 -- Update lock/desaturation states for a table of parent frames
-- Uses itemButtons tracking to avoid GetChildren() table allocation
function Guda_UpdateLockStates(parentsTable) function Guda_UpdateLockStates(parentsTable)
if not parentsTable then return end if not parentsTable then return end
for _, parent in pairs(parentsTable) do for _, parent in pairs(parentsTable) do
if parent then if parent and parent.itemButtons then
local buttons = { parent:GetChildren() } for button in pairs(parent.itemButtons) do
for _, button in ipairs(buttons) do
if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then
-- GetContainerItemInfo returns: texture, itemCount, locked, quality, readable -- GetContainerItemInfo returns: texture, itemCount, locked, quality, readable
-- The 3rd return value is the lock state (boolean or nil) -- 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 end
--===================================================== --=====================================================
-- Junk Icon Helper Functions (reusable for all views) -- Junk Icon Pool (Baganator-inspired memory optimization)
-- Shows a vendor sell icon on junk items -- Uses frame pooling to avoid creating new frames per button
--===================================================== --=====================================================
local junkIconPool = {}
-- Create junk icon for a button (parented to UIParent for full opacity) -- Get a junk icon from pool or create new one
local function CreateJunkIcon(button) local function AcquireJunkIcon()
if button.junkIcon then return button.junkIcon end 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) local texture = icon:CreateTexture(nil, "OVERLAY")
junkFrame:SetFrameStrata("HIGH") texture:SetAllPoints(icon)
junkFrame:SetWidth(14) texture:SetTexture("Interface\\GossipFrame\\VendorGossipIcon")
junkFrame:SetHeight(14) texture:SetTexCoord(0, 1, 0, 1)
icon.texture = texture
local texture = junkFrame:CreateTexture(nil, "OVERLAY") end
texture:SetAllPoints(junkFrame) return icon
texture:SetTexture("Interface\\GossipFrame\\VendorGossipIcon")
texture:SetTexCoord(0, 1, 0, 1)
junkFrame:Hide()
button.junkIcon = junkFrame
return junkFrame
end end
-- Update junk icon visibility and position -- Release a junk icon back to the pool
local function UpdateJunkIcon(button, isJunk, iconSize) local function ReleaseJunkIcon(icon)
-- Create icon if it doesn't exist if icon then
if not button.junkIcon then icon:Hide()
CreateJunkIcon(button) icon:ClearAllPoints()
table.insert(junkIconPool, icon)
end end
end
-- Update junk icon visibility and position (uses pooling)
local function UpdateJunkIcon(button, isJunk, iconSize)
if isJunk then if isJunk then
-- Acquire from pool if needed
if not button.junkIcon then
button.junkIcon = AcquireJunkIcon()
end
-- Scale icon size based on button size -- Scale icon size based on button size
local junkIconSize = math.max(10, math.min(14, iconSize * 0.30)) local junkIconSize = math.max(10, math.min(14, iconSize * 0.30))
button.junkIcon:SetWidth(junkIconSize) button.junkIcon:SetWidth(junkIconSize)
@@ -63,14 +71,19 @@ local function UpdateJunkIcon(button, isJunk, iconSize)
button.junkIcon:SetAlpha(1.0) button.junkIcon:SetAlpha(1.0)
button.junkIcon:Show() button.junkIcon:Show()
else else
button.junkIcon:Hide() -- Release back to pool when not needed
if button.junkIcon then
ReleaseJunkIcon(button.junkIcon)
button.junkIcon = nil
end
end end
end end
-- Hide junk icon (for cleanup/reset) -- Hide junk icon (releases to pool)
local function HideJunkIcon(button) local function HideJunkIcon(button)
if button.junkIcon then if button.junkIcon then
button.junkIcon:Hide() ReleaseJunkIcon(button.junkIcon)
button.junkIcon = nil
end end
end end
@@ -251,6 +264,11 @@ function Guda_GetItemButton(parent)
buttonPool[nextButtonID] = button buttonPool[nextButtonID] = button
nextButtonID = nextButtonID + 1 nextButtonID = nextButtonID + 1
-- Register button with parent for tracking (avoids GetChildren() allocation)
if Guda_RegisterItemButton then
Guda_RegisterItemButton(parent, button)
end
return button return button
end end
@@ -337,8 +355,7 @@ function Guda_ItemButton_OnLoad(self)
self.questIcon = iconFrame self.questIcon = iconFrame
end end
-- Create junk icon overlay (vendor sell icon in top-left corner) -- Note: Junk icon is acquired from pool on-demand in UpdateJunkIcon, not pre-created
CreateJunkIcon(self)
-- Ensure the item button sits above its container backdrop and is mouse-enabled -- Ensure the item button sits above its container backdrop and is mouse-enabled
local parent = self:GetParent() local parent = self:GetParent()
@@ -976,12 +993,21 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
isLocked = locked isLocked = locked
end 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) -- Ensure itemData is populated for live items (needed for ItemDetection)
if itemLink then if itemLink then
local itemName, _, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture = GetItemInfo(itemLink) local itemName, _, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture = GetItemInfo(itemLink)
-- For bank main bag, get quality from GetItemInfo -- For bank main bag, get quality from GetItemInfo
if self.isBank and bagID == -1 then if self.isBank and bagID == -1 then
itemQuality = itemRarity 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 end
if not itemData then if not itemData then
-- Create new itemData -- Create new itemData