mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
Compare commits
20 Commits
classicapi_next
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 37ca7cef40 | |||
| 4474a607f7 | |||
| ca6c55a5e7 | |||
| c1a7bdb51b | |||
| 5d5f987555 | |||
| eddc49fc60 | |||
| 54e3c3b3f4 | |||
| 4fa254b4e6 | |||
| b82d06d061 | |||
| 891467cd7e | |||
| 87ea8bdd41 | |||
| 53a1cf1441 | |||
| 073621e233 | |||
| 3e2b3db31f | |||
| 7101ad167a | |||
| afc5e513ca | |||
| 5e27e6fe58 | |||
| 6edd37a76f | |||
| c8bc4c134d | |||
| 5213bd5474 |
@@ -18,6 +18,6 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Package and release to GitHub
|
||||
uses: BigWigsMods/packager@v2
|
||||
uses: brues-code/packager@vCAPI
|
||||
env:
|
||||
GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+32
-19
@@ -3,11 +3,17 @@
|
||||
|
||||
ClassicAPI is a client mod (sibling to Nampower/SuperWoW) that backports the
|
||||
modern C_* API into the 1.12.1 Lua environment. It is a HARD REQUIREMENT of
|
||||
this addon (ClassicAPI v1.12.1+, which added the positional
|
||||
C_UnitAuras.UnitAura), so the wrappers below call the API directly — no
|
||||
fallbacks. The load-time requirement check (Core.lua) uses IsAvailable() to
|
||||
warn when the DLL is missing and HasMinimumVersion() when it's too old; users
|
||||
who don't want ClassicAPI should run the upstream addon.
|
||||
this addon (ClassicAPI v1.15.8+, which scoped GetMouseButtonClicked to the
|
||||
click dispatch; v1.15.0 added frame:RegisterUnitEvent), so the
|
||||
wrappers below call the API directly — no fallbacks. The load-time requirement
|
||||
check (Core.lua) uses IsAvailable() to warn when the DLL is missing and
|
||||
HasMinimumVersion() when it's too old; users who don't want ClassicAPI should
|
||||
run the upstream addon.
|
||||
|
||||
The minimum is not advisory: Utility.lua calls frame:RegisterUnitEvent at file
|
||||
scope, so an older ClassicAPI aborts that chunk and leaves most of the addon
|
||||
undefined. Raise the Core.lua minimum in step with any new API adopted at file
|
||||
scope.
|
||||
|
||||
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
|
||||
@@ -37,6 +43,14 @@ function API.GetVersionNumber()
|
||||
return CLASSIC_API_VERSION or 0
|
||||
end
|
||||
|
||||
-- Returns the loaded version as major, minor, patch (0, 0, 0 if absent).
|
||||
function API.GetVersion()
|
||||
local v = CLASSIC_API_VERSION or 0
|
||||
local major = math.floor(v / 10000)
|
||||
local minor = math.floor(v / 100) - major * 100
|
||||
return major, minor, v - math.floor(v / 100) * 100
|
||||
end
|
||||
|
||||
-- True if the ClassicAPI client mod is loaded at all.
|
||||
function API.IsAvailable()
|
||||
return CLASSIC_API_VERSION ~= nil
|
||||
@@ -56,7 +70,7 @@ end
|
||||
|
||||
-- Scan one aura range of `unit` (filter = "HELPFUL" or "HARMFUL") for an aura
|
||||
-- matching the dispel type. Uses the positional C_UnitAuras.UnitAura (added in
|
||||
-- ClassicAPI v1.12.1, this addon's minimum) -- no table allocated per slot, with
|
||||
-- ClassicAPI v1.12.1, below this addon's minimum) -- no table allocated per slot, with
|
||||
-- dispelName as the 4th return. The filtered index self-terminates at the end of
|
||||
-- the range (nil name); 48 is a backstop over vanilla's 32 helpful / 16 harmful slots.
|
||||
local function scanDispel(unit, filter, dispelType, wantAny)
|
||||
@@ -234,8 +248,8 @@ end
|
||||
-- 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).
|
||||
-- seconds remaining (nil if ClassicAPI didn't observe the applying cast).
|
||||
-- Player-only (vanilla LoC is local-only).
|
||||
function API.GetSchoolLockout()
|
||||
local n = C_LossOfControl.GetActiveLossOfControlDataCount() or 0
|
||||
for i = 1, n do
|
||||
@@ -299,10 +313,9 @@ end
|
||||
-- Unit Health
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Health deficit (max - current) for `unit` in one call. Falls back to
|
||||
-- UnitHealthMax - UnitHealth without ClassicAPI.
|
||||
API.UnitHealthMissing = UnitHealthMissing or function(unit)
|
||||
return (UnitHealthMax(unit) or 0) - (UnitHealth(unit) or 0)
|
||||
-- Health deficit (max - current) for `unit` in one call.
|
||||
function API.UnitHealthMissing(unit)
|
||||
return UnitHealthMissing(unit)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -311,18 +324,18 @@ end
|
||||
|
||||
-- Current power for a specific Enum.PowerType (0=Mana, 1=Rage, 2=Focus,
|
||||
-- 3=Energy, 4=Happiness), or the unit's primary power when powerType is omitted.
|
||||
-- Display-divided (rage reads 0..100). Falls back to UnitMana without ClassicAPI.
|
||||
API.UnitPower = UnitPower or function(unit, powerType)
|
||||
return UnitMana(unit)
|
||||
-- Display-divided (rage reads 0..100).
|
||||
function API.UnitPower(unit, powerType)
|
||||
return UnitPower(unit, powerType)
|
||||
end
|
||||
|
||||
API.UnitPowerMax = UnitPowerMax or function(unit, powerType)
|
||||
return UnitManaMax(unit)
|
||||
function API.UnitPowerMax(unit, powerType)
|
||||
return UnitPowerMax(unit, powerType)
|
||||
end
|
||||
|
||||
-- Power deficit (max - current) for the type / primary power, in one call.
|
||||
API.UnitPowerMissing = UnitPowerMissing or function(unit, powerType)
|
||||
return (UnitManaMax(unit) or 0) - (UnitMana(unit) or 0)
|
||||
function API.UnitPowerMissing(unit, powerType)
|
||||
return UnitPowerMissing(unit, powerType)
|
||||
end
|
||||
|
||||
-- Unit's primary power type as an integer (0=Mana .. 4=Happiness).
|
||||
|
||||
+6
-86
@@ -15,23 +15,6 @@ CleveRoids.ComboPointTracking = CleveRoids.ComboPointTracking or {}
|
||||
-- Structure: CleveRoids_ComboDurations[spellID][comboPoints] = duration
|
||||
CleveRoids_ComboDurations = CleveRoids_ComboDurations or {}
|
||||
|
||||
-- Storage for last Rip cast (for Carnage talent mechanic)
|
||||
-- Carnage talent: When Ferocious Bite procs Carnage, it refreshes Rip and Rake to their original duration
|
||||
-- Detection: When combo points don't drop to 0 after FB (they stay at 1 = Carnage proc)
|
||||
-- Talent Position: Tab 2 (Feral Combat), Talent 17
|
||||
-- Rank 1: 10% per CP, Rank 2: 20% per CP
|
||||
CleveRoids.lastRipCast = CleveRoids.lastRipCast or {
|
||||
duration = nil,
|
||||
targetGUID = nil,
|
||||
timestamp = 0
|
||||
}
|
||||
|
||||
CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
|
||||
duration = nil,
|
||||
targetGUID = nil,
|
||||
timestamp = 0
|
||||
}
|
||||
|
||||
-- Family membership without hardcoded rank lists. C_Spell.GetSpellName resolves
|
||||
-- ANY spellID from the client's Spell.dbc -- every rank (so no enumeration or
|
||||
-- spellbook scan) and TWoW's custom spells alike -- so "is this spellID a Rip?"
|
||||
@@ -86,10 +69,8 @@ CleveRoids.ComboScalingSpellsByID = {
|
||||
[9896] = { base = 10, increment = 2, name = "Rip" }, -- Rank 6
|
||||
}
|
||||
|
||||
-- Ferocious Bite spell IDs (for Carnage talent mechanic)
|
||||
-- Carnage talent: When FB procs Carnage, refreshes Rip and Rake back to their original duration
|
||||
-- Proc detection: combo points stay at 1 after FB instead of dropping to 0
|
||||
-- Talent Position: Tab 2 (Feral Combat), Talent 17
|
||||
-- Ferocious Bite spell IDs. A combo-point finisher, so the cast-time combo
|
||||
-- snapshot has to know it.
|
||||
CleveRoids.FerociousBiteSpellIDs = {
|
||||
[22557] = true, -- Rank 1
|
||||
[22568] = true, -- Rank 2
|
||||
@@ -99,10 +80,6 @@ CleveRoids.FerociousBiteSpellIDs = {
|
||||
[31018] = true, -- Rank 6
|
||||
}
|
||||
|
||||
-- Rip / Rake families (for Carnage talent). Seeded by Rank 1; matches every rank.
|
||||
CleveRoids.RipSpellIDs = RankSet(1079)
|
||||
CleveRoids.RakeSpellIDs = RankSet(1822)
|
||||
|
||||
-- Combined table for all bleed spells that need immunity detection
|
||||
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity)
|
||||
-- NOTE: These are the DEBUFF spell IDs (what appears on target), not cast spell IDs
|
||||
@@ -168,14 +145,6 @@ CleveRoids.ImmolateSpellIDs = {
|
||||
[25309] = true, -- Rank 8
|
||||
}
|
||||
|
||||
-- =============================================================================
|
||||
-- WARLOCK: Dark Harvest Duration Acceleration (TWoW Custom)
|
||||
-- Channeled spell that accelerates DoT tick rate by 30% while channeling
|
||||
-- Complex tracking: debuff expires 30% faster while Dark Harvest is active
|
||||
-- =============================================================================
|
||||
-- TWoW custom (see MoltenBlast note).
|
||||
CleveRoids.DarkHarvestSpellIDs = RankSet(52550)
|
||||
|
||||
-- =============================================================================
|
||||
-- DRUID: Rake Debuff Cap Boss Whitelist
|
||||
-- These bosses are likely to hit the 48 debuff cap, causing Rake to get pushed off
|
||||
@@ -745,7 +714,7 @@ function Extension.OnLoad()
|
||||
Extension.RegisterEvent("SPELLCAST_FAILED", "OnSpellcastFailed")
|
||||
Extension.RegisterEvent("SPELLCAST_INTERRUPTED", "OnSpellcastInterrupted")
|
||||
Extension.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged")
|
||||
Extension.RegisterEvent("UNIT_AURA", "OnUnitAura")
|
||||
Extension.RegisterUnitEvent("UNIT_AURA", "OnUnitAura", "target", "player")
|
||||
Extension.RegisterEvent("PLAYER_COMBO_POINTS", "OnComboPointsChanged")
|
||||
|
||||
-- PERFORMANCE OPTIMIZATION: Removed OnUpdate polling for combo points
|
||||
@@ -765,63 +734,14 @@ function Extension.OnTargetChanged()
|
||||
CleveRoids.UpdateComboPoints()
|
||||
end
|
||||
|
||||
-- Registered as a unit event for target and player, so arg1 is always one of
|
||||
-- those two -- no token check needed.
|
||||
function Extension.OnUnitAura()
|
||||
if arg1 == "target" or arg1 == "player" then
|
||||
CleveRoids.UpdateComboPoints()
|
||||
end
|
||||
CleveRoids.UpdateComboPoints()
|
||||
end
|
||||
|
||||
function Extension.OnComboPointsChanged()
|
||||
CleveRoids.UpdateComboPoints()
|
||||
|
||||
-- CARNAGE PROC DETECTION (Cursive-style)
|
||||
-- When Ferocious Bite is used, combo points should drop to 0
|
||||
-- If Carnage procs, combo points will be 1 instead (the Carnage-granted combo point)
|
||||
-- Check: After Ferocious Bite (within 0.5s), if combo points > 0, Carnage procced
|
||||
if CleveRoids.lastFerociousBiteTime and CleveRoids.lastFerociousBiteTargetGUID then
|
||||
local timeSinceBite = GetTime() - CleveRoids.lastFerociousBiteTime
|
||||
if timeSinceBite < 0.5 then
|
||||
local currentCP = CleveRoids.GetComboPoints()
|
||||
if currentCP > 0 then
|
||||
-- Carnage procced! Combo points didn't drop to 0 (or rose back to 1)
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cffff00ff[Carnage]|r PROC DETECTED! CP=%d after Ferocious Bite (%.2fs ago)",
|
||||
currentCP, timeSinceBite)
|
||||
)
|
||||
end
|
||||
|
||||
-- Apply the Carnage refresh to Rip and Rake
|
||||
local targetGUID = CleveRoids.lastFerociousBiteTargetGUID
|
||||
local targetName = CleveRoids.lastFerociousBiteTargetName or "Unknown"
|
||||
local biteSpellID = CleveRoids.lastFerociousBiteSpellID
|
||||
|
||||
-- Call the Carnage refresh function in Utility.lua
|
||||
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyCarnageRefresh then
|
||||
CleveRoids.libdebuff.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID)
|
||||
end
|
||||
|
||||
-- Clear the tracking to prevent multiple refreshes
|
||||
CleveRoids.lastFerociousBiteTime = nil
|
||||
CleveRoids.lastFerociousBiteTargetGUID = nil
|
||||
CleveRoids.lastFerociousBiteTargetName = nil
|
||||
CleveRoids.lastFerociousBiteSpellID = nil
|
||||
end
|
||||
else
|
||||
-- Time window expired, clear tracking
|
||||
if CleveRoids.lastFerociousBiteTime then
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cffff00ff[Carnage]|r No proc - time window expired (%.2fs)", timeSinceBite)
|
||||
)
|
||||
end
|
||||
CleveRoids.lastFerociousBiteTime = nil
|
||||
CleveRoids.lastFerociousBiteTargetGUID = nil
|
||||
CleveRoids.lastFerociousBiteTargetName = nil
|
||||
CleveRoids.lastFerociousBiteSpellID = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Event handlers
|
||||
|
||||
+25
-372
@@ -22,19 +22,6 @@ function Extension.DLOG(msg)
|
||||
end
|
||||
end
|
||||
|
||||
function Extension.FocusNameHook()
|
||||
local hook = Extension.internal.memberHooks[CleveRoids]["GetFocusName"]
|
||||
local target = hook.original()
|
||||
|
||||
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.unitname then
|
||||
target = pfUI.uf.focus.unitname
|
||||
end
|
||||
|
||||
--Extension.DLOG(target)
|
||||
|
||||
return target
|
||||
end
|
||||
|
||||
-- Check if pfUI's macrotweak module is loaded
|
||||
function Extension.IsPfUIMacrotweakLoaded()
|
||||
if not pfUI then return false end
|
||||
@@ -92,334 +79,6 @@ function Extension.HandleSendChatMessageHook()
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper function to check for Carnage duration override
|
||||
-- Returns override duration and timeleft if found, nil otherwise
|
||||
local function GetCarnageOverride(effect)
|
||||
if not effect or not CleveRoids.carnageDurationOverrides then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do
|
||||
local spellName = C_Spell.GetSpellName(spellID)
|
||||
if spellName then
|
||||
local baseName = CleveRoids.StripRank(spellName)
|
||||
if baseName == effect and override.timestamp and (GetTime() - override.timestamp) < 5 then
|
||||
local timeleft = override.duration - (GetTime() - override.timestamp)
|
||||
if timeleft < 0 then timeleft = 0 end
|
||||
return override.duration, timeleft
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- Hook pfUI's libdebuff to use our combo-aware durations
|
||||
-- NOTE: pfUI 7.6+ (GetUnitField edition) handles combo durations and Carnage internally.
|
||||
-- We only inject when there's a mismatch between pfUI's data and ours.
|
||||
function Extension.HookPfUILibdebuff()
|
||||
if not pfUI or not pfUI.api or not pfUI.api.libdebuff then
|
||||
return false
|
||||
end
|
||||
|
||||
local pflib = pfUI.api.libdebuff
|
||||
|
||||
-- Check if pfUI 7.6+ with GetUnitField-based libdebuff is active
|
||||
-- If so, pfUI handles combo durations and Carnage internally - we only override on mismatch
|
||||
local hasPfUI76 = CleveRoids.hasPfUI76
|
||||
|
||||
-- pfUI 7.6+ handles all durations internally - no hooks needed
|
||||
if hasPfUI76 then
|
||||
Extension.DLOG("Skipped all libdebuff hooks (pfUI 7.6+ handles internally)")
|
||||
return false
|
||||
end
|
||||
|
||||
-- Hook GetDuration if it exists
|
||||
-- pfUI's GetDuration signature: function(effect, rank) where effect is spell NAME
|
||||
if pflib.GetDuration and not Extension.pfLibDebuffHooked then
|
||||
local originalGetDuration = pflib.GetDuration
|
||||
|
||||
pflib.GetDuration = function(self, effect, rank)
|
||||
local pfuiDuration = originalGetDuration(self, effect, rank)
|
||||
|
||||
-- Check for Carnage duration overrides (only if pfUI doesn't have it)
|
||||
local carnageDuration = GetCarnageOverride(effect)
|
||||
if carnageDuration then
|
||||
-- Only override if pfUI's duration is significantly different (>1s difference)
|
||||
if not pfuiDuration or math.abs(carnageDuration - pfuiDuration) > 1 then
|
||||
if Extension.Debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00aaff[pfUI Duration Override]|r %s: Carnage %.1fs (pfUI: %.1fs)",
|
||||
effect, carnageDuration, pfuiDuration or 0)
|
||||
)
|
||||
end
|
||||
return carnageDuration
|
||||
end
|
||||
end
|
||||
|
||||
-- Check name-based tracking for fresh combo casts (only if pfUI returned 0 or nil)
|
||||
if (not pfuiDuration or pfuiDuration == 0) and CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking[effect] then
|
||||
local tracking = CleveRoids.ComboPointTracking[effect]
|
||||
if tracking.duration and tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then
|
||||
if Extension.Debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00aaff[pfUI Duration Override]|r %s: Combo tracking %.1fs (pfUI: %.1fs)",
|
||||
effect, tracking.duration, pfuiDuration or 0)
|
||||
)
|
||||
end
|
||||
return tracking.duration
|
||||
end
|
||||
end
|
||||
|
||||
return pfuiDuration
|
||||
end
|
||||
|
||||
Extension.pfLibDebuffHooked = true
|
||||
Extension.DLOG("Hooked pfUI.api.libdebuff.GetDuration (mismatch-only mode)")
|
||||
end
|
||||
|
||||
-- Hook AddEffect if it exists (pre-7.6 only - 7.6+ returns early above)
|
||||
if pflib.AddEffect and not Extension.pfLibAddEffectHooked then
|
||||
local originalAddEffect = pflib.AddEffect
|
||||
|
||||
pflib.AddEffect = function(self, unit, unitlevel, effect, duration, caster)
|
||||
-- RANK CHECKING: Preserve higher rank's remaining time if lower rank was cast
|
||||
-- NOTE: 'unit' is a unit NAME (e.g., "Expert Training Dummy"), not a unit ID
|
||||
-- Defensive: verify libdebuff is a table, not a function
|
||||
if caster == "player" and type(CleveRoids.libdebuff) == "table" and duration and duration > 0 then
|
||||
-- Try to find the GUID for this unit name
|
||||
local unitGUID = nil
|
||||
|
||||
-- Check if this is the current target
|
||||
if UnitName("target") == unit then
|
||||
unitGUID = CleveRoids.GetGUID("target")
|
||||
end
|
||||
|
||||
-- If we couldn't match to current target, check guidToName mapping
|
||||
if not unitGUID and CleveRoids.libdebuff.guidToName then
|
||||
for guid, name in pairs(CleveRoids.libdebuff.guidToName) do
|
||||
if name == unit then
|
||||
unitGUID = guid
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if a higher rank of this spell is already active
|
||||
if unitGUID and CleveRoids.libdebuff.objects and CleveRoids.libdebuff.objects[unitGUID] then
|
||||
-- Find all spell IDs that match this effect name
|
||||
for spellID, rec in pairs(CleveRoids.libdebuff.objects[unitGUID]) do
|
||||
if rec and rec.start and rec.duration then
|
||||
-- Get spell name for this ID
|
||||
local spellName = C_Spell.GetSpellName(spellID)
|
||||
if spellName then
|
||||
local baseName = CleveRoids.StripRank(spellName)
|
||||
if baseName == effect then
|
||||
-- Same spell - check if still active
|
||||
local remaining = rec.duration + rec.start - GetTime()
|
||||
if remaining > 0 then
|
||||
-- If incoming duration > remaining time, we're trying to add more time
|
||||
-- This means either a refresh or lower rank cast - preserve existing timer
|
||||
if duration > remaining then
|
||||
if Extension.Debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00aaff[pfUI Rank Preserve]|r %s: Preserving timer (%.1fs remaining vs %.1fs incoming)",
|
||||
effect, remaining, duration)
|
||||
)
|
||||
end
|
||||
|
||||
-- Preserve the existing timer
|
||||
duration = remaining
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check for Carnage duration overrides FIRST (highest priority)
|
||||
local carnageDuration = GetCarnageOverride(effect)
|
||||
if carnageDuration then
|
||||
duration = duration or carnageDuration
|
||||
caster = caster or "player" -- Ensure caster is set for UnitOwnDebuff filtering
|
||||
end
|
||||
|
||||
-- Check if this is a combo scaling spell by name
|
||||
if not duration and CleveRoids.IsComboScalingSpell and CleveRoids.IsComboScalingSpell(effect) then
|
||||
if CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking[effect] then
|
||||
local tracking = CleveRoids.ComboPointTracking[effect]
|
||||
if tracking.duration and tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then
|
||||
duration = tracking.duration
|
||||
caster = caster or "player"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return originalAddEffect(self, unit, unitlevel, effect, duration, caster)
|
||||
end
|
||||
|
||||
Extension.pfLibAddEffectHooked = true
|
||||
Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect")
|
||||
end
|
||||
|
||||
-- Hook UnitDebuff to return Carnage override duration to display code
|
||||
-- Only override when pfUI's duration differs significantly from ours
|
||||
if pflib.UnitDebuff and not Extension.pfLibUnitDebuffHooked then
|
||||
local originalUnitDebuff = pflib.UnitDebuff
|
||||
|
||||
pflib.UnitDebuff = function(self, unit, id)
|
||||
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitDebuff(self, unit, id)
|
||||
|
||||
-- Only check Carnage override if pfUI returned data but duration might be wrong
|
||||
if effect then
|
||||
local carnageDuration, carnageTimeleft = GetCarnageOverride(effect)
|
||||
if carnageDuration then
|
||||
-- Only override if there's a significant difference (>1s)
|
||||
if not duration or math.abs(carnageDuration - duration) > 1 then
|
||||
if Extension.Debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00aaff[pfUI UnitDebuff Override]|r %s: Carnage %.1fs/%.1fs (pfUI: %.1fs/%.1fs)",
|
||||
effect, carnageDuration, carnageTimeleft, duration or 0, timeleft or 0)
|
||||
)
|
||||
end
|
||||
duration = carnageDuration
|
||||
timeleft = carnageTimeleft
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
|
||||
end
|
||||
|
||||
Extension.pfLibUnitDebuffHooked = true
|
||||
Extension.DLOG("Hooked pfUI.api.libdebuff.UnitDebuff (mismatch-only mode)")
|
||||
end
|
||||
|
||||
-- Hook UnitOwnDebuff to return Carnage override duration when selfdebuff is enabled
|
||||
-- Only override when pfUI's duration differs significantly from ours
|
||||
if pflib.UnitOwnDebuff and not Extension.pfLibUnitOwnDebuffHooked then
|
||||
local originalUnitOwnDebuff = pflib.UnitOwnDebuff
|
||||
|
||||
pflib.UnitOwnDebuff = function(self, unit, id)
|
||||
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitOwnDebuff(self, unit, id)
|
||||
|
||||
if effect then
|
||||
local carnageDuration, carnageTimeleft = GetCarnageOverride(effect)
|
||||
if carnageDuration then
|
||||
-- Only override if there's a significant difference (>1s)
|
||||
if not duration or math.abs(carnageDuration - duration) > 1 then
|
||||
if Extension.Debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00aaff[pfUI UnitOwnDebuff Override]|r %s: Carnage %.1fs/%.1fs (pfUI: %.1fs/%.1fs)",
|
||||
effect, carnageDuration, carnageTimeleft, duration or 0, timeleft or 0)
|
||||
)
|
||||
end
|
||||
duration = carnageDuration
|
||||
timeleft = carnageTimeleft
|
||||
end
|
||||
end
|
||||
-- If UnitOwnDebuff returned nil but we have a Carnage override, synthesize from UnitDebuff
|
||||
-- This fallback is only needed for edge cases where pfUI doesn't track the debuff yet
|
||||
elseif not effect and CleveRoids.carnageDurationOverrides then
|
||||
-- Use pflib:UnitDebuff which includes our Carnage override hook
|
||||
local baseEffect, baseRank, baseTex, baseStacks, baseDtype, baseDur, baseLeft, _ = pflib:UnitDebuff(unit, id)
|
||||
if baseEffect then
|
||||
local carnageDuration2, carnageTimeleft2 = GetCarnageOverride(baseEffect)
|
||||
if carnageDuration2 then
|
||||
return baseEffect, baseRank, baseTex, baseStacks, baseDtype, carnageDuration2, carnageTimeleft2, "player"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
|
||||
end
|
||||
|
||||
Extension.pfLibUnitOwnDebuffHooked = true
|
||||
Extension.DLOG("Hooked pfUI.api.libdebuff.UnitOwnDebuff (mismatch-only mode)")
|
||||
end
|
||||
|
||||
return Extension.pfLibDebuffHooked or Extension.pfLibAddEffectHooked or Extension.pfLibUnitDebuffHooked or Extension.pfLibUnitOwnDebuffHooked
|
||||
end
|
||||
|
||||
-- Synchronize combo durations to pfUI's libdebuff objects
|
||||
-- NOTE: pfUI 7.6+ handles combo durations internally - skip sync entirely
|
||||
function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
|
||||
if not pfUI or not pfUI.api or not pfUI.api.libdebuff then
|
||||
return
|
||||
end
|
||||
|
||||
-- pfUI 7.6+ handles all durations internally
|
||||
if CleveRoids.hasPfUI76 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get unit name from GUID
|
||||
local unitName = nil
|
||||
local unitLevel = 0
|
||||
|
||||
-- Check if this is the current target
|
||||
local targetGUID = CleveRoids.GetGUID("target")
|
||||
if targetGUID == guid then
|
||||
unitName = UnitName("target")
|
||||
unitLevel = UnitLevel("target") or 0
|
||||
end
|
||||
|
||||
-- If we couldn't find the unit, use GUID to name mapping from libdebuff
|
||||
if not unitName and CleveRoids.libdebuff and CleveRoids.libdebuff.guidToName then
|
||||
unitName = CleveRoids.libdebuff.guidToName[guid]
|
||||
-- Default to level 0 if we don't have the unit targeted
|
||||
unitLevel = 0
|
||||
end
|
||||
|
||||
if not unitName then
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find unit name for GUID")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Get spell name from spell ID
|
||||
local spellName = C_Spell.GetSpellName(spellID)
|
||||
if not spellName then
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find spell name for ID " .. spellID)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove rank from spell name to match pfUI's format
|
||||
local effectName = CleveRoids.StripRank(spellName)
|
||||
|
||||
-- Update pfUI's stored debuff duration
|
||||
local pflib = pfUI.api.libdebuff
|
||||
if pflib.objects and pflib.objects[unitName] then
|
||||
-- Try both the specific level and level 0 (fallback)
|
||||
for _, level in ipairs({unitLevel, 0}) do
|
||||
if pflib.objects[unitName][level] and pflib.objects[unitName][level][effectName] then
|
||||
local old_duration = pflib.objects[unitName][level][effectName].duration
|
||||
pflib.objects[unitName][level][effectName].duration = duration
|
||||
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00ffaa[pfUI Sync]|r Updated %s on %s (L%d): %ds -> %ds",
|
||||
effectName, unitName, level, old_duration or 0, duration)
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cffaaaa00[pfUI Sync]|r Effect not found in pfUI storage: %s on %s",
|
||||
effectName, unitName)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- Register action event handler for pfUI button updates
|
||||
function Extension.RegisterPfUIActionEventHandler()
|
||||
if not pfUI or Extension.actionHandlerRegistered then
|
||||
@@ -429,7 +88,17 @@ function Extension.RegisterPfUIActionEventHandler()
|
||||
-- Register a handler that will be called whenever CleveRoids updates macro states
|
||||
if CleveRoids.RegisterActionEventHandler then
|
||||
Extension.DLOG("Registering pfUI action event handler")
|
||||
CleveRoids.RegisterActionEventHandler(function(slot, event, ...)
|
||||
-- Declared without `...` on purpose: in 1.12's Lua 5.0 a vararg function
|
||||
-- allocates an `arg` table on every call, and this handler never read it.
|
||||
-- That mattered because UpdateAllManagedCooldowns fans
|
||||
-- ACTIONBAR_UPDATE_COOLDOWN out across every managed slot -- up to 120 calls
|
||||
-- -- on each SPELL_UPDATE_COOLDOWN, which fires on every GCD and cooldown
|
||||
-- tick. Those calls all allocated a table and then did nothing, because the
|
||||
-- whole body only ever applied to ACTIONBAR_SLOT_CHANGED. Hence the early
|
||||
-- return before any work.
|
||||
CleveRoids.RegisterActionEventHandler(function(slot, event)
|
||||
if event ~= "ACTIONBAR_SLOT_CHANGED" then return end
|
||||
|
||||
local button = pfUI.bars and pfUI.bars.buttons and pfUI.bars.buttons[slot]
|
||||
|
||||
if Extension.Debug then
|
||||
@@ -441,23 +110,21 @@ function Extension.RegisterPfUIActionEventHandler()
|
||||
))
|
||||
end
|
||||
|
||||
-- For slot change events, do a full button update so the icon,
|
||||
-- cooldown, and tooltip refresh through CleveRoids' hooked
|
||||
-- GetActionTexture / GetActionCooldown / GameTooltip:SetAction.
|
||||
-- Full button update so the icon, cooldown and tooltip refresh.
|
||||
-- pfUI's ButtonMacroScan defers to us for managed macros (leaves
|
||||
-- spellslot nil), so its ButtonFullUpdate routes through those hooks
|
||||
-- and follows the active conditional — no manual cooldown override
|
||||
-- needed.
|
||||
if event == "ACTIONBAR_SLOT_CHANGED" then
|
||||
-- Mark the slot for update in pfUI's cache (processed next OnUpdate)
|
||||
if pfUI.bars and pfUI.bars.update then
|
||||
pfUI.bars.update[slot] = true
|
||||
end
|
||||
-- spellslot nil), so ButtonFullUpdate reads the stock action-bar
|
||||
-- functions -- which now resolve through the value we publish with
|
||||
-- C_Macro.SetMacroDisplay, rather than the Lua overrides this addon
|
||||
-- used to install.
|
||||
|
||||
-- Also directly call ButtonFullUpdate if the button exists
|
||||
if button and pfUI.bars.ButtonFullUpdate then
|
||||
pfUI.bars.ButtonFullUpdate(button)
|
||||
end
|
||||
-- Mark the slot for update in pfUI's cache (processed next OnUpdate)
|
||||
if pfUI.bars and pfUI.bars.update then
|
||||
pfUI.bars.update[slot] = true
|
||||
end
|
||||
|
||||
-- Also directly call ButtonFullUpdate if the button exists
|
||||
if button and pfUI.bars.ButtonFullUpdate then
|
||||
pfUI.bars.ButtonFullUpdate(button)
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -474,9 +141,6 @@ function Extension.SetupCompatibility()
|
||||
if Extension.pfUILoaded then
|
||||
Extension.DLOG("pfUI detected")
|
||||
|
||||
-- Hook libdebuff for combo duration support
|
||||
Extension.HookPfUILibdebuff()
|
||||
|
||||
-- Register action event handler for button updates
|
||||
Extension.RegisterPfUIActionEventHandler()
|
||||
|
||||
@@ -631,16 +295,9 @@ function Extension.SetupPfUIEventHooks(lib)
|
||||
ev:UnregisterEvent("AURA_CAST_ON_OTHER")
|
||||
ev:UnregisterEvent("DEBUFF_ADDED_OTHER")
|
||||
ev:UnregisterEvent("DEBUFF_REMOVED_OTHER")
|
||||
ev:UnregisterEvent("BUFF_ADDED_OTHER")
|
||||
ev:UnregisterEvent("BUFF_REMOVED_SELF")
|
||||
ev:UnregisterEvent("BUFF_REMOVED_OTHER")
|
||||
|
||||
-- pfUI 7.6+ also handles cast tracking internally
|
||||
if lib.hasPfUI76 then
|
||||
ev:UnregisterEvent("SPELL_START_OTHER")
|
||||
ev:UnregisterEvent("SPELL_FAILED_OTHER")
|
||||
end
|
||||
|
||||
-- Keep registered: SPELL_START_SELF (channel duration capture before early return),
|
||||
-- UNIT_DIED (AllCasterAuraTracking + OverflowBuff cleanup), UNIT_CASTEVENT (SuperWoW),
|
||||
-- PLAYER_TARGET_CHANGED, UNIT_AURA (SeedUnit)
|
||||
@@ -700,7 +357,6 @@ end
|
||||
|
||||
function Extension.OnLoad()
|
||||
Extension.DLOG("Extension pfUI Loaded.")
|
||||
Extension.HookMethod(CleveRoids, "GetFocusName", "FocusNameHook", true)
|
||||
|
||||
-- Export extension for external access
|
||||
CleveRoids.Compatibility_pfUI = Extension
|
||||
@@ -743,7 +399,7 @@ end
|
||||
|
||||
function Extension.OnPlayerLogin()
|
||||
-- Ensure lib.objects is linked correctly (InitPfUIIntegration is idempotent).
|
||||
if pfUI and not CleveRoids.hasPfUI76 then
|
||||
if pfUI then
|
||||
local lib = CleveRoids.libdebuff
|
||||
if lib and lib.InitPfUIIntegration then
|
||||
lib:InitPfUIIntegration()
|
||||
@@ -775,9 +431,6 @@ function Extension.OnPlayerLogin()
|
||||
-- Print startup status only if pfUI global exists and compatibility was set up
|
||||
if Extension.pfUILoaded and pfUI then
|
||||
local statusMsg = "|cff00ff00[SCRM]|r pfUI compatibility loaded"
|
||||
if CleveRoids.hasPfUI76 then
|
||||
statusMsg = statusMsg .. " (7.6+ GUID cast tracking)"
|
||||
end
|
||||
-- statusMsg = statusMsg .. ". Use /pfuicd for debug."
|
||||
DEFAULT_CHAT_FRAME:AddMessage(statusMsg)
|
||||
if not Extension.actionHandlerRegistered then
|
||||
|
||||
+238
-326
@@ -779,14 +779,6 @@ CleveRoids.DownrankBlocked = CleveRoids.DownrankBlocked or {}
|
||||
function CleveRoids.GetAuraTrackingData(targetGuid)
|
||||
if not targetGuid then return nil, false end
|
||||
|
||||
-- pfUI path: read directly from pfUI's table (has downrank protection built-in)
|
||||
if CleveRoids.hasPfUI76 and pfUI and pfUI.libdebuff_all_auras then
|
||||
local data = pfUI.libdebuff_all_auras[targetGuid]
|
||||
if data then return data, true end
|
||||
-- Fall through: our table may have test entries even when pfUI is active
|
||||
end
|
||||
|
||||
-- Standalone path (or pfUI had no data for this GUID)
|
||||
local data = CleveRoids.AllCasterAuraTracking[targetGuid]
|
||||
if data then return data, false end
|
||||
return nil, false
|
||||
@@ -854,74 +846,6 @@ function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Helper to find aura by name (or spell ID string) for a target
|
||||
-- Returns player's entry for personal debuffs, any caster for shared auras.
|
||||
-- Reads from pfUI.libdebuff_all_auras when pfUI 7.6+ is active.
|
||||
function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName)
|
||||
if not targetGuid or not searchName then return nil, nil end
|
||||
local targetData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid)
|
||||
if not targetData then return nil, nil end
|
||||
|
||||
-- Resolve spell ID to name for direct lookup
|
||||
local searchID = tonumber(searchName)
|
||||
if searchID then
|
||||
local resolvedName = C_Spell.GetSpellName(searchID)
|
||||
if not resolvedName then return nil, nil end
|
||||
searchName = resolvedName
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
|
||||
-- Try exact match first (O(1) hash lookup)
|
||||
local casters = targetData[searchName]
|
||||
|
||||
-- Case-insensitive fallback
|
||||
if not casters then
|
||||
local searchLower = string.lower(searchName)
|
||||
for spellName, c in pairs(targetData) do
|
||||
local baseName = CleveRoids.StripRank(spellName)
|
||||
if string.lower(baseName) == searchLower then
|
||||
casters = c
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not casters then return nil, nil end
|
||||
|
||||
local playerGuid = CleveRoids.GetGUID("player")
|
||||
|
||||
-- Always check player's own entry first
|
||||
if playerGuid and casters[playerGuid] then
|
||||
local auraData = casters[playerGuid]
|
||||
local startTime = AuraStart(auraData, isPfUI)
|
||||
if startTime and auraData.duration then
|
||||
local remaining = auraData.duration + startTime - now
|
||||
if remaining > 0 then return remaining, playerGuid end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get a spellId from any entry to check personal vs shared
|
||||
local anySpellId = nil
|
||||
for _, aData in pairs(casters) do
|
||||
anySpellId = aData.spellId
|
||||
break
|
||||
end
|
||||
|
||||
-- Personal debuff and player has no active entry → don't use other players' data
|
||||
if IsPersonalAura(anySpellId, searchName) then return nil, nil end
|
||||
|
||||
-- Shared aura: return any active caster's entry
|
||||
for cGuid, auraData in pairs(casters) do
|
||||
local startTime = AuraStart(auraData, isPfUI)
|
||||
if startTime and auraData.duration then
|
||||
local remaining = auraData.duration + startTime - now
|
||||
if remaining > 0 then return remaining, cGuid end
|
||||
end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- HitInfo bitfield values (from NampowerAPI.lua, duplicated for local access)
|
||||
-- Converted to decimal for Lua 5.0 compatibility (no hex literals)
|
||||
local HITINFO_MISS = 16 -- 0x10
|
||||
@@ -973,43 +897,6 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo,
|
||||
CleveRoids.LastSwing.resistAmount = totalResist or 0
|
||||
CleveRoids.LastSwing.targetGuid = targetGuid
|
||||
|
||||
-- Paladin: refresh active Judgements on melee hit (Nampower fallback for UNIT_CASTEVENT)
|
||||
if CleveRoids.playerClass == "PALADIN" and targetGuid then
|
||||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||||
if lib and lib.objects then
|
||||
local normalizedTarget = CleveRoids.NormalizeGUID(targetGuid)
|
||||
if normalizedTarget and lib.objects[normalizedTarget] then
|
||||
for spellID, rec in pairs(lib.objects[normalizedTarget]) do
|
||||
if lib.judgementSpells and lib.judgementSpells[spellID] and rec.start and rec.duration then
|
||||
local remaining = rec.duration + rec.start - GetTime()
|
||||
if remaining > 0 and rec.caster == "player" then
|
||||
rec.start = GetTime()
|
||||
|
||||
if CleveRoids.debug then
|
||||
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
|
||||
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on melee hit - new duration: %ds",
|
||||
baseName, spellID, rec.duration)
|
||||
)
|
||||
end
|
||||
|
||||
-- Sync to pfUI if loaded (pre-7.6 only)
|
||||
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
|
||||
local spellName = C_Spell.GetSpellName(spellID) or nil
|
||||
local baseName = CleveRoids.StripRank(spellName)
|
||||
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
|
||||
local targetLevel = UnitLevel("target") or 0
|
||||
if targetName and baseName then
|
||||
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Process AUTO_ATTACK_SELF event (player being attacked)
|
||||
@@ -1137,31 +1024,11 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur
|
||||
end
|
||||
end
|
||||
|
||||
-- NEW: Populate ownBuffCasts and allBuffAuras for all player buffs (not just overflow)
|
||||
-- Use the isBuffNotDebuff result determined above (avoids redundant slot scanning)
|
||||
local lib = CleveRoids.libdebuff
|
||||
if isBuffNotDebuff and spellId and durationMs and durationMs > 0 and lib and not lib.hasPfUIEnhanced then
|
||||
local spellName = C_Spell.GetSpellName(spellId)
|
||||
if spellName then
|
||||
local playerGuid = CleveRoids.GetGUID("player")
|
||||
if playerGuid then
|
||||
lib.ownBuffCasts[playerGuid] = lib.ownBuffCasts[playerGuid] or {}
|
||||
lib.ownBuffCasts[playerGuid][spellName] = {
|
||||
startTime = now,
|
||||
duration = durationMs / 1000,
|
||||
spellId = spellId,
|
||||
casterGuid = casterGuid,
|
||||
}
|
||||
lib.allBuffAuras[playerGuid] = lib.allBuffAuras[playerGuid] or {}
|
||||
lib.allBuffAuras[playerGuid][spellName] = lib.allBuffAuras[playerGuid][spellName] or {}
|
||||
lib.allBuffAuras[playerGuid][spellName][casterGuid or "unknown"] = {
|
||||
startTime = now,
|
||||
duration = durationMs / 1000,
|
||||
rank = 0,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Removed: this populated lib.ownBuffCasts and lib.allBuffAuras for every player
|
||||
-- buff. Both tables were write-only -- populated here and on BUFF_ADDED_OTHER,
|
||||
-- swept periodically, cleared on removal and death, and never read for aura state.
|
||||
-- Player buff timing now comes from C_UnitAuras, which reads expirationTime out of
|
||||
-- the engine's own player-buff table.
|
||||
end
|
||||
|
||||
local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAuraName,
|
||||
@@ -1175,7 +1042,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
|
||||
-- full downrank protection — we read from that table via GetAuraTrackingData().
|
||||
if spellId and durationMs and durationMs > 0 then
|
||||
local spellName = C_Spell.GetSpellName(spellId)
|
||||
if spellName and not CleveRoids.hasPfUI76 then
|
||||
if spellName then
|
||||
CleveRoids._allCasterAuraDirty = true
|
||||
if not CleveRoids.AllCasterAuraTracking[targetGuid] then
|
||||
CleveRoids.AllCasterAuraTracking[targetGuid] = {}
|
||||
@@ -1198,7 +1065,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
|
||||
string.format("|cffff6600[AuraTrack]|r %s Rank %d blocked by Rank %d (%.1fs left) on %s",
|
||||
spellName, newRank, existingRank, timeleft,
|
||||
string.sub(tostring(targetGuid), 1, 16)))
|
||||
spellName = nil -- skip pendingBuffCasts below too
|
||||
spellName = nil -- downranked: don't record this cast
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1228,24 +1095,6 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
|
||||
string.sub(tostring(casterGuid), 1, 16), durationMs / 1000))
|
||||
end
|
||||
|
||||
-- Store in pendingBuffCasts for BUFF_ADDED_OTHER to confirm as buff
|
||||
-- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff)
|
||||
local lib = CleveRoids.libdebuff
|
||||
if lib and not lib.hasPfUIEnhanced then
|
||||
local spellNameForPending = C_Spell.GetSpellName(spellId)
|
||||
if spellNameForPending then
|
||||
local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid)
|
||||
if normTargetGuid then
|
||||
lib.pendingBuffCasts[normTargetGuid] = lib.pendingBuffCasts[normTargetGuid] or {}
|
||||
lib.pendingBuffCasts[normTargetGuid][spellId] = {
|
||||
casterGuid = CleveRoids.NormalizeGUID(casterGuid),
|
||||
duration = durationMs / 1000,
|
||||
spellName = spellNameForPending,
|
||||
time = now,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Store cap status for this target GUID (if available)
|
||||
@@ -1378,7 +1227,7 @@ autoAttackFrame:SetScript("OnEvent", function()
|
||||
if spellId and spellId > 0 and durationMs and durationMs > 0 then
|
||||
local playerGUID = CleveRoids.GetGUID("player")
|
||||
local durSpellName = C_Spell.GetSpellName(spellId)
|
||||
if playerGUID and durSpellName and not CleveRoids.hasPfUI76 then
|
||||
if playerGUID and durSpellName then
|
||||
CleveRoids._allCasterAuraDirty = true
|
||||
if not CleveRoids.AllCasterAuraTracking[playerGUID] then
|
||||
CleveRoids.AllCasterAuraTracking[playerGUID] = {}
|
||||
@@ -3478,6 +3327,51 @@ function CleveRoids.ClearSpellNameCaches()
|
||||
_baseNameCacheSize = 0
|
||||
end
|
||||
|
||||
-- Resolve one aura on `unit` straight from ClassicAPI. This is the source of truth
|
||||
-- for aura state: C_UnitAuras reads the unit's own descriptor, and its Aura::Source
|
||||
-- cache reconstructs duration/expirationTime for ANY unit from the observed
|
||||
-- SMSG_SPELL_GO -- caster-modified, so talent extensions and combo-point finisher
|
||||
-- scaling are already applied, and Carnage's roll-gated Rip/Rake refresh is handled
|
||||
-- in the DLL rather than mirrored here.
|
||||
--
|
||||
-- Matching stays ours: by spellID when the conditional gave a number, else by
|
||||
-- lowercased name (C_UnitAuras' own by-name lookup is case-sensitive and exact,
|
||||
-- which would miss [debuff:thunder_clap]).
|
||||
--
|
||||
-- Returns found, stacks, remaining, spellId. `remaining` is -1 for an aura with no
|
||||
-- duration (permanent), nil when ClassicAPI has no timing for it -- an aura cast
|
||||
-- before we logged in, or one refreshed at max stacks -- and seconds otherwise.
|
||||
-- Presence and stacks are always reliable; only timing is best-effort.
|
||||
local function ResolveUnitAuraViaClassicAPI(unit, searchID, searchName, isbuff)
|
||||
local filter = isbuff and "HELPFUL" or "HARMFUL"
|
||||
local i = 1
|
||||
while i <= 48 do
|
||||
local name, _, count, _, duration, expirationTime, _, _, _, spellId =
|
||||
C_UnitAuras.UnitAura(unit, i, filter)
|
||||
if not name then break end
|
||||
|
||||
local hit
|
||||
if searchID then
|
||||
hit = (spellId == searchID)
|
||||
elseif searchName then
|
||||
hit = (_string_lower(name) == searchName)
|
||||
end
|
||||
|
||||
if hit then
|
||||
local remaining
|
||||
if expirationTime and expirationTime > 0 then
|
||||
remaining = expirationTime - GetTime()
|
||||
if remaining < 0 then remaining = 0 end
|
||||
elseif duration == 0 then
|
||||
remaining = -1 -- no duration: permanent aura
|
||||
end
|
||||
return true, count or 0, remaining, spellId
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function CleveRoids.ValidateAura(unit, args, isbuff)
|
||||
if not args or not UnitExists(unit) then return false end
|
||||
|
||||
@@ -3736,148 +3630,56 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
|
||||
end
|
||||
end
|
||||
|
||||
-- allBuffAuras fallback for player buff timing: when slow path found the buff but
|
||||
-- returned no remaining time, check lib.allBuffAuras for cached AURA_CAST timing.
|
||||
if found and remaining == nil and isPlayer and isbuff and searchName then
|
||||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||||
if lib and lib.allBuffAuras then
|
||||
local playerGuid = CleveRoids.GetGUID("player")
|
||||
if playerGuid and lib.allBuffAuras[playerGuid] then
|
||||
-- Try exact name match first
|
||||
local casters = lib.allBuffAuras[playerGuid][args.name]
|
||||
-- Try lowercase match if exact didn't work
|
||||
if not casters then
|
||||
for bName, c in pairs(lib.allBuffAuras[playerGuid]) do
|
||||
if _string_lower(bName) == searchName then
|
||||
casters = c
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if casters then
|
||||
for _, cData in pairs(casters) do
|
||||
local elapsed = GetTime() - (cData.startTime or 0)
|
||||
remaining = cData.duration > 0 and (cData.duration - elapsed) or -1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Player timing gap-fill, from ClassicAPI. Replaces the lib.allBuffAuras lookup
|
||||
-- that cached AURA_CAST start/duration for this: for the player, C_UnitAuras reads
|
||||
-- expirationTime out of the engine's own player-buff table, so it is the more
|
||||
-- authoritative source, not a fallback. Only runs when the scans above found the
|
||||
-- aura but produced no time.
|
||||
if found and remaining == nil and isPlayer and (searchID or searchName) then
|
||||
local _, _, capRemaining = ResolveUnitAuraViaClassicAPI(unit, searchID, searchName, isbuff)
|
||||
if capRemaining ~= nil then
|
||||
remaining = capRemaining
|
||||
end
|
||||
end
|
||||
|
||||
-- Non-player overflow fallback: buff exists in server slots 33-48 (no client slot)
|
||||
-- AllCasterAuraTracking already has data from AURA_CAST_ON_OTHER for all aura applications.
|
||||
-- If the normal scan didn't find the buff, check there for presence + duration.
|
||||
-- Guard: verify the spell isn't a visible debuff on the target (AllCasterAuraTracking
|
||||
-- stores both buffs and debuffs, so without this check [buff:DebuffName] could false-positive).
|
||||
if not found and not isPlayer and isbuff and (searchID or searchName) then
|
||||
local targetGuid = CleveRoids.GetGUID(unit)
|
||||
if targetGuid then
|
||||
-- Check if the spell is in a visible debuff slot — if so, it's a debuff, not a buff
|
||||
local isDebuff = false
|
||||
local di = 1
|
||||
while true do
|
||||
local dtex, _, _, dspellId = UnitDebuff(unit, di)
|
||||
if not dtex then break end
|
||||
if dspellId then
|
||||
if searchID then
|
||||
if dspellId == searchID then
|
||||
isDebuff = true
|
||||
break
|
||||
end
|
||||
elseif searchName then
|
||||
local lowerName = GetLowercaseSpellName(dspellId)
|
||||
if lowerName and lowerName == searchName then
|
||||
isDebuff = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
di = di + 1
|
||||
end
|
||||
|
||||
if not isDebuff then
|
||||
local trackRemaining = CleveRoids.FindAllCasterAuraByName(targetGuid,
|
||||
searchID and tostring(searchID) or args.name)
|
||||
if trackRemaining then
|
||||
found = true
|
||||
stacks = 0
|
||||
remaining = trackRemaining
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Removed: the non-player overflow fallback. It existed because a buff can sit in
|
||||
-- server slots 33-48 with no client slot, so it read presence/duration out of
|
||||
-- AllCasterAuraTracking, guarded by a UnitDebuff slot scan to stop [buff:Name]
|
||||
-- matching a debuff (that table stores both). ClassicAPI makes all of it moot: its
|
||||
-- HELPFUL/HARMFUL filters select on each aura's real polarity flag rather than
|
||||
-- which slot range it happens to occupy, so "a debuff parked in a buff slot still
|
||||
-- reads harmful" (docs/API.md) and an overflowed buff still reads helpful. The
|
||||
-- ClassicAPI resolution above therefore already covers the overflow case, and it
|
||||
-- classifies more accurately than the slot-range guard did.
|
||||
|
||||
local ops = CleveRoids.operators
|
||||
local cmp = CleveRoids.comparators
|
||||
|
||||
-- For non-player units with time comparisons, try to get time from tracking systems
|
||||
-- Non-player aura timing, straight from ClassicAPI. This replaces the old
|
||||
-- lib.allBuffAuras lookup and the AllCasterAuraTracking / FindAllCasterAuraByName
|
||||
-- fallback beneath it: both existed only because vanilla cannot report a timer for
|
||||
-- an aura on another unit, which the Aura::Source cache now does. It also drops
|
||||
-- the libdebuff UnitBuff timeleft bug those comments worked around.
|
||||
local nonPlayerAuraTimeRemaining = nil
|
||||
if not isPlayer and args.name then
|
||||
-- NOTE: libdebuff's UnitBuff has a bug where timeleft returns incorrect values
|
||||
-- (showing ~1000s instead of actual remaining time). Skip it for buff time checks
|
||||
-- and rely on all-caster tracking from AURA_CAST events instead.
|
||||
|
||||
-- Fast path: Check lib.allBuffAuras (spellName-indexed, O(1) lookup)
|
||||
-- More efficient than FindAllCasterAuraByName which does ID→name translation
|
||||
if nonPlayerAuraTimeRemaining == nil and isbuff then
|
||||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||||
if lib and lib.allBuffAuras then
|
||||
local targetGuid = CleveRoids.GetGUID(unit)
|
||||
if targetGuid then
|
||||
local buffEntries = lib.allBuffAuras[targetGuid]
|
||||
if buffEntries then
|
||||
-- Try exact name match first
|
||||
local casters = buffEntries[args.name]
|
||||
-- Try lowercase match if exact didn't work
|
||||
if not casters and searchName then
|
||||
for bName, c in pairs(buffEntries) do
|
||||
if _string_lower(bName) == searchName then
|
||||
casters = c
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if casters then
|
||||
for _, cData in pairs(casters) do
|
||||
local elapsed = GetTime() - (cData.startTime or 0)
|
||||
local rem = cData.duration > 0 and (cData.duration - elapsed) or -1
|
||||
if rem == nil or rem > 0 or cData.duration <= 0 then
|
||||
nonPlayerAuraTimeRemaining = rem
|
||||
if not found then
|
||||
found = true
|
||||
stacks = 0
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not isPlayer and (searchID or searchName) then
|
||||
local capFound, capStacks, capRemaining =
|
||||
ResolveUnitAuraViaClassicAPI(unit, searchID, searchName, isbuff)
|
||||
if capFound then
|
||||
if not found then
|
||||
found = true
|
||||
stacks = capStacks or 0
|
||||
end
|
||||
-- Left nil when ClassicAPI has no timing (aura predates login, or a
|
||||
-- max-stack refresh); the caller then falls back to its found-and-0 default.
|
||||
nonPlayerAuraTimeRemaining = capRemaining
|
||||
end
|
||||
|
||||
-- Second try: All-caster tracking from AURA_CAST events (works for any caster)
|
||||
-- Only use if libdebuff didn't find it (libdebuff has more accurate timing for player casts)
|
||||
if nonPlayerAuraTimeRemaining == nil then
|
||||
local targetGuid = CleveRoids.GetGUID(unit)
|
||||
if targetGuid then
|
||||
local remaining, casterGuid = CleveRoids.FindAllCasterAuraByName(targetGuid, args.name)
|
||||
|
||||
-- Debug output when enabled
|
||||
if CleveRoids.debug then
|
||||
local hasData = CleveRoids.AllCasterAuraTracking[targetGuid] ~= nil
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cffff9900[AuraLookup]|r %s on GUID %s: hasData=%s, remaining=%s",
|
||||
tostring(args.name), string.sub(tostring(targetGuid), 1, 16),
|
||||
tostring(hasData), tostring(remaining)
|
||||
))
|
||||
end
|
||||
|
||||
if remaining then
|
||||
nonPlayerAuraTimeRemaining = remaining
|
||||
end
|
||||
end
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cffff9900[AuraLookup]|r %s on %s: found=%s, stacks=%s, remaining=%s",
|
||||
tostring(args.name), tostring(unit), tostring(capFound),
|
||||
tostring(capStacks), tostring(capRemaining)
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5045,7 +4847,8 @@ end
|
||||
-- ============================================================================
|
||||
|
||||
-- Maps CC type names to mechanic constants (matches DBC mechanic IDs)
|
||||
-- Note: Some types map to multiple mechanics via CCMechanicGroups below
|
||||
-- One name, one mechanic: [cc:type] follows the exact DBC mechanic. Several
|
||||
-- names may share a mechanic as aliases, but no name spans two mechanics.
|
||||
CleveRoids.CCMechanics = {
|
||||
-- Movement/control impairment
|
||||
charm = 1, -- Mind Control, Seduction
|
||||
@@ -5054,31 +4857,30 @@ CleveRoids.CCMechanics = {
|
||||
disarm = 3, -- Disarm, Riposte disarm
|
||||
distract = 4, -- Distract (Rogue ability)
|
||||
fear = 5, -- Fear, Psychic Scream, Howl of Terror
|
||||
grip = 6, -- Grip effects
|
||||
fumble = 6, -- DBC Fumble mechanic
|
||||
grip = 6, -- Legacy alias for fumble
|
||||
root = 7, -- Entangling Roots, Frost Nova, Improved Hamstring
|
||||
pacify = 8, -- Pacify effects
|
||||
silence = 9, -- Silence, Kick, Counterspell (lockout)
|
||||
sleep = 10, -- Hibernate, Wyvern Sting sleep
|
||||
snare = 11, -- Hamstring, Wing Clip, Crippling Poison
|
||||
slow = 11, -- Alias for snare
|
||||
stun = 12, -- Consolidated: Stun(12) + Knockout(14) + Sap(30)
|
||||
freeze = 13, -- Freeze effects (Frost Nova freeze)
|
||||
stun = 12, -- Stun (Cheap Shot, Kidney Shot, Hammer of Justice)
|
||||
freeze = 13, -- Freeze effects
|
||||
knockout = 14, -- Knockout (Gouge, Repentance)
|
||||
bleed = 15, -- Rend, Garrote, Deep Wounds
|
||||
polymorph = 17, -- Polymorph (all variants)
|
||||
banish = 18, -- Banish (Warlock)
|
||||
shackle = 20, -- Shackle Undead
|
||||
turn = 23, -- Turn effects
|
||||
horror = 24, -- Death Coil (Warlock), Intimidating Shout (horror)
|
||||
interrupt = 26, -- Interrupt mechanic
|
||||
daze = 27, -- Dazed effects
|
||||
}
|
||||
|
||||
-- Mechanic groups: CC types that check multiple DBC mechanics
|
||||
-- Used when a single conditional should match several related effects
|
||||
CleveRoids.CCMechanicGroups = {
|
||||
stun = {12, 14, 30}, -- Stun(12), Knockout/Gouge(14), Sap(30)
|
||||
sap = 30, -- Sap/Sapped mechanic
|
||||
}
|
||||
|
||||
-- CC types that count as "crowd controlled" (loss of control)
|
||||
-- Note: Mechanics 12, 14, 30 are all consolidated under "stun" for conditionals
|
||||
-- Individual DBC mechanics remain distinct; this table only powers [cc]/[cc:any].
|
||||
CleveRoids.CCTypesLossOfControl = {
|
||||
[1] = true, -- charm
|
||||
[2] = true, -- disoriented
|
||||
@@ -5087,12 +4889,12 @@ CleveRoids.CCTypesLossOfControl = {
|
||||
[10] = true, -- sleep
|
||||
[12] = true, -- stun (Cheap Shot, Kidney Shot, etc.)
|
||||
[13] = true, -- freeze
|
||||
[14] = true, -- knockout/gouge (now part of stun group)
|
||||
[14] = true, -- knockout/gouge
|
||||
[17] = true, -- polymorph
|
||||
[18] = true, -- banish
|
||||
[20] = true, -- shackle
|
||||
[24] = true, -- horror
|
||||
[30] = true, -- sap (now part of stun group)
|
||||
[30] = true, -- sap
|
||||
}
|
||||
|
||||
-- Check if BuffLib is available with full mechanic support
|
||||
@@ -5159,19 +4961,6 @@ function CleveRoids.ValidateUnitCC(unit, ccType)
|
||||
return CleveRoids.ValidateUnitAnyCrowdControl(unit)
|
||||
end
|
||||
|
||||
-- Check if this CC type maps to a group of mechanics
|
||||
local mechanicGroup = CleveRoids.CCMechanicGroups[ccTypeLower]
|
||||
if mechanicGroup then
|
||||
-- Check all mechanics in the group (e.g., stun checks 12, 14, 30)
|
||||
for _, mechanic in ipairs(mechanicGroup) do
|
||||
if CleveRoids.ValidateUnitCCSingleMechanic(unit, mechanic) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Single mechanic lookup
|
||||
local mechanic = CleveRoids.CCMechanics[ccTypeLower]
|
||||
if not mechanic then return false end
|
||||
|
||||
@@ -5439,32 +5228,154 @@ local function ResolvePetHappinessState(value)
|
||||
return PET_HAPPINESS_STATES[GetLowercaseString(value)]
|
||||
end
|
||||
|
||||
-- ClassicAPI's totem bar is TBC-ordered: SUMMON_TOTEM_SLOT1..4 in Spell.dbc.
|
||||
local TOTEM_SLOTS = {
|
||||
fire = 1,
|
||||
earth = 2,
|
||||
water = 3,
|
||||
air = 4,
|
||||
["1"] = 1,
|
||||
["2"] = 2,
|
||||
["3"] = 3,
|
||||
["4"] = 4,
|
||||
}
|
||||
|
||||
-- Resolve a [totem:X] argument to a slot. An element name or slot number names
|
||||
-- the slot directly; anything else is matched against the name of the totem
|
||||
-- standing in each slot, so [totem:Searing_Totem] asks for that totem rather
|
||||
-- than "whatever is in the fire slot". nil when neither matches -- which
|
||||
-- includes naming a totem that is not currently out.
|
||||
local function ResolveTotemSlot(name)
|
||||
if type(name) ~= "string" or name == "" then return nil end
|
||||
|
||||
local searchName = GetLowercaseString(
|
||||
CleveRoids.Trim(string.gsub(CleveRoids.StripRank(name), "_", " ")))
|
||||
|
||||
local slot = TOTEM_SLOTS[searchName]
|
||||
if slot then return slot end
|
||||
|
||||
for i = 1, 4 do
|
||||
-- Second return, not the first: GetTotemInfo's haveTotem reports whether
|
||||
-- the player carries the slot's TOOL item, not whether a totem is out.
|
||||
local _, totemName = GetTotemInfo(i)
|
||||
if totemName and totemName ~= "" and GetLowercaseString(totemName) == searchName then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- True while any totem slot has a timer running. Drives the OnUpdate re-test
|
||||
-- that keeps [totem:X<N] icons honest -- see the caller for why polling is the
|
||||
-- only option. GetTotemTimeLeft, not GetTotemInfo: this asks whether a countdown
|
||||
-- is in progress (a totem with no timer has nothing to go stale), and it reads
|
||||
-- the slot alone, where GetTotemInfo walks the bags for the tool item.
|
||||
function CleveRoids.AnyTotemTimerRunning()
|
||||
for i = 1, 4 do
|
||||
local timeLeft = GetTotemTimeLeft(i)
|
||||
if timeLeft and timeLeft > 0 then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- [totem:X] / [nototem:X], shaped like the aura validators: X is a plain name or
|
||||
-- a parsed comparison entry, comparisons read seconds left, and `#N` stack
|
||||
-- comparisons read 1 for a standing totem -- a totem is either up or it isn't.
|
||||
--
|
||||
-- An empty slot reads -1 on both axes, the same "missing counts as least" the
|
||||
-- aura path uses, so [totem:Searing_Totem<5] passes while the totem is expiring
|
||||
-- AND while it is absent: one clause for the whole recast macro.
|
||||
local function ValidateTotem(args)
|
||||
if not args then return false end
|
||||
|
||||
if type(args) ~= "table" then
|
||||
args = { name = args }
|
||||
end
|
||||
|
||||
local remaining, stacks = -1, -1
|
||||
local slot = ResolveTotemSlot(args.name)
|
||||
if slot then
|
||||
-- Occupancy comes from the name, not from the time left: a totem whose
|
||||
-- summon spell carries no SpellDuration row reads 0 seconds, and that is
|
||||
-- "up with no timer", not "absent".
|
||||
local _, totemName = GetTotemInfo(slot)
|
||||
if totemName and totemName ~= "" then
|
||||
stacks = 1
|
||||
remaining = GetTotemTimeLeft(slot)
|
||||
end
|
||||
end
|
||||
|
||||
local ops = CleveRoids.operators
|
||||
local cmp = CleveRoids.comparators
|
||||
|
||||
-- Multi-comparison (e.g. >2&<8) - ALL must pass
|
||||
if args.comparisons and type(args.comparisons) == "table" then
|
||||
for _, comp in ipairs(args.comparisons) do
|
||||
if not ops[comp.operator] then return false end
|
||||
local value = comp.checkStacks and stacks or remaining
|
||||
if not cmp[comp.operator](value, comp.amount) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if not args.amount and not args.operator and not args.checkStacks then
|
||||
return stacks == 1
|
||||
elseif args.amount and ops[args.operator] then
|
||||
return cmp[args.operator](args.checkStacks and stacks or remaining, args.amount)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- A list of Conditionals and their functions to validate them
|
||||
CleveRoids.Keywords = {
|
||||
-- [button:N] — true while mouse button N is held (1=Left, 2=Right, 3=Middle,
|
||||
-- 4/5=extra). Routed through Multi so OR/AND lists and repeated groups behave
|
||||
-- like every other argument conditional ([button:1/2] = left or right).
|
||||
-- [button] with no argument — true if any mapped mouse button is held.
|
||||
-- [button:N] — N is the button that INVOKED this action (1=Left, 2=Right,
|
||||
-- 3=Middle, 4/5=extra), not one held down as a modifier. That is retail's
|
||||
-- meaning, and it is why a keybind press counts as button 1: retail activates
|
||||
-- through the left-click path, so [button:1] passes for a keybind and a
|
||||
-- left-click alike, while [button:2] passes only for an actual right-click.
|
||||
-- Routed through Multi so OR/AND lists behave like every other argument
|
||||
-- conditional ([button:1/2] = invoked by left or right).
|
||||
--
|
||||
-- Bare [button] / [nobutton] ask whether a click drove this at all -- the one
|
||||
-- thing the underlying data says that retail's conditional cannot express,
|
||||
-- and the practical "activated by a keybind, not a click" test.
|
||||
button = function(conditionals)
|
||||
if type(conditionals.button) ~= "table" then
|
||||
return CleveRoids.AnyMouseButtonDown()
|
||||
return CleveRoids.WasClickActivated()
|
||||
end
|
||||
local invoking = CleveRoids.GetActivatingButton()
|
||||
return Multi(conditionals.button, function(button)
|
||||
local name = CleveRoids.buttons[button]
|
||||
return name and IsMouseButtonDown(name) or false
|
||||
return CleveRoids.buttons[button] == invoking
|
||||
end, conditionals, "button")
|
||||
end,
|
||||
|
||||
nobutton = function(conditionals)
|
||||
if type(conditionals.nobutton) ~= "table" then
|
||||
return not CleveRoids.AnyMouseButtonDown()
|
||||
return not CleveRoids.WasClickActivated()
|
||||
end
|
||||
local invoking = CleveRoids.GetActivatingButton()
|
||||
return NegatedMulti(conditionals.nobutton, function(button)
|
||||
local name = CleveRoids.buttons[button]
|
||||
return not (name and IsMouseButtonDown(name))
|
||||
return CleveRoids.buttons[button] ~= invoking
|
||||
end, conditionals, "nobutton")
|
||||
end,
|
||||
|
||||
-- [totem:X] — X names a totem-bar slot (fire/earth/water/air, or 1-4) or the
|
||||
-- totem itself ([totem:Searing_Totem]). Bare [totem] takes the action as its
|
||||
-- argument, as [mybuff] does, so /cast [nototem] Searing Totem is the whole
|
||||
-- recast macro. Time and stack comparisons follow the aura precedent; see
|
||||
-- ValidateTotem.
|
||||
totem = function(conditionals)
|
||||
return Multi(conditionals.totem, function(v)
|
||||
return ValidateTotem(v)
|
||||
end, conditionals, "totem")
|
||||
end,
|
||||
|
||||
nototem = function(conditionals)
|
||||
return NegatedMulti(conditionals.nototem, function(v)
|
||||
return not ValidateTotem(v)
|
||||
end, conditionals, "nototem")
|
||||
end,
|
||||
|
||||
exists = function(conditionals)
|
||||
return UnitExists(conditionals.target)
|
||||
end,
|
||||
@@ -9398,6 +9309,7 @@ CleveRoids.STATIC_CONDITIONALS = {
|
||||
mod = true, nomod = true,
|
||||
keydown = true, nokeydown = true,
|
||||
button = true, nobutton = true,
|
||||
totem = true, nototem = true,
|
||||
swimming = true, noswimming = true, swim = true, noswim = true,
|
||||
indoors = true, noindoors = true, outdoors = true, nooutdoors = true,
|
||||
rooted = true, norooted = true,
|
||||
|
||||
+9
-8
@@ -358,14 +358,15 @@ SlashCmdList.RUNMACRO = function(msg)
|
||||
return CleveRoids.ExecuteMacroByName(CleveRoids.Trim(msg))
|
||||
end
|
||||
|
||||
-- Global RunMacro wrapper for user convenience (delegates to namespaced internal function)
|
||||
-- This pattern ensures internal logic uses CleveRoids.ExecuteMacroByName and won't break
|
||||
-- if another addon overwrites the global RunMacro
|
||||
-- NOTE: When SuperMacro is also loaded, Compatibility/SuperMacro.lua redirects this to
|
||||
-- SuperMacro_RunMacro so macros go through RunLine (where CRM commands are intercepted)
|
||||
function RunMacro(name)
|
||||
return CleveRoids.ExecuteMacroByName(name)
|
||||
end
|
||||
-- The global RunMacro is installed by Core.lua, which loads before this file. Do not
|
||||
-- redefine it here: Core's hook accepts a macro index as well as a name (Blizzard's
|
||||
-- RunMacro takes either), delegates to SuperMacro when that is driving execution,
|
||||
-- falls back to the saved Blizzard original when a macro will not resolve, and clears
|
||||
-- the stop/skip flags at the start of a top-level run, which is what lets /stopmacro,
|
||||
-- /skipmacro and /firstaction work across parent/child macro boundaries. A plain
|
||||
-- name-only wrapper here silently replaced all of that.
|
||||
-- When SuperMacro is loaded, Compatibility/SuperMacro.lua redirects the global to
|
||||
-- SuperMacro_RunMacro so macros go through RunLine (where CRM commands are intercepted).
|
||||
|
||||
SLASH_RETARGET1 = "/retarget"
|
||||
SlashCmdList.RETARGET = function(msg)
|
||||
|
||||
+41
-25
@@ -697,30 +697,9 @@ local function EnsureCondHighlightPool()
|
||||
end
|
||||
end
|
||||
|
||||
-- Test whether conditionals pass for a given command + alternative text.
|
||||
-- Returns: true (passes), false (fails), nil (unconditional / no conditionals)
|
||||
local function TestConditionalPasses(cmd, alternative)
|
||||
-- Strip ? tooltip hints (irrelevant for conditional evaluation)
|
||||
if string.find(alternative, "?", 1, true) then
|
||||
alternative = string.gsub(alternative, "%?", "")
|
||||
end
|
||||
|
||||
local hasConditional = string.find(alternative, "%[") ~= nil
|
||||
|
||||
-- Dynamic commands: delegate to TestAction
|
||||
if CleveRoids.dynamicCmds[cmd] then
|
||||
local result = CleveRoids.TestAction(cmd, alternative)
|
||||
if not hasConditional then
|
||||
return nil -- unconditional
|
||||
end
|
||||
return result ~= nil and result ~= false
|
||||
end
|
||||
|
||||
-- Non-dynamic commands: parse and evaluate Keywords manually
|
||||
if not hasConditional then
|
||||
return nil -- unconditional
|
||||
end
|
||||
|
||||
-- Evaluate one single-group clause of a non-dynamic command against Keywords.
|
||||
-- Returns: true (passes), false (fails), nil (could not parse)
|
||||
local function EvaluateNonDynamic(alternative)
|
||||
local ok, action, conditionals = pcall(CleveRoids.GetParsedMsg, alternative)
|
||||
if not ok or not conditionals then
|
||||
return nil
|
||||
@@ -745,6 +724,43 @@ local function TestConditionalPasses(cmd, alternative)
|
||||
return passes
|
||||
end
|
||||
|
||||
-- Test whether conditionals pass for a given command + alternative text.
|
||||
-- Returns: true (passes), false (fails), nil (unconditional / no conditionals)
|
||||
local function TestConditionalPasses(cmd, alternative)
|
||||
-- Strip ? tooltip hints (irrelevant for conditional evaluation)
|
||||
if string.find(alternative, "?", 1, true) then
|
||||
alternative = string.gsub(alternative, "%?", "")
|
||||
end
|
||||
|
||||
local hasConditional = string.find(alternative, "%[") ~= nil
|
||||
|
||||
-- Dynamic commands: delegate to TestAction
|
||||
if CleveRoids.dynamicCmds[cmd] then
|
||||
local result = CleveRoids.TestAction(cmd, alternative)
|
||||
if not hasConditional then
|
||||
return nil -- unconditional
|
||||
end
|
||||
return result ~= nil and result ~= false
|
||||
end
|
||||
|
||||
-- Non-dynamic commands: evaluate Keywords manually. `[a][b] X` passes when
|
||||
-- any of its groups does.
|
||||
if not hasConditional then
|
||||
return nil -- unconditional
|
||||
end
|
||||
|
||||
local variants = CleveRoids.ExpandBracketGroups(alternative)
|
||||
if not variants then
|
||||
return EvaluateNonDynamic(alternative)
|
||||
end
|
||||
for i = 1, variants.n do
|
||||
if EvaluateNonDynamic(variants[i]) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Find character offset ranges for each semicolon-separated alternative.
|
||||
-- Returns array of { start, finish } pairs (1-based indices within argsText).
|
||||
local function FindAlternativeOffsets(argsText)
|
||||
@@ -1164,7 +1180,7 @@ local function ReportAllMacroErrors()
|
||||
|
||||
-- Collect body (syntax) errors per macro. Macro names are no longer
|
||||
-- restricted (slot/index-based identification), so no name checks here.
|
||||
for i = 1, 36 do
|
||||
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
|
||||
local nameOk, name = pcall(GetMacroInfo, i)
|
||||
if nameOk and name and name ~= "" then
|
||||
local errors = {}
|
||||
|
||||
@@ -423,7 +423,9 @@ function CleveRoids.IndexActionSlot(slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
CleveRoids.TestForActiveAction(CleveRoids.GetAction(slot))
|
||||
local actions = CleveRoids.GetAction(slot)
|
||||
CleveRoids.TestForActiveAction(actions)
|
||||
CleveRoids.PublishDisplay(actions)
|
||||
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
|
||||
end
|
||||
|
||||
|
||||
@@ -66,6 +66,10 @@ function CleveRoids.RegisterExtension(name)
|
||||
CleveRoids.RegisterEvent(name, eventName, callbackName)
|
||||
end
|
||||
|
||||
extension.RegisterUnitEvent = function(eventName, callbackName, ...)
|
||||
CleveRoids.RegisterUnitEvent(name, eventName, callbackName, unpack(arg))
|
||||
end
|
||||
|
||||
extension.Hook = function(functionName, callbackName, dontCallOriginal)
|
||||
CleveRoids.RegisterHook(name, functionName, callbackName, dontCallOriginal)
|
||||
end
|
||||
@@ -146,6 +150,20 @@ function CleveRoids.RegisterEvent(extensionName, eventName, callbackName)
|
||||
extension.internal.frame:RegisterEvent(eventName)
|
||||
end
|
||||
|
||||
-- Registers a callback for a UNIT_* event, filtered to the given unit tokens.
|
||||
-- The callback then only runs for those units: the client drops every other
|
||||
-- unit's copy, instead of all of them reaching Lua to be compared away. Use
|
||||
-- this over RegisterEvent whenever the handler starts by testing arg1.
|
||||
-- extensionName: The name of the extension trying to register the callback
|
||||
-- eventName: The UNIT_* event to register
|
||||
-- callbackName: The name of the callback that gets called when the event fires
|
||||
-- ...: the unit tokens to accept (e.g. "player", "target")
|
||||
function CleveRoids.RegisterUnitEvent(extensionName, eventName, callbackName, ...)
|
||||
local extension = CleveRoids.Extensions[extensionName]
|
||||
extension.internal.eventHandlers[eventName] = callbackName
|
||||
extension.internal.frame:RegisterUnitEvent(eventName, unpack(arg))
|
||||
end
|
||||
|
||||
-- Hooks the given function by it's name
|
||||
-- extensionName: The name of the extension trying to register the callback
|
||||
-- functionName: The name of the function that'll be hooked
|
||||
|
||||
@@ -19,12 +19,40 @@ 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 = {}
|
||||
@@ -43,13 +71,11 @@ 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}
|
||||
-- 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 = {}
|
||||
|
||||
-- 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
|
||||
|
||||
+28
-27
@@ -561,7 +561,6 @@ end
|
||||
local ERROR_TYPES = {
|
||||
INVALID_CONDITIONAL = "Invalid conditional",
|
||||
MISMATCHED_BRACKETS = "Mismatched brackets",
|
||||
EMPTY_CONDITIONAL = "Empty conditional block",
|
||||
INVALID_OPERATOR = "Invalid operator",
|
||||
MISSING_ARGUMENT = "Missing argument",
|
||||
INVALID_COMMAND = "Unknown command",
|
||||
@@ -849,16 +848,19 @@ local function validateLine(line, lineNum)
|
||||
actionPart = "/" .. actionPart -- Add leading slash if missing after split
|
||||
end
|
||||
|
||||
-- Parse conditionals if present - use non-greedy match
|
||||
local condStart = safeStringFind(actionPart, "%[")
|
||||
-- Parse the leading run of [conditional] blocks, if any. `[a][b] X` is
|
||||
-- Blizzard-style OR chaining: every block is validated, and the action
|
||||
-- is what follows the last one.
|
||||
local blocks = {}
|
||||
local condEnd = nil
|
||||
local conditionBlock = nil
|
||||
local condStart = safeStringFind(actionPart, "%[")
|
||||
local len = safeStringLen(actionPart)
|
||||
|
||||
if condStart then
|
||||
while condStart do
|
||||
-- Find matching closing bracket
|
||||
local depth = 0
|
||||
local inQuotes = false
|
||||
local len = safeStringLen(actionPart)
|
||||
local closePos = nil
|
||||
|
||||
for i = condStart, len do
|
||||
local char = safeStringSub(actionPart, i, i)
|
||||
@@ -872,23 +874,30 @@ local function validateLine(line, lineNum)
|
||||
elseif char == "]" then
|
||||
depth = depth - 1
|
||||
if depth == 0 then
|
||||
condEnd = i
|
||||
conditionBlock = safeStringSub(actionPart, condStart + 1, i - 1)
|
||||
closePos = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not closePos then break end
|
||||
|
||||
table.insert(blocks, safeStringSub(actionPart, condStart + 1, closePos - 1))
|
||||
condEnd = closePos
|
||||
|
||||
-- Another block directly after this one (whitespace allowed)?
|
||||
local _, wsEnd = safeStringFind(actionPart, "^%s*", closePos + 1)
|
||||
local nextPos = (wsEnd or closePos) + 1
|
||||
if safeStringSub(actionPart, nextPos, nextPos) == "[" then
|
||||
condStart = nextPos
|
||||
else
|
||||
condStart = nil
|
||||
end
|
||||
end
|
||||
|
||||
if conditionBlock then
|
||||
if safeTrim(conditionBlock) == "" then
|
||||
table.insert(localErrors, {
|
||||
type = ERROR_TYPES.EMPTY_CONDITIONAL,
|
||||
line = lineNum,
|
||||
message = "Empty conditional block []"
|
||||
})
|
||||
else
|
||||
for _, conditionBlock in ipairs(blocks) do
|
||||
-- `[]` is the always-true group; nothing to validate
|
||||
if safeTrim(conditionBlock) ~= "" then
|
||||
-- Check for invalid @ target syntax
|
||||
local _, _, target = safeStringFind(conditionBlock, "(@[^%s,]+)")
|
||||
if target and not safeStringFind(target, "^@[a-z]+%d*") then
|
||||
@@ -936,7 +945,9 @@ local function validateLine(line, lineNum)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if condEnd then
|
||||
-- Check for action after conditionals
|
||||
-- Extract the command from this action part
|
||||
local _, _, cmdFromAction = safeStringFind(actionPart, "^(/[a-z]+%d*)")
|
||||
@@ -1085,17 +1096,7 @@ function CleveRoids.ValidateAllMacros()
|
||||
local results = {}
|
||||
local totalErrors = 0
|
||||
|
||||
-- Account-wide macros are indexed from 1 up to GetNumMacros().
|
||||
-- Character-specific macros occupy the slots immediately following the account-wide ones.
|
||||
-- In Classic clients, the macro UI has 18 General (Account) slots and 18 Character-Specific slots.
|
||||
local numAccountMacros = GetNumMacros()
|
||||
|
||||
-- The WoW API GetMacroInfo(index) supports indexing up to 36 (1-18 for General, 19-36 for Character)
|
||||
-- in Classic clients, even though the total is GetNumMacros() + GetNumCharacterMacros() in Retail.
|
||||
-- To ensure we check all 36 possible slots:
|
||||
local totalSlots = 36
|
||||
|
||||
for i = 1, totalSlots do
|
||||
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
|
||||
local nameSuccess, name = pcall(GetMacroInfo, i)
|
||||
|
||||
-- Check if GetMacroInfo returned a name (i.e., the slot is used)
|
||||
|
||||
@@ -10,7 +10,7 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
|
||||
|-----|:--------:|---------|
|
||||
| [Nampower](https://github.com/brues-code/nampower/releases) (v3.0.0+) | ✅ | Spell queueing, DBC data, auto-attack events |
|
||||
| [UnitXP_SP3](https://codeberg.org/konaka/UnitXP_SP3/releases) | ✅ | Distance checks, `[multiscan]` enemy scanning |
|
||||
| [ClassicAPI](https://github.com/brues-code/ClassicAPI/releases) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed |
|
||||
| [ClassicAPI](https://github.com/brues-code/ClassicAPI/releases) (v1.15.8+) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed, unit-filtered events |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -27,6 +27,7 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
|
||||
- **Arguments** use colon: `[mod:alt]`, `[hp:>50]`
|
||||
- **Negation** with `no` prefix: `[nobuff]`, `[nomod:alt]`
|
||||
- **Target** with `@`: `[@mouseover,help]`, `[@party1,hp:<50]`
|
||||
- **Fallback groups**, Blizzard-style: `[@mouseover,help][@focus,help][] Rejuvenation` tries each `[...]` in order and the first that passes casts the shared spell; `[]` always passes. Mixes freely with `;`
|
||||
- **Spell names** with spaces: `"Mark of the Wild"` or `Mark_of_the_Wild`
|
||||
|
||||
**Multi-value logic:**
|
||||
|
||||
+413
-1534
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,188 @@
|
||||
# CC, Learned Immunity and Diminishing Returns
|
||||
|
||||
How SuperCleveRoidMacros classifies crowd control, what it learns about NPC
|
||||
immunity, and why diminishing returns only ever acts as a brake on that
|
||||
learning. Source citations are into `C:\Git\vmangos` and `C:\Git\nampower`.
|
||||
|
||||
## Three identities, never conflated
|
||||
|
||||
```text
|
||||
ccType Exact DBC mechanic. Backs [cc:*] and CC landing verification.
|
||||
Conditionals.lua, CleveRoids.CCMechanics
|
||||
|
||||
immunityType Exact DBC mechanic, recorded against an NPC name when a CC
|
||||
spell comes back IMMUNE.
|
||||
Utility.lua, MECHANIC_TO_IMMUNITY_TYPE / GetSpellImmunityType
|
||||
|
||||
DR type The real Vanilla diminishing-return pool. Internal only.
|
||||
Utility.lua, GetSpellImmunityDRType
|
||||
```
|
||||
|
||||
The first two are the same taxonomy used for different purposes; the third is
|
||||
a separate axis and is never exposed to macros. Folding any pair together is
|
||||
what produced the bugs this design replaces: a target that resisted Gouge got
|
||||
written down as stun-immune, and every later Kidney Shot was suppressed
|
||||
against a target that had never resisted one.
|
||||
|
||||
## `[cc:*]` follows the DBC mechanic exactly
|
||||
|
||||
One conditional, one mechanic. Several names may alias a single mechanic, but
|
||||
no name spans two:
|
||||
|
||||
```text
|
||||
[cc:stun] -> mechanic 12 only
|
||||
[cc:freeze] -> mechanic 13 only
|
||||
[cc:knockout] -> mechanic 14 only
|
||||
[cc:sap] -> mechanic 30 only
|
||||
[cc:daze] -> mechanic 27 only
|
||||
```
|
||||
|
||||
Bare `[cc]` and `[cc:any]` still mean "any loss-of-control effect" and are
|
||||
driven by `CleveRoids.CCTypesLossOfControl`, which is deliberately independent
|
||||
of the per-mechanic table. An aggregate does not require redefining the parts.
|
||||
|
||||
Current aliases:
|
||||
|
||||
```text
|
||||
slow -> snare
|
||||
disoriented -> disorient
|
||||
grip -> fumble (mechanic 6 is DBC Fumble)
|
||||
incap / incapacitate / incapacitated -> knockout (immunity input only)
|
||||
```
|
||||
|
||||
Aliases exist so published macros keep working. They resolve to the canonical
|
||||
mechanic name before anything is stored, so SavedVariables only ever hold
|
||||
canonical keys.
|
||||
|
||||
## Vanilla DBC mechanic taxonomy
|
||||
|
||||
The identities SCRM names. Where a mechanic is named at all, it is named as
|
||||
itself:
|
||||
|
||||
| DBC mechanic | ID | Canonical `ccType` |
|
||||
|---|---:|---|
|
||||
| Charm | 1 | `charm` |
|
||||
| Disoriented | 2 | `disorient` |
|
||||
| Disarm | 3 | `disarm` |
|
||||
| Distract | 4 | `distract` |
|
||||
| Fear | 5 | `fear` |
|
||||
| Fumble | 6 | `fumble` |
|
||||
| Root | 7 | `root` |
|
||||
| Pacify | 8 | `pacify` |
|
||||
| Silence | 9 | `silence` |
|
||||
| Sleep | 10 | `sleep` |
|
||||
| Snare | 11 | `snare` |
|
||||
| Stun | 12 | `stun` |
|
||||
| Freeze | 13 | `freeze` |
|
||||
| Knockout | 14 | `knockout` |
|
||||
| Bleed | 15 | `bleed` |
|
||||
| Polymorph | 17 | `polymorph` |
|
||||
| Banish | 18 | `banish` |
|
||||
| Shackle | 20 | `shackle` |
|
||||
| Turn | 23 | `turn` |
|
||||
| Horror | 24 | `horror` |
|
||||
| Interrupt | 26 | `interrupt` |
|
||||
| Daze | 27 | `daze` |
|
||||
| Sapped | 30 | `sap` |
|
||||
|
||||
Mechanics 16, 19, 21, 22, 25, 28 and 29 (Bandage, Shield, Mount, Persuade,
|
||||
Invulnerability, Discovery, Immune Shield) are real DBC values but are not
|
||||
crowd control, so they get no conditional.
|
||||
|
||||
## Diminishing returns
|
||||
|
||||
DR matters here for exactly one reason: a target at DR level 4 returns
|
||||
`IMMUNE`, and that must not be recorded as permanent immunity. Nothing else in
|
||||
the addon consumes DR state, and there is no player-facing DR feature.
|
||||
|
||||
### Only three pools can diminish a creature
|
||||
|
||||
`GetDiminishingReturnsGroupType` in
|
||||
`vmangos/src/game/Spells/SpellEntry.h:52-79` returns `DRTYPE_ALL` for exactly
|
||||
three groups:
|
||||
|
||||
```text
|
||||
DIMINISHING_CONTROL_STUN
|
||||
DIMINISHING_TRIGGER_STUN
|
||||
DIMINISHING_KIDNEYSHOT
|
||||
```
|
||||
|
||||
Every other staged group — sleep, both roots, fear, Warlock fear, charm,
|
||||
polymorph, silence, disarm, Death Coil, freeze, banish, knockout — is
|
||||
`DRTYPE_PLAYER`, and `Unit.cpp:7881` applies those only when the victim is a
|
||||
player. So on an NPC target they can never generate a DR immunity, and they
|
||||
must never hold back immunity learning.
|
||||
|
||||
This is why `GetSpellImmunityDRType` returns nil for everything that is not
|
||||
mechanic 12. It is not an approximation; it is the complete `DRTYPE_ALL` set.
|
||||
|
||||
### Classifying a stun into its pool
|
||||
|
||||
`SpellEntry::GetDiminishingReturnsGroup(bool triggered)`
|
||||
(`vmangos/src/game/Spells/SpellEntry.cpp:281-432`) resolves the pool, and the
|
||||
`triggered` argument means the runtime fact of whether an aura triggered the
|
||||
cast — this is not a pure DBC property. SCRM reconstructs it from nampower
|
||||
`SPELL_CAST_EVENT` correlation, which only fires for client-initiated casts:
|
||||
a spell ID with a recent `CleveRoids.pendingCasts` entry was cast deliberately,
|
||||
one without reached `SPELL_GO`/`SPELL_MISS` as a proc.
|
||||
|
||||
```text
|
||||
Kidney Shot -> stun_kidneyshot
|
||||
Charge / Intercept stun -> stun_control
|
||||
other mechanic-12, client cast -> stun_control
|
||||
other mechanic-12, proc -> stun_trigger
|
||||
```
|
||||
|
||||
Kidney Shot is matched on rogue family bit 21 (`0x00200000`,
|
||||
`SpellClassMask.h:228`) rather than a bare ID list, so custom ranks that keep
|
||||
their DBC family data still classify correctly. Charge (7922) and Intercept
|
||||
(20253, 20614, 20615) are internally triggered but explicitly returned as
|
||||
controlled stun by `SpellEntry.cpp:389-399`; they are the only such exceptions
|
||||
in 1.12.
|
||||
|
||||
### Pools that exist but do not apply
|
||||
|
||||
Recorded so nobody re-adds them to the safeguard. Knockout and Sap are
|
||||
distinct mechanics that share `DIMINISHING_KNOCKOUT`
|
||||
(`SpellEntry.cpp:424-425`), and Horror maps to `DIMINISHING_DEATHCOIL`
|
||||
(`SpellEntry.cpp:428-429`). Both are `DRTYPE_PLAYER`. `DIMINISHING_LIMITONLY`
|
||||
is a PvP duration cap rather than a staged pool, and `DIMINISHING_NONE` is not
|
||||
a pool at all.
|
||||
|
||||
### The safeguard itself
|
||||
|
||||
`recentCCHits[targetGUID][drType]` counts landed CC per DR pool, so the three
|
||||
stun pools keep separate histories. A hit more than `DR_RESET_WINDOW` (20s)
|
||||
after the previous one in that pool restarts the count at 1, matching the DR
|
||||
decay the skip check uses. Without that reset the counter only ever climbed,
|
||||
so three stuns on a target permanently disqualified it from ever teaching the
|
||||
addon anything again.
|
||||
|
||||
An `IMMUNE` result is treated as DR, and discarded, only while the pool holds
|
||||
three or more hits inside the window. Otherwise it is learned.
|
||||
|
||||
## nampower `SPELL_MISS` arguments
|
||||
|
||||
`TriggerSpellMissEvent` (`nampower/spellevents.cpp:888-917`) signals both
|
||||
`SPELL_MISS_SELF` and `SPELL_MISS_OTHER` with the same layout:
|
||||
|
||||
```text
|
||||
arg1 = casterGuid arg2 = targetGuid arg3 = spellId arg4 = missInfo
|
||||
```
|
||||
|
||||
`SPELL_MISS_SELF` carries `casterGuid` too, even though it is by definition
|
||||
the player — the event is chosen by comparing the caster to the active player
|
||||
GUID, not by changing the payload. Reading `arg1` as the spell ID hands a GUID
|
||||
string to the immunity recorder and silently loses every miss.
|
||||
|
||||
## Known limitation: mouseover casts
|
||||
|
||||
`ProcessSpellMissSelf` refuses to learn permanent immunity when it cannot
|
||||
resolve a queryable unit, because the temporary-immunity-buff check needs one
|
||||
— an NPC under Divine Shield must not be recorded as permanently immune. A CC
|
||||
cast at a mouseover that is not the current target can therefore return
|
||||
`IMMUNE` without being learned.
|
||||
|
||||
This is the safeguard working as designed, not a known bug. Do not loosen it
|
||||
without a reproduction showing a real failure, since the failure mode on the
|
||||
other side is permanent bad data in `CleveRoids_ImmunityData`.
|
||||
+57
-9
@@ -25,10 +25,23 @@ 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.
|
||||
- **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.
|
||||
- **~~Caveat: `expirationTime` is player-only~~ — NO LONGER TRUE.** This note said
|
||||
target/focus `expirationTime` was always `0`, so debuff *timers* had to stay on
|
||||
libdebuff. ClassicAPI has since added the `Aura::Source` cache: for a non-player
|
||||
unit `expirationTime` is reconstructed from the observed `SMSG_SPELL_GO`, and
|
||||
`duration` is the **caster-modified** value (talent extensions like Improved
|
||||
Shadow Word: Pain included). It also handles combo-point finisher scaling
|
||||
automatically (Rupture at 4 CP reads 14s, no registration) and ships Carnage's
|
||||
roll-gated Rip/Rake refresh in the DLL (`src/turtle/Carnage.cpp`, exposed as
|
||||
`RegisterAuraDurationModifierByTrigger`).
|
||||
- **Real remaining caveats** (all best-effort, from `docs/API.md`):
|
||||
- Only auras observed *after login* carry caster/timing; older ones report
|
||||
`expirationTime` 0 and `sourceUnit` nil.
|
||||
- Max-stack refresh is a blind spot: re-applying at max stacks (Shadow Weaving
|
||||
5→5) emits no client-visible change, so the entry elapses and evicts.
|
||||
- Out-of-range group members are spell-ID only, with `applications` always 1.
|
||||
- **Consequence:** most of libdebuff is now redundant — see
|
||||
"libdebuff retirement" below.
|
||||
|
||||
- **DONE (slice 1 — dispel-type conditionals):** added `ClassicAPI.lua`
|
||||
detection module (`CleveRoids.ClassicAPI`, mirrors NampowerAPI's
|
||||
@@ -150,11 +163,13 @@ Findings from the full Conditionals.lua audit. These look like ClassicAPI
|
||||
candidates but are **worse** than the current implementation — recorded so we
|
||||
don't re-investigate.
|
||||
|
||||
- **Auras / `ValidateAura`** — `C_UnitAuras.expirationTime` is **player-only**
|
||||
(`0` for every other unit). It cannot replace the nampower `GetUnitField`
|
||||
batch read or the remote-duration tracking (libdebuff / overflow slots). The
|
||||
dispel-*type* path already uses `C_UnitAuras` (that's the one thing it's good
|
||||
for); aura *timing/stacks* on non-player units must stay on nampower/libdebuff.
|
||||
- ~~**Auras / `ValidateAura`** — `C_UnitAuras.expirationTime` is player-only~~ —
|
||||
**STALE, do not trust this entry.** It was written before the `Aura::Source`
|
||||
cache landed. Non-player `expirationTime`/`duration` now work (caster-modified,
|
||||
talent extensions included), so this is no longer a reason to keep libdebuff's
|
||||
remote-duration tracking. See Tier 1 §1 and "libdebuff retirement" for the
|
||||
current picture and the real caveats. The nampower `GetUnitField` batch read is
|
||||
a separate question and has not been re-examined.
|
||||
- **`GetCurrentShapeshiftIndex` form loop / `[stance]`·`[form]`** —
|
||||
`GetShapeshiftFormID()` returns the **DBC form id** (Cat=1, Bear=5,
|
||||
Shadowform=28…), NOT the 1-based **bar index** these conditionals compare
|
||||
@@ -183,6 +198,39 @@ don't re-investigate.
|
||||
added `[focus]`/`[nofocus]`. `/focus` is provided by ClassicAPI's companion
|
||||
addon.
|
||||
|
||||
## libdebuff retirement
|
||||
|
||||
libdebuff exists because vanilla cannot report debuff durations on units other
|
||||
than the player. ClassicAPI's `Aura::Source` cache now does exactly that, so most
|
||||
of the library is redundant. This is a staged replacement, not a delete: measure
|
||||
first, then remove per group.
|
||||
|
||||
Current footprint: 28 public `lib:` methods, ~726 internal references in
|
||||
`Utility.lua`, consumers in 8 files (`Conditionals.lua` 45, `Compatibility/pfUI.lua`
|
||||
39, `Core.lua` 9). Note pfUI did **not** delete its own libdebuff — v9.0.25 still
|
||||
ships ~1695 lines of it, re-based on `C_UnitAuras.GetAuraDataByIndex` /
|
||||
`GetAuraDataBySpellName`. "Re-base on C_UnitAuras", not "remove", is the precedent.
|
||||
|
||||
**Group A — replaceable by `C_UnitAuras` (do these first):**
|
||||
`GetDuration`, `GetDebuffCaster`, `IsOurDebuff`, `UnitBuff`/`UnitDebuff`,
|
||||
`FindPlayerDebuff`/`FindPlayerBuff`, `GetAllDebuffsOnTarget`, `GetCachedIcon`,
|
||||
`ApplyCarnageRefresh`, and the Dark Harvest trio (`ApplyDarkHarvestStart`/`End`,
|
||||
`GetDarkHarvestReduction`, `GetTimeRemainingWithDarkHarvest`) — ClassicAPI ships
|
||||
Carnage refresh and Dark Harvest tick compression in the DLL.
|
||||
|
||||
**Group B — no `C_UnitAuras` equivalent, keep:**
|
||||
`ShouldApplyDebuffRank`, `DidSpellFail`, `WasSpellReflected`, `DidTargetEvade`,
|
||||
`ProcessMissReason`, `IsPersonalDebuff`, `GetSpellRank`/`GetSpellBaseName`,
|
||||
`HasPendingCast`. These are miss/rank/learning logic, not aura state.
|
||||
|
||||
**Gate before removing Group A:** confirm parity in-game on one spell where
|
||||
ClassicAPI does the hard part — Rip under Carnage. Compare
|
||||
`CleveRoids.libdebuff:GetDuration(spellID)` against
|
||||
`C_UnitAuras.GetUnitAuraBySpellID(unit, spellID).duration` and the derived
|
||||
remaining (`expirationTime - GetTime()`), on a target you have debuffed. If those
|
||||
agree across a Carnage proc, Group A can go. Watch the documented best-effort
|
||||
gaps: an aura cast before you logged in, and refresh-at-max-stacks.
|
||||
|
||||
## Marginal / optional follow-ups
|
||||
|
||||
- **`[swimming]`** → `IsSwimming()` would drop the nampower-2.36 version gate +
|
||||
|
||||
Reference in New Issue
Block a user