From 06768c985a29cb3a6410550fbaefaeb2109f43e3 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:14:40 -0500 Subject: [PATCH] Merge classicapi-next: friend list and spell fields on ClassicAPI Squashed from the classicapi-next branch: - Read spell fields through ClassicAPI instead of nampower (libdebuff, swingtimer) - Read friend and who-list class tokens from C_FriendList (socialmod, libunitscan) - Count online friends with C_FriendList.GetNumOnlineFriends - Add a friend notes module (modules/friendnotes.lua) - Bump ClassicAPI minimum version to 10906 --- init/modules.xml | 1 + libs/libdebuff.lua | 8 +-- libs/libunitscan.lua | 21 ++++--- modules/friendnotes.lua | 79 ++++++++++++++++++++++++ modules/panel.lua | 22 +++---- modules/socialmod.lua | 130 ++++++++++++++++++++-------------------- modules/swingtimer.lua | 39 +++++------- pfUI.lua | 2 +- 8 files changed, 179 insertions(+), 123 deletions(-) create mode 100644 modules/friendnotes.lua diff --git a/init/modules.xml b/init/modules.xml index 182c993a..c9b0069f 100644 --- a/init/modules.xml +++ b/init/modules.xml @@ -81,4 +81,5 @@ + \ No newline at end of file diff --git a/libs/libdebuff.lua b/libs/libdebuff.lua index 3498079e..b05a099b 100644 --- a/libs/libdebuff.lua +++ b/libs/libdebuff.lua @@ -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, diff --git a/libs/libunitscan.lua b/libs/libunitscan.lua index ad00ed8c..fed17d81 100644 --- a/libs/libunitscan.lua +++ b/libs/libunitscan.lua @@ -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 diff --git a/modules/friendnotes.lua b/modules/friendnotes.lua new file mode 100644 index 00000000..dfc51466 --- /dev/null +++ b/modules/friendnotes.lua @@ -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) diff --git a/modules/panel.lua b/modules/panel.lua index 11a078b9..39bfe6f6 100644 --- a/modules/panel.lua +++ b/modules/panel.lua @@ -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 diff --git a/modules/socialmod.lua b/modules/socialmod.lua index 4cf14cf3..b13a660d 100644 --- a/modules/socialmod.lua +++ b/modules/socialmod.lua @@ -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 diff --git a/modules/swingtimer.lua b/modules/swingtimer.lua index 38100f86..d9d5a99a 100644 --- a/modules/swingtimer.lua +++ b/modules/swingtimer.lua @@ -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 diff --git a/pfUI.lua b/pfUI.lua index b8d6532b..3e956e72 100644 --- a/pfUI.lua +++ b/pfUI.lua @@ -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 = 10906 -- (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"