diff --git a/Core.lua b/Core.lua index 2a1953c..7c7402b 100644 --- a/Core.lua +++ b/Core.lua @@ -134,7 +134,17 @@ local function PerformEquipSwap(item, inventoryId, useQueueScript) if not item or not inventoryId then return false end -- Check if in combat and swapping weapons - local isWeapon = (inventoryId == 16 or inventoryId == 17 or inventoryId == 18) + -- Slot 18 (ranged) is only a weapon for Hunter/Warrior/Rogue/Mage/Warlock/Priest + -- For Druid/Paladin/Shaman, slot 18 is idol/libram/totem - can swap freely + local isWeapon = (inventoryId == 16 or inventoryId == 17) + if inventoryId == 18 then + -- Use cached playerClass for performance + local playerClass = CleveRoids.playerClass + -- Only treat slot 18 as weapon for classes that use ranged weapons + isWeapon = (playerClass == "HUNTER" or playerClass == "WARRIOR" or + playerClass == "ROGUE" or playerClass == "MAGE" or + playerClass == "WARLOCK" or playerClass == "PRIEST") + end if isWeapon and UnitAffectingCombat("player") then -- Don't swap while casting @@ -322,16 +332,31 @@ end -- PERFORMANCE: Self-disabling frame for queue processing with throttling -- Only processes every EQUIP_QUEUE_INTERVAL instead of every frame -CleveRoids.EQUIP_QUEUE_INTERVAL = 0.05 -- 50ms between queue checks +CleveRoids.EQUIP_QUEUE_INTERVAL = 0.1 -- 100ms between queue checks (was 50ms) CleveRoids.equipQueueLastUpdate = 0 CleveRoids.equipQueueFrame = CreateFrame("Frame") CleveRoids.equipQueueFrame:Hide() -CleveRoids.equipQueueFrame:SetScript("OnUpdate", function() - local now = GetTime() - if (now - CleveRoids.equipQueueLastUpdate) < CleveRoids.EQUIP_QUEUE_INTERVAL then - return -- Throttle: skip this frame + +-- PERFORMANCE: Upvalue for faster access in OnUpdate +local equipQueueFrame = CleveRoids.equipQueueFrame +local equipQueueInterval = CleveRoids.EQUIP_QUEUE_INTERVAL + +equipQueueFrame:SetScript("OnUpdate", function() + -- PERFORMANCE: Use arg1 (elapsed time) if available, otherwise GetTime() + local elapsed = arg1 + if elapsed then + CleveRoids.equipQueueLastUpdate = (CleveRoids.equipQueueLastUpdate or 0) + elapsed + if CleveRoids.equipQueueLastUpdate < equipQueueInterval then + return -- Throttle: skip this frame + end + CleveRoids.equipQueueLastUpdate = 0 + else + local now = GetTime() + if (now - (CleveRoids.equipQueueLastUpdate or 0)) < equipQueueInterval then + return + end + CleveRoids.equipQueueLastUpdate = now end - CleveRoids.equipQueueLastUpdate = now CleveRoids.ProcessEquipmentQueue() if CleveRoids.equipmentQueueLen == 0 then @@ -571,6 +596,46 @@ function CleveRoids.GetProxyActionSlot(slot) return CleveRoids.actionSlots[slot] or CleveRoids.actionSlots[slot.."()"] end +-- Resolves nested macro references for #showtooltip propagation +-- If an action is a {MacroName} reference and that macro has #showtooltip, +-- returns the inner macro's active action; otherwise returns nil +-- depth parameter prevents infinite recursion +function CleveRoids.ResolveNestedMacroActive(action, depth) + if not action or not action.action then return nil end + depth = depth or 0 + if depth > 5 then return nil end -- prevent infinite recursion + + -- Check if this action is a macro reference {MacroName} + local macroName = CleveRoids.GetMacroNameFromAction(action.action) + if not macroName then return nil end + + -- Get or parse the inner macro + local innerMacro = CleveRoids.GetMacro(macroName) + if not innerMacro then + innerMacro = CleveRoids.ParseMacro(macroName) + end + if not innerMacro or not innerMacro.actions then return nil end + + -- Check if inner macro has #showtooltip (indicated by having a tooltip or action list) + if not innerMacro.actions.tooltip and table.getn(innerMacro.actions.list or {}) == 0 then + return nil + end + + -- Run TestForActiveAction on inner macro to get its current active + CleveRoids.TestForActiveAction(innerMacro.actions) + + -- If inner macro has an active action, check if it's also a nested macro + if innerMacro.actions.active then + local deeperActive = CleveRoids.ResolveNestedMacroActive(innerMacro.actions.active, depth + 1) + if deeperActive then + return deeperActive + end + return innerMacro.actions.active + end + + return nil +end + function CleveRoids.TestForActiveAction(actions) if not actions then return end local currentActive = actions.active @@ -582,9 +647,20 @@ function CleveRoids.TestForActiveAction(actions) if actions.tooltip and table.getn(actions.list) == 0 then if CleveRoids.TestAction(actions.cmd or "", actions.args or "") then - hasActive = true - actions.active = actions.tooltip + newActiveAction = actions.tooltip + -- Resolve nested macro references for #showtooltip propagation + local macroName = CleveRoids.GetMacroNameFromAction(actions.tooltip.action) + if macroName then + local resolved = CleveRoids.ResolveNestedMacroActive(actions.tooltip, 0) + if resolved then + newActiveAction = resolved + else + -- Inner macro didn't resolve - no active action + hasActive = false + newActiveAction = nil + end + end end else -- First pass: find first action with conditionals that passes @@ -598,11 +674,6 @@ function CleveRoids.TestForActiveAction(actions) -- Note: CleveRoids._ignoretooltip is a side-effect flag set by GetParsedMsg local shouldSkipForIcon = CleveRoids._ignoretooltip == 1 - if not conditionals and not firstUnconditional and not shouldSkipForIcon then - -- Track first unconditional action as fallback (unless it has '?' prefix) - firstUnconditional = action - end - -- break on first action that passes tests (unless it should be skipped for icon) if result and not shouldSkipForIcon then hasActive = true @@ -612,9 +683,31 @@ function CleveRoids.TestForActiveAction(actions) if not newActiveAction then hasActive = false end else newActiveAction = action + -- Resolve nested macro references for #showtooltip propagation + -- If inner macro doesn't resolve, continue to next action + local macroName = CleveRoids.GetMacroNameFromAction(action.action) + if macroName then + local resolved = CleveRoids.ResolveNestedMacroActive(action, 0) + if resolved then + newActiveAction = resolved + else + -- Inner macro didn't resolve - try next action + hasActive = false + newActiveAction = nil + end + end end if hasActive then break end end + + -- Track first unconditional non-macro action as fallback + if not conditionals and not firstUnconditional and not shouldSkipForIcon then + -- Don't use unresolvable macro refs as fallback + local macroName = CleveRoids.GetMacroNameFromAction(action.action) + if not macroName then + firstUnconditional = action + end + end end -- If no conditional action passed, use first unconditional action @@ -848,26 +941,58 @@ function CleveRoids.TestForActiveAction(actions) return changed end +-- PERFORMANCE: Static buffer references for hot path +local _actionsToSlotsBuffer = CleveRoids._actionsToSlotsBuffer +local _slotsBuffer = CleveRoids._slotsBuffer +local _actionsListBuffer = CleveRoids._actionsListBuffer + function CleveRoids.TestForAllActiveActions() + -- PERFORMANCE: Reuse static buffers instead of creating new tables each call -- Group slots by their actions object to handle shared macro references - -- When the same macro appears on multiple slots, they share the same actions object - local actionsToSlots = {} + local actionsToSlots = _actionsToSlotsBuffer + local actionsList = _actionsListBuffer + local actionsCount = 0 + for slot, actions in pairs(CleveRoids.Actions) do if not actionsToSlots[actions] then - actionsToSlots[actions] = {} + -- Reuse or create slots array from pool + local slots = _slotsBuffer[actions] + if not slots then + slots = {} + _slotsBuffer[actions] = slots + end + slots[1] = slot + slots._count = 1 + actionsToSlots[actions] = slots + actionsCount = actionsCount + 1 + actionsList[actionsCount] = actions + else + local slots = actionsToSlots[actions] + local count = slots._count + 1 + slots[count] = slot + slots._count = count end - table.insert(actionsToSlots[actions], slot) end -- Test each unique actions object once and send events to ALL slots that share it - for actions, slots in pairs(actionsToSlots) do + for i = 1, actionsCount do + local actions = actionsList[i] + local slots = actionsToSlots[actions] local stateChanged = CleveRoids.TestForActiveAction(actions) if stateChanged then -- Send event to ALL slots that use this macro - for _, slot in ipairs(slots) do - CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot) + local count = slots._count + for j = 1, count do + CleveRoids.SendEventForAction(slots[j], "ACTIONBAR_SLOT_CHANGED", slots[j]) end end + -- Clear for reuse (reset count and clear buffer reference) + for j = 1, slots._count do + slots[j] = nil + end + slots._count = 0 + actionsToSlots[actions] = nil + actionsList[i] = nil -- Clear actionsList entry for reuse end end @@ -903,10 +1028,14 @@ function CleveRoids.GetActiveAction(slot) return action and action.active end +-- PERFORMANCE: Static buffer for arg backup +local _originalArgsBuffer = CleveRoids._originalArgsBuffer + function CleveRoids.SendEventForAction(slot, event, ...) local old_this = this - local original_global_args = {} + -- PERFORMANCE: Reuse static buffer instead of creating table each call + local original_global_args = _originalArgsBuffer for i = 1, 10 do original_global_args[i] = _G["arg" .. i] end @@ -1857,10 +1986,16 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo castMsg = msg .. "(" .. rank .. ")" end end - if CleveRoids.hasSuperwow and action == CastSpellByName and conditionals.target then - CastSpellByName(castMsg, conditionals.target) -- SuperWoW handles targeting via argument - elseif action == CastSpellByName then - action(castMsg) + if action == CastSpellByName then + -- Use smart cast for spell queuing based on Nampower settings + local API = CleveRoids.NampowerAPI + if API and API.SmartCast then + API.SmartCast(castMsg, conditionals.target) + elseif CleveRoids.hasSuperwow and conditionals.target then + CastSpellByName(castMsg, conditionals.target) + else + CastSpellByName(castMsg) + end else -- For other actions like UseContainerItem etc. action(msg) @@ -2262,14 +2397,36 @@ function CleveRoids.DoUse(msg) return end - -- Resolve by name/id via our cache - local item = CleveRoids.GetItem(msg) -- looks in equipped, then bags + -- PERFORMANCE: Try fast lookup methods first, fall back to full scan + -- Full scan is now optimized with GetNameFromLink() instead of GetItemInfo() + local item + + -- 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 @@ -2278,8 +2435,43 @@ function CleveRoids.DoUse(msg) -- 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) + 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 + end + end + end return end end @@ -2300,51 +2492,76 @@ function CleveRoids.EquipBagItem(msg, offhand) local invslot = offhand and 17 or 16 - -- Check if item is already equipped (fast path) - local currentItemLink = GetInventoryItemLink("player", invslot) - if currentItemLink then - local _, _, currentID = string.find(currentItemLink, "item:(%d+)") - local currentItemName = GetItemInfo(currentItemLink) - local msgLower = string.lower(msg) + -- PERFORMANCE: Fast check if already equipped (single function call) + if CleveRoids.IsItemEquipped and CleveRoids.IsItemEquipped(msg, invslot) then + return true + end - if currentID then - local _, _, msgID = string.find(msg, "(%d+)") - if msgID and tonumber(msgID) == tonumber(currentID) then - return true -- Already equipped + -- Note what's currently in the target slot so we can invalidate its cache + local oldSlotLink = GetInventoryItemLink("player", invslot) + local oldSlotName = nil + if oldSlotLink then + local _, _, name = string_find(oldSlotLink, "|h%[(.-)%]|h") + oldSlotName = name + end + + -- Helper to invalidate displaced item's cache + local function InvalidateDisplacedItem() + if oldSlotName and CleveRoids.Items then + CleveRoids.Items[oldSlotName] = nil + CleveRoids.Items[string_lower(oldSlotName)] = nil + end + end + + -- PERFORMANCE: Try EquipItemByName FIRST before any lookups + -- This is the fastest path - no item lookup, no cursor operations + if EquipItemByName then + local ok = pcall(EquipItemByName, msg, invslot) + if ok then + -- Invalidate cache entry if it exists + if CleveRoids.Items then + CleveRoids.Items[msg] = nil + CleveRoids.Items[string_lower(msg)] = nil end - end - if currentItemName and string.lower(currentItemName) == msgLower then - return true -- Already equipped - end - end - - -- Item lookup - local item = CleveRoids.GetItem(msg) - if not item or not item.name then - CleveRoids.IndexItems() - item = CleveRoids.GetItem(msg) - if not item or not item.name then - return false - end - end - - -- Double-check it's not already equipped (by item ID after lookup) - if currentItemLink and item.id then - local _, _, currentID = string.find(currentItemLink, "item:(%d+)") - if currentID and tonumber(currentID) == tonumber(item.id) then + InvalidateDisplacedItem() return true end end + -- PERFORMANCE: Use fast lookup first, fall back to full scan + -- Full scan is now optimized with GetNameFromLink() instead of GetItemInfo() + local item = CleveRoids.GetItemFast and CleveRoids.GetItemFast(msg) + if not item then + -- Try quick targeted scan (stops when found) + item = CleveRoids.FindItemQuick and CleveRoids.FindItemQuick(msg) + end + if not item then + -- Full scan fallback (now optimized, safe during combat) + item = CleveRoids.GetItem(msg) + end + + if not item or not item.name then + return false + end + + -- Already equipped check (by item ID from lookup) + if item.inventoryID == invslot then + return true + end + if not item.bagID and not item.inventoryID then return false end - -- Try EquipItemByName first (fastest, no cursor operations) - if item.name and EquipItemByName then + -- Try EquipItemByName with resolved name (in case msg was partial/different case) + if item.name and EquipItemByName and item.name ~= msg then local ok = pcall(EquipItemByName, item.name, invslot) if ok then - CleveRoids.Items[item.name] = nil + if CleveRoids.Items then + CleveRoids.Items[item.name] = nil + CleveRoids.Items[string_lower(item.name)] = nil + end + InvalidateDisplacedItem() return true end end @@ -2352,18 +2569,13 @@ function CleveRoids.EquipBagItem(msg, offhand) -- Fallback: Manual pickup and equip CleveRoids.equipInProgress = true - if type(CloseStackSplitFrame) == "function" then - CloseStackSplitFrame() - end + -- PERFORMANCE: Single cursor check at start if CursorHasItem and CursorHasItem() then ClearCursor() - CleveRoids.equipInProgress = false - return false end local pickupSuccess = false if item.bagID and item.slot then - CleveRoids.GetNextBagSlotForUse(item, msg) PickupContainerItem(item.bagID, item.slot) pickupSuccess = CursorHasItem and CursorHasItem() elseif item.inventoryID then @@ -2380,7 +2592,11 @@ function CleveRoids.EquipBagItem(msg, offhand) EquipCursorItem(invslot) ClearCursor() - CleveRoids.Items[item.name] = nil + if CleveRoids.Items and item.name then + CleveRoids.Items[item.name] = nil + CleveRoids.Items[string_lower(item.name)] = nil + end + InvalidateDisplacedItem() CleveRoids.equipInProgress = false return true end @@ -2523,7 +2739,13 @@ function CleveRoids.DoCastSequence(sequence) local r = (sp and sp.rank) or (sp and sp.highest and sp.highest.rank) if r and r ~= "" then msg = msg .. "(" .. r .. ")" end end - CastSpellByName(msg) + -- Use smart cast for spell queuing based on Nampower settings + local API = CleveRoids.NampowerAPI + if API and API.SmartCast then + API.SmartCast(msg) + else + CastSpellByName(msg) + end return true end @@ -2557,18 +2779,55 @@ end CleveRoids.lastCleanupTime = 0 CleveRoids.CLEANUP_INTERVAL = 5 -- Run cleanup every 5 seconds +-- PERFORMANCE: Upvalues for OnUpdate hot path +local GetTime = GetTime +local UnitAffectingCombat = UnitAffectingCombat +local pairs = pairs + function CleveRoids.OnUpdate(self) - -- PERFORMANCE: Equipment queue now processed by self-disabling equipQueueFrame - -- This eliminates per-frame checks when queue is empty + -- PERFORMANCE: Single GetTime() call per frame + local time = GetTime() + + -- PERFORMANCE: Early exit if not ready (before any other checks) + if not CleveRoids.ready then + -- Handle initialization timer only when not ready + if CleveRoids.initializationTimer and time >= CleveRoids.initializationTimer then + CleveRoids.IndexItems() + CleveRoids.IndexActionBars() + CleveRoids.ready = true + CleveRoids.initializationTimer = nil + CleveRoids.TestForAllActiveActions() + CleveRoids.lastUpdate = time + end + return + end + + -- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division) + local refreshRate = CleveRoids.cachedRefreshRate + if not refreshRate then + refreshRate = 1 / (CleveRoidMacros.refresh or 5) + CleveRoids.cachedRefreshRate = refreshRate + end + + -- PERFORMANCE: Throttle check FIRST - skip most work on non-throttled frames + local lastUpdate = CleveRoids.lastUpdate or 0 + local bypassThrottle = CleveRoids.isActionUpdateQueued and CleveRoidMacros.realtime == 0 + local shouldUpdate = bypassThrottle or (time - lastUpdate) >= refreshRate + + if not shouldUpdate then + return -- Early exit for non-throttled frames + end + + CleveRoids.lastUpdate = time -- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED) - -- Skip entirely in combat - EquipBagItem handles cache invalidation - if CleveRoids.equipIndexPendingTime and not UnitAffectingCombat("player") then - local now = GetTime() - if (now - (CleveRoids.lastEquipIndexTime or 0)) >= 0.2 then - CleveRoids.lastEquipIndexTime = now + -- PERFORMANCE: Skip check entirely if no pending update + local pendingTime = CleveRoids.equipIndexPendingTime + if pendingTime and not UnitAffectingCombat("player") then + if (time - (CleveRoids.lastEquipIndexTime or 0)) >= 0.2 then + CleveRoids.lastEquipIndexTime = time CleveRoids.equipIndexPendingTime = nil - CleveRoids.lastItemIndexTime = now + CleveRoids.lastItemIndexTime = time CleveRoids.IndexItems() CleveRoids.Actions = {} CleveRoids.Macros = {} @@ -2580,68 +2839,34 @@ function CleveRoids.OnUpdate(self) end end - -- PERFORMANCE OPTIMIZATION: Removed UpdateCastingState() polling - -- Casting state is now fully event-driven via SPELLCAST_* events - -- This eliminates continuous GetCurrentCastingInfo() polling - - local time = GetTime() - local refreshRate = CleveRoidMacros.refresh or 5 - refreshRate = 1/refreshRate - if CleveRoids.initializationTimer and time >= CleveRoids.initializationTimer then - CleveRoids.IndexItems() - CleveRoids.IndexActionBars() - CleveRoids.ready = true - CleveRoids.initializationTimer = nil - CleveRoids.TestForAllActiveActions() - CleveRoids.lastUpdate = time - return - end - if not CleveRoids.ready then return end - - -- Check for expired reactive procs BEFORE throttle to ensure immediate icon updates - if CleveRoids.reactiveProcs then + -- PERFORMANCE: Check for expired reactive procs only if we have any + -- Use statically allocated removal buffer to avoid per-frame allocation + local reactiveProcs = CleveRoids.reactiveProcs + if reactiveProcs then local hasExpiredProc = false - for spellName, procData in pairs(CleveRoids.reactiveProcs) do + local toRemove = CleveRoids._procRemovalBuffer + local removeCount = 0 + + for spellName, procData in pairs(reactiveProcs) do if procData and procData.expiry and time >= procData.expiry then - -- DEBUG: Show proc expiration - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff9900[REACTIVE PROC]|r %s expired at time=%.3f", spellName, time) - ) - end - CleveRoids.reactiveProcs[spellName] = nil + removeCount = removeCount + 1 + toRemove[removeCount] = spellName hasExpiredProc = true end end - -- If any proc expired, immediately update all actions to refresh icon states + + -- Remove expired procs using indexed array (no pairs() overhead) + for i = 1, removeCount do + reactiveProcs[toRemove[i]] = nil + toRemove[i] = nil -- Clear for next use + end + + -- If any proc expired, immediately update all actions if hasExpiredProc then - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[REACTIVE PROC]|r Forcing immediate TestForAllActiveActions()") - -- Debug: Show how many actions we're checking - local actionCount = 0 - for _ in pairs(CleveRoids.Actions) do - actionCount = actionCount + 1 - end - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff9900[REACTIVE PROC]|r Checking %d action slots", actionCount) - ) - end - -- Force immediate update (don't just queue it) CleveRoids.TestForAllActiveActions() - CleveRoids.isActionUpdateQueued = false -- Clear queue flag since we just processed - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[REACTIVE PROC]|r TestForAllActiveActions() completed") - end + CleveRoids.isActionUpdateQueued = false end end - - -- Throttle the update loop to avoid excessive CPU usage. - -- HOWEVER: If an action update is queued, allow it to bypass throttle for reactive abilities - local bypassThrottle = CleveRoids.isActionUpdateQueued and CleveRoidMacros.realtime == 0 - if not bypassThrottle and (time - CleveRoids.lastUpdate) < refreshRate then - return - end - CleveRoids.lastUpdate = time -- Check the saved variable to decide which update mode to use. if CleveRoidMacros.realtime == 1 then -- Realtime Mode: Force an update on every throttled tick for maximum responsiveness. @@ -3485,9 +3710,6 @@ end -- Nampower SPELL_CAST_EVENT handler for reliable channel tracking -- This is the PRIMARY source of truth for channel state (not GetCurrentCastingInfo polling) function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGuid, itemId) - DEFAULT_CHAT_FRAME:AddMessage(string.format("[SPELL_CAST_EVENT] success=%s, spellId=%s, castType=%s", - tostring(success), tostring(spellId), tostring(castType))) - local CHANNEL = 4 if castType == CHANNEL and success == 1 then @@ -3495,10 +3717,9 @@ function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGui CleveRoids.CurrentSpell.type = "channeled" CleveRoids.CurrentSpell.castingSpellId = spellId - local spellName = SpellInfo(spellId) + local spellName = SpellInfo and SpellInfo(spellId) if spellName then CleveRoids.CurrentSpell.spellName = spellName - DEFAULT_CHAT_FRAME:AddMessage(string.format("[SPELL_CAST_EVENT] Channel START: %s", spellName)) end -- Force immediate action update @@ -4048,6 +4269,7 @@ SlashCmdList["CLEVEROID"] = function(msg) local num = tonumber(val) if num and num >= 1 and num <= 10 then CleveRoidMacros.refresh = num + CleveRoids.cachedRefreshRate = nil -- Invalidate cached rate CleveRoids.Print("refresh set to " .. num .. " times per second") else CleveRoids.Print("Usage: /cleveroid refresh X - Set refresh rate. (1 to 10 updates per second. Default: 5)") diff --git a/Extensions/Tooltip/Generic.lua b/Extensions/Tooltip/Generic.lua index a6976ff..390e4f9 100644 --- a/Extensions/Tooltip/Generic.lua +++ b/Extensions/Tooltip/Generic.lua @@ -25,6 +25,13 @@ 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() @@ -516,8 +523,9 @@ function CleveRoids.GetItem(text) if qid and itemID and qid == itemID then return makeInventoryItem(inv, link, Items) - elseif qname and itemID then - local nm = GetItemInfo(itemID) + 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 @@ -536,8 +544,9 @@ function CleveRoids.GetItem(text) if qid and itemID and qid == itemID then return makeBagItem(bag, slot, link, Items) - elseif qname and itemID then - local nm = GetItemInfo(itemID) + 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 @@ -572,6 +581,144 @@ function CleveRoids.GetNextBagSlotForUse(item, text) 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. +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 + + -- Quick scan equipped items first (only 19 slots) + for inv = 1, 19 do + local link = GetInventoryItemLink("player", inv) + if link then + local _, _, itemID = string_find(link, "item:(%d+)") + if itemID then + itemID = tonumber(itemID) + -- 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 + 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 = string_find(link, "item:(%d+)") + if itemID then + itemID = tonumber(itemID) + -- 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 + 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 link = GetInventoryItemLink("player", inventoryId) + if not link then return false end + + local _, _, currentID = string_find(link, "item:(%d+)") + if not currentID then return false end + + -- Check by ID (fast path) + local textId = tonumber(text) + if textId and textId == tonumber(currentID) then + return true + end + + -- Check by name - extract from link instead of GetItemInfo for performance + local currentName = GetNameFromLink(link) + if currentName then + local textLower = string_lower(text) + if string_lower(currentName) == textLower then + return true + end + end + + return false +end + local Extension = CleveRoids.RegisterExtension("Generic_show") Extension.RegisterEvent("SPELLS_CHANGED", "SPELLS_CHANGED") diff --git a/Init.lua b/Init.lua index 71ac5a2..8c496f7 100644 --- a/Init.lua +++ b/Init.lua @@ -105,6 +105,17 @@ CleveRoids.EQUIP_GLOBAL_COOLDOWN = 0.5 -- Global cooldown -- PERFORMANCE: Table pool for queue entries to reduce garbage collection CleveRoids.queueEntryPool = {} +-- PERFORMANCE: Static buffer for proc removal to avoid per-frame allocation +CleveRoids._procRemovalBuffer = {} + +-- PERFORMANCE: Static buffer for action grouping to avoid per-call allocation +CleveRoids._actionsToSlotsBuffer = {} +CleveRoids._slotsBuffer = {} +CleveRoids._actionsListBuffer = {} + +-- PERFORMANCE: Static buffer for arg backup in SendEventForAction +CleveRoids._originalArgsBuffer = {} + -- Spell queue state (Nampower) CleveRoids.queuedSpell = nil CleveRoids.lastCastSpell = nil @@ -189,11 +200,22 @@ CleveRoids.WeaponTypeNames = { CleveRoids.hasNampower = (QueueSpellByName ~= nil) CleveRoids.hasUnitXP = pcall(UnitXP, "nop", "nop") +-- Extended Nampower feature flags (populated by NampowerAPI.lua) +CleveRoids.nampowerVersion = { major = 0, minor = 0, patch = 0 } +CleveRoids.hasExtendedNampower = false -- True if v2.12+ with new API functions + -- Feature detection messages local function PrintFeatures() local features = {} if CleveRoids.hasSuperwow then table.insert(features, "SuperWoW") end - if CleveRoids.hasNampower then table.insert(features, "Nampower") end + if CleveRoids.hasNampower then + local ver = CleveRoids.nampowerVersion + if ver.major > 0 then + table.insert(features, string.format("Nampower v%d.%d.%d", ver.major, ver.minor, ver.patch)) + else + table.insert(features, "Nampower") + end + end if CleveRoids.hasUnitXP then table.insert(features, "UnitXP") end if CleveRoids.hasTurtle then table.insert(features, "Turtle") end @@ -207,6 +229,31 @@ local initFrame = CreateFrame("Frame") initFrame:RegisterEvent("PLAYER_ENTERING_WORLD") initFrame:SetScript("OnEvent", function() this:UnregisterAllEvents() + + -- Initialize NampowerAPI if available + if CleveRoids.NampowerAPI then + local API = CleveRoids.NampowerAPI + + -- Get version info + local major, minor, patch = API.GetVersion() + CleveRoids.nampowerVersion = { major = major, minor = minor, patch = patch } + + -- Check for extended API (v2.12+) + CleveRoids.hasExtendedNampower = API.HasMinimumVersion(2, 12, 0) + + -- Sync feature flags + if API.features then + CleveRoids.hasGetSpellRec = API.features.hasGetSpellRec + CleveRoids.hasGetItemStats = API.features.hasGetItemStats + CleveRoids.hasGetUnitData = API.features.hasGetUnitData + CleveRoids.hasGetSpellModifiers = API.features.hasGetSpellModifiers + CleveRoids.hasEnhancedSpellFunctions = API.features.hasEnhancedSpellFunctions + end + + -- Initialize the API + API.Initialize() + end + PrintFeatures() end) diff --git a/NampowerAPI.lua b/NampowerAPI.lua new file mode 100644 index 0000000..145137a --- /dev/null +++ b/NampowerAPI.lua @@ -0,0 +1,1065 @@ +--[[ + NampowerAPI.lua - Nampower API Integration Layer + + Provides wrapper functions for Nampower's extended Lua API with fallbacks + for older versions or when functions are unavailable. + + New Nampower Functions Wrapped: + - GetSpellRec / GetSpellRecField - Spell record data from client DB + - GetItemStats / GetItemStatsField - Item stats from client DB + - GetUnitData / GetUnitField - Low-level unit field access + - GetSpellModifiers - Spell modifier calculations (talents, buffs, etc.) + + Enhanced Functions (now accept spell name or "spellId:number"): + - GetSpellTexture, GetSpellName, GetSpellCooldown, GetSpellAutocast + - ToggleSpellAutocast, PickupSpell, CastSpell, IsCurrentCast, IsSpellPassive + + Settings Integration: + - Reads from NampowerSettings addon when available + - Falls back to CVars when addon not present +]] + +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids + +-- Nampower API namespace +CleveRoids.NampowerAPI = CleveRoids.NampowerAPI or {} +local API = CleveRoids.NampowerAPI + +-------------------------------------------------------------------------------- +-- VERSION DETECTION +-------------------------------------------------------------------------------- + +-- Get Nampower version as major, minor, patch +-- Returns 0, 0, 0 if Nampower not installed +function API.GetVersion() + if GetNampowerVersion then + local major, minor, patch = GetNampowerVersion() + return major or 0, minor or 0, patch or 0 + end + return 0, 0, 0 +end + +-- Check if installed version meets minimum requirement +function API.HasMinimumVersion(reqMajor, reqMinor, reqPatch) + local major, minor, patch = API.GetVersion() + + if major > reqMajor then + return true + elseif major == reqMajor then + if minor > reqMinor then + return true + elseif minor == reqMinor and patch >= reqPatch then + return true + end + end + return false +end + +-- Feature flags based on version +API.features = { + -- Base Nampower features (v2.0+) + hasNampower = (QueueSpellByName ~= nil), + hasSpellQueue = (QueueSpellByName ~= nil), + hasIsSpellInRange = (IsSpellInRange ~= nil), + hasGetCurrentCastingInfo = (GetCurrentCastingInfo ~= nil), + hasGetSpellIdForName = (GetSpellIdForName ~= nil), + + -- Extended API (v2.8+) + hasGetSpellRec = (GetSpellRec ~= nil), + hasGetSpellRecField = (GetSpellRecField ~= nil), + hasGetItemStats = (GetItemStats ~= nil), + hasGetItemStatsField = (GetItemStatsField ~= nil), + hasGetUnitData = (GetUnitData ~= nil), + hasGetUnitField = (GetUnitField ~= nil), + hasGetSpellModifiers = (GetSpellModifiers ~= nil), + + -- Enhanced spell functions (accept name/"spellId:number") + hasEnhancedSpellFunctions = false, -- Detected at runtime +} + +-- Detect if enhanced spell functions are available (accept name/spellId:number) +local function DetectEnhancedSpellFunctions() + if not GetSpellTexture then return false end + + -- Try calling GetSpellTexture with a spell name + -- If it doesn't error and returns something, enhanced functions are available + local success, result = pcall(function() + -- Use "Attack" which should exist for all characters + return GetSpellTexture("Attack") + end) + + API.features.hasEnhancedSpellFunctions = success and (result ~= nil) + return API.features.hasEnhancedSpellFunctions +end + +-------------------------------------------------------------------------------- +-- NAMPOWER SETTINGS ACCESS +-------------------------------------------------------------------------------- + +-- Cache for settings to avoid repeated lookups +API.settingsCache = {} +API.settingsCacheTime = 0 +API.SETTINGS_CACHE_DURATION = 5 -- Refresh every 5 seconds + +-- All known Nampower CVars with their defaults +API.defaultSettings = { + NP_QueueCastTimeSpells = "1", + NP_QueueInstantSpells = "1", + NP_QueueChannelingSpells = "1", + NP_QueueTargetingSpells = "1", + NP_QueueOnSwingSpells = "0", + NP_QueueSpellsOnCooldown = "1", + NP_InterruptChannelsOutsideQueueWindow = "0", + NP_SpellQueueWindowMs = "500", + NP_OnSwingBufferCooldownMs = "500", + NP_ChannelQueueWindowMs = "1500", + NP_TargetingQueueWindowMs = "500", + NP_CooldownQueueWindowMs = "250", + NP_MinBufferTimeMs = "55", + NP_NonGcdBufferTimeMs = "100", + NP_MaxBufferIncreaseMs = "30", + NP_RetryServerRejectedSpells = "1", + NP_QuickcastTargetingSpells = "0", + NP_ReplaceMatchingNonGcdCategory = "0", + NP_OptimizeBufferUsingPacketTimings = "0", + NP_PreventRightClickTargetChange = "0", + NP_PreventRightClickPvPAttack = "1", + NP_DoubleCastToEndChannelEarly = "0", + NP_SpamProtectionEnabled = "1", + NP_ChannelLatencyReductionPercentage = "75", + NP_NameplateDistance = "41", +} + +-- Get a Nampower setting value +-- Priority: NampowerSettings addon (per-character) > CVar > default +function API.GetSetting(settingName) + -- Check if NampowerSettings addon is loaded and has per-character settings enabled + if Nampower and Nampower.db and Nampower.db.profile then + if Nampower.db.profile.per_character_settings and Nampower.db.profile[settingName] ~= nil then + return Nampower.db.profile[settingName] + end + end + + -- Fall back to CVar + local cvarValue = GetCVar(settingName) + if cvarValue then + return cvarValue + end + + -- Return default if known + return API.defaultSettings[settingName] +end + +-- Get a Nampower setting as a boolean +function API.GetSettingBool(settingName) + local value = API.GetSetting(settingName) + if type(value) == "boolean" then + return value + end + return value == "1" or value == true +end + +-- Get a Nampower setting as a number +function API.GetSettingNumber(settingName) + local value = API.GetSetting(settingName) + return tonumber(value) or 0 +end + +-- Check if spell queuing is enabled for a given spell type +function API.IsQueueingEnabled(spellType) + if not API.features.hasNampower then + return false + end + + if spellType == "cast" or spellType == "casttime" then + return API.GetSettingBool("NP_QueueCastTimeSpells") + elseif spellType == "instant" or spellType == "gcd" then + return API.GetSettingBool("NP_QueueInstantSpells") + elseif spellType == "channel" or spellType == "channeling" then + return API.GetSettingBool("NP_QueueChannelingSpells") + elseif spellType == "targeting" or spellType == "aoe" then + return API.GetSettingBool("NP_QueueTargetingSpells") + elseif spellType == "onswing" or spellType == "swing" then + return API.GetSettingBool("NP_QueueOnSwingSpells") + elseif spellType == "cooldown" then + return API.GetSettingBool("NP_QueueSpellsOnCooldown") + end + + -- Default: all queuing enabled + return API.GetSettingBool("NP_QueueCastTimeSpells") +end + +-- Get the queue window for a given spell type (in seconds) +function API.GetQueueWindow(spellType) + local ms = 500 -- default + + if spellType == "channel" or spellType == "channeling" then + ms = API.GetSettingNumber("NP_ChannelQueueWindowMs") + elseif spellType == "targeting" or spellType == "aoe" then + ms = API.GetSettingNumber("NP_TargetingQueueWindowMs") + elseif spellType == "cooldown" then + ms = API.GetSettingNumber("NP_CooldownQueueWindowMs") + elseif spellType == "onswing" or spellType == "swing" then + ms = API.GetSettingNumber("NP_OnSwingBufferCooldownMs") + else + ms = API.GetSettingNumber("NP_SpellQueueWindowMs") + end + + return ms / 1000 -- Convert to seconds +end + +-------------------------------------------------------------------------------- +-- SPELL RECORD API (GetSpellRec / GetSpellRecField) +-------------------------------------------------------------------------------- + +-- Cache for spell records +API.spellRecCache = {} + +-- Get full spell record data +-- Returns nil if spell not found or API unavailable +function API.GetSpellRecord(spellId) + if not spellId or spellId == 0 then return nil end + + -- Check cache first + if API.spellRecCache[spellId] then + return API.spellRecCache[spellId] + end + + -- Use native function if available + if GetSpellRec then + local rec = GetSpellRec(spellId) + if rec then + API.spellRecCache[spellId] = rec + return rec + end + end + + return nil +end + +-- Get a specific field from spell record +-- Returns nil if not found, raises error if field name invalid (native behavior) +function API.GetSpellField(spellId, fieldName) + if not spellId or spellId == 0 then return nil end + + -- Use native function if available (more efficient for single field) + if GetSpellRecField then + local success, result = pcall(GetSpellRecField, spellId, fieldName) + if success then + return result + end + -- If it errors on invalid field name, let it propagate + -- But if spell just not found, return nil + return nil + end + + -- Fallback to full record lookup + local rec = API.GetSpellRecord(spellId) + if rec then + return rec[fieldName] + end + + return nil +end + +-- Get spell name by ID (enhanced, uses new API when available) +function API.GetSpellNameById(spellId) + if not spellId or spellId == 0 then return nil end + + -- Try GetSpellNameAndRankForId first (Nampower function) + if GetSpellNameAndRankForId then + local name, rank = GetSpellNameAndRankForId(spellId) + if name then return name, rank end + end + + -- Fall back to SpellInfo (SuperWoW) + if SpellInfo then + local name = SpellInfo(spellId) + if name then return name end + end + + -- Fall back to GetSpellRecField + if GetSpellRecField then + local name = GetSpellRecField(spellId, "name") + local rank = GetSpellRecField(spellId, "rank") + if name then return name, rank end + end + + return nil +end + +-- Get spell cast time in seconds +function API.GetSpellCastTime(spellId) + local castTime = API.GetSpellField(spellId, "castTime") + if castTime then + return castTime / 1000 -- Convert from ms to seconds + end + return nil +end + +-- Get spell school (0=Physical, 1=Holy, 2=Fire, 3=Nature, 4=Frost, 5=Shadow, 6=Arcane) +function API.GetSpellSchool(spellId) + return API.GetSpellField(spellId, "school") +end + +-- Get spell mana cost +function API.GetSpellManaCost(spellId) + return API.GetSpellField(spellId, "manaCost") +end + +-- Get spell range (max range in yards) +function API.GetSpellRange(spellId) + local rangeMax = API.GetSpellField(spellId, "rangeMax") + if rangeMax then + return rangeMax / 10 -- Convert from game units to yards + end + return nil +end + +-- Get spell cooldown from record (base cooldown, not current) +function API.GetSpellBaseCooldown(spellId) + local recoveryTime = API.GetSpellField(spellId, "recoveryTime") + if recoveryTime then + return recoveryTime / 1000 -- Convert from ms to seconds + end + return nil +end + +-------------------------------------------------------------------------------- +-- ITEM STATS API (GetItemStats / GetItemStatsField) +-------------------------------------------------------------------------------- + +-- Cache for item stats +API.itemStatsCache = {} + +-- Get full item stats data +function API.GetItemRecord(itemId) + if not itemId or itemId == 0 then return nil end + + -- Check cache first + if API.itemStatsCache[itemId] then + return API.itemStatsCache[itemId] + end + + -- Use native function if available + if GetItemStats then + local stats = GetItemStats(itemId) + if stats then + API.itemStatsCache[itemId] = stats + return stats + end + end + + return nil +end + +-- Get a specific field from item stats +function API.GetItemField(itemId, fieldName) + if not itemId or itemId == 0 then return nil end + + -- Use native function if available (more efficient) + if GetItemStatsField then + local success, result = pcall(GetItemStatsField, itemId, fieldName) + if success then + return result + end + return nil + end + + -- Fallback to full record lookup + local stats = API.GetItemRecord(itemId) + if stats then + return stats[fieldName] + end + + return nil +end + +-- Get item level +function API.GetItemLevel(itemId) + -- Use dedicated function if available + if GetItemLevel then + local success, result = pcall(GetItemLevel, itemId) + if success then return result end + end + + return API.GetItemField(itemId, "itemLevel") +end + +-- Get item name +function API.GetItemName(itemId) + return API.GetItemField(itemId, "displayName") +end + +-- Get item quality (0=Poor, 1=Common, 2=Uncommon, 3=Rare, 4=Epic, 5=Legendary) +function API.GetItemQuality(itemId) + return API.GetItemField(itemId, "quality") +end + +-- Get weapon speed in seconds +function API.GetWeaponSpeed(itemId) + local delay = API.GetItemField(itemId, "delay") + if delay then + return delay / 1000 + end + return nil +end + +-------------------------------------------------------------------------------- +-- UNIT DATA API (GetUnitData / GetUnitField) +-------------------------------------------------------------------------------- + +-- Get full unit data (no caching - unit data changes frequently) +function API.GetUnitRecord(unitToken) + if not unitToken then return nil end + + if GetUnitData then + return GetUnitData(unitToken) + end + + return nil +end + +-- Get a specific unit field +function API.GetUnitFieldValue(unitToken, fieldName) + if not unitToken then return nil end + + -- Use native function if available (more efficient) + if GetUnitField then + local success, result = pcall(GetUnitField, unitToken, fieldName) + if success then + return result + end + return nil + end + + -- Fallback to full record + local data = API.GetUnitRecord(unitToken) + if data then + return data[fieldName] + end + + return nil +end + +-- Get unit's current auras as spell IDs +function API.GetUnitAuras(unitToken) + return API.GetUnitFieldValue(unitToken, "aura") +end + +-- Get unit resistances table +function API.GetUnitResistances(unitToken) + return API.GetUnitFieldValue(unitToken, "resistances") +end + +-- Get specific resistance value +-- school: 1=Armor, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow, 7=Arcane +function API.GetUnitResistance(unitToken, school) + local resistances = API.GetUnitResistances(unitToken) + if resistances and school then + return resistances[school] + end + return nil +end + +-- Check if unit has a specific aura by spell ID +function API.UnitHasAura(unitToken, spellId) + local auras = API.GetUnitAuras(unitToken) + if not auras or not spellId then return false end + + for _, auraId in ipairs(auras) do + if auraId == spellId then + return true + end + end + return false +end + +-------------------------------------------------------------------------------- +-- SPELL MODIFIERS API (GetSpellModifiers) +-------------------------------------------------------------------------------- + +-- Modifier type constants +API.MODIFIER_DAMAGE = 0 +API.MODIFIER_DURATION = 1 +API.MODIFIER_THREAT = 2 +API.MODIFIER_ATTACK_POWER = 3 +API.MODIFIER_CHARGES = 4 +API.MODIFIER_RANGE = 5 +API.MODIFIER_RADIUS = 6 +API.MODIFIER_CRITICAL_CHANCE = 7 +API.MODIFIER_ALL_EFFECTS = 8 +API.MODIFIER_NOT_LOSE_CASTING_TIME = 9 +API.MODIFIER_CASTING_TIME = 10 +API.MODIFIER_COOLDOWN = 11 +API.MODIFIER_SPEED = 12 +API.MODIFIER_COST = 14 +API.MODIFIER_CRIT_DAMAGE_BONUS = 15 +API.MODIFIER_RESIST_MISS_CHANCE = 16 +API.MODIFIER_JUMP_TARGETS = 17 +API.MODIFIER_CHANCE_OF_SUCCESS = 18 +API.MODIFIER_ACTIVATION_TIME = 19 +API.MODIFIER_EFFECT_PAST_FIRST = 20 +API.MODIFIER_CASTING_TIME_OLD = 21 +API.MODIFIER_DOT = 22 +API.MODIFIER_HASTE = 23 +API.MODIFIER_SPELL_BONUS_DAMAGE = 24 +API.MODIFIER_MULTIPLE_VALUE = 27 +API.MODIFIER_RESIST_DISPEL_CHANCE = 28 + +-- Get spell modifiers +-- Returns: flatMod, percentMod, hasModifier +function API.GetModifiers(spellId, modifierType) + if not GetSpellModifiers then + return 0, 0, false + end + + if not spellId or spellId == 0 then + return 0, 0, false + end + + local flat, percent, ret = GetSpellModifiers(spellId, modifierType) + return flat or 0, percent or 0, ret and ret ~= 0 +end + +-- Get duration modifier for a spell (useful for debuff tracking) +-- Returns modified duration given base duration +function API.GetModifiedDuration(spellId, baseDuration) + if not baseDuration then return nil end + + local flat, percent, hasModifier = API.GetModifiers(spellId, API.MODIFIER_DURATION) + + if not hasModifier then + return baseDuration + end + + -- Apply flat modifier first, then percentage + local modified = baseDuration + flat + if percent ~= 0 then + modified = modified * (1 + percent / 100) + end + + return modified +end + +-- Get damage modifier for a spell +function API.GetDamageModifier(spellId) + local flat, percent = API.GetModifiers(spellId, API.MODIFIER_DAMAGE) + return flat, percent +end + +-- Get cooldown modifier for a spell +function API.GetCooldownModifier(spellId) + local flat, percent = API.GetModifiers(spellId, API.MODIFIER_COOLDOWN) + return flat, percent +end + +-- Get cast time modifier for a spell +function API.GetCastTimeModifier(spellId) + local flat, percent = API.GetModifiers(spellId, API.MODIFIER_CASTING_TIME) + return flat, percent +end + +-- Get cost modifier for a spell +function API.GetCostModifier(spellId) + local flat, percent = API.GetModifiers(spellId, API.MODIFIER_COST) + return flat, percent +end + +-------------------------------------------------------------------------------- +-- ENHANCED SPELL LOOKUP (using new "spellId:number" and name syntax) +-------------------------------------------------------------------------------- + +-- Convert spell identifier to spellId:number format if needed +-- Input can be: spellSlot (number), "spellId:123", or "Spell Name" +function API.NormalizeSpellIdentifier(identifier, bookType) + if not identifier then return nil end + + -- Already in spellId:number format + if type(identifier) == "string" and string.find(identifier, "^spellId:") then + return identifier + end + + -- If it's a number and enhanced functions exist, keep as-is (slot) or convert to spellId + if type(identifier) == "number" then + if API.features.hasEnhancedSpellFunctions then + -- Could be a spell slot - check if we have GetSpellSlotTypeIdForName to get the ID + if identifier > 0 and identifier < 1000 then + -- Likely a spell slot, use as-is + return identifier, bookType or BOOKTYPE_SPELL + else + -- Likely a spell ID, convert to spellId:number format + return "spellId:" .. identifier + end + end + return identifier, bookType + end + + -- It's a spell name + if type(identifier) == "string" then + -- If enhanced functions available, pass name directly + if API.features.hasEnhancedSpellFunctions then + return identifier + end + + -- Fall back to manual lookup + local spell = CleveRoids.GetSpell and CleveRoids.GetSpell(identifier) + if spell then + return spell.spellSlot, spell.bookType + end + end + + return nil +end + +-- Get spell texture (works with name, spellId:number, or slot) +function API.GetSpellTexture(identifier, bookType) + if API.features.hasEnhancedSpellFunctions then + -- Pass directly to native function + local id = API.NormalizeSpellIdentifier(identifier) + if id then + return GetSpellTexture(id, bookType) + end + else + -- Fall back to slot-based lookup + local slot, book = API.NormalizeSpellIdentifier(identifier, bookType) + if slot then + return GetSpellTexture(slot, book or BOOKTYPE_SPELL) + end + end + return nil +end + +-- Get spell name (works with name, spellId:number, or slot) +function API.GetSpellName(identifier, bookType) + if API.features.hasEnhancedSpellFunctions then + local id = API.NormalizeSpellIdentifier(identifier) + if id then + return GetSpellName(id, bookType) + end + else + local slot, book = API.NormalizeSpellIdentifier(identifier, bookType) + if slot then + return GetSpellName(slot, book or BOOKTYPE_SPELL) + end + end + return nil +end + +-- Get spell cooldown (works with name, spellId:number, or slot) +-- Returns: start, duration (same as native) +function API.GetSpellCooldown(identifier, bookType) + if API.features.hasEnhancedSpellFunctions then + local id = API.NormalizeSpellIdentifier(identifier) + if id then + return GetSpellCooldown(id, bookType) + end + else + local slot, book = API.NormalizeSpellIdentifier(identifier, bookType) + if slot then + return GetSpellCooldown(slot, book or BOOKTYPE_SPELL) + end + end + return nil +end + +-- Check if spell is passive +function API.IsSpellPassive(identifier, bookType) + if API.features.hasEnhancedSpellFunctions then + local id = API.NormalizeSpellIdentifier(identifier) + if id then + return IsSpellPassive(id, bookType) + end + else + local slot, book = API.NormalizeSpellIdentifier(identifier, bookType) + if slot then + return IsSpellPassive(slot, book or BOOKTYPE_SPELL) + end + end + return nil +end + +-------------------------------------------------------------------------------- +-- SPELL ID LOOKUP UTILITIES +-------------------------------------------------------------------------------- + +-- Cache for spell ID lookups +API.spellIdCache = {} + +-- Get spell ID from name (cached) +function API.GetSpellIdFromName(spellName) + if not spellName then return nil end + + -- Check cache + if API.spellIdCache[spellName] then + return API.spellIdCache[spellName] + end + + -- Use Nampower's GetSpellIdForName + if GetSpellIdForName then + local spellId = GetSpellIdForName(spellName) + if spellId and spellId > 0 then + API.spellIdCache[spellName] = spellId + return spellId + end + end + + return nil +end + +-- Get spell slot, book type, and ID from name +function API.GetSpellSlotInfo(spellName) + if not spellName then return nil end + + -- Use Nampower's GetSpellSlotTypeIdForName + if GetSpellSlotTypeIdForName then + local slot, bookType, spellId = GetSpellSlotTypeIdForName(spellName) + if slot and slot > 0 then + return slot, bookType, spellId + end + end + + -- Fallback to CleveRoids.GetSpell + if CleveRoids.GetSpell then + local spell = CleveRoids.GetSpell(spellName) + if spell then + local spellId = API.GetSpellIdFromName(spellName) + return spell.spellSlot, spell.bookType, spellId + end + end + + return nil +end + +-------------------------------------------------------------------------------- +-- INITIALIZATION +-------------------------------------------------------------------------------- + +-- Initialize the API module +function API.Initialize() + -- Detect enhanced spell functions + DetectEnhancedSpellFunctions() + + -- Log available features + local version = string.format("%d.%d.%d", API.GetVersion()) + if API.features.hasNampower then + CleveRoids.Print("NampowerAPI: v" .. version .. " detected") + + local features = {} + if API.features.hasGetSpellRec then table.insert(features, "SpellRec") end + if API.features.hasGetItemStats then table.insert(features, "ItemStats") end + if API.features.hasGetUnitData then table.insert(features, "UnitData") end + if API.features.hasGetSpellModifiers then table.insert(features, "SpellModifiers") end + if API.features.hasEnhancedSpellFunctions then table.insert(features, "EnhancedSpellFuncs") end + + if table.getn(features) > 0 then + CleveRoids.Print(" Extended API: " .. table.concat(features, ", ")) + end + end +end + +-- Clear caches (call on respec/spell change) +function API.ClearCaches() + API.spellRecCache = {} + API.itemStatsCache = {} + API.spellIdCache = {} + API.settingsCache = {} + API.spellTypeCache = {} +end + +-------------------------------------------------------------------------------- +-- CONVENIENCE WRAPPERS FOR EXISTING CODE +-------------------------------------------------------------------------------- + +-- Check if a spell is in range (wrapper around IsSpellInRange with fallbacks) +function API.IsSpellInRange(spellIdentifier, unit) + unit = unit or "target" + + if not IsSpellInRange then + return nil -- Can't determine + end + + -- Convert name to ID if needed + local checkValue = spellIdentifier + if type(spellIdentifier) == "string" and not string.find(spellIdentifier, "^spellId:") then + local spellId = API.GetSpellIdFromName(spellIdentifier) + if spellId and spellId > 0 then + checkValue = spellId + end + end + + return IsSpellInRange(checkValue, unit) +end + +-- Check if a spell is usable (wrapper around IsSpellUsable) +function API.IsSpellUsable(spellIdentifier) + if not _G.IsSpellUsable then + return nil + end + + -- If enhanced functions available, pass directly + if API.features.hasEnhancedSpellFunctions then + return _G.IsSpellUsable(spellIdentifier) + end + + -- Convert to ID if name + if type(spellIdentifier) == "string" then + local spellId = API.GetSpellIdFromName(spellIdentifier) + if spellId then + return _G.IsSpellUsable(spellId) + end + end + + return _G.IsSpellUsable(spellIdentifier) +end + +-------------------------------------------------------------------------------- +-- SMART CASTING SYSTEM +-------------------------------------------------------------------------------- + +-- Spell type constants +API.SPELL_TYPE_UNKNOWN = 0 +API.SPELL_TYPE_CAST = 1 -- Has cast time +API.SPELL_TYPE_INSTANT = 2 -- Instant, on GCD +API.SPELL_TYPE_CHANNEL = 3 -- Channeled spell +API.SPELL_TYPE_ON_SWING = 4 -- Next-melee (Heroic Strike, etc.) +API.SPELL_TYPE_NON_GCD = 5 -- Instant, not on GCD (trinkets, etc.) + +-- Known on-swing spells (by name, localized via CleveRoids.Localized when available) +API.onSwingSpells = { + ["Heroic Strike"] = true, + ["Cleave"] = true, + ["Maul"] = true, + ["Slam"] = true, + ["Raptor Strike"] = true, + ["Mongoose Bite"] = true, +} + +-- Known channeled spells (by name) +API.channeledSpells = { + ["Arcane Missiles"] = true, + ["Blizzard"] = true, + ["Drain Life"] = true, + ["Drain Mana"] = true, + ["Drain Soul"] = true, + ["Evocation"] = true, + ["Health Funnel"] = true, + ["Hellfire"] = true, + ["Hurricane"] = true, + ["Mind Flay"] = true, + ["Rain of Fire"] = true, + ["Tranquility"] = true, + ["Volley"] = true, + ["Mind Soothe"] = true, + ["Mind Vision"] = true, + ["First Aid"] = true, -- Bandaging +} + +-- Cache for spell type lookups +API.spellTypeCache = {} + +-- Determine spell type for a given spell +-- Returns: SPELL_TYPE_* constant +function API.GetSpellType(spellName) + if not spellName then return API.SPELL_TYPE_UNKNOWN end + + -- Strip rank from name for cache lookup + local baseName = string.gsub(spellName, "%s*%([Rr]ank%s*%d+%)%s*$", "") + + -- Check cache first + if API.spellTypeCache[baseName] then + return API.spellTypeCache[baseName] + end + + local spellType = API.SPELL_TYPE_UNKNOWN + + -- Check known on-swing spells first (highest priority) + if API.onSwingSpells[baseName] then + spellType = API.SPELL_TYPE_ON_SWING + API.spellTypeCache[baseName] = spellType + return spellType + end + + -- Check known channeled spells + if API.channeledSpells[baseName] then + spellType = API.SPELL_TYPE_CHANNEL + API.spellTypeCache[baseName] = spellType + return spellType + end + + -- Try to use GetSpellRec for accurate detection + local spellId = API.GetSpellIdFromName(spellName) + if spellId and spellId > 0 and API.features.hasGetSpellRec then + local rec = API.GetSpellRecord(spellId) + if rec then + -- Check for channeled via attributes + -- SPELL_ATTR_EX_CHANNELED = 4 (0x00000004) in attributesEx + local attrEx = rec.attributesEx or 0 + if bit and bit.band(attrEx, 4) ~= 0 then + spellType = API.SPELL_TYPE_CHANNEL + API.spellTypeCache[baseName] = spellType + return spellType + end + + -- Check cast time + local castTime = rec.castTime or 0 + if castTime > 0 then + spellType = API.SPELL_TYPE_CAST + else + -- Instant spell - check if it's on GCD + -- startRecoveryTime > 0 means it triggers GCD + local recoveryTime = rec.startRecoveryTime or 0 + if recoveryTime > 0 then + spellType = API.SPELL_TYPE_INSTANT + else + spellType = API.SPELL_TYPE_NON_GCD + end + end + + API.spellTypeCache[baseName] = spellType + return spellType + end + end + + -- Fallback: assume instant if no other info + -- This is safe because Nampower will handle it correctly anyway + spellType = API.SPELL_TYPE_INSTANT + API.spellTypeCache[baseName] = spellType + return spellType +end + +-- Get the queue setting name for a spell type +local function GetQueueSettingForType(spellType) + if spellType == API.SPELL_TYPE_CAST then + return "NP_QueueCastTimeSpells" + elseif spellType == API.SPELL_TYPE_INSTANT then + return "NP_QueueInstantSpells" + elseif spellType == API.SPELL_TYPE_CHANNEL then + return "NP_QueueChannelingSpells" + elseif spellType == API.SPELL_TYPE_ON_SWING then + return "NP_QueueOnSwingSpells" + elseif spellType == API.SPELL_TYPE_NON_GCD then + return "NP_QueueInstantSpells" -- Non-GCD uses instant setting + end + return nil +end + +-- Check if queuing is enabled for a specific spell +function API.IsSpellQueueingEnabled(spellName) + if not API.features.hasSpellQueue then + return false + end + + local spellType = API.GetSpellType(spellName) + local settingName = GetQueueSettingForType(spellType) + + if settingName then + return API.GetSettingBool(settingName) + end + + -- Default: use cast time spell setting + return API.GetSettingBool("NP_QueueCastTimeSpells") +end + +-- Smart cast function that uses QueueSpellByName when appropriate +-- Returns: true if cast was attempted, false otherwise +-- Parameters: +-- spellName: The spell name (with optional rank) +-- target: Optional target unit (for SuperWoW CastSpellByName) +-- forceQueue: If true, always use QueueSpellByName (if available) +-- forceNoQueue: If true, never use QueueSpellByName +function API.SmartCast(spellName, target, forceQueue, forceNoQueue) + if not spellName then return false end + + -- Determine whether to use queuing + local useQueue = false + + if forceNoQueue then + useQueue = false + elseif forceQueue then + useQueue = API.features.hasSpellQueue + else + -- Check if queuing is enabled for this spell type + useQueue = API.IsSpellQueueingEnabled(spellName) + end + + -- Cast the spell + if useQueue and QueueSpellByName then + -- Use Nampower's queue system + QueueSpellByName(spellName) + return true + elseif CastSpellByName then + -- Use standard casting + if target and CleveRoids.hasSuperwow then + -- SuperWoW supports target parameter + CastSpellByName(spellName, target) + else + CastSpellByName(spellName) + end + return true + end + + return false +end + +-- Cast without queuing (uses CastSpellByNameNoQueue if available) +function API.CastNoQueue(spellName, target) + if not spellName then return false end + + -- Use Nampower's no-queue function if available + if CastSpellByNameNoQueue then + CastSpellByNameNoQueue(spellName) + return true + end + + -- Fall back to standard cast + if CastSpellByName then + if target and CleveRoids.hasSuperwow then + CastSpellByName(spellName, target) + else + CastSpellByName(spellName) + end + return true + end + + return false +end + +-- Force queue a spell (uses QueueSpellByName directly) +function API.ForceQueue(spellName) + if not spellName then return false end + + if QueueSpellByName then + QueueSpellByName(spellName) + return true + end + + -- Fall back to standard cast if queue not available + if CastSpellByName then + CastSpellByName(spellName) + return true + end + + return false +end + +-- Queue a script to run after current cast (uses QueueScript if available) +function API.QueueScript(script, priority) + if not script then return false end + + if QueueScript then + QueueScript(script, priority or 1) + return true + end + + return false +end + +-- Clear spell type cache (call on spec change) +function API.ClearSpellTypeCache() + API.spellTypeCache = {} +end + +-- Expose API globally for other addons +_G.CleveRoidsNampowerAPI = API diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 9117dbc..0fc2141 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -7,6 +7,7 @@ ## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools Localization.lua Init.lua +NampowerAPI.lua Utility.lua Core.lua Conditionals.lua