Files
SuperCleveRoidMacros/Init.lua
T
Brues eddc49fc60 Make [button:N] mean the invoking button, as retail does
[button:N] was a held-state modifier: IsMouseButtonDown(name), true while you
physically hold that button. Retail's is a property of the activation -- which
button clicked the action button -- and a keybind press counts as button 1
because it activates through the left-click path. Different in kind, and the
visible break was [button:1] Rejuvenation; Regrowth casting Regrowth from a
keybind, where retail casts Rejuvenation.

It now reads ClassicAPI's GetMouseButtonClicked, defaulting to LeftButton when
no click dispatch is running. That default IS the keybind rule: Button:Click()
with no argument reports LeftButton, and a macro run from anywhere else answers
button 1 the same way.

This also reverts 891467c. That commit refreshed the action bar on
GLOBAL_MOUSE_DOWN/UP so the icon could track the held state -- under the real
semantics there is no such state to track, and the icon correctly sits on
whatever button 1 resolves to. PLAYER_TOTEM_UPDATE stays; it shared the comment
and inherits it.

Requires ClassicAPI v1.15.8, up from v1.15.0. v1.15.8 scoped
GetMouseButtonClicked to the click dispatch; before it a HELD button kept the
value alive (by design -- OnDragStart reads it), so turning the camera with
right-click down made every keybind press read RightButton. The function exists
below v1.15.8 and answers wrongly, and a conditional that is quietly wrong is
worse than one that warns, so the floor moved rather than the conditional being
gated.

Bare [button] / [nobutton] survive as an extension retail has no equivalent for,
now meaning "a click dispatch is running" -- the practical "activated by a
keybind, not a click" test, and no longer leaning on anything but the documented
nil.
2026-09-15 01:17:06 -05:00

411 lines
17 KiB
Lua

local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
_G.CleveRoids = CleveRoids
-- Global flag for other addons to detect SRCM regardless of folder name
-- (pfUI macrotweak checks IsAddOnLoaded("SuperCleveRoidMacros") which fails
-- if the folder was renamed, e.g. "SuperCleveRoidMacros-main" from GitHub)
_G.SRCM_LOADED = true
CleveRoids.ready = false
CleveRoids.Hooks = CleveRoids.Hooks or {}
CleveRoids.Hooks.GameTooltip = {}
CleveRoids.Extensions = CleveRoids.Extensions or {}
CleveRoids.actionEventHandlers = {}
CleveRoids.mouseOverResolvers = {}
CleveRoids.mouseoverUnit = CleveRoids.mouseoverUnit or nil
CleveRoids.mouseOverUnit = nil
-- Every macro slot the client can hold: 18 account-wide (1-18) followed by 18
-- character-specific (19-36). This is the index space GetMacroInfo and
-- C_Macro.SetMacroDisplay address, and it is fixed -- GetNumMacros() returns how
-- many of each tab are *used*, which cannot be summed into a range, because the
-- character block starts at 19 no matter how few account macros exist. Blizzard's
-- own MAX_MACROS is no help either: it lives in the load-on-demand Blizzard_MacroUI
-- and is nil until the player opens the macro window.
CleveRoids.MAX_MACRO_SLOTS = 36
-- Environment flags
CleveRoids.hasSuperwow = SetAutoloot and true or false
CleveRoids.hasTurtle = (type(_G.TURTLE_WOW_VERSION) ~= "nil")
CleveRoids.supported = CleveRoids.hasTurtle
-- ClassicAPI macro display (C_Macro.SetMacroDisplay): hand ClassicAPI the resolved
-- action per macro instead of replacing the action-bar globals in Lua, so the icon,
-- tooltip, cooldown sweep, range and usable state all come from the client -- including
-- the drag cursor and the macro window grid, which Lua cannot reach.
--
-- ClassicAPI stands down from macro display entirely when it sees this addon loaded;
-- ClassicAPIMacroDisplay is what tells it we drive it instead, and ReleaseDisplays
-- clears it to hand every macro back. A fork that leaves the flag unset keeps the old
-- behavior -- both must never drive the same buttons. It doubles as the internal
-- "may we call C_Macro.SetMacroDisplay" guard, so the two can never disagree.
--
-- Feature-detect rather than version-check: SetMacroDisplay ships in ClassicAPI
-- v1.15.0, below this addon's minimum, so a nil here means the client mod is
-- missing outright -- the case Core.lua's requirement check warns about but
-- keeps running.
CleveRoids.ClassicAPIMacroDisplay =
(type(C_Macro) == "table" and C_Macro.SetMacroDisplay ~= nil) and true or false
CleveRoids.ParsedMsg = {}
CleveRoids.ExpandedGroups = {}
CleveRoids.Items = {}
CleveRoids.Spells = {}
CleveRoids.PetSpells = {}
CleveRoids.Talents = {}
CleveRoids.Cooldowns = {}
CleveRoids.Macros = {}
CleveRoids.Actions = {}
CleveRoids.Sequences = {}
CleveRoids.lastUpdate = 0
CleveRoids.lastGetItem = nil
CleveRoids.currentSequence = nil
CleveRoids.bookTypes = {BOOKTYPE_SPELL, BOOKTYPE_PET}
CleveRoids.unknownTexture = "Interface\\Icons\\INV_Misc_QuestionMark"
CleveRoids.spell_tracking = {}
-- GUID-based cast tracking, populated from our own SPELL_START handlers and pruned
-- in OnUpdate. Format: [casterGuid] = {spellID, spellName, icon, startTime, duration,
-- endTime}
CleveRoids.castTracking = {}
-- Combo point tracking (initialized early for /cast hook)
CleveRoids.lastComboPoints = 0
CleveRoids.lastComboPointsTime = 0
-- Resist tracking state
-- Structure: { resistType = "full"|"partial", targetGUID = guid }
CleveRoids.resistState = nil
-- KEY_DOWN/KEY_UP state table (populated when Nampower v2.41+ hasKeyEvents)
CleveRoids._keyState = {}
-- Holds information about the currently cast spell
CleveRoids.CurrentSpell = {
-- "channeled" or "cast"
type = "",
-- the name of the spell
spellName = "",
-- is the Attack ability enabled
autoAttack = false,
-- is the Auto Shot ability enabled
autoShot = false,
-- is the Shoot ability (wands) enabled
wand = false,
}
-- Enhanced casting state tracking
CleveRoids.UpdateCastingState = function()
if not GetCurrentCastingInfo then return false end
local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo()
-- Update CurrentSpell based on actual cast state
-- NOTE: Channel state is EXCLUSIVELY managed by SPELLCAST_CHANNEL_START/STOP events
-- This function NEVER touches channel state, only regular casts
if casting == 1 then
CleveRoids.CurrentSpell.type = "cast"
CleveRoids.CurrentSpell.castingSpellId = castId
elseif CleveRoids.CurrentSpell.type == "cast" then
-- Only clear if we were in a regular cast (not channel)
CleveRoids.CurrentSpell.type = ""
CleveRoids.CurrentSpell.castingSpellId = nil
end
-- DO NOT touch channel state here - events handle it
-- Always update metadata from GetCurrentCastingInfo (onswing/autoattack only here)
CleveRoids.CurrentSpell.autoAttack = (autoattack == 1)
CleveRoids.CurrentSpell.onSwingPending = (onswing == 1)
CleveRoids.CurrentSpell.visualSpellId = visId
CleveRoids.CurrentSpell.autoRepeatSpellId = autoId
-- Enhanced timing data from GetCastInfo (Nampower 2.18+)
if GetCastInfo then
local ok, info = pcall(GetCastInfo)
if ok and info then
CleveRoids.CurrentSpell.castRemainingMs = info.castRemainingMs
CleveRoids.CurrentSpell.castEndTime = info.castEndS
CleveRoids.CurrentSpell.gcdRemainingMs = info.gcdRemainingMs
CleveRoids.CurrentSpell.gcdEndTime = info.gcdEndS
else
CleveRoids.CurrentSpell.castRemainingMs = nil
CleveRoids.CurrentSpell.castEndTime = nil
CleveRoids.CurrentSpell.gcdRemainingMs = nil
CleveRoids.CurrentSpell.gcdEndTime = nil
end
end
return true
end
CleveRoids.dynamicCmds = {
["/cast"] = true,
["/castpet"] = true,
["/castsequence"] = true,
["/use"] = true,
["/feedpet"] = true,
["/equip"] = true,
["/equipmh"] = true,
["/equipoh"] = true,
["/equip11"] = true,
["/equip12"] = true,
["/equip13"] = true,
["/equip14"] = true,
["/applymain"] = true,
["/applyoff"] = true,
}
-- Equipment swap queue system
CleveRoids.equipmentQueue = {}
CleveRoids.equipmentQueueLen = 0 -- PERFORMANCE: Track length to avoid table.getn() every frame
CleveRoids.lastEquipTime = {}
CleveRoids.lastGlobalEquipTime = 0
CleveRoids.EQUIP_COOLDOWN = 1.5 -- Per-slot cooldown
CleveRoids.EQUIP_GLOBAL_COOLDOWN = 0.5 -- Global cooldown
-- PERFORMANCE: Table pool for queue entries to reduce garbage collection
CleveRoids.queueEntryPool = {}
-- PERFORMANCE: Static buffer for proc removal to avoid per-frame allocation
CleveRoids._procRemovalBuffer = {}
-- PERFORMANCE: Static buffer for action grouping to avoid per-call allocation
CleveRoids._actionsToSlotsBuffer = {}
CleveRoids._slotsBuffer = {}
CleveRoids._actionsListBuffer = {}
-- PERFORMANCE: Static buffer for arg backup in SendEventForAction
CleveRoids._originalArgsBuffer = {}
-- KEYED DEBUG: Only prints when the message for a given key changes from last print.
-- Usage: CleveRoids.DebugChanged("immunity_moonfire", formatted_msg)
-- Prevents spam when the same state is reported repeatedly (e.g., per-frame or per-eval).
CleveRoids._lastDebugState = {}
function CleveRoids.DebugChanged(key, msg)
if not CleveRoids.debug then return end
if CleveRoids._lastDebugState[key] == msg then return end
CleveRoids._lastDebugState[key] = msg
DEFAULT_CHAT_FRAME:AddMessage(msg)
end
-- Spell queue state (Nampower)
CleveRoids.queuedSpell = nil
CleveRoids.lastCastSpell = nil
-- Macro execution control
CleveRoids.stopMacroFlag = false
-- PERFORMANCE: Event-driven cached state (updated on events, not polled)
CleveRoids._cachedPlayerInCombat = nil -- Updated on PLAYER_REGEN_DISABLED, PLAYER_REGEN_ENABLED
CleveRoids.ignoreKeywords = {
action = true,
ignoretooltip = true,
cancelaura = true,
noSpam = true, -- ! prefix flag: prevent toggle-off at execution time
_operators = true, -- Metadata for AND/OR operator tracking
_groups = true, -- Grouped conditional values for AND/OR evaluation
multiscan = true, -- Processed before Keywords loop (target resolution)
cursor = true, -- Modifier: place ground-target spell/item at cursor (CastAtCursor)
stopattack = true, -- Post-cast modifier: stop autoattack after cast (CheapShot pattern)
}
-- Deprecated conditional names, rewritten to their current keyword in ParseMsg
-- before anything downstream (evaluation, _groups) sees them. This is the path
-- for renamed *modifiers* -- ignoreKeywords entries, which have no Keywords
-- predicate that an alias could just point at the way [stl] does for [stealth].
-- MacroErrorChecker also reads this table, so the old names stay valid syntax.
CleveRoids.conditionalAliases = {
mouseuse = "cursor", -- pre-ClassicAPI name, back when it clicked the AoE reticle
}
-- TODO: Localize?
CleveRoids.countedItemTypes = {
["Consumable"] = true,
["Reagent"] = true,
["Projectile"] = true,
["Trade Goods"] = true,
}
-- TODO: Localize?
CleveRoids.actionSlots = {}
CleveRoids.reactiveSlots = {}
CleveRoids.reactiveSpells = {
["Revenge"] = true,
["Overpower"] = true,
["Riposte"] = true,
["Surprise Attack"] = true,
["Lacerate"] = true,
["Baited Shot"] = true,
["Counterattack"] = true,
["Arcane Surge"] = true,
["Aquatic Form"] = true,
}
-- PERFORMANCE: Static lookup for toggled buff abilities (built once, used per-frame)
CleveRoids._toggledBuffAbilities = {
[CleveRoids.Localized.Spells["Prowl"]] = true,
[CleveRoids.Localized.Spells["Shadowmeld"]] = true,
}
function CleveRoids.IsToggledBuffAbility(spellName)
return CleveRoids._toggledBuffAbilities[spellName]
end
CleveRoids.auraTextures = {
[CleveRoids.Localized.Spells["Stealth"]] = "Interface\\Icons\\Ability_Stealth",
[CleveRoids.Localized.Spells["Prowl"]] = "Interface\\Icons\\Spell_Nature_Invisibilty",
[CleveRoids.Localized.Spells["Shadowform"]] = "Interface\\Icons\\Spell_Shadow_Shadowform",
[CleveRoids.Localized.Spells["Shadowmeld"]] = "Interface\\Icons\\Spell_Nature_WispSplode",
["Seal of Wisdom"] = "Interface\\Icons\\Spell_Holy_RighteousnessAura",
["Seal of the Crusader"] = "Interface\\Icons\\Spell_Holy_HolySmite",
["Seal of Light"] = "Interface\\Icons\\Spell_Holy_HealingAura",
["Seal of the Justice"] = "Interface\\Icons\\Spell_Holy_SealOfWrath",
["Seal of Righteousness"] = "Interface\\Icons\\Ability_ThunderBolt",
["Seal of Command"] = "Interface\\Icons\\Ability_Warrior_InnerRage",
}
-- I need to make a 2h modifier
-- Maps easy-to-use weapon type names (e.g. Axes, Shields) to their inventory
-- slot plus the locale-independent item class/subclass IDs that identify them
-- (read via C_Item.GetItemInfoInstant). class 2 = Weapon, 4 = Armor (shields).
-- subClass is a set because the logical "Axes"/"Swords"/"Maces" types span both
-- the one-handed and two-handed weapon subclasses.
CleveRoids.WeaponTypeNames = {
Daggers = { slot = "MainHandSlot", class = 2, subClass = { [15] = true } },
Fists = { slot = "MainHandSlot", class = 2, subClass = { [13] = true } },
Axes = { slot = "MainHandSlot", class = 2, subClass = { [0] = true, [1] = true } },
Swords = { slot = "MainHandSlot", class = 2, subClass = { [7] = true, [8] = true } },
Staves = { slot = "MainHandSlot", class = 2, subClass = { [10] = true } },
Maces = { slot = "MainHandSlot", class = 2, subClass = { [4] = true, [5] = true } },
Polearms = { slot = "MainHandSlot", class = 2, subClass = { [6] = true } },
-- OH
Daggers2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [15] = true } },
Fists2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [13] = true } },
Axes2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [0] = true, [1] = true } },
Swords2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [7] = true, [8] = true } },
Maces2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [4] = true, [5] = true } },
Shields = { slot = "SecondaryHandSlot", class = 4, subClass = { [6] = true } },
-- ranged
Guns = { slot = "RangedSlot", class = 2, subClass = { [3] = true } },
Crossbows = { slot = "RangedSlot", class = 2, subClass = { [18] = true } },
Bows = { slot = "RangedSlot", class = 2, subClass = { [2] = true } },
Thrown = { slot = "RangedSlot", class = 2, subClass = { [16] = true } },
Wands = { slot = "RangedSlot", class = 2, subClass = { [19] = true } },
}
-- Detect available features
CleveRoids.hasNampower = (QueueSpellByName ~= nil)
CleveRoids.hasUnitXP = pcall(UnitXP, "nop", "nop")
-- Extended Nampower feature flags (populated by NampowerAPI.lua)
CleveRoids.nampowerVersion = { major = 0, minor = 0, patch = 0 }
CleveRoids.hasExtendedNampower = false -- True if v2.12+ with new API functions
-- Feature detection messages
local function PrintFeatures()
local features = {}
if CleveRoids.hasSuperwow then table.insert(features, "SuperWoW") end
if CleveRoids.hasNampower then
local ver = CleveRoids.nampowerVersion
if ver.major > 0 then
table.insert(features, string.format("Nampower v%d.%d.%d", ver.major, ver.minor, ver.patch))
else
table.insert(features, "Nampower")
end
end
if CleveRoids.hasUnitXP then table.insert(features, "UnitXP") end
if CleveRoids.hasTurtle then table.insert(features, "Turtle") end
if table.getn(features) > 0 then
CleveRoids.Print("Enhanced features: " .. table.concat(features, ", "))
end
end
-- Immunity data version - increment this when changing immunity data format
-- This will cause all immunity data to be reset on addon update
-- v3: Fixed Master Strike false physical immunity recording (split CC spell handling)
-- v4: Fixed false immunity recording when target dies with spells in-flight (dead = IMMUNE)
-- v5: Fixed false physical immunity from unknown spell schools (now uses DBC lookup; unknown defaults to nil not "physical")
-- v6: Reset stale immunity data that may contain false positives
CleveRoids.IMMUNITY_DATA_VERSION = 6
-- Call on next frame to ensure everything is loaded
local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:SetScript("OnEvent", function()
this:UnregisterAllEvents()
-- Check immunity data version and reset if outdated
CleveRoidMacros = CleveRoidMacros or {}
CleveRoids_ImmunityData = CleveRoids_ImmunityData or {}
local savedVersion = CleveRoidMacros.immunityDataVersion or 0
if savedVersion < CleveRoids.IMMUNITY_DATA_VERSION then
-- Check if there was existing data to clear
local hadData = next(CleveRoids_ImmunityData) ~= nil
-- Version changed - reset all immunity data
CleveRoids_ImmunityData = {}
CleveRoidMacros.immunityDataVersion = CleveRoids.IMMUNITY_DATA_VERSION
if hadData then
-- Show message if we actually cleared existing data
CleveRoids.Print("|cffff9900Immunity data reset|r - addon updated to data version " .. CleveRoids.IMMUNITY_DATA_VERSION)
end
end
-- Detect pfUI macrotweak conflict (folder name mismatch)
-- pfUI macrotweak disables itself via IsAddOnLoaded("SuperCleveRoidMacros"),
-- but this fails when the addon folder is renamed (e.g. GitHub download adds "-main").
-- When both are active: conflicting SendChatMessage hooks, duplicate /use and /equip
-- handlers, and #showtooltip can leak into chat (pfUI only filters "#showtooltip "
-- with trailing space, missing bare "#showtooltip").
if pfUI and pfUI.module and pfUI.module["macrotweak"]
and not IsAddOnLoaded("SuperCleveRoidMacros") then
CleveRoids.Print("|cffff0000WARNING:|r Your addon folder name is not |cff00ff00SuperCleveRoidMacros|r.")
CleveRoids.Print("This causes a conflict with pfUI's macrotweak module.")
CleveRoids.Print("Please rename the folder to exactly |cff00ff00SuperCleveRoidMacros|r and /reload.")
end
-- Initialize NampowerAPI if available
if CleveRoids.NampowerAPI then
local API = CleveRoids.NampowerAPI
-- Get version info
local major, minor, patch = API.GetVersion()
CleveRoids.nampowerVersion = { major = major, minor = minor, patch = patch }
-- Check for extended API (v2.12+)
CleveRoids.hasExtendedNampower = API.HasMinimumVersion(2, 12, 0)
-- Sync feature flags
if API.features then
CleveRoids.hasGetSpellRec = API.features.hasGetSpellRec
CleveRoids.hasGetItemStats = API.features.hasGetItemStats
CleveRoids.hasGetUnitData = API.features.hasGetUnitData
CleveRoids.hasGetSpellModifiers = API.features.hasGetSpellModifiers
CleveRoids.hasEnhancedSpellFunctions = API.features.hasEnhancedSpellFunctions
-- v2.37+: CastSpellByName supports unit token strings as 2nd param
CleveRoids.hasCastSpellByNameUnitToken = API.features.hasCastSpellByNameUnitToken
end
-- Initialize the API
API.Initialize()
end
PrintFeatures()
end)
_G["CleveRoids"] = CleveRoids