From 86fa4476f632843a5803ce88b1893499ed852c3b Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Fri, 5 Dec 2025 18:26:33 -0500 Subject: [PATCH] fix equip with conditionals and dont pollute runmacro global namespace --- Compatibility/SuperMacro.lua | 23 ++- Console.lua | 9 ++ Core.lua | 276 +++++++++++++++++++++------------ Extensions/Tooltip/Generic.lua | 53 ++++++- NampowerAPI.lua | 62 +++++++- 5 files changed, 317 insertions(+), 106 deletions(-) diff --git a/Compatibility/SuperMacro.lua b/Compatibility/SuperMacro.lua index e3734ef..9198e3c 100644 --- a/Compatibility/SuperMacro.lua +++ b/Compatibility/SuperMacro.lua @@ -5,6 +5,7 @@ - Intercepts CRM-owned slashcommands (including {…}, […], !, ?, ~) - Forwards to CRM's SlashCmdList handlers; preserves /castsequence - Leaves SuperMacro unmodified; safe fallback to original RunLine + - Redirects global RunMacro to SuperMacro_RunMacro so RunLine hook can intercept ]] do @@ -14,12 +15,14 @@ do CRM.Hooks = CRM.Hooks or {} -- Commands owned by CRM (keep in sync with Console.lua registrations) + -- NOTE: /runmacro is NOT intercepted here - we let SuperMacro's RunLine handle it + -- naturally via SuperMacro_RunMacro. This avoids conflicts with SuperMacro's keybinding system. local INTERCEPT = { cast=true, castsequence=true, use=true, startattack=true, stopattack=true, stopcasting=true, target=true, retarget=true, cancelaura=true, unbuff=true, unqueue=true, unshift=true, equip=true, equipmh=true, equipoh=true, - runmacro=true, + -- runmacro intentionally NOT included - see note above -- pet petattack=true, petfollow=true, petpassive=true, petaggressive=true, petdefensive=true, petwait=true, @@ -114,6 +117,17 @@ do end CRM.SM_RunLineHooked = true + + -- Redirect global RunMacro to SuperMacro_RunMacro so all macro execution + -- goes through RunLine (where our hook intercepts CRM commands). + -- This ensures both SuperMacro and CleveRoids features work together. + if type(_G.SuperMacro_RunMacro) == "function" then + _G.RunMacro = function(index) + return _G.SuperMacro_RunMacro(index) + end + -- Also update the Macro alias + _G.Macro = _G.SuperMacro_RunMacro + end end -- ===== Order-agnostic loader: install only when BOTH addons are present ===== @@ -133,7 +147,8 @@ do local function both_loaded() if SM_LOADED and CRM_LOADED then return true end -- heuristic fallback in case this file loads late: - local sm_ok = type(_G.RunLine) == "function" + -- Check for SuperMacro's namespaced internal function (preferred) or RunLine + local sm_ok = type(_G.SuperMacro_RunMacro) == "function" or type(_G.RunLine) == "function" local crm_ok = _G.CleveRoids and _G.SlashCmdList and type(_G.SlashCmdList.CAST) == "function" return sm_ok and crm_ok end @@ -142,7 +157,9 @@ do if both_loaded() then install_supermacro_hook() end end - f:SetScript("OnEvent", function(_, evt, addon) + f:SetScript("OnEvent", function() + local evt = event + local addon = arg1 if evt == "ADDON_LOADED" then if type(addon) == "string" then note_loaded(addon) end try_install() diff --git a/Console.lua b/Console.lua index 607499b..7abba31 100644 --- a/Console.lua +++ b/Console.lua @@ -271,6 +271,15 @@ SlashCmdList.RUNMACRO = function(msg) return CleveRoids.ExecuteMacroByName(CleveRoids.Trim(msg)) end +-- Global RunMacro wrapper for user convenience (delegates to namespaced internal function) +-- This pattern ensures internal logic uses CleveRoids.ExecuteMacroByName and won't break +-- if another addon overwrites the global RunMacro +-- NOTE: When SuperMacro is also loaded, Compatibility/SuperMacro.lua redirects this to +-- SuperMacro_RunMacro so macros go through RunLine (where CRM commands are intercepted) +function RunMacro(name) + return CleveRoids.ExecuteMacroByName(name) +end + SLASH_RETARGET1 = "/retarget" SlashCmdList.RETARGET = function(msg) CleveRoids.DoRetarget() diff --git a/Core.lua b/Core.lua index 7c7402b..7fd0b54 100644 --- a/Core.lua +++ b/Core.lua @@ -119,8 +119,21 @@ local function GetInventoryIdFromSlot(slotName) return SLOT_TO_INVID[slotName] or GetInventorySlotInfo(slotName) end +-- Check if slot 18 is a relic (no GCD) for current player class +local function IsRelicSlot(slot) + if slot ~= 18 then return false end + local playerClass = CleveRoids.playerClass + return playerClass == "PALADIN" or playerClass == "DRUID" or playerClass == "SHAMAN" +end + -- PERFORMANCE: Cache GetTime() result for cooldown checks within same frame +-- Slot 18 (relic/idol/libram/totem) has no GCD for Paladin/Druid/Shaman local function IsSlotOnCooldown(slot, now) + -- Relics have no GCD - skip cooldown check entirely + if IsRelicSlot(slot) then + return false + end + now = now or GetTime() local slotTime = CleveRoids.lastEquipTime[slot] or 0 local globalTime = CleveRoids.lastGlobalEquipTime or 0 @@ -265,8 +278,11 @@ function CleveRoids.QueueEquipItem(item, slotName) local success = PerformEquipSwap(item, inventoryId, true) if success then - CleveRoids.lastEquipTime[inventoryId] = now - CleveRoids.lastGlobalEquipTime = now + -- Don't set cooldowns for relic slots (no GCD) + if not IsRelicSlot(inventoryId) then + CleveRoids.lastEquipTime[inventoryId] = now + CleveRoids.lastGlobalEquipTime = now + end return true end end @@ -313,8 +329,11 @@ function CleveRoids.ProcessEquipmentQueue() local success = PerformEquipSwap(queued.item, queued.inventoryId, true) if success then - CleveRoids.lastEquipTime[queued.inventoryId] = now - CleveRoids.lastGlobalEquipTime = now + -- Don't set cooldowns for relic slots (no GCD) + if not IsRelicSlot(queued.inventoryId) then + CleveRoids.lastEquipTime[queued.inventoryId] = now + CleveRoids.lastGlobalEquipTime = now + end RemoveQueueEntry(i) else queued.retries = queued.retries + 1 @@ -1146,40 +1165,54 @@ function CleveRoids.GetMacroBody(name) return macro and macro.body end --- Attempts to execute a macro by the given name (Blizzard or Super tab) +-- Attempts to execute a macro by the given name or index (Blizzard or Super tab) -- Returns: true if something was executed, false otherwise +-- Supports: macro name (string), macro index (number or numeric string like "19") +-- Uses CleveRoids' own ExecuteMacroBody for processing (supports enhanced syntax) function CleveRoids.ExecuteMacroByName(name) if not name or name == "" then return false end local body - -- 1) Blizzard macro runner by name/ID - local id = GetMacroIndexByName(name) - if id and id ~= 0 and type(RunMacro) == "function" then - if pcall(RunMacro, name) then return true end - if pcall(RunMacro, id) then return true end - local _n, _tex, b2 = GetMacroInfo(id) - if b2 and b2 ~= "" then body = body or b2 end - end + -- Check if input is a numeric index (e.g., "19" or 19) + local numericIndex = tonumber(name) - -- 2) SuperMacro runner by name - if type(GetSuperMacroInfo) == "function" and type(RunSuperMacro) == "function" then - local _n2, _t2, b3 = GetSuperMacroInfo(name) - if b3 and b3 ~= "" then - if pcall(RunSuperMacro, name) then return true end - body = body or b3 + -- 1) Try Blizzard macro (by index or name) + if numericIndex then + -- Numeric index - get body directly (supports character macros 19-36) + local _n, _tex, b = GetMacroInfo(numericIndex) + if b and b ~= "" then body = b end + else + -- String name - look up by name + local id = GetMacroIndexByName(name) + if id and id ~= 0 then + local _n, _tex, b = GetMacroInfo(id) + if b and b ~= "" then body = b end end end - -- 3) CRM cache fallback - if not body and type(CleveRoids.GetMacro) == "function" then - local m = CleveRoids.GetMacro(name) - if m and m.body and m.body ~= "" then - body = m.body + -- 2) SuperMacro's Super macros (by name only, not index) + -- These are the 7000-char extended macros stored in SM_SUPER + if not body and not numericIndex then + if type(GetSuperMacroInfo) == "function" then + local _n2, _t2, b2 = GetSuperMacroInfo(name) + if b2 and b2 ~= "" then body = b2 end + end + end + + -- 3) CRM cache fallback (by name only) + if not body and not numericIndex then + if type(CleveRoids.GetMacro) == "function" then + local m = CleveRoids.GetMacro(name) + if m and m.body and m.body ~= "" then + body = m.body + end end end if not body or body == "" then return false end + + -- Execute using CleveRoids' processor (supports conditionals, extended syntax) return CleveRoids.ExecuteMacroBody(body) end @@ -1938,7 +1971,14 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo for k, v in pairs(conditionals) do if not CleveRoids.ignoreKeywords[k] then - if not CleveRoids.Keywords[k] or not CleveRoids.Keywords[k](conditionals) then + local result = CleveRoids.Keywords[k] and CleveRoids.Keywords[k](conditionals) + -- Debug logging for equipped conditional when equipDebugLog is enabled + if CleveRoids.equipDebugLog and (k == "equipped" or k == "noequipped") then + local valStr = type(v) == "table" and table.concat(v, ", ") or tostring(v) + CleveRoids.Print("|cff888888[EquipLog] Conditional [" .. k .. ":" .. valStr .. "] = " .. + (result and "|cff00ff00PASS|r" or "|cffff0000FAIL|r") .. "|r") + end + if not result then if needRetarget then TargetLastTarget() needRetarget = false @@ -2397,82 +2437,29 @@ function CleveRoids.DoUse(msg) return end - -- PERFORMANCE: Try fast lookup methods first, fall back to full scan - -- Full scan is now optimized with GetNameFromLink() instead of GetItemInfo() - local item + -- Simple bag scan and equip - no cache, no Nampower API (causes crashes) + local qname = string_lower(msg) - -- Try fast cache lookup first (no scanning) - if CleveRoids.GetItemFast then - item = CleveRoids.GetItemFast(msg) - end - - -- Try quick targeted scan (stops when found) - if not item and CleveRoids.FindItemQuick then - item = CleveRoids.FindItemQuick(msg) - end - - -- Full scan fallback (now optimized, safe during combat) - if not item then - item = CleveRoids.GetItem(msg) - end - - if not item then return end - - -- If it's an equipped item (trinket etc.), use it directly - if item.inventoryID then - ClearCursor() - UseInventoryItem(item.inventoryID) - -- Invalidate cache - item location may have changed (e.g., trinket swap) - if item.name and CleveRoids.Items then - CleveRoids.Items[item.name] = nil - CleveRoids.Items[string_lower(item.name)] = nil - end - return - end - - -- Otherwise, use the bag item - if item.bagID and item.slot then - -- If we tracked multiple stacks, advance politely - CleveRoids.GetNextBagSlotForUse(item, msg) - - -- Before equipping, note what's in commonly swapped slots - -- so we can invalidate cache entries for displaced items - -- Slots: 13/14 (trinkets), 16/17 (weapons), 18 (relic/idol/libram) - local swapSlots = {13, 14, 16, 17, 18} - local oldSlotItems = {} - for _, slotId in ipairs(swapSlots) do - local link = GetInventoryItemLink("player", slotId) + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) or 0 + for bagSlot = 1, numSlots do + local link = GetContainerItemLink(bag, bagSlot) if link then - local _, _, name = string_find(link, "|h%[(.-)%]|h") - if name then oldSlotItems[slotId] = name end - end - end - - ClearCursor() - UseContainerItem(item.bagID, item.slot) - - -- Invalidate cache for the item we just used - if item.name and CleveRoids.Items then - CleveRoids.Items[item.name] = nil - CleveRoids.Items[string_lower(item.name)] = nil - end - - -- Invalidate cache for any displaced items in swap slots - if CleveRoids.Items then - for slotId, oldName in pairs(oldSlotItems) do - local newLink = GetInventoryItemLink("player", slotId) - local newName = nil - if newLink then - local _, _, name = string_find(newLink, "|h%[(.-)%]|h") - newName = name - end - if newName ~= oldName then - CleveRoids.Items[oldName] = nil - CleveRoids.Items[string_lower(oldName)] = nil + 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") + end + ClearCursor() + UseContainerItem(bag, bagSlot) + return end end end - return + end + + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[EquipLog] /equip " .. msg .. " - not found in bags (already equipped?)|r") end end @@ -4238,6 +4225,8 @@ SlashCmdList["CLEVEROID"] = function(msg) DEFAULT_CHAT_FRAME:AddMessage('/cleveroid diagnosetalent - Diagnose talent modifier issues') DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Equipment Modifiers:|r") DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testequip - Test equipment modifier for a spell') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equipdebug - Debug item lookup for /equip') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equiplog - Toggle real-time equip command logging') DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Reactive Proc Tracking:|r") DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listprocs - Show active reactive ability procs') DEFAULT_CHAT_FRAME:AddMessage('/cleveroid setproc [duration] - Manually set proc (testing)') @@ -4951,6 +4940,101 @@ SlashCmdList["CLEVEROID"] = function(msg) return end + -- equiplog (toggle real-time equip logging) + if cmd == "equiplog" then + CleveRoids.equipDebugLog = not CleveRoids.equipDebugLog + CleveRoids.Print("Equip command logging " .. (CleveRoids.equipDebugLog and "|cff00ff00ENABLED|r" or "|cffff0000DISABLED|r")) + if CleveRoids.equipDebugLog then + CleveRoids.Print("Use /equip commands to see detailed lookup info") + end + return + end + + -- equipdebug (debug item lookup for /equip command) + if cmd == "equipdebug" then + local itemName = val + if val2 and val2 ~= "" then + itemName = val .. " " .. val2 + end + if not itemName or itemName == "" then + CleveRoids.Print("Usage: /cleveroid equipdebug ") + CleveRoids.Print('Example: /cleveroid equipdebug Idol of Ferocity') + return + end + -- Strip quotes if present + itemName = string.gsub(itemName, '"', "") + itemName = string.gsub(itemName, "_", " ") + + CleveRoids.Print("|cff00ff00=== Equipment Debug: " .. itemName .. " ===|r") + + -- Check equipped slot 18 (relic/idol) + local slot18Link = GetInventoryItemLink("player", 18) + if slot18Link then + local _, _, slot18Name = string.find(slot18Link, "|h%[(.-)%]|h") + CleveRoids.Print("Slot 18 equipped: " .. (slot18Name or "unknown")) + else + CleveRoids.Print("Slot 18 equipped: (empty)") + end + + -- Check cache + local Items = CleveRoids.Items or {} + local cached = Items[itemName] + CleveRoids.Print("|cffffaa00Cache lookup:|r") + if cached then + if type(cached) == "table" then + if cached.inventoryID then + CleveRoids.Print(" Found in cache: EQUIPPED (inventoryID=" .. cached.inventoryID .. ")") + elseif cached.bagID then + CleveRoids.Print(" Found in cache: BAG (bag=" .. cached.bagID .. ", slot=" .. cached.slot .. ")") + else + CleveRoids.Print(" Found in cache: (type unknown)") + end + elseif type(cached) == "string" then + CleveRoids.Print(" Found in cache: -> " .. cached .. " (canonical name)") + end + else + CleveRoids.Print(" Not in cache") + end + + -- Try GetItemFast + CleveRoids.Print("|cffffaa00GetItemFast:|r") + local fastItem = CleveRoids.GetItemFast and CleveRoids.GetItemFast(itemName) + if fastItem then + if fastItem.inventoryID then + CleveRoids.Print(" Returns: EQUIPPED (inventoryID=" .. fastItem.inventoryID .. ")") + elseif fastItem.bagID then + CleveRoids.Print(" Returns: BAG (bag=" .. fastItem.bagID .. ", slot=" .. fastItem.slot .. ")") + end + else + CleveRoids.Print(" Returns: nil") + end + + -- Try FindItemQuick + CleveRoids.Print("|cffffaa00FindItemQuick (scans equipped then bags):|r") + local quickItem = CleveRoids.FindItemQuick and CleveRoids.FindItemQuick(itemName) + if quickItem then + if quickItem.inventoryID then + CleveRoids.Print(" Returns: EQUIPPED (inventoryID=" .. quickItem.inventoryID .. ")") + elseif quickItem.bagID then + CleveRoids.Print(" Returns: BAG (bag=" .. quickItem.bagID .. ", slot=" .. quickItem.slot .. ")") + end + else + CleveRoids.Print(" Returns: nil (item not found)") + end + + -- Check HasGearEquipped (used by [equipped] conditional) + CleveRoids.Print("|cffffaa00HasGearEquipped (conditional check):|r") + local hasEquipped = CleveRoids.HasGearEquipped and CleveRoids.HasGearEquipped(itemName) + CleveRoids.Print(" [equipped:" .. itemName .. "] = " .. (hasEquipped and "|cff00ff00TRUE|r" or "|cffff0000FALSE|r")) + + -- Combat state + CleveRoids.Print("|cffffaa00Combat state:|r") + CleveRoids.Print(" UnitAffectingCombat: " .. (UnitAffectingCombat("player") and "IN COMBAT" or "not in combat")) + CleveRoids.Print(" playerClass: " .. (CleveRoids.playerClass or "unknown")) + + return + end + -- listprocs (show active reactive procs) if cmd == "listprocs" or cmd == "procs" or cmd == "reactive" then CleveRoids.Print("=== Active Reactive Procs ===") diff --git a/Extensions/Tooltip/Generic.lua b/Extensions/Tooltip/Generic.lua index 390e4f9..b2ee8aa 100644 --- a/Extensions/Tooltip/Generic.lua +++ b/Extensions/Tooltip/Generic.lua @@ -628,18 +628,63 @@ 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. function CleveRoids.FindItemQuick(text) if not text or text == "" then return nil end - -- First try fast cache lookup - local cached = CleveRoids.GetItemFast(text) - if cached then return cached end - 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 + local link = GetInventoryItemLink("player", cached.inventoryID) + if link then + local nm = GetNameFromLink(link) + if nm and qname and string_lower(nm) == qname then + cached._validated = true + return cached -- Cache is valid + elseif qid then + local _, _, itemID = string_find(link, "item:(%d+)") + if itemID and tonumber(itemID) == qid then + cached._validated = true + return cached -- Cache is valid + end + 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 + local link = GetContainerItemLink(cached.bagID, cached.slot) + if link then + local nm = GetNameFromLink(link) + if nm and qname and string_lower(nm) == qname then + cached._validated = true + return cached -- Cache is valid + elseif qid then + local _, _, itemID = string_find(link, "item:(%d+)") + if itemID and tonumber(itemID) == qid then + cached._validated = true + return cached -- Cache is valid + end + 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) diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 9f31138..5212809 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -342,10 +342,10 @@ function API.GetItemRecord(itemId) return API.itemStatsCache[itemId] end - -- Use native function if available + -- Use native function if available (wrapped in pcall for safety) if GetItemStats then - local stats = GetItemStats(itemId) - if stats then + local ok, stats = pcall(GetItemStats, itemId) + if ok and stats then API.itemStatsCache[itemId] = stats return stats end @@ -406,6 +406,62 @@ function API.GetWeaponSpeed(itemId) return nil end +-- Get item inventory type (equipment slot) +-- Returns: inventoryType number, or nil if not equippable +-- Common types: 0=Non-equip, 1=Head, 2=Neck, 3=Shoulder, 4=Shirt, 5=Chest, +-- 6=Waist, 7=Legs, 8=Feet, 9=Wrist, 10=Hands, 11=Finger, 12=Trinket, +-- 13=One-Hand, 14=Shield, 15=Ranged, 16=Back, 17=Two-Hand, +-- 18=Bag, 20=Robe, 21=Main Hand, 22=Off Hand, 23=Holdable, +-- 24=Ammo, 25=Thrown, 26=Ranged Right, 28=Relic +function API.GetItemInventoryType(itemId) + local ok, result = pcall(API.GetItemField, itemId, "inventoryType") + if ok then return result end + return nil +end + +-- Convert inventoryType to equipment slot ID(s) +-- Returns slotId, altSlotId (for rings/trinkets) +API.inventoryTypeToSlot = { + [1] = 1, -- Head -> HeadSlot + [2] = 2, -- Neck -> NeckSlot + [3] = 3, -- Shoulder -> ShoulderSlot + [4] = 4, -- Shirt -> ShirtSlot + [5] = 5, -- Chest -> ChestSlot + [6] = 6, -- Waist -> WaistSlot + [7] = 7, -- Legs -> LegsSlot + [8] = 8, -- Feet -> FeetSlot + [9] = 9, -- Wrist -> WristSlot + [10] = 10, -- Hands -> HandsSlot + [11] = {11, 12}, -- Finger -> Finger0Slot or Finger1Slot + [12] = {13, 14}, -- Trinket -> Trinket0Slot or Trinket1Slot + [13] = 16, -- One-Hand -> MainHandSlot (can also go off-hand) + [14] = 17, -- Shield -> SecondaryHandSlot + [15] = 18, -- Ranged -> RangedSlot + [16] = 15, -- Back -> BackSlot + [17] = 16, -- Two-Hand -> MainHandSlot + [20] = 5, -- Robe -> ChestSlot + [21] = 16, -- Main Hand -> MainHandSlot + [22] = 17, -- Off Hand -> SecondaryHandSlot + [23] = 17, -- Holdable -> SecondaryHandSlot + [24] = 0, -- Ammo -> AmmoSlot + [25] = 18, -- Thrown -> RangedSlot + [26] = 18, -- Ranged Right -> RangedSlot + [28] = 18, -- Relic -> RangedSlot +} + +-- Get equipment slot ID for an item +-- Returns slotId, altSlotId for items that can go in multiple slots +function API.GetItemEquipSlot(itemId) + local ok, invType = pcall(API.GetItemInventoryType, itemId) + if not ok or not invType or invType == 0 then return nil end + + local slotInfo = API.inventoryTypeToSlot[invType] + if type(slotInfo) == "table" then + return slotInfo[1], slotInfo[2] -- Primary and alt slot + end + return slotInfo, nil +end + -------------------------------------------------------------------------------- -- UNIT DATA API (GetUnitData / GetUnitField) --------------------------------------------------------------------------------