Merge pull request #35 from vatichild/feat/sort-engine-performance
Feat/sort engine performance
This commit is contained in:
+80
-66
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user