Files
SuperCleveRoidMacros/Core.lua
T
Jrc13245 916e70d7bb revert
2025-08-25 17:09:09 -04:00

2159 lines
75 KiB
Lua

--[[
Author: Dennis Werner Garske (DWG) / brian / Mewtiny / brian / Mewtiny
License: MIT License
]]
-- Setup to wrap our stuff in a table so we don't pollute the global environment
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
_G.CleveRoids = CleveRoids
CleveRoids.lastItemIndexTime = 0
CleveRoids.initializationTimer = nil
CleveRoids.isActionUpdateQueued = true -- Flag to check if a full action update is needed
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
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
function CleveRoids.GetSpellCost(spellSlot, bookType)
CleveRoids.Frame:SetOwner(WorldFrame, "ANCHOR_NONE")
CleveRoids.Frame:SetSpell(spellSlot, bookType)
local costText = CleveRoids.Frame.costFontString:GetText() or ""
local _, _, cost = string.find(costText, "^(%d+) [^ys]")
local line = CleveRoids.Frame.reagentFontString:GetText() or ""
-- strip color and link wrappers if present
line = line:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|H.-|h(.-)|h", "%1")
-- accept "Reagent(s):" OR "Tool(s):" (case-insensitive), grab what comes after the colon
local reagent = line:match("^%s*[Rr]eagents?:%s*(.+)$")
or line:match("^%s*[Tt]ools?:%s*(.+)$")
-- if multiple are ever listed, keep the first token
if reagent then
reagent = reagent:match("^[^,]+")
reagent = reagent and reagent:match("^%s*(.-)%s*$") or nil
end
return (cost and tonumber(cost) or 0), (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
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
if actions.tooltip and table.getn(actions.list) == 0 then
if CleveRoids.TestAction(actions.cmd or "", actions.args or "") then
hasActive = true
actions.active = actions.tooltip
end
else
for _, action in ipairs(actions.list) do
-- break on first action that passes tests
if CleveRoids.TestAction(action.cmd, action.args) then
hasActive = true
if action.sequence then
newSequence = action.sequence
newActiveAction = CleveRoids.GetCurrentSequenceAction(newSequence)
if not newActiveAction then hasActive = false end
else
newActiveAction = action
end
if hasActive then break end
end
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
actions.active.inRange = 1
-- nampower range check (rebuild name(rank) like DoWithConditionals)
if IsSpellInRange then
local unit = actions.active.conditionals and actions.active.conditionals.target or "target"
if unit == "focus" and pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
unit = pfUI.uf.focus.label .. pfUI.uf.focus.id
end
local castName = actions.active.action
if actions.active.spell and actions.active.spell.name then
local rank = actions.active.spell.rank
or (actions.active.spell.highest and actions.active.spell.highest.rank)
if rank and rank ~= "" then
castName = actions.active.spell.name .. "(" .. rank .. ")"
end
end
if UnitExists(unit) then
local r = IsSpellInRange(castName, unit)
if r ~= nil then
actions.active.inRange = r
end
end
end
actions.active.oom = (UnitMana("player") < actions.active.spell.cost)
local start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType)
local onCooldown = (start > 0 and duration > 0)
if actions.active.isReactive then
if 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
elseif actions.active.inRange ~= 0 and not actions.active.oom then
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
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
end
end
return changed
end
function CleveRoids.TestForAllActiveActions()
for slot, actions in pairs(CleveRoids.Actions) do
local stateChanged = CleveRoids.TestForActiveAction(actions)
if stateChanged then
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
end
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
local text = GetActionText(slot)
if text then
local macro = CleveRoids.GetMacro(text)
if macro then
actions = macro.actions
CleveRoids.TestForActiveAction(actions)
CleveRoids.Actions[slot] = actions
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
return actions
end
end
end
function CleveRoids.GetActiveAction(slot)
local action = CleveRoids.GetAction(slot)
return action and action.active
end
function CleveRoids.SendEventForAction(slot, event, ...)
local old_this = this
local original_global_args = {}
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
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
function CleveRoids.ExecuteMacroBody(body,inline)
local lines = CleveRoids.splitString(body, "\n")
if inline then lines = CleveRoids.splitString(body, "\\n"); end
for k,v in pairs(lines) do
if CleveRoids.stopmacro then
CleveRoids.stopmacro = false
return true
end
ChatFrameEditBox:SetText(v)
ChatEdit_SendText(ChatFrameEditBox)
end
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
-- name: The name of the macro
-- returns: Whether the macro was executed or not
function CleveRoids.ExecuteMacroByName(name)
local body = CleveRoids.GetMacroBody(name)
if not body then
return false
end
CleveRoids.ExecuteMacroBody(body)
return true
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()
-- 1. Add specific compatibility for pfUI.
-- pfUI stores its focus unit information in a table.
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.unitname then
return pfUI.uf.focus.unitname
end
-- Fallback for other focus addons
if ClassicFocus_CurrentFocus then
return ClassicFocus_CurrentFocus
elseif CURR_FOCUS_TARGET then
return CURR_FOCUS_TARGET
end
return nil
end
-- Attempts to target the focus target.
-- returns: Whether or not it succeeded
function CleveRoids.TryTargetFocus()
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
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 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"
-- Use the most reliable method first to get the texture for an equipped item.
local itemTexture = GetInventoryItemTexture("player", slotId)
-- Check if the texture was successfully found.
if itemTexture then
texture = itemTexture
else
-- If the primary method fails, fall back to the unknown texture.
-- This prevents errors if the slot is empty or the item data is unusual.
texture = CleveRoids.unknownTexture
end
else
-- Original logic for named items and spells
if not spell then
item = CleveRoids.GetItem(text)
end
if not item then
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 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,
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)
local args = string.gsub(text, "(%s*,%s*)", ",")
local _, c, cond = string.find(args, "(%[.*%])")
local _, r, reset, resetVal = string.find(args, "(%s*%]*%s*reset=([%w/]+)%s+)")
actionSeq = CleveRoids.Trim((r and string.sub(args, r+1)) or (c and string.sub(args, c+1)) or args)
args = (cond or "") .. actionSeq
if not actionSeq then
return
end
local sequence = {
index = 1,
reset = {},
status = 0,
list = {},
lastUpdate = 0,
cond = cond,
args = args,
cmd = "/castsequence"
}
if resetVal then
for _, rule in ipairs(CleveRoids.Split(resetVal, "/")) do
local secs = tonumber(rule)
if secs and secs > 0 then
sequence.reset.secs = secs
else
sequence.reset[string.lower(rule)] = true
end
end
end
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
function CleveRoids.ParseMacro(name)
if not name then return end
local macroID = GetMacroIndexByName(name)
if not macroID then return end
local _, texture, body = GetMacroInfo(macroID)
if not body and GetSuperMacroInfo then
_, texture, body = GetSuperMacroInfo(name)
end
if not texture or not body then return end
local macro = {
id = macroID,
name = name,
texture = texture,
body = body,
actions = {},
}
macro.actions.list = {}
-- build a list of testable actions for the macro
for i, line in ipairs(CleveRoids.splitString(body, "\n")) do
line = CleveRoids.Trim(line)
local cmd, args = CleveRoids.SplitCommandAndArgs(line)
-- check for #showtooltip
if i == 1 then
local _, _, st, _, tt = string.find(line, "(#showtooltip)(%s?(.*))")
-- if no #showtooltip, nothing to keep track of
if not st then
break
end
tt = CleveRoids.Trim(tt)
-- #showtooltip and item/spell/macro specified, only use this tooltip
if st and tt ~= "" then
for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(tt)) do
macro.actions.tooltip = CleveRoids.CreateActionInfo(arg)
local action = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(arg))
action.cmd = "/cast"
action.args = arg
action.isReactive = CleveRoids.reactiveSpells[action.action]
table.insert(macro.actions.list, action)
end
break
end
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
CleveRoids.Macros[name] = macro
return macro
end
function CleveRoids.ParseMsg(msg)
if not msg then return end
local conditionals = {}
msg, conditionals.ignoretooltip = string.gsub(CleveRoids.Trim(msg), "^%?", "")
local _, cbEnd, conditionBlock = string.find(msg, "%[(.+)%]")
local _, _, noSpam, cancelAura, action = string.find(string.sub(msg, (cbEnd or 0) + 1), "^%s*(!?)(~?)([^!~]+.*)")
action = CleveRoids.Trim(action or "")
-- Store the action along with the conditionals incase it's needed
conditionals.action = action
-- FLEXIBLY remove rank text like "(Rank 9)" to get the base spell name
action = string.gsub(action, "%s*%(.-%)%s*$", "")
if noSpam and noSpam ~= "" then
local spamCond = CleveRoids.GetSpammableConditional(action)
if spamCond then
conditionals[spamCond] = { action }
end
end
if cancelAura and cancelAura ~= "" then
conditionals.cancelaura = action
end
if not conditionBlock then
return conditionals.action, conditionals
end
-- Set the action's target to @unitid if found
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 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
local conditionGroup = CleveRoids.splitStringIgnoringQuotes(conditionGroups, ":")
local condition, args = conditionGroup[1], conditionGroup[2]
-- No args, the action is the implicit argument
if not args or args == "" then
if not conditionals[condition] then
conditionals[condition] = action
else
if type(conditionals[condition]) ~= "table" then
conditionals[condition] = { conditionals[condition] }
end
table.insert(conditionals[condition], action)
end
else
-- Has args. Ensure the key's value is a table and add the new arguments.
if not conditionals[condition] then
conditionals[condition] = {}
elseif type(conditionals[condition]) ~= "table" then
conditionals[condition] = { conditionals[condition] }
end
-- Split the args by / for multiple values
for _, arg_item in CleveRoids.splitString(args, "/") 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)
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")
-- FIXED: This regex now accepts decimal numbers
local _, _, name, operator, amount = string.find(arg_for_find, "([^>~=<]*)([>~=<]+)(#?%d*%.?%d+)")
if not operator or not amount then
table.insert(conditionals[condition], 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)
table.insert(conditionals[condition], {
name = CleveRoids.Trim(name_to_use),
operator = operator,
amount = tonumber(final_amount_str),
checkStacks = should_check_stacks
})
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
if CleveRoids.ParsedMsg[msg] then
return CleveRoids.ParsedMsg[msg].action, CleveRoids.ParsedMsg[msg].conditionals
end
CleveRoids.ParsedMsg[msg] = {}
CleveRoids.ParsedMsg[msg].action, CleveRoids.ParsedMsg[msg].conditionals = CleveRoids.ParseMsg(msg)
return CleveRoids.ParsedMsg[msg].action, CleveRoids.ParsedMsg[msg].conditionals
end
function CleveRoids.GetMacro(name)
return CleveRoids.Macros[name] or CleveRoids.ParseMacro(name)
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)
sequence.index = 1
end
function CleveRoids.AdvanceSequence(sequence)
if sequence.index < table.getn(sequence.list) then
sequence.index = sequence.index + 1
else
CleveRoids.ResetSequence(sequence)
end
end
function CleveRoids.TestAction(cmd, args)
local msg, conditionals = CleveRoids.GetParsedMsg(args)
if string.find(msg, "#showtooltip") or conditionals.ignoretooltip == 1 then
return
end
if not conditionals then
if not msg then
return
else
-- action is a {macro} or item/spell
return CleveRoids.GetMacroNameFromAction(msg) or msg
end
end
local origTarget = conditionals.target
if cmd == "" or not CleveRoids.dynamicCmds[cmd] then
-- untestables
return
end
if conditionals.target == "focus" then
local focusUnitId = nil
-- First, try to get the specific UnitID from pfUI's data for reliability.
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
focusUnitId = pfUI.uf.focus.label .. pfUI.uf.focus.id
end
if focusUnitId then
-- If we found a specific unit (e.g., "party1", "raid2"), use it for the test.
conditionals.target = focusUnitId
else
-- If we only have a name, check if that name exists. If not, the condition fails.
if not CleveRoids.GetFocusName() then
return -- No focus exists, so this action is not valid.
end
-- Fallback for testing: We can't safely target by name here, so we default to "target".
-- This part of the original logic remains as a fallback, but the pfUI check above will handle most cases.
conditionals.target = "target"
end
end
if conditionals.target == "mouseover" then
if not CleveRoids.IsValidTarget("mouseover", conditionals.help) then
return false
end
end
CleveRoids.FixEmptyTarget(conditionals)
-- CleveRoids.SetHelp(conditionals)
for k, v in pairs(conditionals) do
if not CleveRoids.ignoreKeywords[k] then
if not CleveRoids.Keywords[k] or not CleveRoids.Keywords[k](conditionals) then
-- failed test
conditionals.target = origTarget
return
end
end
end
-- tests passed
conditionals.target = origTarget
return CleveRoids.GetMacroNameFromAction(msg) or msg
end
function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBeforeAction, action)
local msg, conditionals = CleveRoids.GetParsedMsg(msg)
-- 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
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" then
local focusUnitId = nil
-- Attempt to get the direct UnitID from pfUI's focus frame data. This is more reliable.
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
focusUnitId = pfUI.uf.focus.label .. pfUI.uf.focus.id
end
if focusUnitId then
-- If we found a valid UnitID, we will use it for all subsequent checks and the final cast.
-- This avoids changing the player's actual target.
conditionals.target = focusUnitId
needRetarget = false
else
-- return false if pfUI is installed and no focus is set instead of "invalid target"
if (pfUI.uf.focus.label == nil or pfUI.uf.focus.label == "") and pfUI then return false end
-- If the direct UnitID isn't found, fall back to the original (but likely failing) method of targeting 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 = "target"
needRetarget = true
end
end
for k, v in pairs(conditionals) do
if not CleveRoids.ignoreKeywords[k] then
if not CleveRoids.Keywords[k] or not CleveRoids.Keywords[k](conditionals) then
if needRetarget then
TargetLastTarget()
needRetarget = false
end
conditionals.target = origTarget
return false
end
end
end
if conditionals.target ~= nil and targetBeforeAction and not (CleveRoids.hasSuperwow and action == CastSpellByName) 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
CleveRoids.stopmacro = true
return true
end
local result = true
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
result = CleveRoids.ExecuteMacroBody(string.sub(msg, 3, -3), true)
else
result = CleveRoids.ExecuteMacroByName(string.sub(msg, 2, -2))
end
else -- This 'else' corresponds to 'if string.sub(msg, 1, 1) == "{"...'
local castMsg = msg
-- FLEXIBLY check for any rank text like "(Rank 9)" before adding the highest rank
if action == CastSpellByName and not string.find(msg, "%(.*%)") 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 CleveRoids.hasSuperwow and action == CastSpellByName and conditionals.target then
CastSpellByName(castMsg, conditionals.target) -- SuperWoW handles targeting via argument
elseif action == CastSpellByName then
action(castMsg)
else
-- For other actions like UseContainerItem etc.
action(msg)
end
end
if needRetarget then
TargetLastTarget()
end
conditionals.target = origTarget
return result
end
function CleveRoids.DoCast(msg)
local handled = false
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
if CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.CAST_SlashCmd, CleveRoids.FixEmptyTarget, not CleveRoids.hasSuperwow, CastSpellByName) then
-- If a spell is successfully attempted, immediately exit to prevent casting multiple spells.
return true
end
end
return false
end
-- Target using GUIDs (actually unit tokens) and correct targeting.
function CleveRoids.DoTarget(msg)
local action, conditionals = CleveRoids.GetParsedMsg(msg)
if action ~= "" or not next(conditionals) then
CleveRoids.Hooks.TARGET_SlashCmd(msg)
return true
end
-- Validate a *unit token* against parsed conditionals
local function IsGuidValid(unitTok, conds)
if not unitTok or not UnitExists(unitTok) or UnitIsDeadOrGhost(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
----------------------------------------------------------------
-- FAST-PATH: explicit @unit (e.g. [@mouseover], [@focus], [@party1])
----------------------------------------------------------------
do
local unitTok = conditionals.target
-- Resolve @mouseover to an actual unit token that exists (works on pfUI frames)
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
-- Resolve @focus via pfUI focus emulation if present
if unitTok == "focus" and pfUI and pfUI.uf and pfUI.uf.focus
and pfUI.uf.focus.label and pfUI.uf.focus.id then
local fTok = pfUI.uf.focus.label .. pfUI.uf.focus.id
if UnitExists(fTok) then unitTok = fTok else unitTok = nil end
end
-- If explicit unit resolves and passes conditionals, target it now (works out of range)
if unitTok and UnitExists(unitTok) and IsGuidValid(unitTok, conditionals) then
TargetUnit(unitTok)
return true
end
end
----------------------------------------------------------------
-- 1) Keep current target if already valid
if UnitExists("target") and IsGuidValid("target", conditionals) then
return true
end
-- 2) Build candidates: party1..4 and raid1..40 (not mutually exclusive)
local candidates = {}
-- Party
for i = 1, 4 do
local u = "party"..i
if UnitExists(u) then table.insert(candidates, { unitId = u }) end
end
-- Raid (all 1..40)
for i = 1, 40 do
local u = "raid"..i
if UnitExists(u) then table.insert(candidates, { unitId = u }) end
end
-- Optional: also consider targettarget and mouseover if present
if UnitExists("targettarget") then table.insert(candidates, { unitId = "targettarget" }) end
if UnitExists("mouseover") then table.insert(candidates, { unitId = "mouseover" }) end
-- 3) Find first valid and target it
for _, c in ipairs(candidates) do
if IsGuidValid(c.unitId, conditionals) then
TargetUnit(c.unitId)
return true
end
end
-- 4) Nothing found; preserve original target
return true
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)
local handled = false
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, true, action) then
handled = true
break
end
end
return handled
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
local handled = false
local action = function()
if not UnitExists("target") or UnitIsDead("target") then TargetNearestEnemy() end
if not CleveRoids.CurrentSpell.autoAttack and not CleveRoids.CurrentSpell.autoAttackLock and UnitExists("target") and UnitCanAttack("player", "target") then
CleveRoids.CurrentSpell.autoAttackLock = true
CleveRoids.autoAttackLockElapsed = GetTime()
AttackTarget()
end
end
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
-- We pass 'nil' for the hook, so DoWithConditionals does nothing if it fails to parse conditionals.
if CleveRoids.DoWithConditionals(v, nil, CleveRoids.FixEmptyTarget, false, action) then
handled = true
break
end
end
return handled
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
local handled = false
local action = function()
if CleveRoids.CurrentSpell.autoAttack and UnitExists("target") then
AttackTarget()
CleveRoids.CurrentSpell.autoAttack = false
end
end
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
if CleveRoids.DoWithConditionals(v, nil, CleveRoids.FixEmptyTarget, false, action) then
handled = true
break
end
end
return handled
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
local handled = false
local action = function()
SpellStopCasting()
end
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
if CleveRoids.DoWithConditionals(v, nil, CleveRoids.FixEmptyTarget, false, action) then
handled = true
break
end
end
return handled
end
-- Attempts to use or equip an item from the player's inventory 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)
local handled = false
local action = function(msg)
-- 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
UseInventoryItem(slotId)
return -- Exit after using the item by slot.
end
-- Original logic: if it's not a slot number, try to resolve by name.
local item = CleveRoids.GetItem(msg)
if item and item.inventoryID then
-- This is for using an already-equipped item (like a trinket).
-- This action does not cause an inventory change that needs a fast re-index.
return UseInventoryItem(item.inventoryID)
elseif item and item.bagID then
-- This will use an item from a bag. It could be a potion (use) or a weapon (equip).
-- We need to check if it's an equippable item before using it.
local isEquippable = false
local itemName, _, _, _, _, _, _, _, itemEquipLoc = GetItemInfo(msg)
if itemName and itemEquipLoc and itemEquipLoc ~= "" then
isEquippable = true
end
CleveRoids.GetNextBagSlotForUse(item, msg)
UseContainerItem(item.bagID, item.slot)
-- If it was an equippable item, force a cache refresh on the next inventory event.
if isEquippable then
CleveRoids.lastItemIndexTime = 0
end
return
end
if (MerchantFrame:IsVisible() and MerchantFrame.selectedTab == 1) then return end
end
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
v = string.gsub(v, "^%?", "")
local subject = v
local _,e = string.find(v,"%]")
if e then subject = CleveRoids.Trim(string.sub(v,e+1)) end
local wasHandled = false
-- If the subject is not a number, check if it's a spell.
if (not tonumber(subject)) and CleveRoids.GetSpell(subject) then
wasHandled = CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.CAST_SlashCmd, CleveRoids.FixEmptyTarget, not CleveRoids.hasSuperwow, CastSpellByName)
else
-- Otherwise, treat it as an item (by name or slot ID).
wasHandled = CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action)
end
if wasHandled then
handled = true
break
end
end
return handled -- Corrected typo from 'Handled' to 'handled'
end
function CleveRoids.EquipBagItem(msg, offhand)
-- First, get item data from the addon's own reliable cache.
local item = CleveRoids.GetItem(msg)
if not item or not item.name then
-- If the addon can't find the item at all, do nothing.
return false
end
local invslot = offhand and 17 or 16 -- 17 is off-hand, 16 is main-hand
-- Now, check the currently equipped item using live game data.
local currentItemLink = GetInventoryItemLink("player", invslot)
if currentItemLink then
local currentItemName = GetItemInfo(currentItemLink)
if currentItemName and currentItemName == item.name then
-- The correct item is already in the correct slot. Stop here.
return true
end
end
-- If the check above fails, proceed with the original logic to equip the item.
-- We can reuse the 'item' object we already found.
if not item.bagID and not item.inventoryID then
return false
end
if item.bagID then
CleveRoids.GetNextBagSlotForUse(item, msg)
PickupContainerItem(item.bagID, item.slot)
else
PickupInventoryItem(item.inventoryID)
end
EquipCursorItem(invslot)
ClearCursor()
CleveRoids.lastItemIndexTime = 0
return true
end
-- TODO: Refactor all these DoWithConditionals sections
function CleveRoids.DoEquipMainhand(msg)
local handled = false
local action = function(msg)
return CleveRoids.EquipBagItem(msg, false)
end
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
v = string.gsub(v, "^%?", "")
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then
handled = true
break
end
end
return handled
end
function CleveRoids.DoEquipOffhand(msg)
local handled = false
local action = function(msg)
return CleveRoids.EquipBagItem(msg, true)
end
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
v = string.gsub(v, "^%?", "")
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then
handled = true
break
end
end
return handled
end
function CleveRoids.DoUnshift(msg)
local handled
local action = function(msg)
local currentShapeshiftIndex = CleveRoids.GetCurrentShapeshiftIndex()
if currentShapeshiftIndex ~= 0 then
CastShapeshiftForm(currentShapeshiftIndex)
end
end
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
handled = false
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then
handled = true
break
end
end
if handled == nil then
action()
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
function CleveRoids.DoStopMacro(msg)
local handled = false
for k, v in pairs(CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))) do
if CleveRoids.DoWithConditionals(msg, nil, nil, not CleveRoids.hasSuperwow, "STOPMACRO") then
handled = true -- we parsed at least one command
break
end
end
return handled
end
function CleveRoids.DoCastSequence(sequence)
if not CleveRoids.hasSuperwow then
CleveRoids.Print("|cFFFF0000/castsequence|r requires |cFF00FFFFSuperWoW|r.")
return
end
if CleveRoids.currentSequence and not CleveRoids.CheckSpellCast("player") then
CleveRoids.currentSequence = nil
elseif CleveRoids.currentSequence then
return
end
if sequence.index > 1 then
if sequence.reset then
for k, _ in sequence.reset do
if CleveRoids.kmods[k] and CleveRoids.kmods[k]() then
CleveRoids.ResetSequence(sequence)
end
end
end
end
local active = CleveRoids.GetCurrentSequenceAction(sequence)
if active and active.action then
sequence.status = 0
sequence.lastUpdate = GetTime()
sequence.expires = 0
CleveRoids.currentSequence = sequence
local action = (sequence.cond or "") .. active.action
local result = CleveRoids.DoWithConditionals(action, nil, nil, not CleveRoids.hasSuperwow, CastSpellByName)
return result
end
end
CleveRoids.DoConditionalCancelAura = function(msg)
local trimmedMsg = CleveRoids.Trim(msg or "")
if trimmedMsg == "" then
return false
end
if CleveRoids.DoWithConditionals(trimmedMsg, nil, CleveRoids.FixEmptyTarget, false, CleveRoids.CancelAura) then
return true
else
return false
end
end
function CleveRoids.OnUpdate(self)
local time = GetTime()
local refreshRate = CleveRoidMacros.refresh or 5
refreshRate = 1/refreshRate
if CleveRoids.initializationTimer and time >= CleveRoids.initializationTimer then
CleveRoids.IndexItems()
CleveRoids.IndexActionBars()
CleveRoids.ready = true
CleveRoids.initializationTimer = nil
CleveRoids.TestForAllActiveActions()
CleveRoids.lastUpdate = time
return
end
if not CleveRoids.ready then return end
-- Throttle the update loop to avoid excessive CPU usage.
if (time - CleveRoids.lastUpdate) < refreshRate then return end
CleveRoids.lastUpdate = time
-- Check the saved variable to decide which update mode to use.
if CleveRoidMacros.realtime == 1 then
-- Realtime Mode: Force an update on every throttled tick for maximum responsiveness.
CleveRoids.TestForAllActiveActions()
else
-- Event-Driven Mode (Default): Only update if a relevant game event has queued it.
if CleveRoids.isActionUpdateQueued then
CleveRoids.TestForAllActiveActions()
CleveRoids.isActionUpdateQueued = false -- Reset the flag after updating
end
end
-- The rest of this function handles time-based logic that must always run.
if CleveRoids.CurrentSpell.autoAttackLock and (time - CleveRoids.autoAttackLockElapsed) > refreshRate then
CleveRoids.CurrentSpell.autoAttackLock = false
CleveRoids.autoAttackLockElapsed = nil
end
for _, sequence in pairs(CleveRoids.Sequences) do
if sequence.index > 1 and sequence.reset.secs and (time - (sequence.lastUpdate or 0)) >= sequence.reset.secs then
CleveRoids.ResetSequence(sequence)
end
end
for guid,cast in pairs(CleveRoids.spell_tracking) do
if time > cast.expires then
CleveRoids.spell_tracking[guid] = nil
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)
local actions = CleveRoids.GetAction(slot)
local action_to_display_info = nil
if actions then
if actions.active then
action_to_display_info = actions.active
elseif actions.tooltip then
action_to_display_info = actions.tooltip
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 then
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
local rank_info = current_spell_data.rank or (current_spell_data.highest and current_spell_data.highest.rank)
if rank_info and rank_info ~= "" then
GameTooltipTextRight1:SetText("|cff808080" .. rank_info .. "|r")
else
GameTooltipTextRight1:SetText("")
end
GameTooltipTextRight1:Show()
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 then
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
local rank_info = current_spell_data.rank or (current_spell_data.highest and current_spell_data.highest.rank)
if rank_info and rank_info ~= "" then
GameTooltipTextRight1:SetText("|cff808080" .. rank_info .. "|r")
else
GameTooltipTextRight1:SetText("")
end
GameTooltipTextRight1:Show()
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)
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)
local actions = CleveRoids.GetAction(slot)
if actions and actions.active then
return (1 and actions.active.inRange ~= -1 or nil)
else
return CleveRoids.Hooks.ActionHasRange(slot)
end
end
CleveRoids.Hooks.IsActionInRange = IsActionInRange
function IsActionInRange(slot, unit)
local actions = CleveRoids.GetAction(slot)
if actions and actions.active and actions.active.type == "spell" then
return actions.active.inRange
else
return CleveRoids.Hooks.IsActionInRange(slot, unit)
end
end
CleveRoids.Hooks.OriginalIsUsableAction = IsUsableAction
CleveRoids.Hooks.IsUsableAction = IsUsableAction
function IsUsableAction(slot, unit)
local actions = CleveRoids.GetAction(slot)
if actions and actions.active then
return actions.active.usable, actions.active.oom
else
return CleveRoids.Hooks.IsUsableAction(slot, unit)
end
end
CleveRoids.Hooks.IsCurrentAction = IsCurrentAction
function IsCurrentAction(slot)
local active = CleveRoids.GetActiveAction(slot)
if not active then
return CleveRoids.Hooks.IsCurrentAction(slot)
else
local name
if active.spell then
local rank = active.spell.rank or active.spell.highest.rank
name = active.spell.name..(rank and ("("..rank..")"))
elseif active.item then
name = active.item.name
end
return CleveRoids.Hooks.IsCurrentAction(CleveRoids.GetProxyActionSlot(name) or slot)
end
end
CleveRoids.Hooks.GetActionTexture = GetActionTexture
function GetActionTexture(slot)
local actions = CleveRoids.GetAction(slot)
if actions and (actions.active or actions.tooltip) then
local proxySlot = (actions.active and actions.active.spell) and CleveRoids.GetProxyActionSlot(actions.active.spell.name)
if proxySlot and CleveRoids.Hooks.GetActionTexture(proxySlot) ~= actions.active.spell.texture then
return CleveRoids.Hooks.GetActionTexture(proxySlot)
else
return (actions.active and actions.active.texture) or (actions.tooltip and actions.tooltip.texture) or CleveRoids.unknownTexture
end
end
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)
local actions = CleveRoids.GetAction(slot)
-- Check for actions.active OR actions.tooltip
if actions and (actions.active or actions.tooltip) then
-- 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
return 0, 0, 0
else
return CleveRoids.Hooks.GetActionCooldown(slot)
end
end
CleveRoids.Hooks.GetActionCount = GetActionCount
function GetActionCount(slot)
local action = CleveRoids.GetAction(slot)
local count
if action and action.active then
local slotId = tonumber(action.active.action)
if slotId and slotId >= 1 and slotId <= 19 then
return GetInventoryItemCount("player", slotId)
end
if action.active.item then
count = action.active.item.count
elseif action.active.spell and action.active.spell.reagent then
local reagent = CleveRoids.GetItem(action.active.spell.reagent)
count = reagent and reagent.count
end
end
return count or CleveRoids.Hooks.GetActionCount(slot)
end
CleveRoids.Hooks.IsConsumableAction = IsConsumableAction
function IsConsumableAction(slot)
local action = CleveRoids.GetAction(slot)
if action and action.active then
local slotId = tonumber(action.active.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 action.active.item and
(CleveRoids.countedItemTypes[action.active.item.type]
or CleveRoids.countedItemTypes[action.active.item.name])
then
return 1
end
if action.active.spell and action.active.spell.reagent then
return 1
end
end
return CleveRoids.Hooks.IsConsumableAction(slot)
end
-- Create a hidden tooltip frame to read buff names
if not AuraScanTooltip and not CleveRoids.hasSuperwow then
CreateFrame("GameTooltip", "AuraScanTooltip")
AuraScanTooltip:SetOwner(WorldFrame, "ANCHORNONE")
AuraScanTooltip:AddFontStrings(
AuraScanTooltip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText"),
AuraScanTooltip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
)
end
-- This single dummy frame handles events AND serves as our tooltip scanner.
CleveRoids.Frame = CreateFrame("GameTooltip")
-- 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)
-- == CORE EVENT REGISTRATION ==
CleveRoids.Frame:RegisterEvent("PLAYER_LOGIN")
CleveRoids.Frame:RegisterEvent("ADDON_LOADED")
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")
-- == 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")
CleveRoids.Frame:RegisterEvent("PLAYER_LEAVE_COMBAT")
CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
CleveRoids.Frame:RegisterEvent("UNIT_AURA")
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH")
CleveRoids.Frame:RegisterEvent("UNIT_POWER")
if CleveRoids.hasSuperwow then
CleveRoids.Frame:RegisterEvent("UNIT_CASTEVENT")
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")
function CleveRoids.Frame:PLAYER_LOGIN()
_, CleveRoids.playerClass = UnitClass("player")
_, CleveRoids.playerGuid = UnitExists("player")
CleveRoids.IndexSpells()
CleveRoids.initializationTimer = GetTime() + 1.5
if not CleveRoids.hasSuperwow or not IsSpellInRange then
if not CleveRoids.hasSuperwow then
CleveRoids.Print("|cFFFF0000CleveRoidMacros|r requires |cFF00FFFFbalakethelock's SuperWoW|r:")
CleveRoids.Print("https://github.com/balakethelock/SuperWoW")
end
if not IsSpellInRange then
CleveRoids.Print("|cFFFF0000CleveRoidMacros|r requires |cFF00FFFFpepopo978's Nampower|r:")
CleveRoids.Print("https://github.com/pepopo978/nampower")
end
CleveRoids.DisableAddon("Missing Requirements")
return
else
CleveRoids.Print("|cFF4477FFCleveR|r|cFFFFFFFFoid Macros|r |cFF00FF00Loaded|r - See the README.")
end
end
function CleveRoids.Frame:ADDON_LOADED(addon)
if addon ~= "CleveRoidMacros" then
return
end
CleveRoids.InitializeExtensions()
if SuperMacroFrame then
local hooks = {
cast = { action = CleveRoids.DoCast },
target = { action = CleveRoids.DoTarget },
use = { action = CleveRoids.DoUse },
castsequence = { action = CleveRoids.DoCastSequence }
}
-- Hook SuperMacro's RunLine to stay compatible
CleveRoids.Hooks.RunLine = RunLine
CleveRoids.RunLine = function(...)
for i = 1, arg.n do
if CleveRoids.stopmacro then
CleveRoids.stopmacro = false
return true
end
local intercepted = false
local text = arg[i]
for k,v in pairs(hooks) do
local begin, _end = string.find(text, "^/"..k.."%s+[!%[]")
if begin then
local msg = string.sub(text, _end)
v.action(msg)
intercepted = true
break
end
end
if not intercepted then
CleveRoids.Hooks.RunLine(text)
end
end
end
RunLine = CleveRoids.RunLine
end
end
function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time)
if action == "MAINHAND" or action == "OFFHAND" then return 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 }
elseif cast
and (
(cast.spell_id == spell_id and (action == "FAIL" or action == "CAST"))
or (GetTime() > cast.expires)
)
then
CleveRoids.spell_tracking[caster] = nil
end
-- handle cast sequence
if CleveRoids.currentSequence and caster == CleveRoids.playerGuid then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
local name, rank = SpellInfo(spell_id)
local isSeqSpell = (active.action == name or active.action == (name.."("..rank..")"))
if isSeqSpell then
local status = CleveRoids.currentSequence.status
if status == 0 and (action == "START" or action == "CHANNEL") and cast_time > 0 then
CleveRoids.currentSequence.status = 1
CleveRoids.currentSequence.expires = GetTime() + cast_time - 2000
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
function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
CleveRoids.CurrentSpell.type = "channeled"
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:SPELLCAST_CHANNEL_STOP()
CleveRoids.CurrentSpell.type = ""
CleveRoids.CurrentSpell.spellName = ""
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
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
for _, sequence in pairs(CleveRoids.Sequences) do
if CleveRoids.currentSequence ~= sequence and sequence.index > 1 and sequence.reset.combat then
CleveRoids.ResetSequence(sequence)
end
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:PLAYER_TARGET_CHANGED()
CleveRoids.CurrentSpell.autoAttack = false
CleveRoids.CurrentSpell.autoAttackLock = false
for _, sequence in pairs(CleveRoids.Sequences) do
if CleveRoids.currentSequence ~= sequence and sequence.index > 1 and sequence.reset.target then
CleveRoids.ResetSequence(sequence)
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.IndexActionBars()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:SPELLS_CHANGED()
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()
local now = GetTime()
-- Only index items if more than 1 second has passed since the last index
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.ParsedMsg = {}
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
end
function CleveRoids.Frame:UNIT_INVENTORY_CHANGED()
if arg1 ~= "player" then return end
CleveRoids.Frame:BAG_UPDATE()
end
function CleveRoids.Frame:START_AUTOREPEAT_SPELL()
local _, className = UnitClass("player")
if className == "HUNTER" 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 _, className = UnitClass("player")
if className == "HUNTER" 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
end
function CleveRoids.Frame:UNIT_AURA()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:UNIT_HEALTH()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:UNIT_POWER()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
CleveRoids.Hooks.SendChatMessage = SendChatMessage
function SendChatMessage(msg, ...)
if msg and string.find(msg, "^#showtooltip") then
return
end
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
-- Bandaid so pfUI doesn't need to be edited
-- pfUI/modules/thirdparty-vanilla.lua:914
CleverMacro = true
---- START of pfUI Focus Fix ----
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
---- END of pfUI Focus Fix ----
SLASH_CLEVEROID1 = "/cleveroid"
SLASH_CLEVEROID2 = "/cleveroidmacros"
SlashCmdList["CLEVEROID"] = function(msg)
if type(msg) ~= "string" then
msg = ""
end
local cmd, val
local s, e, a, b = string.find(msg, "^(%S*)%s*(%S*)$")
if a then cmd = a else cmd = "" end
if b then val = b else val = "" end
-- No command: show current value
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)")
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.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
-- Unknown command fallback
CleveRoids.Print("Usage:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates rather than event based 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)")
end