6 Commits
Author SHA1 Message Date
Brues 08deab469f Read friend and who class tokens from C_FriendList
Use C_FriendList.GetFriendInfoByIndex and GetWhoInfo in the social
lists, the friends panel tooltip, and the unit scan. Both return the
class token directly (classFilename / filename), so the localized class
name no longer needs a reversal through L["class"]. Color friend names
with the class ColorMixin's WrapTextInColorCode.
2026-08-08 01:21:46 -05:00
Brues 30fa6dc9de Count online friends with C_FriendList.GetNumOnlineFriends
Replace the manual loop over GetFriendInfo with a single
C_FriendList.GetNumOnlineFriends call in the friends panel widget.
2026-08-08 00:10:01 -05:00
Brues a886eb55cb Add a friend notes module
Store client-side notes for friends through ClassicAPI's C_FriendList
note API. Add an Edit Note entry to the friend right-click menu, which
opens a dialog that reads and writes the note. Show the note in a
tooltip when the cursor is over a friend in the list.
2026-08-07 23:58:53 -05:00
Brues f263507d92 Bump ClassicAPI minimum version to 10905 2026-08-07 23:58:53 -05:00
Brues 1e6b4d927a Read spell fields through ClassicAPI instead of nampower
Replace GetSpellRecField (nampower) with the C_Spell field readers.

libdebuff reads the dispel type with C_Spell.GetSpellDispelType.

swingtimer detects on-next-swing spells with C_Spell.IsNextMeleeSpell,
and the swing-reset rule with C_Spell.ResetsMeleeSwing. The on-next-swing
check now covers both attribute bits, and the freeze check folds in the
keep-swings attribute, both matching the server rule.

Note: needs the ClassicAPI release that adds these functions. Bump
PFUI_CLASSIC_API_MIN when that version is out.
2026-08-07 23:58:53 -05:00
Brues cd08db75d2 Color button highlights from PFUI_CLASS_COLORS
Read the player class color from PFUI_CLASS_COLORS instead of
GetClassColor in SkinButton, SetHighlight, and SkinDropDown.
GetClassColor returns the stock pink for Shaman, but pfUI remaps it to
blue, so the button highlights and locked borders now match.
2026-08-07 23:52:22 -05:00
9 changed files with 183 additions and 127 deletions
+4 -4
View File
@@ -587,7 +587,7 @@ end
function pfUI.api.SetHighlight(frame, cr, cg, cb)
if not frame then return end
if not cr or not cg or not cb then
cr, cg, cb = GetClassColor(UnitClassBase('player'))
cr, cg, cb = PFUI_CLASS_COLORS[UnitClassBase('player')]:GetRGB()
end
frame.cr, frame.cg, frame.cb = cr, cg, cb, ca
@@ -632,7 +632,7 @@ function pfUI.api.SkinButton(button, cr, cg, cb, icon, disableHighlight)
if not b then b = button end
if not b then return end
if not cr or not cg or not cb then
cr, cg, cb = GetClassColor(UnitClassBase('player'))
cr, cg, cb = PFUI_CLASS_COLORS[UnitClassBase('player')]:GetRGB()
end
pfUI.api.CreateBackdrop(b, nil, true)
b:SetNormalTexture("")
@@ -709,7 +709,7 @@ end
function pfUI.api.SkinRotateButton(button)
pfUI.api.CreateBackdrop(button)
local cr, cg, cb = GetClassColor(UnitClassBase('player'))
local cr, cg, cb = PFUI_CLASS_COLORS[UnitClassBase('player')]:GetRGB()
local btnW, btnH = button:GetSize()
button:SetSize(btnW - 18, btnH - 18)
@@ -907,7 +907,7 @@ function pfUI.api.SkinDropDown(frame, cr, cg, cb, useSmall)
end
if not cr or not cg or not cb then
cr, cg, cb = GetClassColor(UnitClassBase('player'))
cr, cg, cb = PFUI_CLASS_COLORS[UnitClassBase('player')]:GetRGB()
end
SetHighlight(button, cr, cg, cb)
+1
View File
@@ -81,4 +81,5 @@
<Include file="..\modules\equipmentmanager.lua"/>
<Include file="..\modules\loothistory.lua"/>
<Include file="..\modules\newitem.lua"/>
<Include file="..\modules\friendnotes.lua"/>
</Ui>
+3 -5
View File
@@ -376,11 +376,9 @@ local function GetDebuffSlotMap(guid)
local texture = libdebuff:GetSpellIcon(spellId)
local stacks = (auraApps and auraApps[auraSlot] or 0) + 1
local dtype = nil
if GetSpellRecField then
local dispelId = GetSpellRecField(spellId, "dispel")
if dispelId and dispelId > 0 then
dtype = dispelTypeMap[dispelId]
end
local dispelId = C_Spell.GetSpellDispelType(spellId)
if dispelId and dispelId > 0 then
dtype = dispelTypeMap[dispelId]
end
map[displaySlot] = {
auraSlot = auraSlot,
+10 -11
View File
@@ -96,13 +96,12 @@ libunitscan:SetScript("OnEvent", function()
RememberByUnit("player", name, class)
elseif event == "FRIENDLIST_UPDATE" then
local name, class, level
for i = 1, GetNumFriends() do
name, level, class = GetFriendInfo(i)
class = L["class"][class] or nil
-- friendlist updates due to friend going off-line return level 0, let's not overwrite good older values
level = level > 0 and level or nil
AddData("players", name, class, level)
local info = C_FriendList.GetFriendInfoByIndex(i)
if info then
local level = info.level > 0 and info.level or nil
AddData("players", info.name, info.classFilename, level)
end
end
elseif event == "GUILD_ROSTER_UPDATE" then
@@ -137,11 +136,11 @@ libunitscan:SetScript("OnEvent", function()
end
elseif event == "WHO_LIST_UPDATE" or event == "CHAT_MSG_SYSTEM" then
local name, class, level, guild, _
for i = 1, GetNumWhoResults() do
name, guild, level, _, class, _ = GetWhoInfo(i)
class = L["class"][class] or nil
AddData("players", name, class, level, nil, guild)
for i = 1, C_FriendList.GetNumWhoResults() do
local info = C_FriendList.GetWhoInfo(i)
if info then
AddData("players", info.fullName, info.filename, info.level, nil, info.fullGuildName)
end
end
elseif event == "UPDATE_MOUSEOVER_UNIT" or event == "PLAYER_TARGET_CHANGED" or event == "NAME_PLATE_UNIT_ADDED" then
+79
View File
@@ -0,0 +1,79 @@
-- Friend notes
-- Client-side notes for friends via ClassicAPI's C_FriendList note API
-- (SetFriendNotes / GetFriendInfo(.notes)). Adds an "Edit Note" entry to the
-- friend right-click menu (the "FRIEND" UnitPopup) and shows the note in a
-- tooltip when you hover a friend in the list.
pfUI:RegisterModule("friendnotes", function ()
local EDIT_TOKEN = "PFUI_FRIEND_NOTE"
-- Note editor. In 1.12 StaticPopup, OnAccept runs with `this` = the OK
-- button (the dialog is this:GetParent()); the friend name arrives as the
-- data argument passed to StaticPopup_Show.
StaticPopupDialogs["PFUI_FRIEND_NOTE_EDIT"] = {
text = SET_FRIENDNOTE_LABEL,
button1 = SAVE,
button2 = CANCEL,
hasEditBox = 1,
maxLetters = 128,
OnAccept = function(name)
C_FriendList.SetFriendNotes(name, _G[this:GetParent():GetName().."EditBox"]:GetText())
end,
EditBoxOnEnterPressed = function(name)
C_FriendList.SetFriendNotes(name, this:GetText())
this:GetParent():Hide()
end,
EditBoxOnEscapePressed = function() this:GetParent():Hide() end,
timeout = 0, whileDead = 1, hideOnEscape = 1,
}
local function OpenNoteEditor(name)
if not name or name == "" then return end
local info = C_FriendList.GetFriendInfo(name)
local dialog = StaticPopup_Show("PFUI_FRIEND_NOTE_EDIT", name, nil, name)
if not dialog then return end
local editbox = _G[dialog:GetName().."EditBox"]
editbox:SetText((info and info.notes) or "")
editbox:HighlightText()
editbox:SetFocus()
end
-- Add "Edit Note" to the friend right-click menu, just before Cancel.
UnitPopupButtons[EDIT_TOKEN] = { text = SET_NOTE, dist = 0 }
local friendMenu = UnitPopupMenus["FRIEND"]
table.insert(friendMenu, table.getn(friendMenu), EDIT_TOKEN)
-- Route our entry to the editor and close the menu; delegate the rest. A
-- bare assignment lands on pfUI.env, so set the global explicitly.
local UnitPopup_OnClick_orig = UnitPopup_OnClick
_G.UnitPopup_OnClick = function()
if this and this.value == EDIT_TOKEN then
local dropdown = _G[UIDROPDOWNMENU_INIT_MENU]
local name = dropdown and dropdown.name
CloseDropDownMenus()
OpenNoteEditor(name)
return
end
return UnitPopup_OnClick_orig()
end
-- Show the note in a tooltip while hovering a friend in the list. Friend
-- buttons carry their friend index via SetID (see FriendsFrame_Update).
local function FriendButton_OnEnter()
local index = this:GetID()
if not index or index < 1 then return end
local info = C_FriendList.GetFriendInfoByIndex(index)
if not info or not info.notes or info.notes == "" then return end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(info.name or "")
GameTooltip:AddLine(info.notes, 1, 1, 1, 1)
GameTooltip:Show()
end
for i = 1, FRIENDS_TO_DISPLAY do
local button = _G["FriendsFrameFriendButton"..i]
if button then
button:HookScript("OnEnter", FriendButton_OnEnter)
button:HookScript("OnLeave", GameTooltip_Hide)
end
end
end)
+7 -15
View File
@@ -296,18 +296,18 @@ pfUI:RegisterModule("panel", function()
local playerzone = GetRealZoneText()
for friendIndex=1, all do
local friend_name, friend_level, friend_class, friend_area, friend_connected = GetFriendInfo(friendIndex)
if friend_connected and friend_class and friend_level then
local info = C_FriendList.GetFriendInfoByIndex(friendIndex)
if info and info.connected and info.classFilename and info.level then
if not init then
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:ClearLines()
GameTooltip:AddLine("|cff555555" .. T["Friends Online"])
init = true
end
local ccolor = PFUI_CLASS_COLORS[L["class"][friend_class]] or { 1, 1, 1 }
local lcolor = GetDifficultyColor(tonumber(friend_level)) or { 1, 1, 1 }
local zcolor = friend_area == playerzone and "|cff33ffcc" or "|cffcccccc"
GameTooltip:AddDoubleLine(rgbhex(ccolor) .. friend_name .. rgbhex(lcolor) .. " [" .. friend_level .. "]", zcolor .. friend_area)
local ccolor = PFUI_CLASS_COLORS[info.classFilename]
local lcolor = GetDifficultyColor(tonumber(info.level)) or { 1, 1, 1 }
local zcolor = info.area == playerzone and "|cff33ffcc" or "|cffcccccc"
GameTooltip:AddDoubleLine(ccolor:WrapTextInColorCode(info.name) .. rgbhex(lcolor) .. " [" .. info.level .. "]", zcolor .. info.area)
end
end
@@ -315,15 +315,7 @@ pfUI:RegisterModule("panel", function()
end
widget.Click = function() ToggleFriendsFrame(1) end
widget:SetScript("OnEvent", function()
local online = 0
local all = GetNumFriends()
for friendIndex=1, all do
local friend_name, friend_level, friend_class, friend_area, friend_connected = GetFriendInfo(friendIndex)
if ( friend_connected ) then
online = online + 1
end
end
local online = C_FriendList.GetNumOnlineFriends()
pfUI.panel:OutputPanel("friends", FRIENDS .. ": " .. online, widget.Tooltip, widget.Click)
end)
end
+64 -66
View File
@@ -77,20 +77,19 @@ pfUI:RegisterModule("socialmod", function ()
local off = FauxScrollFrame_GetOffset(FriendsFrameFriendsScrollFrame)
for i=1, FRIENDS_TO_DISPLAY do
local name, level, class, zone, connected, status = GetFriendInfo(off + i)
if not name or name == _G.UNKNOWN then break end
local info = C_FriendList.GetFriendInfoByIndex(off + i)
if not info or not info.name or info.name == _G.UNKNOWN then break end
local name = info.name
local friendName = _G["FriendsFrameFriendButton"..i.."ButtonTextName"]
local friendLoc = _G["FriendsFrameFriendButton"..i..FRIENDS_NAME_LOCATION]
local friendInfo = _G["FriendsFrameFriendButton"..i.."ButtonTextInfo"]
local caption = friendName or friendLoc
if connected then
if not class or class == _G.UNKNOWN then break end
local ccolor = PFUI_CLASS_COLORS[L["class"][class]] or { 1, 1, 1 }
local lcolor = GetDifficultyColor(tonumber(level)) or { 1, 1, 1 }
zone = ( zone == playerzone and "|cffffffff" or "|cffcccccc" ) .. zone .. "|r"
local cname = rgbhex(ccolor) .. name .. "|r"
if info.connected then
local ccolor = PFUI_CLASS_COLORS[info.classFilename]
local status = info.afk and CHAT_FLAG_AFK or info.dnd and CHAT_FLAG_DND or ""
local zone = ( info.area == playerzone and "|cffffffff" or "|cffcccccc" ) .. info.area .. "|r"
local cname = ccolor:WrapTextInColorCode(name)
if playerdb[name] then
playerdb[name].lastseen = date("%a %d-%b-%Y")
playerdb[name].cname = cname
@@ -103,7 +102,7 @@ pfUI:RegisterModule("socialmod", function ()
friendLoc:SetText(format(TEXT(FRIENDS_LIST_TEMPLATE), cname, zone, status))
end
friendInfo:SetText(format(TEXT(FRIENDS_LEVEL_TEMPLATE), level, class))
friendInfo:SetText(format(TEXT(FRIENDS_LEVEL_TEMPLATE), info.level, info.className))
caption:SetVertexColor(1,1,1,.9)
friendInfo:SetVertexColor(1,1,1,.9)
else
@@ -124,74 +123,73 @@ pfUI:RegisterModule("socialmod", function ()
do -- add colors to who list
hooksecurefunc("WhoList_Update", function()
local num, max = GetNumWhoResults()
local num, max = C_FriendList.GetNumWhoResults()
local off = FauxScrollFrame_GetOffset(WhoListScrollFrame)
local playerzone = GetRealZoneText()
local playerrace = UnitRace("player")
local playerguild = GetGuildInfo("player")
if num + 1 >= MAX_WHOS_FROM_SERVER then
WhoFrameTotals:SetText("|cffffffff" .. format(GetText("WHO_FRAME_TOTAL_TEMPLATE", nil, num), max).." |cffaaaaaa"..format(WHO_FRAME_SHOWN_TEMPLATE, MAX_WHOS_FROM_SERVER))
else
WhoFrameTotals:SetText("|cffffffff" .. format(GetText("WHO_FRAME_TOTAL_TEMPLATE", nil, num), num).." |cffaaaaaa"..format(WHO_FRAME_SHOWN_TEMPLATE, num))
end
for i=1, WHOS_TO_DISPLAY do
local name, guild, level, race, class, zone = GetWhoInfo(off + i)
local displayedText = ""
local info = C_FriendList.GetWhoInfo(off + i)
if info then
-- filename is the class token, so no L["class"] reversal is needed
local class = info.filename
if num + 1 >= MAX_WHOS_FROM_SERVER then
displayedText = format(WHO_FRAME_SHOWN_TEMPLATE, MAX_WHOS_FROM_SERVER)
WhoFrameTotals:SetText("|cffffffff" .. format(GetText("WHO_FRAME_TOTAL_TEMPLATE", nil, num), max).." |cffaaaaaa"..displayedText)
else
displayedText = format(WHO_FRAME_SHOWN_TEMPLATE, num)
WhoFrameTotals:SetText("|cffffffff" .. format(GetText("WHO_FRAME_TOTAL_TEMPLATE", nil, num), num).." |cffaaaaaa"..displayedText)
end
_G["WhoFrameButton"..i.."Name"]:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
class = L["class"][class]
_G["WhoFrameButton"..i.."Name"]:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
if (UIDropDownMenu_GetSelectedID(WhoFrameDropDown) == 1) then
if (zone == playerzone) then
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(.5, 1, 1)
else
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(1, 1, 1)
end
elseif (UIDropDownMenu_GetSelectedID(WhoFrameDropDown) == 2) then
if (guild == playerguild) then
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(.5, 1, 1)
else
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(1, 1, 1)
end
elseif (UIDropDownMenu_GetSelectedID(WhoFrameDropDown) == 3) then
if (race == playerrace) then
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(.5, 1, 1)
else
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(1, 1, 1)
end
end
if class then
local classicon = _G["WhoFrameButton"..i].classicon
local coords = CLASS_ICON_TCOORDS[class]
local color = PFUI_CLASS_COLORS[class]
-- do we have classicons? (skin enabled?)
if classicon then
_G["WhoFrameButton"..i.."Class"]:SetTextColor(0,0,0,0)
_G["WhoFrameButton"..i.."Name"]:SetTextColor(color.r,color.g,color.b,1)
if coords then
classicon:Show()
classicon:SetTexCoord(unpack(coords))
if (UIDropDownMenu_GetSelectedID(WhoFrameDropDown) == 1) then
if (info.area == playerzone) then
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(.5, 1, 1)
else
classicon:Hide()
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(1, 1, 1)
end
else
_G["WhoFrameButton"..i.."Class"]:SetTextColor(color.r,color.g,color.b,1)
end
end
local color = GetDifficultyColor(level)
_G["WhoFrameButton"..i.."Level"]:SetTextColor(color.r, color.g, color.b)
elseif (UIDropDownMenu_GetSelectedID(WhoFrameDropDown) == 2) then
if (info.fullGuildName == playerguild) then
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(.5, 1, 1)
else
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(1, 1, 1)
end
elseif (UIDropDownMenu_GetSelectedID(WhoFrameDropDown) == 3) then
if (info.raceStr == playerrace) then
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(.5, 1, 1)
else
_G["WhoFrameButton"..i.."Variable"]:SetTextColor(1, 1, 1)
end
end
if class then
local classicon = _G["WhoFrameButton"..i].classicon
local coords = CLASS_ICON_TCOORDS[class]
local color = PFUI_CLASS_COLORS[class]
-- do we have classicons? (skin enabled?)
if classicon then
_G["WhoFrameButton"..i.."Class"]:SetTextColor(0,0,0,0)
_G["WhoFrameButton"..i.."Name"]:SetTextColor(color.r,color.g,color.b,1)
if coords then
classicon:Show()
classicon:SetTexCoord(unpack(coords))
else
classicon:Hide()
end
else
_G["WhoFrameButton"..i.."Class"]:SetTextColor(color.r,color.g,color.b,1)
end
end
local color = GetDifficultyColor(info.level)
_G["WhoFrameButton"..i.."Level"]:SetTextColor(color.r, color.g, color.b)
end
end
end)
end
+14 -25
View File
@@ -9,11 +9,6 @@ pfUI:RegisterModule("swingtimer", function ()
local ON_SWING_QUEUED = 0
local ON_SWING_QUEUE_POPPED = 1
-- Spell.dbc bits used to mirror server-side swing-reset rules.
local FLAG_AUTOATTACK = tonumber("0x08", 16) -- SPELL_INTERRUPT_FLAG_AUTOATTACK
local ATTR_KEEP_SWINGS = tonumber("0x20000", 16) -- SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS
local ATTR_ON_NEXT_SWING = tonumber("0x04", 16) -- SPELL_ATTR_ON_NEXT_SWING
-- Consolidate state into a table to avoid Lua 5.0 upvalue limit (32 max)
local S = {
mhTimer = 0, mhTimerMax = 1,
@@ -43,12 +38,11 @@ pfUI:RegisterModule("swingtimer", function ()
local WAND_SHOOT_SPELLID = 5019
local THROW_SPELLID = 2764 -- one-shot ranged, not auto-repeat
-- ATTR_ON_NEXT_SWING: spell replaces next auto-attack swing.
-- On-next-swing spells replace the next auto-attack swing.
-- Covers Raptor Strike, Maul, Mongoose Bite, Holy Strike, etc. automatically.
local function IsOnSwingSpell(spellId)
if S.onSwingCache[spellId] ~= nil then return S.onSwingCache[spellId] end
local attr = GetSpellRecField(spellId, "attributes") or 0
local result = bit.band(attr, ATTR_ON_NEXT_SWING) ~= 0
local result = C_Spell.IsNextMeleeSpell(spellId)
S.onSwingCache[spellId] = result
return result
end
@@ -734,13 +728,12 @@ pfUI:RegisterModule("swingtimer", function ()
S.pendingCastSpellId = arg1
-- Freeze the swing timer for cast-time spells that DON'T reset auto-
-- attack on completion (Slam, Hammer of Wrath on Turtle, etc.) — those
-- let the swing resume from where it paused. Detect dynamically via the
-- absent AUTOATTACK interrupt flag (8); spells with that bit reset on
-- SPELL_GO_SELF so freezing isn't necessary. Subsumes the old hardcoded
-- swingDelaySpells list (no list maintenance for new Slam-style spells).
-- let the swing resume from where it paused. C_Spell.ResetsMeleeSwing
-- mirrors the server rule; spells that reset don't need freezing (they
-- reset on SPELL_GO_SELF). Subsumes the old hardcoded swingDelaySpells
-- list (no list maintenance for new Slam-style spells).
if S.mhActive then
local iflags = GetSpellRecField(arg1, "interruptFlags") or 0
if bit.band(iflags, FLAG_AUTOATTACK) == 0 then
if not C_Spell.ResetsMeleeSwing(arg1) then
S.mhFrozenAt = GetTime()
end
end
@@ -777,17 +770,13 @@ pfUI:RegisterModule("swingtimer", function ()
S.hsQueued = false; S.cleaveQueued = false; S.maulQueued = false
ResetMH()
else
-- Mirror the server rule for "does this spell reset the auto-attack
-- swing" (Spell::IsMeleeAttackResetSpell in Turtle's core):
-- InterruptFlags has SPELL_INTERRUPT_FLAG_AUTOATTACK (0x08)
-- AND AttributesEx2 lacks NOT_RESET_AUTO_ACTIONS (0x20000).
-- If neither path resets and we're holding a frozen-swing-during-cast
-- (mhFrozenAt set by SPELL_START_SELF for non-AUTOATTACK spells), this
-- is a Slam-style cast — push the timer forward by the cast duration
-- so the bar resumes from where it paused.
local iflags = GetSpellRecField(spellId, "interruptFlags") or 0
if bit.band(iflags, FLAG_AUTOATTACK) ~= 0
and bit.band(GetSpellRecField(spellId, "attributesEx2") or 0, ATTR_KEEP_SWINGS) == 0 then
-- C_Spell.ResetsMeleeSwing mirrors the server rule (Turtle's
-- Spell::IsMeleeAttackResetSpell): InterruptFlags has AUTOATTACK and
-- AttributesEx2 lacks NOT_RESET_AUTO_ACTIONS. When the spell resets the
-- swing, snap the timers to full. Otherwise (elseif) a frozen-swing-
-- during-cast is a Slam-style cast — push the timer forward by the cast
-- duration so the bar resumes from where it paused.
if C_Spell.ResetsMeleeSwing(spellId) then
if S.mhActive and S.mhSpeed > 0 then
UpdateWeaponSpeeds()
S.mhTimerMax = S.mhSpeed
+1 -1
View File
@@ -23,7 +23,7 @@ do
-- ClassicAPI dependency check.
-- pfUI relies pervasively on the modern C_* / SuperWoW / nameplate / focus
-- API surface that ClassicAPI polyfills, so presence is required.
local PFUI_CLASSIC_API_MIN = 10903 -- (X*10000 + Y*100 + Z)
local PFUI_CLASSIC_API_MIN = 10905 -- (X*10000 + Y*100 + Z)
local PFUI_CLASSIC_API_LATEST = PFUI_CLASSIC_API_MIN
local PFUI_CLASSIC_API_WEBSITE = "https://github.com/brues-code/ClassicAPI"
local PFUI_CLASSIC_API_LATEST_URL = PFUI_CLASSIC_API_WEBSITE .. "/releases/latest"