From c05fc754e6d1d1c3382880e684fc85ee21abb957 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:03:15 -0500 Subject: [PATCH] Add ClassicAPI dispel-type conditionals via C_UnitAuras Introduce ClassicAPI.lua, a feature-detection layer for the ClassicAPI client mod that mirrors NampowerAPI's HasMinimumVersion pattern (IsAvailable / HasMinimumVersion / HasUnitAuras). UnitHasDispelType scans a unit's auras via C_UnitAuras and reports the dispel type, scanning debuffs or buffs based on a helpful flag. Add conditionals backed by it: - Debuff side (defensive cleanse): [magic] [curse] [disease] [poison] [dispellable] + negations - Buff side (offensive dispel/strip): [magicbuff] [dispellablebuff] + negations All are no-arg booleans, default to @target (honor @unit), and degrade gracefully without ClassicAPI: positive checks return false, negated return true, since vanilla cannot read aura dispel types. The existing ValidateAura path is untouched. MacroErrorChecker auto-accepts them via the Keywords table; they're registered in BOOLEAN_CONDITIONALS so they parse as flags. Loaded after NampowerAPI in the TOC. --- ClassicAPI.lua | 99 ++++++++++++++++++++++++++++++++++++++++ Conditionals.lua | 81 ++++++++++++++++++++++++++++++++ Core.lua | 15 ++++++ SuperCleveRoidMacros.toc | 1 + docs/CLASSICAPI-TODO.md | 28 ++++++++++-- 5 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 ClassicAPI.lua diff --git a/ClassicAPI.lua b/ClassicAPI.lua new file mode 100644 index 0000000..56dd8ff --- /dev/null +++ b/ClassicAPI.lua @@ -0,0 +1,99 @@ +--[[ + ClassicAPI.lua - ClassicAPI Integration Layer + + ClassicAPI is a client mod (sibling to Nampower/SuperWoW) that backports the + modern C_* API into the 1.12.1 Lua environment. This module provides feature + detection and thin wrappers so every ClassicAPI-backed code path can fall + back gracefully when the DLL is absent, mirroring NampowerAPI's + HasMinimumVersion pattern. + + Detection: the global CLASSIC_API_VERSION is defined once the client has + booted, encoded as X*10000 + Y*100 + Z for a vX.Y.Z tag (untagged dev builds + report the sentinel 99999999). See ClassicAPI docs/API.md. + + Currently used for: + - C_UnitAuras: dispel-type detection (Magic/Curse/Disease/Poison) on any unit, + powering the [magic]/[curse]/[disease]/[poison]/[dispellable] conditionals. +]] + +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids + +-- Force table creation if not already a table (guards against addon conflicts) +if type(CleveRoids.ClassicAPI) ~= "table" then + CleveRoids.ClassicAPI = {} +end +local API = CleveRoids.ClassicAPI + +-------------------------------------------------------------------------------- +-- VERSION DETECTION +-------------------------------------------------------------------------------- + +-- Returns the encoded version number (X*10000 + Y*100 + Z), or 0 if absent. +function API.GetVersionNumber() + return CLASSIC_API_VERSION or 0 +end + +-- True if the ClassicAPI client mod is loaded at all. +function API.IsAvailable() + return CLASSIC_API_VERSION ~= nil +end + +-- Check if the loaded ClassicAPI meets a minimum version (major, minor, patch). +function API.HasMinimumVersion(reqMajor, reqMinor, reqPatch) + local v = CLASSIC_API_VERSION + if not v then return false end + local req = (reqMajor or 0) * 10000 + (reqMinor or 0) * 100 + (reqPatch or 0) + return v >= req +end + +-------------------------------------------------------------------------------- +-- C_UnitAuras +-------------------------------------------------------------------------------- + +-- Cached capability flag (C_UnitAuras is injected by the DLL before addons load). +local _hasUnitAuras = nil +function API.HasUnitAuras() + if _hasUnitAuras == nil then + _hasUnitAuras = (type(C_UnitAuras) == "table" + and type(C_UnitAuras.GetDebuffDataByIndex) == "function" + and type(C_UnitAuras.GetBuffDataByIndex) == "function") or false + end + return _hasUnitAuras +end + +-- Module-level scan (passed to pcall with args to avoid per-call closure alloc). +-- indexFn is C_UnitAuras.GetBuffDataByIndex or GetDebuffDataByIndex. +-- Vanilla descriptors hold 32 helpful / 16 harmful slots; cap as a backstop. +local function scanDispel(indexFn, unit, dispelType, wantAny) + local i = 1 + while i <= 48 do + local data = indexFn(unit, i) + if not data then return false end + local dn = data.dispelName + if dn and dn ~= "" and (wantAny or dn == dispelType) then + return true + end + i = i + 1 + end + return false +end + +-- True if `unit` has an aura of the given dispel type. +-- dispelType: "Magic" | "Curse" | "Disease" | "Poison", or "any"/nil for any +-- dispellable aura (any non-empty dispelName). +-- helpful: true -> scan buffs (offensive dispel / strip / purge) +-- false -> scan debuffs (defensive cleanse, default) +-- Returns false (never errors) when ClassicAPI/C_UnitAuras is unavailable or the +-- unit doesn't exist, so callers degrade gracefully. +function API.UnitHasDispelType(unit, dispelType, helpful) + if not unit then return false end + if not API.HasUnitAuras() then return false end + if not UnitExists(unit) then return false end + + local indexFn = helpful and C_UnitAuras.GetBuffDataByIndex + or C_UnitAuras.GetDebuffDataByIndex + local wantAny = (dispelType == nil or dispelType == "any") + local ok, found = pcall(scanDispel, indexFn, unit, dispelType, wantAny) + return (ok and found) or false +end diff --git a/Conditionals.lua b/Conditionals.lua index 7036337..5b8adda 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -6515,6 +6515,87 @@ CleveRoids.Keywords = { end, conditionals, "nomydebuff") end, + -- Dispel-type conditionals (ClassicAPI C_UnitAuras). + -- [magic]/[curse]/[disease]/[poison] = target has a debuff of that dispel + -- type; [dispellable] = target has any dispellable debuff. These require the + -- ClassicAPI client mod; without it the positive checks return false and the + -- negated checks return true (graceful degradation — vanilla cannot read + -- aura dispel types). They honor @unit like [debuff] does. + magic = function(conditionals) + if not conditionals.target then return false end + return CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Magic") + end, + + nomagic = function(conditionals) + if not conditionals.target then return false end + return not CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Magic") + end, + + curse = function(conditionals) + if not conditionals.target then return false end + return CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Curse") + end, + + nocurse = function(conditionals) + if not conditionals.target then return false end + return not CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Curse") + end, + + disease = function(conditionals) + if not conditionals.target then return false end + return CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Disease") + end, + + nodisease = function(conditionals) + if not conditionals.target then return false end + return not CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Disease") + end, + + poison = function(conditionals) + if not conditionals.target then return false end + return CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Poison") + end, + + nopoison = function(conditionals) + if not conditionals.target then return false end + return not CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Poison") + end, + + dispellable = function(conditionals) + if not conditionals.target then return false end + return CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "any") + end, + + nodispellable = function(conditionals) + if not conditionals.target then return false end + return not CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "any") + end, + + -- Buff-side dispel-type conditionals (offensive dispel / strip / purge). + -- [magicbuff] = unit has a dispellable Magic buff (the only kind offensive + -- dispel removes in 1.12); [dispellablebuff] = unit has any dispel-typed + -- buff. Same ClassicAPI requirement and graceful degradation as the debuff + -- variants. The `true` arg makes UnitHasDispelType scan buffs, not debuffs. + magicbuff = function(conditionals) + if not conditionals.target then return false end + return CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Magic", true) + end, + + nomagicbuff = function(conditionals) + if not conditionals.target then return false end + return not CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "Magic", true) + end, + + dispellablebuff = function(conditionals) + if not conditionals.target then return false end + return CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "any", true) + end, + + nodispellablebuff = function(conditionals) + if not conditionals.target then return false end + return not CleveRoids.ClassicAPI.UnitHasDispelType(conditionals.target, "any", true) + end, + power = function(conditionals) return Multi(conditionals.power, function(args) if type(args) ~= "table" then return false end diff --git a/Core.lua b/Core.lua index 707b168..5368fb1 100644 --- a/Core.lua +++ b/Core.lua @@ -152,6 +152,21 @@ local BOOLEAN_CONDITIONALS = { nopet = true, bg = true, nobg = true, + -- Dispel-type conditionals (ClassicAPI C_UnitAuras) + magic = true, + nomagic = true, + curse = true, + nocurse = true, + disease = true, + nodisease = true, + poison = true, + nopoison = true, + dispellable = true, + nodispellable = true, + magicbuff = true, + nomagicbuff = true, + dispellablebuff = true, + nodispellablebuff = true, } local requirementCheckFrame = CreateFrame("Frame") diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index ddb59a9..a7ab89d 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -8,6 +8,7 @@ Localization.lua Init.lua NampowerAPI.lua +ClassicAPI.lua Utility.lua Core.lua Conditionals.lua diff --git a/docs/CLASSICAPI-TODO.md b/docs/CLASSICAPI-TODO.md index 4f6200a..a5de1d3 100644 --- a/docs/CLASSICAPI-TODO.md +++ b/docs/CLASSICAPI-TODO.md @@ -20,16 +20,38 @@ API references are line numbers into `C:\Git\ClassicAPI\docs\API.md`. `applications` (stacks), `duration`. - **Unlocks:** `[dispellable]` / `[curse]` / `[magic]` target conditionals and spellID-based (rank/locale-proof) aura matching. -- **Replaces today:** pfUI libdebuff + tooltip scanning (`Conditionals.lua:1815+`). - **Caveat:** `expirationTime` is only populated for `unit=="player"`; it's `0` for target/focus (vanilla server limitation). Target debuff *timers* still need the existing libdebuff tracking — only presence/stacks/school/spellId are reliable cross-unit. +- **DONE (slice 1 — dispel-type conditionals):** added `ClassicAPI.lua` + detection module (`CleveRoids.ClassicAPI`, mirrors NampowerAPI's + `HasMinimumVersion`) and the `[magic]`/`[curse]`/`[disease]`/`[poison]`/ + `[dispellable]` conditionals + negations, backed by + `C_UnitAuras.GetDebuffDataByIndex(...).dispelName`. No-arg booleans, honor + `@unit`, graceful fallback (positive→false, negated→true) when ClassicAPI is + absent. Purely additive — the existing `ValidateAura` path is untouched. +- **DONE (slice 2 — buff-side / offensive dispel):** `UnitHasDispelType` now + takes a `helpful` flag (scans `GetBuffDataByIndex`), exposed as + `[magicbuff]`/`[dispellablebuff]` + negations — detect a dispellable buff to + strip off an enemy (e.g. `[harm,magicbuff] Dispel Magic`). Magic-only is the + practical set since 1.12 offensive dispel removes Magic buffs. +- **Follow-ups (still TODO):** + - spellID-based aura matching via `GetUnitAuraBySpellID` (rank/locale-proof), + optionally wired into `ValidateAura` as a fast path with libdebuff fallback. + - more reliable cross-unit stacks via `applications`. + - class-aware `[dispellable]` (only types this character can actually remove). + - optional name filtering on the type keywords (e.g. `[magic:Polymorph]`). + ### 2. `C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs - **API:** `API.md:7159` — returns 12-tuple including `enchantID` for main/off/ranged. -- **Unlocks:** `[poison:]` / `[weaponenchant:]` conditionals — detect - *which* poison/oil/sharpening stone is applied, not just that one exists. +- **Unlocks:** detect *which* temp enchant (poison/oil/sharpening stone) is + applied to a weapon, not just that one exists. +- **Naming:** `[poison]` is now taken by the target dispel-type conditional + (slice 1 above). Use weapon-specific keywords for this — e.g. + `[mhenchant:]` / `[ohenchant:]` (or `[mhpoison:]`/`[ohpoison:]`), + not a bare `[poison]`. - Vanilla's global only reports presence; this is a genuinely new capability (rogue/shaman/enhance).