Optional ClassicAPI support

When ClassicAPI is installed and "Use ClassicAPI" is on (Mods tab, reload):
- Detect ClassicAPI and each feature separately; show it in the Mods tab
- Read auras with C_UnitAuras.GetAuraSlots (fill form) + UnitAuraBySlot,
  reusing entries, with ClassicAPI durations, expirations and casters
- UNIT_AURA_DELTA: refresh-only events update stacks and timers in place
  without rebuilding the aura icons or the health bar
- Aura timers without SuperWoW; native Cooldown frames with one shared
  text driver instead of a Model and OnUpdateModel per icon
- UNIT_IN_RANGE_UPDATE, UNIT_DISTANCE_CHECK_UPDATE, UNIT_CONNECTION and
  INCOMING_RESURRECT_CHANGED; UnitHasIncomingResurrection in res checks
- HealPredict learns heals from COMBAT_LOG_EVENT_UNFILTERED when the
  GUID formats match SuperWoW's
- Distance and line of sight when UnitXP SP3 and SuperWoW are missing

Without ClassicAPI, or with the option off, behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwwTK1hwTEWHqsan2mV
This commit is contained in:
2026-11-02 19:36:30 +00:00
co-authored by Claude Opus 5.5
parent f7ba1c934f
commit 0f803aca4d
11 changed files with 779 additions and 93 deletions
+234 -39
View File
@@ -13,6 +13,11 @@ local AllUnitsSet = util.AllUnitsSet
local superwow = util.IsSuperWowPresent()
local canGetAuraIDs = util.CanClientGetAuraIDs()
-- ClassicAPI aura reading, decided once options load (see ApplyClassicAPIOptions)
local classicAuras = false
local classicResurrection = false
local GetAuraSlots, UnitAuraBySlot
local compost = PTPool
-- Non-instance variable
@@ -22,7 +27,8 @@ PTUnit.Cached = {}
PTUnit.Unit = nil
PTUnit.AurasPopulated = false
-- Buff/debuff entry contents: {"name", "stacks", "texture", "index", "type", "id"(SuperWoW only)}
-- Buff/debuff entry contents: {"name", "stacks", "texture", "index", "type", "id"(SuperWoW/ClassicAPI only), "time"}
-- With ClassicAPI, "index" is the C_UnitAuras index and "filter" is "HELPFUL" or "HARMFUL" (used for tooltips)
PTUnit.Buffs = {} -- Array of all buffs
PTUnit.BuffsMap = {} -- Key: Name | Value: Array of buffs with key's name
PTUnit.BuffsIDSet = {} -- Set of currently applied buff IDs
@@ -46,6 +52,20 @@ PTUnit.Distance = 0
PTUnit.InSight = true
PTUnit.IsNew = false
function ApplyClassicAPIOptions()
classicAuras = util.IsClassicAPIActive("AuraSlots")
if classicAuras then
GetAuraSlots = C_UnitAuras.GetAuraSlots
UnitAuraBySlot = C_UnitAuras.UnitAuraBySlot
canGetAuraIDs = true -- Spell IDs come with every aura
end
classicResurrection = util.IsClassicAPIActive("Resurrection")
end
function UsesClassicAuras()
return classicAuras
end
-- Non-GUID function
function CreateCaches()
if superwow then
@@ -203,24 +223,35 @@ function PTUnit:IsInSight()
return self.InSight
end
-- ClassicAPI sees resurrections from any nearby caster, including players without HealComm
local function hasClassicResurrection(unit)
return classicResurrection and UnitHasIncomingResurrection(unit) == true
end
function PTUnit:IsBeingResurrected()
if not self.Unit then
return false
end
if PTHealPredict then
return PTHealPredict.IsBeingResurrected(self.Unit)
return PTHealPredict.IsBeingResurrected(self.Unit) or hasClassicResurrection(self.Unit)
end
return Puppeteer.HealComm:UnitisResurrecting(UnitName(self.Unit))
return Puppeteer.HealComm:UnitisResurrecting(UnitName(self.Unit)) or hasClassicResurrection(self.Unit)
end
function PTUnit:GetResurrectionCasts()
if not self.Unit then
return 0
end
local count
if PTHealPredict then
return PTHealPredict.GetResurrectionCount(self.Unit)
count = PTHealPredict.GetResurrectionCount(self.Unit)
else
count = Puppeteer.HealComm:UnitisResurrecting(UnitName(self.Unit)) and 1 or 0
end
return Puppeteer.HealComm:UnitisResurrecting(UnitName(self.Unit)) and 1 or 0
if count == 0 and hasClassicResurrection(self.Unit) then
count = 1
end
return count
end
-- Aura storage is reused across rescans so that UNIT_AURA produces no garbage:
@@ -310,6 +341,187 @@ function PTUnit:ClearAuras()
end
local claimedAuras = {}
-- The SuperWoW/Nampower aura time with the most time left for this spell ID that no other aura claimed yet
local function FindTrackedTime(self, id, time)
local auraTimes = id and self.AuraTimes[id]
if not auraTimes then
return nil
end
local auraTime = nil
local highestDuration = -100
for owner, aura in pairs(auraTimes) do
local timeLeft = aura.endTime - time
if not claimedAuras[aura] and timeLeft > highestDuration then
auraTime = aura
highestDuration = timeLeft
end
end
if auraTime then
claimedAuras[auraTime] = 1
end
return auraTime
end
-- ClassicAPI aura reading. One slot enumeration per range, then a positional read per aura,
-- so a scan creates no tables after each entry's first use.
local slotBuffer = {}
-- Fills the entry's own time table from ClassicAPI's duration and expiration. Returns nil when unknown.
local function ApplyClassicTime(entry, duration, expirationTime, source, now)
if not expirationTime or expirationTime <= 0 or not duration or duration <= 0 then
return nil
end
local startTime = expirationTime - duration
if startTime > now then -- A talent-extended aura must not start in the future
startTime = now
duration = expirationTime - now
end
if duration <= 0 then
return nil
end
local auraTime = entry.classicTime
if not auraTime then
auraTime = {}
entry.classicTime = auraTime
end
auraTime.startTime = startTime
auraTime.endTime = expirationTime
auraTime.duration = duration
auraTime.owner = source -- A unit token, or nil when the cast was not seen
auraTime.ownerName = source and UnitName(source) or nil
return auraTime
end
function PTUnit:ScanClassicAuras(unit, time)
local buffs = self.Buffs
local buffsMap = self.BuffsMap
local buffsIDSet = self.BuffsIDSet
local buffEntries = self.BuffEntries
local trackedHealingBuffs = PuppeteerSettings.TrackedHealingBuffs
local _, count = GetAuraSlots(unit, "HELPFUL", nil, nil, slotBuffer)
for index = 1, count or 0 do
local name, texture, stacks, type, duration, expirationTime, source, _, _, id =
UnitAuraBySlot(unit, slotBuffer[index])
if name then
if trackedHealingBuffs[name] then
self.HasHealingModifier = true
end
local buff = GetEntry(buffEntries, getn(buffs) + 1)
buff.name = name
buff.index = index
buff.filter = "HELPFUL"
buff.texture = texture
buff.stacks = stacks or 0
buff.type = type or ""
buff.id = id
buff.time = ApplyClassicTime(buff, duration, expirationTime, source, time) or FindTrackedTime(self, id, time)
local nameArray = buffsMap[name]
if not nameArray then
nameArray = GetArray()
buffsMap[name] = nameArray
end
if id ~= nil then
buffsIDSet[id] = true
end
table.insert(nameArray, buff)
table.insert(buffs, buff)
end
end
local afflictedDebuffTypes = self.AfflictedDebuffTypes
local trackedDebuffTypes = self.TrackedDebuffTypes
local debuffs = self.Debuffs
local debuffsMap = self.DebuffsMap
local debuffsIDSet = self.DebuffsIDSet
local typedDebuffs = self.TypedDebuffs -- Dispellable debuffs
local debuffEntries = self.DebuffEntries
local trackedHealingDebuffs = PuppeteerSettings.TrackedHealingDebuffs
local importantDebuffs = PuppeteerSettings.ImportantDebuffs
local ignoredDispellableDebuffs = PuppeteerSettings.IgnoredDispellableDebuffs
-- HARMFUL also returns debuffs the server moved into free buff slots, which UnitDebuff misses
_, count = GetAuraSlots(unit, "HARMFUL", nil, nil, slotBuffer)
for index = 1, count or 0 do
local name, texture, stacks, type, duration, expirationTime, source, _, _, id =
UnitAuraBySlot(unit, slotBuffer[index])
if name then
type = type or ""
if trackedHealingDebuffs[name] then
self.HasHealingModifier = true
end
if importantDebuffs[name] then
self.HasImportantDebuff = true
end
local debuff = GetEntry(debuffEntries, getn(debuffs) + 1)
debuff.name = name
debuff.index = index
debuff.filter = "HARMFUL"
debuff.texture = texture
debuff.stacks = stacks or 0
debuff.type = type
debuff.id = id
debuff.time = ApplyClassicTime(debuff, duration, expirationTime, source, time) or FindTrackedTime(self, id, time)
local nameArray = debuffsMap[name]
if not nameArray then
nameArray = GetArray()
debuffsMap[name] = nameArray
end
if id ~= nil then
debuffsIDSet[id] = true
end
table.insert(nameArray, debuff)
if type ~= "" then
afflictedDebuffTypes[type] = 1
if not ignoredDispellableDebuffs[name] then
trackedDebuffTypes[type] = 1
end
local typeArray = typedDebuffs[type]
if not typeArray then
typeArray = GetArray()
typedDebuffs[type] = typeArray
end
table.insert(typeArray, debuff)
end
table.insert(debuffs, debuff)
end
end
end
-- Re-reads stacks and timers of the scanned auras in place. Returns false if the aura list itself
-- changed, in which case a full UpdateAuras is needed.
local function RefreshClassicList(unit, filter, list, now)
local _, count = GetAuraSlots(unit, filter, nil, nil, slotBuffer)
count = count or 0
if count ~= getn(list) then
return false
end
for index = 1, count do
local name, _, stacks, _, duration, expirationTime, source, _, _, id = UnitAuraBySlot(unit, slotBuffer[index])
local entry = list[index]
if not name or entry.id ~= id or entry.index ~= index then
return false
end
entry.stacks = stacks or 0
local classicTime = ApplyClassicTime(entry, duration, expirationTime, source, now)
if classicTime then
entry.time = classicTime
elseif entry.time == entry.classicTime then
entry.time = nil
end
end
return true
end
-- For UNIT_AURA_DELTA events that only refreshed auras (new duration or stack count)
function PTUnit:RefreshClassicAuras()
local unit = self.Unit
if not classicAuras or not unit or self == PTUnit or not self.AurasPopulated then
return false
end
local now = GetTime()
return RefreshClassicList(unit, "HELPFUL", self.Buffs, now) and RefreshClassicList(unit, "HARMFUL", self.Debuffs, now)
end
function PTUnit:UpdateAuras()
local unit = self.Unit
@@ -326,11 +538,20 @@ function PTUnit:UpdateAuras()
return
end
local time = GetTime()
if classicAuras then
self:ScanClassicAuras(unit, time)
self.AurasPopulated = true
util.ClearTable(claimedAuras)
Puppeteer.EndTiming("PTUnitAuraUpdate")
return
end
local buffs = self.Buffs
local buffsMap = self.BuffsMap
local buffsIDSet = self.BuffsIDSet
local buffEntries = self.BuffEntries
local time = GetTime()
for index = 1, 32 do
local texture, stacks, id = UnitBuff(unit, index)
if not texture then
@@ -340,21 +561,7 @@ function PTUnit:UpdateAuras()
if PuppeteerSettings.TrackedHealingBuffs[name] then
self.HasHealingModifier = true
end
local auraTime = nil
if id and self.AuraTimes[id] then
local auraTimes = self.AuraTimes[id]
local highestDuration = -100
for owner, aura in pairs(auraTimes) do
local timeLeft = aura.endTime - time
if not claimedAuras[aura] and timeLeft > highestDuration then
auraTime = aura
highestDuration = timeLeft
end
end
if auraTime then
claimedAuras[auraTime] = 1
end
end
local auraTime = FindTrackedTime(self, id, time)
local buff = GetEntry(buffEntries, index)
buff.name = name
buff.index = index
@@ -395,21 +602,7 @@ function PTUnit:UpdateAuras()
if PuppeteerSettings.ImportantDebuffs[name] then
self.HasImportantDebuff = true
end
local auraTime = nil
if id and self.AuraTimes[id] then
local auraTimes = self.AuraTimes[id]
local highestDuration = -100
for owner, aura in pairs(auraTimes) do
local timeLeft = aura.endTime - time
if not claimedAuras[aura] and timeLeft > highestDuration then
auraTime = aura
highestDuration = timeLeft
end
end
if auraTime then
claimedAuras[auraTime] = 1
end
end
local auraTime = FindTrackedTime(self, id, time)
local debuff = GetEntry(debuffEntries, index)
debuff.name = name
debuff.index = index
@@ -542,10 +735,12 @@ function PTUnit:ApplyTimedAura(spellName, spellID, owner, duration, isNampower)
end
function PTUnit:GetAuraTimeRemaining(name)
if not superwow then
return
local auraTime = superwow and self.AuraTimesByName[name] or nil
if not auraTime and classicAuras then -- ClassicAPI times are kept on the aura entries
local array = self.BuffsMap[name] or self.DebuffsMap[name]
local entry = array and array[1]
auraTime = entry and entry.time
end
local auraTime = self.AuraTimesByName[name]
if not auraTime then
return
end