Add [locked]/[nolocked] school-interrupt conditional via C_LossOfControl

Detects a spell-school interrupt lockout (Counterspell/Kick/Pummel/Earth
Shock) on the player -- the server-side lockout that no debuff scan can see,
since it's a SMSG_SPELL_COOLDOWN packet, not an aura. ClassicAPI's
C_LossOfControl aggregates it as SCHOOL_INTERRUPT with a lockoutSchool mask.

ClassicAPI.GetSchoolLockout() returns the locked school mask; ValidateSchoolLocked
tests a school name against it (bitmask check without the 5.1-only % operator).
[locked] = any school kicked, [locked:frost] = that school, [locked:fire/frost]
= either; [nolocked:...] negates with AND (neither). Registered in
BOOLEAN_CONDITIONALS for the bare form; LOSS_OF_CONTROL_ADDED/UPDATE wired to
refresh the icon. Full silences remain [mycc:silence].
This commit is contained in:
Brues
2026-07-28 08:54:23 -05:00
parent 2e8ed33a6a
commit dd61767eb9
3 changed files with 99 additions and 30 deletions
+25
View File
@@ -208,6 +208,31 @@ function API.GetEnchantName(enchantID)
return info and info.name or nil
end
--------------------------------------------------------------------------------
-- Loss Of Control
--------------------------------------------------------------------------------
-- Locked spell-school mask from an active SCHOOL_INTERRUPT (Counterspell / Kick /
-- Pummel / Earth Shock lockout) on the player, or 0 when not kicked. Read from
-- C_LossOfControl, which synthesizes the lockout from the server's own
-- SMSG_SPELL_COOLDOWN packet -- a state no debuff scan can see. Also returns the
-- seconds remaining (nil if ClassicAPI didn't observe the applying cast). Returns
-- 0 for a client without C_LossOfControl. Player-only (vanilla LoC is local-only).
function API.GetSchoolLockout()
if type(C_LossOfControl) ~= "table"
or type(C_LossOfControl.GetActiveLossOfControlDataCount) ~= "function" then
return 0, nil
end
local n = C_LossOfControl.GetActiveLossOfControlDataCount() or 0
for i = 1, n do
local d = C_LossOfControl.GetActiveLossOfControlData(i)
if d and d.locType == "SCHOOL_INTERRUPT" then
return d.lockoutSchool or 0, d.timeRemaining
end
end
return 0, nil
end
--------------------------------------------------------------------------------
-- Spell
--------------------------------------------------------------------------------
+58 -21
View File
@@ -2953,11 +2953,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
tryUnit("targettarget")
tryUnit("targettargettarget")
tryUnit("pettarget")
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
local focusUnit = pfUI.uf.focus.label .. pfUI.uf.focus.id
tryUnit(focusUnit)
tryUnit(focusUnit .. "target")
end
tryUnit("focus")
tryUnit("focustarget")
for i = 1, 4 do
tryUnit("party" .. i .. "target")
end
@@ -2967,22 +2964,8 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
end
end
-- 3. Nameplate scan: visible nameplates give live GUIDs without target switching.
-- ClassicAPI's C_NamePlate.GetNamePlateGUIDs() lists every unit with an
-- allocated nameplate (including default vanilla nameplates), replacing the
-- old WorldFrame child-walk + frame:GetName(1) GUID-extraction.
local plateGuids = CleveRoids.ClassicAPI.GetNamePlateGUIDs()
if plateGuids then
for i = 1, table.getn(plateGuids) do
local guid = plateGuids[i]
if guid and not checked[guid] and UnitExists(guid) and UnitCanAttack("player", guid) then
checked[guid] = true
CleveRoids.knownEnemyGuids[guid] = true
if checkFunc(guid) then
count = count + 1
end
end
end
for i = 1, 40 do
tryUnit("nameplate"..i)
end
-- During tooltip evaluation, skip known-enemy cache iteration.
@@ -5228,6 +5211,33 @@ function CleveRoids.GetSpellMechanic(spellID)
return CleveRoids.ClassicAPI.GetSpellMechanicByID(spellID) or 0
end
-- School name -> spell-school mask bit (WoW SPELL_SCHOOL_MASK_*).
local LOCKOUT_SCHOOL_MASK = {
physical = 1, holy = 2, fire = 4, nature = 8, frost = 16, shadow = 32, arcane = 64,
}
-- Is `bit` set in `mask`? Shift right past `bit`, then test the low bit with
-- math.mod (the % operator is 5.1-only; 1.12 is Lua 5.0, but math.mod exists).
local function SchoolMaskHasBit(mask, bit)
if not mask or mask < bit then return false end
return math.mod(math.floor(mask / bit), 2) == 1
end
-- [locked] / [locked:school] -- player is under a spell-school interrupt lockout
-- (Counterspell / Kick / Pummel / Earth Shock), read from C_LossOfControl. This
-- is the lockout no debuff scan can detect; full silences stay on [mycc:silence].
-- school nil/true = any school kicked; a name matches that school's mask bit.
function CleveRoids.ValidateSchoolLocked(school)
local mask = CleveRoids.ClassicAPI.GetSchoolLockout()
if not mask or mask == 0 then return false end
if school == nil or school == true or school == "" then
return true
end
local bit = LOCKOUT_SCHOOL_MASK[string.lower(school)]
if not bit then return false end
return SchoolMaskHasBit(mask, bit)
end
-- Validate CC on a unit (target, focus, player, etc.)
-- Returns true if the unit has the specified CC mechanic active
function CleveRoids.ValidateUnitCC(unit, ccType)
@@ -8499,6 +8509,33 @@ CleveRoids.Keywords = {
end, conditionals, "nomycc")
end,
-- [locked] / [locked:school] - player is under a spell-school interrupt
-- lockout (Counterspell/Kick/Pummel/Earth Shock), via ClassicAPI
-- C_LossOfControl -- the server lockout no debuff scan can see. Bare = any
-- school kicked; [locked:frost] = that school; [locked:fire/frost] = either.
-- Full silences are separate ([mycc:silence]). Player-only.
locked = function(conditionals)
local v = conditionals.locked
if v == nil or v == true or (type(v) == "table" and table.getn(v) == 0) then
return CleveRoids.ValidateSchoolLocked(nil)
end
return Or(v, function(school)
return CleveRoids.ValidateSchoolLocked(school)
end)
end,
-- [nolocked:school] - player is NOT school-locked. AND logic on negation:
-- [nolocked:fire/frost] = neither Fire nor Frost is kicked.
nolocked = function(conditionals)
local v = conditionals.nolocked
if v == nil or v == true or (type(v) == "table" and table.getn(v) == 0) then
return not CleveRoids.ValidateSchoolLocked(nil)
end
return NegatedMulti(v, function(school)
return not CleveRoids.ValidateSchoolLocked(school)
end, conditionals, "nolocked")
end,
-- ========================================================================
-- RESIST TRACKING CONDITIONALS
-- ========================================================================
+16 -9
View File
@@ -151,6 +151,8 @@ local BOOLEAN_CONDITIONALS = {
nomhenchant = true,
ohenchant = true,
noohenchant = true,
locked = true,
nolocked = true,
group = true,
nogroup = true,
moving = true,
@@ -1581,18 +1583,9 @@ function CleveRoids.TryTargetFocus()
return true
end
-- Resolves the player's focus to a usable unit token.
-- Prefers pfUI's emulated focus (resolves to a real token like "party2"/"raid5")
-- for pfUI users, then falls back to ClassicAPI's native "focus" token (which
-- every UnitX function accepts) for everyone else.
-- Returns the resolved token, or nil when no focus is set so @focus clauses
-- silently fall through to the next macro alternative (no warning spam).
function CleveRoids.GetFocusUnitId()
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
return pfUI.uf.focus.label .. pfUI.uf.focus.id
end
-- ClassicAPI native focus token (set via /focus or the FOCUSTARGET keybind)
if UnitExists("focus") then
return "focus"
end
@@ -4860,6 +4853,13 @@ CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_ENABLED") -- Left actual combat (n
CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
CleveRoids.Frame:RegisterEvent("PLAYER_STARTED_MOVING")
-- ClassicAPI loss-of-control (school-interrupt lockout) for [locked]/[nolocked].
-- Gated on the namespace so an older ClassicAPI without it doesn't error on an
-- unknown event.
if type(C_LossOfControl) == "table" then
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_ADDED")
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
end
-- Use GUID events when available (v2.39+), fall back to standard per-token events
if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then
CleveRoids.Frame:RegisterEvent("UNIT_AURA_GUID")
@@ -5799,6 +5799,13 @@ function CleveRoids.Frame:PLAYER_FOCUS_CHANGED()
CleveRoids.QueueActionUpdate()
end
end
-- School-interrupt lockout applied/changed -> refresh [locked]/[nolocked] icons.
function CleveRoids.Frame:LOSS_OF_CONTROL_ADDED()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
CleveRoids.Frame.LOSS_OF_CONTROL_UPDATE = CleveRoids.Frame.LOSS_OF_CONTROL_ADDED
function CleveRoids.Frame:UPDATE_SHAPESHIFT_FORM()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()