diff --git a/Conditionals.lua b/Conditionals.lua index 614a0e1..6ce8683 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -1388,6 +1388,74 @@ function CleveRoids.GetPlayerAura(index, isbuff) return GetPlayerBuffTexture(bid), GetPlayerBuffApplications(bid), spellID, GetPlayerBuffTimeLeft(bid) end +-- PERFORMANCE: Local function refs and reusable pattern for buff checking +local _string_lower = string.lower +local _string_gsub = string.gsub +local _RANK_PATTERN = "%s*%(%s*Rank%s+%d+%s*%)" + +-- PERFORMANCE: Simple cache for lowercase spell names (cleared periodically) +local _spellNameCache = {} +local _spellNameCacheSize = 0 +local _MAX_SPELL_CACHE = 200 + +-- PERFORMANCE: Cache for base spell names (rank stripped, not lowercased) +local _baseNameCache = {} +local _baseNameCacheSize = 0 + +local function GetLowercaseSpellName(spellID) + local cached = _spellNameCache[spellID] + if cached then return cached end + + local name = SpellInfo(spellID) + if not name then return nil end + + -- Strip rank and lowercase + local baseName = _string_gsub(name, _RANK_PATTERN, "") + local lowerName = _string_lower(baseName) + + -- Cache if not too large + if _spellNameCacheSize < _MAX_SPELL_CACHE then + _spellNameCache[spellID] = lowerName + _spellNameCacheSize = _spellNameCacheSize + 1 + end + + return lowerName +end + +-- PERFORMANCE: Get base spell name (rank stripped) and full name - cached +local function GetSpellNames(spellID) + local cached = _baseNameCache[spellID] + if cached then + return cached.base, cached.full + end + + local fullName = SpellInfo(spellID) + if not fullName then return nil, nil end + + local baseName = _string_gsub(fullName, _RANK_PATTERN, "") + + -- Cache if not too large + if _baseNameCacheSize < _MAX_SPELL_CACHE then + _baseNameCache[spellID] = { base = baseName, full = fullName } + _baseNameCacheSize = _baseNameCacheSize + 1 + end + + return baseName, fullName +end + +-- Clear spell name caches (called periodically from Core.lua cleanup) +function CleveRoids.ClearSpellNameCaches() + for k in pairs(_spellNameCache) do + _spellNameCache[k] = nil + end + _spellNameCacheSize = 0 + + for k in pairs(_baseNameCache) do + _baseNameCache[k] = nil + end + _baseNameCacheSize = 0 +end + function CleveRoids.ValidateAura(unit, args, isbuff) if not args or not UnitExists(unit) then return false end @@ -1405,7 +1473,7 @@ function CleveRoids.ValidateAura(unit, args, isbuff) local i = isPlayer and 0 or 1 -- PERFORMANCE: Cache lowercased search name to avoid repeated string.lower calls - local searchName = args.name and string.lower(args.name) + local searchName = args.name and _string_lower(args.name) -- Primary search: BUFFS if isbuff==true, DEBUFFS if isbuff==false while true do @@ -1429,14 +1497,11 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if not texture then break end if current_spellID and searchName then - local auraName = SpellInfo(current_spellID) - if auraName then - -- Strip rank for comparison (handles "Moonfire (Rank 7)" vs "moonfire") - local baseName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "") - if string.lower(baseName) == searchName or string.lower(auraName) == searchName then - found = true - break - end + -- PERFORMANCE: Use cached lowercase spell name lookup + local lowerName = GetLowercaseSpellName(current_spellID) + if lowerName and lowerName == searchName then + found = true + break end end @@ -1455,14 +1520,11 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if not texture then break end if current_spellID then - local auraName = SpellInfo(current_spellID) - if auraName then - -- Strip rank for comparison - local baseName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "") - if string.lower(baseName) == searchName or string.lower(auraName) == searchName then - found = true - break - end + -- PERFORMANCE: Use cached lowercase spell name lookup + local lowerName = GetLowercaseSpellName(current_spellID) + if lowerName and lowerName == searchName then + found = true + break end end @@ -1635,18 +1697,16 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not tex then break end if debuffSpellID then - local fullName = SpellInfo(debuffSpellID) - if fullName then - local baseName = string.gsub(fullName, "%s*%(%s*Rank%s+%d+%s*%)", "") - if baseName == args.name or fullName == args.name then - found = true - texture = tex - stacks = debuffStacks or 0 - spellID = debuffSpellID - -- No duration tracking for shared debuffs - remaining = nil - break - end + -- PERFORMANCE: Use cached spell name lookup + local baseName, fullName = GetSpellNames(debuffSpellID) + if baseName and (baseName == args.name or fullName == args.name) then + found = true + texture = tex + stacks = debuffStacks or 0 + spellID = debuffSpellID + -- No duration tracking for shared debuffs + remaining = nil + break end end end @@ -1658,17 +1718,15 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not tex then break end if buffSpellID then - local fullName = SpellInfo(buffSpellID) - if fullName then - local baseName = string.gsub(fullName, "%s*%(%s*Rank%s+%d+%s*%)", "") - if baseName == args.name or fullName == args.name then - found = true - texture = tex - stacks = buffStacks or 0 - spellID = buffSpellID - remaining = nil - break - end + -- PERFORMANCE: Use cached spell name lookup + local baseName, fullName = GetSpellNames(buffSpellID) + if baseName and (baseName == args.name or fullName == args.name) then + found = true + texture = tex + stacks = buffStacks or 0 + spellID = buffSpellID + remaining = nil + break end end end @@ -1683,10 +1741,9 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not texture then break end if CleveRoids.hasSuperwow then - local fullName = SpellInfo(spellID) - -- Strip rank for comparison (handles "Moonfire (Rank 7)" vs "Moonfire") - local baseName = string.gsub(fullName or "", "%s*%(%s*Rank%s+%d+%s*%)", "") - if baseName == args.name or fullName == args.name then + -- PERFORMANCE: Use cached spell name lookup + local baseName, fullName = GetSpellNames(spellID) + if baseName and (baseName == args.name or fullName == args.name) then found = true break end @@ -1705,10 +1762,9 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not texture then break end if CleveRoids.hasSuperwow then - local fullName = SpellInfo(spellID) - -- Strip rank for comparison - local baseName = string.gsub(fullName or "", "%s*%(%s*Rank%s+%d+%s*%)", "") - if baseName == args.name or fullName == args.name then + -- PERFORMANCE: Use cached spell name lookup + local baseName, fullName = GetSpellNames(spellID) + if baseName and (baseName == args.name or fullName == args.name) then found = true break end diff --git a/Core.lua b/Core.lua index b36e49a..c9f8303 100644 --- a/Core.lua +++ b/Core.lua @@ -827,14 +827,6 @@ function CleveRoids.TestForActiveAction(actions) local previousOom = actions.active.oom local previousInRange = actions.active.inRange - -- Debug: Log when we're checking a reactive ability - if CleveRoids.debug and actions.active.isReactive then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ffff[TestForActiveAction]|r Checking reactive spell: %s", - tostring(actions.active.action)) - ) - end - if actions.active.spell then -- Default to -1 (unknown) so we fall through to proxy slot or original function -- Only set to 1/0 when we have a definitive answer from IsSpellInRange @@ -2522,11 +2514,37 @@ function CleveRoids.DoUse(msg) end -- Try to interpret as item ID (numbers > 19) - -- Resolve item ID to item name using available APIs + -- v2.18+: Use FindPlayerItemSlot directly for item IDs (no name resolution needed) if slotId and slotId > 19 then - local itemName = nil - -- Try Nampower API first (most reliable) local API = CleveRoids.NampowerAPI + -- v2.18+: Native lookup can find item directly by ID + if API and API.features and API.features.hasFindPlayerItemSlot then + local itemInfo = API.FindItemFast(slotId) + if itemInfo then + ClearCursor() + if itemInfo.inventoryID then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[UseLog] /use " .. slotId .. " via UseInventoryItem(" .. itemInfo.inventoryID .. ") [v2.18 ID lookup]|r") + end + UseInventoryItem(itemInfo.inventoryID) + return + elseif itemInfo.bagID and itemInfo.slot then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[UseLog] /use " .. slotId .. " via UseContainerItem(" .. itemInfo.bagID .. "," .. itemInfo.slot .. ") [v2.18 ID lookup]|r") + end + UseContainerItem(itemInfo.bagID, itemInfo.slot) + return + end + end + -- Item not found by ID - fail + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cffff8800[UseLog] Item ID " .. slotId .. " not found in inventory [v2.18]|r") + end + return + end + + -- Fallback: Resolve item ID to name for legacy lookup + local itemName = nil if API and API.GetItemName then itemName = API.GetItemName(slotId) end @@ -2548,6 +2566,30 @@ function CleveRoids.DoUse(msg) end end + -- v2.18+: Use native fast lookup (much faster than Lua cache + scan) + local API = CleveRoids.NampowerAPI + if API and API.features and API.features.hasFindPlayerItemSlot then + local itemInfo = API.FindItemFast(msg) + if itemInfo then + ClearCursor() + if itemInfo.inventoryID then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. itemInfo.inventoryID .. ") [v2.18 native]|r") + end + UseInventoryItem(itemInfo.inventoryID) + return + elseif itemInfo.bagID and itemInfo.slot then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. itemInfo.bagID .. "," .. itemInfo.slot .. ") [v2.18 native]|r") + end + UseContainerItem(itemInfo.bagID, itemInfo.slot) + return + end + end + -- v2.18 lookup didn't find item - fall through to legacy path + -- (might be partial match or different case that native doesn't handle) + end + -- PERFORMANCE: Try cache lookup first (O(1) instead of O(n) scan) -- IMPORTANT: Validate cache hits to prevent stale data during combat -- (IndexItems() is skipped during combat, so cache may have old bag/slot locations) @@ -2652,11 +2694,56 @@ function CleveRoids.EquipBagItem(msg, offhand) return false end + local invslot = offhand and 17 or 16 + local API = CleveRoids.NampowerAPI + + -- v2.18+: Use native fast lookup for item ID or name + if API and API.features and API.features.hasFindPlayerItemSlot then + local searchTerm = msg + local itemId = tonumber(msg) + + -- Check if already equipped in target slot + if API.IsItemInSlot(searchTerm, invslot) then + return true + end + + -- Find the item (works with both ID and name) + local itemInfo = API.FindItemFast(searchTerm) + if itemInfo then + -- Already equipped in different slot - need to swap + if itemInfo.inventoryID then + if itemInfo.inventoryID == invslot then + return true -- Already in correct slot + end + -- Pick up from current slot and equip to target + ClearCursor() + PickupInventoryItem(itemInfo.inventoryID) + if CursorHasItem and CursorHasItem() then + EquipCursorItem(invslot) + ClearCursor() + return true + end + elseif itemInfo.bagID and itemInfo.slot then + -- In bag - equip to target slot + ClearCursor() + PickupContainerItem(itemInfo.bagID, itemInfo.slot) + if CursorHasItem and CursorHasItem() then + EquipCursorItem(invslot) + ClearCursor() + return true + end + end + end + + -- Item not found via v2.18 lookup + return false + end + + -- Fallback for older Nampower versions -- Try to interpret as item ID (numbers > 19) local itemId = tonumber(msg) if itemId and itemId > 19 then local itemName = nil - local API = CleveRoids.NampowerAPI if API and API.GetItemName then itemName = API.GetItemName(itemId) end @@ -2670,8 +2757,6 @@ function CleveRoids.EquipBagItem(msg, offhand) end end - local invslot = offhand and 17 or 16 - -- PERFORMANCE: Fast check if already equipped (single function call) if CleveRoids.IsItemEquipped and CleveRoids.IsItemEquipped(msg, invslot) then return true @@ -3106,6 +3191,15 @@ function CleveRoids.OnUpdate(self) end end end + + -- MEMORY: Clear spell name caches every 60 seconds (12 cleanup cycles) + CleveRoids._spellCacheCleanupCounter = (CleveRoids._spellCacheCleanupCounter or 0) + 1 + if CleveRoids._spellCacheCleanupCounter >= 12 then + CleveRoids._spellCacheCleanupCounter = 0 + if CleveRoids.ClearSpellNameCaches then + CleveRoids.ClearSpellNameCaches() + end + end end end diff --git a/Extensions/Tooltip/Generic.lua b/Extensions/Tooltip/Generic.lua index 3ec4663..224fc05 100644 --- a/Extensions/Tooltip/Generic.lua +++ b/Extensions/Tooltip/Generic.lua @@ -681,9 +681,31 @@ end -- 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 diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 9e486c2..f8b43c2 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -76,6 +76,17 @@ API.features = { -- Enhanced spell functions (accept name/"spellId:number") hasEnhancedSpellFunctions = false, -- Detected at runtime + + -- v2.18+ APIs (inventory/equipment/cast info) + hasFindPlayerItemSlot = (FindPlayerItemSlot ~= nil), + hasGetEquippedItems = (GetEquippedItems ~= nil), + hasGetEquippedItem = (GetEquippedItem ~= nil), + hasGetBagItems = (GetBagItems ~= nil), + hasGetBagItem = (GetBagItem ~= nil), + hasGetCastInfo = (GetCastInfo ~= nil), + hasGetSpellIdCooldown = (GetSpellIdCooldown ~= nil), + hasGetItemIdCooldown = (GetItemIdCooldown ~= nil), + hasChannelStopCastingNextTick = (ChannelStopCastingNextTick ~= nil), } -- Detect if enhanced spell functions are available (accept name/spellId:number) @@ -121,6 +132,7 @@ API.defaultSettings = { NP_MaxBufferIncreaseMs = "30", NP_RetryServerRejectedSpells = "1", NP_QuickcastTargetingSpells = "0", + NP_QuickcastOnDoubleCast = "0", -- v2.18+: cast targeting spells by double-casting NP_ReplaceMatchingNonGcdCategory = "0", NP_OptimizeBufferUsingPacketTimings = "0", NP_PreventRightClickTargetChange = "0", @@ -545,6 +557,633 @@ function API.UnitHasAura(unitToken, spellId) return false end +-------------------------------------------------------------------------------- +-- INVENTORY & EQUIPMENT API (v2.18+) +-------------------------------------------------------------------------------- + +-- Find an item in player's inventory by ID or name +-- Returns: bagIndex, slot (see wiki for bag index meanings) +-- bagIndex nil + slot = equipped item (slot is equipment slot 0-18) +-- bagIndex + slot = item in bag +-- nil, nil = item not found +function API.FindPlayerItemSlot(itemIdOrName) + if not itemIdOrName then return nil, nil end + + -- Use native function if available (v2.18+) + if FindPlayerItemSlot then + return FindPlayerItemSlot(itemIdOrName) + end + + -- Fallback: manual search for older versions + -- This is much slower but works without Nampower 2.18 + local itemName = itemIdOrName + if type(itemIdOrName) == "number" then + -- Try to get item name from ID + itemName = API.GetItemName(itemIdOrName) + if not itemName then + -- Try GetItemInfo as fallback (only works for cached items) + itemName = GetItemInfo(itemIdOrName) + end + if not itemName then + return nil, nil -- Can't resolve item ID + end + end + + -- Search equipped items first (slots 0-18, but WoW uses 1-19 in API) + for slot = 0, 18 do + local link = GetInventoryItemLink("player", slot + 1) -- GetInventoryItemLink is 1-indexed + if link then + local name = GetItemInfo(link) + if name and string.lower(name) == string.lower(itemName) then + return nil, slot -- Equipped + end + end + end + + -- Search bags (0 = backpack, 1-4 = bags) + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) + for slot = 1, numSlots do + local link = GetContainerItemLink(bag, slot) + if link then + local name = GetItemInfo(link) + if name and string.lower(name) == string.lower(itemName) then + return bag, slot + end + end + end + end + + return nil, nil -- Not found +end + +-- Get all equipped items for a unit +-- Returns table with slot indices (0-18) as keys, item info tables as values +function API.GetEquippedItems(unitToken) + unitToken = unitToken or "player" + + -- Use native function if available (v2.18+) + if GetEquippedItems then + return GetEquippedItems(unitToken) + end + + -- Fallback: manual lookup for player only + if unitToken ~= "player" then + return nil -- Can't inspect other units without native API + end + + local items = {} + for slot = 0, 18 do + local link = GetInventoryItemLink("player", slot + 1) + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + if itemId then + items[slot] = { + itemId = tonumber(itemId), + -- Other fields not available without native API + } + end + end + end + + return items +end + +-- Get equipped item info for a specific slot +-- Slot numbers: 1=Head, 2=Neck, 3=Shoulder... 16=MainHand, 17=OffHand, 18=Ranged +function API.GetEquippedItem(unitToken, slot) + unitToken = unitToken or "player" + + -- Use native function if available (v2.18+) + if GetEquippedItem then + return GetEquippedItem(unitToken, slot) + end + + -- Fallback: manual lookup for player + if unitToken ~= "player" then + return nil + end + + local link = GetInventoryItemLink("player", slot + 1) -- 1-indexed + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + if itemId then + return { + itemId = tonumber(itemId), + } + end + end + + return nil +end + +-- Get all items in all bags +-- Returns nested table: bagIndex -> { slot -> itemInfo } +function API.GetBagItems() + -- Use native function if available (v2.18+) + if GetBagItems then + return GetBagItems() + end + + -- Fallback: manual enumeration + local bags = {} + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) + if numSlots > 0 then + bags[bag] = {} + for slot = 1, numSlots do + local link = GetContainerItemLink(bag, slot) + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + local _, count = GetContainerItemInfo(bag, slot) + if itemId then + bags[bag][slot] = { + itemId = tonumber(itemId), + stackCount = count or 1, + } + end + end + end + end + end + + return bags +end + +-- Get item info for a specific bag slot +function API.GetBagItem(bagIndex, slot) + -- Use native function if available (v2.18+) + if GetBagItem then + return GetBagItem(bagIndex, slot) + end + + -- Fallback: manual lookup + local link = GetContainerItemLink(bagIndex, slot) + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + local _, count = GetContainerItemInfo(bagIndex, slot) + if itemId then + return { + itemId = tonumber(itemId), + stackCount = count or 1, + } + end + end + + return nil +end + +-------------------------------------------------------------------------------- +-- OPTIMIZED ITEM FINDING (v2.18+) +-- These functions provide fast item lookup compatible with CleveRoids.Items format +-------------------------------------------------------------------------------- + +-- Fast item lookup using v2.18 FindPlayerItemSlot +-- Returns item info table compatible with CleveRoids.Items format: +-- { name, itemId, inventoryID } for equipped items +-- { name, itemId, bagID, slot } for bag items +-- Returns nil if item not found +function API.FindItemFast(itemIdOrName) + if not itemIdOrName then return nil end + + -- Use native v2.18 function if available (much faster than Lua scan) + if FindPlayerItemSlot then + local bag, slot = FindPlayerItemSlot(itemIdOrName) + + if slot then + local itemInfo = {} + + if bag == nil then + -- Equipped item (slot is equipment slot 0-18) + -- Nampower uses 0-indexed, WoW API uses 1-indexed for GetInventoryItemLink + local invSlot = slot + 1 + local link = GetInventoryItemLink("player", invSlot) + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + local _, _, name = string.find(link, "|h%[(.-)%]|h") + itemInfo.inventoryID = invSlot + itemInfo.itemId = tonumber(itemId) + itemInfo.name = name + itemInfo._source = "nampower_equipped" + return itemInfo + end + else + -- Bag item + local link = GetContainerItemLink(bag, slot) + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + local _, _, name = string.find(link, "|h%[(.-)%]|h") + local _, count = GetContainerItemInfo(bag, slot) + itemInfo.bagID = bag + itemInfo.slot = slot + itemInfo.itemId = tonumber(itemId) + itemInfo.name = name + itemInfo.count = count or 1 + itemInfo._source = "nampower_bag" + return itemInfo + end + end + end + + return nil -- Not found via native lookup + end + + -- Fallback: no v2.18 API, return nil (caller should use existing methods) + return nil +end + +-- Check if an item is equipped in a specific slot (fast) +-- Returns true if item matches by ID or name +function API.IsItemInSlot(itemIdOrName, inventorySlot) + if not itemIdOrName or not inventorySlot then return false end + + -- Use native v2.18 GetEquippedItem if available + if GetEquippedItem then + local slotInfo = GetEquippedItem("player", inventorySlot - 1) -- 0-indexed + if slotInfo and slotInfo.itemId then + local checkId = tonumber(itemIdOrName) + if checkId then + return slotInfo.itemId == checkId + else + -- Check by name + local itemName = API.GetItemName(slotInfo.itemId) + if itemName then + return string.lower(itemName) == string.lower(itemIdOrName) + end + end + end + return false + end + + -- Fallback: use GetInventoryItemLink + local link = GetInventoryItemLink("player", inventorySlot) + if not link then return false end + + local checkId = tonumber(itemIdOrName) + if checkId then + local _, _, currentId = string.find(link, "item:(%d+)") + return currentId and tonumber(currentId) == checkId + else + local _, _, currentName = string.find(link, "|h%[(.-)%]|h") + return currentName and string.lower(currentName) == string.lower(itemIdOrName) + end +end + +-- Check if item is equipped anywhere (fast) +-- Returns inventorySlot if equipped, nil otherwise +function API.FindEquippedItem(itemIdOrName) + if not itemIdOrName then return nil end + + -- Use native v2.18 FindPlayerItemSlot if available + if FindPlayerItemSlot then + local bag, slot = FindPlayerItemSlot(itemIdOrName) + if bag == nil and slot then + return slot + 1 -- Convert to 1-indexed WoW API slot + end + return nil -- Not equipped (might be in bag or not found) + end + + -- Fallback: scan equipped slots + local checkId = tonumber(itemIdOrName) + local checkName = (not checkId) and string.lower(itemIdOrName) or nil + + for invSlot = 1, 19 do + local link = GetInventoryItemLink("player", invSlot) + if link then + if checkId then + local _, _, currentId = string.find(link, "item:(%d+)") + if currentId and tonumber(currentId) == checkId then + return invSlot + end + elseif checkName then + local _, _, currentName = string.find(link, "|h%[(.-)%]|h") + if currentName and string.lower(currentName) == checkName then + return invSlot + end + end + end + end + + return nil +end + +-- Find item in bags only (not equipped) - fast +-- Returns bag, slot if found, nil otherwise +function API.FindBagItem(itemIdOrName) + if not itemIdOrName then return nil, nil end + + -- Use native v2.18 FindPlayerItemSlot if available + if FindPlayerItemSlot then + local bag, slot = FindPlayerItemSlot(itemIdOrName) + if bag ~= nil and slot then + return bag, slot + end + return nil, nil -- Not in bags (might be equipped or not found) + end + + -- Fallback: scan bags + local checkId = tonumber(itemIdOrName) + local checkName = (not checkId) and string.lower(itemIdOrName) or nil + + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) or 0 + for slot = 1, numSlots do + local link = GetContainerItemLink(bag, slot) + if link then + if checkId then + local _, _, currentId = string.find(link, "item:(%d+)") + if currentId and tonumber(currentId) == checkId then + return bag, slot + end + elseif checkName then + local _, _, currentName = string.find(link, "|h%[(.-)%]|h") + if currentName and string.lower(currentName) == checkName then + return bag, slot + end + end + end + end + end + + return nil, nil +end + +-- Use an item by ID or name (fast lookup) +-- Returns true if item was used, false otherwise +function API.UseItem(itemIdOrName) + if not itemIdOrName then return false end + + -- Try native v2.18 lookup first + local itemInfo = API.FindItemFast(itemIdOrName) + if itemInfo then + ClearCursor() + if itemInfo.inventoryID then + UseInventoryItem(itemInfo.inventoryID) + return true + elseif itemInfo.bagID and itemInfo.slot then + UseContainerItem(itemInfo.bagID, itemInfo.slot) + return true + end + end + + -- No v2.18 API or item not found via native lookup + return false +end + +-- Equip an item from bags to a specific slot (fast lookup) +-- Returns true if equip was attempted, false if item not found +function API.EquipItem(itemIdOrName, targetSlot) + if not itemIdOrName then return false end + + -- Check if already equipped in target slot + if targetSlot and API.IsItemInSlot(itemIdOrName, targetSlot) then + return true -- Already equipped + end + + -- Find the item + local itemInfo = API.FindItemFast(itemIdOrName) + if not itemInfo then return false end + + -- If already equipped (but not in target slot), need to swap + if itemInfo.inventoryID then + if targetSlot and itemInfo.inventoryID ~= targetSlot then + -- Item equipped in wrong slot - pick up and move + ClearCursor() + PickupInventoryItem(itemInfo.inventoryID) + if CursorHasItem and CursorHasItem() then + EquipCursorItem(targetSlot) + ClearCursor() + return true + end + end + return true -- Already equipped (and no target slot specified) + end + + -- Item in bag - equip it + if itemInfo.bagID and itemInfo.slot then + ClearCursor() + PickupContainerItem(itemInfo.bagID, itemInfo.slot) + if CursorHasItem and CursorHasItem() then + if targetSlot then + EquipCursorItem(targetSlot) + else + -- Auto-equip to appropriate slot + AutoEquipCursorItem() + end + ClearCursor() + return true + end + end + + return false +end + +-------------------------------------------------------------------------------- +-- CAST INFO API (v2.18+) +-------------------------------------------------------------------------------- + +-- Get detailed information about current cast/channel +-- Returns table with: castId, spellId, guid, castType, castStartS, castEndS, +-- castRemainingMs, castDurationMs, gcdEndS, gcdRemainingMs +-- Returns nil if no active cast +function API.GetCastInfo() + -- Use native function if available (v2.18+) + if GetCastInfo then + return GetCastInfo() + end + + -- Fallback: use GetCurrentCastingInfo (less detailed) + if GetCurrentCastingInfo then + local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo() + if castId and castId > 0 then + return { + spellId = castId, + castType = channeling == 1 and 3 or 0, -- 3 = CHANNEL, 0 = NORMAL + -- Timing info not available without native GetCastInfo + } + end + end + + return nil +end + +-- Check if currently casting (convenience wrapper) +function API.IsCasting() + local info = API.GetCastInfo() + return info ~= nil +end + +-- Get GCD remaining in milliseconds +function API.GetGCDRemainingMs() + local info = API.GetCastInfo() + if info and info.gcdRemainingMs then + return info.gcdRemainingMs + end + return 0 +end + +-- Get cast remaining in milliseconds +function API.GetCastRemainingMs() + local info = API.GetCastInfo() + if info and info.castRemainingMs then + return info.castRemainingMs + end + return 0 +end + +-------------------------------------------------------------------------------- +-- COOLDOWN API (v2.18+) +-------------------------------------------------------------------------------- + +-- Get detailed cooldown information for a spell +-- Returns table with: isOnCooldown, cooldownRemainingMs, +-- individual cooldown: individualStartS, individualDurationMs, individualRemainingMs, isOnIndividualCooldown +-- category cooldown: categoryId, categoryStartS, categoryDurationMs, categoryRemainingMs, isOnCategoryCooldown +-- GCD: gcdCategoryId, gcdCategoryStartS, gcdCategoryDurationMs, gcdCategoryRemainingMs, isOnGcdCategoryCooldown +function API.GetSpellCooldownInfo(spellId) + if not spellId or spellId == 0 then return nil end + + -- Use native function if available (v2.18+) + if GetSpellIdCooldown then + return GetSpellIdCooldown(spellId) + end + + -- Fallback: use GetSpellCooldown (less detailed) + local spellName = API.GetSpellNameById(spellId) + if spellName then + local start, duration = GetSpellCooldown(spellName, BOOKTYPE_SPELL) + if start and start > 0 then + local remaining = (start + duration) - GetTime() + if remaining > 0 then + return { + isOnCooldown = 1, + cooldownRemainingMs = remaining * 1000, + } + end + end + end + + return { isOnCooldown = 0, cooldownRemainingMs = 0 } +end + +-- Get detailed cooldown information for an item +function API.GetItemCooldownInfo(itemId) + if not itemId or itemId == 0 then return nil end + + -- Use native function if available (v2.18+) + if GetItemIdCooldown then + return GetItemIdCooldown(itemId) + end + + -- Fallback: use GetItemCooldown (less detailed) + local start, duration = GetItemCooldown(itemId) + if start and start > 0 then + local remaining = (start + duration) - GetTime() + if remaining > 0 then + return { + isOnCooldown = 1, + cooldownRemainingMs = remaining * 1000, + } + end + end + + return { isOnCooldown = 0, cooldownRemainingMs = 0 } +end + +-- Check if a spell is on cooldown (ignoring GCD) +function API.IsSpellOnCooldown(spellId, ignoreGCD) + local info = API.GetSpellCooldownInfo(spellId) + if not info then return false end + + if ignoreGCD then + -- Only check individual and category cooldowns + return (info.isOnIndividualCooldown == 1) or (info.isOnCategoryCooldown == 1) + end + + return info.isOnCooldown == 1 +end + +-- Get remaining cooldown for a spell in seconds +function API.GetSpellCooldownRemaining(spellId) + local info = API.GetSpellCooldownInfo(spellId) + if info and info.cooldownRemainingMs then + return info.cooldownRemainingMs / 1000 + end + return 0 +end + +-------------------------------------------------------------------------------- +-- CHANNEL CONTROL API (v2.18+) +-------------------------------------------------------------------------------- + +-- Stop channeling early on the next tick +-- Only works if NP_QueueChannelingSpells is enabled +function API.StopChannelNextTick() + if ChannelStopCastingNextTick then + ChannelStopCastingNextTick() + return true + end + return false +end + +-------------------------------------------------------------------------------- +-- EVENT CONSTANTS (v2.18+) +-------------------------------------------------------------------------------- + +-- Spell queue event codes +API.QUEUE_EVENT = { + ON_SWING_QUEUED = 0, + ON_SWING_QUEUE_POPPED = 1, + NORMAL_QUEUED = 2, + NORMAL_QUEUE_POPPED = 3, + NON_GCD_QUEUED = 4, + NON_GCD_QUEUE_POPPED = 5, +} + +-- Spell cast event types +API.CAST_TYPE = { + NORMAL = 1, + NON_GCD = 2, + ON_SWING = 3, + CHANNEL = 4, + TARGETING = 5, + TARGETING_NON_GCD = 6, +} + +-- Buff/debuff event names (v2.18+) +API.AURA_EVENTS = { + "BUFF_ADDED_SELF", + "BUFF_REMOVED_SELF", + "BUFF_ADDED_OTHER", + "BUFF_REMOVED_OTHER", + "DEBUFF_ADDED_SELF", + "DEBUFF_REMOVED_SELF", + "DEBUFF_ADDED_OTHER", + "DEBUFF_REMOVED_OTHER", +} + +-- Spell school constants (for reference) +API.SPELL_SCHOOL = { + PHYSICAL = 0, + HOLY = 1, + FIRE = 2, + NATURE = 3, + FROST = 4, + SHADOW = 5, + ARCANE = 6, +} + +-- Resistance indices (1-indexed for Lua tables) +API.RESISTANCE_INDEX = { + ARMOR = 1, + HOLY = 2, + FIRE = 3, + NATURE = 4, + FROST = 5, + SHADOW = 6, + ARCANE = 7, +} + -------------------------------------------------------------------------------- -- SPELL MODIFIERS API (GetSpellModifiers) -------------------------------------------------------------------------------- diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index d7ab375..c3079f5 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -2,7 +2,7 @@ ## Author: DWG, _brain, Torio, yani9o ## Title: [Super]CleveRoidMacros ## Notes: /cleveroid for settings -## Version: 1.5 +## Version: 1.6 ## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar ## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools Localization.lua