Files
Brues c5ddc6c313 Publish resolved macro actions to ClassicAPI's C_Macro.SetMacroDisplay
Hands ClassicAPI the action this addon resolved for each macro, so the icon,
tooltip, cooldown sweep, range and usable state come from the client instead of
from replaced action-bar globals. That also reaches the drag cursor and the macro
window grid, which Lua cannot touch, and lets the client answer range for a macro
slot directly rather than borrowing a proxy slot.

- CleveRoids.useClassicAPIDisplay feature-detects C_Macro.SetMacroDisplay rather
  than checking CLASSIC_API_VERSION, which reports a dev sentinel while the API is
  unreleased. ClassicAPIMacroDisplay tells ClassicAPI we drive macro display, which
  it otherwise stands down from when it sees this addon.
- macro.actions.macroID back-references the Blizzard index, since the update loop
  walks actions objects rather than macros. nil for SuperMacro macros, which have
  no index and so cannot be published.
- PublishDisplay sends false, not nothing, when no action matched: that claims the
  macro and shows the question mark, where silence would hand it back to
  ClassicAPI's own #showtooltip parser.
- PublishAllDisplays runs once the addon is ready and after every re-parse, since
  ClassicAPI re-evaluates nothing for us. It covers macros that aren't on a bar,
  which is what keeps the macro window grid correct. Deferred past load because
  SetMacroDisplay returns false until the player is in the world.
- ReleaseDisplays hands every macro back on DisableAddon.

The per-slot ACTIONBAR_SLOT_CHANGED fan-out is kept for the non-integrated path.
Publishing repaints every slot holding the macro through the client's own notifier,
so it is redundant when the API is present, but removing it outright would leave
buttons never repainting on a ClassicAPI build without the API.
2026-09-10 14:40:16 -05:00

841 lines
30 KiB
Lua

--[[
Author: Dennis Werner Garske (DWG) / brian / Mewtiny
License: MIT License
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
-- PERFORMANCE: Upvalues for frequently called functions
local GetTime = GetTime
local GetContainerItemLink = GetContainerItemLink
local GetContainerItemInfo = GetContainerItemInfo
local GetContainerNumSlots = GetContainerNumSlots
local GetInventoryItemLink = GetInventoryItemLink
local GetInventoryItemTexture = GetInventoryItemTexture
local GetInventoryItemCount = GetInventoryItemCount
local GetItemInfo = GetItemInfo
local GetSpellInfo = GetSpellInfo
local tonumber = tonumber
local type = type
local pairs = pairs
local string_find = string.find
local string_lower = string.lower
local string_gsub = string.gsub
local table_insert = table.insert
local table_getn = table.getn
-- PERFORMANCE: Extract item name directly from link without GetItemInfo call
-- Link format: |cFFFFFFFF|Hitem:12345:0:0:0|h[Item Name]|h|r
local function GetNameFromLink(link)
if not link then return nil end
local _, _, name = string_find(link, "|h%[(.-)%]|h")
return name
end
-- Indexes all spells the current player and pet knows
function CleveRoids.IndexSpells()
local spells = {}
local i = 0
local book = 1
local bookType = CleveRoids.bookTypes[book]
local maxBooks = table.getn(CleveRoids.bookTypes)
spells[bookType] = {}
while true do
i = i + 1
-- ClassicAPI GetSpellInfo(slot, bookType) returns the spellID as the
-- 10th value, giving us slot -> spellID natively (plus name/rank/icon
-- in one call, replacing GetSpellName + GetSpellTexture).
local spellName, spellRank, texture, _, _, _, _, _, _, spellId = GetSpellInfo(i, bookType)
if not spellName then
i = 0
book = book + 1
if book > maxBooks then
break
end
bookType = CleveRoids.bookTypes[book]
spells[bookType] = {}
else
local cost, reagent, reagentId = CleveRoids.GetSpellCost(i, bookType, spellId)
if not spells[bookType][spellName] then
spells[bookType][spellName] = {
spellSlot = i,
id = spellId,
name = spellName,
bookType = bookType,
texture = texture,
cost = cost,
reagentId = reagentId,
}
end
if spellRank and not spells[bookType][spellName][spellRank] then
spells[bookType][spellName][spellRank] = {
spellSlot = i,
id = spellId,
name = spellName,
rank = spellRank,
bookType = bookType,
texture = texture,
cost = cost,
reagentId = reagentId,
}
spells[bookType][spellName].highest = spells[bookType][spellName][spellRank]
end
-- For spells with no rank string, the base entry is the highest
if not spellRank and not spells[bookType][spellName].highest then
spells[bookType][spellName].highest = spells[bookType][spellName]
end
if reagent then
CleveRoids.countedItemTypes[reagent] = true
elseif reagentId and Item then
Item:CreateFromItemID(reagentId):ContinueOnItemLoad(function()
local loadedName = C_Item.GetItemNameByID(reagentId)
if loadedName and loadedName ~= "" then
CleveRoids.countedItemTypes[loadedName] = true
end
end)
end
end
end
CleveRoids.Spells = spells
end
-- Indexes all pet spells and their action bar slots
function CleveRoids.IndexPetSpells()
local petSpells = {}
if not UnitExists("pet") then
CleveRoids.PetSpells = petSpells
return
end
for i = 1, NUM_PET_ACTION_SLOTS do
local name, subtext, texture, isToken = GetPetActionInfo(i)
if name and not isToken then
-- Store the spell name and its slot
petSpells[name] = {
slot = i,
name = name,
subtext = subtext,
texture = texture
}
end
end
CleveRoids.PetSpells = petSpells
end
-- Gets a pet spell by name
function CleveRoids.GetPetSpell(spellName)
if not spellName or not CleveRoids.PetSpells then
return nil
end
spellName = CleveRoids.Trim(spellName)
return CleveRoids.PetSpells[spellName]
end
function CleveRoids.IndexTalents()
local talents = {[1] = true}
for tab = 1, GetNumTalentTabs() do
for i = 1, GetNumTalents(tab) do
local name, _, _, _, rank = GetTalentInfo(tab, i)
if name then
talents[name] = tonumber(rank)
end
end
end
CleveRoids.Talents = talents
end
-- Lightweight equipment-only indexing for combat situations
-- Updates existing cache rather than rebuilding it
-- PERFORMANCE: Index a single equipment slot instead of all 20
-- Use when we know exactly which slot changed (e.g., from EquipBagItem or
-- PLAYER_EQUIPMENT_CHANGED). hasCurrent is the event's arg2 (does the slot now
-- hold an item); pass false to skip the API probe on a slot we know is empty.
function CleveRoids.IndexEquipSlot(inventoryID, hasCurrent)
if not inventoryID then return end
local items = CleveRoids.Items or {}
-- Clear any stale inventoryID still pointing at this slot: the item that was
-- here is now unequipped or swapped out (its real location comes from the
-- paired BAG_UPDATE rebuild). Equip changes are user-paced, so this table
-- scan is off the hot path.
for _, entry in pairs(items) do
if type(entry) == "table" and entry.inventoryID == inventoryID then
entry.inventoryID = nil
end
end
-- hasCurrent == false (arg2) => slot is now empty, nothing to add.
local itemID = hasCurrent ~= false and GetInventoryItemID("player", inventoryID)
if itemID then
local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
local count = GetInventoryItemCount("player", inventoryID)
if not items[name] then
items[name] = {
inventoryID = inventoryID,
id = itemID,
name = name,
count = count,
texture = texture,
link = itemLink,
}
items[itemID] = name
local lowerName = string.lower(name)
if lowerName ~= name then
items[lowerName] = name
end
else
items[name].inventoryID = inventoryID
items[name].count = count
end
end
end
CleveRoids.lastGetItem = nil
CleveRoids.Items = items
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
-- Rebuilt each pass: itemIDs the player owns that GetItemInfo couldn't
-- resolve yet (cold cache under ClassicAPI's async warmup). The
-- GET_ITEM_INFO_RECEIVED handler consults this so it only re-indexes for
-- our own uncached items and ignores the flood of unrelated fills (quest
-- DB scans, AH, chat-link hovers, inspects) in O(1).
local pendingItemInfo = {}
CleveRoids.pendingItemInfo = pendingItemInfo
-- PERFORMANCE: Local function references
local GetContainerNumSlots = GetContainerNumSlots
local GetContainerItemInfo = GetContainerItemInfo
local GetInventoryItemCount = GetInventoryItemCount
-- Scan bags (reverse order to prefer first stack)
for bagID = 0, NUM_BAG_SLOTS do
local numSlots = GetContainerNumSlots(bagID)
for slot = numSlots, 1, -1 do
local itemID = C_Container.GetContainerItemID(bagID, slot)
if itemID then
-- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ bagID = bagID, slotIndex = slot })
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)
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,
id = itemID,
name = name,
type = itemType,
count = count,
texture = texture,
link = itemLink,
bagSlots = {{bagID, slot}},
slotsIndex = 1,
}
items[itemID] = name
local lowerName = string_lower(name)
if lowerName ~= name then
items[lowerName] = name
end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end
end
end
end
end
-- Scan equipped items
for inventoryID = 1, 19 do
local itemID = GetInventoryItemID("player", inventoryID)
if itemID then
-- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
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)
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,
name = name,
count = count,
texture = texture,
link = itemLink,
}
items[itemID] = name
local lowerName = string_lower(name)
if lowerName ~= name then
items[lowerName] = name
end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end
end
end
end
CleveRoids.lastGetItem = nil
CleveRoids.Items = items
-- relink an item action to the item if it wasn't in inventory/bags before
for slot, actions in CleveRoids.Actions do
for i, action in actions.list do
if action.item then
action.item = CleveRoids.GetItem(action.action)
end
end
end
end
function CleveRoids.ClearSlot(slots, slot)
if slots[slot] then
slots[slots[slot]] = nil
end
slots[slot] = nil
end
-- Map a 1-based action slot to (actionType, id, name, rank) via ClassicAPI's
-- GetActionInfo, resolving spell/item/macro names from the id. actionType is
-- "SPELL" | "ITEM" | "MACRO" to match existing callers; returns nil for an
-- empty slot. Bag-instance items (no itemID from GetActionInfo) yield no name.
function CleveRoids.GetActionButtonInfo(slot)
local actionType, id = CleveRoids.ClassicAPI.GetActionInfo(slot)
if not actionType then return end
if actionType == "spell" and id then
local rank = C_Spell.GetSpellSubtext(id)
if rank == "" then rank = nil end
return "SPELL", id, C_Spell.GetSpellName(id), rank
elseif actionType == "item" and id then
local item = CleveRoids.GetItem(id)
return "ITEM", id, (item and item.name)
elseif actionType == "macro" and id then
return "MACRO", id, GetMacroInfo(id)
end
end
function CleveRoids.IndexActionSlot(slot)
if not HasAction(slot) then
CleveRoids.Actions[slot] = nil
-- When clearing a reactive slot, check if we need to find it elsewhere
local clearedReactiveName = CleveRoids.reactiveSlots[slot]
CleveRoids.ClearSlot(CleveRoids.reactiveSlots, slot)
CleveRoids.ClearSlot(CleveRoids.actionSlots, slot)
-- If we cleared a reactive spell, rescan to find it in another slot
if clearedReactiveName then
for i = 1, 120 do
if i ~= slot and HasAction(i) then
local _, _, scanName = CleveRoids.GetActionButtonInfo(i)
if scanName == clearedReactiveName then
CleveRoids.reactiveSlots[clearedReactiveName] = i
CleveRoids.reactiveSlots[i] = clearedReactiveName
break
end
end
end
end
else
local actionType, _, name, rank = CleveRoids.GetActionButtonInfo(slot)
if name then
local reactiveName = CleveRoids.reactiveSpells[name] and name
local actionSlotName = name..(rank and ("("..rank..")") or "")
if reactiveName then
-- Always update the mapping to ensure we track the correct slot
-- Clear old mapping for this spell name first
local oldSlot = CleveRoids.reactiveSlots[reactiveName]
if oldSlot and oldSlot ~= slot then
CleveRoids.reactiveSlots[oldSlot] = nil
end
CleveRoids.reactiveSlots[reactiveName] = slot
CleveRoids.reactiveSlots[slot] = reactiveName
elseif not reactiveName then
CleveRoids.ClearSlot(CleveRoids.reactiveSlots, slot)
end
if actionType == "SPELL" or actionType == "ITEM" then
if not CleveRoids.actionSlots[actionSlotName] then
CleveRoids.actionSlots[actionSlotName] = slot
CleveRoids.actionSlots[slot] = actionSlotName
end
end
end
end
local actions = CleveRoids.GetAction(slot)
CleveRoids.TestForActiveAction(actions)
CleveRoids.PublishDisplay(actions)
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
end
function CleveRoids.IndexActionBars()
for i = 1, 120 do
CleveRoids.IndexActionSlot(i)
end
end
function CleveRoids.GetSpell(text)
text = CleveRoids.Trim(text)
-- Explicit "spell:<id>" form. If the player knows the spell, reuse its cached
-- spellbook entry (full cost/cooldown/usability). FindSpellBookSlotByID
-- (ClassicAPI) resolves per-rank IDs and pet spells natively and returns the
-- same bookType string CleveRoids.Spells is keyed by. If the spell is not in
-- either book, fall back to an id-only entry that SetAction renders via
-- SetSpellByID; cost=0 keeps the downstream active-action checks nil-safe.
local _, _, spellId = string.find(text, "^spell:(%d+)$")
if spellId then
spellId = tonumber(spellId)
local slot, book = FindSpellBookSlotByID(spellId)
if slot then
local name, rank = GetSpellInfo(slot, book)
local byName = name and CleveRoids.Spells[book] and CleveRoids.Spells[book][name]
if byName then
return (rank and rank ~= "" and byName[rank]) or byName.highest or byName
end
end
return { id = spellId, texture = C_Spell.GetSpellTexture(spellId) or CleveRoids.unknownTexture, cost = 0 }
end
local rs, _, rank = string.find(text, "[^%s]%((Rank %d+)%)$")
local name = rank and string.sub(text, 1, rs) or text
for book, spells in CleveRoids.Spells do
if spells and spells[name] then
local entry = spells[name][rank or "highest"]
if entry then return entry end
-- Fallback: for spells with no rank, return base entry
if not rank and spells[name].spellSlot then
return spells[name]
end
end
end
end
function CleveRoids.GetTalent(text)
text = CleveRoids.Trim(text)
return CleveRoids.Talents[text]
end
-- PERFORMANCE: Helper functions defined once (not inline per-call)
local function makeInventoryItem(inventoryID, link, Items)
if not link then link = GetInventoryItemLink("player", inventoryID) end
if not link then return end
local itemID = GetInventoryItemID("player", inventoryID)
local name = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
local texture = GetInventoryItemTexture("player", inventoryID)
local count = GetInventoryItemCount("player", inventoryID)
local it = {
inventoryID = inventoryID,
id = itemID,
name = name,
texture = texture,
link = link,
count = count
}
if name then
Items[name] = it
local lowerName = string_lower(name)
if lowerName ~= name then
Items[lowerName] = name
end
end
if itemID then Items[itemID] = name end
return it
end
local function makeBagItem(bagID, slot, link, Items)
if not link then
link = GetContainerItemLink(bagID, slot)
end
if not link then return end
local itemID = C_Container.GetContainerItemID(bagID, slot)
local name = C_Item.GetItemName({bagID = bagID, slotIndex = slot})
local count = 0
local texture, itemCount = GetContainerItemInfo(bagID, slot)
if itemCount then count = itemCount end
local it = {
bagID = bagID,
slot = slot,
id = itemID,
name = name,
texture = texture,
link = link,
count = count,
bagSlots = { { bagID, slot } },
slotsIndex = 1
}
if name then
Items[name] = it
local lowerName = string_lower(name)
if lowerName ~= name then
Items[lowerName] = name
end
end
if itemID then Items[itemID] = name end
return it
end
function CleveRoids.GetItem(text)
if not text or text == "" then return end
-- Explicit "item:<id>" form: force resolution by item ID. Reuses the numeric
-- lookup below (equipped -> bags -> GetItemInfo), so location-aware tooltip
-- rendering (SetInventoryItem/SetBagItem) still applies when the player has it.
local _, _, prefixedId = string.find(text, "^item:(%d+)$")
if prefixedId then text = prefixedId end
local Items = CleveRoids.Items
local item = Items[text] or Items[tostring(text)]
if not item then
local lowerText = string_lower(text)
local canonicalName = Items[lowerText]
if canonicalName and type(canonicalName) == "string" then
item = Items[canonicalName]
end
end
if item then
if type(item) == "table" then
return item
else
return Items[tostring(item)]
end
end
local qid = tonumber(text)
local qname = (type(text) == "string") and string_lower(text) or nil
-- Direct inventory slot lookup by ID
if qid and qid >= 1 and qid <= 19 then
local it = makeInventoryItem(qid, nil, Items)
if it then return it end
end
-- Scan equipped items
for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv)
if link then
local itemID = GetInventoryItemID("player", inv)
if qid and itemID and qid == itemID then
return makeInventoryItem(inv, link, Items)
elseif qname then
-- PERFORMANCE: Extract name from link directly instead of GetItemInfo
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeInventoryItem(inv, link, Items)
end
end
end
end
-- Scan bags
for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do
local link = GetContainerItemLink(bag, slot)
if link then
local itemID = C_Container.GetContainerItemID(bag, slot)
if qid and itemID and qid == itemID then
return makeBagItem(bag, slot, link, Items)
elseif qname then
-- PERFORMANCE: Extract name from link directly instead of GetItemInfo
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeBagItem(bag, slot, link, Items)
end
end
end
end
end
local name, link, _, _, _, _, _, _, texture = GetItemInfo(text)
if not name then return end
local fallback = { id = text, name = name, link = link, texture = texture }
CleveRoids.Items[name] = fallback
local lowerName = string.lower(name)
if lowerName ~= name then
CleveRoids.Items[lowerName] = name
end
return fallback
end
function CleveRoids.GetNextBagSlotForUse(item, text)
if not item then return end
-- PERFORMANCE: Compare item directly instead of calling GetItem(text) again
if CleveRoids.lastGetItem == item then
if item.bagSlots and table.getn(item.bagSlots) > item.slotsIndex then
item.slotsIndex = item.slotsIndex + 1
item.bagID, item.slot = unpack(item.bagSlots[item.slotsIndex])
end
end
CleveRoids.lastGetItem = item
return item
end
--------------------------------------------------------------------------------
-- FAST ITEM LOOKUP (avoids full bag scan)
--------------------------------------------------------------------------------
-- PERFORMANCE: Fast item lookup by name only - no full scan fallback
-- Returns item table or nil. Does NOT trigger IndexItems().
-- Use this for equipment swapping where we know the exact item name.
function CleveRoids.GetItemFast(text)
if not text or text == "" then return nil end
local Items = CleveRoids.Items
if not Items then return nil end
-- Direct cache lookup
local item = Items[text]
if item and type(item) == "table" then
return item
end
-- Try lowercase
local lowerText = string_lower(text)
local canonicalName = Items[lowerText]
if canonicalName and type(canonicalName) == "string" then
item = Items[canonicalName]
if item and type(item) == "table" then
return item
end
end
-- Try as item ID
local itemId = tonumber(text)
if itemId then
local name = Items[itemId]
if name and type(name) == "string" then
item = Items[name]
if item and type(item) == "table" then
return item
end
end
end
return nil
end
-- PERFORMANCE: Quick scan for a single item by name (doesn't update full cache)
-- Only scans until item is found, then stops.
-- Optimized: extracts name from link directly instead of calling GetItemInfo.
-- Uses cache with VALIDATION - checks if item is actually at cached location before trusting it.
-- v2.18+: Uses native FindPlayerItemSlot for O(1) lookup when available.
function CleveRoids.FindItemQuick(text)
if not text or text == "" then return nil end
-- v2.18+: Try native fast lookup first (much faster than Lua scanning)
local API = CleveRoids.NampowerAPI
if API and API.features and API.features.hasFindPlayerItemSlot then
local itemInfo = API.FindItemFast(text)
if itemInfo then
-- Update cache with the found item for future lookups
local Items = CleveRoids.Items or {}
if itemInfo.name then
Items[itemInfo.name] = itemInfo
Items[string_lower(itemInfo.name)] = itemInfo.name
if itemInfo.itemId then
Items[itemInfo.itemId] = itemInfo.name
end
end
return itemInfo
end
-- Item not found via native lookup - no need to scan
return nil
end
-- Fallback: no v2.18 API, use existing cache + scan logic
local Items = CleveRoids.Items or {}
local qid = tonumber(text)
-- Only compute lowercase name if we're NOT searching by ID
local qname = (not qid) and string_lower(text) or nil
-- Try cache first, but VALIDATE the cached location is still correct
local cached = CleveRoids.GetItemFast(text)
if cached then
-- Validate: check if item is actually at the cached location
if cached.inventoryID then
if qid then
if GetInventoryItemID("player", cached.inventoryID) == qid then
cached._validated = true
return cached -- Cache is valid
end
else
local nm = C_Item.GetItemName({ equipmentSlotIndex = cached.inventoryID })
if nm and qname and string_lower(nm) == qname then
cached._validated = true
return cached -- Cache is valid
end
end
-- Cache is stale - item not at cached equipped slot, invalidate
if cached.name then
Items[cached.name] = nil
Items[string_lower(cached.name)] = nil
end
elseif cached.bagID and cached.slot then
if qid then
if C_Container.GetContainerItemID(cached.bagID, cached.slot) == qid then
cached._validated = true
return cached -- Cache is valid
end
else
local nm = C_Item.GetItemName({ bagID = cached.bagID, slotIndex = cached.slot })
if nm and qname and string_lower(nm) == qname then
cached._validated = true
return cached -- Cache is valid
end
end
-- Cache is stale - item not at cached bag slot, invalidate
if cached.name then
Items[cached.name] = nil
Items[string_lower(cached.name)] = nil
end
end
end
-- Cache miss or stale - do a fresh scan
-- Quick scan equipped items first (only 19 slots)
for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv)
if link then
local itemID = GetInventoryItemID("player", inv)
-- ID match: fast path
if qid and qid == itemID then
return makeInventoryItem(inv, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeInventoryItem(inv, link, Items)
end
end
end
end
-- Quick scan bags - stop as soon as found
for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do
local link = GetContainerItemLink(bag, slot)
if link then
local itemID = C_Container.GetContainerItemID(bag, slot)
-- ID match: fast path
if qid and qid == itemID then
return makeBagItem(bag, slot, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeBagItem(bag, slot, link, Items)
end
end
end
end
end
return nil
end
-- PERFORMANCE: Check if item is already equipped in slot (fast path)
-- Returns true if the item (by name or ID) is already in the specified slot
function CleveRoids.IsItemEquipped(text, inventoryId)
if not text or not inventoryId then return false end
local currentID = GetInventoryItemID("player", inventoryId)
if not currentID then return false end
-- Check by ID (fast path)
local textId = tonumber(text)
if textId and textId == currentID then
return true
end
-- Check by name (decorated, so suffixed gear still matches)
local currentName = C_Item.GetItemName({ equipmentSlotIndex = inventoryId })
if currentName and string_lower(currentName) == string_lower(text) then
return true
end
return false
end
local Extension = CleveRoids.RegisterExtension("Generic_show")
Extension.RegisterEvent("SPELLS_CHANGED", "SPELLS_CHANGED")
function Extension.OnLoad()
end
function Extension.SPELLS_CHANGED()
end