1497 lines
52 KiB
Lua
1497 lines
52 KiB
Lua
-- Guda Utility Functions
|
|
|
|
local addon = Guda
|
|
|
|
local Utils = {}
|
|
addon.Modules.Utils = Utils
|
|
|
|
--=============================================================================
|
|
-- Timer Frame Pool (prevents memory leaks from temporary timers)
|
|
-- Global function so all modules can use it
|
|
--=============================================================================
|
|
local timerPool = {}
|
|
local TIMER_POOL_MAX = 10 -- Limit pool size to prevent unbounded growth
|
|
|
|
function Guda_ScheduleTimer(delay, callback)
|
|
-- Try to get a frame from the pool
|
|
local frame = table.remove(timerPool)
|
|
if not frame then
|
|
frame = CreateFrame("Frame")
|
|
end
|
|
|
|
frame.elapsed = 0
|
|
frame.delay = delay
|
|
frame.callback = callback
|
|
frame:SetScript("OnUpdate", function()
|
|
this.elapsed = this.elapsed + arg1
|
|
if this.elapsed >= this.delay then
|
|
this:SetScript("OnUpdate", nil)
|
|
this:Hide()
|
|
-- Return frame to pool if not full
|
|
if table.getn(timerPool) < TIMER_POOL_MAX then
|
|
table.insert(timerPool, this)
|
|
end
|
|
-- Execute callback
|
|
this.callback()
|
|
end
|
|
end)
|
|
frame:Show()
|
|
end
|
|
|
|
--=============================================================================
|
|
-- Tooltip Scan Caching
|
|
-- Caches results of expensive tooltip scanning operations
|
|
--=============================================================================
|
|
local tooltipCache = {
|
|
questItem = {}, -- IsQuestItem results
|
|
specialText = {}, -- HasSpecialTooltipText results
|
|
bindOnEquip = {}, -- IsBindOnEquip results
|
|
uniqueItem = {}, -- IsUniqueItem results
|
|
restoreTag = {}, -- GetConsumableRestoreTag results
|
|
}
|
|
local tooltipCacheStats = {
|
|
hits = 0,
|
|
misses = 0,
|
|
}
|
|
|
|
-- Clear all tooltip caches
|
|
function Utils:ClearTooltipCache()
|
|
tooltipCache.questItem = {}
|
|
tooltipCache.specialText = {}
|
|
tooltipCache.bindOnEquip = {}
|
|
tooltipCache.uniqueItem = {}
|
|
tooltipCache.restoreTag = {}
|
|
tooltipCacheStats.hits = 0
|
|
tooltipCacheStats.misses = 0
|
|
addon:Debug("Tooltip cache cleared")
|
|
end
|
|
|
|
-- Get tooltip cache statistics
|
|
function Utils:GetTooltipCacheStats()
|
|
local total = tooltipCacheStats.hits + tooltipCacheStats.misses
|
|
local hitRate = total > 0 and (tooltipCacheStats.hits / total * 100) or 0
|
|
return {
|
|
hits = tooltipCacheStats.hits,
|
|
misses = tooltipCacheStats.misses,
|
|
total = total,
|
|
hitRate = hitRate,
|
|
}
|
|
end
|
|
|
|
-- Generate cache key from item link
|
|
local function GetTooltipCacheKey(itemLink)
|
|
if not itemLink then return nil end
|
|
-- Extract item ID from link for stable caching
|
|
local _, _, itemID = string.find(itemLink, "item:(%d+)")
|
|
return itemID
|
|
end
|
|
|
|
--=============================================================================
|
|
-- Frame Budget System (Baganator-inspired performance optimization)
|
|
-- Prevents any single operation from causing frame lag by spreading work
|
|
-- across multiple frames when operations exceed a time budget.
|
|
--=============================================================================
|
|
|
|
-- Frame budget configuration
|
|
local FRAME_BUDGET_SECONDS = 0.1 -- 100ms budget per frame (same as Baganator)
|
|
local lastEntryTime = 0
|
|
local workQueue = {}
|
|
local workQueueFrame = nil
|
|
|
|
-- Report that we're starting work (call at the beginning of expensive operations)
|
|
-- This resets the frame budget timer
|
|
function Utils:ReportEntry()
|
|
lastEntryTime = GetTime()
|
|
end
|
|
|
|
-- Check if we've exceeded the frame budget
|
|
-- Returns true if we should defer remaining work to the next frame
|
|
function Utils:CheckTimeout()
|
|
return (GetTime() - lastEntryTime) > FRAME_BUDGET_SECONDS
|
|
end
|
|
|
|
-- Queue work to be executed in the next frame
|
|
-- callback: function to call
|
|
-- context: optional context/owner for the callback (for debugging/cleanup)
|
|
function Utils:QueueWork(callback, context)
|
|
if type(callback) ~= "function" then
|
|
addon:Error("QueueWork: callback must be a function")
|
|
return
|
|
end
|
|
|
|
table.insert(workQueue, {callback = callback, context = context or "unknown"})
|
|
|
|
-- Create the work queue processor frame if it doesn't exist
|
|
if not workQueueFrame then
|
|
workQueueFrame = CreateFrame("Frame", "Guda_WorkQueueFrame", UIParent)
|
|
workQueueFrame.elapsed = 0
|
|
workQueueFrame:Hide()
|
|
|
|
workQueueFrame:SetScript("OnUpdate", function()
|
|
-- Process queued work with frame budget
|
|
Utils:ReportEntry()
|
|
|
|
local processedCount = 0
|
|
local maxPerFrame = 50 -- Safety limit to prevent infinite loops
|
|
|
|
while table.getn(workQueue) > 0 and processedCount < maxPerFrame do
|
|
-- Check if we've exceeded frame budget
|
|
if Utils:CheckTimeout() then
|
|
-- Still have work but exceeded budget, continue next frame
|
|
addon:Debug("Frame budget exceeded, deferring %d items to next frame", table.getn(workQueue))
|
|
return
|
|
end
|
|
|
|
local work = table.remove(workQueue, 1)
|
|
if work and work.callback then
|
|
local success, err = pcall(work.callback)
|
|
if not success then
|
|
addon:Error("QueueWork callback error [%s]: %s", tostring(work.context), tostring(err))
|
|
end
|
|
end
|
|
processedCount = processedCount + 1
|
|
end
|
|
|
|
-- All work done, hide the frame to stop OnUpdate
|
|
if table.getn(workQueue) == 0 then
|
|
workQueueFrame:Hide()
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Show the frame to start processing
|
|
workQueueFrame:Show()
|
|
end
|
|
|
|
-- Clear all queued work (useful when frame is hidden)
|
|
function Utils:ClearWorkQueue()
|
|
workQueue = {}
|
|
if workQueueFrame then
|
|
workQueueFrame:Hide()
|
|
end
|
|
end
|
|
|
|
-- Get the number of items in the work queue (for debugging)
|
|
function Utils:GetWorkQueueSize()
|
|
return table.getn(workQueue)
|
|
end
|
|
|
|
-- Process items in batches with frame budget awareness
|
|
-- items: table of items to process
|
|
-- processor: function(item, index) called for each item
|
|
-- onComplete: optional function called when all items are processed
|
|
-- batchSize: optional number of items to process before checking timeout (default 10)
|
|
function Utils:ProcessWithBudget(items, processor, onComplete, batchSize)
|
|
if not items or table.getn(items) == 0 then
|
|
if onComplete then onComplete() end
|
|
return
|
|
end
|
|
|
|
batchSize = batchSize or 10
|
|
local index = 1
|
|
local totalItems = table.getn(items)
|
|
|
|
local function processNextBatch()
|
|
Utils:ReportEntry()
|
|
local batchCount = 0
|
|
|
|
while index <= totalItems and batchCount < batchSize do
|
|
if Utils:CheckTimeout() then
|
|
-- Budget exceeded, queue continuation
|
|
Utils:QueueWork(processNextBatch, "ProcessWithBudget")
|
|
return
|
|
end
|
|
|
|
local item = items[index]
|
|
if item then
|
|
local success, err = pcall(processor, item, index)
|
|
if not success then
|
|
addon:Error("ProcessWithBudget processor error at index %d: %s", index, tostring(err))
|
|
end
|
|
end
|
|
|
|
index = index + 1
|
|
batchCount = batchCount + 1
|
|
end
|
|
|
|
-- Check if we have more items
|
|
if index <= totalItems then
|
|
-- More items to process, queue next batch
|
|
Utils:QueueWork(processNextBatch, "ProcessWithBudget")
|
|
else
|
|
-- All done
|
|
if onComplete then
|
|
local success, err = pcall(onComplete)
|
|
if not success then
|
|
addon:Error("ProcessWithBudget onComplete error: %s", tostring(err))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Start processing
|
|
processNextBatch()
|
|
end
|
|
|
|
-- Performance metrics tracking
|
|
local performanceStats = {
|
|
budgetExceededCount = 0,
|
|
totalUpdates = 0,
|
|
lastUpdateDuration = 0,
|
|
averageUpdateDuration = 0,
|
|
}
|
|
|
|
-- Get the current frame budget setting (in seconds)
|
|
function Utils:GetFrameBudget()
|
|
return FRAME_BUDGET_SECONDS
|
|
end
|
|
|
|
-- Set the frame budget (in seconds, min 0.016 = 60fps, max 0.5)
|
|
function Utils:SetFrameBudget(seconds)
|
|
if type(seconds) ~= "number" then return end
|
|
FRAME_BUDGET_SECONDS = math.max(0.016, math.min(0.5, seconds))
|
|
addon:Debug("Frame budget set to %.3f seconds", FRAME_BUDGET_SECONDS)
|
|
end
|
|
|
|
-- Record the end of an update cycle for performance tracking
|
|
function Utils:RecordUpdateEnd()
|
|
local duration = GetTime() - lastEntryTime
|
|
performanceStats.lastUpdateDuration = duration
|
|
performanceStats.totalUpdates = performanceStats.totalUpdates + 1
|
|
|
|
-- Update rolling average
|
|
local alpha = 0.1 -- Smoothing factor
|
|
performanceStats.averageUpdateDuration = performanceStats.averageUpdateDuration * (1 - alpha) + duration * alpha
|
|
|
|
if duration > FRAME_BUDGET_SECONDS then
|
|
performanceStats.budgetExceededCount = performanceStats.budgetExceededCount + 1
|
|
end
|
|
end
|
|
|
|
-- Get performance statistics
|
|
function Utils:GetPerformanceStats()
|
|
return {
|
|
frameBudget = FRAME_BUDGET_SECONDS,
|
|
lastUpdateDuration = performanceStats.lastUpdateDuration,
|
|
averageUpdateDuration = performanceStats.averageUpdateDuration,
|
|
totalUpdates = performanceStats.totalUpdates,
|
|
budgetExceededCount = performanceStats.budgetExceededCount,
|
|
workQueueSize = table.getn(workQueue),
|
|
}
|
|
end
|
|
|
|
-- Reset performance statistics
|
|
function Utils:ResetPerformanceStats()
|
|
performanceStats.budgetExceededCount = 0
|
|
performanceStats.totalUpdates = 0
|
|
performanceStats.lastUpdateDuration = 0
|
|
performanceStats.averageUpdateDuration = 0
|
|
end
|
|
|
|
-- Print current performance statistics (for debugging)
|
|
function Utils:PrintPerformanceStats()
|
|
local stats = self:GetPerformanceStats()
|
|
addon:Print(Guda_L["=== Guda Performance Stats ==="])
|
|
addon:Print(Guda_L["Frame Budget: %.0fms"], stats.frameBudget * 1000)
|
|
addon:Print(Guda_L["Last Update: %.1fms"], stats.lastUpdateDuration * 1000)
|
|
addon:Print("Avg Update: %.1fms", stats.averageUpdateDuration * 1000)
|
|
addon:Print(Guda_L["Total Updates: %d"], stats.totalUpdates)
|
|
addon:Print(Guda_L["Budget Exceeded: %d times"], stats.budgetExceededCount)
|
|
addon:Print("Work Queue: %d items", stats.workQueueSize)
|
|
end
|
|
|
|
--=============================================================================
|
|
-- SafeCall: Nil-safe module method invocation
|
|
-- Replaces verbose nil-checks like:
|
|
-- if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.Method then
|
|
-- return addon.Modules.Utils:Method(arg1, arg2)
|
|
-- end
|
|
-- With:
|
|
-- return Utils:SafeCall("Utils", "Method", arg1, arg2)
|
|
--=============================================================================
|
|
|
|
-- Call a method on a module safely, returns nil if module/method doesn't exist
|
|
-- Parameters:
|
|
-- moduleName: Name of the module in addon.Modules (e.g., "Utils", "DB", "BagFrame")
|
|
-- methodName: Name of the method to call (e.g., "GetQualityColor", "GetSetting")
|
|
-- ...: Arguments to pass to the method
|
|
-- Returns: The return value(s) of the method, or nil if not callable
|
|
function Utils:SafeCall(moduleName, methodName, ...)
|
|
if not addon or not addon.Modules then
|
|
return nil
|
|
end
|
|
|
|
local module = addon.Modules[moduleName]
|
|
if not module then
|
|
return nil
|
|
end
|
|
|
|
local method = module[methodName]
|
|
if not method or type(method) ~= "function" then
|
|
return nil
|
|
end
|
|
|
|
-- Call method with module as self (for : style calls)
|
|
return method(module, unpack(arg))
|
|
end
|
|
|
|
-- Check if a module method exists without calling it
|
|
function Utils:HasMethod(moduleName, methodName)
|
|
if not addon or not addon.Modules then
|
|
return false
|
|
end
|
|
|
|
local module = addon.Modules[moduleName]
|
|
if not module then
|
|
return false
|
|
end
|
|
|
|
local method = module[methodName]
|
|
return method ~= nil and type(method) == "function"
|
|
end
|
|
|
|
-- Get a module reference safely
|
|
function Utils:GetModule(moduleName)
|
|
if not addon or not addon.Modules then
|
|
return nil
|
|
end
|
|
return addon.Modules[moduleName]
|
|
end
|
|
|
|
-- Format money (copper to gold/silver/copper string) - WoW 1.12.1 version
|
|
function Utils:FormatMoney(copper, showZero, useColors)
|
|
if not copper or copper == 0 then
|
|
if useColors then
|
|
return "|cFFFFFFFF" .. "0" .. "|r" .. "|cFFEDA55F" .. "c" .. "|r"
|
|
else
|
|
return "0c"
|
|
end
|
|
end
|
|
|
|
local gold = math.floor(copper / 10000)
|
|
local silver = math.floor(mod(copper, 10000) / 100)
|
|
local bronze = mod(copper, 100)
|
|
|
|
local str = ""
|
|
|
|
if useColors then
|
|
-- Colored version for tooltips - white numbers, colored g/s/c letters
|
|
if gold > 0 then
|
|
str = str .. "|cFFFFFFFF" .. gold .. "|r" .. "|cFFFFD700" .. "g" .. "|r "
|
|
end
|
|
if silver > 0 or gold > 0 then
|
|
str = str .. "|cFFFFFFFF" .. silver .. "|r" .. "|cFFC7C7CF" .. "s" .. "|r "
|
|
end
|
|
str = str .. "|cFFFFFFFF" .. bronze .. "|r" .. "|cFFEDA55F" .. "c" .. "|r"
|
|
else
|
|
-- Plain text version
|
|
if gold > 0 then
|
|
str = str .. gold .. "g "
|
|
end
|
|
if silver > 0 or gold > 0 then
|
|
str = str .. silver .. "s "
|
|
end
|
|
str = str .. bronze .. "c"
|
|
end
|
|
|
|
return str
|
|
end
|
|
|
|
-- Get item info with caching
|
|
local itemCache = {}
|
|
|
|
-- Extract itemID from itemLink (Lua 5.0 compatible)
|
|
-- Returns: itemID as number, or nil if extraction fails
|
|
function Utils:ExtractItemID(itemLink)
|
|
if not itemLink then return nil end
|
|
local _, _, itemID = string.find(itemLink, "item:(%d+)")
|
|
return itemID and tonumber(itemID) or nil
|
|
end
|
|
|
|
-- Get item info with error handling (does not cache)
|
|
-- Turtle WoW GetItemInfo signature:
|
|
-- itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice
|
|
function Utils:GetItemInfoSafe(itemID)
|
|
if not itemID then
|
|
addon:Debug("GetItemInfoSafe: nil itemID")
|
|
return nil
|
|
end
|
|
|
|
local itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = GetItemInfo(itemID)
|
|
|
|
if not itemName then
|
|
addon:Debug("GetItemInfoSafe: GetItemInfo failed for itemID %d", itemID)
|
|
return nil
|
|
end
|
|
|
|
return itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice
|
|
end
|
|
|
|
-- Get item info with caching (wrapper for backward compatibility)
|
|
function Utils:GetItemInfo(itemLink)
|
|
if not itemLink then return nil end
|
|
|
|
if itemCache[itemLink] then
|
|
return unpack(itemCache[itemLink])
|
|
end
|
|
|
|
local itemID = self:ExtractItemID(itemLink)
|
|
if not itemID then return nil end
|
|
|
|
local itemName, retLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = self:GetItemInfoSafe(itemID)
|
|
|
|
if itemName then
|
|
itemCache[itemLink] = {itemName, retLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice}
|
|
return itemName, retLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Create a single shared tooltip for all scanning operations
|
|
-- This tooltip is used by: Utils, ItemDetection, SortEngine
|
|
local scanTooltip = CreateFrame("GameTooltip", "GudaScanTooltip", nil, "GameTooltipTemplate")
|
|
scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
local SCAN_TOOLTIP_NAME = "GudaScanTooltip"
|
|
|
|
-- Public getter for the shared scan tooltip (used by ItemDetection, SortEngine)
|
|
function Utils:GetScanTooltip()
|
|
return scanTooltip, SCAN_TOOLTIP_NAME
|
|
end
|
|
|
|
-- Get item link from mailbox attachment (WoW 1.12.1 workaround)
|
|
function Utils:GetInboxItemLink(index, itemIndex)
|
|
-- Try global function first (if it exists on this server/version)
|
|
if GetInboxItemLink then
|
|
-- Turtle WoW might support (index, itemIndex) for multiple attachments
|
|
local link = GetInboxItemLink(index, itemIndex or 1)
|
|
if link then return link end
|
|
|
|
-- Fallback to single argument if that failed
|
|
link = GetInboxItemLink(index)
|
|
if link then return link end
|
|
end
|
|
|
|
-- In 1.12.1, GameTooltip:GetHyperlink() does not exist.
|
|
-- Let's try to use GetItemInfo(name) as the primary way.
|
|
local name, texture, count, quality = GetInboxItem(index, itemIndex or 1)
|
|
if name then
|
|
local itemName, link = GetItemInfo(name)
|
|
if link then
|
|
return link
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Get quality color
|
|
function Utils:GetQualityColor(quality)
|
|
local color = addon.Constants.QUALITY_COLORS[quality] or addon.Constants.QUALITY_COLORS[1]
|
|
return color.r, color.g, color.b
|
|
end
|
|
|
|
-- Extract RGB color from an item link's |cffRRGGBB prefix (the title color)
|
|
-- Returns r, g, b (0-1 floats) or nil if no color found
|
|
function Utils:GetLinkColor(itemLink)
|
|
if not itemLink then return nil end
|
|
-- Lua 5.0: use string.find with captures instead of string.match
|
|
local _, _, hex = string.find(itemLink, "|c(%x%x%x%x%x%x%x%x)")
|
|
if not hex then return nil end
|
|
-- hex is AARRGGBB
|
|
local r = tonumber(string.sub(hex, 3, 4), 16) / 255
|
|
local g = tonumber(string.sub(hex, 5, 6), 16) / 255
|
|
local b = tonumber(string.sub(hex, 7, 8), 16) / 255
|
|
return r, g, b
|
|
end
|
|
|
|
-- Create colored text
|
|
function Utils:ColorText(text, r, g, b)
|
|
local red = math.floor(r * 255)
|
|
local green = math.floor(g * 255)
|
|
local blue = math.floor(b * 255)
|
|
return string.format("|cFF%02X%02X%02X%s|r", red, green, blue, text)
|
|
end
|
|
|
|
-- Deep copy table
|
|
function Utils:DeepCopy(orig)
|
|
local orig_type = type(orig)
|
|
local copy
|
|
if orig_type == 'table' then
|
|
copy = {}
|
|
for orig_key, orig_value in next, orig, nil do
|
|
copy[Utils:DeepCopy(orig_key)] = Utils:DeepCopy(orig_value)
|
|
end
|
|
setmetatable(copy, Utils:DeepCopy(getmetatable(orig)))
|
|
else
|
|
copy = orig
|
|
end
|
|
return copy
|
|
end
|
|
|
|
-- 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)
|
|
return CLASS_COLORS[class] or CLASS_COLORS._default
|
|
end
|
|
|
|
-- Format time ago
|
|
function Utils:FormatTimeAgo(timestamp)
|
|
local diff = time() - timestamp
|
|
if diff < 60 then
|
|
return "Just now"
|
|
elseif diff < 3600 then
|
|
return math.floor(diff / 60) .. "m ago"
|
|
elseif diff < 86400 then
|
|
return math.floor(diff / 3600) .. "h ago"
|
|
else
|
|
return math.floor(diff / 86400) .. "d ago"
|
|
end
|
|
end
|
|
|
|
-- Create hidden tooltip for scanning (only once)
|
|
local function GetScanTooltip()
|
|
return scanTooltip
|
|
end
|
|
|
|
-- Helper: Check if a color is yellow/gold (Use:, Equip:, Chance on hit: effects)
|
|
-- Yellow in WoW tooltips can be various shades:
|
|
-- Gold: RGB ~(1, 0.82, 0) or (255, 209, 0)
|
|
-- Yellow: RGB ~(1, 1, 0)
|
|
-- Light gold: RGB ~(1, 0.85, 0.1)
|
|
local function IsYellowColor(r, g, b)
|
|
if not r or not g or not b then return false end
|
|
-- Yellow/Gold: high red (>0.8), medium-high green (>0.5), low blue (<0.4)
|
|
return r > 0.8 and g > 0.5 and b < 0.4
|
|
end
|
|
|
|
-- Helper: Check if a color is green (set bonuses, special properties)
|
|
-- Green in WoW tooltips is typically RGB ~(0, 1, 0) or (0.12, 1, 0)
|
|
local function IsGreenColor(r, g, b)
|
|
if not r or not g or not b then return false end
|
|
-- Green: low red (<0.4), high green (>0.7), low blue (<0.4)
|
|
return r < 0.4 and g > 0.7 and b < 0.4
|
|
end
|
|
|
|
-- 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)
|
|
function Utils:HasSpecialTooltipText(bagID, slotID, itemLink)
|
|
bagID = tonumber(bagID)
|
|
slotID = tonumber(slotID)
|
|
|
|
-- Get item link for cache key if not provided
|
|
local cacheLink = itemLink
|
|
if not cacheLink and bagID and slotID then
|
|
cacheLink = GetContainerItemLink(bagID, slotID)
|
|
end
|
|
|
|
-- Check cache first
|
|
local cacheKey = GetTooltipCacheKey(cacheLink)
|
|
if cacheKey and tooltipCache.specialText[cacheKey] ~= nil then
|
|
tooltipCacheStats.hits = tooltipCacheStats.hits + 1
|
|
local cached = tooltipCache.specialText[cacheKey]
|
|
return cached.hasSpecial, cached.textType
|
|
end
|
|
tooltipCacheStats.misses = tooltipCacheStats.misses + 1
|
|
|
|
local tooltip = GetScanTooltip()
|
|
if not tooltip then return false, nil end
|
|
|
|
tooltip:ClearLines()
|
|
|
|
-- Set the tooltip to the item
|
|
if bagID and slotID then
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
elseif itemLink then
|
|
local _, _, itemString = string.find(itemLink, "(item:%d+:%d+:%d+:%d+)")
|
|
if itemString then
|
|
tooltip:SetHyperlink(itemString)
|
|
else
|
|
return false, nil
|
|
end
|
|
else
|
|
return false, nil
|
|
end
|
|
|
|
local numLines = tooltip:NumLines() or 0
|
|
if numLines == 0 then
|
|
-- Cache negative result
|
|
if cacheKey then
|
|
tooltipCache.specialText[cacheKey] = { hasSpecial = false, textType = nil }
|
|
end
|
|
return false, nil
|
|
end
|
|
|
|
-- Scan tooltip lines for yellow or green text
|
|
for i = 2, numLines do -- Start from line 2 (skip item name on line 1)
|
|
local leftLine = getglobal("GudaScanTooltipTextLeft" .. i)
|
|
if leftLine and leftLine:IsShown() then
|
|
local text = leftLine:GetText()
|
|
local r, g, b = leftLine:GetTextColor()
|
|
|
|
if text and r and g and b then
|
|
local textLower = string.lower(text)
|
|
|
|
-- Check for yellow/gold text (Use:, Equip:, Chance on hit:, etc.)
|
|
if IsYellowColor(r, g, b) then
|
|
-- 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
|
|
|
|
-- Check for green text (set bonuses, enchants, special properties)
|
|
-- Green text is ALWAYS considered special (no pattern check needed)
|
|
if IsGreenColor(r, g, b) then
|
|
addon:Debug("HasSpecialTooltipText: Found GREEN text: %s (r=%.2f g=%.2f b=%.2f)", text, r, g, b)
|
|
-- Cache positive result
|
|
if cacheKey then
|
|
tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "green" }
|
|
end
|
|
return true, "green"
|
|
end
|
|
|
|
-- Also check for "Use:" or "Equip:" regardless of color (some items may have different colors)
|
|
if string.find(textLower, "^use:") or string.find(textLower, "^equip:") then
|
|
addon:Debug("HasSpecialTooltipText: Found Use/Equip text: %s", text)
|
|
-- Cache positive result
|
|
if cacheKey then
|
|
tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "yellow" }
|
|
end
|
|
return true, "yellow"
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Also check right side of tooltip
|
|
local rightLine = getglobal("GudaScanTooltipTextRight" .. i)
|
|
if rightLine and rightLine:IsShown() then
|
|
local text = rightLine:GetText()
|
|
local r, g, b = rightLine:GetTextColor()
|
|
|
|
if text and r and g and b then
|
|
if IsYellowColor(r, g, b) or IsGreenColor(r, g, b) then
|
|
addon:Debug("HasSpecialTooltipText: Found special text (right): %s", text)
|
|
local textType = IsYellowColor(r, g, b) and "yellow" or "green"
|
|
-- Cache positive result
|
|
if cacheKey then
|
|
tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = textType }
|
|
end
|
|
return true, textType
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cache negative result
|
|
if cacheKey then
|
|
tooltipCache.specialText[cacheKey] = { hasSpecial = false, textType = nil }
|
|
end
|
|
return false, nil
|
|
end
|
|
|
|
-- Check if an item is a quest item by scanning its tooltip (internal helper)
|
|
-- Returns: isQuestItem, isQuestStarter
|
|
local function ScanTooltipForQuest(tooltip, tooltipName)
|
|
local isQuestItem = false
|
|
local isQuestStarter = false
|
|
|
|
for i = 1, tooltip:NumLines() do
|
|
local line = getglobal(tooltipName .. "TextLeft" .. i)
|
|
if line then
|
|
local text = line:GetText()
|
|
if text then
|
|
local tl = string.lower(text)
|
|
-- Check for quest starter patterns first
|
|
if string.find(tl, "quest starter") or
|
|
string.find(tl, "this item begins a quest") or
|
|
string.find(tl, "starts a quest") then
|
|
isQuestItem = true
|
|
isQuestStarter = true
|
|
break
|
|
-- Check for regular quest item patterns
|
|
elseif string.find(tl, "quest item") or
|
|
string.find(tl, "manual") then
|
|
isQuestItem = true
|
|
-- Don't break, might still find a quest starter pattern
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return isQuestItem, isQuestStarter
|
|
end
|
|
|
|
-- Check if item has "Permanently..." text (enchanting scrolls/vellums)
|
|
-- These should NOT be considered quest items even if they have Quest category
|
|
local function IsPermanentEnchantItem(tooltip, tooltipName)
|
|
if not tooltip then return false end
|
|
local numLines = tooltip:NumLines() or 0
|
|
for i = 1, numLines do
|
|
local line = getglobal(tooltipName .. "TextLeft" .. i)
|
|
if line then
|
|
local text = line:GetText()
|
|
if text then
|
|
local tl = string.lower(text)
|
|
if string.find(tl, "permanently") then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Consolidated quest item detection function
|
|
-- Handles tooltip scanning, category checks, equipment filtering, and QuestItemsDB lookup
|
|
-- Parameters:
|
|
-- bagID, slotID: Required for tooltip scanning (can be nil for other char items)
|
|
-- itemData: Optional item data table with link, class/category, type fields
|
|
-- isOtherChar: Boolean, true if checking an item from another character's saved data
|
|
-- isBank: Boolean, true if the item is in the bank
|
|
-- Returns: isQuestItem (boolean), isQuestStarter (boolean)
|
|
function Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, isBank)
|
|
bagID = tonumber(bagID)
|
|
slotID = tonumber(slotID)
|
|
|
|
local isQuestItem = false
|
|
local isQuestStarter = false
|
|
|
|
-- Get item link and category info
|
|
local itemLink = itemData and itemData.link
|
|
local itemCategory = itemData and (itemData.class or itemData.category) or ""
|
|
local itemType = itemData and itemData.type or ""
|
|
local itemID
|
|
|
|
-- For live items, query the link directly
|
|
if not isOtherChar and bagID and slotID then
|
|
itemLink = GetContainerItemLink(bagID, slotID)
|
|
end
|
|
|
|
if itemLink then
|
|
itemID = self:ExtractItemID(itemLink)
|
|
if itemID then
|
|
local _, _, _, _, cat, typ = self:GetItemInfoSafe(itemID)
|
|
itemCategory = cat or itemCategory
|
|
itemType = typ or itemType
|
|
end
|
|
end
|
|
|
|
-- Early exit: exclude known non-quest items that the API misclassifies as "Quest"
|
|
if itemID and addon.Constants and addon.Constants.QUEST_CATEGORY_EXCLUSIONS and addon.Constants.QUEST_CATEGORY_EXCLUSIONS[itemID] then
|
|
return false, false
|
|
end
|
|
|
|
-- Check if item is equipment (should not be classified as quest unless explicitly Quest category)
|
|
local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or
|
|
itemType == "Weapon" or itemType == "Armor")
|
|
local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest")
|
|
|
|
-- Priority 1: If explicitly categorized as Quest, check if it's actually an enchant item first
|
|
if isQuestCategory then
|
|
-- Check tooltip for "Permanently" (enchanting scrolls should not be quest items)
|
|
if not isOtherChar and bagID and slotID then
|
|
local tooltip = GetScanTooltip()
|
|
tooltip:ClearLines()
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
if IsPermanentEnchantItem(tooltip, "GudaScanTooltip") then
|
|
return false, false -- Not a quest item, it's an enchant scroll
|
|
end
|
|
-- Also scan for quest starter text
|
|
local _, starterDetected = ScanTooltipForQuest(tooltip, "GudaScanTooltip")
|
|
return true, starterDetected
|
|
end
|
|
return true, false
|
|
end
|
|
|
|
-- Priority 2: Tooltip scanning for current character items
|
|
if not isOtherChar and bagID and slotID then
|
|
local tooltip = GetScanTooltip()
|
|
tooltip:ClearLines()
|
|
|
|
-- Handle bank items differently
|
|
if isBank and bagID == -1 then
|
|
if tooltip.SetInventoryItem then
|
|
tooltip:SetInventoryItem("player", 39 + slotID)
|
|
else
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
end
|
|
else
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
end
|
|
|
|
isQuestItem, isQuestStarter = ScanTooltipForQuest(tooltip, "GudaScanTooltip")
|
|
|
|
-- Filter out equipment that has quest-like text but isn't categorized as Quest
|
|
if isQuestItem and isEquipment and not isQuestCategory then
|
|
isQuestItem = false
|
|
isQuestStarter = false
|
|
end
|
|
end
|
|
|
|
-- Priority 3: Check QuestItemsDB for known faction-specific quest items
|
|
if not isQuestItem and itemID and addon.IsQuestItemByID then
|
|
local playerFaction = UnitFactionGroup("player")
|
|
if addon:IsQuestItemByID(itemID, playerFaction) then
|
|
isQuestItem = true
|
|
end
|
|
end
|
|
|
|
return isQuestItem, isQuestStarter
|
|
end
|
|
|
|
-- Legacy compatibility wrapper - keep the old function name working
|
|
function Utils:IsQuestItemTooltip(bagID, slotID)
|
|
local isQuest, _ = self:IsQuestItem(bagID, slotID, nil, false, false)
|
|
return isQuest
|
|
end
|
|
|
|
-- Check if an item has a gray title in its tooltip or link
|
|
function Utils:IsItemGrayTooltip(bagID, slotID, itemLink)
|
|
-- Check link first if provided (works for other characters too)
|
|
if itemLink and string.find(itemLink, "|cff9d9d9d") then
|
|
return true
|
|
end
|
|
|
|
if not bagID or not slotID then return false end
|
|
|
|
local tooltip = GetScanTooltip()
|
|
tooltip:ClearLines()
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
|
|
local line = getglobal("GudaScanTooltipTextLeft1")
|
|
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
|
|
end
|
|
|
|
-- Get bag slot count
|
|
function Utils:GetBagSlotCount(bagID)
|
|
if bagID == -1 then
|
|
-- Bank has 24 slots in vanilla
|
|
return 24
|
|
elseif bagID == -2 then
|
|
-- Keyring - vanilla WoW has 12-32 slots depending on version
|
|
local slots = GetContainerNumSlots(bagID)
|
|
if not slots or slots == 0 then
|
|
-- Fallback: keyring typically has 12 slots in vanilla
|
|
return 12
|
|
end
|
|
return slots
|
|
else
|
|
return GetContainerNumSlots(bagID) or 0
|
|
end
|
|
end
|
|
|
|
-- Check if bag is valid
|
|
function Utils:IsBagValid(bagID)
|
|
if bagID == 0 or bagID == -1 or bagID == -2 then
|
|
return true
|
|
end
|
|
return GetContainerNumSlots(bagID) and GetContainerNumSlots(bagID) > 0
|
|
end
|
|
|
|
-- Truncate text to length
|
|
function Utils:TruncateText(text, maxLen)
|
|
if not text then return "" end
|
|
if string.len(text) <= maxLen then
|
|
return text
|
|
end
|
|
return string.sub(text, 1, maxLen - 3) .. "..."
|
|
end
|
|
|
|
-- Returns: "soul", "herb", "enchant", "quiver", "ammo", or nil
|
|
-- This is the consolidated bag type detection function with tooltip fallback
|
|
-- =============================================================================
|
|
-- Locale-independent type resolution
|
|
-- =============================================================================
|
|
-- WoW's GetItemInfo returns LOCALIZED type/subtype strings ("Quiver" on enUS,
|
|
-- "箭袋" on zhCN, etc.). We resolve the local-client strings at runtime by
|
|
-- querying a few reference itemIDs that exist in every WoW 1.12 client, so
|
|
-- the rest of the code can match against the local language without hardcoding.
|
|
local localizedBagTypes = {} -- lowercased localized subType -> "quiver"/"ammo"/...
|
|
local localizedProjectiles = {} -- localized subType (any case) -> "arrow"/"bullet"
|
|
local localizedTypesResolved = false
|
|
|
|
-- Locale-specific subType strings live in Localization.lua (Guda_LSubtypes).
|
|
-- Avoids GetItemInfo cache cold-start: lookups are pure table reads, populated
|
|
-- once on first use from the current client locale (with enUS as a fallback).
|
|
local function ResolveLocalizedTypes()
|
|
if localizedTypesResolved then return end
|
|
if not Guda_LSubtypes then return end
|
|
local clientLocale = (GetLocale and GetLocale()) or "enUS"
|
|
local map = Guda_LSubtypes[clientLocale] or Guda_LSubtypes.enUS
|
|
local en = Guda_LSubtypes.enUS or {}
|
|
-- Merge enUS first (so English clients on a partial locale still work),
|
|
-- then the active locale on top.
|
|
local sources = { en, map }
|
|
for i = 1, 2 do
|
|
local src = sources[i]
|
|
if src then
|
|
if src.quiver then localizedBagTypes[string.lower(src.quiver)] = "quiver" end
|
|
if src.ammo then localizedBagTypes[string.lower(src.ammo)] = "ammo" end
|
|
if src.soul then localizedBagTypes[string.lower(src.soul)] = "soul" end
|
|
if src.herb then localizedBagTypes[string.lower(src.herb)] = "herb" end
|
|
if src.enchant then localizedBagTypes[string.lower(src.enchant)] = "enchant" end
|
|
if src.arrow then localizedProjectiles[src.arrow] = "arrow" end
|
|
if src.bullet then localizedProjectiles[src.bullet] = "bullet" end
|
|
end
|
|
end
|
|
localizedTypesResolved = true
|
|
end
|
|
|
|
function Utils:GetSpecializedBagType(bagID)
|
|
-- Skip backpack, bank, and keyring
|
|
if bagID == 0 or bagID == -1 or bagID == -2 then
|
|
return nil
|
|
end
|
|
|
|
-- Get the bag item
|
|
local invSlot = ContainerIDToInventoryID(bagID)
|
|
if not invSlot then
|
|
return nil
|
|
end
|
|
|
|
local link = GetInventoryItemLink("player", invSlot)
|
|
if not link then
|
|
return nil
|
|
end
|
|
|
|
local itemID = self:ExtractItemID(link)
|
|
|
|
-- Try GetItemInfo first (more reliable when available)
|
|
if itemID then
|
|
local _, _, _, _, _, itemType = self:GetItemInfoSafe(itemID)
|
|
if itemType then
|
|
local typeLower = string.lower(itemType)
|
|
|
|
-- Locale-independent: match against resolved local-client strings
|
|
ResolveLocalizedTypes()
|
|
if localizedBagTypes[typeLower] then
|
|
return localizedBagTypes[typeLower]
|
|
end
|
|
|
|
if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then
|
|
return "soul"
|
|
end
|
|
if string.find(typeLower, "herb bag") then
|
|
return "herb"
|
|
end
|
|
if string.find(typeLower, "enchanting bag") then
|
|
return "enchant"
|
|
end
|
|
if string.find(typeLower, "quiver") then
|
|
return "quiver"
|
|
end
|
|
if string.find(typeLower, "ammo pouch") then
|
|
return "ammo"
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Fallback: tooltip scanning for all bag types
|
|
local tooltip = GetScanTooltip()
|
|
tooltip:ClearLines()
|
|
tooltip:SetInventoryItem("player", invSlot)
|
|
|
|
for i = 1, tooltip:NumLines() do
|
|
local line = getglobal("GudaScanTooltipTextLeft" .. i)
|
|
if line then
|
|
local text = line:GetText()
|
|
if text then
|
|
local textLower = string.lower(text)
|
|
|
|
-- Soul Bag / Soul Pouch
|
|
if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or
|
|
(string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then
|
|
return "soul"
|
|
end
|
|
|
|
-- Herb Bag
|
|
if string.find(textLower, "herb bag") then
|
|
return "herb"
|
|
end
|
|
|
|
-- Enchanting Bag
|
|
if string.find(textLower, "enchanting bag") then
|
|
return "enchant"
|
|
end
|
|
|
|
-- Quiver
|
|
if string.find(textLower, "quiver") then
|
|
return "quiver"
|
|
end
|
|
|
|
-- Ammo Pouch
|
|
if string.find(textLower, "ammo pouch") then
|
|
return "ammo"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Simple helper to check if a bag is of a specific type
|
|
function Utils:IsBagType(bagID, bagType)
|
|
return self:GetSpecializedBagType(bagID) == bagType
|
|
end
|
|
|
|
-- Convenience wrappers for common bag type checks
|
|
function Utils:IsAmmoQuiverBag(bagID)
|
|
local bagType = self:GetSpecializedBagType(bagID)
|
|
return bagType == "quiver" or bagType == "ammo"
|
|
end
|
|
|
|
function Utils:IsHerbBag(bagID)
|
|
return self:IsBagType(bagID, "herb")
|
|
end
|
|
|
|
function Utils:IsSoulBag(bagID)
|
|
return self:IsBagType(bagID, "soul")
|
|
end
|
|
|
|
-- Get container priority for sorting (higher = more important)
|
|
function Utils:GetContainerPriority(bagID)
|
|
local bagType = self:GetSpecializedBagType(bagID)
|
|
if bagType == "enchant" then
|
|
return 50
|
|
elseif bagType == "herb" then
|
|
return 45
|
|
elseif bagType == "soul" then
|
|
return 40
|
|
elseif bagType == "quiver" then
|
|
return 30
|
|
elseif bagType == "ammo" then
|
|
return 20
|
|
else
|
|
return 10 -- Regular bag
|
|
end
|
|
end
|
|
|
|
-- Soul Shard item ID
|
|
local SOUL_SHARD_ID = 6265
|
|
|
|
-- Check if item is a Soul Shard
|
|
function Utils:IsSoulShard(itemLink)
|
|
if not itemLink then return false end
|
|
local itemID = self:ExtractItemID(itemLink)
|
|
return itemID == SOUL_SHARD_ID
|
|
end
|
|
|
|
-- Extract hyperlink from item link for tooltip scanning
|
|
local function ExtractHyperlink(itemLink)
|
|
if not itemLink then return nil end
|
|
local _, _, hyperlink = string.find(itemLink, "|H(.+)|h")
|
|
return hyperlink
|
|
end
|
|
|
|
-- Detect if a consumable has 'Use: Restores' or mentions 'while eating'/'while drinking'
|
|
function Utils:GetConsumableRestoreTag(bagID, slotID, itemLink)
|
|
if not bagID or not slotID then return nil end
|
|
|
|
-- Get item link for cache key if not provided
|
|
local cacheLink = itemLink or GetContainerItemLink(bagID, slotID)
|
|
|
|
-- Check cache first
|
|
local cacheKey = GetTooltipCacheKey(cacheLink)
|
|
if cacheKey and tooltipCache.restoreTag[cacheKey] ~= nil then
|
|
tooltipCacheStats.hits = tooltipCacheStats.hits + 1
|
|
local cached = tooltipCache.restoreTag[cacheKey]
|
|
-- Return nil if cached as false, otherwise return the tag
|
|
return cached ~= false and cached or nil
|
|
end
|
|
tooltipCacheStats.misses = tooltipCacheStats.misses + 1
|
|
|
|
local tooltip = GetScanTooltip()
|
|
tooltip:ClearLines()
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
local tag = nil
|
|
for i = 1, tooltip:NumLines() do
|
|
local line = getglobal("GudaScanTooltipTextLeft" .. i)
|
|
if line then
|
|
local text = line:GetText()
|
|
if text then
|
|
local tl = string.lower(text)
|
|
if string.find(tl, "while eating") then
|
|
tag = "eat"
|
|
break
|
|
elseif string.find(tl, "while drinking") then
|
|
tag = "drink"
|
|
break
|
|
elseif string.find(tl, "use: restores") then
|
|
tag = "restore"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cache the result (nil becomes false for cache check purposes)
|
|
if cacheKey then
|
|
tooltipCache.restoreTag[cacheKey] = tag or false
|
|
end
|
|
|
|
return tag
|
|
end
|
|
|
|
-- Check if item is Arrow or Bullet (for Quiver routing).
|
|
-- Locale-independent: also matches against the local client's translated
|
|
-- subtype strings, resolved from reference vanilla items at runtime.
|
|
function Utils:IsArrowOrBullet(itemType)
|
|
if not itemType then return false end
|
|
if itemType == "Arrow" or itemType == "Bullet" then return true end
|
|
ResolveLocalizedTypes()
|
|
return localizedProjectiles[itemType] ~= nil
|
|
end
|
|
|
|
-- Check if item is Ammo (any type - for Ammo Pouch)
|
|
function Utils:IsAmmo(itemType)
|
|
return self:IsArrowOrBullet(itemType)
|
|
end
|
|
|
|
-- Returns "arrow", "bullet", or nil for the given item subtype string.
|
|
function Utils:GetProjectileKind(itemType)
|
|
if not itemType then return nil end
|
|
if itemType == "Arrow" then return "arrow" end
|
|
if itemType == "Bullet" then return "bullet" end
|
|
ResolveLocalizedTypes()
|
|
return localizedProjectiles[itemType]
|
|
end
|
|
|
|
-- Get preferred container type for an item
|
|
-- Returns: "soul", "herb", "enchant", "quiver", "ammo", or nil
|
|
function Utils:GetItemPreferredContainer(itemLink)
|
|
if not itemLink then return nil end
|
|
|
|
-- Check for soul shards first
|
|
if self:IsSoulShard(itemLink) then
|
|
return "soul"
|
|
end
|
|
|
|
-- Get item info using utility function
|
|
local itemID = self:ExtractItemID(itemLink)
|
|
if not itemID then return nil end
|
|
|
|
local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture = self:GetItemInfoSafe(itemID)
|
|
if not itemType then return nil end
|
|
|
|
-- Locale-independent projectile routing. Don't gate on itemCategory string
|
|
-- (it's localized); the projectile kind lookup already covers both English
|
|
-- and the local client's strings via runtime-resolved reference items.
|
|
local projectileKind = self:GetProjectileKind(itemType)
|
|
if projectileKind == "arrow" then
|
|
return "quiver"
|
|
elseif projectileKind == "bullet" then
|
|
return "ammo"
|
|
end
|
|
|
|
-- Route herbs to herb bags (robust: category/subtype OR texture pattern fallback)
|
|
if self:IsHerbItem(itemLink) then
|
|
return "herb"
|
|
end
|
|
|
|
-- Route enchanting materials to enchanting bags
|
|
if self:IsEnchantingItem(itemLink) then
|
|
return "enchant"
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Check if an item is "Binds when equipped" by scanning its tooltip
|
|
-- For bank items, use itemLink since SetBagItem may not work for bank slots
|
|
function Utils:IsBindOnEquip(bagID, slotID, itemLink)
|
|
if not bagID or not slotID then return false end
|
|
|
|
-- Get item link for cache key if not provided
|
|
local cacheLink = itemLink or GetContainerItemLink(bagID, slotID)
|
|
|
|
-- Check cache first
|
|
local cacheKey = GetTooltipCacheKey(cacheLink)
|
|
if cacheKey and tooltipCache.bindOnEquip[cacheKey] ~= nil then
|
|
tooltipCacheStats.hits = tooltipCacheStats.hits + 1
|
|
return tooltipCache.bindOnEquip[cacheKey]
|
|
end
|
|
tooltipCacheStats.misses = tooltipCacheStats.misses + 1
|
|
|
|
local tooltip = GetScanTooltip()
|
|
if not tooltip then return false end
|
|
|
|
tooltip:ClearLines()
|
|
|
|
-- Try SetBagItem first (works for regular bags and bank when open)
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
|
|
local numLines = tooltip:NumLines()
|
|
|
|
-- If no lines and we have itemLink, try SetHyperlink with itemString as fallback
|
|
if (not numLines or numLines == 0) and itemLink then
|
|
-- Extract itemString from link (format: item:12345:0:0:0...)
|
|
local _, _, itemString = string.find(itemLink, "(item:%d+:%d+:%d+:%d+)")
|
|
if itemString then
|
|
tooltip:ClearLines()
|
|
tooltip:SetHyperlink(itemString)
|
|
numLines = tooltip:NumLines()
|
|
end
|
|
end
|
|
|
|
if not numLines or numLines == 0 then
|
|
if cacheKey then tooltipCache.bindOnEquip[cacheKey] = false end
|
|
return false
|
|
end
|
|
|
|
-- Check tooltip lines for "Binds when equipped"
|
|
for i = 1, numLines do
|
|
local line = getglobal("GudaScanTooltipTextLeft" .. i)
|
|
if line then
|
|
local text = line:GetText()
|
|
if text and string.find(string.lower(text), "binds when equipped") then
|
|
if cacheKey then tooltipCache.bindOnEquip[cacheKey] = true end
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
if cacheKey then tooltipCache.bindOnEquip[cacheKey] = false end
|
|
return false
|
|
end
|
|
|
|
-- Check if an item is "Unique" by scanning its tooltip
|
|
function Utils:IsUniqueItem(bagID, slotID, itemLink)
|
|
if not bagID or not slotID then return false end
|
|
|
|
-- Get item link for cache key if not provided
|
|
local cacheLink = itemLink or GetContainerItemLink(bagID, slotID)
|
|
|
|
-- Check cache first
|
|
local cacheKey = GetTooltipCacheKey(cacheLink)
|
|
if cacheKey and tooltipCache.uniqueItem[cacheKey] ~= nil then
|
|
tooltipCacheStats.hits = tooltipCacheStats.hits + 1
|
|
return tooltipCache.uniqueItem[cacheKey]
|
|
end
|
|
tooltipCacheStats.misses = tooltipCacheStats.misses + 1
|
|
|
|
local tooltip = GetScanTooltip()
|
|
if not tooltip then
|
|
if cacheKey then tooltipCache.uniqueItem[cacheKey] = false end
|
|
return false
|
|
end
|
|
|
|
tooltip:ClearLines()
|
|
|
|
-- Try SetBagItem first (works for regular bags and bank when open)
|
|
tooltip:SetBagItem(bagID, slotID)
|
|
|
|
local numLines = tooltip:NumLines()
|
|
|
|
-- If no lines and we have itemLink, try SetHyperlink with itemString as fallback
|
|
if (not numLines or numLines == 0) and itemLink then
|
|
local _, _, itemString = string.find(itemLink, "(item:%d+:%d+:%d+:%d+)")
|
|
if itemString then
|
|
tooltip:ClearLines()
|
|
tooltip:SetHyperlink(itemString)
|
|
numLines = tooltip:NumLines()
|
|
end
|
|
end
|
|
|
|
if not numLines or numLines == 0 then
|
|
if cacheKey then tooltipCache.uniqueItem[cacheKey] = false end
|
|
return false
|
|
end
|
|
|
|
-- Check tooltip lines for "Unique" (but not "Unique-Equipped")
|
|
for i = 1, numLines do
|
|
local line = getglobal("GudaScanTooltipTextLeft" .. i)
|
|
if line then
|
|
local text = line:GetText()
|
|
if text then
|
|
local textLower = string.lower(text)
|
|
-- Match "unique" but not "unique-equipped"
|
|
if textLower == "unique" or string.find(textLower, "^unique$") or string.find(textLower, "^unique%s") then
|
|
if cacheKey then tooltipCache.uniqueItem[cacheKey] = true end
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if cacheKey then tooltipCache.uniqueItem[cacheKey] = false end
|
|
return false
|
|
end
|
|
|
|
-- Check if an item is equipment (armor, weapon, or other equippable)
|
|
-- Returns: true if item is equipment, false otherwise
|
|
function Utils:IsEquipment(itemLink)
|
|
if not itemLink then return false end
|
|
|
|
local itemID = self:ExtractItemID(itemLink)
|
|
if not itemID then return false end
|
|
|
|
local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc = self:GetItemInfoSafe(itemID)
|
|
|
|
-- Check if item has an equip location (most reliable method)
|
|
if itemEquipLoc and itemEquipLoc ~= "" and itemEquipLoc ~= "INVTYPE_BAG" then
|
|
return true
|
|
end
|
|
|
|
-- Fallback: Check category (in case itemEquipLoc is not set)
|
|
if itemCategory then
|
|
local categoryLower = string.lower(itemCategory)
|
|
if categoryLower == "armor" or categoryLower == "weapon" then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Determine if an item is a herb (for routing to herb bags)
|
|
-- NEW rule (per request): require BOTH
|
|
-- 1) itemCategory == "Trade Goods"
|
|
-- 2) texture contains "INV_Misc_Herb" (case-insensitive; prefix tolerated)
|
|
function Utils:IsHerbItem(itemLink)
|
|
if not itemLink then return false end
|
|
|
|
local itemID = self:ExtractItemID(itemLink)
|
|
if not itemID then return false end
|
|
|
|
local name, _, quality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture = self:GetItemInfoSafe(itemID)
|
|
|
|
if not itemTexture then
|
|
return false
|
|
end
|
|
|
|
-- Texture path is locale-independent (English in every WoW client),
|
|
-- so we don't need to gate on the localized "Trade Goods" category.
|
|
local tex = string.lower(itemTexture)
|
|
tex = string.gsub(tex, "^interface\\\\icons\\\\", "")
|
|
|
|
if string.find(tex, "inv_misc_herb") or string.find(tex, "misc_herb") then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Determine if an item is an enchanting material (for routing to enchanting bags)
|
|
-- Rules:
|
|
-- 1) itemCategory == "Trade Goods"
|
|
-- 2) (itemSubType == "Enchanting") OR texture contains "INV_Enchant" (case-insensitive)
|
|
function Utils:IsEnchantingItem(itemLink)
|
|
if not itemLink then return false end
|
|
|
|
local itemID = self:ExtractItemID(itemLink)
|
|
if not itemID then return false end
|
|
|
|
local name, _, quality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture = self:GetItemInfoSafe(itemID)
|
|
|
|
-- Explicit English subtype (rare but possible on some servers)
|
|
if itemType == "Enchanting" or itemSubType == "Enchanting" then
|
|
return true
|
|
end
|
|
|
|
if not itemTexture then
|
|
return false
|
|
end
|
|
|
|
-- Texture path is locale-independent — primary signal for non-English clients.
|
|
local tex = string.lower(itemTexture)
|
|
tex = string.gsub(tex, "^interface\\\\icons\\\\", "")
|
|
|
|
if string.find(tex, "inv_enchant") or string.find(tex, "enchant") then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Check if a bag is Enchanting Bag
|
|
function Utils:IsEnchantBag(bagID)
|
|
return self:IsBagType(bagID, "enchant")
|
|
end |