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
+166 -36
View File
@@ -223,12 +223,13 @@ function PTUnitFrame:UpdateSight()
end
end
local preciseDistance = util.CanClientGetPreciseDistance()
local CanClientGetPreciseDistance = util.CanClientGetPreciseDistance -- Read on use: ClassicAPI may provide it once options load
local RANGE_PREFIX_FAR = GetColorPrefix(0.75, 0.75, 0.75)
local RANGE_PREFIX_OUT = GetColorPrefix(1, 0.3, 0.3)
local RANGE_PREFIX_NEAR = GetColorPrefix(1, 0.6, 0)
function PTUnitFrame:UpdateRangeText()
local dist = ceil(self.distance)
local preciseDistance = CanClientGetPreciseDistance()
local prefix
if dist >= (preciseDistance and self:GetShowDistanceThreshold() or 28) and dist < 9999 then
if dist > 80 then
@@ -855,7 +856,7 @@ local AURA_DURATION_TEXT_FLASH_THRESHOLD = 5
local AURA_DURATION_TEXT_LOW_THRESHOLD = 30
-- A map of all seconds below the flash threshold to an array of colors to interpolate
local durationTextFlashColorsRange
if util.IsSuperWowPresent() then
do -- Built for every client, since ClassicAPI can enable timers after this file loads
local flashColorsReset = {{1, 1, 0.75}, {1, 0.7, 0.55}, {1, 0.6, 0.6}}
local flashColors = {{1, 1, 0.25}, {1, 0.6, 0.35}, {1, 0.4, 0.4}}
@@ -872,6 +873,96 @@ if util.IsSuperWowPresent() then
durationTextFlashColorsRange[seconds] = {colors[2], colors[1]}
end
end
-- Whole-second strings for the duration text, so a ticking timer doesn't convert a number every second
local secondsTexts = {}
for i = 0, 60 do
secondsTexts[i] = tostring(i)
end
-- Aura timers drawn by ClassicAPI's native Cooldown frames. The sweep animates in the client; only the
-- duration text is updated here, by one shared driver instead of a script on every icon.
local nativeCooldownsUsable -- nil until the first try
local function CreateNativeCooldown(parent)
if nativeCooldownsUsable == false or not util.IsClassicAPIActive("Cooldown") then
return nil
end
local ok, cooldown = pcall(CreateFrame, "Cooldown", nil, parent, "CooldownFrameTemplate")
if not ok or not cooldown or not cooldown.SetCooldown or not cooldown.SetDrawBling
or not cooldown.SetHideCountdownNumbers then
nativeCooldownsUsable = false
if ok and cooldown then
cooldown:Hide()
end
return nil
end
nativeCooldownsUsable = true
cooldown.native = true
cooldown:SetDrawBling(false) -- No flash each time an aura runs out
cooldown:SetHideCountdownNumbers(true) -- Puppeteer draws its own text
return cooldown
end
local activeAuraTimers = {} -- Key: aura component with a native cooldown | Value: true
local auraTimerDriver = CreateFrame("Frame", "PTAuraTimerDriver")
local function UpdateNativeTimerText(component, now)
local duration = component.duration
local durationText = component.durationText
local secondsPrecise = duration.timerEnd - now
if secondsPrecise <= 0 then
durationText:SetSeconds(nil)
activeAuraTimers[component] = nil
return
end
local seconds = floor(secondsPrecise)
if seconds <= (duration.displayAt or AURA_DURATION_TEXT_FLASH_THRESHOLD) then
local changed = durationText.seconds ~= seconds
if changed then
durationText:SetSeconds(seconds)
end
if seconds <= AURA_DURATION_TEXT_FLASH_THRESHOLD then
durationText:SetTextColor(
util.InterpolateColorsNoTable(durationTextFlashColorsRange[seconds], secondsPrecise - seconds))
elseif changed then
if seconds <= AURA_DURATION_TEXT_LOW_THRESHOLD then
durationText:SetTextColor(1, 1, 0.25)
else
durationText:SetTextColor(1, 1, 1)
end
end
elseif durationText.seconds ~= nil then
durationText:SetSeconds(nil)
end
end
local function AuraTimerDriver_OnUpdate()
local now = GetTime()
for component in pairs(activeAuraTimers) do
if component.frame:IsVisible() then
UpdateNativeTimerText(component, now)
end
end
if next(activeAuraTimers) == nil then
auraTimerDriver:SetScript("OnUpdate", nil)
end
end
local function StartNativeTimer(component)
activeAuraTimers[component] = true
if not auraTimerDriver:GetScript("OnUpdate") then
auraTimerDriver:SetScript("OnUpdate", AuraTimerDriver_OnUpdate)
end
UpdateNativeTimerText(component, GetTime())
end
-- Hides an aura icon's timer sweep and text
local function ClearAuraTimer(component)
activeAuraTimers[component] = nil
component.durationText:SetSeconds(nil)
CooldownFrame_SetTimer(component.duration, 0, 0, 0)
component.hasTimer = nil
end
-- "Nm" duration strings, cached so they aren't rebuilt every time a timer ticks
local minutesTexts = {}
function GetMinutesText(minutes)
@@ -896,9 +987,9 @@ function PTUnitFrame:AllocateAura()
local stackText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
stackText:SetTextColor(1, 1, 1)
-- Duration display, only used when SuperWoW is present
if util.IsSuperWowPresent() then
local duration = CreateFrame("Model", nil, frame, "CooldownFrameTemplate")
-- Duration display, only used when SuperWoW or ClassicAPI provides aura times
if util.HasAuraTimers() then
local duration = CreateNativeCooldown(frame) or CreateFrame("Model", nil, frame, "CooldownFrameTemplate")
duration.noCooldownCount = true
duration:SetAlpha(0.8)
local durationOverlayFrame = CreateFrame("Frame", nil, frame)
@@ -911,7 +1002,7 @@ function PTUnitFrame:AllocateAura()
self:SetText("")
return
end
self:SetText(seconds <= 60 and seconds or GetMinutesText(ceil(seconds / 60)))
self:SetText(seconds <= 60 and secondsTexts[seconds] or GetMinutesText(ceil(seconds / 60)))
local fontSize = ceil(frame:GetHeight() * (seconds < 540 and (seconds < 10 and 0.6 or 0.45) or 0.35))
if fontSize ~= self.fontSize then
self.fontSize = fontSize
@@ -933,6 +1024,10 @@ function PTUnitFrame:AllocateAura()
end
duration:SetScript("OnUpdate", nil)
end
if duration.native then
return {["frame"] = frame, ["icon"] = icon, ["border"] = border, ["stackText"] = stackText,
["overlay"] = durationOverlayFrame, ["durationText"] = durationText, ["duration"] = duration}
end
local SetSequenceTime = duration.SetSequenceTime
local GetTime = GetTime
duration:SetScript("OnUpdateModel", function()
@@ -1075,9 +1170,8 @@ function PTUnitFrame:ReleaseAuras()
table.insert(self.auraButtonPool, button)
end
if util.IsSuperWowPresent() then
aura.durationText:SetSeconds(nil)
CooldownFrame_SetTimer(aura.duration, 0, 0, 0)
if aura.duration then
ClearAuraTimer(aura)
end
table.insert(self.auraIconPool, aura)
@@ -1108,10 +1202,13 @@ local enemyDebuffSorter = function(a, b)
if not a.time and not b.time then
return a.name > b.name
end
if UnitIsUnit(a.time.owner, "player") and not UnitIsUnit(b.time.owner, "player") then
-- ClassicAPI times have no owner when the cast wasn't seen
local aMine = a.time.owner ~= nil and UnitIsUnit(a.time.owner, "player")
local bMine = b.time.owner ~= nil and UnitIsUnit(b.time.owner, "player")
if aMine and not bMine then
return true
end
if not UnitIsUnit(a.time.owner, "player") and UnitIsUnit(b.time.owner, "player") then
if not aMine and bMine then
return false
end
return (a.time.startTime + a.time.duration) < (b.time.startTime + b.time.duration)
@@ -1177,7 +1274,7 @@ function PTUnitFrame:UpdateAuras()
if not enemy then
table.sort(debuffs, self.DebuffSorter)
util.AppendArrayElements(debuffs, typedDebuffs)
elseif util.IsSuperWowPresent() then
elseif util.HasAuraTimers() then
table.sort(debuffs, enemyDebuffSorter)
end
@@ -1224,7 +1321,10 @@ function PTUnitFrame:ApplyAuraTooltip(auraFrame)
tooltip:SetOwner(auraFrame, "ANCHOR_BOTTOMLEFT")
local unit = self:GetResolvedUnit()
if auraData then
if type == "Buff" then
if auraData.filter and tooltip.SetUnitAura then
-- ClassicAPI auras are indexed in C_UnitAuras order, read from the cache's unit
tooltip:SetUnitAura(cache.Unit or unit, index, auraData.filter)
elseif type == "Buff" then
tooltip:SetUnitBuff(unit, index)
else
tooltip:SetUnitDebuff(unit, index)
@@ -1298,6 +1398,11 @@ function PTUnitFrame:CreateAura(component, aura, xOffset, yOffset, type, size)
frame.auraData = aura
frame.auraIndex = aura.index
frame.auraType = type
-- Kept so RefreshAuras can update the icon in place
component.xOffset = xOffset
component.yOffset = yOffset
component.auraKind = type
component.size = size
local icon = component.icon
icon:SetAllPoints(frame)
@@ -1306,24 +1411,30 @@ function PTUnitFrame:CreateAura(component, aura, xOffset, yOffset, type, size)
local button = component.button
button:SetAllPoints(frame)
if util.IsSuperWowPresent() then
local durationUI = component.duration
if durationUI then
local overlay = component.overlay
overlay:SetAllPoints()
local duration = component.duration
duration:SetAllPoints()
duration:SetScale(size * 0.0275)
durationUI:SetAllPoints()
if not durationUI.native then
durationUI:SetScale(size * 0.0275)
end
end
local stackText = component.stackText
if stacks > 1 then
local stackText = component.stackText
stackText:SetPoint("CENTER", frame, "CENTER", 0, 0)
stackText:SetFont("Fonts\\FRIZQT__.TTF", math.ceil(size * (stacks < 10 and 0.75 or 0.6)))
stackText:SetText(stacks)
else
stackText:SetText("")
end
-- ClassicAPI times have no owner when the cast wasn't seen
local castByPlayer = aura.time ~= nil and aura.time.owner ~= nil and UnitIsUnit(aura.time.owner, "player")
if type == "Buff" then
if selfBorderedBuffs[aura.name] and aura.time and UnitIsUnit(aura.time.owner, "player") then
if selfBorderedBuffs[aura.name] and castByPlayer then
local border = component.border
border:Show()
border:SetVertexColor(1, 1, 0)
@@ -1334,34 +1445,53 @@ function PTUnitFrame:CreateAura(component, aura, xOffset, yOffset, type, size)
local border = component.border
border:Show()
local color = debuffTypeBorderColors[aura.type] or debuffTypeBorderColors["Other"]
if aura.time and UnitIsUnit(aura.time.owner, "player") and self:IsEnemy() then
if castByPlayer and self:IsEnemy() then
border:SetVertexColor(1, 1, 0)
else
border:SetVertexColor(color[1], color[2], color[3])
end
end
if aura.time then
local startTime = aura.time.startTime
local endTime = aura.time.endTime
local duration = aura.time.duration
if endTime - GetTime() > -4 then -- Don't display duration if the predicted time has lapsed
local durationUI = component.duration
if not durationUI then
return
end
local auraTime = aura.time
if auraTime and auraTime.endTime - GetTime() > -4 then -- Don't display duration if the predicted time has lapsed
local startTime = auraTime.startTime
local duration = auraTime.duration
CooldownFrame_SetTimer(durationUI, startTime, duration, 1)
CooldownFrame_SetTimer(durationUI, startTime, duration, 1)
component.hasTimer = true
if duration < 60 then
durationUI.displayAt = PTOptions.ShowAuraTimesAt.Short
elseif duration <= 60 * 2 then
durationUI.displayAt = PTOptions.ShowAuraTimesAt.Medium
else
durationUI.displayAt = PTOptions.ShowAuraTimesAt.Long
end
if duration < 60 then
durationUI.displayAt = PTOptions.ShowAuraTimesAt.Short
elseif duration <= 60 * 2 then
durationUI.displayAt = PTOptions.ShowAuraTimesAt.Medium
else
durationUI.displayAt = PTOptions.ShowAuraTimesAt.Long
end
-- To prevent having a frame where the duration is not updated
component.durationText:SetSeconds(nil)
-- To prevent having a frame where the duration is not updated
component.durationText:SetSeconds(nil)
if durationUI.native then
durationUI.timerEnd = startTime + duration
StartNativeTimer(component)
else
util.CallWithThis(durationUI, durationUI:GetScript("OnUpdateModel"))
end
elseif component.hasTimer then -- A refreshed aura that lost its time
ClearAuraTimer(component)
end
end
-- Updates the shown aura icons in place, for auras that were only refreshed (new duration or stack count).
-- The aura list itself must be unchanged; otherwise use UpdateAuras.
function PTUnitFrame:RefreshAuras()
for _, component in ipairs(self.auraIcons) do
local aura = component.frame.auraData
if aura and component.size then
self:CreateAura(component, aura, component.xOffset, component.yOffset, component.auraKind, component.size)
end
end
end