Files
SuperCleveRoidMacros/Init.lua
T
Brues daaab2fd11 Retire the hasPfUI76 flag and the pfUI 7.6 branches it gated
Assume a modern pfUI, and drop the version-tiered branching entirely: 31 call
sites across five files, plus the flag and its detector.

Note which side was actually dead. HasPfUI76() required four things -- pfUI
version >= 7.6, Nampower >= 2.40, pfUI.libdebuff_objects_guid, and
pfUI.libdebuff_casts. The last one does not exist: pfUI exports 28 libdebuff_*
tables and libdebuff_casts is not among them (libdebuff_recent_casts is a
dedup table keyed [targetGuid][spellName][casterGuid], a different shape). So
the detector returned false at any version, and it was the hasPfUI76-TRUE
branches that never ran, not the fallbacks. Removing those is behaviour-
preserving; keeping SCRM as the owner of castTracking is what the code has
always actually done.

Removed accordingly:
- lib:HasPfUI76(), both flag declarations, and the block that would have aliased
  castTracking to pfUI.libdebuff_casts and re-pointed lib.objects/iconCache.
- HookPfUILibdebuff's "7.6 handles durations internally" early return, so the
  GetDuration/AddEffect hooks below it are visibly the live path again.
- The SPELL_START_OTHER / SPELL_FAILED_OTHER unregister arm, the pfUI branch of
  GetAuraTrackingData, the pfUI76 arm of the libdebuff zone/death cleanup, the
  pfUI backing table in the /cleveroid aura dump, and two status-string suffixes.
- SyncComboDurationToPfUI (77 lines) with its two call sites: it returned
  immediately whenever hasPfUI76, and is inert once the tier split is gone.

Unconditionalised the paths that were gated on `not hasPfUI76`: the castTracking
populate/clear/sweep, InitPfUIIntegration on login, and ten libdebuff duration
lookups.

Verified no hasPfUI76 reference remains, and that per-file block balance is
unchanged from HEAD.
2026-09-10 14:40:16 -05:00

397 lines
16 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 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