fix: sort ping-pong

This commit is contained in:
Salikh Gurgenidze
2025-12-28 04:23:34 +04:00
parent 094614e9a2
commit 91ed711290
+189 -133
View File
@@ -97,100 +97,137 @@ local GEM_PATTERNS = {
local scanTooltip = CreateFrame("GameTooltip", "Guda_SortScanTooltip", nil, "GameTooltipTemplate")
scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
-- Check if an item is a quest item by scanning its tooltip
local function IsQuestItemTooltip(bagID, slotID)
if not bagID or not slotID then return false end
-- Property cache to prevent race conditions during rapid moves
local propertyCache = {}
scanTooltip:ClearLines()
scanTooltip:SetBagItem(bagID, slotID)
function SortEngine:ClearCache()
propertyCache = {}
end
-- Check all tooltip lines for quest-related text (case-insensitive)
for i = 1, scanTooltip:NumLines() do
local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
local tl = string.lower(text)
if string.find(tl, "quest starter") or
string.find(tl, "this item begins a quest") or
string.find(tl, "starts a quest") or
string.find(tl, "quest item") or
string.find(tl, "manual") then
return true
local function GetItemProperties(bagID, slotID, itemLink)
if not itemLink then return nil end
-- Extract base item link for stable caching (item:ID:Enchant:...)
local _, _, baseLink = string.find(itemLink, "(item:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+)")
local cacheKey = baseLink or itemLink
if propertyCache[cacheKey] then
return propertyCache[cacheKey]
end
local props = {
isQuest = false,
isQuestStarter = false,
isQuestUsable = false,
isGray = false,
restoreTag = nil
}
-- Quality check from link color code (|cff9d9d9d is gray)
if string.find(itemLink, "|cff9d9d9d") then
props.isGray = true
end
if bagID and slotID then
scanTooltip:ClearLines()
scanTooltip:SetBagItem(bagID, slotID)
local numLines = scanTooltip:NumLines()
if numLines and numLines > 0 then
for i = 1, numLines do
local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
local tl = string.lower(text)
-- Quest item check
if string.find(tl, "quest starter") or
string.find(tl, "this item begins a quest") or
string.find(tl, "starts a quest") or
string.find(tl, "quest item") or
string.find(tl, "manual") then
props.isQuest = true
end
-- Starter check
if string.find(tl, "quest starter") or
string.find(tl, "this item begins a quest") or
string.find(tl, "starts a quest") then
props.isQuestStarter = true
end
-- Usable check
if string.find(tl, "use:") or
string.find(tl, "right%-click") or
string.find(tl, "right click") or
string.find(tl, "click to") then
props.isQuestUsable = true
end
-- Restore tag check (higher priority tags override lower ones)
if string.find(tl, "while eating") then
props.restoreTag = "eat"
elseif string.find(tl, "while drinking") then
if props.restoreTag ~= "eat" then
props.restoreTag = "drink"
end
elseif string.find(tl, "use: restores") then
if not props.restoreTag then
props.restoreTag = "restore"
end
end
-- Gray check (header line color fallback)
if i == 1 and string.find(text, "|cff9d9d9d") then
props.isGray = true
end
end
end
end
end
end
return false
propertyCache[cacheKey] = props
return props
end
-- Check if an item is a quest item by scanning its tooltip
local function IsQuestItemTooltip(bagID, slotID)
if not bagID or not slotID then return false end
local link = GetContainerItemLink(bagID, slotID)
local props = GetItemProperties(bagID, slotID, link)
return props and props.isQuest or false
end
-- Check if a quest item is usable (has 'Use:' or click to text)
local function IsQuestItemUsable(bagID, slotID)
if not bagID or not slotID then return false end
scanTooltip:ClearLines()
scanTooltip:SetBagItem(bagID, slotID)
for i = 1, scanTooltip:NumLines() do
local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
local tl = string.lower(text)
if string.find(tl, "use:") or string.find(tl, "right%-click") or string.find(tl, "right click") or string.find(tl, "click to") or string.find(tl, "starts a quest") then
return true
end
end
end
end
return false
local link = GetContainerItemLink(bagID, slotID)
local props = GetItemProperties(bagID, slotID, link)
return props and props.isQuestUsable or false
end
-- Check if item is a quest starter (explicit 'Starts a Quest' or 'This Item Begins a Quest')
local function IsQuestItemStarter(bagID, slotID)
if not bagID or not slotID then return false end
scanTooltip:ClearLines()
scanTooltip:SetBagItem(bagID, slotID)
for i = 1, scanTooltip:NumLines() do
local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
local tl = string.lower(text)
if string.find(tl, "quest starter") or string.find(tl, "this item begins a quest") or string.find(tl, "starts a quest") then
return true
end
end
end
end
return false
local link = GetContainerItemLink(bagID, slotID)
local props = GetItemProperties(bagID, slotID, link)
return props and props.isQuestStarter or false
end
-- Check if an item has a gray title in its tooltip or link
local function IsItemGrayTooltip(bagID, slotID, itemLink)
-- Check link first if provided
if itemLink and string.find(itemLink, "|cff9d9d9d") then
return true
end
local props = GetItemProperties(bagID, slotID, itemLink)
return props and props.isGray or false
end
if not bagID or not slotID then return false end
scanTooltip:ClearLines()
scanTooltip:SetBagItem(bagID, slotID)
local line = getglobal("Guda_SortScanTooltipTextLeft1")
if line then
local text = line:GetText()
if text then
-- Poor quality color code is |cff9d9d9d
if string.find(text, "|cff9d9d9d") then
return true
end
end
end
return false
-- Check if an item has a restore tag (eat, drink, or restore)
local function GetItemRestoreTagTooltip(bagID, slotID)
if not bagID or not slotID then return nil end
local link = GetContainerItemLink(bagID, slotID)
local props = GetItemProperties(bagID, slotID, link)
return props and props.restoreTag or nil
end
-- Extract itemID from item link
@@ -200,6 +237,19 @@ local function GetItemID(link)
return tonumber(itemID) or 0
end
-- Get item quality from link color code
local function GetQualityFromLink(link)
if not link then return 1 end
if string.find(link, "|cff9d9d9d") then return 0 end -- Gray
if string.find(link, "|cffffffff") then return 1 end -- White
if string.find(link, "|cff1eff00") then return 2 end -- Green
if string.find(link, "|cff0070dd") then return 3 end -- Blue
if string.find(link, "|cffa335ee") then return 4 end -- Purple
if string.find(link, "|cffff8000") then return 5 end -- Orange
if string.find(link, "|cffe6cc80") then return 6 end -- Red/Artifact
return 1
end
-- Extract texture pattern for grouping similar items
local function GetTexturePattern(textureName)
if not textureName then return "" end
@@ -466,24 +516,30 @@ local function AddSortKeys(items)
for _, item in ipairs(items) do
if item.data and item.data.link then
local itemID = GetItemID(item.data.link)
local itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount,
itemSubType, itemTexture, itemEquipLoc, itemSellPrice = GetItemInfo(itemID)
item.itemName = itemName
--addon:Print("itemTexture:%s",itemTexture or 0);
--addon:Print("itemEquipLoc:%s",itemEquipLoc or 0);
--addon:Print("itemSubType:%s",itemSubType or 0);
--addon:Print("itemType:%s",itemType or 0);
--addon:Print("itemCategory:%s",itemCategory or 0);
--addon:Print("itemLevel:%s",itemLevel or 0);
-- Use data collected during the initial scan (item.data) for absolute stability
local d = item.data
local itemName = d.name
local itemLink = d.link
local itemRarity = d.quality
local itemLevel = d.iLevel
local itemCategory = d.class
local itemType = d.type
local itemSubType = d.subclass
local itemTexture = d.texture
local itemEquipLoc = d.equipLoc
local itemStackSize = d.stackSize or 1
item.itemName = itemName or ""
if not itemName then
-- Skip items that couldn't be loaded
-- Skip items that couldn't be loaded (fallback safety)
item.sortedClass = 999
item.isEquippable = false
item.priority = 1000
item.equipSlotOrder = 999
item.invertedQuality = 0
item.invertedItemLevel = 0
item.itemName = ""
item.sortedSubclass = 999
item.subclass = ""
item.texturePattern = ""
@@ -522,11 +578,10 @@ local function AddSortKeys(items)
-- Heuristic: Detect items that should be in the Quest category (priority 7)
-- but aren't categorized as such by the game (e.g. some "Manual" items)
if item.sortedClass ~= (CATEGORY_ORDER["Quest"] or 7) then
local nameLower = item.itemName and string.lower(item.itemName) or ""
local nameLower = string.lower(item.itemName)
if string.find(nameLower, "manual") or string.find(nameLower, "quest") then
item.sortedClass = CATEGORY_ORDER["Quest"] or 7
elseif IsQuestItemTooltip(item.bagID, item.slot) then
-- If name-based heuristic fails, check tooltip
item.sortedClass = CATEGORY_ORDER["Quest"] or 7
end
end
@@ -535,31 +590,24 @@ local function AddSortKeys(items)
-- Subclass ordering
-- Detect consumable restore/eat/drink tag (if available) BEFORE computing subclass priority
item.restoreTag = nil
if addon.Modules.Utils and addon.Modules.Utils.GetConsumableRestoreTag then
if item.bagID and item.slot then
local tag = addon.Modules.Utils:GetConsumableRestoreTag(item.bagID, item.slot)
if tag then
item.restoreTag = tag
end
item.restoreTag = GetItemRestoreTagTooltip(item.bagID, item.slot)
local baseSub = GetSubclassOrder(itemSubType, item.itemName)
local function _cons_prk(t)
if t == "eat" then return -300 end
if t == "drink" then return -200 end
if t == "restore" then return -100 end
return 0
end
end
local baseSub = GetSubclassOrder(itemSubType, item.name)
local function _cons_prk(t)
if t == "eat" then return -300 end
if t == "drink" then return -200 end
if t == "restore" then return -100 end
return 0
end
item.sortedSubclass = _cons_prk(item.restoreTag) + baseSub
item.sortedSubclass = _cons_prk(item.restoreTag) + baseSub
item.subclass = itemSubType or ""
-- Quest flags: mark quest items and detect starter/usable states
item.isQuest = false
item.isQuestStarter = false
item.isQuestUsable = false
local nameLower = item.itemName and string.lower(item.itemName) or ""
if itemCategory == "Quest" or string.find(nameLower, "quest") or (item.data and item.data.class == "Quest") or IsQuestItemTooltip(item.bagID, item.slot) then
local nameLower = string.lower(item.itemName)
if itemCategory == "Quest" or string.find(nameLower, "quest") or item.data.class == "Quest" or IsQuestItemTooltip(item.bagID, item.slot) then
item.isQuest = true
if IsQuestItemStarter(item.bagID, item.slot) then item.isQuestStarter = true end
if IsQuestItemUsable(item.bagID, item.slot) then item.isQuestUsable = true end
@@ -568,7 +616,6 @@ local function AddSortKeys(items)
-- Texture pattern for grouping similar items (especially trade goods)
item.texturePattern = GetTexturePattern(itemTexture)
-- Group Trade Goods: meats (names ending with 'meat') before eggs
local nameLower = item.itemName and string.lower(item.itemName) or ""
if itemType == "Trade Goods" then
if string.find(nameLower, "meat$") then
item.texturePattern = "trade_meat"
@@ -584,12 +631,9 @@ local function AddSortKeys(items)
item.invertedItemID = -tonumber(itemID)
-- Stack info for reverse stack sorting
item.maxStackCount = tonumber(itemStackCount) or 1
item.maxStackCount = tonumber(itemStackSize) or 1
item.isStackable = item.maxStackCount > 1
item.stackCount = tonumber(item.data.count) or 1
-- Name for alphabetical sorting
item.itemName = item.name or ""
end
end
end
@@ -747,6 +791,11 @@ local function CollectItems(bagIDs)
-- Get fresh item info with ALL return values
local itemID = GetItemID(itemLink)
local name, link, quality, iLevel, category, itemType, stackCount, subType, iconTex, equipLoc, sellPrice = GetItemInfo(itemID)
-- Fallback for quality if GetItemInfo fails (ensures stability)
if not quality then
quality = GetQualityFromLink(itemLink)
end
sequence = sequence + 1
table.insert(items, {
@@ -755,14 +804,16 @@ local function CollectItems(bagIDs)
sequence = sequence, -- Preserve original order
data = {
link = itemLink,
texture = texture,
texture = texture or iconTex,
count = itemCount or 1,
quality = quality or 0,
name = name,
iLevel = iLevel,
type = itemType,
class = category,
subclass = subType, -- NOW INCLUDED!
subclass = subType,
equipLoc = equipLoc,
stackSize = stackCount or 1,
locked = locked,
},
quality = quality or 0,
@@ -898,8 +949,6 @@ end
-- and spilling into previous bags when needed.
local function BuildGreyTailPositions(bagIDs, greyCount)
local positions = {}
local index = 1
if greyCount <= 0 then return positions end
-- Order bags: lowest priority first (these are considered "last"),
@@ -927,26 +976,42 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
end)
-- Collect tail slots from end to start, spilling to previous bags as needed.
local tailSlots = {}
for _, info in ipairs(ordered) do
for slot = info.numSlots, 1, -1 do
if index <= greyCount then
positions[index] = { bag = info.bagID, slot = slot }
index = index + 1
if table.getn(tailSlots) < greyCount then
table.insert(tailSlots, { bag = info.bagID, slot = slot })
else
break
end
end
if index > greyCount then break end
if table.getn(tailSlots) >= greyCount then break end
end
return positions
-- 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)
return tailSlots
end
-- Split a list of collected items into non-greys and greys (quality 0)
local function SplitGreyItems(items)
local nonGreys, greys = {}, {}
for _, item in ipairs(items) do
if tonumber(item.quality or 0) == 0 then
-- Use same logic as AddSortKeys for determining Junk/Grey status (stability)
if tonumber(item.quality or 0) == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then
table.insert(greys, item)
else
table.insert(nonGreys, item)
@@ -1033,20 +1098,8 @@ function SortEngine:AnalyzeContainer(bagIDs, containerType)
for i, item in ipairs(greys) do
local target = tailPositions[i]
-- FIX: Check if grey item is already in a tail position of a valid regular bag
local isInTailPosition = false
-- Check if this grey item is already in the end slots of any valid regular bag
for _, bagID in ipairs(validRegularBags) do
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
if item.bagID == bagID and item.slot >= (numSlots - table.getn(greys) + 1) then
isInTailPosition = true
break
end
end
-- Only count as out of place if it's NOT in a tail position AND doesn't match target
if target and (item.bagID ~= target.bag or item.slot ~= target.slot) and not isInTailPosition then
-- Only count as out of place if it doesn't match the specific stable target
if target and (item.bagID ~= target.bag or item.slot ~= target.slot) then
totalOutOfPlace = totalOutOfPlace + 1
end
end
@@ -1333,6 +1386,9 @@ end
--===========================================================================
function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sortType)
-- Clear property cache at the start of a sort operation
self:ClearCache()
-- Analyze to determine how many passes are needed
local analysis = analyzeFunction()