From 2fd4ac3298ec7b84ef71327696c563056854f41b Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Fri, 12 Dec 2025 15:43:21 -0500 Subject: [PATCH] reduce bag event usage for lag and fix macro calling within macros --- Compatibility/SuperMacro.lua | 2 +- Conditionals.lua | 268 +++++++++++++++++---------------- Core.lua | 165 +++++++++++++++----- Extensions/Tooltip/Generic.lua | 76 ++++++++-- 4 files changed, 325 insertions(+), 186 deletions(-) diff --git a/Compatibility/SuperMacro.lua b/Compatibility/SuperMacro.lua index 6b02d77..f2d1adf 100644 --- a/Compatibility/SuperMacro.lua +++ b/Compatibility/SuperMacro.lua @@ -105,7 +105,7 @@ do if type(fn) == "function" then local b2, e2 = string.find(text, "^%s*/" .. k .. "%s+[!%[%{%?~]") if b2 then - local msg2 = string.gsub(string.sub(text, e2 + 1), "^%s+", "") + local msg2 = string.gsub(string.sub(text, e2), "^%s+", "") pcall(fn, msg2) return true -- IMPORTANT end diff --git a/Conditionals.lua b/Conditionals.lua index b2c6ddd..ab4974c 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -164,6 +164,112 @@ function CleveRoids.InvalidateEquipmentCache() _equipmentCacheValid = false end +-- ============================================================================ +-- PERFORMANCE: Unified item location lookup using CleveRoids.Items cache +-- Returns: { type="inventory"|"bag", inventoryID=N } or { type="bag", bag=N, slot=N } +-- Returns nil if item not found +-- ============================================================================ +local string_lower = string.lower +local string_find = string.find + +-- Fast item lookup using cache - O(1) instead of O(n) scan +-- @param item: item ID (number) or item name (string) +-- @return table with location info, or nil if not found +function CleveRoids.FindItemLocation(item) + local Items = CleveRoids.Items + if not Items then return nil end + + local itemData = nil + + -- Case 1: Numeric item ID + local numericItem = tonumber(item) + if numericItem then + -- Check if it's an equipment slot (1-19) + if numericItem >= 1 and numericItem <= 19 then + local link = GetInventoryItemLink("player", numericItem) + if link then + return { type = "inventory", inventoryID = numericItem } + end + return nil + end + + -- Look up by item ID in cache (Items[id] = name) + local itemName = Items[numericItem] + if itemName then + itemData = Items[itemName] + end + else + -- Case 2: String item name + if type(item) == "string" and item ~= "" then + -- Try exact match first, then lowercase + itemData = Items[item] + if not itemData or type(itemData) == "string" then + local lowerItem = string_lower(item) + local resolved = Items[lowerItem] + if type(resolved) == "string" then + itemData = Items[resolved] + elseif type(resolved) == "table" then + itemData = resolved + end + elseif type(itemData) == "string" then + -- Resolve indirection (lowercase -> canonical name) + itemData = Items[itemData] + end + end + end + + if not itemData or type(itemData) ~= "table" then + return nil + end + + -- Return location info + if itemData.inventoryID then + return { type = "inventory", inventoryID = itemData.inventoryID, itemData = itemData } + elseif itemData.bagID and itemData.slot then + return { type = "bag", bag = itemData.bagID, slot = itemData.slot, itemData = itemData } + end + + return nil +end + +-- Fast item existence check using cache +-- @param item: item ID (number) or item name (string) +-- @return boolean +function CleveRoids.HasItemCached(item) + return CleveRoids.FindItemLocation(item) ~= nil +end + +-- Fast item cooldown lookup using cache +-- @param item: item ID (number) or item name (string) +-- @return remainingSeconds, totalDuration, enabled +function CleveRoids.GetItemCooldownCached(item) + local location = CleveRoids.FindItemLocation(item) + if not location then + return 0, 0, 0 + end + + local start, duration, enable + if location.type == "inventory" then + start, duration, enable = GetInventoryItemCooldown("player", location.inventoryID) + else + start, duration, enable = GetContainerItemCooldown(location.bag, location.slot) + end + + -- Normalize cooldown values + start = tonumber(start) or 0 + duration = tonumber(duration) or 0 + enable = tonumber(enable) or 0 + + if duration <= 0 or start <= 0 then + return 0, 0, enable + end + + local remaining = (start + duration) - GetTime() + if remaining < 0 then remaining = 0 end + + return remaining, duration, enable +end + --This table maps stat keys to the functions that retrieve their values. local stat_checks = { -- Base Stats (Corrected to use the 'effective' stat with gear) @@ -1862,82 +1968,35 @@ end -- Check if an item exists in bags or equipped -- Returns: true if found, false otherwise +-- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup, with fallback for substring matches function CleveRoids.HasItem(item) - -- Case A: numeric value passed - local numericItem = tonumber(item) - if numericItem then - -- Is it an inventory slot? - if numericItem >= 1 and numericItem <= 19 then - local link = GetInventoryItemLink("player", numericItem) - return link ~= nil - end - - -- Otherwise, treat it as an item ID - search equipped items first - for slot = 0, 19 do - local link = GetInventoryItemLink("player", slot) - if link then - local _, _, id = string.find(link, "item:(%d+)") - if id and tonumber(id) == numericItem then - return true - end - end - end - - -- Then search bags for the item ID - for bag = 0, 4 do - local size = GetContainerNumSlots(bag) - if size and size > 0 then - for slotIndex = 1, size do - local link = GetContainerItemLink(bag, slotIndex) - if link then - local _, _, id = string.find(link, "item:(%d+)") - if id and tonumber(id) == numericItem then - return true - end - end - end - end - end - - -- Item ID not found - return false + -- Fast path: check cache first (O(1) lookup) + if CleveRoids.HasItemCached(item) then + return true end - -- Case B: string item name -> try equipped slots first + -- Slow path fallback: only needed for substring matching on strings + -- The cache handles exact matches by ID and name, but not partial/substring matches if type(item) == "string" and item ~= "" then local itemLower = string.lower(item) - -- Check equipped slots - local slots = { 13, 14, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19 } -- trinkets first - for i = 1, table.getn(slots) do - local s = slots[i] - local link = GetInventoryItemLink("player", s) + -- Check equipped slots for substring match + for slot = 0, 19 do + local link = GetInventoryItemLink("player", slot) if link then - -- Extract item name from link using pattern [ItemName] - local _, _, linkName = string.find(link, "%[(.+)%]") - if linkName and string.lower(linkName) == itemLower then - return true - end - -- Fallback: simple substring match if string.find(string.lower(link), itemLower, 1, true) then return true end end end - -- Search bags for the named item + -- Check bags for substring match for bag = 0, 4 do local size = GetContainerNumSlots(bag) if size and size > 0 then for slotIndex = 1, size do local link = GetContainerItemLink(bag, slotIndex) if link then - -- Extract item name from link using pattern [ItemName] - local _, _, linkName = string.find(link, "%[(.+)%]") - if linkName and string.lower(linkName) == itemLower then - return true - end - -- Fallback: simple substring match if string.find(string.lower(link), itemLower, 1, true) then return true end @@ -1947,16 +2006,15 @@ function CleveRoids.HasItem(item) end end - -- Item not found return false end -- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed -- Hardened item cooldown resolver (Vanilla 1.12.1 / Lua 5.0) -- Returns: remainingSeconds, totalDuration, enabled +-- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup, with fallback for substring matches function CleveRoids.GetItemCooldown(item) - local start, duration, enable = nil, nil, nil - + -- Helper to normalize cooldown values local function _norm(s, d, e) s = tonumber(s) or 0 d = tonumber(d) or 0 @@ -1969,94 +2027,44 @@ function CleveRoids.GetItemCooldown(item) return rem, d, e end - -- Helper: Check if a numeric value is a valid inventory slot (1-19) - local function _isInventorySlot(num) - return num and num >= 1 and num <= 19 + -- Fast path: check cache first (O(1) lookup) + local remaining, duration, enable = CleveRoids.GetItemCooldownCached(item) + if duration > 0 or remaining > 0 then + return remaining, duration, enable end - -- Case A: numeric value passed - local numericItem = tonumber(item) - if numericItem then - -- Is it an inventory slot? - if _isInventorySlot(numericItem) then - start, duration, enable = GetInventoryItemCooldown("player", numericItem) - return _norm(start, duration, enable) - end + -- If cache found the item but cooldown is 0, that's a valid result + local location = CleveRoids.FindItemLocation(item) + if location then + return 0, 0, enable or 0 + end - -- Otherwise, treat it as an item ID - search equipped items first + -- Slow path fallback: only needed for substring matching on strings + if type(item) == "string" and item ~= "" then + local itemLower = string.lower(item) + local start, dur, en + + -- Check equipped slots for substring match for slot = 0, 19 do local link = GetInventoryItemLink("player", slot) if link then - local _, _, id = string.find(link, "item:(%d+)") - if id and tonumber(id) == numericItem then - start, duration, enable = GetInventoryItemCooldown("player", slot) - return _norm(start, duration, enable) - end - end - end - - -- Then search bags for the item ID - for bag = 0, 4 do - local size = GetContainerNumSlots(bag) - if size and size > 0 then - for slotIndex = 1, size do - local link = GetContainerItemLink(bag, slotIndex) - if link then - local _, _, id = string.find(link, "item:(%d+)") - if id and tonumber(id) == numericItem then - start, duration, enable = GetContainerItemCooldown(bag, slotIndex) - return _norm(start, duration, enable) - end - end - end - end - end - - -- Item ID not found in inventory or bags - return 0, 0, 0 - end - - -- Case B: string item name -> try equipped slots first - if type(item) == "string" and item ~= "" then - local itemLower = string.lower(item) - - -- scan a few common equipment slots; expand if your engine needs more - local slots = { 13, 14, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19 } -- trinkets first - for i = 1, table.getn(slots) do - local s = slots[i] - local link = GetInventoryItemLink("player", s) - if link then - -- Extract item name from link using pattern [ItemName] - local _, _, linkName = string.find(link, "%[(.+)%]") - if linkName and string.lower(linkName) == itemLower then - start, duration, enable = GetInventoryItemCooldown("player", s) - return _norm(start, duration, enable) - end - -- Fallback: simple substring match if string.find(string.lower(link), itemLower, 1, true) then - start, duration, enable = GetInventoryItemCooldown("player", s) - return _norm(start, duration, enable) + start, dur, en = GetInventoryItemCooldown("player", slot) + return _norm(start, dur, en) end end end - -- Case C: search bags for the named item + -- Check bags for substring match for bag = 0, 4 do local size = GetContainerNumSlots(bag) if size and size > 0 then for slotIndex = 1, size do local link = GetContainerItemLink(bag, slotIndex) if link then - -- Extract item name from link using pattern [ItemName] - local _, _, linkName = string.find(link, "%[(.+)%]") - if linkName and string.lower(linkName) == itemLower then - start, duration, enable = GetContainerItemCooldown(bag, slotIndex) - return _norm(start, duration, enable) - end - -- Fallback: simple substring match if string.find(string.lower(link), itemLower, 1, true) then - start, duration, enable = GetContainerItemCooldown(bag, slotIndex) - return _norm(start, duration, enable) + start, dur, en = GetContainerItemCooldown(bag, slotIndex) + return _norm(start, dur, en) end end end diff --git a/Core.lua b/Core.lua index ff95666..d330202 100644 --- a/Core.lua +++ b/Core.lua @@ -523,34 +523,74 @@ local function CRM_GetBagScanTip() end -- Return total in *bags only* (not bank), by id when possible; fallback to name +-- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup when available function CleveRoids.GetReagentCount(reagentName) if not reagentName or reagentName == "" then return 0 end + -- Fast path: check cache first + local Items = CleveRoids.Items + if Items then + local wantId = _ReagentIdByName[reagentName] + + -- Try by ID first if we have a mapping + if wantId then + local itemName = Items[wantId] + if itemName then + local itemData = Items[itemName] + if itemData and itemData.count and not itemData.inventoryID then + -- Item is in bags (not equipped), return cached count + return itemData.count + end + end + end + + -- Try by name + local itemData = Items[reagentName] + if type(itemData) == "string" then + itemData = Items[itemData] -- Resolve indirection + end + if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then + return itemData.count + end + + -- Try lowercase + local lowerName = string.lower(reagentName) + local resolved = Items[lowerName] + if type(resolved) == "string" then + itemData = Items[resolved] + elseif type(resolved) == "table" then + itemData = resolved + end + if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then + return itemData.count + end + end + + -- Slow path fallback: full bag scan (only when cache miss) local wantId = _ReagentIdByName[reagentName] local total = 0 for bag = 0, 4 do local slots = GetContainerNumSlots(bag) or 0 for slot = 1, slots do - local _, count = GetContainerItemInfo(bag, slot) - count = count or 0 - -- Prefer link → id when available - local link = (GetContainerItemLink and GetContainerItemLink(bag, slot)) or nil + local _, count = GetContainerItemInfo(bag, slot) + count = count or 0 + local link = GetContainerItemLink and GetContainerItemLink(bag, slot) if link then - local _, _, idstr = string.find(link, "item:(%d+)") - local id = idstr and tonumber(idstr) or nil + local _, _, idstr = string.find(link, "item:(%d+)") + local id = idstr and tonumber(idstr) or nil if (wantId and id == wantId) or (not wantId and string.find(link, "%["..reagentName.."%]")) then - total = total + (count or 0) + total = total + count end else - -- Fallback: scan bag slot tooltip for the name + -- Fallback: scan bag slot tooltip for the name (expensive, only when no link) local tip = CRM_GetBagScanTip() tip:ClearLines() tip:SetBagItem(bag, slot) local left1 = _G[tip:GetName().."TextLeft1"] local name = left1 and left1:GetText() if name and name == reagentName then - total = total + (count or 0) + total = total + count end end end @@ -2410,44 +2450,28 @@ function CleveRoids.DoUse(msg) return end - -- NEW: Try to interpret as an item ID (numeric but > 19) - if slotId and slotId > 19 then - -- Search equipped slots for this item ID - for slot = 0, 19 do - local link = GetInventoryItemLink("player", slot) - if link then - local _, _, id = string.find(link, "item:(%d+)") - if id and tonumber(id) == slotId then - ClearCursor() - UseInventoryItem(slot) - return - end + -- PERFORMANCE: Try cache lookup first (O(1) instead of O(n) scan) + local location = CleveRoids.FindItemLocation(msg) + if location then + ClearCursor() + if location.type == "inventory" then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. location.inventoryID .. ") [cached]|r") end - end - - -- Search bags for this item ID - for bag = 0, 4 do - local size = GetContainerNumSlots(bag) or 0 - for bagSlot = 1, size do - local link = GetContainerItemLink(bag, bagSlot) - if link then - local _, _, id = string.find(link, "item:(%d+)") - if id and tonumber(id) == slotId then - ClearCursor() - UseContainerItem(bag, bagSlot) - return - end - end + UseInventoryItem(location.inventoryID) + else + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. location.bag .. "," .. location.slot .. ") [cached]|r") end + UseContainerItem(location.bag, location.slot) end - - -- Item ID not found return end - -- Search equipped inventory slots first (for trinkets, etc.) + -- Slow path fallback: full scan for substring matches or cache miss local qname = string_lower(msg) + -- Search equipped inventory slots first (for trinkets, etc.) for slot = 0, 19 do local link = GetInventoryItemLink("player", slot) if link then @@ -2463,7 +2487,7 @@ function CleveRoids.DoUse(msg) end end - -- Then search bags - no cache, no Nampower API (causes crashes) + -- Then search bags for bag = 0, 4 do local numSlots = GetContainerNumSlots(bag) or 0 for bagSlot = 1, numSlots do @@ -2472,7 +2496,7 @@ function CleveRoids.DoUse(msg) local _, _, nm = string_find(link, "|h%[(.-)%]|h") if nm and string_lower(nm) == qname then if CleveRoids.equipDebugLog then - CleveRoids.Print("|cff888888[EquipLog] /equip " .. msg .. " via UseContainerItem(" .. bag .. "," .. bagSlot .. ")|r") + CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. bag .. "," .. bagSlot .. ")|r") end ClearCursor() UseContainerItem(bag, bagSlot) @@ -2813,6 +2837,12 @@ function CleveRoids.OnUpdate(self) return end + -- PERFORMANCE: Delayed WDB warmup after login (ensures GetItemInfo works after WDB clear) + if CleveRoids.wdbWarmupTime and time >= CleveRoids.wdbWarmupTime then + CleveRoids.wdbWarmupTime = nil + CleveRoids.DoWDBWarmup() + end + -- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division) local refreshRate = CleveRoids.cachedRefreshRate if not refreshRate then @@ -3550,6 +3580,59 @@ function CleveRoids.Frame:PLAYER_LOGIN() CleveRoids.IndexPetSpells() CleveRoids.initializationTimer = GetTime() + 1.5 CRM_SM_InstallHook() + + -- Schedule delayed WDB warmup (loads items into client cache via tooltip scan) + -- This ensures GetItemInfo() works for all inventory items after a WDB clear + CleveRoids.wdbWarmupTime = GetTime() + 3.0 -- 3 second delay after login +end + +-- PERFORMANCE: WDB warmup - tooltip scan all bag items to ensure they're cached +-- This prevents GetItemInfo() returning nil for items after a WDB clear +function CleveRoids.DoWDBWarmup() + if CleveRoids.wdbWarmupDone then return end + CleveRoids.wdbWarmupDone = true + + -- Create a hidden tooltip for scanning if it doesn't exist + local tip = CleveRoidsWDBTip + if not tip then + tip = CreateFrame("GameTooltip", "CleveRoidsWDBTip", UIParent, "GameTooltipTemplate") + tip:SetOwner(WorldFrame, "ANCHOR_NONE") + end + + local scanned = 0 + + -- Scan all bag slots + for bag = 0, 4 do + local slots = GetContainerNumSlots(bag) or 0 + for slot = 1, slots do + local link = GetContainerItemLink(bag, slot) + if link then + -- Tooltip scan loads the item into WDB + tip:ClearLines() + tip:SetBagItem(bag, slot) + scanned = scanned + 1 + end + end + end + + -- Scan equipped items + for slot = 0, 19 do + local link = GetInventoryItemLink("player", slot) + if link then + tip:ClearLines() + tip:SetInventoryItem("player", slot) + scanned = scanned + 1 + end + end + + -- Now trigger a full item index to populate the cache with valid data + if CleveRoids.IndexItems then + CleveRoids.IndexItems() + end + + if CleveRoids.debug then + CleveRoids.Print("|cff88ff88[WDB Warmup]|r Scanned " .. scanned .. " items into cache") + end end function CleveRoids.Frame:ADDON_LOADED(addon) diff --git a/Extensions/Tooltip/Generic.lua b/Extensions/Tooltip/Generic.lua index b2ee8aa..ac82d12 100644 --- a/Extensions/Tooltip/Generic.lua +++ b/Extensions/Tooltip/Generic.lua @@ -219,10 +219,44 @@ function CleveRoids.IndexEquipSlot(inventoryID) end +-- PERFORMANCE: Persistent cache for GetItemInfo results (survives between IndexItems calls) +-- This prevents repeated GetItemInfo calls for the same itemID +local _itemInfoCache = {} +local _itemInfoCacheSize = 0 +local _ITEM_INFO_CACHE_MAX = 500 -- Limit cache size + +local function GetCachedItemInfo(itemID) + local cached = _itemInfoCache[itemID] + if cached then + return cached.name, cached.link, cached.quality, cached.level, cached.type, cached.subType, cached.stackCount, cached.equipLoc, cached.texture + end + + local name, link, quality, level, itemType, subType, stackCount, equipLoc, texture = GetItemInfo(itemID) + if name then + -- Cache the result + if _itemInfoCacheSize < _ITEM_INFO_CACHE_MAX then + _itemInfoCache[itemID] = { + name = name, link = link, quality = quality, level = level, + type = itemType, subType = subType, stackCount = stackCount, + equipLoc = equipLoc, texture = texture + } + _itemInfoCacheSize = _itemInfoCacheSize + 1 + end + end + return name, link, quality, level, itemType, subType, stackCount, equipLoc, texture +end + function CleveRoids.IndexItems() local items = {} local NUM_BAG_SLOTS = NUM_BAG_SLOTS -- Upvalue for bag constant + -- PERFORMANCE: Local function references + local GetContainerNumSlots = GetContainerNumSlots + local GetContainerItemLink = GetContainerItemLink + local GetContainerItemInfo = GetContainerItemInfo + local GetInventoryItemLink = GetInventoryItemLink + local GetInventoryItemCount = GetInventoryItemCount + -- Scan bags (reverse order to prefer first stack) for bagID = 0, NUM_BAG_SLOTS do local numSlots = GetContainerNumSlots(bagID) @@ -230,12 +264,22 @@ function CleveRoids.IndexItems() local link = GetContainerItemLink(bagID, slot) if link then local _, _, itemID = string_find(link, "item:(%d+)") - local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID) - if name then + -- PERFORMANCE: Try to extract name from link first to check for duplicates + local _, _, linkName = string_find(link, "%[(.+)%]") + local existing = linkName and items[linkName] + + if existing then + -- Item already seen - just update count and add bag slot (skip GetItemInfo) local _, count = GetContainerItemInfo(bagID, slot) - local existing = items[name] - if not existing then + existing.count = (existing.count or 0) + (count or 0) + table_insert(existing.bagSlots, {bagID, slot}) + else + -- New item - need full info + local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetCachedItemInfo(itemID) + + if name then + local _, count = GetContainerItemInfo(bagID, slot) items[name] = { bagID = bagID, slot = slot, @@ -253,9 +297,6 @@ function CleveRoids.IndexItems() if lowerName ~= name then items[lowerName] = name end - else - existing.count = (existing.count or 0) + (count or 0) - table_insert(existing.bagSlots, {bagID, slot}) end end end @@ -267,11 +308,21 @@ function CleveRoids.IndexItems() local link = GetInventoryItemLink("player", inventoryID) if link then local _, _, itemID = string_find(link, "item:(%d+)") - local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID) - if name then + + -- PERFORMANCE: Try to extract name from link first + local _, _, linkName = string_find(link, "%[(.+)%]") + local existing = linkName and items[linkName] + + if existing then + -- Item in bags AND equipped - update existing entry + existing.inventoryID = inventoryID local count = GetInventoryItemCount("player", inventoryID) - local existing = items[name] - if not existing then + existing.count = (existing.count or 0) + (count or 0) + else + -- New equipped-only item + local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetCachedItemInfo(itemID) + if name then + local count = GetInventoryItemCount("player", inventoryID) items[name] = { inventoryID = inventoryID, id = itemID, @@ -285,9 +336,6 @@ function CleveRoids.IndexItems() if lowerName ~= name then items[lowerName] = name end - else - existing.inventoryID = inventoryID - existing.count = (existing.count or 0) + (count or 0) end end end