performance: sort engine performance
This commit is contained in:
+80
-66
@@ -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
@@ -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
|
||||
|
||||
+68
-26
@@ -12,6 +12,10 @@ 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
|
||||
local MAX_BANK_MOVES_PER_CYCLE = 30 -- Bank has 50% more slots, needs more moves per cycle
|
||||
|
||||
-- Current sort context (set by ExecuteSort, used by ApplySort)
|
||||
local currentSortType = "bags"
|
||||
|
||||
-- Transfer status constants (like Baganator's SortStatus)
|
||||
local TransferStatus = {
|
||||
@@ -131,10 +135,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 +164,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 +253,7 @@ local function GetItemProperties(bagID, slotID, itemLink)
|
||||
end
|
||||
|
||||
propertyCache[cacheKey] = props
|
||||
propertyCacheSize = propertyCacheSize + 1
|
||||
return props
|
||||
end
|
||||
|
||||
@@ -990,12 +1013,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 +1060,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 +1100,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 +1118,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
|
||||
|
||||
@@ -1137,12 +1166,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 +1192,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 +1656,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 +1667,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 +1681,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 +1710,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 +1736,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 +1769,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()
|
||||
|
||||
+6
-9
@@ -173,9 +173,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 +193,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 +272,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()
|
||||
|
||||
+61
-19
@@ -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)
|
||||
|
||||
+44
-26
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user