mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
653f8db3d7
The autoAttack flag could drift stale-true (optimistic set after AttackTarget, or a target dying without PLAYER_LEAVE_COMBAT), making /startattack skip the attack until the target was dropped and reselected. The old fallback called the overridden IsCurrentAction, which just echoes the cached flag for the attack slot, so it never detected drift. Use the original Hooks.IsCurrentAction as ground truth and sync the flag to it.
7404 lines
295 KiB
Lua
7404 lines
295 KiB
Lua
--[[
|
|
Author: Dennis Werner Garske (DWG) / brian / Mewtiny / brian / Mewtiny
|
|
License: MIT License
|
|
]]
|
|
|
|
-- DEBUG: Error catcher for "attempt to index a function value"
|
|
-- Remove this block once the error is identified
|
|
local _G = _G or getfenv(0)
|
|
local originalErrorHandler = geterrorhandler and geterrorhandler()
|
|
if seterrorhandler then
|
|
seterrorhandler(function(msg)
|
|
if msg and string.find(msg, "index a function value") then
|
|
local trace = debugstack and debugstack(2, 20, 0) or "no stack available"
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000[SCRM DEBUG] index function error:|r " .. tostring(msg))
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00Stack:|r " .. tostring(trace))
|
|
end
|
|
if originalErrorHandler then
|
|
return originalErrorHandler(msg)
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Setup to wrap our stuff in a table so we don't pollute the global environment
|
|
local CleveRoids = _G.CleveRoids or {}
|
|
_G.CleveRoids = CleveRoids
|
|
CleveRoids.lastItemIndexTime = 0
|
|
CleveRoids.initializationTimer = nil
|
|
CleveRoids.isActionUpdateQueued = true
|
|
CleveRoids.lastEquipTime = CleveRoids.lastEquipTime or {}
|
|
CleveRoids.lastWeaponSwapTime = 0
|
|
CleveRoids.equipInProgress = false
|
|
|
|
-- PERFORMANCE: Event throttling to reduce spam from high-frequency events
|
|
CleveRoids.lastUnitAuraUpdate = 0
|
|
CleveRoids.lastUnitHealthUpdate = 0
|
|
CleveRoids.lastUnitPowerUpdate = 0
|
|
CleveRoids.EVENT_THROTTLE = 0.1 -- 100ms throttle for high-frequency events
|
|
|
|
-- PERFORMANCE: Spell ID cache to avoid repeated GetSpellIdForName lookups
|
|
CleveRoids.spellIdCache = {}
|
|
|
|
-- PERFORMANCE: Spell name construction cache
|
|
CleveRoids.spellNameCache = {}
|
|
|
|
-- PERFORMANCE: Upvalues for frequently called global functions (avoid global lookups)
|
|
local GetTime = GetTime
|
|
local UnitExists = UnitExists
|
|
local UnitAffectingCombat = UnitAffectingCombat
|
|
local GetContainerItemLink = GetContainerItemLink
|
|
local GetContainerItemInfo = GetContainerItemInfo
|
|
local GetContainerNumSlots = GetContainerNumSlots
|
|
local GetInventoryItemLink = GetInventoryItemLink
|
|
local GetItemInfo = GetItemInfo
|
|
local PickupContainerItem = PickupContainerItem
|
|
local PickupInventoryItem = PickupInventoryItem
|
|
local EquipCursorItem = EquipCursorItem
|
|
local CursorHasItem = CursorHasItem
|
|
local ClearCursor = ClearCursor
|
|
local TargetUnit = TargetUnit
|
|
local pcall = pcall
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local type = type
|
|
local tonumber = tonumber
|
|
local tostring = tostring
|
|
local string_find = string.find
|
|
local string_lower = string.lower
|
|
local string_gsub = string.gsub
|
|
local table_insert = table.insert
|
|
local table_getn = table.getn
|
|
|
|
-- Deferred stop-attack system (CheapShot pattern)
|
|
-- Stops autoattack and clears target. OnUpdate keeps clearing target while
|
|
-- autoattack is queued to prevent any swing from landing. No retarget.
|
|
local _DeferStopFrame = CreateFrame("Frame")
|
|
local _deferStopActive = false
|
|
local _deferStartTime = 0
|
|
local DEFER_STOP_TIMEOUT = 0.5
|
|
|
|
_DeferStopFrame:SetScript("OnUpdate", function()
|
|
if not _deferStopActive then
|
|
_DeferStopFrame:Hide()
|
|
return
|
|
end
|
|
|
|
-- Timeout: stop polling
|
|
if (GetTime() - _deferStartTime) >= DEFER_STOP_TIMEOUT then
|
|
_deferStopActive = false
|
|
_DeferStopFrame:Hide()
|
|
return
|
|
end
|
|
|
|
-- While autoattack is queued, keep clearing target to prevent swings
|
|
local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo()
|
|
if autoattack == 1 then
|
|
if UnitExists("target") then
|
|
ClearTarget()
|
|
end
|
|
else
|
|
-- Autoattack stopped, done
|
|
_deferStopActive = false
|
|
_DeferStopFrame:Hide()
|
|
end
|
|
end)
|
|
_DeferStopFrame:Hide()
|
|
|
|
function CleveRoids.DeferStopAttack()
|
|
CleveRoids.CurrentSpell.autoAttack = false
|
|
CleveRoids.CurrentSpell.autoAttackLock = false
|
|
_deferStartTime = GetTime()
|
|
-- Only toggle auto-attack OFF if it's currently active (avoid turning it ON)
|
|
local attackSlot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
|
|
if attackSlot and CleveRoids.Hooks.IsCurrentAction(attackSlot) then
|
|
AttackTarget()
|
|
end
|
|
ClearTarget()
|
|
_deferStopActive = true
|
|
_DeferStopFrame:Show()
|
|
end
|
|
|
|
-- PERFORMANCE: Module-level constant for boolean conditionals (avoid per-call table creation)
|
|
local BOOLEAN_CONDITIONALS = {
|
|
combat = true,
|
|
nocombat = true,
|
|
stealth = true,
|
|
nostealth = true,
|
|
channeled = true,
|
|
nochanneled = true,
|
|
checkchanneled = true,
|
|
checkcasting = true,
|
|
dead = true,
|
|
alive = true,
|
|
help = true,
|
|
harm = true,
|
|
exists = true,
|
|
party = true,
|
|
raid = true,
|
|
resting = true,
|
|
noresting = true,
|
|
isplayer = true,
|
|
isnpc = true,
|
|
mhimbue = true,
|
|
nomhimbue = true,
|
|
ohimbue = true,
|
|
noohimbue = true,
|
|
group = true,
|
|
nogroup = true,
|
|
moving = true,
|
|
nomoving = true,
|
|
stopattack = true,
|
|
pet = true,
|
|
nopet = true,
|
|
focus = true,
|
|
nofocus = true,
|
|
bg = true,
|
|
nobg = true,
|
|
-- Dispel-type conditionals (ClassicAPI C_UnitAuras)
|
|
magic = true,
|
|
nomagic = true,
|
|
curse = true,
|
|
nocurse = true,
|
|
disease = true,
|
|
nodisease = true,
|
|
poison = true,
|
|
nopoison = true,
|
|
dispellable = true,
|
|
nodispellable = true,
|
|
magicbuff = true,
|
|
nomagicbuff = true,
|
|
dispellablebuff = true,
|
|
nodispellablebuff = true,
|
|
}
|
|
|
|
local requirementCheckFrame = CreateFrame("Frame")
|
|
requirementCheckFrame:RegisterEvent("ADDON_LOADED")
|
|
requirementCheckFrame:SetScript("OnEvent", function()
|
|
if arg1 ~= "SuperCleveRoidMacros" then return end
|
|
|
|
-- Check requirements immediately when our addon loads
|
|
local hasNampower = (IsSpellInRange ~= nil)
|
|
local hasUnitXP = pcall(UnitXP, "nop", "nop")
|
|
local hasNampower30 = hasNampower and CleveRoids.NampowerAPI
|
|
and CleveRoids.NampowerAPI.HasMinimumVersion(3, 0, 0)
|
|
local hasClassicAPI = CleveRoids.ClassicAPI and CleveRoids.ClassicAPI.IsAvailable()
|
|
|
|
if not hasNampower30 or not hasUnitXP or not hasClassicAPI then
|
|
-- Show warnings (don't disable — tearing down a partially-initialized addon causes hangs)
|
|
if not hasNampower then
|
|
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFAvitasia's Nampower v3.0.0+|r is required:")
|
|
CleveRoids.Print("https://github.com/brues-code/nampower")
|
|
CleveRoids.Print("Many features will be unavailable without Nampower.")
|
|
elseif not hasNampower30 then
|
|
local major, minor, patch = CleveRoids.NampowerAPI.GetVersion()
|
|
CleveRoids.Print(format("|cFFFF9900WARNING:|r |cFF00FFFFNampower v3.0.0+|r is required (you have v%d.%d.%d):", major, minor, patch))
|
|
CleveRoids.Print("https://github.com/brues-code/nampower")
|
|
CleveRoids.Print("Some features may be unavailable with this version.")
|
|
end
|
|
if not hasUnitXP then
|
|
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFKonaka's UnitXP_SP3|r is required:")
|
|
CleveRoids.Print("https://codeberg.org/konaka/UnitXP_SP3")
|
|
end
|
|
if not hasClassicAPI then
|
|
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI|r is required:")
|
|
CleveRoids.Print("https://github.com/brues-code/ClassicAPI")
|
|
CleveRoids.Print("Dispel-type and movement conditionals will be unavailable without it.")
|
|
end
|
|
end
|
|
|
|
-- Requirements met - allow normal initialization
|
|
CleveRoids.Print("|cFF4477FFSuperCleveR|r|cFFFFFFFFoid Macros|r |cFF00FF00Loaded|r - See the README.")
|
|
|
|
-- Unregister this check frame
|
|
this:UnregisterAllEvents()
|
|
end)
|
|
|
|
local SLOT_TO_INVID = {
|
|
["MainHandSlot"] = 16,
|
|
["SecondaryHandSlot"] = 17,
|
|
["RangedSlot"] = 18,
|
|
["HeadSlot"] = 1,
|
|
["NeckSlot"] = 2,
|
|
["ShoulderSlot"] = 3,
|
|
["ChestSlot"] = 5,
|
|
["WaistSlot"] = 6,
|
|
["LegsSlot"] = 7,
|
|
["FeetSlot"] = 8,
|
|
["WristSlot"] = 9,
|
|
["HandsSlot"] = 10,
|
|
["Finger0Slot"] = 11,
|
|
["Finger1Slot"] = 12,
|
|
["Trinket0Slot"] = 13,
|
|
["Trinket1Slot"] = 14,
|
|
["BackSlot"] = 15,
|
|
["ShirtSlot"] = 4,
|
|
["TabardSlot"] = 19,
|
|
}
|
|
|
|
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
|
|
|
|
return (now - slotTime) < CleveRoids.EQUIP_COOLDOWN or
|
|
(now - globalTime) < CleveRoids.EQUIP_GLOBAL_COOLDOWN
|
|
end
|
|
|
|
-- PERFORMANCE: Consolidated cursor handling, fewer CursorHasItem() calls
|
|
local function PerformEquipSwap(item, inventoryId, useQueueScript)
|
|
if not item or not inventoryId then return false end
|
|
|
|
-- Check if in combat and swapping weapons
|
|
-- 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
|
|
if CleveRoids.CurrentSpell.type ~= "" then
|
|
return false
|
|
end
|
|
|
|
-- Check for on-swing spells if available (Nampower)
|
|
if GetCurrentCastingInfo then
|
|
local castId, _, _, casting, channeling, onswing = GetCurrentCastingInfo()
|
|
|
|
if onswing == 1 then
|
|
return false
|
|
end
|
|
|
|
-- If casting/channeling and QueueScript available, queue the swap for after cast
|
|
if (casting == 1 or channeling == 1) and QueueScript and useQueueScript and item.name then
|
|
local script = string.format('EquipItemByName("%s",%d)', item.name, inventoryId)
|
|
QueueScript(script, 3) -- Priority 3 = after queued spells
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Try EquipItemByName first if available (handles everything internally)
|
|
if item.name and EquipItemByName then
|
|
local ok = pcall(EquipItemByName, item.name, inventoryId)
|
|
if ok then return true end
|
|
end
|
|
|
|
-- Fallback: Manual pickup and equip
|
|
if item.bagID and item.slot then
|
|
PickupContainerItem(item.bagID, item.slot)
|
|
elseif item.inventoryID then
|
|
PickupInventoryItem(item.inventoryID)
|
|
else
|
|
return false
|
|
end
|
|
|
|
-- Single cursor check after pickup attempt
|
|
if not CursorHasItem() then
|
|
return false
|
|
end
|
|
|
|
EquipCursorItem(inventoryId)
|
|
local success = not CursorHasItem()
|
|
ClearCursor()
|
|
return success
|
|
end
|
|
|
|
-- PERFORMANCE: Get a queue entry from pool or create new
|
|
local function GetQueueEntry()
|
|
local pool = CleveRoids.queueEntryPool
|
|
local n = pool and table_getn(pool) or 0
|
|
if n > 0 then
|
|
local entry = pool[n]
|
|
pool[n] = nil
|
|
return entry
|
|
end
|
|
return {}
|
|
end
|
|
|
|
-- PERFORMANCE: Return entry to pool for reuse
|
|
local function ReleaseQueueEntry(entry)
|
|
if not entry then return end
|
|
-- Clear the entry (set to nil, don't create new table)
|
|
entry.item = nil
|
|
entry.slotName = nil
|
|
entry.inventoryId = nil
|
|
entry.queueTime = nil
|
|
entry.retries = nil
|
|
entry.maxRetries = nil
|
|
entry.itemId = nil
|
|
-- Add to pool (max 10 pooled entries)
|
|
local pool = CleveRoids.queueEntryPool
|
|
if table_getn(pool) < 10 then
|
|
table_insert(pool, entry)
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Swap-and-pop removal (O(1) instead of O(n))
|
|
local function RemoveQueueEntry(i)
|
|
local queue = CleveRoids.equipmentQueue
|
|
local n = CleveRoids.equipmentQueueLen
|
|
local entry = queue[i]
|
|
|
|
if i < n then
|
|
queue[i] = queue[n]
|
|
end
|
|
queue[n] = nil
|
|
CleveRoids.equipmentQueueLen = n - 1
|
|
|
|
ReleaseQueueEntry(entry)
|
|
end
|
|
|
|
-- Queue equipment swap function
|
|
function CleveRoids.QueueEquipItem(item, slotName)
|
|
if not item or not slotName then return false end
|
|
|
|
local inventoryId = GetInventoryIdFromSlot(slotName)
|
|
if not inventoryId then return false end
|
|
|
|
local now = GetTime()
|
|
local itemId = item.id
|
|
|
|
-- PERFORMANCE: Check for duplicate queue entries before adding
|
|
local queue = CleveRoids.equipmentQueue
|
|
local queueLen = CleveRoids.equipmentQueueLen
|
|
for i = 1, queueLen do
|
|
local q = queue[i]
|
|
if q and q.inventoryId == inventoryId and q.itemId == itemId then
|
|
return false -- Already queued
|
|
end
|
|
end
|
|
|
|
-- Try immediate equip if not on cooldown (use QueueScript for smoother mid-cast swaps)
|
|
if not IsSlotOnCooldown(inventoryId, now) then
|
|
local success = PerformEquipSwap(item, inventoryId, true)
|
|
|
|
if success then
|
|
-- 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
|
|
|
|
-- Queue for later using pooled entry
|
|
local entry = GetQueueEntry()
|
|
entry.item = item
|
|
entry.slotName = slotName
|
|
entry.inventoryId = inventoryId
|
|
entry.queueTime = now
|
|
entry.retries = 0
|
|
entry.maxRetries = 5
|
|
entry.itemId = itemId
|
|
|
|
CleveRoids.equipmentQueueLen = queueLen + 1
|
|
queue[CleveRoids.equipmentQueueLen] = entry
|
|
|
|
-- PERFORMANCE: Start the queue processing frame
|
|
if CleveRoids.equipQueueFrame then
|
|
CleveRoids.equipQueueFrame:Show()
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- PERFORMANCE: Process equipment queue (called from self-disabling frame)
|
|
function CleveRoids.ProcessEquipmentQueue()
|
|
local queueLen = CleveRoids.equipmentQueueLen
|
|
if queueLen == 0 then return end
|
|
|
|
local now = GetTime()
|
|
local queue = CleveRoids.equipmentQueue
|
|
local i = 1
|
|
|
|
while i <= CleveRoids.equipmentQueueLen do
|
|
local queued = queue[i]
|
|
|
|
-- Remove expired entries first (>10 seconds old)
|
|
if (now - queued.queueTime) > 10 then
|
|
RemoveQueueEntry(i)
|
|
-- Don't increment i, we swapped in the last element
|
|
elseif not IsSlotOnCooldown(queued.inventoryId, now) then
|
|
-- Try to equip (use QueueScript for smoother mid-cast swaps)
|
|
local success = PerformEquipSwap(queued.item, queued.inventoryId, true)
|
|
|
|
if success then
|
|
-- 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
|
|
if queued.retries >= queued.maxRetries then
|
|
RemoveQueueEntry(i)
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
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.1 -- 100ms between queue checks (was 50ms)
|
|
CleveRoids.equipQueueLastUpdate = 0
|
|
CleveRoids.equipQueueFrame = CreateFrame("Frame")
|
|
CleveRoids.equipQueueFrame:Hide()
|
|
|
|
-- PERFORMANCE: Upvalue for faster access in OnUpdate
|
|
local equipQueueFrame = CleveRoids.equipQueueFrame
|
|
local equipQueueInterval = CleveRoids.EQUIP_QUEUE_INTERVAL
|
|
|
|
equipQueueFrame:SetScript("OnUpdate", function()
|
|
-- Prevent operations during shutdown (crash prevention)
|
|
if CleveRoids.isShuttingDown then return end
|
|
|
|
-- 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.ProcessEquipmentQueue()
|
|
if CleveRoids.equipmentQueueLen == 0 then
|
|
this:Hide()
|
|
end
|
|
end)
|
|
|
|
-- Improved DisableAddon function
|
|
function CleveRoids.DisableAddon(reason)
|
|
-- Prevent multiple disable calls
|
|
if CleveRoids.disabled then return end
|
|
|
|
-- Mark state
|
|
CleveRoids.disabled = true
|
|
|
|
-- Stop main frame activity
|
|
if CleveRoids.Frame then
|
|
if CleveRoids.Frame.UnregisterAllEvents then
|
|
CleveRoids.Frame:UnregisterAllEvents()
|
|
end
|
|
if CleveRoids.Frame.SetScript then
|
|
CleveRoids.Frame:SetScript("OnEvent", nil)
|
|
CleveRoids.Frame:SetScript("OnUpdate", nil)
|
|
end
|
|
end
|
|
|
|
-- Neuter all slash commands
|
|
if SlashCmdList then
|
|
local disabledMsg = function()
|
|
CleveRoids.Print("|cffff0000SuperCleveRoidMacros is disabled|r" ..
|
|
(reason and (": " .. tostring(reason)) or ""))
|
|
end
|
|
|
|
SlashCmdList.CLEVEROID = disabledMsg
|
|
SlashCmdList.CAST = disabledMsg
|
|
SlashCmdList.USE = disabledMsg
|
|
SlashCmdList.EQUIP = disabledMsg
|
|
SlashCmdList.EQUIPMH = disabledMsg
|
|
SlashCmdList.EQUIPOH = disabledMsg
|
|
end
|
|
|
|
-- Try to disable for next login
|
|
local addonName = "SuperCleveRoidMacros"
|
|
if type(DisableAddOn) == "function" then
|
|
pcall(DisableAddOn, addonName)
|
|
end
|
|
|
|
-- Final notice
|
|
CleveRoids.Print("|cffff0000Disabled|r - " .. (reason or "Unknown reason"))
|
|
CleveRoids.Print("Please install required dependencies and /reload")
|
|
end
|
|
|
|
local frame = CreateFrame("Frame")
|
|
frame:RegisterEvent("VARIABLES_LOADED")
|
|
frame:SetScript("OnEvent", function()
|
|
if not CleveRoidMacros then CleveRoidMacros = {} end
|
|
|
|
if type(CleveRoidMacros.realtime) ~= "number" then
|
|
CleveRoidMacros.realtime = 0
|
|
end
|
|
|
|
if type(CleveRoidMacros.refresh) ~= "number" then
|
|
CleveRoidMacros.refresh = 5
|
|
end
|
|
|
|
if type(CleveRoidMacros.macrocheck) ~= "number" then
|
|
CleveRoidMacros.macrocheck = 1 -- enabled by default
|
|
end
|
|
end)
|
|
|
|
-- Queues a full update of all action bars.
|
|
-- This is called by game event handlers to avoid running heavy logic inside the event itself.
|
|
function CleveRoids.QueueActionUpdate()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.isActionUpdateQueued = true
|
|
end
|
|
end
|
|
|
|
local function _StripColor(s)
|
|
if not s then return s end
|
|
if string.sub(s,1,2) == "|c" then return string.sub(s,11,-3) end
|
|
return s
|
|
end
|
|
|
|
local _ReagentBySpell = {
|
|
["Vanish"] = "Flash Powder", -- 5140
|
|
["Blind"] = "Blinding Powder", -- 5530
|
|
}
|
|
|
|
-- Expose for use in Generic.lua (IndexSpells fallback)
|
|
CleveRoids.ReagentBySpell = _ReagentBySpell
|
|
|
|
-- Minimal map for rogue reagents; extend as needed
|
|
local _ReagentIdByName = {
|
|
["Flash Powder"] = 5140, -- Vanish
|
|
["Blinding Powder"] = 5530, -- Blind
|
|
}
|
|
|
|
-- Lazy bag-scan tooltip (only if we need to scan a bag slot by name)
|
|
local function CRM_GetBagScanTip()
|
|
local tip = _G.CleveRoidsBagScanTip
|
|
if tip then return tip end
|
|
local ok, created = pcall(CreateFrame, "GameTooltip", "CleveRoidsBagScanTip", UIParent, "GameTooltipTemplate")
|
|
if ok and created then
|
|
tip = created
|
|
else
|
|
tip = CreateFrame("GameTooltip", "CleveRoidsBagScanTip", UIParent)
|
|
local L1 = tip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
|
|
local R1 = tip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
|
tip:AddFontStrings(L1, R1)
|
|
for i=2,10 do
|
|
tip:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
|
|
tip:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
|
|
end
|
|
end
|
|
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
_G.CleveRoidsBagScanTip = tip
|
|
return tip
|
|
end
|
|
|
|
-- Return total in *bags only* (not bank), by id when possible; fallback to name
|
|
-- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup when available
|
|
function CleveRoids.GetReagentCount(reagentName)
|
|
if not reagentName or reagentName == "" then return 0 end
|
|
|
|
-- Fast path: check cache first
|
|
local Items = CleveRoids.Items
|
|
if Items then
|
|
local wantId = _ReagentIdByName[reagentName]
|
|
|
|
-- Try by ID first if we have a mapping
|
|
if wantId then
|
|
local itemName = Items[wantId]
|
|
if itemName then
|
|
local itemData = Items[itemName]
|
|
if itemData and itemData.count and not itemData.inventoryID then
|
|
-- Item is in bags (not equipped), return cached count
|
|
return itemData.count
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Try by name
|
|
local itemData = Items[reagentName]
|
|
if type(itemData) == "string" then
|
|
itemData = Items[itemData] -- Resolve indirection
|
|
end
|
|
if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then
|
|
return itemData.count
|
|
end
|
|
|
|
-- Try lowercase
|
|
local lowerName = string.lower(reagentName)
|
|
local resolved = Items[lowerName]
|
|
if type(resolved) == "string" then
|
|
itemData = Items[resolved]
|
|
elseif type(resolved) == "table" then
|
|
itemData = resolved
|
|
end
|
|
if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then
|
|
return itemData.count
|
|
end
|
|
end
|
|
|
|
-- Slow path fallback: full bag scan (only when cache miss)
|
|
local wantId = _ReagentIdByName[reagentName]
|
|
local total = 0
|
|
|
|
for bag = 0, 4 do
|
|
local slots = GetContainerNumSlots(bag) or 0
|
|
for slot = 1, slots do
|
|
local _, count = GetContainerItemInfo(bag, slot)
|
|
count = count or 0
|
|
local link = GetContainerItemLink and GetContainerItemLink(bag, slot)
|
|
if link then
|
|
local _, _, idstr = string.find(link, "item:(%d+)")
|
|
local id = idstr and tonumber(idstr) or nil
|
|
if (wantId and id == wantId) or (not wantId and string.find(link, "%["..reagentName.."%]")) then
|
|
total = total + count
|
|
end
|
|
else
|
|
-- Fallback: scan bag slot tooltip for the name (expensive, only when no link)
|
|
local tip = CRM_GetBagScanTip()
|
|
tip:ClearLines()
|
|
tip:SetBagItem(bag, slot)
|
|
local left1 = _G[tip:GetName().."TextLeft1"]
|
|
local name = left1 and left1:GetText()
|
|
if name and name == reagentName then
|
|
total = total + count
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return total
|
|
end
|
|
|
|
-- Live bag scan for item count, bypassing Items cache to avoid stale counts
|
|
-- during bag moves (especially backpack ↔ other bag transitions)
|
|
function CleveRoids.GetLiveItemCount(itemName)
|
|
if not itemName or itemName == "" then return 0 end
|
|
-- Escape Lua pattern special chars in item name to avoid crashes
|
|
local escaped = string.gsub(itemName, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
|
|
local pattern = "%[" .. escaped .. "%]"
|
|
local lowerPattern = string.lower(pattern)
|
|
local total = 0
|
|
for bag = 0, 4 do
|
|
local slots = GetContainerNumSlots(bag) or 0
|
|
for slot = 1, slots do
|
|
local link = GetContainerItemLink and GetContainerItemLink(bag, slot)
|
|
if link and string.find(string.lower(link), lowerPattern) then
|
|
local _, count = GetContainerItemInfo(bag, slot)
|
|
total = total + (count or 0)
|
|
end
|
|
end
|
|
end
|
|
return total
|
|
end
|
|
|
|
function CleveRoids.GetSpellCost(spellSlot, bookType)
|
|
-- Fast path: existing fixed-slot read
|
|
CleveRoids.Frame:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
CleveRoids.Frame:SetSpell(spellSlot, bookType)
|
|
|
|
local cost, reagent
|
|
local costText = CleveRoids.Frame.costFontString:GetText()
|
|
if costText then
|
|
_, _, cost = string.find(costText, "^(%d+)%s+[^yYsS]")
|
|
end
|
|
|
|
local reagentText = CleveRoids.Frame.reagentFontString:GetText()
|
|
if reagentText then
|
|
_, _, reagent = string.find(reagentText, "^Reagents?%s*:%s*(.*)")
|
|
end
|
|
reagent = _StripColor(reagent)
|
|
if reagent == "" then reagent = nil end
|
|
|
|
-- Fallback: scan all lines on a named tooltip (handles Vanish layout)
|
|
if not reagent or not cost then
|
|
local tip = CleveRoidsTooltipScan
|
|
if not tip then
|
|
-- belt & suspenders: create if somehow missing
|
|
local ok,_ = pcall(CreateFrame, "GameTooltip", "CleveRoidsTooltipScan", UIParent, "GameTooltipTemplate")
|
|
if not ok or not CleveRoidsTooltipScan then
|
|
CleveRoidsTooltipScan = CreateFrame("GameTooltip", "CleveRoidsTooltipScan", UIParent)
|
|
local L1 = CleveRoidsTooltipScan:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
|
|
local R1 = CleveRoidsTooltipScan:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
|
CleveRoidsTooltipScan:AddFontStrings(L1, R1)
|
|
for i=2,32 do
|
|
CleveRoidsTooltipScan:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
|
|
CleveRoidsTooltipScan:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
|
|
end
|
|
end
|
|
CleveRoidsTooltipScan:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
tip = CleveRoidsTooltipScan
|
|
end
|
|
|
|
tip:ClearLines()
|
|
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
tip:SetSpell(spellSlot, bookType)
|
|
|
|
local base = tip:GetName() or "CleveRoidsTooltipScan"
|
|
local maxLines = (tip.NumLines and tip:NumLines()) or 32
|
|
|
|
for i = 1, maxLines do
|
|
local L = _G[base.."TextLeft"..i]
|
|
local R = _G[base.."TextRight"..i]
|
|
local lt = L and L:GetText() or ""
|
|
local rt = R and R:GetText() or ""
|
|
|
|
if not reagent then
|
|
if string.find(lt, "^[Rr]eagents?%s*:") then
|
|
reagent = _StripColor((rt ~= "" and rt) or (string.gsub(lt, "^[Rr]eagents?%s*:%s*", "")))
|
|
elseif string.find(rt, "^[Rr]eagents?%s*:") then
|
|
reagent = _StripColor((lt ~= "" and lt) or (string.gsub(rt, "^[Rr]eagents?%s*:%s*", "")))
|
|
end
|
|
if reagent == "" then reagent = nil end
|
|
end
|
|
|
|
if not cost and rt ~= "" then
|
|
local _, _, num = string.find(rt, "^(%d+)%s+(Mana|Energy|Rage|Focus)")
|
|
if num then cost = tonumber(num) end
|
|
end
|
|
|
|
if reagent and cost then break end
|
|
end
|
|
end
|
|
|
|
if not reagent or reagent == "" then
|
|
reagent = nil
|
|
local name = GetSpellName(spellSlot, bookType)
|
|
if name then
|
|
name = CleveRoids.StripRank(name)
|
|
reagent = _ReagentBySpell[name]
|
|
end
|
|
end
|
|
|
|
return (cost and tonumber(cost) or 0), (reagent and reagent ~= "" and tostring(reagent) or nil)
|
|
end
|
|
|
|
function CleveRoids.GetProxyActionSlot(slot)
|
|
if not slot then return end
|
|
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
|
|
local currentSequence = actions.sequence
|
|
local hasActive = false
|
|
local newActiveAction = nil
|
|
local newSequence = nil
|
|
local firstUnconditional = nil
|
|
|
|
-- Use tooltip-only branch when:
|
|
-- 1. #showtooltip has an explicit argument (explicitTooltip = true), OR
|
|
-- 2. #showtooltip exists but action list is empty (bare "#showtooltip")
|
|
-- This ensures explicit tooltips always show their icon/usability correctly.
|
|
if actions.tooltip and (actions.explicitTooltip or table.getn(actions.list) == 0) then
|
|
-- For explicit tooltips, directly use the tooltip as active action
|
|
-- (actions.cmd/args are empty, so TestAction would fail)
|
|
if actions.explicitTooltip then
|
|
hasActive = true
|
|
newActiveAction = actions.tooltip
|
|
elseif CleveRoids.TestAction(actions.cmd or "", actions.args or "") then
|
|
-- Bare #showtooltip (no arg) - validate with TestAction
|
|
hasActive = true
|
|
newActiveAction = actions.tooltip
|
|
end
|
|
|
|
-- Resolve nested macro references for #showtooltip propagation
|
|
if hasActive and newActiveAction then
|
|
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
|
|
for _, action in ipairs(actions.list) do
|
|
local result = CleveRoids.TestAction(action.cmd, action.args)
|
|
|
|
-- Check if action has conditionals
|
|
local _, conditionals = CleveRoids.GetParsedMsg(action.args)
|
|
|
|
-- Skip this action for icon display if it has the '?' prefix
|
|
-- Note: CleveRoids._ignoretooltip is a side-effect flag set by GetParsedMsg
|
|
local shouldSkipForIcon = CleveRoids._ignoretooltip == 1
|
|
|
|
-- break on first action that passes tests (unless it should be skipped for icon)
|
|
if result and not shouldSkipForIcon then
|
|
hasActive = true
|
|
if action.sequence then
|
|
newSequence = action.sequence
|
|
newActiveAction = CleveRoids.GetCurrentSequenceAction(newSequence)
|
|
-- Check if current step is a stop step - if so, skip this sequence for icon
|
|
if newActiveAction and newActiveAction.action then
|
|
local stepLower = string.lower(CleveRoids.Trim(newActiveAction.action))
|
|
if stepLower == "nil" or stepLower == "null" or stepLower == "pass" or stepLower == "noop" then
|
|
hasActive = false
|
|
newActiveAction = nil
|
|
newSequence = nil
|
|
end
|
|
end
|
|
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
|
|
if not hasActive and firstUnconditional then
|
|
hasActive = true
|
|
newActiveAction = firstUnconditional
|
|
end
|
|
end
|
|
|
|
-- If the active action has no texture (item wasn't in bags at parse time),
|
|
-- resolve it now via live bag lookup so the icon updates immediately
|
|
if newActiveAction and not newActiveAction.texture
|
|
and not newActiveAction.spell and not newActiveAction.petSpell then
|
|
local freshItem = CleveRoids.GetItem(newActiveAction.action)
|
|
if freshItem and freshItem.texture then
|
|
newActiveAction.item = freshItem
|
|
newActiveAction.texture = freshItem.texture
|
|
newActiveAction.type = newActiveAction.type or "item"
|
|
end
|
|
end
|
|
|
|
local changed = false
|
|
if currentActive ~= newActiveAction or currentSequence ~= newSequence then
|
|
actions.active = newActiveAction
|
|
actions.sequence = newSequence
|
|
changed = true
|
|
end
|
|
|
|
if not hasActive then
|
|
if actions.active ~= nil or actions.sequence ~= nil then
|
|
actions.active = nil
|
|
actions.sequence = nil
|
|
changed = true
|
|
end
|
|
return changed
|
|
end
|
|
|
|
if actions.active then
|
|
local previousUsable = actions.active.usable
|
|
local previousOom = actions.active.oom
|
|
local previousInRange = actions.active.inRange
|
|
|
|
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
|
|
actions.active.inRange = -1
|
|
|
|
-- Enhanced nampower range check with spell ID support
|
|
if IsSpellInRange then
|
|
local unit = actions.active.conditionals and actions.active.conditionals.target or "target"
|
|
if unit == "focus" then
|
|
unit = CleveRoids.GetFocusUnitId()
|
|
elseif unit == "focustarget" then
|
|
local focusUnit = CleveRoids.GetFocusUnitId()
|
|
unit = focusUnit and (focusUnit .. "target") or nil
|
|
end
|
|
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(unit)
|
|
if resolvedMark then unit = resolvedMark end
|
|
|
|
-- PERFORMANCE: Cache spell name construction using two-level cache (no string concat for lookup)
|
|
local castName = actions.active.action
|
|
if actions.active.spell and actions.active.spell.name then
|
|
local spellName = actions.active.spell.name
|
|
local rank = actions.active.spell.rank
|
|
or (actions.active.spell.highest and actions.active.spell.highest.rank)
|
|
if rank and rank ~= "" then
|
|
-- Two-level cache: spellNameCache[spellName][rank] = "SpellName(Rank X)"
|
|
local nameCache = CleveRoids.spellNameCache[spellName]
|
|
if not nameCache then
|
|
nameCache = {}
|
|
CleveRoids.spellNameCache[spellName] = nameCache
|
|
end
|
|
castName = nameCache[rank]
|
|
if not castName then
|
|
castName = spellName .. "(" .. rank .. ")"
|
|
nameCache[rank] = castName
|
|
end
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Try to get spell ID with caching
|
|
local spellId = nil
|
|
if GetSpellIdForName then
|
|
-- Check cache first
|
|
local cachedId = CleveRoids.spellIdCache[castName]
|
|
if cachedId then
|
|
spellId = cachedId
|
|
else
|
|
spellId = GetSpellIdForName(castName)
|
|
if spellId and spellId > 0 then
|
|
CleveRoids.spellIdCache[castName] = spellId
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check range using API wrapper (handles all cases properly)
|
|
local API = CleveRoids.NampowerAPI
|
|
local checkValue = spellId and spellId > 0 and spellId or castName
|
|
|
|
if UnitExists(unit) then
|
|
-- We have a target - use API wrapper for proper range checking
|
|
-- API.IsSpellInRange handles: native check, -1 for self-cast, UnitXP fallback
|
|
if API then
|
|
local r = API.IsSpellInRange(checkValue, unit)
|
|
if r ~= nil then
|
|
actions.active.inRange = r
|
|
end
|
|
elseif IsSpellInRange then
|
|
-- No API, use native directly
|
|
local nativeResult = IsSpellInRange(checkValue, unit)
|
|
if nativeResult == 0 or nativeResult == 1 then
|
|
actions.active.inRange = nativeResult
|
|
elseif nativeResult == -1 then
|
|
-- Self-cast/ground-targeted spell, always in range
|
|
actions.active.inRange = 1
|
|
end
|
|
end
|
|
end
|
|
-- No target: don't set inRange, let proxy slot / original behavior handle it
|
|
end
|
|
|
|
-- Check if spell is usable first (handles forms, stances, and power type correctly)
|
|
local isUsableBySpell, notEnoughPower = nil, nil
|
|
if IsSpellUsable then
|
|
-- pcall to handle spells not in spellbook (Nampower throws error)
|
|
local ok, usable, oom = pcall(IsSpellUsable, actions.active.action)
|
|
if ok then
|
|
isUsableBySpell, notEnoughPower = usable, oom
|
|
end
|
|
end
|
|
|
|
-- For OOM check, use proper mana source
|
|
if notEnoughPower ~= nil then
|
|
-- Prefer IsSpellUsable result if available (Nampower)
|
|
actions.active.oom = (notEnoughPower == 1)
|
|
else
|
|
-- SuperWoW: UnitMana returns (current power, caster mana) for druids
|
|
local currentPower, casterMana = UnitMana("player")
|
|
|
|
-- For druids with SuperWoW, use caster mana for spell cost checks
|
|
local manaToCheck = currentPower
|
|
if CleveRoids.playerClass == "DRUID" and type(casterMana) == "number" then
|
|
manaToCheck = casterMana
|
|
end
|
|
|
|
actions.active.oom = (manaToCheck < actions.active.spell.cost)
|
|
end
|
|
|
|
local start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType)
|
|
local onCooldown = (start > 0 and duration > 0)
|
|
|
|
if actions.active.isReactive then
|
|
-- For Overpower, Revenge, Riposte: ONLY use combat log tracking
|
|
local spellName = actions.active.action
|
|
local useCombatLogOnly = (spellName == "Overpower" or spellName == "Revenge" or spellName == "Riposte")
|
|
|
|
if useCombatLogOnly then
|
|
-- Only trust HasReactiveProc for these spells
|
|
local hasProc = CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(spellName)
|
|
if hasProc then
|
|
-- Proc is active, show as usable if in range and have enough rage/mana
|
|
if actions.active.inRange ~= 0 and not actions.active.oom then
|
|
actions.active.usable = 1
|
|
elseif pfUI and pfUI.bars and actions.active.oom then
|
|
actions.active.usable = 2 -- pfUI: out of mana/rage
|
|
else
|
|
actions.active.usable = nil
|
|
end
|
|
else
|
|
-- No proc = not usable
|
|
actions.active.usable = nil
|
|
end
|
|
CleveRoids.DebugChanged("usable_" .. spellName,
|
|
string.format("|cff00ff00[UPDATE USABLE]|r %s: hasProc=%s, usable=%s",
|
|
spellName, tostring(hasProc), tostring(actions.active.usable)))
|
|
else
|
|
-- For other reactive spells, use the original fallback logic
|
|
-- Check combat log-based proc tracking first (stance-independent)
|
|
if CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(actions.active.action) then
|
|
-- Proc is active, show as usable if in range and have enough rage/mana
|
|
if actions.active.inRange ~= 0 and not actions.active.oom then
|
|
actions.active.usable = 1
|
|
elseif pfUI and pfUI.bars and actions.active.oom then
|
|
actions.active.usable = 2 -- pfUI: out of mana/rage
|
|
else
|
|
actions.active.usable = nil
|
|
end
|
|
-- Use Nampower's IsSpellUsable if available (stance-aware fallback)
|
|
elseif IsSpellUsable then
|
|
-- pcall to handle spells not in spellbook (Nampower throws error)
|
|
local ok, usable, oom = pcall(IsSpellUsable, actions.active.action)
|
|
if ok and usable == 1 and oom ~= 1 then
|
|
actions.active.usable = (pfUI and pfUI.bars) and nil or 1
|
|
else
|
|
actions.active.usable = nil
|
|
end
|
|
actions.active.oom = false
|
|
elseif not CleveRoids.IsReactiveUsable(actions.active.action) then
|
|
actions.active.oom = false
|
|
actions.active.usable = nil
|
|
else
|
|
actions.active.usable = (pfUI and pfUI.bars) and nil or 1
|
|
end
|
|
end
|
|
elseif isUsableBySpell == 1 and actions.active.inRange ~= 0 then
|
|
-- Use IsUsableSpell result if available (handles forms/stances correctly)
|
|
actions.active.usable = 1
|
|
elseif isUsableBySpell == 0 then
|
|
-- IsSpellUsable returned 0 = wrong stance/form, not enough power type, etc.
|
|
actions.active.usable = nil
|
|
elseif actions.active.inRange ~= 0 and not actions.active.oom then
|
|
-- Fallback to mana check ONLY if IsSpellUsable not available
|
|
actions.active.usable = 1
|
|
|
|
-- pfUI:actionbar.lua -- update usable [out-of-range = 1, oom = 2, not-usable = 3, default = 0]
|
|
elseif pfUI and pfUI.bars and actions.active.oom then
|
|
actions.active.usable = 2
|
|
else
|
|
actions.active.usable = nil
|
|
end
|
|
|
|
-- Check if this is a toggled buff ability (Prowl, Shadowmeld) and darken if buff is active
|
|
-- This must come AFTER all other usability checks to have final say
|
|
-- PERFORMANCE: Cache normalized spell name on the action object to avoid gsub per-frame
|
|
local spellName = actions.active._normalizedName
|
|
if not spellName then
|
|
spellName = string.gsub(actions.active.action, "%s*%(.-%)%s*$", "")
|
|
spellName = string.gsub(spellName, "_", " ")
|
|
actions.active._normalizedName = spellName
|
|
end
|
|
|
|
-- PERFORMANCE: Use cached lookup instead of creating table per-call
|
|
if CleveRoids.IsToggledBuffAbility(spellName) then
|
|
if CleveRoids.ValidatePlayerBuff(spellName) then
|
|
-- Buff is active, darken the icon like "wrong stance" (grayed out, not red)
|
|
actions.active.usable = nil
|
|
actions.active.oom = false -- Make sure we don't show red tint
|
|
end
|
|
end
|
|
else
|
|
actions.active.inRange = 1
|
|
actions.active.usable = 1
|
|
end
|
|
if actions.active.usable ~= previousUsable or
|
|
actions.active.oom ~= previousOom or
|
|
actions.active.inRange ~= previousInRange then
|
|
changed = true
|
|
if CleveRoids.debug and actions.active.isReactive then
|
|
DEFAULT_CHAT_FRAME:AddMessage(
|
|
string.format("|cffff00ff[STATE CHANGED]|r %s: usable %s->%s, will send ACTIONBAR_SLOT_CHANGED",
|
|
actions.active.action, tostring(previousUsable), tostring(actions.active.usable))
|
|
)
|
|
end
|
|
end
|
|
end
|
|
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
|
|
local actionsToSlots = _actionsToSlotsBuffer
|
|
local actionsList = _actionsListBuffer
|
|
local actionsCount = 0
|
|
|
|
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
|
local Actions = CleveRoids.Actions
|
|
local slot, actions = next(Actions)
|
|
while slot do
|
|
if not actionsToSlots[actions] then
|
|
-- 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
|
|
slot, actions = next(Actions, slot)
|
|
end
|
|
|
|
-- Test each unique actions object once and send events to ALL slots that share it
|
|
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
|
|
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
|
|
|
|
function CleveRoids.ClearAction(slot)
|
|
if not CleveRoids.Actions[slot] then return end
|
|
CleveRoids.Actions[slot].active = nil
|
|
CleveRoids.Actions[slot] = nil
|
|
end
|
|
|
|
function CleveRoids.GetAction(slot)
|
|
if not slot or not CleveRoids.ready then return end
|
|
|
|
local actions = CleveRoids.Actions[slot]
|
|
if actions then return actions end
|
|
|
|
-- Identify the macro by its index via ClassicAPI's GetActionInfo so the
|
|
-- slot is mapped without going through the macro name (no unique-name
|
|
-- requirement for action-bar macros).
|
|
local macro
|
|
local kind, macroID = CleveRoids.ClassicAPI.GetActionInfo(slot)
|
|
if kind == "macro" and macroID then
|
|
macro = CleveRoids.GetMacroByIndex(macroID)
|
|
end
|
|
|
|
-- Fallback for SuperMacro (and anything GetActionInfo doesn't tag as a
|
|
-- macro): resolve by name.
|
|
if not macro then
|
|
local text = GetActionText(slot)
|
|
if text then
|
|
macro = CleveRoids.GetMacro(text)
|
|
end
|
|
end
|
|
|
|
if macro then
|
|
actions = macro.actions
|
|
CleveRoids.TestForActiveAction(actions)
|
|
CleveRoids.Actions[slot] = actions
|
|
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
|
|
return actions
|
|
end
|
|
end
|
|
|
|
function CleveRoids.GetActiveAction(slot)
|
|
local action = CleveRoids.GetAction(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
|
|
|
|
-- 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
|
|
|
|
if type(arg) == "table" then
|
|
|
|
local n_varargs_from_arg_table = arg.n or 0
|
|
for i = 1, 10 do
|
|
if i <= n_varargs_from_arg_table then
|
|
_G["arg" .. i] = arg[i]
|
|
else
|
|
_G["arg" .. i] = nil
|
|
end
|
|
end
|
|
else
|
|
for i = 1, 10 do
|
|
_G["arg" .. i] = nil
|
|
end
|
|
end
|
|
|
|
local button_to_call_event_on
|
|
local page = floor((slot - 1) / NUM_ACTIONBAR_BUTTONS) + 1
|
|
local pageSlot = slot - (page - 1) * NUM_ACTIONBAR_BUTTONS
|
|
|
|
if slot >= 73 then
|
|
button_to_call_event_on = _G["BonusActionButton" .. pageSlot]
|
|
elseif slot >= 61 then
|
|
button_to_call_event_on = _G["MultiBarBottomLeftButton" .. pageSlot]
|
|
elseif slot >= 49 then
|
|
button_to_call_event_on = _G["MultiBarBottomRightButton" .. pageSlot]
|
|
elseif slot >= 37 then
|
|
button_to_call_event_on = _G["MultiBarLeftButton" .. pageSlot]
|
|
elseif slot >= 25 then
|
|
button_to_call_event_on = _G["MultiBarRightButton" .. pageSlot]
|
|
end
|
|
|
|
if button_to_call_event_on then
|
|
this = button_to_call_event_on
|
|
ActionButton_OnEvent(event)
|
|
end
|
|
|
|
if page == CURRENT_ACTIONBAR_PAGE then
|
|
local main_bar_button = _G["ActionButton" .. pageSlot]
|
|
if main_bar_button and main_bar_button ~= button_to_call_event_on then
|
|
this = main_bar_button
|
|
ActionButton_OnEvent(event)
|
|
elseif not button_to_call_event_on and main_bar_button then
|
|
this = main_bar_button
|
|
ActionButton_OnEvent(event)
|
|
end
|
|
end
|
|
|
|
this = old_this
|
|
|
|
for i = 1, 10 do
|
|
_G["arg" .. i] = original_global_args[i]
|
|
end
|
|
|
|
-- FIX: Skip addon handler calls during initial indexing to prevent 120 rapid calls
|
|
if CleveRoids._suppressActionHandlers then
|
|
return
|
|
end
|
|
|
|
if type(arg) == "table" and arg.n then
|
|
|
|
if arg.n == 0 then
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event) end
|
|
elseif arg.n == 1 then
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1]) end
|
|
elseif arg.n == 2 then
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2]) end
|
|
elseif arg.n == 3 then
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3]) end
|
|
elseif arg.n == 4 then
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4]) end
|
|
elseif arg.n == 5 then
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5]) end
|
|
elseif arg.n == 6 then
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5], arg[6]) end
|
|
else
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5], arg[6], arg[7]) end
|
|
end
|
|
else
|
|
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do
|
|
fn_h(slot, event)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Executes the given Macro's body
|
|
-- body: The Macro's body
|
|
-- Note on macro flags:
|
|
-- stopMacroFlag: Stops ALL remaining lines (propagates to parent macros)
|
|
-- skipMacroFlag: Stops only current macro (cleared on return, parent continues)
|
|
function CleveRoids.ExecuteMacroBody(body,inline)
|
|
local lines = CleveRoids.splitString(body, "\n")
|
|
if inline then lines = CleveRoids.splitString(body, "\\n"); end
|
|
|
|
-- Save parent's stopMacroFlag state (in case we need to preserve it)
|
|
local parentStopFlag = CleveRoids.stopMacroFlag
|
|
|
|
-- Clear flags at start of this macro execution
|
|
-- Note: We DON'T clear if parent already set stopMacroFlag (shouldn't execute at all)
|
|
if not parentStopFlag then
|
|
CleveRoids.stopMacroFlag = false
|
|
end
|
|
CleveRoids.skipMacroFlag = false -- Always start fresh for skipmacro
|
|
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff00ffff[MacroRef]|r ExecuteMacroBody called with " .. table.getn(lines) .. " lines")
|
|
end
|
|
|
|
for k,v in pairs(lines) do
|
|
local trimmed = CleveRoids.Trim(v)
|
|
local cmdHandled = false
|
|
|
|
-- Skip #showtooltip lines - they're only meaningful for action bar display,
|
|
-- not for execution. This allows inner macros to have #showtooltip without
|
|
-- causing issues when called from other macros.
|
|
if string.find(trimmed, "^#showtooltip") then
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff888888[MacroRef]|r Skipping #showtooltip line in inner macro")
|
|
end
|
|
cmdHandled = true
|
|
end
|
|
|
|
-- IMPORTANT: Check for /nofirstaction BEFORE the stop flag check
|
|
-- This allows /nofirstaction to clear the stopMacroFlag set by /firstaction
|
|
local _, _, nofirstactionArgs = string.find(trimmed, "^/nofirstaction%s*(.*)")
|
|
if nofirstactionArgs then
|
|
-- Capture stopOnCastFlag BEFORE DoNoFirstAction clears it
|
|
-- If it was true, stopMacroFlag was set by /firstaction, not /stopmacro
|
|
local wasFirstActionActive = CleveRoids.stopOnCastFlag
|
|
CleveRoids.DoNoFirstAction(nofirstactionArgs)
|
|
-- Also clear stopMacroFlag if it was set by firstaction mechanism
|
|
-- (but NOT if it was set by explicit /stopmacro)
|
|
if wasFirstActionActive and CleveRoids.stopMacroFlag then
|
|
-- stopOnCastFlag was true before we cleared it, and stopMacroFlag is set
|
|
-- This means stopMacroFlag was set by the firstaction mechanism, clear it
|
|
CleveRoids.stopMacroFlag = false
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff00ff00[MacroRef]|r /nofirstaction cleared stopMacroFlag - resuming macro")
|
|
end
|
|
end
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff88ff88[MacroRef]|r Executing line " .. k .. ": " .. string.sub(v, 1, 60))
|
|
end
|
|
cmdHandled = true
|
|
end
|
|
|
|
-- Check both macro stop flags before each line (but skip if we just handled /nofirstaction)
|
|
-- stopMacroFlag: stop this AND parent macros
|
|
-- skipMacroFlag: stop only this macro (parent continues)
|
|
if not cmdHandled and (CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag) then
|
|
-- If stopOnCastFlag is true, the stop was caused by the firstaction mechanism
|
|
-- Don't break the loop - just skip this line (allows /nofirstaction to be reached)
|
|
if CleveRoids.stopOnCastFlag and CleveRoids.stopMacroFlag and not CleveRoids.skipMacroFlag then
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cffff8800[MacroRef]|r Skipping line " .. k .. " (firstaction mode)")
|
|
end
|
|
cmdHandled = true -- Skip this line but continue loop to allow /nofirstaction
|
|
else
|
|
-- This is a /stopmacro or /skipmacro stop - actually break
|
|
if CleveRoids.macroRefDebug then
|
|
local reason = CleveRoids.stopMacroFlag and "/stopmacro" or "/skipmacro"
|
|
CleveRoids.Print("|cffff8800[MacroRef]|r Stopped at line " .. k .. " due to " .. reason)
|
|
end
|
|
break
|
|
end
|
|
end
|
|
|
|
if not cmdHandled then
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff88ff88[MacroRef]|r Executing line " .. k .. ": " .. string.sub(v, 1, 60))
|
|
end
|
|
|
|
-- IMPORTANT: Handle macro control commands directly to bypass Blizzard's built-in /stopmacro
|
|
-- Blizzard intercepts bare /stopmacro before it reaches our SlashCmdList handler
|
|
|
|
-- Check for /stopmacro (with or without conditionals)
|
|
local _, _, stopmacroArgs = string.find(trimmed, "^/stopmacro%s*(.*)")
|
|
if stopmacroArgs then
|
|
CleveRoids.DoStopMacro(stopmacroArgs)
|
|
cmdHandled = true
|
|
end
|
|
|
|
-- Check for /skipmacro (with or without conditionals)
|
|
if not cmdHandled then
|
|
local _, _, skipmacroArgs = string.find(trimmed, "^/skipmacro%s*(.*)")
|
|
if skipmacroArgs then
|
|
CleveRoids.DoSkipMacro(skipmacroArgs)
|
|
cmdHandled = true
|
|
end
|
|
end
|
|
|
|
-- Check for /firstaction (with or without conditionals)
|
|
if not cmdHandled then
|
|
local _, _, firstactionArgs = string.find(trimmed, "^/firstaction%s*(.*)")
|
|
if firstactionArgs then
|
|
CleveRoids.DoFirstAction(firstactionArgs)
|
|
cmdHandled = true
|
|
end
|
|
end
|
|
|
|
-- FIX: Handle /startattack directly - not a native vanilla WoW command
|
|
-- Without SuperMacro, ChatEdit_SendText may not route addon slash commands properly
|
|
if not cmdHandled then
|
|
local _, _, startattackArgs = string.find(trimmed, "^/startattack%s*(.*)")
|
|
if startattackArgs then
|
|
if SlashCmdList.STARTATTACK then
|
|
SlashCmdList.STARTATTACK(startattackArgs)
|
|
end
|
|
cmdHandled = true
|
|
end
|
|
end
|
|
|
|
-- FIX: Handle /stopattack directly - not a native vanilla WoW command
|
|
if not cmdHandled then
|
|
local _, _, stopattackArgs = string.find(trimmed, "^/stopattack%s*(.*)")
|
|
if stopattackArgs then
|
|
if SlashCmdList.STOPATTACK then
|
|
SlashCmdList.STOPATTACK(stopattackArgs)
|
|
end
|
|
cmdHandled = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- For all other commands, use ChatEdit_SendText
|
|
if not cmdHandled then
|
|
ChatFrameEditBox:SetText(v)
|
|
ChatEdit_SendText(ChatFrameEditBox)
|
|
end
|
|
end
|
|
|
|
-- Clear skipMacroFlag after this macro completes (don't propagate to parent)
|
|
-- stopMacroFlag is intentionally LEFT SET so it propagates up
|
|
CleveRoids.skipMacroFlag = false
|
|
|
|
return true
|
|
end
|
|
|
|
-- Gets the body of the Macro with the given name
|
|
-- name: The name of the Macro
|
|
-- returns: The body of the macro
|
|
function CleveRoids.GetMacroBody(name)
|
|
local macro = CleveRoids.GetMacro(name)
|
|
return macro and macro.body
|
|
end
|
|
|
|
-- 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
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cffff0000[MacroRef]|r ExecuteMacroByName called with empty name")
|
|
end
|
|
return false
|
|
end
|
|
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff00ffff[MacroRef]|r ExecuteMacroByName called with: '" .. name .. "'")
|
|
end
|
|
|
|
local body
|
|
local source = nil
|
|
|
|
-- Check if input is a numeric index (e.g., "19" or 19)
|
|
local numericIndex = tonumber(name)
|
|
|
|
-- 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
|
|
source = "Blizzard (index " .. numericIndex .. ")"
|
|
end
|
|
else
|
|
-- String name - look up by name
|
|
local id = GetMacroIndexByName(name)
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff888888[MacroRef]|r GetMacroIndexByName('" .. name .. "') = " .. tostring(id))
|
|
end
|
|
if id and id ~= 0 then
|
|
local _n, _tex, b = GetMacroInfo(id)
|
|
if b and b ~= "" then
|
|
body = b
|
|
source = "Blizzard (slot " .. id .. ")"
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 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
|
|
source = "SuperMacro"
|
|
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
|
|
source = "CRM cache"
|
|
end
|
|
end
|
|
end
|
|
|
|
if not body or body == "" then
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cffff0000[MacroRef]|r Macro '" .. name .. "' not found in any source")
|
|
end
|
|
return false
|
|
end
|
|
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff00ff00[MacroRef]|r Found macro '" .. name .. "' from " .. source)
|
|
end
|
|
|
|
-- Execute using CleveRoids' processor (supports conditionals, extended syntax)
|
|
return CleveRoids.ExecuteMacroBody(body)
|
|
end
|
|
|
|
function CleveRoids.SetHelp(conditionals)
|
|
if conditionals.harm then
|
|
conditionals.help = false
|
|
end
|
|
end
|
|
|
|
function CleveRoids.FixEmptyTarget(conditionals)
|
|
if not conditionals.target then
|
|
if UnitExists("target") then
|
|
conditionals.target = "target"
|
|
elseif GetCVar("autoSelfCast") == "1" and not conditionals.target == "help" then
|
|
conditionals.target = "player"
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Fixes the conditionals' target by targeting the target with the given name
|
|
-- conditionals: The conditionals containing the current target
|
|
-- name: The name of the player to target
|
|
-- hook: The target hook
|
|
-- returns: Whether or not we've changed the player's current target
|
|
function CleveRoids.FixEmptyTargetSetTarget(conditionals, name, hook)
|
|
if not conditionals.target then
|
|
hook(name)
|
|
conditionals.target = "target"
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Returns the name of the focus target or nil
|
|
function CleveRoids.GetFocusName()
|
|
return UnitName('focus')
|
|
end
|
|
|
|
-- Attempts to target the focus target.
|
|
-- returns: Whether or not it succeeded
|
|
function CleveRoids.TryTargetFocus()
|
|
-- ClassicAPI native focus token: exact target switch, no name matching.
|
|
if UnitExists("focus") then
|
|
TargetUnit("focus")
|
|
return UnitExists("target")
|
|
end
|
|
|
|
-- Fallback: pfUI / name-based focus.
|
|
local name = CleveRoids.GetFocusName()
|
|
|
|
if not name then
|
|
return false
|
|
end
|
|
|
|
TargetByName(name, true)
|
|
|
|
if not UnitExists("target") or (string.lower(UnitName("target")) ~= name) then
|
|
-- The target switch failed (out of range, LoS, etc.)
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Resolves the player's focus to a usable unit token.
|
|
-- Prefers pfUI's emulated focus (resolves to a real token like "party2"/"raid5")
|
|
-- for pfUI users, then falls back to ClassicAPI's native "focus" token (which
|
|
-- every UnitX function accepts) for everyone else.
|
|
-- Returns the resolved token, or nil when no focus is set so @focus clauses
|
|
-- silently fall through to the next macro alternative (no warning spam).
|
|
function CleveRoids.GetFocusUnitId()
|
|
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id
|
|
and UnitExists(pfUI.uf.focus.label .. pfUI.uf.focus.id) then
|
|
return pfUI.uf.focus.label .. pfUI.uf.focus.id
|
|
end
|
|
-- ClassicAPI native focus token (set via /focus or the FOCUSTARGET keybind)
|
|
if UnitExists("focus") then
|
|
return "focus"
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function CleveRoids.GetMacroNameFromAction(text)
|
|
if string.sub(text, 1, 1) == "{" and string.sub(text, -1) == "}" then
|
|
local name
|
|
if string.sub(text, 2, 2) == "\"" and string.sub(text, -2, -2) == "\"" then
|
|
return string.sub(text, 3, -3)
|
|
else
|
|
return string.sub(text, 2, -2)
|
|
end
|
|
end
|
|
end
|
|
|
|
function CleveRoids.CreateActionInfo(action, conditionals)
|
|
local _, _, text = string.find(action, "!?%??~?(.*)")
|
|
local spell = CleveRoids.GetSpell(text)
|
|
local petSpell -- Add this line
|
|
local item, macroName, macro, macroTooltip, actionType, texture
|
|
|
|
-- NEW: Check if the action is a slot number
|
|
local slotId = tonumber(text)
|
|
if slotId and slotId >= 1 and slotId <= 19 then
|
|
actionType = "item"
|
|
local itemTexture = GetInventoryItemTexture("player", slotId)
|
|
if itemTexture then
|
|
texture = itemTexture
|
|
else
|
|
texture = CleveRoids.unknownTexture
|
|
end
|
|
else
|
|
-- Original logic for named items and spells
|
|
if not spell then
|
|
petSpell = CleveRoids.GetPetSpell(text) -- Add this line
|
|
end
|
|
if not spell and not petSpell then -- Modify this line
|
|
item = CleveRoids.GetItem(text)
|
|
end
|
|
if not item and not petSpell then -- Modify this line
|
|
macroName = CleveRoids.GetMacroNameFromAction(text)
|
|
macro = CleveRoids.GetMacro(macroName)
|
|
macroTooltip = (macro and macro.actions) and macro.actions.tooltip
|
|
end
|
|
|
|
if spell then
|
|
actionType = "spell"
|
|
texture = spell.texture or CleveRoids.unknownTexture
|
|
elseif petSpell then -- Add this block
|
|
actionType = "petspell"
|
|
texture = petSpell.texture or CleveRoids.unknownTexture
|
|
elseif item then
|
|
actionType = "item"
|
|
texture = (item and item.texture) or CleveRoids.unknownTexture
|
|
elseif macro then
|
|
actionType = "macro"
|
|
texture = (macro.actions and macro.actions.tooltip and macro.actions.tooltip.texture)
|
|
or (macro and macro.texture)
|
|
or CleveRoids.unknownTexture
|
|
end
|
|
end
|
|
|
|
local info = {
|
|
action = text,
|
|
item = item,
|
|
spell = spell,
|
|
petSpell = petSpell, -- Add this line
|
|
macro = macroTooltip,
|
|
type = actionType,
|
|
texture = texture,
|
|
conditionals = conditionals,
|
|
}
|
|
|
|
return info
|
|
end
|
|
|
|
function CleveRoids.SplitCommandAndArgs(text)
|
|
local _, _, cmd, args = string.find(text, "(/%w+%s?)(.*)")
|
|
if cmd and args then
|
|
cmd = CleveRoids.Trim(cmd)
|
|
text = CleveRoids.Trim(args)
|
|
end
|
|
return cmd, args
|
|
end
|
|
|
|
function CleveRoids.ParseSequence(text)
|
|
if not text or text == "" then return end
|
|
|
|
-- normalize commas
|
|
local args = string.gsub(text, "(%s*,%s*)", ",")
|
|
|
|
-- optional [conditionals] block
|
|
local _, condEnd, condBlock = string.find(args, "(%[.*%])")
|
|
|
|
-- accept reset= anywhere; no trailing space required; strip it out once
|
|
local _, _, resetVal = string.find(args, "[Rr][Ee][Ss][Ee][Tt]=([%w/]+)")
|
|
if resetVal then
|
|
args = string.gsub(args, "%s*[Rr][Ee][Ss][Ee][Tt]=[%w/]+%s*", " ", 1)
|
|
end
|
|
|
|
-- actions are what's left after any ] (if present)
|
|
local actionSeq = CleveRoids.Trim((condEnd and string.sub(args, condEnd + 1)) or args)
|
|
args = (condBlock or "") .. actionSeq
|
|
if actionSeq == "" then return end
|
|
|
|
local sequence = {
|
|
index = 1,
|
|
reset = {},
|
|
status = 0,
|
|
lastUpdate = 0,
|
|
args = args,
|
|
cond = condBlock or "", -- Store conditional block for evaluation in DoCastSequence
|
|
list = {},
|
|
perTargetState = {}, -- [guid] = {index = N, lastUpdate = T} for reset=target
|
|
}
|
|
|
|
-- fill reset rules: seconds or flags (target/combat/alt/ctrl/shift)
|
|
if resetVal then
|
|
for _, rule in pairs(CleveRoids.Split(resetVal, "/")) do
|
|
rule = string.lower(CleveRoids.Trim(rule))
|
|
local secs = tonumber(rule)
|
|
if secs and secs > 0 then
|
|
sequence.reset.secs = secs
|
|
else
|
|
sequence.reset[rule] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- build steps
|
|
for _, a in ipairs(CleveRoids.Split(actionSeq, ",")) do
|
|
local sa = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(a))
|
|
table.insert(sequence.list, sa)
|
|
end
|
|
|
|
CleveRoids.Sequences[text] = sequence
|
|
return sequence
|
|
end
|
|
|
|
-- Shared: build, parse, and cache a macro under `cacheKey`. For action-bar
|
|
-- macros the key is the Blizzard macro index (unique), so duplicate or blank
|
|
-- macro names no longer collide; SuperMacro macros (no index) are keyed by name.
|
|
-- macroID is the Blizzard index if known, else nil.
|
|
local function BuildMacro(cacheKey, macroID, name, texture, body)
|
|
local macro = {
|
|
id = macroID,
|
|
name = name,
|
|
texture = texture,
|
|
body = body,
|
|
actions = {},
|
|
}
|
|
macro.actions.list = {}
|
|
|
|
-- build a list of testable actions for the macro
|
|
local hasShowTooltip = false
|
|
local showTooltipHasArg = false
|
|
|
|
for i, line in ipairs(CleveRoids.splitString(body, "\n")) do
|
|
line = CleveRoids.Trim(line)
|
|
local cmd, args = CleveRoids.SplitCommandAndArgs(line)
|
|
|
|
-- check for #showtooltip on first line
|
|
if i == 1 then
|
|
local _, _, st, _, tt = string.find(line, "(#showtooltip)(%s?(.*))")
|
|
|
|
if st then
|
|
hasShowTooltip = true
|
|
tt = CleveRoids.Trim(tt)
|
|
|
|
-- #showtooltip with explicit item/spell/macro specified
|
|
if tt ~= "" then
|
|
showTooltipHasArg = true
|
|
for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(tt)) do
|
|
-- Parse the arg to extract the spell name from any conditionals
|
|
local parsedArg = CleveRoids.GetParsedMsg(arg)
|
|
macro.actions.tooltip = CleveRoids.CreateActionInfo(parsedArg)
|
|
local action = CleveRoids.CreateActionInfo(parsedArg)
|
|
action.cmd = "/cast"
|
|
action.args = arg
|
|
action.isReactive = CleveRoids.reactiveSpells[action.action]
|
|
table.insert(macro.actions.list, action)
|
|
end
|
|
end
|
|
-- Continue parsing remaining lines for action list (needed for nested macro resolution)
|
|
end
|
|
-- No #showtooltip: still continue parsing to build action list for inner macro resolution
|
|
else
|
|
-- make sure we have a testable action
|
|
if line ~= "" and args ~= "" and CleveRoids.dynamicCmds[cmd] then
|
|
for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(args)) do
|
|
local action = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(arg))
|
|
|
|
if cmd == "/castsequence" then
|
|
local sequence = CleveRoids.GetSequence(args)
|
|
if sequence then
|
|
action.sequence = sequence
|
|
end
|
|
end
|
|
action.cmd = cmd
|
|
action.args = arg
|
|
action.isReactive = CleveRoids.reactiveSpells[action.action]
|
|
table.insert(macro.actions.list, action)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If #showtooltip was present but had no argument, use the first action as the tooltip
|
|
if hasShowTooltip and not showTooltipHasArg and table.getn(macro.actions.list) > 0 then
|
|
macro.actions.tooltip = macro.actions.list[1]
|
|
end
|
|
|
|
-- Store whether #showtooltip had an explicit argument (for icon fallback logic)
|
|
macro.actions.explicitTooltip = showTooltipHasArg
|
|
|
|
CleveRoids.Macros[cacheKey] = macro
|
|
return macro
|
|
end
|
|
|
|
-- Parse a macro by its Blizzard macro index (1-36), caching by index. This is
|
|
-- the action-bar path: a slot's macro is identified by index via ClassicAPI's
|
|
-- GetActionInfo, so duplicate/blank macro names no longer collide.
|
|
function CleveRoids.ParseMacroByIndex(macroID)
|
|
if not macroID or macroID == 0 then return end
|
|
local name, texture, body = GetMacroInfo(macroID)
|
|
if not texture or not body then return end
|
|
return BuildMacro(macroID, macroID, name, texture, body)
|
|
end
|
|
|
|
-- Parse a macro by name (explicit {MacroName} / runmacro references, and
|
|
-- SuperMacro macros that have no Blizzard index). Resolves to the index path
|
|
-- when possible so the cache stays index-keyed.
|
|
function CleveRoids.ParseMacro(name)
|
|
if not name then return end
|
|
|
|
local macroID = GetMacroIndexByName(name)
|
|
if macroID and macroID ~= 0 then
|
|
return CleveRoids.ParseMacroByIndex(macroID)
|
|
end
|
|
|
|
-- SuperMacro has no Blizzard macro index; cache by name.
|
|
if GetSuperMacroInfo then
|
|
local _, texture, body = GetSuperMacroInfo(name)
|
|
if texture and body then
|
|
return BuildMacro(name, nil, name, texture, body)
|
|
end
|
|
end
|
|
end
|
|
|
|
function CleveRoids.ParseMsg(msg)
|
|
if not msg then return end
|
|
local conditionals = {}
|
|
|
|
-- reset side flag for this parse
|
|
CleveRoids._ignoretooltip = 0
|
|
|
|
-- strip optional leading '?' and remember how many we stripped
|
|
local ignorecount
|
|
msg, ignorecount = string.gsub(CleveRoids.Trim(msg), "^%?", "")
|
|
conditionals.ignoretooltip = ignorecount
|
|
CleveRoids._ignoretooltip = ignorecount
|
|
|
|
-- capture a single [...] conditional block if present
|
|
local _, cbEnd, conditionBlock = string.find(msg, "%[(.+)%]")
|
|
|
|
-- split off flags/action after the condition block (or from start if none)
|
|
local _, _, noSpam, cancelAura, action = string.find(
|
|
string.sub(msg, (cbEnd or 0) + 1),
|
|
"^%s*(!?)(~?)([^!~]+.*)"
|
|
)
|
|
action = CleveRoids.Trim(action or "")
|
|
|
|
-- Also check for ? at start of action (after conditional block)
|
|
-- This handles "[cond] ?action" syntax where ? appears after conditionals
|
|
if string.sub(action, 1, 1) == "?" then
|
|
action = CleveRoids.Trim(string.sub(action, 2))
|
|
conditionals.ignoretooltip = 1
|
|
CleveRoids._ignoretooltip = 1
|
|
end
|
|
|
|
-- store the raw action for callers and strip trailing "(Rank X)" for comparisons
|
|
conditionals.action = action
|
|
action = CleveRoids.StripRank(action)
|
|
|
|
-- IMPORTANT: if there's NO conditional block, return nil conditionals so
|
|
-- DoWithConditionals will hit the {macroName} execution branch.
|
|
if not conditionBlock then
|
|
local hasFlag = (noSpam and noSpam ~= "") or (cancelAura and cancelAura ~= "")
|
|
if hasFlag and action ~= "" then
|
|
if noSpam ~= "" then
|
|
-- ! prefix: handled at execution time via CastSpellNoToggle
|
|
-- (see DoWithConditionals), no gate conditional needed.
|
|
conditionals.noSpam = true
|
|
end
|
|
if cancelAura ~= "" then
|
|
conditionals.cancelaura = action
|
|
end
|
|
return conditionals.action, conditionals
|
|
end
|
|
return conditionals.action, nil
|
|
end
|
|
|
|
-- With a condition block present, build out the conditionals table
|
|
|
|
-- ! prefix with explicit conditionals: store as a flag for execution-time anti-toggle,
|
|
-- rather than injecting an implicit conditional that gets AND'd with user conditions.
|
|
-- The anti-toggle check happens in DoWithConditionals before CastSpellByName.
|
|
if noSpam and noSpam ~= "" then
|
|
conditionals.noSpam = true
|
|
end
|
|
if cancelAura and cancelAura ~= "" then
|
|
conditionals.cancelaura = action
|
|
end
|
|
|
|
-- Set the action's target to @unitid if found (e.g., @mouseover)
|
|
local _, _, target = string.find(conditionBlock, "(@[^%s,]+)")
|
|
if target then
|
|
conditionBlock = CleveRoids.Trim(string.gsub(conditionBlock, target, ""))
|
|
conditionals.target = string.sub(target, 2)
|
|
end
|
|
|
|
if conditionBlock and conditionals.action then
|
|
-- Split the conditional block by comma or space
|
|
for _, conditionGroups in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do
|
|
if conditionGroups ~= "" then
|
|
-- Split conditional groups by colon (rejoin extra parts for multi-part args
|
|
-- e.g., distance:30:facing>1 → condition="distance", args="30:facing>1")
|
|
local conditionGroup = CleveRoids.splitStringIgnoringQuotes(conditionGroups, ":")
|
|
local condition = conditionGroup[1]
|
|
local args = conditionGroup[2]
|
|
for _cgi = 3, table.getn(conditionGroup) do
|
|
args = (args or "") .. ":" .. conditionGroup[_cgi]
|
|
end
|
|
|
|
-- No args → the action is the implicit argument
|
|
if not args or args == "" then
|
|
if not conditionals[condition] then
|
|
-- PERFORMANCE: Use module-level constant instead of creating table per-call
|
|
if BOOLEAN_CONDITIONALS[condition] then
|
|
conditionals[condition] = true
|
|
else
|
|
conditionals[condition] = conditionals.action
|
|
-- Create first group for non-boolean conditionals
|
|
if not conditionals._groups then
|
|
conditionals._groups = {}
|
|
end
|
|
conditionals._groups[condition] = { { values = { conditionals.action }, n = 1, operator = "OR" }, n = 1 }
|
|
end
|
|
else
|
|
-- existing code for when conditionals[condition] already exists
|
|
if type(conditionals[condition]) ~= "table" then
|
|
conditionals[condition] = { conditionals[condition] }
|
|
end
|
|
table.insert(conditionals[condition], conditionals.action)
|
|
|
|
-- Multiple instances of same conditional = AND logic
|
|
if not conditionals._operators then
|
|
conditionals._operators = {}
|
|
end
|
|
conditionals._operators[condition] = "AND"
|
|
|
|
-- Add new group for this instance
|
|
if not conditionals._groups then
|
|
conditionals._groups = {}
|
|
end
|
|
if not conditionals._groups[condition] then
|
|
conditionals._groups[condition] = { n = 0 }
|
|
end
|
|
local grp = conditionals._groups[condition]
|
|
local newEntry = { values = { conditionals.action }, n = 1, operator = "OR" }
|
|
grp.n = grp.n + 1
|
|
grp[grp.n] = newEntry
|
|
end
|
|
else
|
|
-- Has args. Ensure the key's value is a table and add new arguments.
|
|
-- Track if this conditional already existed (repeated via comma)
|
|
local conditionAlreadyExists = conditionals[condition] ~= nil
|
|
|
|
if not conditionals[condition] then
|
|
conditionals[condition] = {}
|
|
elseif type(conditionals[condition]) ~= "table" then
|
|
conditionals[condition] = { conditionals[condition] }
|
|
end
|
|
|
|
-- Detect which separator is used: / (OR) or & (AND)
|
|
-- Initialize metadata tables if needed
|
|
if not conditionals._operators then
|
|
conditionals._operators = {}
|
|
end
|
|
if not conditionals._groups then
|
|
conditionals._groups = {}
|
|
end
|
|
|
|
-- Check which separator is present in the CURRENT args
|
|
local hasSlash = string.find(args, "/")
|
|
local hasAmpersand = string.find(args, "&")
|
|
|
|
-- Detect if & is part of a multi-comparison pattern (e.g., >0&<10)
|
|
-- Pattern: operator+number followed by & (with optional whitespace) followed by operator
|
|
-- IMPORTANT: Only whitespace allowed around &, NOT letters (to distinguish from Rip>3&Rake>3)
|
|
local isMultiComparison = hasAmpersand and string.find(args, "[>~=<]+%d+%.?%d*%s*&%s*[>~=<]")
|
|
|
|
local separator = "/"
|
|
local operatorType = "OR"
|
|
|
|
if hasAmpersand and not hasSlash and not isMultiComparison then
|
|
separator = "&"
|
|
operatorType = "AND"
|
|
elseif hasAmpersand and hasSlash then
|
|
-- Both separators present - default to / (OR) and warn
|
|
-- Could add a warning here in the future
|
|
separator = "/"
|
|
operatorType = "OR"
|
|
end
|
|
|
|
-- Store the operator type for this conditional (for backwards compat)
|
|
-- Note: when groups exist, Multi/NegatedMulti will use per-group operators
|
|
conditionals._operators[condition] = operatorType
|
|
|
|
-- Create a new group for this conditional instance
|
|
-- Structure: { values = { ... }, operator = "OR" or "AND" }
|
|
if not conditionals._groups[condition] then
|
|
conditionals._groups[condition] = { n = 0 }
|
|
end
|
|
local currentGroup = { values = {}, n = 0, operator = operatorType }
|
|
local grp = conditionals._groups[condition]
|
|
grp.n = grp.n + 1
|
|
grp[grp.n] = currentGroup
|
|
|
|
-- Split args by the determined separator
|
|
for _, arg_item in CleveRoids.splitString(args, separator) do
|
|
local processed_arg = CleveRoids.Trim(arg_item)
|
|
|
|
processed_arg = string.gsub(processed_arg, '"', "")
|
|
processed_arg = string.gsub(processed_arg, "_", " ")
|
|
processed_arg = CleveRoids.Trim(processed_arg)
|
|
|
|
-- normalize "name#N" → "name=#N" and "#N" → "=#N"
|
|
local arg_for_find = processed_arg
|
|
arg_for_find = string.gsub(arg_for_find, "^#(%d+)$", "=#%1")
|
|
arg_for_find = string.gsub(arg_for_find, "([^>~=<]+)#(%d+)", "%1=#%2")
|
|
|
|
-- accept decimals too; capture name/op/amount
|
|
local _, _, name, operator, amount = string.find(arg_for_find, "([^>~=<]*)([>~=<]+)(#?%d*%.?%d+)")
|
|
|
|
if CleveRoids.debug then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"|cffff9900[Parse]|r cond=%s arg='%s' find='%s' name=%s op=%s amt=%s",
|
|
tostring(condition), tostring(processed_arg), tostring(arg_for_find),
|
|
tostring(name), tostring(operator), tostring(amount)
|
|
))
|
|
end
|
|
|
|
if not operator or not amount then
|
|
-- No operator found, treat as simple string argument
|
|
table.insert(conditionals[condition], processed_arg)
|
|
currentGroup.n = currentGroup.n + 1
|
|
currentGroup.values[currentGroup.n] = processed_arg
|
|
else
|
|
local name_to_use = (name and name ~= "") and name or conditionals.action
|
|
local final_amount_str, num_replacements = string.gsub(amount, "#", "")
|
|
local should_check_stacks = (num_replacements == 1)
|
|
|
|
-- SPECIAL HANDLING FOR CONDITIONALS WITH MULTIPLE COMPARISONS
|
|
-- Detect if this conditional has multiple operators
|
|
-- Example: "ap>1800/<2200" or "Recently_Bandaged>0&<10" or "health>50&<80"
|
|
-- Works with ANY conditional that supports numeric operators
|
|
-- Pattern: only whitespace or separators allowed between comparisons, NOT letters
|
|
if string.find(processed_arg, "[>~=<]+%d+%.?%d*%s*[/&]%s*[>~=<]") then
|
|
-- This arg has multiple comparisons, parse them all
|
|
local stat_name = name_to_use
|
|
local comparisons = {}
|
|
|
|
-- Extract all operator+number pairs
|
|
-- Pattern matches operator followed by optional decimal number
|
|
for op, num in string.gfind(processed_arg, "([>~=<]+)(#?%d*%.?%d+)") do
|
|
local clean_num = string.gsub(num, "#", "")
|
|
local check_stacks = (string.find(num, "#") ~= nil)
|
|
table.insert(comparisons, {
|
|
operator = op,
|
|
amount = tonumber(clean_num),
|
|
checkStacks = check_stacks
|
|
})
|
|
end
|
|
|
|
if table.getn(comparisons) > 0 then
|
|
local entry = {
|
|
name = CleveRoids.Trim(stat_name),
|
|
comparisons = comparisons -- Store all comparisons
|
|
}
|
|
table.insert(conditionals[condition], entry)
|
|
currentGroup.n = currentGroup.n + 1
|
|
currentGroup.values[currentGroup.n] = entry
|
|
else
|
|
-- Fallback to single comparison if parsing failed
|
|
local entry = {
|
|
name = CleveRoids.Trim(name_to_use),
|
|
operator = operator,
|
|
amount = tonumber(final_amount_str),
|
|
checkStacks = should_check_stacks
|
|
}
|
|
table.insert(conditionals[condition], entry)
|
|
currentGroup.n = currentGroup.n + 1
|
|
currentGroup.values[currentGroup.n] = entry
|
|
end
|
|
else
|
|
-- Normal single-comparison conditional (existing behavior)
|
|
local entry = {
|
|
name = CleveRoids.Trim(name_to_use),
|
|
operator = operator,
|
|
amount = tonumber(final_amount_str),
|
|
checkStacks = should_check_stacks
|
|
}
|
|
table.insert(conditionals[condition], entry)
|
|
currentGroup.n = currentGroup.n + 1
|
|
currentGroup.values[currentGroup.n] = entry
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return conditionals.action, conditionals
|
|
end
|
|
end
|
|
|
|
|
|
-- Get previously parsed or parse, store and return
|
|
function CleveRoids.GetParsedMsg(msg)
|
|
if not msg then return end
|
|
|
|
-- PERFORMANCE: Check cache first before doing string operations
|
|
local cached = CleveRoids.ParsedMsg[msg]
|
|
if cached then
|
|
-- Use cached ignoretooltip value (already computed during first parse)
|
|
CleveRoids._ignoretooltip = cached.ignoretooltip or 0
|
|
return cached.action, cached.conditionals
|
|
end
|
|
|
|
-- Only compute ignoretooltip for new messages (not cache hits)
|
|
-- PERFORMANCE: Use string.sub for simple prefix check instead of gsub
|
|
local trimmed = CleveRoids.Trim(msg)
|
|
local ignorecount = (string.sub(trimmed, 1, 1) == "?") and 1 or 0
|
|
CleveRoids._ignoretooltip = ignorecount
|
|
|
|
local action, conditionals = CleveRoids.ParseMsg(msg)
|
|
-- Use ParseMsg's final _ignoretooltip value for cache, not the pre-computed one.
|
|
-- ParseMsg may detect ? after conditional blocks (e.g., "[cond] ?action")
|
|
-- which the prefix check above would miss.
|
|
local finalIgnore = CleveRoids._ignoretooltip
|
|
CleveRoids.ParsedMsg[msg] = {
|
|
action = action,
|
|
conditionals = conditionals,
|
|
ignoretooltip = finalIgnore,
|
|
}
|
|
return action, conditionals
|
|
end
|
|
|
|
|
|
function CleveRoids.GetMacro(name)
|
|
return CleveRoids.Macros[name] or CleveRoids.ParseMacro(name)
|
|
end
|
|
|
|
-- Get a parsed macro by its Blizzard macro index (1-36). Used by the action-bar
|
|
-- path so macros are identified by slot/index rather than name.
|
|
function CleveRoids.GetMacroByIndex(macroID)
|
|
if not macroID then return end
|
|
return CleveRoids.Macros[macroID] or CleveRoids.ParseMacroByIndex(macroID)
|
|
end
|
|
|
|
function CleveRoids.GetSequence(args)
|
|
return CleveRoids.Sequences[args] or CleveRoids.ParseSequence(args)
|
|
end
|
|
|
|
function CleveRoids.GetCurrentSequenceAction(sequence)
|
|
return sequence.list[sequence.index]
|
|
end
|
|
|
|
function CleveRoids.ResetSequence(sequence)
|
|
-- Save the GUID before clearing it
|
|
local targetGuid = sequence.lastTargetGuid
|
|
|
|
sequence.index = 1
|
|
sequence.lastTargetGuid = nil -- Clear stored GUID so next use captures fresh target
|
|
|
|
-- Also reset per-target state for current target if tracking
|
|
if sequence.reset and sequence.reset.target and targetGuid then
|
|
if sequence.perTargetState then
|
|
sequence.perTargetState[targetGuid] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
function CleveRoids.AdvanceSequence(sequence)
|
|
if sequence.index < table.getn(sequence.list) then
|
|
-- Not at the end yet, just advance normally
|
|
local oldIndex = sequence.index
|
|
sequence.index = sequence.index + 1
|
|
|
|
if CleveRoids.debug then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"|cff00ff00[Sequence Advanced]|r %d -> %d (of %d)",
|
|
oldIndex, sequence.index, table.getn(sequence.list)
|
|
))
|
|
end
|
|
|
|
-- Sync per-target state if tracking by target
|
|
if sequence.reset and sequence.reset.target and sequence.lastTargetGuid then
|
|
sequence.perTargetState = sequence.perTargetState or {}
|
|
sequence.perTargetState[sequence.lastTargetGuid] = {
|
|
index = sequence.index,
|
|
lastUpdate = GetTime(),
|
|
}
|
|
end
|
|
else
|
|
-- Sequence completed all steps - always wrap back to step 1.
|
|
-- reset=secs/target/combat only apply as mid-sequence idle resets
|
|
-- (matching standard WoW castsequence behavior).
|
|
CleveRoids.ResetSequence(sequence)
|
|
end
|
|
end
|
|
|
|
function CleveRoids.TestAction(cmd, args)
|
|
local msg, conditionals = CleveRoids.GetParsedMsg(args)
|
|
|
|
-- Nil-safe guards
|
|
local hasShowTooltip = (type(msg) == "string") and string.find(msg, "#showtooltip")
|
|
local ignoreTooltip = ((type(conditionals) == "table") and (conditionals.ignoretooltip == 1))
|
|
or (CleveRoids._ignoretooltip == 1)
|
|
|
|
-- If the line is explicitly ignored for tooltip (via '?'),
|
|
-- or it is a '#showtooltip' token, do not contribute icon/tooltip.
|
|
if hasShowTooltip or ignoreTooltip then
|
|
CleveRoids._ignoretooltip = 0 -- clear for next parse
|
|
return
|
|
end
|
|
|
|
-- No [] block → return a testable token so the UI can pick a texture
|
|
if not conditionals then
|
|
if not msg or msg == "" then
|
|
return
|
|
else
|
|
return CleveRoids.GetMacroNameFromAction(msg) or msg
|
|
end
|
|
end
|
|
|
|
local origTarget = conditionals.target
|
|
if cmd == "" or not CleveRoids.dynamicCmds[cmd] then
|
|
return
|
|
end
|
|
|
|
if conditionals.target == "focus" then
|
|
local focusUnitId = CleveRoids.GetFocusUnitId()
|
|
if focusUnitId then
|
|
conditionals.target = focusUnitId
|
|
else
|
|
if not CleveRoids.GetFocusName() then
|
|
return
|
|
end
|
|
conditionals.target = "target"
|
|
end
|
|
elseif conditionals.target == "focustarget" then
|
|
local focusUnitId = CleveRoids.GetFocusUnitId()
|
|
if focusUnitId then
|
|
conditionals.target = focusUnitId .. "target"
|
|
else
|
|
if not CleveRoids.GetFocusName() then
|
|
return
|
|
end
|
|
conditionals.target = "targettarget"
|
|
end
|
|
end
|
|
|
|
if conditionals.target == "mouseover" then
|
|
if not CleveRoids.IsValidTarget("mouseover", conditionals.help) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Resolve named raid marks (skull/cross/etc.) and mark1-mark8 to native mark# unit tokens
|
|
if conditionals.target then
|
|
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(conditionals.target)
|
|
if resolvedMark then conditionals.target = resolvedMark end
|
|
end
|
|
|
|
CleveRoids.FixEmptyTarget(conditionals)
|
|
|
|
-- Flag for tooltip evaluation. CountEnemiesMatching skips expensive
|
|
-- nameplate scan and known-enemy cache, using only target + unit tokens.
|
|
CleveRoids._isTestingAction = true
|
|
|
|
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
|
local k, v = next(conditionals)
|
|
while k do
|
|
if not CleveRoids.ignoreKeywords[k] then
|
|
if not CleveRoids.Keywords[k] or not CleveRoids.Keywords[k](conditionals) then
|
|
CleveRoids._isTestingAction = false
|
|
conditionals.target = origTarget
|
|
return
|
|
end
|
|
end
|
|
k, v = next(conditionals, k)
|
|
end
|
|
|
|
CleveRoids._isTestingAction = false
|
|
conditionals.target = origTarget
|
|
return CleveRoids.GetMacroNameFromAction(msg) or msg
|
|
end
|
|
|
|
function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBeforeAction, action)
|
|
-- Ensure TestAction flag is cleared for actual execution (safety net for error recovery)
|
|
CleveRoids._isTestingAction = false
|
|
|
|
-- Check macro stop flags (skip non-control commands when flag is set)
|
|
-- This enables /stopmacro, /skipmacro, /firstaction, /nofirstaction to work without SuperMacro for vanilla macros
|
|
if (CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag) and action ~= "STOPMACRO" and action ~= "SKIPMACRO" and action ~= "FIRSTACTION" and action ~= "NOFIRSTACTION" then
|
|
return false
|
|
end
|
|
|
|
local msg, conditionals = CleveRoids.GetParsedMsg(msg)
|
|
|
|
-- Debug: Log parsed msg and action type
|
|
if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then
|
|
CleveRoids.Print("|cff888888[EquipLog] DoWithConditionals: parsed msg='" .. tostring(msg) .. "' action=" .. tostring(action) .. "|r")
|
|
end
|
|
|
|
-- No conditionals. Just exit.
|
|
if not conditionals then
|
|
if not msg then -- if not even an empty string
|
|
return false
|
|
else
|
|
if string.sub(msg, 1, 1) == "{" and string.sub(msg, -1) == "}" then
|
|
if string.sub(msg, 2, 2) == "\"" and string.sub(msg, -2, -2) == "\"" then
|
|
return CleveRoids.ExecuteMacroBody(string.sub(msg, 3, -3), true)
|
|
else
|
|
return CleveRoids.ExecuteMacroByName(string.sub(msg, 2, -2))
|
|
end
|
|
end
|
|
|
|
-- Handle STOPMACRO/SKIPMACRO/FIRSTACTION/NOFIRSTACTION without conditionals (bare command)
|
|
if action == "STOPMACRO" then
|
|
CleveRoids.stopMacroFlag = true
|
|
return true
|
|
elseif action == "SKIPMACRO" then
|
|
CleveRoids.skipMacroFlag = true
|
|
return true
|
|
elseif action == "FIRSTACTION" then
|
|
CleveRoids.stopOnCastFlag = true
|
|
return true
|
|
elseif action == "NOFIRSTACTION" then
|
|
-- Clear flags - same logic as DoNoFirstAction
|
|
local wasFirstActionActive = CleveRoids.stopOnCastFlag
|
|
CleveRoids.stopOnCastFlag = false
|
|
if wasFirstActionActive and CleveRoids.stopMacroFlag then
|
|
CleveRoids.stopMacroFlag = false
|
|
end
|
|
return true
|
|
end
|
|
|
|
if hook then
|
|
hook(msg)
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
if conditionals.cancelaura then
|
|
if CleveRoids.CancelAura(conditionals.cancelaura) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
local origTarget = conditionals.target
|
|
if conditionals.target == "mouseover" then
|
|
if UnitExists("mouseover") then
|
|
conditionals.target = "mouseover"
|
|
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
|
|
conditionals.target = CleveRoids.mouseoverUnit
|
|
else
|
|
conditionals.target = "mouseover"
|
|
end
|
|
end
|
|
|
|
local needRetarget = false
|
|
if fixEmptyTargetFunc then
|
|
needRetarget = fixEmptyTargetFunc(conditionals, msg, hook)
|
|
end
|
|
|
|
-- CleveRoids.SetHelp(conditionals)
|
|
|
|
if conditionals.target == "focus" or conditionals.target == "focustarget" then
|
|
local isFocusTarget = conditionals.target == "focustarget"
|
|
local focusUnitId = CleveRoids.GetFocusUnitId()
|
|
|
|
if focusUnitId then
|
|
-- Use the resolved pfUI unit token directly (avoids changing the player's target)
|
|
conditionals.target = focusUnitId .. (isFocusTarget and "target" or "")
|
|
needRetarget = false
|
|
else
|
|
-- return false if pfUI is installed and no focus is set instead of "invalid target"
|
|
if pfUI and pfUI.uf and (not pfUI.uf.focus or pfUI.uf.focus.label == nil or pfUI.uf.focus.label == "") then return false end
|
|
-- Fall back to targeting focus by name
|
|
if not CleveRoids.TryTargetFocus() then
|
|
UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1.0, 0.0, 0.0, 1.0)
|
|
conditionals.target = origTarget
|
|
return false
|
|
end
|
|
conditionals.target = isFocusTarget and "targettarget" or "target"
|
|
needRetarget = true
|
|
end
|
|
end
|
|
|
|
-- Resolve named raid marks (skull/cross/etc.) and mark1-mark8 to native mark# unit tokens
|
|
if conditionals.target then
|
|
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(conditionals.target)
|
|
if resolvedMark then conditionals.target = resolvedMark end
|
|
end
|
|
|
|
-- Handle [multiscan:priority] - scan enemies and find best target
|
|
-- Must be processed BEFORE the Keywords loop since it sets conditionals.target
|
|
-- Pass origTarget so @unit syntax makes that unit exempt from combat check
|
|
if conditionals.multiscan then
|
|
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, origTarget)
|
|
if not scanResult then
|
|
-- No valid target found - fail this conditional line (try next)
|
|
conditionals.target = origTarget
|
|
return false
|
|
end
|
|
-- Set target to the found GUID for soft-casting via SuperWoW or Nampower v2.37+
|
|
conditionals.target = scanResult
|
|
-- Don't need to retarget since we're using GUID directly
|
|
needRetarget = false
|
|
end
|
|
|
|
for k, v in pairs(conditionals) do
|
|
if not CleveRoids.ignoreKeywords[k] 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
|
|
-- Debug logging for macro reference debug
|
|
if CleveRoids.macroRefDebug then
|
|
local valStr = (v == true) and "" or (type(v) == "table" and table.concat(v, ", ") or tostring(v))
|
|
CleveRoids.Print("|cff888888[MacroRef]|r [" .. k .. (valStr ~= "" and (":" .. valStr) or "") .. "] = " ..
|
|
(result and "|cff00ff00PASS|r" or "|cffff0000FAIL|r"))
|
|
end
|
|
if not result then
|
|
if needRetarget then
|
|
TargetLastTarget()
|
|
needRetarget = false
|
|
end
|
|
conditionals.target = origTarget
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
if conditionals.target ~= nil and targetBeforeAction and not (action == CastSpellByName and (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken)) then
|
|
if not UnitIsUnit("target", conditionals.target) then
|
|
if SpellIsTargeting() then
|
|
SpellStopCasting()
|
|
end
|
|
TargetUnit(conditionals.target)
|
|
needRetarget = true
|
|
else
|
|
if needRetarget then needRetarget = false end
|
|
end
|
|
elseif needRetarget then
|
|
TargetLastTarget()
|
|
needRetarget = false
|
|
end
|
|
|
|
if action == "STOPMACRO" then
|
|
-- Set flag to stop subsequent lines in current macro AND parent macros
|
|
CleveRoids.stopMacroFlag = true
|
|
return true
|
|
elseif action == "SKIPMACRO" then
|
|
-- Set flag to skip remaining lines in current submacro only
|
|
CleveRoids.skipMacroFlag = true
|
|
return true
|
|
elseif action == "FIRSTACTION" then
|
|
-- Set flag to stop on first successful cast/use
|
|
CleveRoids.stopOnCastFlag = true
|
|
return true
|
|
elseif action == "NOFIRSTACTION" then
|
|
-- Clear flags to re-enable multi-queue behavior
|
|
-- Also clear stopMacroFlag if it was set by the firstaction mechanism
|
|
local wasFirstActionActive = CleveRoids.stopOnCastFlag
|
|
CleveRoids.stopOnCastFlag = false
|
|
if wasFirstActionActive and CleveRoids.stopMacroFlag then
|
|
CleveRoids.stopMacroFlag = false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local result = true
|
|
if string.sub(msg, 1, 1) == "{" and string.sub(msg, -1) == "}" then
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff00ffff[MacroRef]|r Detected macro reference: " .. msg)
|
|
end
|
|
if string.sub(msg, 2, 2) == "\"" and string.sub(msg, -2,-2) == "\"" then
|
|
result = CleveRoids.ExecuteMacroBody(string.sub(msg, 3, -3), true)
|
|
else
|
|
result = CleveRoids.ExecuteMacroByName(string.sub(msg, 2, -2))
|
|
end
|
|
elseif msg == "" or msg == nil then
|
|
-- Empty action (conditionals passed but no spell to cast)
|
|
-- For non-spell actions (pet commands, etc.), still execute the action
|
|
if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then
|
|
CleveRoids.Print("|cffff8800[EquipLog] Empty msg branch - calling action() with no args|r")
|
|
end
|
|
if action and action ~= CastSpellByName then
|
|
action()
|
|
result = true
|
|
else
|
|
result = false
|
|
end
|
|
else
|
|
local castMsg = msg
|
|
-- FLEXIBLY check for any rank text like "(Rank 9)" before adding the highest rank
|
|
-- Use specific "(Rank" check instead of any parentheses, so spells like
|
|
-- "Faerie Fire (Feral)" still get their rank appended automatically
|
|
if action == CastSpellByName and not string.find(msg, "%(Rank") then
|
|
local sp = CleveRoids.GetSpell(msg)
|
|
local rank = sp and (sp.rank or (sp.highest and sp.highest.rank))
|
|
if rank and rank ~= "" then
|
|
castMsg = msg .. "(" .. rank .. ")"
|
|
end
|
|
end
|
|
if conditionals.cursor then
|
|
-- [cursor]: place a ground-target spell/item at the cursor via
|
|
-- ClassicAPI (no AoE-reticle click). Both functions fall back to a
|
|
-- normal cast/use for non-ground actions.
|
|
if action == CastSpellByName then
|
|
local sp = CleveRoids.GetSpell(msg)
|
|
local spellId = sp and sp.id
|
|
if not spellId and GetSpellIdForName then
|
|
spellId = GetSpellIdForName(castMsg)
|
|
end
|
|
if spellId then
|
|
C_Spell.CastAtCursor(spellId)
|
|
else
|
|
CastSpellByName(castMsg)
|
|
end
|
|
else
|
|
C_Item.UseAtCursor(msg)
|
|
end
|
|
elseif action == CastSpellByName then
|
|
if msg == CleveRoids.Localized.Attack and (conditionals.noSpam or conditionals.checkchanneled) then
|
|
-- Melee Attack toggle: use the non-toggling AttackTarget (CastSpellNoToggle
|
|
-- covers auto-repeat + self-auras, not the melee swing).
|
|
AttackTarget()
|
|
elseif conditionals.noSpam then
|
|
-- ! prefix anti-spam. CastSpellNoToggle handles engine toggles
|
|
-- (auto-repeat Shoot/Auto Shot/Wand, forms/stances/aspects/seals):
|
|
-- it no-ops when already active instead of toggling off. It only
|
|
-- covers true toggle auras, though, so also skip re-applying an
|
|
-- already-active regular self-buff (e.g. Mark of the Wild, Renew).
|
|
if CleveRoids.ValidatePlayerBuff(msg) then
|
|
result = false
|
|
else
|
|
CastSpellNoToggle(castMsg)
|
|
end
|
|
elseif (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken) and conditionals.target and origTarget then
|
|
CastSpellByName(castMsg, conditionals.target)
|
|
else
|
|
CastSpellByName(castMsg)
|
|
end
|
|
else
|
|
-- For other actions like UseContainerItem etc.
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff00ff00[EquipLog] Calling action('" .. tostring(msg) .. "')|r")
|
|
end
|
|
action(msg)
|
|
end
|
|
end
|
|
|
|
-- [mouseuse] modifier: auto-click the AOE targeting circle at cursor position
|
|
if conditionals.mouseuse and SpellIsTargeting() then
|
|
local wasAttacking = CleveRoids.CurrentSpell.autoAttack
|
|
CameraOrSelectOrMoveStart()
|
|
CameraOrSelectOrMoveStop()
|
|
-- CameraOrSelectOrMoveStart can start auto-attack as a side effect.
|
|
-- Stop it immediately if it wasn't active before.
|
|
if not wasAttacking then
|
|
local slot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
|
|
if slot and CleveRoids.Hooks.IsCurrentAction(slot) then
|
|
AttackTarget()
|
|
CleveRoids.CurrentSpell.autoAttack = false
|
|
end
|
|
end
|
|
end
|
|
|
|
if needRetarget then
|
|
TargetLastTarget()
|
|
end
|
|
|
|
-- [stopattack]: Deferred to OnUpdate (next frame) using CheapShot pattern.
|
|
-- AttackTarget()+ClearTarget() on first frame, enforce clear for 0.2s,
|
|
-- then retarget via stored GUID.
|
|
if conditionals.stopattack then
|
|
CleveRoids.DeferStopAttack()
|
|
end
|
|
|
|
conditionals.target = origTarget
|
|
return result
|
|
end
|
|
|
|
function CleveRoids.DoCast(msg)
|
|
if CleveRoids.ChannelTimeDebug then
|
|
if msg and string.find(msg, "Arcane") then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[/cast BUTTON PRESS]|r " .. msg)
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = parts[i]
|
|
if CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.CAST_SlashCmd, CleveRoids.FixEmptyTarget, false, CastSpellByName) then
|
|
-- If /firstaction was used, stop macro evaluation after first successful cast
|
|
if CleveRoids.stopOnCastFlag then
|
|
CleveRoids.stopMacroFlag = true
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Resolve unit for /pfcast: mouseover → GetMouseFocus label+id → mouseoverUnit → target → player
|
|
-- Mirrors pfUI's own unit resolution order so /pfcast conditionals evaluate against the same unit
|
|
-- that pfUI would cast on.
|
|
local function ResolvePfCastUnit()
|
|
if UnitExists("mouseover") then
|
|
return "mouseover"
|
|
end
|
|
local frame = GetMouseFocus and GetMouseFocus()
|
|
if frame and frame.label and frame.id then
|
|
return frame.label .. frame.id
|
|
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
|
|
return CleveRoids.mouseoverUnit
|
|
elseif UnitExists("target") then
|
|
return "target"
|
|
elseif GetCVar and GetCVar("autoSelfCast") == "1" then
|
|
return "player"
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- /pfcast with CleveRoids conditionals: evaluate conditionals then cast via pfUI's mouseover chain.
|
|
-- Called from the SlashCmdList.PFCAST hook (set up by Extensions/Mouseover/pfUI.lua after pfUI loads).
|
|
function CleveRoids.DoPfCast(msg)
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = parts[i]
|
|
-- If conditionals are present but no explicit @unit, inject the pfUI-resolved unit so
|
|
-- all conditionals ([help], [nodebuff:X], etc.) evaluate against the same unit pfUI
|
|
-- would cast on, and the final CastSpellByName gets the correct unit token.
|
|
if string.find(v, "%[") and not string.find(v, "@") then
|
|
local unit = ResolvePfCastUnit()
|
|
if unit then
|
|
v = string.gsub(v, "%[", "[@" .. unit .. ",", 1)
|
|
end
|
|
end
|
|
if CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.PFCAST_SlashCmd, CleveRoids.FixEmptyTarget, false, CastSpellByName) then
|
|
if CleveRoids.stopOnCastFlag then
|
|
CleveRoids.stopMacroFlag = true
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- PERFORMANCE: Module-level pet cast action to avoid closure allocation per call
|
|
local function _petCastAction(spellName)
|
|
local petSpell = CleveRoids.GetPetSpell(spellName)
|
|
if petSpell and petSpell.slot then
|
|
CastPetAction(petSpell.slot)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoCastPet(msg)
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = parts[i]
|
|
if CleveRoids.DoWithConditionals(v, _petCastAction, CleveRoids.FixEmptyTarget, false, _petCastAction) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoTarget(msg)
|
|
-- Check macro stop flags
|
|
if CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag then
|
|
return false
|
|
end
|
|
|
|
local action, conditionals = CleveRoids.GetParsedMsg(msg)
|
|
|
|
if action ~= "" or type(conditionals) ~= "table" or not next(conditionals) then
|
|
CleveRoids.Hooks.TARGET_SlashCmd(msg)
|
|
return true
|
|
end
|
|
|
|
local function IsGuidValid(unitTok, conds)
|
|
if not unitTok or not UnitExists(unitTok) or CleveRoids.IsUnitDeadOrGhost(unitTok) then
|
|
return false
|
|
end
|
|
local orig = conds.target
|
|
conds.target = unitTok
|
|
local ok = true
|
|
for k, _ in pairs(conds) do
|
|
if not CleveRoids.ignoreKeywords[k] then
|
|
local fn = CleveRoids.Keywords[k]
|
|
if not fn or not fn(conds) then ok = false; break end
|
|
end
|
|
end
|
|
conds.target = orig
|
|
return ok
|
|
end
|
|
|
|
-- Save original target GUID for potential restoration (SuperWoW returns GUID as 2nd value)
|
|
local originalTargetGuid = CleveRoids.GetGUID("target")
|
|
|
|
-- Handle [multiscan:priority] - use ResolveMultiscanTarget for enemy scanning
|
|
-- ResolveMultiscanTarget handles its own target save/restore internally
|
|
if conditionals.multiscan then
|
|
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, conditionals.target)
|
|
if scanResult then
|
|
TargetUnit(scanResult)
|
|
return true
|
|
end
|
|
-- No match found - restore original target if scanning changed it
|
|
if originalTargetGuid and UnitExists(originalTargetGuid) then
|
|
TargetUnit(originalTargetGuid)
|
|
elseif not originalTargetGuid then
|
|
ClearTarget()
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Track if an explicit target was specified via @unit syntax
|
|
local explicitTarget = conditionals.target
|
|
|
|
do
|
|
local unitTok = conditionals.target
|
|
|
|
if unitTok == "mouseover" then
|
|
if UnitExists("mouseover") then
|
|
unitTok = "mouseover"
|
|
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
|
|
unitTok = CleveRoids.mouseoverUnit
|
|
elseif pfUI and pfUI.uf and pfUI.uf.mouseover and pfUI.uf.mouseover.unit
|
|
and UnitExists(pfUI.uf.mouseover.unit) then
|
|
unitTok = pfUI.uf.mouseover.unit
|
|
else
|
|
unitTok = nil
|
|
end
|
|
end
|
|
|
|
if unitTok == "focus" or unitTok == "focustarget" then
|
|
local isFocusTarget = unitTok == "focustarget"
|
|
local fTok = CleveRoids.GetFocusUnitId()
|
|
if fTok then
|
|
unitTok = fTok .. (isFocusTarget and "target" or "")
|
|
if not UnitExists(unitTok) then unitTok = nil end
|
|
else
|
|
unitTok = nil
|
|
end
|
|
end
|
|
|
|
-- Resolve named raid marks (skull/cross/etc.) and mark1-mark8 to native mark# unit tokens
|
|
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(unitTok)
|
|
if resolvedMark then unitTok = resolvedMark end
|
|
|
|
if unitTok and UnitExists(unitTok) and IsGuidValid(unitTok, conditionals) then
|
|
TargetUnit(unitTok)
|
|
return true
|
|
end
|
|
|
|
-- If an explicit target was specified via @unit syntax but doesn't exist or isn't valid,
|
|
-- return false instead of falling through to candidate search.
|
|
-- This ensures "/target [@mouseover,harm,alive]" does nothing when mouse is over empty ground.
|
|
if explicitTarget then
|
|
return false
|
|
end
|
|
end
|
|
|
|
if UnitExists("target") and IsGuidValid("target", conditionals) then
|
|
return true
|
|
end
|
|
|
|
local candidates = {}
|
|
local wantsHelp = conditionals.help
|
|
local wantsHarm = conditionals.harm
|
|
|
|
local function addCandidate(unitId)
|
|
if not UnitExists(unitId) then return end
|
|
|
|
if wantsHelp and not UnitCanAssist("player", unitId) then return end
|
|
|
|
if wantsHarm and not UnitCanAttack("player", unitId) then return end
|
|
|
|
table.insert(candidates, { unitId = unitId })
|
|
end
|
|
|
|
addCandidate("mouseover")
|
|
|
|
addCandidate("target")
|
|
|
|
if not wantsHarm then
|
|
table.insert(candidates, { unitId = "player" })
|
|
end
|
|
|
|
addCandidate("pet")
|
|
|
|
for i = 1, 4 do
|
|
addCandidate("party"..i)
|
|
addCandidate("partypet"..i)
|
|
addCandidate("party"..i.."target")
|
|
end
|
|
|
|
for i = 1, 40 do
|
|
addCandidate("raid"..i)
|
|
addCandidate("raidpet"..i)
|
|
addCandidate("raid"..i.."target")
|
|
end
|
|
|
|
addCandidate("targettarget")
|
|
addCandidate("targettargettarget")
|
|
|
|
addCandidate("pettarget")
|
|
|
|
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
|
|
local focusTok = pfUI.uf.focus.label .. pfUI.uf.focus.id
|
|
addCandidate(focusTok)
|
|
addCandidate(focusTok .. "target")
|
|
end
|
|
|
|
if CleveRoids.hasSuperwow then
|
|
local numChildren = WorldFrame:GetNumChildren()
|
|
local children = { WorldFrame:GetChildren() }
|
|
|
|
for i = 1, numChildren do
|
|
local frame = children[i]
|
|
if frame and frame:IsVisible() then
|
|
local success, guid = pcall(frame.GetName, frame, 1)
|
|
if success and guid and type(guid) == "string" and string.len(guid) > 0 then
|
|
if UnitExists(guid) then
|
|
addCandidate(guid)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, c in ipairs(candidates) do
|
|
if IsGuidValid(c.unitId, conditionals) then
|
|
TargetUnit(c.unitId)
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- UnitXP 3D enemy scanning: cycles through enemies in world space (no nameplate required)
|
|
-- This is the most powerful scan - finds enemies by line of sight and distance
|
|
-- Always enabled unless explicitly looking for friendlies only ([help] without [harm])
|
|
-- UnitXP only finds enemies, so this is safe for any /target with conditionals
|
|
local wantsFriendlyOnly = wantsHelp and not wantsHarm
|
|
if not wantsFriendlyOnly and CleveRoids.hasUnitXP then
|
|
-- Try nearestEnemy first - most common case and most efficient
|
|
local found = UnitXP("target", "nearestEnemy")
|
|
if found and UnitExists("target") and IsGuidValid("target", conditionals) then
|
|
return true
|
|
end
|
|
|
|
-- Determine scan mode: use distance-priority for melee conditionals
|
|
local wantsMelee = conditionals.meleerange or conditionals.nomeleerange
|
|
local scanMode = wantsMelee and "nextEnemyConsideringDistance" or "nextEnemyInCycle"
|
|
|
|
local seenGuids = {}
|
|
local firstGuid = nil
|
|
local maxIterations = 50 -- Safety limit
|
|
|
|
for i = 1, maxIterations do
|
|
found = UnitXP("target", scanMode)
|
|
if not found then break end
|
|
|
|
local currentGuid = CleveRoids.GetGUID("target")
|
|
if not currentGuid then break end
|
|
|
|
-- Check if we've cycled back to start
|
|
if firstGuid == nil then
|
|
firstGuid = currentGuid
|
|
elseif currentGuid == firstGuid then
|
|
break -- Completed full cycle
|
|
end
|
|
|
|
-- Skip already-seen targets
|
|
if not seenGuids[currentGuid] then
|
|
seenGuids[currentGuid] = true
|
|
|
|
-- Test this target against all conditionals
|
|
if IsGuidValid("target", conditionals) then
|
|
return true -- Found matching target
|
|
end
|
|
end
|
|
end
|
|
|
|
-- No match found via UnitXP scan - restore original target if we had one
|
|
if originalTargetGuid and UnitExists(originalTargetGuid) then
|
|
TargetUnit(originalTargetGuid)
|
|
elseif not originalTargetGuid then
|
|
ClearTarget()
|
|
end
|
|
end
|
|
|
|
-- TargetNearestEnemy() fallback: cycles through enemies via tab-targeting
|
|
-- Finds enemy players that UnitXP may miss (e.g. [class:Rogue harm])
|
|
if not wantsFriendlyOnly then
|
|
TargetNearestEnemy()
|
|
if UnitExists("target") then
|
|
local firstGuid2 = CleveRoids.GetGUID("target")
|
|
if firstGuid2 then
|
|
if IsGuidValid("target", conditionals) then
|
|
return true
|
|
end
|
|
|
|
for i = 1, 50 do
|
|
TargetNearestEnemy()
|
|
if not UnitExists("target") then break end
|
|
|
|
local guid = CleveRoids.GetGUID("target")
|
|
if not guid or guid == firstGuid2 then break end
|
|
|
|
if IsGuidValid("target", conditionals) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- No match found - restore original target
|
|
if originalTargetGuid and UnitExists(originalTargetGuid) then
|
|
TargetUnit(originalTargetGuid)
|
|
elseif not originalTargetGuid then
|
|
ClearTarget()
|
|
end
|
|
end
|
|
|
|
-- No matching target found after all scans - restore and fail
|
|
if originalTargetGuid and UnitExists(originalTargetGuid) then
|
|
TargetUnit(originalTargetGuid)
|
|
elseif not originalTargetGuid then
|
|
ClearTarget()
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Attempts to attack a unit by a set of conditionals
|
|
-- msg: The raw message intercepted from a /petattack command
|
|
function CleveRoids.DoPetAction(action, msg)
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], action, CleveRoids.FixEmptyTarget, true, action) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- PERFORMANCE: Module-level action to avoid closure allocation per call
|
|
local function _startAttackAction()
|
|
if not UnitExists("target") or CleveRoids.IsUnitDead("target") then TargetNearestEnemy() end
|
|
-- Ground truth is the real action-bar state, NOT the cached autoAttack flag.
|
|
-- The flag can drift stale-true (e.g. set optimistically after AttackTarget below,
|
|
-- or a target dying without PLAYER_LEAVE_COMBAT firing), which would make us wrongly
|
|
-- believe we're already swinging and skip the attack. Use the ORIGINAL API here:
|
|
-- the overridden global IsCurrentAction just echoes the cached flag for the attack
|
|
-- slot, so it can't detect drift. Fall back to the flag only if the slot is unknown.
|
|
local isAttacking = CleveRoids.CurrentSpell.autoAttack
|
|
local slot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
|
|
if slot then
|
|
isAttacking = CleveRoids.Hooks.IsCurrentAction(slot) and true or false
|
|
CleveRoids.CurrentSpell.autoAttack = isAttacking
|
|
end
|
|
if not isAttacking and not CleveRoids.CurrentSpell.autoAttackLock and UnitExists("target") and UnitCanAttack("player", "target") then
|
|
CleveRoids.CurrentSpell.autoAttackLock = true
|
|
CleveRoids.autoAttackLockElapsed = GetTime()
|
|
AttackTarget()
|
|
-- FIX: Immediately set autoAttack flag so subsequent macro lines know attack started
|
|
-- Don't wait for PLAYER_ENTER_COMBAT event which has a delay
|
|
CleveRoids.CurrentSpell.autoAttack = true
|
|
-- FIX: Queue icon update so action bars reflect the new state
|
|
if CleveRoidMacros and CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Attempts to conditionally start an attack. Returns false if no conditionals are found.
|
|
function CleveRoids.DoConditionalStartAttack(msg)
|
|
if not string.find(msg, "%[") then return false end
|
|
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
-- We pass 'nil' for the hook, so DoWithConditionals does nothing if it fails to parse conditionals.
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _startAttackAction) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- PERFORMANCE: Module-level actions to avoid closure allocation per call
|
|
local function _stopAttackAction()
|
|
CleveRoids.DeferStopAttack()
|
|
end
|
|
|
|
local function _stopCastingAction()
|
|
SpellStopCasting()
|
|
end
|
|
|
|
-- Attempts to conditionally stop an attack. Returns false if no conditionals are found.
|
|
function CleveRoids.DoConditionalStopAttack(msg)
|
|
if not string.find(msg, "%[") then return false end
|
|
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _stopAttackAction) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Attempts to conditionally stop casting. Returns false if no conditionals are found.
|
|
function CleveRoids.DoConditionalStopCasting(msg)
|
|
if not string.find(msg, "%[") then return false end
|
|
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _stopCastingAction) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function _clearTargetAction()
|
|
ClearTarget()
|
|
end
|
|
|
|
-- Attempts to conditionally clear target. Returns false if no conditionals are found.
|
|
function CleveRoids.DoConditionalClearTarget(msg)
|
|
if not string.find(msg, "%[") then return false end
|
|
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _clearTargetAction) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Attempts to use or equip an item by a set of conditionals
|
|
-- Also checks if a condition is a spell so that you can mix item and spell use
|
|
-- msg: The raw message intercepted from a /use or /equip command
|
|
function CleveRoids.DoUse(msg)
|
|
-- Check macro stop flags
|
|
if CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag then
|
|
return false
|
|
end
|
|
|
|
local handled = false
|
|
|
|
local action = function(msg)
|
|
-- Defensive: make sure we are not in "split stack" mode and nothing is on the cursor
|
|
if type(CloseStackSplitFrame) == "function" then CloseStackSplitFrame() end
|
|
if CursorHasItem and CursorHasItem() then ClearCursor() end
|
|
|
|
-- Try to interpret the message as a direct inventory slot ID first.
|
|
local slotId = tonumber(msg)
|
|
if slotId and slotId >= 1 and slotId <= 19 then -- Character slots are 1-19
|
|
ClearCursor() -- extra safety before using equipped items
|
|
UseInventoryItem(slotId)
|
|
return
|
|
end
|
|
|
|
-- Try to interpret as item ID (numbers > 19)
|
|
-- v2.18+: Use FindPlayerItemSlot directly for item IDs (no name resolution needed)
|
|
if slotId and slotId > 19 then
|
|
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
|
|
-- Fall back to GetItemInfo
|
|
if not itemName and GetItemInfo then
|
|
itemName = GetItemInfo(slotId)
|
|
end
|
|
if itemName then
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[UseLog] Resolved item ID " .. slotId .. " to '" .. itemName .. "'|r")
|
|
end
|
|
msg = itemName -- Replace ID with name for subsequent lookups
|
|
else
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff8800[UseLog] Could not resolve item ID " .. slotId .. " - item not in cache|r")
|
|
end
|
|
-- Item not in client cache - can't resolve without seeing it first
|
|
return
|
|
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)
|
|
local location = CleveRoids.FindItemLocation(msg)
|
|
if location then
|
|
local cacheValid = false
|
|
local qname = string_lower(msg)
|
|
|
|
if location.type == "inventory" then
|
|
-- Validate: check if this slot actually contains the item we want
|
|
local link = GetInventoryItemLink("player", location.inventoryID)
|
|
if link then
|
|
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
|
if nm and string_lower(nm) == qname then
|
|
cacheValid = true
|
|
end
|
|
end
|
|
else
|
|
-- Validate: check if this bag slot actually contains the item we want
|
|
local link = GetContainerItemLink(location.bag, location.slot)
|
|
if link then
|
|
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
|
if nm and string_lower(nm) == qname then
|
|
cacheValid = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if cacheValid then
|
|
ClearCursor()
|
|
if location.type == "inventory" then
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. location.inventoryID .. ") [cached]|r")
|
|
end
|
|
UseInventoryItem(location.inventoryID)
|
|
else
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. location.bag .. "," .. location.slot .. ") [cached]|r")
|
|
end
|
|
UseContainerItem(location.bag, location.slot)
|
|
end
|
|
return
|
|
elseif CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " - cache STALE, falling back to scan|r")
|
|
end
|
|
end
|
|
|
|
-- Slow path fallback: full scan for substring matches or cache miss
|
|
local qname = string_lower(msg)
|
|
|
|
-- Search equipped inventory slots first (for trinkets, etc.)
|
|
for slot = 0, 19 do
|
|
local link = GetInventoryItemLink("player", slot)
|
|
if link then
|
|
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
|
if nm and string_lower(nm) == qname then
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. slot .. ")|r")
|
|
end
|
|
ClearCursor()
|
|
UseInventoryItem(slot)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Then search bags
|
|
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 _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
|
if nm and string_lower(nm) == qname then
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. bag .. "," .. bagSlot .. ")|r")
|
|
end
|
|
ClearCursor()
|
|
UseContainerItem(bag, bagSlot)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " - not found in equipped slots or bags|r")
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], action, CleveRoids.FixEmptyTarget, false, action) then
|
|
-- If /firstaction was used, stop macro evaluation after first successful use
|
|
if CleveRoids.stopOnCastFlag then
|
|
CleveRoids.stopMacroFlag = true
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Find an item in bags by name (ignoring equipped items)
|
|
-- Used to prefer bag copies over swapping from paired equipped slots
|
|
-- (e.g., dual-wielding the same weapon with /equipmh + /equipoh)
|
|
local function FindItemInBagsByName(itemName)
|
|
if not itemName then return nil, nil end
|
|
local lowerName = string_lower(itemName)
|
|
for bag = 0, 4 do
|
|
local numSlots = GetContainerNumSlots(bag) or 0
|
|
for slot = 1, numSlots do
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if link then
|
|
local _, _, name = string_find(link, "|h%[(.-)%]|h")
|
|
if name and string_lower(name) == lowerName then
|
|
return bag, slot
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
function CleveRoids.EquipBagItem(msg, slotOrOffhand)
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' slot=" .. tostring(slotOrOffhand) .. "|r")
|
|
end
|
|
|
|
if CleveRoids.equipInProgress then
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff0000[EquipLog] Equip already in progress, skipping|r")
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Accept slot number directly, or boolean for backward compatibility (false=16/MH, true=17/OH)
|
|
local invslot
|
|
if type(slotOrOffhand) == "number" then
|
|
invslot = slotOrOffhand
|
|
else
|
|
invslot = slotOrOffhand and 17 or 16
|
|
end
|
|
local API = CleveRoids.NampowerAPI
|
|
|
|
-- v2.18+: Use native fast lookup for item ID or name
|
|
-- Guard against API being a function instead of table (addon conflict protection)
|
|
if type(API) == "table" and type(API.features) == "table" and API.features.hasFindPlayerItemSlot then
|
|
local searchTerm = msg
|
|
local itemId = tonumber(msg)
|
|
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff888888[EquipLog] Searching for '" .. tostring(searchTerm) .. "' (slot " .. invslot .. ")|r")
|
|
end
|
|
|
|
-- Check if already equipped in target slot
|
|
if API.IsItemInSlot(searchTerm, invslot) then
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff00ff00[EquipLog] '" .. tostring(searchTerm) .. "' already in slot " .. invslot .. "|r")
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Find the item (works with both ID and name)
|
|
local itemInfo = API.FindItemFast(searchTerm)
|
|
if CleveRoids.equipDebugLog then
|
|
if itemInfo then
|
|
CleveRoids.Print("|cff888888[EquipLog] FindItemFast found: invID=" .. tostring(itemInfo.inventoryID) .. " bag=" .. tostring(itemInfo.bagID) .. " slot=" .. tostring(itemInfo.slot) .. "|r")
|
|
else
|
|
CleveRoids.Print("|cffff8800[EquipLog] FindItemFast returned nil|r")
|
|
end
|
|
end
|
|
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
|
|
-- Before swapping from another equipped slot, check bags for another copy
|
|
-- This handles dual-wielding the same weapon (e.g., /equipmh Scimitar + /equipoh Scimitar)
|
|
local bagCopyBag, bagCopySlot = FindItemInBagsByName(itemInfo.name or searchTerm)
|
|
if bagCopyBag then
|
|
ClearCursor()
|
|
PickupContainerItem(bagCopyBag, bagCopySlot)
|
|
if CursorHasItem and CursorHasItem() then
|
|
EquipCursorItem(invslot)
|
|
ClearCursor()
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff00ff00[EquipLog] Equipped bag copy of '" .. tostring(msg) .. "' from bag " .. bagCopyBag .. " slot " .. bagCopySlot .. " (preferred over swap)|r")
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
-- No bag copy found - swap from equipped slot
|
|
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()
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff00ff00[EquipLog] Equipped '" .. tostring(msg) .. "' from bag " .. itemInfo.bagID .. " slot " .. itemInfo.slot .. "|r")
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Item not found via v2.18 lookup - fall through to legacy path
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot, trying legacy fallback|r")
|
|
-- Debug: Try direct FindPlayerItemSlot call
|
|
if FindPlayerItemSlot then
|
|
local bag, slot = FindPlayerItemSlot(msg)
|
|
CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r")
|
|
end
|
|
end
|
|
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
|
|
if API and API.GetItemName then
|
|
itemName = API.GetItemName(itemId)
|
|
end
|
|
if not itemName and GetItemInfo then
|
|
itemName = GetItemInfo(itemId)
|
|
end
|
|
if itemName then
|
|
msg = itemName
|
|
else
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff8800[EquipLog] Can't resolve item ID " .. tostring(itemId) .. "|r")
|
|
end
|
|
return false -- Can't resolve item ID
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Fast check if already equipped (single function call)
|
|
if CleveRoids.IsItemEquipped and CleveRoids.IsItemEquipped(msg, invslot) then
|
|
return true
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- Check if item is already equipped in the paired slot (swap case)
|
|
-- EquipItemByName doesn't handle swapping equipped items, so we must do it manually
|
|
-- Paired slots: trinkets (13<->14), weapons (16<->17), rings (11<->12)
|
|
-- BUT: check bags first for another copy (e.g., dual-wielding same weapon)
|
|
local pairedSlots = {[13] = 14, [14] = 13, [16] = 17, [17] = 16, [11] = 12, [12] = 11}
|
|
local checkSlot = pairedSlots[invslot]
|
|
if checkSlot then
|
|
local link = GetInventoryItemLink("player", checkSlot)
|
|
if link then
|
|
local _, _, slotItemName = string_find(link, "|h%[(.-)%]|h")
|
|
if slotItemName and string_lower(slotItemName) == string_lower(msg) then
|
|
-- Item found in paired slot - but prefer a bag copy if one exists
|
|
local bagCopyBag, bagCopySlot = FindItemInBagsByName(msg)
|
|
if bagCopyBag then
|
|
-- Bag copy available - equip from bag instead of swapping
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff00ffff[EquipLog] Found bag copy, equipping from bag " .. bagCopyBag .. " slot " .. bagCopySlot .. " instead of swapping from slot " .. checkSlot .. "|r")
|
|
end
|
|
ClearCursor()
|
|
PickupContainerItem(bagCopyBag, bagCopySlot)
|
|
if CursorHasItem and CursorHasItem() then
|
|
EquipCursorItem(invslot)
|
|
ClearCursor()
|
|
if CleveRoids.Items then
|
|
CleveRoids.Items[msg] = nil
|
|
CleveRoids.Items[string_lower(msg)] = nil
|
|
end
|
|
InvalidateDisplacedItem()
|
|
return true
|
|
end
|
|
ClearCursor()
|
|
else
|
|
-- No bag copy - swap from paired slot
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cff00ffff[EquipLog] Swapping from slot " .. checkSlot .. " to slot " .. invslot .. "|r")
|
|
end
|
|
ClearCursor()
|
|
PickupInventoryItem(checkSlot)
|
|
if CursorHasItem and CursorHasItem() then
|
|
EquipCursorItem(invslot)
|
|
ClearCursor()
|
|
if CleveRoids.Items then
|
|
CleveRoids.Items[msg] = nil
|
|
CleveRoids.Items[string_lower(msg)] = nil
|
|
end
|
|
InvalidateDisplacedItem()
|
|
return true
|
|
end
|
|
ClearCursor()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Try EquipItemByName for bag items (fast path)
|
|
-- This is the fastest path - no item lookup, no cursor operations
|
|
if EquipItemByName then
|
|
local ok = pcall(EquipItemByName, msg, invslot)
|
|
if ok then
|
|
-- Verify the item actually landed in the target slot
|
|
-- EquipItemByName may silently no-op for same-named items in paired slots
|
|
-- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot)
|
|
local newLink = GetInventoryItemLink("player", invslot)
|
|
if newLink then
|
|
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
|
|
if newName and string_lower(newName) == string_lower(msg) then
|
|
if CleveRoids.Items then
|
|
CleveRoids.Items[msg] = nil
|
|
CleveRoids.Items[string_lower(msg)] = nil
|
|
end
|
|
InvalidateDisplacedItem()
|
|
return true
|
|
end
|
|
end
|
|
-- Verification failed - EquipItemByName didn't place item in target slot
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. msg .. "', " .. invslot .. ") succeeded but item not in target slot, trying fallback|r")
|
|
end
|
|
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
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found in bags or equipped|r")
|
|
end
|
|
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 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
|
|
-- Verify the item actually landed in the target slot (same guard as above)
|
|
local newLink = GetInventoryItemLink("player", invslot)
|
|
if newLink then
|
|
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
|
|
if newName and string_lower(newName) == string_lower(item.name) then
|
|
if CleveRoids.Items then
|
|
CleveRoids.Items[item.name] = nil
|
|
CleveRoids.Items[string_lower(item.name)] = nil
|
|
end
|
|
InvalidateDisplacedItem()
|
|
return true
|
|
end
|
|
end
|
|
-- Verification failed - fall through to manual cursor-based equip
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. item.name .. "', " .. invslot .. ") succeeded but item not in target slot, trying manual fallback|r")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Fallback: Manual pickup and equip
|
|
CleveRoids.equipInProgress = true
|
|
|
|
-- PERFORMANCE: Single cursor check at start
|
|
if CursorHasItem and CursorHasItem() then
|
|
ClearCursor()
|
|
end
|
|
|
|
local pickupSuccess = false
|
|
if item.bagID and item.slot then
|
|
PickupContainerItem(item.bagID, item.slot)
|
|
pickupSuccess = CursorHasItem and CursorHasItem()
|
|
elseif item.inventoryID then
|
|
PickupInventoryItem(item.inventoryID)
|
|
pickupSuccess = CursorHasItem and CursorHasItem()
|
|
end
|
|
|
|
if not pickupSuccess then
|
|
ClearCursor()
|
|
CleveRoids.equipInProgress = false
|
|
return false
|
|
end
|
|
|
|
-- Verify the cursor holds the item we meant to pick up. The bag can shift
|
|
-- between the lookup and the pickup (item consumed/moved/reorganized), in
|
|
-- which case we'd otherwise equip whatever landed on the cursor. Only abort
|
|
-- on a definitive mismatch (false); nil = can't tell -> trust CursorHasItem.
|
|
if item.id and CleveRoids.ClassicAPI.CursorHoldsItemID(item.id) == false then
|
|
if CleveRoids.equipDebugLog then
|
|
CleveRoids.Print("|cffff8800[EquipLog] Cursor holds wrong item after pickup; aborting equip|r")
|
|
end
|
|
ClearCursor()
|
|
CleveRoids.equipInProgress = false
|
|
return false
|
|
end
|
|
|
|
EquipCursorItem(invslot)
|
|
ClearCursor()
|
|
|
|
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
|
|
|
|
-- PERFORMANCE: Module-level actions to avoid closure allocation per call
|
|
local function _equipMainhandAction(msg)
|
|
return CleveRoids.EquipBagItem(msg, false)
|
|
end
|
|
|
|
local function _equipOffhandAction(msg)
|
|
return CleveRoids.EquipBagItem(msg, true)
|
|
end
|
|
|
|
local function _equipTrinket1Action(msg)
|
|
return CleveRoids.EquipBagItem(msg, 13)
|
|
end
|
|
|
|
local function _equipTrinket2Action(msg)
|
|
return CleveRoids.EquipBagItem(msg, 14)
|
|
end
|
|
|
|
local function _equipRing1Action(msg)
|
|
return CleveRoids.EquipBagItem(msg, 11)
|
|
end
|
|
|
|
local function _equipRing2Action(msg)
|
|
return CleveRoids.EquipBagItem(msg, 12)
|
|
end
|
|
|
|
local function _unshiftAction()
|
|
local currentShapeshiftIndex = CleveRoids.GetCurrentShapeshiftIndex()
|
|
if currentShapeshiftIndex ~= 0 then
|
|
CastShapeshiftForm(currentShapeshiftIndex)
|
|
end
|
|
end
|
|
|
|
function CleveRoids.DoEquipMainhand(msg)
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = string.gsub(parts[i], "^%?", "")
|
|
if CleveRoids.DoWithConditionals(v, _equipMainhandAction, CleveRoids.FixEmptyTarget, false, _equipMainhandAction) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoEquipOffhand(msg)
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = string.gsub(parts[i], "^%?", "")
|
|
if CleveRoids.DoWithConditionals(v, _equipOffhandAction, CleveRoids.FixEmptyTarget, false, _equipOffhandAction) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoEquipTrinket1(msg)
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = string.gsub(parts[i], "^%?", "")
|
|
if CleveRoids.DoWithConditionals(v, _equipTrinket1Action, CleveRoids.FixEmptyTarget, false, _equipTrinket1Action) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoEquipTrinket2(msg)
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = string.gsub(parts[i], "^%?", "")
|
|
if CleveRoids.DoWithConditionals(v, _equipTrinket2Action, CleveRoids.FixEmptyTarget, false, _equipTrinket2Action) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoEquipRing1(msg)
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = string.gsub(parts[i], "^%?", "")
|
|
if CleveRoids.DoWithConditionals(v, _equipRing1Action, CleveRoids.FixEmptyTarget, false, _equipRing1Action) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoEquipRing2(msg)
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
local v = string.gsub(parts[i], "^%?", "")
|
|
if CleveRoids.DoWithConditionals(v, _equipRing2Action, CleveRoids.FixEmptyTarget, false, _equipRing2Action) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CleveRoids.DoUnshift(msg)
|
|
local handled
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
|
for i = 1, table.getn(parts) do
|
|
handled = false
|
|
if CleveRoids.DoWithConditionals(parts[i], _unshiftAction, CleveRoids.FixEmptyTarget, false, _unshiftAction) then
|
|
handled = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if handled == nil then
|
|
_unshiftAction()
|
|
end
|
|
|
|
return handled
|
|
end
|
|
|
|
function CleveRoids.DoRetarget()
|
|
if GetUnitName("target") == nil
|
|
or UnitHealth("target") == 0
|
|
or not UnitCanAttack("player", "target")
|
|
then
|
|
ClearTarget()
|
|
TargetNearestEnemy()
|
|
end
|
|
end
|
|
|
|
-- Attempts to stop macro (stops ALL remaining lines including parent macros)
|
|
function CleveRoids.DoStopMacro(msg)
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "STOPMACRO") then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Attempts to skip remaining lines in current submacro ONLY (returns to parent)
|
|
function CleveRoids.DoSkipMacro(msg)
|
|
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "SKIPMACRO") then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Enables "stop on first successful cast" mode for the rest of the macro
|
|
-- When set, successful /cast or /use commands will set stopMacroFlag automatically
|
|
-- This allows priority-based macro evaluation: first matching cast wins
|
|
-- Example usage:
|
|
-- /firstaction
|
|
-- /cast [myrawpower:>48] Shred
|
|
-- /cast [myrawpower:>40] Claw
|
|
-- In this case, if Shred casts, Claw will NOT be queued
|
|
function CleveRoids.DoFirstAction(msg)
|
|
-- Check if there are conditionals
|
|
if msg and CleveRoids.Trim(msg) ~= "" then
|
|
-- Has conditionals - use DoWithConditionals to evaluate them
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "FIRSTACTION") then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
else
|
|
-- No conditionals - just set the flag
|
|
CleveRoids.stopOnCastFlag = true
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Disables "stop on first successful cast" mode, re-enabling multi-queue behavior
|
|
-- Use this to restore normal evaluation after /firstaction
|
|
-- Example usage:
|
|
-- /firstaction
|
|
-- /cast [myrawpower:>48] Shred -- Priority section
|
|
-- /cast [myrawpower:>40] Claw
|
|
-- /nofirstaction
|
|
-- /cast Tiger's Fury -- Can queue alongside above
|
|
-- /startattack
|
|
function CleveRoids.DoNoFirstAction(msg)
|
|
-- Check if there are conditionals
|
|
if msg and CleveRoids.Trim(msg) ~= "" then
|
|
-- Has conditionals - use DoWithConditionals to evaluate them
|
|
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
|
|
for i = 1, table.getn(parts) do
|
|
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "NOFIRSTACTION") then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
else
|
|
-- No conditionals - clear the flags
|
|
-- Capture stopOnCastFlag BEFORE clearing it to know if firstaction was active
|
|
local wasFirstActionActive = CleveRoids.stopOnCastFlag
|
|
CleveRoids.stopOnCastFlag = false
|
|
-- Also clear stopMacroFlag if it was set by the firstaction mechanism
|
|
-- (stopOnCastFlag being true means firstaction mode caused stopMacroFlag to be set)
|
|
if wasFirstActionActive and CleveRoids.stopMacroFlag then
|
|
CleveRoids.stopMacroFlag = false
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
function CleveRoids.DoCastSequence(sequence)
|
|
if type(sequence) == "string" then
|
|
sequence = CleveRoids.GetSequence(sequence)
|
|
if not sequence then return end
|
|
end
|
|
|
|
if CleveRoids.currentSequence and not CleveRoids.CheckSpellCast("player") then
|
|
CleveRoids.currentSequence = nil
|
|
elseif CleveRoids.currentSequence then
|
|
return
|
|
end
|
|
|
|
-- If sequence is complete, don't execute - let macro continue to next line
|
|
if sequence.complete then
|
|
return
|
|
end
|
|
|
|
if sequence.index > 1 and sequence.reset then
|
|
for k,_ in pairs(sequence.reset) do
|
|
if CleveRoids.kmods[k] and CleveRoids.kmods[k]() then
|
|
CleveRoids.ResetSequence(sequence)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
local active = CleveRoids.GetCurrentSequenceAction(sequence)
|
|
if not (active and active.action) then return end
|
|
|
|
-- Debug: Show sequence state
|
|
if CleveRoids.debug then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"|cff00ffff[Sequence]|r Step %d/%d: '%s'",
|
|
sequence.index, table.getn(sequence.list), active.action
|
|
))
|
|
end
|
|
|
|
-- Check for stop steps - does nothing, allows macro to fall through
|
|
-- Sequence stays on this step until a reset condition fires (target/combat/time/modifier)
|
|
-- Usage: /castsequence reset=target Sunder Armor, nil
|
|
local actionLower = string.lower(CleveRoids.Trim(active.action))
|
|
if actionLower == "nil" or actionLower == "null" or actionLower == "pass" or actionLower == "noop" then
|
|
sequence.status = 0
|
|
sequence.lastUpdate = GetTime()
|
|
if CleveRoids.debug then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit stop step, falling through")
|
|
end
|
|
-- Don't cast anything, just return - allows macro to continue to next line
|
|
return
|
|
end
|
|
|
|
sequence.status = 0
|
|
sequence.lastUpdate = GetTime()
|
|
sequence.expires = 0
|
|
|
|
-- Capture target GUID for reset=target (only resets on NEW target, not same target)
|
|
if sequence.reset and sequence.reset.target and UnitExists("target") then
|
|
local targetGuid = CleveRoids.GetGUID("target")
|
|
sequence.lastTargetGuid = targetGuid
|
|
end
|
|
|
|
local prevSeq = CleveRoids.currentSequence
|
|
CleveRoids.currentSequence = sequence
|
|
|
|
local actionText = (sequence.cond or "") .. active.action
|
|
local resolvedText, conds = CleveRoids.GetParsedMsg(actionText)
|
|
|
|
-- Check if this is a macro execution {macroname}
|
|
local macroName = CleveRoids.GetMacroNameFromAction(active.action)
|
|
if macroName then
|
|
-- Execute the macro
|
|
local success = CleveRoids.ExecuteMacroByName(macroName)
|
|
if not success then
|
|
CleveRoids.currentSequence = prevSeq
|
|
end
|
|
return
|
|
end
|
|
|
|
local function cast_by_name(msg)
|
|
msg = msg or ""
|
|
-- Check specifically for "(Rank" to allow spells like "Faerie Fire (Feral)"
|
|
-- to still get their rank appended automatically
|
|
if not string.find(msg, "%(Rank") then
|
|
local sp = CleveRoids.GetSpell(msg)
|
|
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
|
|
-- Let Nampower DLL handle queuing natively via its CastSpellByName hook
|
|
CastSpellByName(msg)
|
|
return true
|
|
end
|
|
|
|
local attempted = false
|
|
if not conds then
|
|
attempted = cast_by_name(resolvedText or active.action)
|
|
else
|
|
local final = CleveRoids.DoWithConditionals(actionText, nil, CleveRoids.FixEmptyTarget, false, CastSpellByName)
|
|
if final then
|
|
-- DoWithConditionals returns true (boolean) when it already cast the spell,
|
|
-- or a string spell name when conditionals were skipped. Only call cast_by_name
|
|
-- if we got a string back (spell wasn't cast yet).
|
|
if type(final) == "string" then
|
|
attempted = cast_by_name(final)
|
|
else
|
|
attempted = true -- Spell was already cast inside DoWithConditionals
|
|
end
|
|
end
|
|
end
|
|
|
|
if not attempted then
|
|
CleveRoids.currentSequence = prevSeq
|
|
end
|
|
end
|
|
|
|
CleveRoids.DoConditionalCancelAura = function(msg)
|
|
-- Check stopmacro flag
|
|
if CleveRoids.stopMacroFlag then return false end
|
|
|
|
local s = CleveRoids.Trim(msg or "")
|
|
if s == "" then return false end
|
|
|
|
-- No conditionals? cancel immediately.
|
|
if not string.find(s, "%[") then
|
|
return CleveRoids.CancelAura(s)
|
|
end
|
|
|
|
-- Has conditionals? Let the framework evaluate them, then run CancelAura.
|
|
return CleveRoids.DoWithConditionals(s, nil, CleveRoids.FixEmptyTarget, false, CleveRoids.CancelAura) or false
|
|
end
|
|
|
|
-- PERFORMANCE: Separate periodic cleanup timer (runs every 5 seconds instead of every frame)
|
|
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
|
|
local next = next
|
|
local IsAltKeyDown = IsAltKeyDown
|
|
local IsShiftKeyDown = IsShiftKeyDown
|
|
local IsControlKeyDown = IsControlKeyDown
|
|
|
|
-- PERFORMANCE: Module-level constants (avoid re-creation per frame)
|
|
local SEQUENCE_STALE_TIME = 300 -- 5 minutes
|
|
|
|
function CleveRoids.OnUpdate(self)
|
|
-- PERFORMANCE: Local alias for global namespace (saves ~80+ global lookups per frame)
|
|
local CR = CleveRoids
|
|
local CRM = CleveRoidMacros
|
|
|
|
-- Prevent SuperWoW API calls during shutdown (crash prevention)
|
|
if CR.isShuttingDown then return end
|
|
|
|
-- Clear macro stop flags at the start of each frame
|
|
-- This ensures /stopmacro and /skipmacro only affect commands in the same frame (same macro execution)
|
|
-- Without SuperMacro, this is necessary because we can't hook into macro line execution
|
|
if CR.stopMacroFlag then
|
|
CR.stopMacroFlag = false
|
|
end
|
|
if CR.skipMacroFlag then
|
|
CR.skipMacroFlag = false
|
|
end
|
|
if CR.stopOnCastFlag then
|
|
CR.stopOnCastFlag = false
|
|
end
|
|
|
|
-- PERFORMANCE: Single GetTime() call per frame
|
|
local time = GetTime()
|
|
|
|
-- PERFORMANCE: Early exit if not ready (before any other checks)
|
|
if not CR.ready then
|
|
-- Handle initialization timer only when not ready
|
|
if CR.initializationTimer and time >= CR.initializationTimer then
|
|
CR.initializationTimer = nil
|
|
CR.IndexItems()
|
|
|
|
-- FIX: Set ready=true BEFORE IndexActionBars so GetAction() works
|
|
-- IndexActionSlot calls GetAction() which has an early-exit if ready=false,
|
|
-- preventing TestForActiveAction from populating actions.active
|
|
CR.ready = true
|
|
|
|
-- FIX: Suppress action event handlers during initial indexing
|
|
-- IndexActionBars sends ACTIONBAR_SLOT_CHANGED for each slot which triggers
|
|
-- addon handlers (pfUI, Bongos). 120 rapid calls can cause issues.
|
|
CR._suppressActionHandlers = true
|
|
CR.IndexActionBars()
|
|
CR._suppressActionHandlers = false
|
|
|
|
CR.TestForAllActiveActions()
|
|
CR.lastUpdate = time
|
|
|
|
-- FIX: Force refresh ALL Blizzard action buttons after initialization
|
|
-- Blizzard queries IsUsableAction during login before we're ready,
|
|
-- caching the wrong state. We must force a re-query for all buttons.
|
|
-- SKIP for pfUI users - pfUI handles its own button updates and this
|
|
-- would trigger 120 rapid handler calls causing issues.
|
|
if not (pfUI and pfUI.bars) then
|
|
for slot = 1, 120 do
|
|
if CR.Actions[slot] then
|
|
CR.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- PERFORMANCE: Delayed WDB warmup after login (ensures GetItemInfo works after WDB clear)
|
|
if CR.wdbWarmupTime and time >= CR.wdbWarmupTime then
|
|
CR.wdbWarmupTime = nil
|
|
CR.DoWDBWarmup()
|
|
end
|
|
|
|
-- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division)
|
|
local refreshRate = CR.cachedRefreshRate
|
|
if not refreshRate then
|
|
refreshRate = 1 / (CRM.refresh or 5)
|
|
CR.cachedRefreshRate = refreshRate
|
|
end
|
|
|
|
-- PERFORMANCE: Throttle check FIRST - skip most work on non-throttled frames
|
|
local lastUpdate = CR.lastUpdate or 0
|
|
local bypassThrottle = CR.isActionUpdateQueued and CRM.realtime == 0
|
|
local shouldUpdate = bypassThrottle or (time - lastUpdate) >= refreshRate
|
|
|
|
if not shouldUpdate then
|
|
return -- Early exit for non-throttled frames
|
|
end
|
|
|
|
CR.lastUpdate = time
|
|
|
|
-- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED)
|
|
-- PERFORMANCE: Skip check entirely if no pending update
|
|
local pendingTime = CR.equipIndexPendingTime
|
|
if pendingTime and not UnitAffectingCombat("player") then
|
|
if (time - (CR.lastEquipIndexTime or 0)) >= 0.2 then
|
|
CR.lastEquipIndexTime = time
|
|
CR.equipIndexPendingTime = nil
|
|
CR.lastItemIndexTime = time
|
|
CR.IndexItems()
|
|
CR.Actions = {}
|
|
CR.Macros = {}
|
|
CR.IndexActionBars()
|
|
|
|
if CRM.realtime == 0 then
|
|
CR.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE: Check for expired reactive procs only if we have any
|
|
-- Use statically allocated removal buffer to avoid per-frame allocation
|
|
local reactiveProcs = CR.reactiveProcs
|
|
if reactiveProcs then
|
|
local hasExpiredProc = false
|
|
local toRemove = CR._procRemovalBuffer
|
|
local removeCount = 0
|
|
|
|
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
|
local spellName, procData = next(reactiveProcs)
|
|
while spellName do
|
|
if procData and procData.expiry and time >= procData.expiry then
|
|
removeCount = removeCount + 1
|
|
toRemove[removeCount] = spellName
|
|
hasExpiredProc = true
|
|
end
|
|
spellName, procData = next(reactiveProcs, spellName)
|
|
end
|
|
|
|
-- 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
|
|
CR.TestForAllActiveActions()
|
|
CR.isActionUpdateQueued = false
|
|
end
|
|
end
|
|
-- PERFORMANCE: Check for modifier key state changes (no events for these in vanilla WoW)
|
|
-- Only queue update if modifier state actually changed - avoids full TestForAllActiveActions
|
|
-- Nampower v2.41+ fires KEY_DOWN/KEY_UP for all keys including modifiers, so skip polling
|
|
if not CR.NampowerAPI.features.hasKeyEvents then
|
|
local altDown = IsAltKeyDown()
|
|
local shiftDown = IsShiftKeyDown()
|
|
local ctrlDown = IsControlKeyDown()
|
|
|
|
if altDown ~= CR._lastAltDown or
|
|
shiftDown ~= CR._lastShiftDown or
|
|
ctrlDown ~= CR._lastCtrlDown then
|
|
CR._lastAltDown = altDown
|
|
CR._lastShiftDown = shiftDown
|
|
CR._lastCtrlDown = ctrlDown
|
|
-- Modifier changed - queue update in event-driven mode, or just mark for realtime
|
|
if CRM.realtime == 0 then
|
|
CR.isActionUpdateQueued = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check the saved variable to decide which update mode to use.
|
|
if CRM.realtime == 1 then
|
|
-- Realtime Mode: Force an update on every throttled tick for maximum responsiveness.
|
|
CR.TestForAllActiveActions()
|
|
else
|
|
-- Event-Driven Mode (Default): Only update if a relevant game event has queued it.
|
|
if CR.isActionUpdateQueued then
|
|
CR.TestForAllActiveActions()
|
|
CR.isActionUpdateQueued = false
|
|
end
|
|
end
|
|
|
|
-- The rest of this function handles time-based logic that must always run.
|
|
if CR.CurrentSpell.autoAttackLock and (time - CR.autoAttackLockElapsed) > refreshRate then
|
|
CR.CurrentSpell.autoAttackLock = false
|
|
CR.autoAttackLockElapsed = nil
|
|
end
|
|
|
|
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
|
local Sequences = CR.Sequences
|
|
local seqKey, sequence = next(Sequences)
|
|
while seqKey do
|
|
if sequence.index > 1 and sequence.reset.secs and (time - (sequence.lastUpdate or 0)) >= sequence.reset.secs then
|
|
CR.ResetSequence(sequence)
|
|
end
|
|
seqKey, sequence = next(Sequences, seqKey)
|
|
end
|
|
|
|
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
|
local spell_tracking = CR.spell_tracking
|
|
local guid, cast = next(spell_tracking)
|
|
while guid do
|
|
local nextGuid = next(spell_tracking, guid) -- Get next before potential removal
|
|
if cast.expires and time > cast.expires then
|
|
spell_tracking[guid] = nil
|
|
end
|
|
guid, cast = nextGuid, nextGuid and spell_tracking[nextGuid]
|
|
end
|
|
|
|
-- Clean stale castTracking entries (standalone mode only, pfUI 7.6 manages its own)
|
|
if not CR.hasPfUI76 then
|
|
local ct = CR.castTracking
|
|
local ctGuid, ctEntry = next(ct)
|
|
while ctGuid do
|
|
local nextCtGuid = next(ct, ctGuid)
|
|
if ctEntry.endTime and time > ctEntry.endTime + 0.5 then
|
|
ct[ctGuid] = nil
|
|
end
|
|
ctGuid = nextCtGuid
|
|
ctEntry = nextCtGuid and ct[nextCtGuid]
|
|
end
|
|
end
|
|
|
|
-- PERFORMANCE OPTIMIZATION: Run memory cleanup less frequently (every 5 seconds instead of every frame)
|
|
-- This reduces CPU usage while maintaining effective memory management
|
|
if (time - CR.lastCleanupTime) >= CR.CLEANUP_INTERVAL then
|
|
CR.lastCleanupTime = time
|
|
|
|
-- MEMORY: Clean up carnageDurationOverrides older than 30 seconds
|
|
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
|
local carnageOverrides = CR.carnageDurationOverrides
|
|
if carnageOverrides then
|
|
local spellID, data = next(carnageOverrides)
|
|
while spellID do
|
|
local nextID = next(carnageOverrides, spellID)
|
|
if data.timestamp and (time - data.timestamp) > 30 then
|
|
carnageOverrides[spellID] = nil
|
|
end
|
|
spellID, data = nextID, nextID and carnageOverrides[nextID]
|
|
end
|
|
end
|
|
|
|
-- MEMORY: Clean up old ComboPointTracking entries (older than 60 seconds)
|
|
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
|
local comboTracking = CR.ComboPointTracking
|
|
if comboTracking then
|
|
local trackName, data = next(comboTracking)
|
|
while trackName do
|
|
local nextName = next(comboTracking, trackName)
|
|
if data.cast_time and (time - data.cast_time) > 60 then
|
|
comboTracking[trackName] = nil
|
|
end
|
|
trackName, data = nextName, nextName and comboTracking[nextName]
|
|
end
|
|
end
|
|
|
|
-- MEMORY: Clear spell name caches every 60 seconds (12 cleanup cycles)
|
|
CR._spellCacheCleanupCounter = (CR._spellCacheCleanupCounter or 0) + 1
|
|
if CR._spellCacheCleanupCounter >= 12 then
|
|
CR._spellCacheCleanupCounter = 0
|
|
if CR.ClearSpellNameCaches then
|
|
CR.ClearSpellNameCaches()
|
|
end
|
|
if CR.ClearStripRankCache then
|
|
CR.ClearStripRankCache()
|
|
end
|
|
end
|
|
|
|
-- MEMORY: Clean up stale perTargetState entries in sequences (older than 5 minutes)
|
|
-- This prevents memory bloat from tracking many targets over time
|
|
-- PERFORMANCE: Use next() instead of pairs() to avoid iterator closure allocation
|
|
local seqKey2, seq2 = next(CR.Sequences)
|
|
while seqKey2 do
|
|
if seq2.perTargetState then
|
|
local ptGuid, ptState = next(seq2.perTargetState)
|
|
while ptGuid do
|
|
local nextPtGuid = next(seq2.perTargetState, ptGuid)
|
|
if ptState.lastUpdate and (time - ptState.lastUpdate) > SEQUENCE_STALE_TIME then
|
|
seq2.perTargetState[ptGuid] = nil
|
|
end
|
|
ptGuid = nextPtGuid
|
|
ptState = nextPtGuid and seq2.perTargetState[nextPtGuid]
|
|
end
|
|
end
|
|
seqKey2, seq2 = next(CR.Sequences, seqKey2)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Initialize the nested table for the GameTooltip hooks if it doesn't exist
|
|
if not CleveRoids.Hooks.GameTooltip then CleveRoids.Hooks.GameTooltip = {} end
|
|
|
|
-- Save the original GameTooltip.SetAction function before we override it
|
|
CleveRoids.Hooks.GameTooltip.SetAction = GameTooltip.SetAction
|
|
|
|
-- Now, define our custom version of the function
|
|
function GameTooltip.SetAction(self, slot)
|
|
if not slot then return end
|
|
local actions = CleveRoids.GetAction(slot)
|
|
|
|
-- If this is our macro but has no active action, show just the macro name
|
|
if actions and not actions.active then
|
|
local macroName = GetActionText(slot)
|
|
if macroName then
|
|
GameTooltip:SetText(macroName)
|
|
GameTooltip:Show()
|
|
return
|
|
end
|
|
end
|
|
|
|
local action_to_display_info = nil
|
|
if actions then
|
|
-- Only show spell/item tooltip when there's an active action
|
|
if actions.active then
|
|
action_to_display_info = actions.active
|
|
end
|
|
end
|
|
|
|
if action_to_display_info and action_to_display_info.action then
|
|
local action_name = action_to_display_info.action
|
|
|
|
-- NEW: Check if action is a slot ID for tooltip
|
|
local slotId = tonumber(action_name)
|
|
if slotId and slotId >= 1 and slotId <= 19 then
|
|
-- Use the more specific SetInventoryItem function to prevent conflicts with other addons.
|
|
GameTooltip:SetInventoryItem("player", slotId)
|
|
GameTooltip:Show()
|
|
return
|
|
end
|
|
-- End new logic
|
|
|
|
local current_spell_data = CleveRoids.GetSpell(action_name)
|
|
if current_spell_data and current_spell_data.id then
|
|
if current_spell_data.spellSlot and current_spell_data.bookType then
|
|
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
|
|
else
|
|
GameTooltip:SetSpellByID(current_spell_data.id)
|
|
end
|
|
GameTooltip:Show()
|
|
return
|
|
end
|
|
|
|
local current_item_data = CleveRoids.GetItem(action_name)
|
|
if current_item_data then
|
|
-- Use specific functions based on where the item is located.
|
|
if current_item_data.inventoryID then
|
|
GameTooltip:SetInventoryItem("player", current_item_data.inventoryID)
|
|
elseif current_item_data.bagID and current_item_data.slot then
|
|
GameTooltip:SetBagItem(current_item_data.bagID, current_item_data.slot)
|
|
else
|
|
-- Fallback to the original method if location is unknown.
|
|
GameTooltip:SetHyperlink(current_item_data.link)
|
|
end
|
|
GameTooltip:Show()
|
|
return
|
|
end
|
|
|
|
if action_to_display_info.macro and type(action_to_display_info.macro) == "table" then
|
|
local nested_action_info = action_to_display_info.macro
|
|
local nested_action_name = nested_action_info.action
|
|
|
|
current_spell_data = CleveRoids.GetSpell(nested_action_name)
|
|
if current_spell_data and current_spell_data.id then
|
|
if current_spell_data.spellSlot and current_spell_data.bookType then
|
|
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
|
|
else
|
|
GameTooltip:SetSpellByID(current_spell_data.id)
|
|
end
|
|
GameTooltip:Show()
|
|
return
|
|
end
|
|
|
|
current_item_data = CleveRoids.GetItem(nested_action_name)
|
|
if current_item_data then
|
|
if current_item_data.inventoryID then
|
|
GameTooltip:SetInventoryItem("player", current_item_data.inventoryID)
|
|
elseif current_item_data.bagID and current_item_data.slot then
|
|
GameTooltip:SetBagItem(current_item_data.bagID, current_item_data.slot)
|
|
else
|
|
GameTooltip:SetHyperlink(current_item_data.link)
|
|
end
|
|
GameTooltip:Show()
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If none of our custom logic handled it, call the original function we saved earlier.
|
|
CleveRoids.Hooks.GameTooltip.SetAction(self, slot)
|
|
end
|
|
|
|
CleveRoids.Hooks.PickupAction = PickupAction
|
|
function PickupAction(slot)
|
|
if not slot then return end
|
|
CleveRoids.ClearAction(slot)
|
|
CleveRoids.ClearSlot(CleveRoids.actionSlots, slot)
|
|
CleveRoids.ClearAction(CleveRoids.reactiveSlots, slot)
|
|
return CleveRoids.Hooks.PickupAction(slot)
|
|
end
|
|
|
|
CleveRoids.Hooks.ActionHasRange = ActionHasRange
|
|
function ActionHasRange(slot)
|
|
if not slot then return nil end
|
|
local actions = CleveRoids.GetAction(slot)
|
|
-- Only override for our macros with #showtooltip
|
|
if actions and actions.tooltip then
|
|
-- If we have an active action with valid range data, use it
|
|
if actions.active then
|
|
if actions.active.inRange ~= -1 then
|
|
return 1 -- Has range check with valid data
|
|
else
|
|
-- For channeled spells (inRange == -1), try proxy slot lookup
|
|
local spellName = actions.active.spell and actions.active.spell.name
|
|
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
|
|
if proxySlot then
|
|
return CleveRoids.Hooks.ActionHasRange(proxySlot)
|
|
end
|
|
end
|
|
end
|
|
-- FIX: Even without actions.active, if the macro has potential spell actions,
|
|
-- return 1 to ensure pfUI calls IsActionInRange (which will do proper checks)
|
|
-- This fixes Super macros where actions.active might not be populated yet
|
|
if actions.tooltip.spell or actions.tooltip.type == "spell" then
|
|
return 1 -- Tooltip is a spell - has range
|
|
end
|
|
-- Check if any action in the list is a spell
|
|
if actions.list then
|
|
for i = 1, table.getn(actions.list) do
|
|
local action = actions.list[i]
|
|
if action and (action.spell or action.type == "spell") then
|
|
return 1 -- Has at least one spell action
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Not a macro we're tracking - pass through to original
|
|
return CleveRoids.Hooks.ActionHasRange(slot)
|
|
end
|
|
|
|
CleveRoids.Hooks.IsActionInRange = IsActionInRange
|
|
function IsActionInRange(slot, unit)
|
|
if not slot then return nil end
|
|
local actions = CleveRoids.GetAction(slot)
|
|
-- Only override for our macros with #showtooltip
|
|
if actions and actions.tooltip then
|
|
-- If we have an active spell action, use its range data
|
|
if actions.active and actions.active.type == "spell" then
|
|
if actions.active.inRange ~= -1 then
|
|
return actions.active.inRange
|
|
else
|
|
-- For channeled spells (inRange == -1), try proxy slot lookup
|
|
local spellName = actions.active.spell and actions.active.spell.name
|
|
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
|
|
if proxySlot then
|
|
return CleveRoids.Hooks.IsActionInRange(proxySlot, unit)
|
|
end
|
|
end
|
|
end
|
|
-- FIX: If no active action but macro has tooltip spell, check its range directly
|
|
-- This ensures range coloring works even when actions.active hasn't been populated
|
|
if not actions.active and actions.tooltip.spell then
|
|
local spell = actions.tooltip.spell
|
|
local spellName = spell.name
|
|
if spellName and IsSpellInRange then
|
|
local targetUnit = unit or "target"
|
|
if UnitExists(targetUnit) then
|
|
-- FIX: Wrap in pcall - spell might not be in spellbook (items, other class spells, etc.)
|
|
local ok, result = pcall(IsSpellInRange, spellName, targetUnit)
|
|
if ok then
|
|
if result == 0 then
|
|
return 0 -- Out of range
|
|
elseif result == 1 then
|
|
return 1 -- In range
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Try proxy slot as fallback
|
|
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
|
|
if proxySlot then
|
|
return CleveRoids.Hooks.IsActionInRange(proxySlot, unit)
|
|
end
|
|
end
|
|
end
|
|
-- Not a macro we're tracking - pass through to original
|
|
return CleveRoids.Hooks.IsActionInRange(slot, unit)
|
|
end
|
|
|
|
CleveRoids.Hooks.OriginalIsUsableAction = IsUsableAction
|
|
CleveRoids.Hooks.IsUsableAction = IsUsableAction
|
|
function IsUsableAction(slot, unit)
|
|
if not slot then return nil, nil end
|
|
local actions = CleveRoids.GetAction(slot)
|
|
|
|
-- If this is one of our macros AND it uses #showtooltip
|
|
if actions and actions.tooltip then
|
|
-- IMPORTANT: Only override usability when #showtooltip is present
|
|
-- Macros without #showtooltip should use default game behavior
|
|
if actions.active then
|
|
-- We have an active action - return its usable state
|
|
-- FIX: Convert oom boolean to number (1/nil) for Blizzard's button code
|
|
local oomValue = actions.active.oom and 1 or nil
|
|
return actions.active.usable, oomValue
|
|
else
|
|
-- FIX: If no active action but tooltip has a spell, check its usability directly
|
|
-- This ensures usability coloring works even when actions.active hasn't been populated
|
|
if actions.tooltip.spell then
|
|
local spell = actions.tooltip.spell
|
|
local spellName = spell.name
|
|
if spellName and IsSpellUsable then
|
|
-- FIX: Wrap in pcall - spell might not be in spellbook (items, other class spells, etc.)
|
|
local ok, usable, notEnoughMana = pcall(IsSpellUsable, spellName)
|
|
if ok then
|
|
if usable == 1 then
|
|
return 1, nil -- Usable
|
|
elseif notEnoughMana == 1 then
|
|
return nil, 1 -- Out of mana
|
|
else
|
|
return nil, nil -- Not usable (wrong stance, etc.)
|
|
end
|
|
end
|
|
end
|
|
-- Fallback: check if spell has a cost and compare to current mana
|
|
if spell.cost and spell.cost > 0 then
|
|
local currentMana = UnitMana("player")
|
|
if currentMana < spell.cost then
|
|
return nil, 1 -- Out of mana
|
|
end
|
|
end
|
|
-- Default to usable if we can't determine otherwise
|
|
return 1, nil
|
|
elseif actions.tooltip.item then
|
|
-- FIX: Handle item tooltips - items are always usable unless on cooldown
|
|
-- The cooldown is handled separately by GetActionCooldown hook
|
|
return 1, nil
|
|
end
|
|
-- No tooltip spell or item - this macro's conditionals all failed
|
|
-- Return nil to make the icon dark
|
|
return nil, nil
|
|
end
|
|
else
|
|
-- Not our macro OR no #showtooltip - use game's default behavior
|
|
return CleveRoids.Hooks.IsUsableAction(slot, unit)
|
|
end
|
|
end
|
|
|
|
CleveRoids.Hooks.IsCurrentAction = IsCurrentAction
|
|
function IsCurrentAction(slot)
|
|
if not slot then return nil end
|
|
local actions = CleveRoids.GetAction(slot)
|
|
|
|
-- When no action is active (all conditionals failed), don't check tooltip's "current" status
|
|
if actions and not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then
|
|
return CleveRoids.Hooks.IsCurrentAction(slot)
|
|
end
|
|
|
|
-- Use the same priority as GetActionTexture: active first, then tooltip
|
|
local actionToCheck = (actions and actions.active) or (actions and actions.tooltip)
|
|
|
|
if not actionToCheck then
|
|
return CleveRoids.Hooks.IsCurrentAction(slot)
|
|
else
|
|
local name
|
|
if actionToCheck.spell then
|
|
if CleveRoids.IsAutoAttackSpell(actionToCheck.spell) then
|
|
return CleveRoids.CurrentSpell.autoAttack and 1 or nil
|
|
end
|
|
|
|
local rank = actionToCheck.spell.rank or actionToCheck.spell.highest.rank
|
|
name = actionToCheck.spell.name..(rank and ("("..rank..")"))
|
|
|
|
-- Check if this spell is currently queued or being cast via Nampower
|
|
-- Get spell ID for comparison
|
|
local spellId = actionToCheck.spell.id
|
|
if not spellId and GetSpellIdForName then
|
|
spellId = GetSpellIdForName(name)
|
|
end
|
|
|
|
if spellId then
|
|
-- Prefer GetCastInfo (Nampower 2.18+) for cleaner API
|
|
if GetCastInfo then
|
|
local ok, info = pcall(GetCastInfo)
|
|
if ok and info and info.spellId == spellId then
|
|
-- Spell is actively being cast/channeled
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Also check GetCurrentCastingInfo for queued spell detection
|
|
if GetCurrentCastingInfo then
|
|
local castId, visId, autoId, casting, channeling = GetCurrentCastingInfo()
|
|
|
|
-- Show glow if actively casting/channeling this spell
|
|
if (casting == 1 and castId == spellId) or (channeling == 1 and visId == spellId) then
|
|
return true
|
|
end
|
|
-- Show glow if this spell is queued (castId set but not yet casting)
|
|
if casting == 0 and channeling == 0 and castId == spellId then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
elseif actionToCheck.item then
|
|
name = actionToCheck.item.name
|
|
end
|
|
|
|
return CleveRoids.Hooks.IsCurrentAction(CleveRoids.GetProxyActionSlot(name) or slot)
|
|
end
|
|
end
|
|
|
|
local function GetSlotMacroTexture(slot)
|
|
local kind, macroId = CleveRoids.ClassicAPI.GetActionInfo(slot)
|
|
if kind == "macro" and macroId then
|
|
local _, texture = GetMacroInfo(macroId)
|
|
return texture
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function IsAutoAttackSpell(spell)
|
|
if not spell then return false end
|
|
if C_Spell and C_Spell.IsAutoAttackSpell and spell.id then
|
|
return C_Spell.IsAutoAttackSpell(spell.id)
|
|
end
|
|
if C_SpellBook and C_SpellBook.IsAutoAttackSpellBookItem and spell.spellSlot then
|
|
return C_SpellBook.IsAutoAttackSpellBookItem(spell.spellSlot, spell.bookType)
|
|
end
|
|
return false
|
|
end
|
|
CleveRoids.IsAutoAttackSpell = IsAutoAttackSpell
|
|
|
|
CleveRoids.Hooks.GetActionTexture = GetActionTexture
|
|
function GetActionTexture(slot)
|
|
if not slot then return nil end
|
|
local actions = CleveRoids.GetAction(slot)
|
|
|
|
-- Check if this is one of our macros
|
|
if actions and (actions.active or actions.tooltip) then
|
|
|
|
-- This block handles the case where all conditionals fail and no explicit
|
|
-- #showtooltip was set. It defaults to the macro's chosen icon.
|
|
if not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then
|
|
-- Get the macro's own icon as fallback
|
|
local macroTexture = GetSlotMacroTexture(slot)
|
|
|
|
-- When no conditionals pass, use macro icon (not first action's icon)
|
|
-- The actions.tooltip is just the first action which didn't pass conditionals
|
|
if macroTexture then
|
|
return macroTexture
|
|
end
|
|
|
|
-- Should never reach here, but return unknown as last resort
|
|
return CleveRoids.unknownTexture
|
|
end
|
|
|
|
-- Prioritize active action, fall back to tooltip
|
|
local a = actions.active or actions.tooltip
|
|
|
|
-- Handle numeric slot actions (e.g., /use 13)
|
|
local slotId = tonumber(a.action)
|
|
if slotId and slotId >= 1 and slotId <= 19 then
|
|
local currentTexture = GetInventoryItemTexture("player", slotId)
|
|
if currentTexture then
|
|
return currentTexture
|
|
end
|
|
|
|
-- Slot is empty, fall back to macro icon
|
|
local macroTexture = GetSlotMacroTexture(slot)
|
|
if macroTexture then
|
|
return macroTexture
|
|
end
|
|
return CleveRoids.unknownTexture
|
|
end
|
|
|
|
-- *** THIS IS THE FIX ***
|
|
-- If an action is active, return its texture directly.
|
|
-- If no action is active, return the tooltip's texture.
|
|
-- If neither has a texture, fall back to the macro's icon.
|
|
local texture = (actions.active and actions.active.texture) or (actions.tooltip and actions.tooltip.texture)
|
|
|
|
-- Check if this is a shapeshift form spell and use active texture if toggled on
|
|
if a and a.spell and a.action then
|
|
-- Strip rank info and underscores from spell name for comparison
|
|
local spellName = string.gsub(a.action, "%s*%(.-%)%s*$", "")
|
|
spellName = string.gsub(spellName, "_", " ")
|
|
-- Check all shapeshift forms to see if this spell matches and is active
|
|
for i = 1, GetNumShapeshiftForms() do
|
|
local icon, name, isActive, isCastable = GetShapeshiftFormInfo(i)
|
|
if name and string.lower(name) == string.lower(spellName) and isActive and icon then
|
|
texture = icon
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check if this is a toggled buff ability (Prowl, Shadowmeld) and swap icon based on buff state
|
|
-- Note: Stealth is handled above by shapeshift form logic
|
|
if a and a.spell and a.action then
|
|
local spellName = string.gsub(a.action, "%s*%(.-%)%s*$", "")
|
|
spellName = string.gsub(spellName, "_", " ")
|
|
|
|
-- Check if this is one of our toggled buff abilities (not shapeshift forms)
|
|
local toggledAbilities = {
|
|
[CleveRoids.Localized.Spells["Prowl"]] = true,
|
|
[CleveRoids.Localized.Spells["Shadowmeld"]] = true,
|
|
}
|
|
|
|
if toggledAbilities[spellName] then
|
|
-- Check if the buff is active
|
|
if CleveRoids.ValidatePlayerBuff(spellName) then
|
|
-- Buff is active, use the active texture from auraTextures
|
|
local activeTexture = CleveRoids.auraTextures[spellName]
|
|
if activeTexture then
|
|
texture = activeTexture
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if a and a.spell and CleveRoids.IsAutoAttackSpell(a.spell) then
|
|
local mainHandTexture = GetInventoryItemTexture("player", 16)
|
|
if mainHandTexture then
|
|
texture = mainHandTexture
|
|
end
|
|
end
|
|
|
|
if texture then
|
|
return texture
|
|
end
|
|
|
|
-- Final fallback: get the macro's icon
|
|
local macroTexture = GetSlotMacroTexture(slot)
|
|
if macroTexture then
|
|
return macroTexture
|
|
end
|
|
|
|
-- Should never reach here
|
|
return CleveRoids.unknownTexture
|
|
|
|
end
|
|
|
|
-- Not one of our macros, use the original function
|
|
return CleveRoids.Hooks.GetActionTexture(slot)
|
|
end
|
|
|
|
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
|
|
CleveRoids.Hooks.GetActionCooldown = GetActionCooldown
|
|
function GetActionCooldown(slot)
|
|
-- Guard against nil/invalid slot
|
|
if not slot then return 0, 0, 0 end
|
|
|
|
local actions = CleveRoids.GetAction(slot)
|
|
-- Check for actions.active OR actions.tooltip
|
|
if actions and (actions.active or actions.tooltip) then
|
|
|
|
-- When no action is active (all conditionals failed) but tooltip exists from #showtooltip,
|
|
-- don't show the tooltip's cooldown - fall back to the original function (no cooldown)
|
|
-- This matches the icon behavior in GetActionTexture (lines 3958-3977)
|
|
if not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then
|
|
return CleveRoids.Hooks.GetActionCooldown(slot)
|
|
end
|
|
|
|
-- Prioritize the active action, but fall back to the tooltip action
|
|
local a = actions.active or actions.tooltip
|
|
|
|
local slotId = tonumber(a.action)
|
|
if slotId and slotId >= 1 and slotId <= 19 then
|
|
return GetInventoryItemCooldown("player", slotId)
|
|
end
|
|
|
|
if a.spell then
|
|
return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType)
|
|
elseif a.item then
|
|
if a.item.bagID and a.item.slot then
|
|
return GetContainerItemCooldown(a.item.bagID, a.item.slot)
|
|
elseif a.item.inventoryID then
|
|
return GetInventoryItemCooldown("player", a.item.inventoryID)
|
|
end
|
|
end
|
|
|
|
-- DEBUG: Log when we fall through without finding spell/item data (once per slot)
|
|
if CleveRoids.cooldownDebug and not CleveRoids.cooldownDebugLogged[slot] then
|
|
CleveRoids.cooldownDebugLogged[slot] = true
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"|cffff0000[CD Issue]|r Slot %d: No cooldown data! action='%s' type=%s spell=%s item=%s",
|
|
slot,
|
|
tostring(a.action),
|
|
tostring(a.type),
|
|
tostring(a.spell ~= nil),
|
|
tostring(a.item ~= nil)
|
|
))
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff8800[CD Issue]|r This slot will show incorrect cooldown. Report macro text to developer.")
|
|
end
|
|
|
|
-- Fallback to original function (preserves SuperWoW's #showtooltip handling)
|
|
return CleveRoids.Hooks.GetActionCooldown(slot)
|
|
else
|
|
return CleveRoids.Hooks.GetActionCooldown(slot)
|
|
end
|
|
end
|
|
|
|
CleveRoids.Hooks.GetActionCount = GetActionCount
|
|
function GetActionCount(slot)
|
|
-- Guard against nil/invalid slot
|
|
if not slot then return 0 end
|
|
|
|
local action = CleveRoids.GetAction(slot)
|
|
local count
|
|
|
|
-- When no action is active (all conditionals failed), don't show tooltip's count
|
|
if action and not action.active and not action.explicitTooltip and action.list and table.getn(action.list) > 0 then
|
|
return CleveRoids.Hooks.GetActionCount(slot)
|
|
end
|
|
|
|
-- Use the same priority as GetActionTexture: active first, then tooltip
|
|
local actionToCheck = (action and action.active) or (action and action.tooltip)
|
|
if actionToCheck then
|
|
|
|
local slotId = tonumber(actionToCheck.action)
|
|
if slotId and slotId >= 1 and slotId <= 19 then
|
|
return GetInventoryItemCount("player", slotId)
|
|
end
|
|
|
|
if actionToCheck.item then
|
|
count = CleveRoids.GetLiveItemCount(actionToCheck.item.name or actionToCheck.action)
|
|
|
|
elseif actionToCheck.spell then
|
|
local reagent = actionToCheck.spell.reagent
|
|
if not reagent or reagent == "" then
|
|
reagent = nil
|
|
local ss, bt = actionToCheck.spell.spellSlot, actionToCheck.spell.bookType
|
|
if ss and bt then
|
|
local _, r = CleveRoids.GetSpellCost(ss, bt)
|
|
reagent = r
|
|
end
|
|
if (not reagent) and _ReagentBySpell and actionToCheck.spell.name then
|
|
reagent = _ReagentBySpell[actionToCheck.spell.name] -- e.g., Vanish → Flash Powder
|
|
end
|
|
actionToCheck.spell.reagent = reagent -- cache it so we don't re-scan every frame
|
|
end
|
|
if reagent and reagent ~= "" then
|
|
count = CleveRoids.GetReagentCount(reagent) -- id-first bag scan, falls back to name/tooltip
|
|
end
|
|
end
|
|
end
|
|
|
|
return count or CleveRoids.Hooks.GetActionCount(slot)
|
|
end
|
|
|
|
CleveRoids.Hooks.IsConsumableAction = IsConsumableAction
|
|
function IsConsumableAction(slot)
|
|
-- Guard against nil/invalid slot
|
|
if not slot then return nil end
|
|
|
|
local action = CleveRoids.GetAction(slot)
|
|
|
|
-- When no action is active (all conditionals failed), don't show tooltip's consumable status
|
|
if action and not action.active and not action.explicitTooltip and action.list and table.getn(action.list) > 0 then
|
|
return CleveRoids.Hooks.IsConsumableAction(slot)
|
|
end
|
|
|
|
-- Use the same priority as GetActionTexture: active first, then tooltip
|
|
local actionToCheck = (action and action.active) or (action and action.tooltip)
|
|
if actionToCheck then
|
|
|
|
local slotId = tonumber(actionToCheck.action)
|
|
if slotId and slotId >= 1 and slotId <= 19 then
|
|
local _, count = GetInventoryItemCount("player", slotId)
|
|
if count and count > 0 then return 1 end
|
|
end
|
|
|
|
if actionToCheck.item and
|
|
(CleveRoids.countedItemTypes[actionToCheck.item.type]
|
|
or CleveRoids.countedItemTypes[actionToCheck.item.name])
|
|
then
|
|
return 1
|
|
end
|
|
|
|
|
|
if actionToCheck.spell then
|
|
local reagent = actionToCheck.spell.reagent
|
|
if (not reagent or reagent == "") and actionToCheck.spell.name then
|
|
reagent = _ReagentBySpell[actionToCheck.spell.name]
|
|
if reagent then
|
|
actionToCheck.spell.reagent = reagent
|
|
end
|
|
end
|
|
if reagent and reagent ~= "" then
|
|
return 1
|
|
end
|
|
end
|
|
end
|
|
|
|
return CleveRoids.Hooks.IsConsumableAction(slot)
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- RunMacro Hook - Use our own macro execution system
|
|
-- ============================================================================
|
|
-- This allows /stopmacro, /skipmacro, /firstaction to work across parent/child
|
|
-- macro boundaries without requiring SuperMacro addon.
|
|
--
|
|
-- When SuperMacro is installed, its RunLine hook takes precedence for extended
|
|
-- macro features. This hook handles vanilla Blizzard macros.
|
|
-- ============================================================================
|
|
if not CleveRoids.RunMacroHooked then
|
|
CleveRoids.Hooks.RunMacro = RunMacro
|
|
|
|
function RunMacro(indexOrName)
|
|
-- Skip if SuperMacro is handling macro execution (it has its own hooks)
|
|
if CleveRoids.SM_RunLineHooked then
|
|
return CleveRoids.Hooks.RunMacro(indexOrName)
|
|
end
|
|
|
|
-- Resolve macro index
|
|
local macroIndex
|
|
if type(indexOrName) == "number" then
|
|
macroIndex = indexOrName
|
|
elseif type(indexOrName) == "string" then
|
|
-- Could be a name or a numeric string
|
|
local num = tonumber(indexOrName)
|
|
if num then
|
|
macroIndex = num
|
|
else
|
|
macroIndex = GetMacroIndexByName(indexOrName)
|
|
end
|
|
end
|
|
|
|
if not macroIndex or macroIndex == 0 then
|
|
-- Fallback to original if we can't resolve
|
|
return CleveRoids.Hooks.RunMacro(indexOrName)
|
|
end
|
|
|
|
-- Get macro body
|
|
local name, icon, body = GetMacroInfo(macroIndex)
|
|
if not body or body == "" then
|
|
return CleveRoids.Hooks.RunMacro(indexOrName)
|
|
end
|
|
|
|
-- Clear macro flags at the start of top-level macro execution
|
|
CleveRoids.stopMacroFlag = false
|
|
CleveRoids.skipMacroFlag = false
|
|
-- Note: stopOnCastFlag is intentionally NOT cleared here - it persists
|
|
-- within a frame and is cleared by OnUpdate
|
|
|
|
if CleveRoids.macroRefDebug then
|
|
CleveRoids.Print("|cff00ff00[RunMacro Hook]|r Executing macro '" .. (name or macroIndex) .. "' via CleveRoids")
|
|
end
|
|
|
|
-- Execute through our system
|
|
CleveRoids.ExecuteMacroBody(body)
|
|
|
|
-- Don't call original - we've handled it
|
|
return
|
|
end
|
|
|
|
CleveRoids.RunMacroHooked = true
|
|
end
|
|
|
|
-- Robust named tooltip for scanning spells/items
|
|
if not CleveRoidsTooltipScan then
|
|
-- Try to create with the standard template first
|
|
local ok, _ = pcall(CreateFrame, "GameTooltip", "CleveRoidsTooltipScan", UIParent, "GameTooltipTemplate")
|
|
if not ok or not CleveRoidsTooltipScan then
|
|
-- Fallback: manual tooltip with plenty of prebuilt lines
|
|
CleveRoidsTooltipScan = CreateFrame("GameTooltip", "CleveRoidsTooltipScan", UIParent)
|
|
local L1 = CleveRoidsTooltipScan:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
|
|
local R1 = CleveRoidsTooltipScan:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
|
CleveRoidsTooltipScan:AddFontStrings(L1, R1)
|
|
for i = 2, 32 do
|
|
CleveRoidsTooltipScan:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
|
|
CleveRoidsTooltipScan:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
|
|
end
|
|
end
|
|
CleveRoidsTooltipScan:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
CleveRoidsTooltipScan:EnableMouse(false)
|
|
end
|
|
|
|
-- This single dummy frame handles events AND serves as our tooltip scanner.
|
|
CleveRoids.Frame = CreateFrame("GameTooltip")
|
|
CleveRoids.Frame:EnableMouse(false) -- Prevent tooltip from capturing mouse input
|
|
|
|
-- Create the extra font strings needed for other functions like GetSpellCost.
|
|
CleveRoids.Frame.costFontString = CleveRoids.Frame:CreateFontString()
|
|
CleveRoids.Frame.rangeFontString = CleveRoids.Frame:CreateFontString()
|
|
CleveRoids.Frame.reagentFontString = CleveRoids.Frame:CreateFontString()
|
|
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame:CreateFontString(), CleveRoids.Frame:CreateFontString())
|
|
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame.costFontString, CleveRoids.Frame.rangeFontString)
|
|
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame:CreateFontString(), CleveRoids.Frame:CreateFontString())
|
|
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame.reagentFontString, CleveRoids.Frame:CreateFontString())
|
|
|
|
CleveRoids.Frame:SetScript("OnUpdate", CleveRoids.OnUpdate)
|
|
CleveRoids.Frame:SetScript("OnEvent", function(...)
|
|
CleveRoids.Frame[event](this,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10)
|
|
end)
|
|
|
|
-- == SHUTDOWN PROTECTION ==
|
|
-- Flag to prevent SuperWoW API calls during logout (crash prevention)
|
|
CleveRoids.isShuttingDown = false
|
|
|
|
-- == CORE EVENT REGISTRATION ==
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_LOGIN")
|
|
CleveRoids.Frame:RegisterEvent("ADDON_LOADED")
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_LOGOUT")
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_LEAVING_WORLD")
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
CleveRoids.Frame:RegisterEvent("UPDATE_MACROS")
|
|
CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED")
|
|
CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
|
|
CleveRoids.Frame:RegisterEvent("BAG_UPDATE")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_INVENTORY_CHANGED")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_PET")
|
|
|
|
-- == STATE CHANGE EVENT REGISTRATION (for performance) ==
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_FOCUS_CHANGED") -- For focus addons
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_ENTER_COMBAT") -- Auto-attack started
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_LEAVE_COMBAT") -- Auto-attack stopped
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_DISABLED") -- Entered actual combat (has threat)
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_ENABLED") -- Left actual combat (no threat)
|
|
CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
|
|
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
|
|
CleveRoids.Frame:RegisterEvent("PLAYER_STARTED_MOVING")
|
|
-- Use GUID events when available (v2.39+), fall back to standard per-token events
|
|
if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then
|
|
CleveRoids.Frame:RegisterEvent("UNIT_AURA_GUID")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH_GUID")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_MANA_GUID")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_RAGE_GUID")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_ENERGY_GUID")
|
|
else
|
|
CleveRoids.Frame:RegisterEvent("UNIT_AURA")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH")
|
|
CleveRoids.Frame:RegisterEvent("UNIT_POWER")
|
|
end
|
|
if CleveRoids.hasSuperwow then
|
|
CleveRoids.Frame:RegisterEvent("UNIT_CASTEVENT")
|
|
end
|
|
if QueueSpellByName then
|
|
CleveRoids.Frame:RegisterEvent("SPELL_QUEUE_EVENT")
|
|
CleveRoids.Frame:RegisterEvent("SPELL_CAST_EVENT")
|
|
end
|
|
CleveRoids.Frame:RegisterEvent("START_AUTOREPEAT_SPELL")
|
|
CleveRoids.Frame:RegisterEvent("STOP_AUTOREPEAT_SPELL")
|
|
CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_START")
|
|
CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
|
|
CleveRoids.Frame:RegisterEvent("SPELLCAST_START")
|
|
CleveRoids.Frame:RegisterEvent("SPELLCAST_STOP")
|
|
CleveRoids.Frame:RegisterEvent("SPELLCAST_FAILED")
|
|
CleveRoids.Frame:RegisterEvent("SPELLCAST_INTERRUPTED")
|
|
|
|
-- Nampower SPELL_CAST_EVENT for reliable channel tracking + cast sequence + spell_tracking
|
|
if GetCurrentCastingInfo then
|
|
CleveRoids.Frame:RegisterEvent("SPELL_CAST_EVENT")
|
|
end
|
|
|
|
-- Nampower v2.25+: SPELL_START_SELF for spell_tracking and cast sequence (Nampower fallback for UNIT_CASTEVENT)
|
|
if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.features and CleveRoids.NampowerAPI.features.hasSpellStartEvents then
|
|
CleveRoids.Frame:RegisterEvent("SPELL_START_SELF")
|
|
CleveRoids.Frame:RegisterEvent("SPELL_FAILED_SELF")
|
|
end
|
|
|
|
-- Nampower v2.41+: keyboard input events for [keydown:X] conditional
|
|
if CleveRoids.NampowerAPI.features.hasKeyEvents then
|
|
CleveRoids.Frame:RegisterEvent("KEY_DOWN")
|
|
CleveRoids.Frame:RegisterEvent("KEY_UP")
|
|
end
|
|
|
|
-- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua
|
|
-- which has the complete implementation including the INTERCEPT path for all commands
|
|
|
|
function CleveRoids.Frame:UNIT_PET()
|
|
if arg1 == "player" then
|
|
CleveRoids.IndexPetSpells()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handle shutdown to prevent SuperWoW API crashes during logout
|
|
function CleveRoids.Frame:PLAYER_LOGOUT()
|
|
CleveRoids.isShuttingDown = true
|
|
this:UnregisterAllEvents()
|
|
this:SetScript("OnUpdate", nil)
|
|
this:SetScript("OnEvent", nil)
|
|
-- Cancel periodic C_Timer tickers
|
|
if CleveRoids._cleanupTicker then
|
|
CleveRoids._cleanupTicker:Cancel()
|
|
end
|
|
if CleveRoids._auraCleanupTicker then
|
|
CleveRoids._auraCleanupTicker:Cancel()
|
|
end
|
|
-- Clear spell caches to prevent stale data
|
|
if CleveRoids.spellIdCache then
|
|
for k in pairs(CleveRoids.spellIdCache) do
|
|
CleveRoids.spellIdCache[k] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:PLAYER_LEAVING_WORLD()
|
|
-- FIX: Only set loading flag, don't unregister events
|
|
-- PLAYER_LEAVING_WORLD fires for both logout AND zone transitions
|
|
-- We need to stay alive for zone transitions; PLAYER_LOGOUT handles actual logout
|
|
CleveRoids.isLoading = true
|
|
end
|
|
|
|
-- FIX: Handle zone transitions (instance entry, etc.)
|
|
-- Refresh action button state after loading screen completes
|
|
function CleveRoids.Frame:PLAYER_ENTERING_WORLD()
|
|
CleveRoids.isLoading = false
|
|
|
|
-- Skip if not yet initialized
|
|
if not CleveRoids.ready then return end
|
|
|
|
-- Refresh all action states after zone transition
|
|
CleveRoids.TestForAllActiveActions()
|
|
|
|
-- FIX: Force Blizzard buttons to re-query IsUsableAction
|
|
-- Loading screen causes UI rebuild which can cache stale button states
|
|
-- SKIP for pfUI users - pfUI handles its own button updates
|
|
if not (pfUI and pfUI.bars) then
|
|
for slot = 1, 120 do
|
|
if CleveRoids.Actions[slot] then
|
|
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Simplified PLAYER_LOGIN - requirements already checked
|
|
function CleveRoids.Frame:PLAYER_LOGIN()
|
|
-- Skip if already disabled
|
|
if CleveRoids.disabled then return end
|
|
|
|
_, CleveRoids.playerClass = UnitClass("player")
|
|
_, CleveRoids.playerGuid = UnitExists("player")
|
|
CleveRoids.IndexSpells()
|
|
CleveRoids.IndexPetSpells()
|
|
CleveRoids.initializationTimer = GetTime() + 1.5
|
|
|
|
-- PERFORMANCE: Initialize event-driven cache states
|
|
CleveRoids._cachedPlayerInCombat = UnitAffectingCombat("player") and true or false
|
|
|
|
-- Schedule delayed WDB warmup (loads items into client cache via tooltip scan)
|
|
-- This ensures GetItemInfo() works for all inventory items after a WDB clear
|
|
CleveRoids.wdbWarmupTime = GetTime() + 3.0 -- 3 second delay after login
|
|
end
|
|
|
|
-- PERFORMANCE: WDB warmup - tooltip scan all bag items to ensure they're cached
|
|
-- This prevents GetItemInfo() returning nil for items after a WDB clear
|
|
function CleveRoids.DoWDBWarmup()
|
|
if CleveRoids.wdbWarmupDone then return end
|
|
CleveRoids.wdbWarmupDone = true
|
|
|
|
-- Create a hidden tooltip for scanning if it doesn't exist
|
|
local tip = CleveRoidsWDBTip
|
|
if not tip then
|
|
tip = CreateFrame("GameTooltip", "CleveRoidsWDBTip", UIParent, "GameTooltipTemplate")
|
|
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
end
|
|
|
|
local scanned = 0
|
|
|
|
-- Scan all bag slots
|
|
for bag = 0, 4 do
|
|
local slots = GetContainerNumSlots(bag) or 0
|
|
for slot = 1, slots do
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if link then
|
|
-- Tooltip scan loads the item into WDB
|
|
tip:ClearLines()
|
|
tip:SetBagItem(bag, slot)
|
|
scanned = scanned + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Scan equipped items
|
|
for slot = 0, 19 do
|
|
local link = GetInventoryItemLink("player", slot)
|
|
if link then
|
|
tip:ClearLines()
|
|
tip:SetInventoryItem("player", slot)
|
|
scanned = scanned + 1
|
|
end
|
|
end
|
|
|
|
-- Now trigger a full item index to populate the cache with valid data
|
|
if CleveRoids.IndexItems then
|
|
CleveRoids.IndexItems()
|
|
end
|
|
|
|
if CleveRoids.debug then
|
|
CleveRoids.Print("|cff88ff88[WDB Warmup]|r Scanned " .. scanned .. " items into cache")
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:ADDON_LOADED(addon)
|
|
-- keep your existing init for CRM:
|
|
if addon == "CleveRoidMacros" or addon == "SuperCleveRoidMacros" then
|
|
CleveRoids.InitializeExtensions()
|
|
end
|
|
-- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua
|
|
end
|
|
|
|
function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time)
|
|
-- Handle melee swings for judgement refresh
|
|
if action == "MAINHAND" or action == "OFFHAND" then
|
|
-- Only process if this is the player's melee swing
|
|
if caster == CleveRoids.playerGuid and CleveRoids.playerClass == "PALADIN" then
|
|
-- Refresh judgements on the target
|
|
-- Defensive: verify libdebuff is a table before accessing properties
|
|
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
|
if target and lib and lib.objects then
|
|
local normalizedTarget = CleveRoids.NormalizeGUID(target)
|
|
if normalizedTarget and lib.objects[normalizedTarget] then
|
|
-- Refresh all active Judgements on the target
|
|
for spellID, rec in pairs(lib.objects[normalizedTarget]) do
|
|
-- Check if this is a judgement by spell ID
|
|
if lib.judgementSpells and lib.judgementSpells[spellID] and rec.start and rec.duration then
|
|
-- Only refresh if the Judgement is still active and was cast by player
|
|
local remaining = rec.duration + rec.start - GetTime()
|
|
if remaining > 0 and rec.caster == "player" then
|
|
-- Refresh the Judgement by updating the start time
|
|
rec.start = GetTime()
|
|
|
|
local spellName = GetSpellRecField(spellID, "name")
|
|
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
|
|
|
|
if CleveRoids.debug then
|
|
DEFAULT_CHAT_FRAME:AddMessage(
|
|
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on %s hit - new duration: %ds",
|
|
baseName, spellID, action, rec.duration)
|
|
)
|
|
end
|
|
|
|
-- Also sync to pfUI if it's loaded (pre-7.6 only)
|
|
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
|
|
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
|
|
local targetLevel = UnitLevel("target") or 0
|
|
|
|
if targetName then
|
|
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player")
|
|
|
|
if CleveRoids.debug then
|
|
DEFAULT_CHAT_FRAME:AddMessage(
|
|
string.format("|cff00ffaa[pfUI Judgement Refresh]|r Synced %s refresh to pfUI", baseName)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return -- Still return early after processing melee
|
|
end
|
|
|
|
-- Debug channel tracking
|
|
if CleveRoids.ChannelTimeDebug then
|
|
local spellName = spell_id and GetSpellRecField and GetSpellRecField(spell_id, "name") or "Unknown"
|
|
if string.find(spellName, "Arcane") then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UNIT_CASTEVENT]|r %s: %s (ID:%s) caster=%s player=%s",
|
|
action, spellName, tostring(spell_id), tostring(caster), tostring(CleveRoids.playerGuid)))
|
|
end
|
|
end
|
|
|
|
-- handle cast spell tracking
|
|
local cast = CleveRoids.spell_tracking[caster]
|
|
if cast_time > 0 and action == "START" or action == "CHANNEL" then
|
|
CleveRoids.spell_tracking[caster] = { spell_id = spell_id, expires = GetTime() + cast_time/1000, type = action }
|
|
|
|
-- ALSO store under "player" literal for easier lookup
|
|
if caster == CleveRoids.playerGuid then
|
|
CleveRoids.spell_tracking["player"] = CleveRoids.spell_tracking[caster]
|
|
|
|
-- For CHANNEL events, capture duration for checkchanneled conditional
|
|
if action == "CHANNEL" then
|
|
CleveRoids.channelStartTime = GetTime()
|
|
-- Try to get accurate duration from spell tooltip (reflects haste)
|
|
local tooltipDuration = CleveRoids.GetChannelDurationFromTooltipByID(spell_id)
|
|
if tooltipDuration then
|
|
CleveRoids.channelDuration = tooltipDuration
|
|
else
|
|
-- Fallback to UNIT_CASTEVENT duration (may not reflect haste)
|
|
CleveRoids.channelDuration = cast_time / 1000
|
|
end
|
|
end
|
|
|
|
-- For START events, capture cast time for checkcasting conditional
|
|
if action == "START" then
|
|
CleveRoids.castStartTime = GetTime()
|
|
-- For cast-time spells, UNIT_CASTEVENT duration is usually accurate
|
|
-- but we could add tooltip scanning here too if needed
|
|
CleveRoids.castDuration = cast_time / 1000
|
|
end
|
|
end
|
|
|
|
if CleveRoids.ChannelTimeDebug and caster == CleveRoids.playerGuid then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[Tracking]|r Set spell_tracking[%s] AND [player]: type=%s, expires=%.2f",
|
|
tostring(caster), action, GetTime() + cast_time/1000))
|
|
end
|
|
-- Always show for channels if debug is on
|
|
if CleveRoids.ChannelTimeDebug and action == "CHANNEL" then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[Tracking]|r caster=%s, playerGuid=%s, match=%s",
|
|
tostring(caster), tostring(CleveRoids.playerGuid), tostring(caster == CleveRoids.playerGuid)))
|
|
end
|
|
elseif cast
|
|
and (
|
|
(cast.spell_id == spell_id and (action == "FAIL" or action == "CAST"))
|
|
or (GetTime() > cast.expires)
|
|
)
|
|
then
|
|
if CleveRoids.ChannelTimeDebug and caster == CleveRoids.playerGuid then
|
|
local reason = ""
|
|
if cast.spell_id == spell_id and (action == "FAIL" or action == "CAST") then
|
|
reason = string.format("spell_id match (%s) and action=%s", tostring(spell_id), action)
|
|
elseif GetTime() > cast.expires then
|
|
reason = string.format("expired (%.2f > %.2f)", GetTime(), cast.expires)
|
|
end
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[Tracking]|r CLEARING spell_tracking - %s", reason))
|
|
end
|
|
CleveRoids.spell_tracking[caster] = nil
|
|
-- Also clear "player" literal if this is the player
|
|
if caster == CleveRoids.playerGuid then
|
|
CleveRoids.spell_tracking["player"] = nil
|
|
end
|
|
end
|
|
|
|
-- handle cast sequence (SuperWoW)
|
|
if CleveRoids.currentSequence and caster == CleveRoids.playerGuid then
|
|
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
|
|
|
|
local name = GetSpellRecField(spell_id, "name")
|
|
local rank = GetSpellRecField(spell_id, "rank")
|
|
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
|
|
local isSeqSpell = active and active.action and (
|
|
active.action == name or
|
|
(nameRank and active.action == nameRank)
|
|
)
|
|
|
|
if isSeqSpell then
|
|
local status = CleveRoids.currentSequence.status
|
|
if status == 0 and (action == "START" or action == "CHANNEL") and cast_time > 0 then
|
|
-- cast_time is ms; GetTime() is seconds
|
|
CleveRoids.currentSequence.status = 1
|
|
CleveRoids.currentSequence.expires = GetTime() + (cast_time / 1000) - 2
|
|
elseif (status == 0 and action == "CAST" and cast_time == 0)
|
|
or (status == 1 and action == "CAST" and CleveRoids.currentSequence.expires) then
|
|
CleveRoids.currentSequence.status = 2
|
|
CleveRoids.currentSequence.lastUpdate = GetTime()
|
|
CleveRoids.AdvanceSequence(CleveRoids.currentSequence)
|
|
CleveRoids.currentSequence = nil
|
|
elseif action == "INTERRUPTED" or action == "FAILED" then
|
|
CleveRoids.currentSequence.status = 1
|
|
end
|
|
end
|
|
end
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
-- Nampower SPELL_CAST_EVENT handler for reliable channel tracking
|
|
-- Also handles spell_tracking clearing and cast sequence advancement (Nampower fallback for UNIT_CASTEVENT)
|
|
function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGuid, itemId)
|
|
local CHANNEL = 4
|
|
|
|
if castType == CHANNEL and success == 1 then
|
|
-- Channel started successfully
|
|
CleveRoids.CurrentSpell.type = "channeled"
|
|
CleveRoids.CurrentSpell.castingSpellId = spellId
|
|
|
|
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
|
|
if spellName then
|
|
CleveRoids.CurrentSpell.spellName = spellName
|
|
end
|
|
|
|
-- Force immediate action update
|
|
CleveRoids.TestForAllActiveActions()
|
|
end
|
|
|
|
-- Nampower fallback: spell_tracking and cast sequence (when SuperWoW not available)
|
|
if not CleveRoids.hasSuperwow and spellId then
|
|
local playerGuid = CleveRoids.playerGuid or CleveRoids.GetGUID("player")
|
|
|
|
-- Clear spell_tracking on success or failure
|
|
if success == 1 or success == 0 then
|
|
local cast = CleveRoids.spell_tracking[playerGuid]
|
|
if cast and cast.spell_id == spellId then
|
|
CleveRoids.spell_tracking[playerGuid] = nil
|
|
CleveRoids.spell_tracking["player"] = nil
|
|
end
|
|
end
|
|
|
|
-- Cast sequence advancement
|
|
if CleveRoids.currentSequence and success == 1 then
|
|
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
|
|
if active and active.action then
|
|
local name = GetSpellRecField(spellId, "name")
|
|
local rank = GetSpellRecField(spellId, "rank")
|
|
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
|
|
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
|
|
|
|
if isSeqSpell then
|
|
CleveRoids.currentSequence.status = 2
|
|
CleveRoids.currentSequence.lastUpdate = GetTime()
|
|
CleveRoids.AdvanceSequence(CleveRoids.currentSequence)
|
|
CleveRoids.currentSequence = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cast sequence failure handling
|
|
if CleveRoids.currentSequence and success == 0 then
|
|
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
|
|
if active and active.action then
|
|
local name = GetSpellRecField(spellId, "name")
|
|
local isSeqSpell = (active.action == name)
|
|
if isSeqSpell then
|
|
CleveRoids.currentSequence.status = 1 -- Reset to retry
|
|
end
|
|
end
|
|
end
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Nampower SPELL_START_SELF handler (v2.25+)
|
|
-- Handles spell_tracking population and cast sequence START detection (Nampower fallback for UNIT_CASTEVENT)
|
|
function CleveRoids.Frame:SPELL_START_SELF(casterGuid, targetGuid, spellId, castTimeMs, durationMs, spellType, ...)
|
|
-- Skip if SuperWoW is handling this via UNIT_CASTEVENT
|
|
if CleveRoids.hasSuperwow then return end
|
|
if not spellId then return end
|
|
|
|
local playerGuid = CleveRoids.playerGuid or CleveRoids.GetGUID("player")
|
|
if casterGuid ~= playerGuid then return end
|
|
|
|
-- Populate spell_tracking (equivalent to UNIT_CASTEVENT START/CHANNEL)
|
|
local isChannel = (spellType == 1)
|
|
local action = isChannel and "CHANNEL" or "START"
|
|
|
|
if castTimeMs and castTimeMs > 0 then
|
|
CleveRoids.spell_tracking[casterGuid] = {
|
|
spell_id = spellId,
|
|
expires = GetTime() + castTimeMs / 1000,
|
|
type = action
|
|
}
|
|
CleveRoids.spell_tracking["player"] = CleveRoids.spell_tracking[casterGuid]
|
|
|
|
-- Channel duration capture (equivalent to UNIT_CASTEVENT CHANNEL)
|
|
if isChannel then
|
|
CleveRoids.channelStartTime = GetTime()
|
|
local tooltipDuration = CleveRoids.GetChannelDurationFromTooltipByID(spellId)
|
|
if tooltipDuration then
|
|
CleveRoids.channelDuration = tooltipDuration
|
|
else
|
|
CleveRoids.channelDuration = castTimeMs / 1000
|
|
end
|
|
end
|
|
|
|
-- Cast duration capture (equivalent to UNIT_CASTEVENT START)
|
|
if not isChannel then
|
|
CleveRoids.castStartTime = GetTime()
|
|
CleveRoids.castDuration = castTimeMs / 1000
|
|
end
|
|
end
|
|
|
|
-- Cast sequence: set status=1 (casting) for cast-time spells
|
|
if CleveRoids.currentSequence and castTimeMs and castTimeMs > 0 then
|
|
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
|
|
if active and active.action then
|
|
local name = GetSpellRecField(spellId, "name")
|
|
local rank = GetSpellRecField(spellId, "rank")
|
|
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
|
|
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
|
|
|
|
if isSeqSpell and CleveRoids.currentSequence.status == 0 then
|
|
CleveRoids.currentSequence.status = 1
|
|
CleveRoids.currentSequence.expires = GetTime() + (castTimeMs / 1000) - 2
|
|
end
|
|
end
|
|
end
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
-- Nampower SPELL_FAILED_SELF handler (v2.25+)
|
|
-- Clears spell_tracking on failure (Nampower fallback for UNIT_CASTEVENT FAIL)
|
|
function CleveRoids.Frame:SPELL_FAILED_SELF(casterGuid, targetGuid, spellId, ...)
|
|
-- Skip if SuperWoW is handling this via UNIT_CASTEVENT
|
|
if CleveRoids.hasSuperwow then return end
|
|
if not spellId then return end
|
|
|
|
local playerGuid = CleveRoids.playerGuid or CleveRoids.GetGUID("player")
|
|
if casterGuid ~= playerGuid then return end
|
|
|
|
-- Clear spell_tracking
|
|
local cast = CleveRoids.spell_tracking[casterGuid]
|
|
if cast and cast.spell_id == spellId then
|
|
CleveRoids.spell_tracking[casterGuid] = nil
|
|
CleveRoids.spell_tracking["player"] = nil
|
|
end
|
|
|
|
-- Cast sequence failure handling
|
|
if CleveRoids.currentSequence then
|
|
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
|
|
if active and active.action then
|
|
local name = GetSpellRecField(spellId, "name")
|
|
local isSeqSpell = name and (active.action == name)
|
|
if isSeqSpell then
|
|
CleveRoids.currentSequence.status = 1 -- Reset to retry
|
|
end
|
|
end
|
|
end
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
|
|
-- Set channel state immediately when event fires
|
|
-- Duration is captured by UNIT_CASTEVENT which fires earlier
|
|
CleveRoids.CurrentSpell.type = "channeled"
|
|
|
|
-- Try to get spell info - prefer GetCastInfo (Nampower 2.18+) for better data
|
|
local spellId = nil
|
|
if GetCastInfo then
|
|
local ok, info = pcall(GetCastInfo)
|
|
if ok and info and info.spellId and info.spellId > 0 then
|
|
spellId = info.spellId
|
|
-- Also capture timing data
|
|
CleveRoids.CurrentSpell.castRemainingMs = info.castRemainingMs
|
|
CleveRoids.CurrentSpell.castEndTime = info.castEndS
|
|
end
|
|
end
|
|
-- Fallback to GetCurrentCastingInfo for older Nampower
|
|
if not spellId and GetCurrentCastingInfo then
|
|
local _, visId = GetCurrentCastingInfo()
|
|
if visId and visId > 0 then
|
|
spellId = visId
|
|
end
|
|
end
|
|
-- Update spell info
|
|
if spellId then
|
|
CleveRoids.CurrentSpell.castingSpellId = spellId
|
|
local spellName = GetSpellRecField(spellId, "name")
|
|
if spellName then
|
|
CleveRoids.CurrentSpell.spellName = spellName
|
|
end
|
|
end
|
|
|
|
-- v2.38+: Override channelDuration with the accurate server-provided value stored by
|
|
-- SPELL_START_SELF (which fires before this event). This beats the tooltip scan or
|
|
-- UNIT_CASTEVENT cast_time fallback, because the server already applied all modifiers.
|
|
if CleveRoids._v238ChannelDuration then
|
|
if not spellId or not CleveRoids._v238ChannelSpellId
|
|
or CleveRoids._v238ChannelSpellId == spellId then
|
|
CleveRoids.channelDuration = CleveRoids._v238ChannelDuration
|
|
end
|
|
CleveRoids._v238ChannelDuration = nil
|
|
CleveRoids._v238ChannelSpellId = nil
|
|
end
|
|
|
|
-- Force immediate action update
|
|
CleveRoids.TestForAllActiveActions()
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELLCAST_CHANNEL_STOP()
|
|
-- Channel ended - clear state immediately
|
|
CleveRoids.CurrentSpell.type = ""
|
|
CleveRoids.CurrentSpell.spellName = ""
|
|
CleveRoids.CurrentSpell.castingSpellId = nil
|
|
|
|
-- WARLOCK DARK HARVEST: Mark channeling as ended
|
|
-- Credits: Avitasia / Cursive addon
|
|
if CleveRoids.darkHarvestData and CleveRoids.darkHarvestData.isActive then
|
|
CleveRoids.darkHarvestData.isActive = false
|
|
CleveRoids.darkHarvestData.endTime = GetTime()
|
|
|
|
-- Apply Dark Harvest end to all DoTs on target (finalizes reduction)
|
|
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyDarkHarvestEnd then
|
|
CleveRoids.libdebuff.ApplyDarkHarvestEnd(CleveRoids.darkHarvestData.targetGUID)
|
|
end
|
|
|
|
if CleveRoids.debug then
|
|
local activeTime = CleveRoids.darkHarvestData.endTime - CleveRoids.darkHarvestData.startTime
|
|
DEFAULT_CHAT_FRAME:AddMessage(
|
|
string.format("|cff9482c9[Dark Harvest]|r Channel ended after %.1fs (DoT acceleration stopped)",
|
|
activeTime)
|
|
)
|
|
end
|
|
end
|
|
|
|
-- Force immediate action update
|
|
CleveRoids.TestForAllActiveActions()
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELLCAST_START()
|
|
-- Cast-time spell started
|
|
-- Duration is captured by UNIT_CASTEVENT which fires earlier
|
|
CleveRoids.CurrentSpell.type = "cast"
|
|
|
|
-- Try to get spell info - prefer GetCastInfo (Nampower 2.18+) for better data
|
|
local spellId = nil
|
|
if GetCastInfo then
|
|
local ok, info = pcall(GetCastInfo)
|
|
if ok and info and info.spellId and info.spellId > 0 then
|
|
spellId = info.spellId
|
|
-- Also capture timing data
|
|
CleveRoids.CurrentSpell.castRemainingMs = info.castRemainingMs
|
|
CleveRoids.CurrentSpell.castEndTime = info.castEndS
|
|
CleveRoids.CurrentSpell.gcdRemainingMs = info.gcdRemainingMs
|
|
CleveRoids.CurrentSpell.gcdEndTime = info.gcdEndS
|
|
end
|
|
end
|
|
-- Fallback to GetCurrentCastingInfo for older Nampower
|
|
if not spellId and GetCurrentCastingInfo then
|
|
local castId = GetCurrentCastingInfo()
|
|
if castId and castId > 0 then
|
|
spellId = castId
|
|
end
|
|
end
|
|
-- Update spell info
|
|
if spellId then
|
|
CleveRoids.CurrentSpell.castingSpellId = spellId
|
|
local spellName = GetSpellRecField(spellId, "name")
|
|
if spellName then
|
|
CleveRoids.CurrentSpell.spellName = spellName
|
|
end
|
|
end
|
|
|
|
-- Force immediate action update
|
|
CleveRoids.TestForAllActiveActions()
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELLCAST_STOP()
|
|
-- Cast finished - clear state immediately
|
|
if CleveRoids.CurrentSpell.type == "cast" then
|
|
CleveRoids.CurrentSpell.type = ""
|
|
CleveRoids.CurrentSpell.spellName = ""
|
|
CleveRoids.CurrentSpell.castingSpellId = nil
|
|
|
|
-- Force immediate action update
|
|
CleveRoids.TestForAllActiveActions()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELLCAST_FAILED()
|
|
-- Cast failed - clear state immediately
|
|
if CleveRoids.CurrentSpell.type == "cast" then
|
|
CleveRoids.CurrentSpell.type = ""
|
|
CleveRoids.CurrentSpell.spellName = ""
|
|
CleveRoids.CurrentSpell.castingSpellId = nil
|
|
|
|
-- Force immediate action update
|
|
CleveRoids.TestForAllActiveActions()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELLCAST_INTERRUPTED()
|
|
-- Cast interrupted - clear state immediately
|
|
if CleveRoids.CurrentSpell.type == "cast" then
|
|
CleveRoids.CurrentSpell.type = ""
|
|
CleveRoids.CurrentSpell.spellName = ""
|
|
CleveRoids.CurrentSpell.castingSpellId = nil
|
|
|
|
-- Force immediate action update
|
|
CleveRoids.TestForAllActiveActions()
|
|
end
|
|
end
|
|
|
|
-- PLAYER_ENTER_COMBAT/PLAYER_LEAVE_COMBAT are for AUTO-ATTACK state (not actual combat)
|
|
function CleveRoids.Frame:PLAYER_ENTER_COMBAT()
|
|
CleveRoids.CurrentSpell.autoAttack = true
|
|
CleveRoids.CurrentSpell.autoAttackLock = false
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:PLAYER_LEAVE_COMBAT()
|
|
CleveRoids.CurrentSpell.autoAttack = false
|
|
CleveRoids.CurrentSpell.autoAttackLock = false
|
|
|
|
-- Full re-index after combat to refresh any items/bags that were skipped
|
|
-- during combat for performance. Use a slight delay to avoid spam.
|
|
local now = GetTime()
|
|
if (now - (CleveRoids.lastItemIndexTime or 0)) > 0.5 then
|
|
CleveRoids.lastItemIndexTime = now
|
|
CleveRoids.IndexItems()
|
|
CleveRoids.Actions = {}
|
|
CleveRoids.Macros = {}
|
|
CleveRoids.IndexActionBars()
|
|
end
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
-- PLAYER_REGEN_DISABLED/PLAYER_REGEN_ENABLED are for ACTUAL combat state (threat/aggro)
|
|
-- These events track what UnitAffectingCombat("player") reports
|
|
function CleveRoids.Frame:PLAYER_REGEN_DISABLED()
|
|
-- PERFORMANCE: Cache actual combat state for event-driven [combat]/[nocombat] conditionals
|
|
CleveRoids._cachedPlayerInCombat = true
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:PLAYER_REGEN_ENABLED()
|
|
-- PERFORMANCE: Cache actual combat state for event-driven [combat]/[nocombat] conditionals
|
|
CleveRoids._cachedPlayerInCombat = false
|
|
|
|
-- Reset any sequence with reset=combat that has progressed past the first step
|
|
-- Uses PLAYER_REGEN_ENABLED (actual combat ends) instead of PLAYER_LEAVE_COMBAT (auto-attack stops)
|
|
for _, sequence in pairs(CleveRoids.Sequences) do
|
|
if sequence.index > 1 and sequence.reset and sequence.reset.combat then
|
|
CleveRoids.ResetSequence(sequence)
|
|
end
|
|
end
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
-- Movement -> [moving]/[nomoving] icon refresh.
|
|
-- PLAYER_STARTED_MOVING is reliable; PLAYER_STOPPED_MOVING is not (key-release
|
|
-- based, misses geometry/click-to-move/root/knockback stops). So STARTED flips
|
|
-- the icon to "moving" and starts a bounded poll; the poll detects the real
|
|
-- stop from IsPlayerMoving() (the signal [moving] uses), refreshes once, and
|
|
-- shuts itself off. Only runs while actually moving, so no idle cost.
|
|
local MOVE_POLL_INTERVAL = 0.1
|
|
local moveTicker = nil
|
|
|
|
local function StopMovePoll()
|
|
if moveTicker then
|
|
moveTicker:Cancel()
|
|
moveTicker = nil
|
|
end
|
|
end
|
|
|
|
local function StartMovePoll()
|
|
if moveTicker then return end -- already polling this movement
|
|
moveTicker = C_Timer.NewTicker(MOVE_POLL_INTERVAL, function()
|
|
if CleveRoids.isShuttingDown then
|
|
StopMovePoll()
|
|
return
|
|
end
|
|
if not CleveRoids.IsPlayerMoving() then
|
|
-- Movement actually ended -- repaint [moving]/[nomoving] icons, stop polling.
|
|
StopMovePoll()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
function CleveRoids.Frame:PLAYER_STARTED_MOVING()
|
|
-- Started moving: flip to the [moving] icon now...
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
-- ...and watch for the (unreliable-event) stop ourselves.
|
|
StartMovePoll()
|
|
end
|
|
|
|
function CleveRoids.Frame:PLAYER_TARGET_CHANGED()
|
|
CleveRoids.CurrentSpell.autoAttack = false
|
|
CleveRoids.CurrentSpell.autoAttackLock = false
|
|
|
|
-- Clear resist state when target changes
|
|
CleveRoids.ClearResistState()
|
|
|
|
-- Handle sequences with reset=target - save/restore per-target state
|
|
-- Instead of resetting, we remember each target's progress in the sequence
|
|
local currentGuid = nil
|
|
if UnitExists("target") then
|
|
currentGuid = CleveRoids.GetGUID("target")
|
|
end
|
|
|
|
for _, sequence in pairs(CleveRoids.Sequences) do
|
|
if sequence.reset and sequence.reset.target then
|
|
local oldGuid = sequence.lastTargetGuid
|
|
|
|
-- Only process if target actually changed
|
|
if oldGuid ~= currentGuid then
|
|
-- Save current state for the old target (if we had one and progressed)
|
|
if oldGuid and sequence.index > 1 then
|
|
sequence.perTargetState = sequence.perTargetState or {}
|
|
sequence.perTargetState[oldGuid] = {
|
|
index = sequence.index,
|
|
lastUpdate = sequence.lastUpdate,
|
|
}
|
|
end
|
|
|
|
-- Restore state for the new target (or start fresh)
|
|
if currentGuid and sequence.perTargetState and sequence.perTargetState[currentGuid] then
|
|
-- Restore saved state for this target
|
|
local saved = sequence.perTargetState[currentGuid]
|
|
sequence.index = saved.index
|
|
sequence.lastUpdate = saved.lastUpdate
|
|
else
|
|
-- New target we haven't seen - start at step 1
|
|
sequence.index = 1
|
|
sequence.status = 0
|
|
end
|
|
|
|
-- Update the current target GUID
|
|
sequence.lastTargetGuid = currentGuid
|
|
end
|
|
end
|
|
end
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:UPDATE_MACROS()
|
|
CleveRoids.currentSequence = nil
|
|
-- Explicitly nil tables before re-assignment
|
|
CleveRoids.ParsedMsg = nil;
|
|
CleveRoids.ParsedMsg = {}
|
|
|
|
CleveRoids.Macros = nil;
|
|
CleveRoids.Macros = {}
|
|
|
|
CleveRoids.Actions = nil;
|
|
CleveRoids.Actions = {}
|
|
|
|
CleveRoids.Sequences = nil;
|
|
CleveRoids.Sequences = {}
|
|
|
|
CleveRoids.IndexSpells()
|
|
CleveRoids.IndexTalents()
|
|
CleveRoids.IndexPetSpells()
|
|
CleveRoids.IndexActionBars()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELLS_CHANGED()
|
|
-- PERFORMANCE: Clear spell caches when spells change (learn new ranks, etc.)
|
|
CleveRoids.spellIdCache = {}
|
|
CleveRoids.spellNameCache = {}
|
|
CleveRoids.Frame:UPDATE_MACROS()
|
|
end
|
|
|
|
function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
|
|
CleveRoids.ClearAction(arg1)
|
|
CleveRoids.IndexActionSlot(arg1)
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:BAG_UPDATE()
|
|
-- In combat: Skip expensive indexing but still queue icon update
|
|
-- so conditionals like [inbag] re-evaluate (they use live bag APIs)
|
|
if UnitAffectingCombat("player") then
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Out of combat: Full indexing with throttle
|
|
local now = GetTime()
|
|
if (now - (CleveRoids.lastItemIndexTime or 0)) > 1.0 then
|
|
CleveRoids.lastItemIndexTime = now
|
|
CleveRoids.IndexItems()
|
|
|
|
-- Directly clear all relevant caches and force a UI refresh for all buttons.
|
|
CleveRoids.Actions = {}
|
|
CleveRoids.Macros = {}
|
|
CleveRoids.IndexActionBars()
|
|
end
|
|
|
|
-- Always queue icon update so conditionals like [inbag] re-evaluate
|
|
-- even when full indexing is throttled (they use live bag APIs)
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:UNIT_INVENTORY_CHANGED()
|
|
if arg1 ~= "player" then return end
|
|
|
|
-- PERFORMANCE: Invalidate equipment cache for HasGearEquipped
|
|
if CleveRoids.InvalidateEquipmentCache then
|
|
CleveRoids.InvalidateEquipmentCache()
|
|
end
|
|
|
|
-- In combat: Skip ALL processing - EquipBagItem already handles cache invalidation
|
|
-- This eliminates lag from IndexEquippedItems during rapid gear swapping
|
|
if UnitAffectingCombat("player") then
|
|
return
|
|
end
|
|
|
|
-- Out of combat: Full indexing with throttle
|
|
local now = GetTime()
|
|
if (now - (CleveRoids.lastEquipIndexTime or 0)) < 0.2 then
|
|
CleveRoids.equipIndexPendingTime = now
|
|
return
|
|
end
|
|
|
|
CleveRoids.lastEquipIndexTime = now
|
|
CleveRoids.equipIndexPendingTime = nil
|
|
CleveRoids.lastItemIndexTime = now
|
|
CleveRoids.IndexItems()
|
|
CleveRoids.Actions = {}
|
|
CleveRoids.Macros = {}
|
|
CleveRoids.IndexActionBars()
|
|
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:START_AUTOREPEAT_SPELL()
|
|
local spells = CleveRoids.Spells[BOOKTYPE_SPELL]
|
|
if spells and spells[CleveRoids.Localized.AutoShot] then
|
|
CleveRoids.CurrentSpell.autoShot = true
|
|
else
|
|
CleveRoids.CurrentSpell.wand = true
|
|
end
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:STOP_AUTOREPEAT_SPELL()
|
|
local spells = CleveRoids.Spells[BOOKTYPE_SPELL]
|
|
if spells and spells[CleveRoids.Localized.AutoShot] then
|
|
CleveRoids.CurrentSpell.autoShot = false
|
|
else
|
|
CleveRoids.CurrentSpell.wand = false
|
|
end
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
|
|
-- Generic event handlers that just queue an update
|
|
function CleveRoids.Frame:PLAYER_FOCUS_CHANGED()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
function CleveRoids.Frame:UPDATE_SHAPESHIFT_FORM()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
function CleveRoids.Frame:SPELL_UPDATE_COOLDOWN()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
|
|
-- COOLDOWN FIX: Explicitly update cooldowns on all managed action buttons
|
|
-- This ensures cooldowns display correctly even when the active action hasn't changed
|
|
CleveRoids.UpdateAllManagedCooldowns()
|
|
end
|
|
|
|
-- Helper function to notify addon handlers about cooldown updates
|
|
-- NOTE: Blizzard action buttons are handled natively via ActionButton_OnEvent which calls
|
|
-- our GetActionCooldown hook. We do NOT call CooldownFrame_SetTimer directly to avoid
|
|
-- conflicts with SuperWoW's own GetActionCooldown handling for #showtooltip macros.
|
|
-- Track which slots have been logged to avoid spam
|
|
CleveRoids.cooldownDebugLogged = {}
|
|
|
|
function CleveRoids.UpdateAllManagedCooldowns()
|
|
local Actions = CleveRoids.Actions
|
|
if not Actions then return end
|
|
|
|
-- Only notify third-party handlers (pfUI/Bongos)
|
|
-- Blizzard buttons update themselves via GetActionCooldown hook
|
|
local handlerCount = CleveRoids.actionEventHandlers and table.getn(CleveRoids.actionEventHandlers) or 0
|
|
if handlerCount == 0 then return end
|
|
|
|
for slot, actions in pairs(Actions) do
|
|
if actions then
|
|
-- Notify action event handlers (for pfUI/Bongos) about the cooldown update
|
|
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do
|
|
fn_h(slot, "ACTIONBAR_UPDATE_COOLDOWN")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- PERFORMANCE OPTIMIZATION: Throttled event handlers to reduce CPU spam
|
|
-- UNIT_AURA can fire dozens of times per second during combat
|
|
-- 100ms throttle reduces calls by 90%+ while maintaining responsiveness
|
|
function CleveRoids.Frame:UNIT_AURA()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
local now = GetTime()
|
|
if (now - CleveRoids.lastUnitAuraUpdate) >= CleveRoids.EVENT_THROTTLE then
|
|
CleveRoids.lastUnitAuraUpdate = now
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
function CleveRoids.Frame:UNIT_HEALTH()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
local now = GetTime()
|
|
if (now - CleveRoids.lastUnitHealthUpdate) >= CleveRoids.EVENT_THROTTLE then
|
|
CleveRoids.lastUnitHealthUpdate = now
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
function CleveRoids.Frame:UNIT_POWER()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
local now = GetTime()
|
|
if (now - CleveRoids.lastUnitPowerUpdate) >= CleveRoids.EVENT_THROTTLE then
|
|
CleveRoids.lastUnitPowerUpdate = now
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- GUID event handlers (v2.39+): fire once per unit state change instead of per-token
|
|
function CleveRoids.Frame:UNIT_AURA_GUID()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
local now = GetTime()
|
|
if (now - CleveRoids.lastUnitAuraUpdate) >= CleveRoids.EVENT_THROTTLE then
|
|
CleveRoids.lastUnitAuraUpdate = now
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
function CleveRoids.Frame:UNIT_HEALTH_GUID()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
local now = GetTime()
|
|
if (now - CleveRoids.lastUnitHealthUpdate) >= CleveRoids.EVENT_THROTTLE then
|
|
CleveRoids.lastUnitHealthUpdate = now
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
-- All power GUID events share the same handler
|
|
local function OnPowerGuidEvent()
|
|
if CleveRoidMacros.realtime == 0 then
|
|
local now = GetTime()
|
|
if (now - CleveRoids.lastUnitPowerUpdate) >= CleveRoids.EVENT_THROTTLE then
|
|
CleveRoids.lastUnitPowerUpdate = now
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
CleveRoids.Frame.UNIT_MANA_GUID = OnPowerGuidEvent
|
|
CleveRoids.Frame.UNIT_RAGE_GUID = OnPowerGuidEvent
|
|
CleveRoids.Frame.UNIT_ENERGY_GUID = OnPowerGuidEvent
|
|
|
|
function CleveRoids.Frame:SPELL_QUEUE_EVENT()
|
|
if event == "SPELL_QUEUE_EVENT" then
|
|
local eventCode = arg1
|
|
local spellId = arg2
|
|
|
|
local NORMAL_QUEUED = 2
|
|
local NON_GCD_QUEUED = 4
|
|
local ON_SWING_QUEUED = 0
|
|
local NORMAL_QUEUE_POPPED = 3
|
|
local NON_GCD_QUEUE_POPPED = 5
|
|
local ON_SWING_QUEUE_POPPED = 1
|
|
|
|
if eventCode == NORMAL_QUEUED or eventCode == NON_GCD_QUEUED or eventCode == ON_SWING_QUEUED then
|
|
CleveRoids.queuedSpell = {
|
|
spellId = spellId,
|
|
queueType = eventCode,
|
|
queueTime = GetTime()
|
|
}
|
|
if GetSpellRecField then
|
|
local name = GetSpellRecField(spellId, "name")
|
|
if name then
|
|
CleveRoids.queuedSpell.spellName = name
|
|
end
|
|
end
|
|
-- BUGFIX: Update casting state when spell is queued (for [casting] conditional)
|
|
if CleveRoids.UpdateCastingState then
|
|
CleveRoids.UpdateCastingState()
|
|
end
|
|
CleveRoids.QueueActionUpdate()
|
|
elseif eventCode == NORMAL_QUEUE_POPPED or eventCode == NON_GCD_QUEUE_POPPED or eventCode == ON_SWING_QUEUE_POPPED then
|
|
CleveRoids.queuedSpell = nil
|
|
-- BUGFIX: Update casting state when spell queue pops (for [casting] conditional)
|
|
if CleveRoids.UpdateCastingState then
|
|
CleveRoids.UpdateCastingState()
|
|
end
|
|
CleveRoids.QueueActionUpdate()
|
|
end
|
|
end
|
|
end
|
|
|
|
function CleveRoids.Frame:SPELL_CAST_EVENT()
|
|
if event == "SPELL_CAST_EVENT" then
|
|
local success = arg1
|
|
local spellId = arg2
|
|
local castType = arg3
|
|
local targetGuid = arg4
|
|
|
|
-- BUGFIX: Update casting state on spell cast events (for [casting] conditional)
|
|
if CleveRoids.UpdateCastingState then
|
|
CleveRoids.UpdateCastingState()
|
|
end
|
|
|
|
if success == 1 then
|
|
CleveRoids.lastCastSpell = {
|
|
spellId = spellId,
|
|
castType = castType,
|
|
targetGuid = targetGuid,
|
|
timestamp = GetTime()
|
|
}
|
|
if GetSpellRecField then
|
|
local name = GetSpellRecField(spellId, "name")
|
|
if name then
|
|
CleveRoids.lastCastSpell.spellName = name
|
|
end
|
|
end
|
|
|
|
-- Track pending cast for SPELL_GO correlation (reactive ability detection)
|
|
-- Keyed by spellId so concurrent casts don't overwrite each other
|
|
CleveRoids.pendingCasts = CleveRoids.pendingCasts or {}
|
|
|
|
-- Clean up consumed entries older than 5 seconds (lightweight, runs per-cast)
|
|
local cleanupTime = GetTime() - 5
|
|
for id, entry in pairs(CleveRoids.pendingCasts) do
|
|
if entry.consumed and entry.consumedAt and entry.consumedAt < cleanupTime then
|
|
CleveRoids.pendingCasts[id] = nil
|
|
end
|
|
end
|
|
|
|
CleveRoids.pendingCasts[spellId] = {
|
|
castType = castType,
|
|
targetGuid = targetGuid,
|
|
timestamp = GetTime(),
|
|
comboPoints = nil,
|
|
}
|
|
|
|
-- Capture combo points NOW (before server consumes them)
|
|
-- SPELL_CAST_EVENT fires client-side, so CP are guaranteed available
|
|
if (CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellId)) or
|
|
(CleveRoids.FerociousBiteSpellIDs and CleveRoids.FerociousBiteSpellIDs[spellId]) then
|
|
local cp = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0
|
|
if cp > 0 then
|
|
CleveRoids.pendingCasts[spellId].comboPoints = cp
|
|
if CleveRoids.debug then
|
|
local castSpellName = GetSpellRecField and GetSpellRecField(spellId, "name") or "Unknown"
|
|
DEFAULT_CHAT_FRAME:AddMessage(
|
|
string.format("|cff00ff88[SPELL_CAST_EVENT]|r Captured %d CP for %s (ID:%d)",
|
|
cp, castSpellName, spellId)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- Cast failed - clear pending entry for this spell
|
|
if CleveRoids.pendingCasts then
|
|
CleveRoids.pendingCasts[spellId] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Nampower v2.41+: KEY_DOWN/KEY_UP events
|
|
-- arg1=keyCode (int), arg2=metaKeyState (Shift=1,Ctrl=2,Alt=4), arg3=repeat, arg4=time
|
|
-- Meta key codes (0=Shift, 1=Ctrl, 2=Alt) are skipped — they are already tracked via IsXKeyDown()
|
|
function CleveRoids.Frame:KEY_DOWN()
|
|
local keyCode = arg1
|
|
if keyCode == 0 or keyCode == 1 or keyCode == 2 then return end
|
|
CleveRoids._keyState[keyCode] = true
|
|
CleveRoids.isActionUpdateQueued = true
|
|
end
|
|
|
|
function CleveRoids.Frame:KEY_UP()
|
|
local keyCode = arg1
|
|
if keyCode == 0 or keyCode == 1 or keyCode == 2 then return end
|
|
CleveRoids._keyState[keyCode] = nil
|
|
CleveRoids.isActionUpdateQueued = true
|
|
end
|
|
|
|
CleveRoids.Hooks.SendChatMessage = SendChatMessage
|
|
function SendChatMessage(msg, ...)
|
|
-- Filter out #showtooltip lines
|
|
-- pfUI's macrotweak also does this, but our pattern is more specific
|
|
if msg and string.find(msg, "^#showtooltip") then
|
|
return
|
|
end
|
|
|
|
-- Call the original (or pfUI's hook if it's in the chain)
|
|
CleveRoids.Hooks.SendChatMessage(msg, unpack(arg))
|
|
end
|
|
|
|
CleveRoids.RegisterActionEventHandler = function(fn)
|
|
if type(fn) == "function" then
|
|
table.insert(CleveRoids.actionEventHandlers, fn)
|
|
end
|
|
end
|
|
|
|
CleveRoids.RegisterMouseOverResolver = function(fn)
|
|
if type(fn) == "function" then
|
|
table.insert(CleveRoids.mouseOverResolvers, fn)
|
|
end
|
|
end
|
|
|
|
CleverMacro = true
|
|
|
|
do
|
|
local f = CreateFrame("Frame")
|
|
f:SetScript("OnEvent", function(self, event, arg1)
|
|
if event == "PLAYER_LOGIN" then
|
|
-- This ensures we wait until the player is fully in the world and all addons are loaded.
|
|
self:UnregisterEvent("PLAYER_LOGIN")
|
|
|
|
-- Ensure both pfUI and its focus module are loaded before attempting to hook.
|
|
-- This also checks that the slash command we want to modify exists.
|
|
if pfUI and pfUI.uf and pfUI.uf.focus and SlashCmdList.PFFOCUS then
|
|
|
|
local original_PFFOCUS_Handler = SlashCmdList.PFFOCUS
|
|
SlashCmdList.PFFOCUS = function(msg)
|
|
-- First, execute the original /focus command from pfUI to set the unit name.
|
|
original_PFFOCUS_Handler(msg)
|
|
|
|
-- Now, if a focus name was set, find the corresponding UnitID.
|
|
if pfUI.uf.focus.unitname then
|
|
local focusName = pfUI.uf.focus.unitname
|
|
local found_label, found_id = nil, nil
|
|
|
|
-- This function iterates through all known friendly units to find a
|
|
-- name match and return its specific UnitID components.
|
|
local function findUnitID()
|
|
-- Check party members and their pets
|
|
for i = 1, GetNumPartyMembers() do
|
|
if strlower(UnitName("party"..i) or "") == focusName then
|
|
return "party", i
|
|
end
|
|
if UnitExists("partypet"..i) and strlower(UnitName("partypet"..i) or "") == focusName then
|
|
return "partypet", i
|
|
end
|
|
end
|
|
|
|
-- Check raid members and their pets
|
|
for i = 1, GetNumRaidMembers() do
|
|
if strlower(UnitName("raid"..i) or "") == focusName then
|
|
return "raid", i
|
|
end
|
|
if UnitExists("raidpet"..i) and strlower(UnitName("raidpet"..i) or "") == focusName then
|
|
return "raidpet", i
|
|
end
|
|
end
|
|
|
|
-- Check player and pet
|
|
if strlower(UnitName("player") or "") == focusName then return "player", nil end
|
|
if UnitExists("pet") and strlower(UnitName("pet") or "") == focusName then return "pet", nil end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
found_label, found_id = findUnitID()
|
|
|
|
-- Store the found label and ID. CleveRoids' Core.lua will use this
|
|
-- for a direct, reliable cast without needing to change your target.
|
|
pfUI.uf.focus.label = found_label
|
|
pfUI.uf.focus.id = found_id
|
|
else
|
|
-- Focus was cleared (e.g., via /clearfocus), so ensure our cached data is cleared too.
|
|
pfUI.uf.focus.label = nil
|
|
pfUI.uf.focus.id = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
f:RegisterEvent("PLAYER_LOGIN")
|
|
end
|
|
|
|
SLASH_CLEVEROID1 = "/cleveroid"
|
|
SLASH_CLEVEROID2 = "/cleveroidmacros"
|
|
SlashCmdList["CLEVEROID"] = function(msg)
|
|
if type(msg) ~= "string" then
|
|
msg = ""
|
|
end
|
|
local cmd, val, val2
|
|
local s, e, a, b, c = string.find(msg, "^(%S*)%s*(%S*)%s*(.*)$")
|
|
if a then cmd = a else cmd = "" end
|
|
if b then val = b else val = "" end
|
|
if c then val2 = c else val2 = "" end
|
|
|
|
-- No command: show current value and available commands
|
|
if cmd == "" then
|
|
CleveRoids.Print("Current Settings:")
|
|
DEFAULT_CHAT_FRAME:AddMessage("realtime (force fast updates, CPU intensive) = " .. CleveRoidMacros.realtime .. " (Default: 0)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("refresh (updates per second) = " .. CleveRoidMacros.refresh .. " (Default: 5)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("macrocheck (syntax checker) = " .. CleveRoidMacros.macrocheck .. " (Default: 1)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("debug (show learning messages) = " .. (CleveRoids.debug and "1" or "0") .. " (Default: 0)")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" ")
|
|
CleveRoids.Print("Available Commands:")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1-10 updates/sec)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages")
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Spell Schools:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listschools - List all learned spell schools')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearschools - Clear learned spell school data')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Immunity Tracking:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listimmune [school] - List immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "<NPC>" <school> [buff] - Add immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "<NPC>" <school> - Remove immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00CC Immunity Tracking:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listccimmune [type] - List CC immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addccimmune "<NPC>" <type> [buff] - Add CC immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeccimmune "<NPC>" <type> - Remove CC immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearccimmune [type] - Clear CC immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage(" CC types: stun, fear, root, silence, sleep, charm, polymorph, banish, horror, disorient, snare")
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Talent Modifiers:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid talenttabs - Show talent tab IDs for your class')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listtab <tab> - List all talents in a tab with their IDs')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid talents - Show current talent ranks')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testtalent <spellID> - Test talent modifier for a spell')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Debuff Tracking Debug:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid debuffdebug [spell] - Debug debuff tracking on target')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00pfUI Tank Integration:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid tankdebug - Debug tank targeting conditionals')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00All-Caster Aura Tracking:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid auradebug - Debug buff/debuff time tracking from all casters')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Overflow Buff Frame:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowframe [on|off] - Enable/disable player overflow buff frame (default: off)')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowtest - Toggle overflow buff frame test mode')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowreset - Reset overflow frame positions to defaults')
|
|
return
|
|
end
|
|
|
|
-- realtime
|
|
if cmd == "realtime" then
|
|
local num = tonumber(val)
|
|
if num == 0 or num == 1 then
|
|
CleveRoidMacros.realtime = num
|
|
CleveRoids.Print("realtime set to " .. num)
|
|
else
|
|
CleveRoids.Print("Usage: /cleveroid realtime 0 or 1 - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load.)")
|
|
CleveRoids.Print("Current realtime = " .. tostring(CleveRoidMacros.realtime))
|
|
end
|
|
return
|
|
end
|
|
|
|
-- refresh
|
|
if cmd == "refresh" then
|
|
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)")
|
|
CleveRoids.Print("Current refresh = " .. tostring(CleveRoidMacros.refresh) .. " times per second")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- macrocheck
|
|
if cmd == "macrocheck" then
|
|
local num = tonumber(val)
|
|
if num == 0 or num == 1 then
|
|
CleveRoidMacros.macrocheck = num
|
|
if num == 1 then
|
|
CleveRoids.Print("Macro syntax checker |cff00ff00enabled|r")
|
|
else
|
|
CleveRoids.Print("Macro syntax checker |cffff0000disabled|r (reload UI to take effect)")
|
|
end
|
|
else
|
|
CleveRoids.Print("Usage: /cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker (Default: 1)")
|
|
CleveRoids.Print("Current macrocheck = " .. tostring(CleveRoidMacros.macrocheck))
|
|
end
|
|
return
|
|
end
|
|
|
|
-- learn (manual set duration)
|
|
if cmd == "learn" then
|
|
local spellID = tonumber(val)
|
|
local duration = tonumber(val2)
|
|
if spellID and duration then
|
|
local playerGUID = CleveRoids.GetGUID("player")
|
|
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
|
|
CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {}
|
|
CleveRoids_LearnedDurations[spellID][playerGUID] = duration
|
|
local spellName = GetSpellRecField(spellID, "name") or "Unknown"
|
|
CleveRoids.Print("Set " .. spellName .. " (ID:" .. spellID .. ") duration to " .. duration .. "s")
|
|
else
|
|
CleveRoids.Print("Usage: /cleveroid learn <spellID> <duration> - Manually set spell duration")
|
|
CleveRoids.Print("Example: /cleveroid learn 11597 30")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- forget (delete learned duration)
|
|
if cmd == "forget" or cmd == "unlearn" then
|
|
if val == "all" then
|
|
CleveRoids_LearnedDurations = {}
|
|
CleveRoids.Print("Forgot all learned spell durations")
|
|
else
|
|
local spellID = tonumber(val)
|
|
if spellID and CleveRoids_LearnedDurations and CleveRoids_LearnedDurations[spellID] then
|
|
local spellName = GetSpellRecField(spellID, "name") or "Unknown"
|
|
CleveRoids_LearnedDurations[spellID] = nil
|
|
CleveRoids.Print("Forgot " .. spellName .. " (ID:" .. spellID .. ") duration")
|
|
elseif spellID then
|
|
CleveRoids.Print("No learned duration for spell ID " .. spellID)
|
|
else
|
|
CleveRoids.Print("Usage: /cleveroid forget <spellID> - Forget learned duration")
|
|
CleveRoids.Print(" /cleveroid forget all - Forget all learned durations")
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- debug (toggle learning messages)
|
|
if cmd == "debug" then
|
|
local num = tonumber(val)
|
|
if num == 0 or num == 1 then
|
|
CleveRoids.debug = (num == 1)
|
|
CleveRoids.Print("debug set to " .. num)
|
|
else
|
|
CleveRoids.debug = not CleveRoids.debug
|
|
CleveRoids.Print("debug " .. (CleveRoids.debug and "enabled" or "disabled"))
|
|
end
|
|
-- Clear keyed debug state so messages re-fire when debug is re-enabled
|
|
CleveRoids._lastDebugState = {}
|
|
return
|
|
end
|
|
|
|
-- listimmune (list immunity data)
|
|
if cmd == "listimmune" or cmd == "immunelist" then
|
|
CleveRoids.ListImmunities(val ~= "" and val or nil)
|
|
return
|
|
end
|
|
|
|
-- clearimmune (clear immunity data)
|
|
if cmd == "clearimmune" then
|
|
CleveRoids.ClearImmunities(val ~= "" and val or nil)
|
|
return
|
|
end
|
|
|
|
-- addimmune (manually add immunity)
|
|
if cmd == "addimmune" then
|
|
-- Parse: /cleveroid addimmune <NPC Name> <school> [buff]
|
|
-- Example: /cleveroid addimmune "Golemagg the Incinerator" fire
|
|
-- Example: /cleveroid addimmune Vaelastrasz fire "Burning Adrenaline"
|
|
local npcName, school, buffName = nil, nil, nil
|
|
|
|
-- Try to extract quoted NPC name
|
|
local _, _, quotedNpc, rest = string.find(msg, '^addimmune%s+"([^"]+)"%s*(.*)$')
|
|
if quotedNpc then
|
|
npcName = quotedNpc
|
|
-- Parse school and optional buff from rest
|
|
local _, _, sch, buff = string.find(rest, "^(%S+)%s*(.*)$")
|
|
school = sch
|
|
if buff and buff ~= "" then
|
|
-- Check if buff is quoted
|
|
local _, _, quotedBuff = string.find(buff, '^"([^"]+)"$')
|
|
buffName = quotedBuff or buff
|
|
end
|
|
else
|
|
-- No quoted NPC, use simple parsing
|
|
npcName = val
|
|
school = val2
|
|
end
|
|
|
|
CleveRoids.AddImmunity(npcName, school, buffName)
|
|
return
|
|
end
|
|
|
|
-- removeimmune (manually remove immunity)
|
|
if cmd == "removeimmune" then
|
|
-- Parse: /cleveroid removeimmune <NPC Name> <school>
|
|
local npcName, school = nil, nil
|
|
|
|
-- Try to extract quoted NPC name
|
|
local _, _, quotedNpc, sch = string.find(msg, '^removeimmune%s+"([^"]+)"%s*(%S*)$')
|
|
if quotedNpc then
|
|
npcName = quotedNpc
|
|
school = sch
|
|
else
|
|
npcName = val
|
|
school = val2
|
|
end
|
|
|
|
CleveRoids.RemoveImmunity(npcName, school)
|
|
return
|
|
end
|
|
|
|
-- ========== CC IMMUNITY COMMANDS ==========
|
|
|
|
-- listccimmune (list CC immunity data)
|
|
if cmd == "listccimmune" or cmd == "ccimmunelist" then
|
|
CleveRoids.ListCCImmunities(val ~= "" and val or nil)
|
|
return
|
|
end
|
|
|
|
-- clearccimmune (clear CC immunity data)
|
|
if cmd == "clearccimmune" then
|
|
CleveRoids.ClearCCImmunities(val ~= "" and val or nil)
|
|
return
|
|
end
|
|
|
|
-- addccimmune (manually add CC immunity)
|
|
if cmd == "addccimmune" then
|
|
-- Parse: /cleveroid addccimmune <NPC Name> <cctype> [buff]
|
|
-- Example: /cleveroid addccimmune "Stone Guardian" stun
|
|
-- Example: /cleveroid addccimmune "Boss Name" fear "Enrage"
|
|
local npcName, ccType, buffName = nil, nil, nil
|
|
|
|
-- Try to extract quoted NPC name
|
|
local _, _, quotedNpc, rest = string.find(msg, '^addccimmune%s+"([^"]+)"%s*(.*)$')
|
|
if quotedNpc then
|
|
npcName = quotedNpc
|
|
-- Parse ccType and optional buff from rest
|
|
local _, _, cct, buff = string.find(rest, "^(%S+)%s*(.*)$")
|
|
ccType = cct
|
|
if buff and buff ~= "" then
|
|
-- Check if buff is quoted
|
|
local _, _, quotedBuff = string.find(buff, '^"([^"]+)"$')
|
|
buffName = quotedBuff or buff
|
|
end
|
|
else
|
|
-- No quoted NPC, use simple parsing
|
|
npcName = val
|
|
ccType = val2
|
|
end
|
|
|
|
CleveRoids.AddCCImmunity(npcName, ccType, buffName)
|
|
return
|
|
end
|
|
|
|
-- removeccimmune (manually remove CC immunity)
|
|
if cmd == "removeccimmune" then
|
|
-- Parse: /cleveroid removeccimmune <NPC Name> <cctype>
|
|
local npcName, ccType = nil, nil
|
|
|
|
-- Try to extract quoted NPC name
|
|
local _, _, quotedNpc, cct = string.find(msg, '^removeccimmune%s+"([^"]+)"%s*(%S*)$')
|
|
if quotedNpc then
|
|
npcName = quotedNpc
|
|
ccType = cct
|
|
else
|
|
npcName = val
|
|
ccType = val2
|
|
end
|
|
|
|
CleveRoids.RemoveCCImmunityCommand(npcName, ccType)
|
|
return
|
|
end
|
|
|
|
-- listschools (show learned spell schools)
|
|
if cmd == "listschools" or cmd == "schools" then
|
|
CleveRoids.Print("|cff88ff88=== Learned Spell Schools ===|r")
|
|
if not CleveRoids_SpellSchools or not next(CleveRoids_SpellSchools) then
|
|
CleveRoids.Print("No spell schools learned yet. Deal damage to enemies!")
|
|
CleveRoids.Print("Nampower damage events will automatically track spell schools.")
|
|
else
|
|
local count = 0
|
|
local bySchool = {}
|
|
|
|
-- Group by school
|
|
for spellID, school in pairs(CleveRoids_SpellSchools) do
|
|
if not bySchool[school] then
|
|
bySchool[school] = {}
|
|
end
|
|
table.insert(bySchool[school], spellID)
|
|
count = count + 1
|
|
end
|
|
|
|
CleveRoids.Print("Total spells tracked: " .. count)
|
|
CleveRoids.Print(" ")
|
|
|
|
-- Display by school
|
|
for school, spellIDs in pairs(bySchool) do
|
|
local schoolColor = "|cff88ff88"
|
|
if school == "fire" then schoolColor = "|cffff4400"
|
|
elseif school == "frost" then schoolColor = "|cff00ffff"
|
|
elseif school == "nature" then schoolColor = "|cff00ff00"
|
|
elseif school == "shadow" then schoolColor = "|cff8800ff"
|
|
elseif school == "arcane" then schoolColor = "|cffff00ff"
|
|
elseif school == "holy" then schoolColor = "|cffffff00"
|
|
elseif school == "bleed" then schoolColor = "|cffff0000"
|
|
end
|
|
|
|
CleveRoids.Print(schoolColor .. string.upper(school) .. "|r (" .. table.getn(spellIDs) .. " spells):")
|
|
for _, spellID in ipairs(spellIDs) do
|
|
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "Unknown"
|
|
CleveRoids.Print(" " .. spellName .. " (ID:" .. spellID .. ")")
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- clearschools (clear learned spell schools)
|
|
if cmd == "clearschools" then
|
|
local count = 0
|
|
if CleveRoids_SpellSchools then
|
|
for _ in pairs(CleveRoids_SpellSchools) do
|
|
count = count + 1
|
|
end
|
|
end
|
|
CleveRoids_SpellSchools = {}
|
|
CleveRoids.spellSchoolMapping = CleveRoids_SpellSchools
|
|
CleveRoids.Print("Cleared " .. count .. " learned spell schools")
|
|
return
|
|
end
|
|
|
|
-- combotrack (show combo point tracking info)
|
|
if cmd == "combotrack" or cmd == "combo" then
|
|
CleveRoids.ShowComboTracking()
|
|
return
|
|
end
|
|
|
|
-- comboclear (clear combo tracking data)
|
|
if cmd == "comboclear" then
|
|
CleveRoids.ComboPointTracking = {}
|
|
CleveRoids.ComboPointTracking.byID = {}
|
|
CleveRoids.Print("Combo point tracking data cleared")
|
|
return
|
|
end
|
|
|
|
-- combolearn (show learned combo durations)
|
|
if cmd == "combolearn" or cmd == "combodurations" then
|
|
CleveRoids.Print("=== Learned Combo Durations ===")
|
|
if not CleveRoids_ComboDurations or not next(CleveRoids_ComboDurations) then
|
|
CleveRoids.Print("No learned combo durations yet. Cast finishers and let them expire!")
|
|
else
|
|
for spellID, cpData in pairs(CleveRoids_ComboDurations) do
|
|
local spellName = GetSpellRecField(spellID, "name") or ("Spell " .. spellID)
|
|
CleveRoids.Print(spellName .. " (ID:" .. spellID .. "):")
|
|
for cp = 1, 5 do
|
|
if cpData[cp] then
|
|
CleveRoids.Print(" " .. cp .. " CP = " .. cpData[cp] .. "s")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- talenttabs (show talent tab IDs)
|
|
if cmd == "talenttabs" or cmd == "tabs" then
|
|
CleveRoids.Print("=== Talent Tabs ===")
|
|
for i = 1, GetNumTalentTabs() do
|
|
local name, _, pointsSpent = GetTalentTabInfo(i)
|
|
CleveRoids.Print("Tab " .. i .. ": " .. name .. " (" .. pointsSpent .. " points)")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- listtab (show all talents in a specific tab with their IDs)
|
|
if cmd == "listtab" or cmd == "tab" then
|
|
local tabNum = tonumber(val)
|
|
if not tabNum or tabNum < 1 or tabNum > GetNumTalentTabs() then
|
|
CleveRoids.Print("Usage: /cleveroid listtab <tab number>")
|
|
CleveRoids.Print("Example: /cleveroid listtab 2")
|
|
CleveRoids.Print("Use /cleveroid talenttabs to see your tab numbers")
|
|
return
|
|
end
|
|
|
|
local tabName = GetTalentTabInfo(tabNum)
|
|
CleveRoids.Print("=== " .. tabName .. " (Tab " .. tabNum .. ") ===")
|
|
|
|
local numTalents = GetNumTalents(tabNum)
|
|
for i = 1, numTalents do
|
|
local name, _, _, _, rank, maxRank = GetTalentInfo(tabNum, i)
|
|
if name then
|
|
local rankText = rank .. "/" .. maxRank
|
|
CleveRoids.Print("ID " .. i .. ": " .. name .. " [" .. rankText .. "]")
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- talents (show current talent ranks)
|
|
if cmd == "talents" or cmd == "talent" then
|
|
CleveRoids.Print("=== Current Talents ===")
|
|
|
|
-- Ensure talents are indexed
|
|
if not CleveRoids.Talents or table.getn(CleveRoids.Talents) == 0 then
|
|
if CleveRoids.IndexTalents then
|
|
CleveRoids.IndexTalents()
|
|
end
|
|
end
|
|
|
|
local count = 0
|
|
for name, rank in pairs(CleveRoids.Talents) do
|
|
if type(name) == "string" and tonumber(rank) and tonumber(rank) > 0 then
|
|
CleveRoids.Print(name .. ": Rank " .. rank)
|
|
count = count + 1
|
|
end
|
|
end
|
|
|
|
if count == 0 then
|
|
CleveRoids.Print("No talents learned yet!")
|
|
else
|
|
CleveRoids.Print("Total: " .. count .. " talents")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- testtalent (test talent modifier for a spell)
|
|
if cmd == "testtalent" or cmd == "talenttest" then
|
|
local spellID = tonumber(val)
|
|
if not spellID then
|
|
CleveRoids.Print("Usage: /cleveroid testtalent <spellID>")
|
|
CleveRoids.Print("Example: /cleveroid testtalent 1943 (Rupture Rank 1)")
|
|
return
|
|
end
|
|
|
|
local spellName = GetSpellRecField(spellID, "name") or ("Spell " .. spellID)
|
|
local modifier = CleveRoids.talentModifiers and CleveRoids.talentModifiers[spellID]
|
|
|
|
if not modifier then
|
|
CleveRoids.Print(spellName .. " (ID:" .. spellID .. ") has no talent modifier configured")
|
|
return
|
|
end
|
|
|
|
-- Get talent rank using position-based or name-based lookup
|
|
local talentRank = 0
|
|
local maxRank = 3
|
|
local talentName = modifier.talent or ("Tab " .. tostring(modifier.tab) .. " ID " .. tostring(modifier.id))
|
|
|
|
-- Position-based lookup (preferred)
|
|
if modifier.tab and modifier.id then
|
|
local _, name, _, _, rank, max = GetTalentInfo(modifier.tab, modifier.id)
|
|
talentRank = tonumber(rank) or 0
|
|
maxRank = tonumber(max) or 3
|
|
if name then
|
|
talentName = name
|
|
end
|
|
end
|
|
|
|
-- Name-based fallback
|
|
if talentRank == 0 and modifier.talent and CleveRoids.GetTalentRank then
|
|
talentRank = CleveRoids.GetTalentRank(modifier.talent)
|
|
end
|
|
|
|
CleveRoids.Print("=== Talent Modifier Test ===")
|
|
CleveRoids.Print("Spell: " .. spellName .. " (ID:" .. spellID .. ")")
|
|
CleveRoids.Print("Talent: " .. talentName)
|
|
CleveRoids.Print("Your Rank: " .. talentRank .. "/" .. maxRank)
|
|
|
|
if talentRank == 0 then
|
|
CleveRoids.Print("|cffff0000You don't have this talent!|r")
|
|
else
|
|
-- Test with a base duration (use 10s as example)
|
|
local baseDur = 10
|
|
local modDur = modifier.modifier(baseDur, talentRank)
|
|
CleveRoids.Print("Example: 10s base -> " .. modDur .. "s modified (+" .. (modDur - baseDur) .. "s)")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- debuffdebug (debug debuff tracking on target)
|
|
if cmd == "debuffdebug" or cmd == "debuff" or cmd == "trackdebug" then
|
|
local searchName = val
|
|
if val2 and val2 ~= "" then
|
|
searchName = val .. " " .. val2
|
|
end
|
|
-- Strip underscores and quotes
|
|
if searchName and searchName ~= "" then
|
|
searchName = string.gsub(searchName, "_", " ")
|
|
searchName = string.gsub(searchName, '"', "")
|
|
end
|
|
|
|
CleveRoids.Print("|cff88ff88=== Debuff Tracking Debug ===|r")
|
|
|
|
local guid = CleveRoids.GetGUID("target")
|
|
if not guid then
|
|
CleveRoids.Print("|cffff0000No target selected!|r")
|
|
return
|
|
end
|
|
|
|
local targetName = UnitName("target") or "Unknown"
|
|
CleveRoids.Print("Target: " .. targetName .. " (GUID: " .. tostring(guid) .. ")")
|
|
|
|
-- Show tracking table for this target
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cff00ff00=== Tracked Debuffs (libdebuff.objects) ===|r")
|
|
local lib = CleveRoids.libdebuff
|
|
if lib and lib.objects and lib.objects[guid] then
|
|
local count = 0
|
|
for spellID, rec in pairs(lib.objects[guid]) do
|
|
if rec and rec.start and rec.duration then
|
|
local timeRemaining = rec.duration + rec.start - GetTime()
|
|
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "Unknown"
|
|
local caster = rec.caster or "unknown"
|
|
local stacks = rec.stacks or 0
|
|
if timeRemaining > 0 then
|
|
CleveRoids.Print(string.format(" |cff00ff00[%d]|r %s: %.1fs left (caster: %s, stacks: %d)",
|
|
spellID, spellName, timeRemaining, caster, stacks))
|
|
count = count + 1
|
|
else
|
|
CleveRoids.Print(string.format(" |cffff0000[%d]|r %s: EXPIRED %.1fs ago (caster: %s)",
|
|
spellID, spellName, -timeRemaining, caster))
|
|
end
|
|
end
|
|
end
|
|
if count == 0 then
|
|
CleveRoids.Print(" (no active tracked debuffs)")
|
|
end
|
|
else
|
|
CleveRoids.Print(" (no tracking data for this target)")
|
|
end
|
|
|
|
-- Show actual debuff slots (1-16 via UnitDebuff, 17-48 via overflow)
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cff00ff00=== Debuff Slots (UnitDebuff 1-16) ===|r")
|
|
local debuffCount = 0
|
|
for i = 1, 16 do
|
|
local texture, stacks, debuffType, spellID = UnitDebuff("target", i)
|
|
if texture then
|
|
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "slot" .. i
|
|
CleveRoids.Print(string.format(" Slot %d: [%d] %s (stacks: %d)",
|
|
i, spellID or 0, spellName, stacks or 0))
|
|
debuffCount = debuffCount + 1
|
|
end
|
|
end
|
|
if debuffCount == 0 then
|
|
CleveRoids.Print(" (no debuffs in slots 1-16)")
|
|
end
|
|
|
|
-- Show overflow debuffs in buff slots (17-48)
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cff00ff00=== Overflow Debuffs (UnitBuff 1-32 as debuffs 17-48) ===|r")
|
|
local overflowCount = 0
|
|
for i = 1, 32 do
|
|
local texture, stacks, spellID = UnitBuff("target", i)
|
|
if texture and spellID then
|
|
-- Check if this might be an overflow debuff by checking libdebuff durations
|
|
local isDebuff = lib and lib.durations and lib.durations[spellID]
|
|
if isDebuff then
|
|
local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "slot" .. i
|
|
CleveRoids.Print(string.format(" Buff Slot %d (=Debuff %d): [%d] %s (stacks: %d) |cffff8800OVERFLOW|r",
|
|
i, i + 16, spellID, spellName, stacks or 0))
|
|
overflowCount = overflowCount + 1
|
|
end
|
|
end
|
|
end
|
|
if overflowCount == 0 then
|
|
CleveRoids.Print(" (no overflow debuffs detected)")
|
|
end
|
|
|
|
-- If a specific debuff name was provided, test the conditional
|
|
if searchName and searchName ~= "" then
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cff00ff00=== Testing [debuff:\"" .. searchName .. "\"] ===|r")
|
|
|
|
-- Test ValidateUnitDebuff
|
|
local result = CleveRoids.ValidateUnitDebuff("target", { name = searchName })
|
|
CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "'}): " ..
|
|
(result and "|cff00ff00true|r" or "|cffff0000false|r"))
|
|
|
|
-- Test with time conditional
|
|
local resultTime = CleveRoids.ValidateUnitDebuff("target", { name = searchName, operator = "<", amount = 99999 })
|
|
CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "', operator='<', amount=99999}): " ..
|
|
(resultTime and "|cff00ff00true|r" or "|cffff0000false|r"))
|
|
|
|
-- Look for spell IDs matching this name
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cff00ff00=== Spell ID Lookup for \"" .. searchName .. "\" ===|r")
|
|
local foundIDs = {}
|
|
-- Check Spells table
|
|
if CleveRoids.Spells then
|
|
for id, name in pairs(CleveRoids.Spells) do
|
|
if type(name) == "string" and string.lower(name) == string.lower(searchName) then
|
|
table.insert(foundIDs, id)
|
|
end
|
|
end
|
|
end
|
|
-- Also check GetSpellRecField
|
|
if GetSpellRecField then
|
|
for id = 1, 30000 do
|
|
local name = GetSpellRecField(id, "name")
|
|
if name and string.lower(name) == string.lower(searchName) then
|
|
local found = false
|
|
for _, existingID in ipairs(foundIDs) do
|
|
if existingID == id then found = true break end
|
|
end
|
|
if not found then
|
|
table.insert(foundIDs, id)
|
|
end
|
|
end
|
|
-- Stop early if we found some
|
|
if table.getn(foundIDs) > 10 then break end
|
|
end
|
|
end
|
|
|
|
if table.getn(foundIDs) > 0 then
|
|
for _, id in ipairs(foundIDs) do
|
|
local tracked = lib and lib.objects and lib.objects[guid] and lib.objects[guid][id]
|
|
local trackedStr = tracked and "|cff00ff00TRACKED|r" or "|cff888888not tracked|r"
|
|
CleveRoids.Print(" SpellID " .. id .. ": " .. trackedStr)
|
|
if tracked then
|
|
local remaining = tracked.duration + tracked.start - GetTime()
|
|
CleveRoids.Print(" -> " .. string.format("%.1fs remaining (caster: %s)", remaining, tracked.caster or "?"))
|
|
end
|
|
end
|
|
else
|
|
CleveRoids.Print(" (no spell IDs found for this name)")
|
|
end
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
-- tankdebug (debug pfUI tank integration)
|
|
if cmd == "tankdebug" then
|
|
CleveRoids.Print("=== Tank Debug ===")
|
|
|
|
-- Check pfUI availability
|
|
if not CleveRoids.HasPfUITanks() then
|
|
CleveRoids.Print("|cffff0000pfUI tank system not available!|r")
|
|
CleveRoids.Print(" pfUI loaded: " .. tostring(type(pfUI) == "table"))
|
|
CleveRoids.Print(" pfUI_config: " .. tostring(type(pfUI_config) == "table"))
|
|
return
|
|
end
|
|
|
|
CleveRoids.Print("|cff00ff00pfUI tank system available|r")
|
|
|
|
-- List raid frame tanks
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cffffaa00Raid Frame Tanks (right-click toggle):|r")
|
|
local raidTankCount = 0
|
|
if type(pfUI) == "table" and type(pfUI.uf) == "table" and
|
|
type(pfUI.uf.raid) == "table" and type(pfUI.uf.raid.tankrole) == "table" then
|
|
for name, isTank in pairs(pfUI.uf.raid.tankrole) do
|
|
if isTank then
|
|
CleveRoids.Print(" - " .. name)
|
|
raidTankCount = raidTankCount + 1
|
|
end
|
|
end
|
|
end
|
|
if raidTankCount == 0 then
|
|
CleveRoids.Print(" (none)")
|
|
end
|
|
|
|
-- List nameplate off-tank names
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cffffaa00Nameplate Off-Tank Names (config setting):|r")
|
|
local npTankCount = 0
|
|
if type(pfUI_config) == "table" and type(pfUI_config.nameplates) == "table" then
|
|
local list = pfUI_config.nameplates.combatofftanks or ""
|
|
if list ~= "" then
|
|
CleveRoids.Print(" Raw setting: \"" .. list .. "\"")
|
|
-- pfUI uses # as separator
|
|
for name in string.gfind(list, "[^#]+") do
|
|
name = string.gsub(name, "^%s*(.-)%s*$", "%1") -- trim
|
|
if name ~= "" then
|
|
CleveRoids.Print(" - " .. name .. " (lowercase: " .. string.lower(name) .. ")")
|
|
npTankCount = npTankCount + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if npTankCount == 0 then
|
|
CleveRoids.Print(" (none)")
|
|
end
|
|
|
|
-- Check current target
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cffffaa00Current Target Check:|r")
|
|
if not UnitExists("target") then
|
|
CleveRoids.Print(" No target selected")
|
|
else
|
|
local targetName = UnitName("target") or "Unknown"
|
|
CleveRoids.Print(" Your target: " .. targetName)
|
|
|
|
if UnitExists("targettarget") then
|
|
local ttName = UnitName("targettarget") or "Unknown"
|
|
CleveRoids.Print(" Target's target: " .. ttName)
|
|
local isTank = CleveRoids.IsPlayerTank(ttName)
|
|
if isTank then
|
|
CleveRoids.Print(" |cff00ff00" .. ttName .. " IS marked as tank|r")
|
|
else
|
|
CleveRoids.Print(" |cffff8800" .. ttName .. " is NOT marked as tank|r")
|
|
CleveRoids.Print(" (lowercase check: " .. string.lower(ttName) .. ")")
|
|
end
|
|
else
|
|
CleveRoids.Print(" Target's target: (none)")
|
|
end
|
|
|
|
-- Test the conditional functions
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cffffaa00Conditional Results:|r")
|
|
local isTargetingTank = CleveRoids.IsTargetingAnyTank("target")
|
|
CleveRoids.Print(" IsTargetingAnyTank('target'): " .. tostring(isTargetingTank))
|
|
CleveRoids.Print(" [targeting:tank] would be: " .. tostring(isTargetingTank))
|
|
CleveRoids.Print(" [notargeting:tank] would be: " .. tostring(not isTargetingTank))
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
-- auradebug (debug all-caster aura tracking)
|
|
if cmd == "auradebug" or cmd == "bufftracking" then
|
|
CleveRoids.Print("=== All-Caster Aura Tracking Debug ===")
|
|
|
|
-- Check CVar status
|
|
local auraCastEnabled = GetCVar("NP_EnableAuraCastEvents")
|
|
if auraCastEnabled == "1" then
|
|
CleveRoids.Print("|cff00ff00NP_EnableAuraCastEvents = 1 (ENABLED)|r")
|
|
else
|
|
CleveRoids.Print("|cffff0000NP_EnableAuraCastEvents = " .. tostring(auraCastEnabled) .. " (DISABLED)|r")
|
|
CleveRoids.Print(" Enable with: |cff00ffff/run SetCVar(\"NP_EnableAuraCastEvents\", \"1\")|r")
|
|
CleveRoids.Print(" Then /reload")
|
|
end
|
|
|
|
-- Check Nampower version
|
|
local API = CleveRoids.NampowerAPI
|
|
if API and API.features then
|
|
CleveRoids.Print("Nampower hasAuraCastEvents: " .. tostring(API.features.hasAuraCastEvents))
|
|
else
|
|
CleveRoids.Print("|cffff0000Nampower API not available|r")
|
|
end
|
|
|
|
-- Show tracking data for current target/mouseover
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cffffaa00Tracked Auras:|r")
|
|
local trackingCount = 0
|
|
local now = GetTime()
|
|
-- Determine which backing table to iterate
|
|
local isPfUI = CleveRoids.hasPfUI76 and pfUI and pfUI.libdebuff_all_auras
|
|
local backingTable = isPfUI and pfUI.libdebuff_all_auras or CleveRoids.AllCasterAuraTracking or {}
|
|
if isPfUI then
|
|
CleveRoids.Print(" (reading from pfUI.libdebuff_all_auras)")
|
|
end
|
|
for targetGuid, spellNames in pairs(backingTable) do
|
|
local unitName = nil
|
|
-- Try to find unit name for this GUID (use pcall to handle invalid units like "focus")
|
|
for _, testUnit in ipairs({"target", "mouseover", "party1", "party2", "party3", "party4"}) do
|
|
local ok, exists, testGuid = pcall(function() return UnitExists(testUnit) end)
|
|
if ok and exists then
|
|
_, testGuid = UnitExists(testUnit)
|
|
if testGuid == targetGuid then
|
|
unitName = UnitName(testUnit)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
for spellName, casters in pairs(spellNames) do
|
|
for casterGuid, auraData in pairs(casters) do
|
|
local startTime = isPfUI and auraData.startTime or auraData.start
|
|
if startTime and auraData.duration then
|
|
local remaining = auraData.duration + startTime - now
|
|
if remaining > 0 then
|
|
local display = unitName or (string.sub(targetGuid, 1, 16) .. "...")
|
|
CleveRoids.Print(string.format(" %s on %s: %.1fs left (caster: %s)",
|
|
spellName, display, remaining, string.sub(tostring(casterGuid), 1, 16)))
|
|
trackingCount = trackingCount + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if trackingCount == 0 then
|
|
CleveRoids.Print(" (no auras currently tracked)")
|
|
end
|
|
|
|
-- Check specific target
|
|
if UnitExists("target") then
|
|
CleveRoids.Print(" ")
|
|
CleveRoids.Print("|cffffaa00Target Buff Check:|r")
|
|
local targetGuid = CleveRoids.GetGUID("target")
|
|
CleveRoids.Print(" Target GUID: " .. tostring(targetGuid))
|
|
local targetData = CleveRoids.GetAuraTrackingData(targetGuid)
|
|
if targetData then
|
|
local count = 0
|
|
for _, casters in pairs(targetData) do
|
|
for _ in pairs(casters) do count = count + 1 end
|
|
end
|
|
CleveRoids.Print(" Tracked aura entries on target: " .. count)
|
|
else
|
|
CleveRoids.Print(" |cffff9900No tracking data for this target|r")
|
|
end
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
-- overflowframe (enable/disable player overflow buff frame)
|
|
if cmd == "overflowframe" then
|
|
CleveRoidMacros = CleveRoidMacros or {}
|
|
if val == "on" or val == "1" then
|
|
CleveRoidMacros.overflowPlayerEnabled = true
|
|
CleveRoids.Print("Player overflow buff frame |cff00ff00enabled|r")
|
|
elseif val == "off" or val == "0" then
|
|
CleveRoidMacros.overflowPlayerEnabled = false
|
|
CleveRoids.Print("Player overflow buff frame |cffff0000disabled|r")
|
|
else
|
|
CleveRoidMacros.overflowPlayerEnabled = not CleveRoidMacros.overflowPlayerEnabled
|
|
if CleveRoidMacros.overflowPlayerEnabled then
|
|
CleveRoids.Print("Player overflow buff frame |cff00ff00enabled|r")
|
|
else
|
|
CleveRoids.Print("Player overflow buff frame |cffff0000disabled|r")
|
|
end
|
|
end
|
|
if CleveRoids.RebuildOverflowFrame then
|
|
CleveRoids.RebuildOverflowFrame()
|
|
end
|
|
return
|
|
end
|
|
|
|
-- overflowtest (toggle overflow buff frame test mode)
|
|
if cmd == "overflowtest" then
|
|
if CleveRoids.ToggleOverflowTest then
|
|
CleveRoids.ToggleOverflowTest()
|
|
else
|
|
CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- overflowreset (reset frame positions to defaults)
|
|
if cmd == "overflowreset" then
|
|
if CleveRoids.ResetOverflowFramePositions then
|
|
CleveRoids.ResetOverflowFramePositions()
|
|
else
|
|
CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Delegate to extension console handlers (chain pattern)
|
|
if CleveRoids.HandleConsoleCommand then
|
|
CleveRoids.HandleConsoleCommand(msg)
|
|
return
|
|
end
|
|
|
|
-- Unknown command fallback
|
|
CleveRoids.Print("Usage:")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates (Default: 0. 1 = on, increases CPU load)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1 to 10 updates per second. Default: 5)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages")
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Immunity Tracking:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listimmune [school] - List immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "<NPC>" <school> [buff] - Add immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "<NPC>" <school> - Remove immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00CC Immunity Tracking:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listccimmune [type] - List CC immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addccimmune "<NPC>" <type> [buff] - Add CC immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeccimmune "<NPC>" <type> - Remove CC immunity')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearccimmune [type] - Clear CC immunity data')
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data')
|
|
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)')
|
|
end
|
|
|
|
SLASH_CLEAREQUIPQUEUE1 = "/clearequipqueue"
|
|
SlashCmdList.CLEAREQUIPQUEUE = function()
|
|
-- Release all entries back to pool
|
|
for i = 1, CleveRoids.equipmentQueueLen do
|
|
local entry = CleveRoids.equipmentQueue[i]
|
|
if entry then
|
|
CleveRoids.equipmentQueue[i] = nil
|
|
-- Return to pool if space available
|
|
if table.getn(CleveRoids.queueEntryPool) < 10 then
|
|
entry.item = nil
|
|
entry.slotName = nil
|
|
entry.inventoryId = nil
|
|
table.insert(CleveRoids.queueEntryPool, entry)
|
|
end
|
|
end
|
|
end
|
|
CleveRoids.equipmentQueueLen = 0
|
|
if CleveRoids.equipQueueFrame then
|
|
CleveRoids.equipQueueFrame:Hide()
|
|
end
|
|
CleveRoids.Print("Equipment queue cleared")
|
|
end
|
|
|
|
SLASH_EQUIPQUEUESTATUS1 = "/equipqueuestatus"
|
|
SlashCmdList.EQUIPQUEUESTATUS = function()
|
|
local count = CleveRoids.equipmentQueueLen
|
|
CleveRoids.Print("Equipment queue has " .. count .. " pending items")
|
|
|
|
for i = 1, count do
|
|
local entry = CleveRoids.equipmentQueue[i]
|
|
if entry then
|
|
local itemName = (entry.item and entry.item.name) or "Unknown"
|
|
local slotName = entry.slotName or "Unknown"
|
|
local retries = entry.retries or 0
|
|
CleveRoids.Print(i .. ". " .. itemName .. " -> " .. slotName .. " (retries: " .. retries .. ")")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Apply temporary weapon enchants (poisons, oils, sharpening stones, etc.)
|
|
-- Usage: /applymain [conditionals] ItemName
|
|
-- Usage: /applyoff [conditionals] ItemName
|
|
-- Example: /applymain [nomhimbue] Instant Poison
|
|
-- Example: /applyoff [combat] Crippling Poison
|
|
|
|
local function FindItemInBags(itemName)
|
|
-- Require a non-empty item name to prevent matching everything
|
|
if not itemName or itemName == "" then
|
|
return nil
|
|
end
|
|
|
|
local searchName = string.lower(itemName)
|
|
for bag = 0, 4 do
|
|
for slot = 1, GetContainerNumSlots(bag) do
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if link then
|
|
local _, _, foundName = string.find(link, "%[(.+)%]")
|
|
if foundName and string.find(string.lower(foundName), searchName, 1, true) then
|
|
return bag, slot, foundName
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function CleveRoids.DoApply(hand, msg)
|
|
local weaponSlots = {
|
|
["main"] = 16,
|
|
["off"] = 17,
|
|
}
|
|
|
|
local slot = weaponSlots[hand]
|
|
if not slot then
|
|
CleveRoids.Print("Invalid hand: " .. tostring(hand))
|
|
return false
|
|
end
|
|
|
|
local action = function(itemName)
|
|
local bag, bagSlot, foundName = FindItemInBags(itemName)
|
|
if not bag then
|
|
CleveRoids.Print("Item not found: " .. itemName)
|
|
return false
|
|
end
|
|
|
|
-- Apply the item to the weapon
|
|
UseContainerItem(bag, bagSlot)
|
|
PickupInventoryItem(slot)
|
|
ReplaceEnchant()
|
|
ClearCursor()
|
|
return true
|
|
end
|
|
|
|
local handled = false
|
|
for _, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
|
|
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then
|
|
-- If /firstaction was used, stop macro evaluation after first successful apply
|
|
if CleveRoids.stopOnCastFlag then
|
|
CleveRoids.stopMacroFlag = true
|
|
end
|
|
handled = true
|
|
break
|
|
end
|
|
end
|
|
return handled
|
|
end
|
|
|
|
SLASH_APPLYMAIN1 = "/applymain"
|
|
SlashCmdList.APPLYMAIN = function(msg)
|
|
-- Require an item name argument
|
|
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
|
|
CleveRoids.Print("Usage: /applymain [conditionals] ItemName")
|
|
return
|
|
end
|
|
CleveRoids.DoApply("main", msg)
|
|
end
|
|
|
|
SLASH_APPLYOFF1 = "/applyoff"
|
|
SlashCmdList.APPLYOFF = function(msg)
|
|
-- Require an item name argument
|
|
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
|
|
CleveRoids.Print("Usage: /applyoff [conditionals] ItemName")
|
|
return
|
|
end
|
|
CleveRoids.DoApply("off", msg)
|
|
end
|