performance: sort engine performance

This commit is contained in:
Salikh Gurgenidze
2026-01-24 03:33:23 +04:00
parent 5376f0f0ef
commit 846edb8d48
6 changed files with 322 additions and 161 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
+68 -26
View File
@@ -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
View File
@@ -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
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)
+44 -26
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