Files
SuperCleveRoidMacros/Init.lua
T
Brues c5ddc6c313 Publish resolved macro actions to ClassicAPI's C_Macro.SetMacroDisplay
Hands ClassicAPI the action this addon resolved for each macro, so the icon,
tooltip, cooldown sweep, range and usable state come from the client instead of
from replaced action-bar globals. That also reaches the drag cursor and the macro
window grid, which Lua cannot touch, and lets the client answer range for a macro
slot directly rather than borrowing a proxy slot.

- CleveRoids.useClassicAPIDisplay feature-detects C_Macro.SetMacroDisplay rather
  than checking CLASSIC_API_VERSION, which reports a dev sentinel while the API is
  unreleased. ClassicAPIMacroDisplay tells ClassicAPI we drive macro display, which
  it otherwise stands down from when it sees this addon.
- macro.actions.macroID back-references the Blizzard index, since the update loop
  walks actions objects rather than macros. nil for SuperMacro macros, which have
  no index and so cannot be published.
- PublishDisplay sends false, not nothing, when no action matched: that claims the
  macro and shows the question mark, where silence would hand it back to
  ClassicAPI's own #showtooltip parser.
- PublishAllDisplays runs once the addon is ready and after every re-parse, since
  ClassicAPI re-evaluates nothing for us. It covers macros that aren't on a bar,
  which is what keeps the macro window grid correct. Deferred past load because
  SetMacroDisplay returns false until the player is in the world.
- ReleaseDisplays hands every macro back on DisableAddon.

The per-slot ACTIONBAR_SLOT_CHANGED fan-out is kept for the non-integrated path.
Publishing repaints every slot holding the macro through the client's own notifier,
so it is redundant when the API is present, but removing it outright would leave
buttons never repainting on a ClassicAPI build without the API.
2026-09-10 14:40:16 -05:00

399 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
-- 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.
--
-- Feature-detect rather than version-check: the API is unreleased, so
-- CLASSIC_API_VERSION reports the dev sentinel. ClassicAPI stands down from macro
-- display entirely when it sees this addon loaded; ClassicAPIMacroDisplay is what
-- tells it we drive it instead. A fork that leaves the flag unset keeps the old
-- behavior -- both must never drive the same buttons.
CleveRoids.useClassicAPIDisplay =
(type(C_Macro) == "table" and C_Macro.SetMacroDisplay ~= nil) and true or false
CleveRoids.ClassicAPIMacroDisplay = CleveRoids.useClassicAPIDisplay
CleveRoids.ParsedMsg = {}
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 by pfUI 7.6 or standalone SPELL_START events)
-- Format: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime}
CleveRoids.castTracking = {}
-- pfUI 7.6+ with Nampower 2.31.0+ detected (GUID-based cast tracking available)
CleveRoids.hasPfUI76 = false
-- 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