From f0d9dca63f06c8f64012b9310df80a7a0a4a89d7 Mon Sep 17 00:00:00 2001 From: brues-code Date: Wed, 27 May 2026 20:35:15 -0500 Subject: [PATCH] Classic API Focus (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * focus: migrate to FocusUnit / "focus" token / PLAYER_FOCUS_CHANGED ClassicAPI now polyfills modern WoW's focus system: FocusUnit / ClearFocus + "focus" / "focustarget" unit tokens accepted by every UnitX function + PLAYER_FOCUS_CHANGED event. Drop pfUI's GUID-juggling pseudo-frame. - env/tables.lua: add "focus" and "focustarget" to pfValidUnits — the focus frames now follow the standard event-driven CreateUnitFrame path with f.label = "focus" - modules/focus.lua: rewrite. Slash commands use FocusUnit / ClearFocus directly; no more manual frame.label/unitname/id manipulation. /focus still does a target-swap to resolve names → units, then FocusUnit("target") captures the GUID before the target is restored. PLAYER_FOCUS_CHANGED triggers immediate frame refresh on assign/clear. - api/unitframes.lua: delete the pseudo-focus block (per-tick "scan all units for matching name" loop) and the "focus"/"focustarget" special- case in the visibility branch — "focus" is now a real token. - modules/castbar.lua: bind the focus castbar to unitstr = "focus" once; drop the OnUpdate that synced pfUI.castbar.focus from pfUI.uf.focus.label/unitname. UnitGUID("focus") resolves at read time. - Drop C_Minimap.SetFocusByGUID / SetFocusByName / ClearFocus calls — C_Minimap subscribes to PLAYER_FOCUS_CHANGED on its own. Net -183 lines. Focus stops being a pfUI-special pseudo-frame and becomes "just another unit token" — same treatment as target/player. * add nameplate to valid units --------- Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com> --- api/unitframes.lua | 49 +-------- env/tables.lua | 29 ++++-- modules/castbar.lua | 30 +----- modules/focus.lua | 247 ++++++++++++-------------------------------- 4 files changed, 89 insertions(+), 266 deletions(-) diff --git a/api/unitframes.lua b/api/unitframes.lua index 2057cacb..a38e0a35 100644 --- a/api/unitframes.lua +++ b/api/unitframes.lua @@ -356,7 +356,7 @@ function pfUI.uf:UpdateVisibility() end -- vanilla visibility - if self.unitname and self.unitname ~= "focus" and self.unitname ~= "focustarget" then + if self.unitname then self:Show() elseif visibility == "hide" then self:Hide() @@ -1462,52 +1462,7 @@ function pfUI.uf.OnUpdate() end end - -- handle pseudo focus frames - if this.unitname and this == pfFocus then - local unitname = ( this.label and UnitName(this.label) ) or "" - - if pfFocusTarget then -- update focus target - pfFocusTarget.label = this.label and this.label .. "target" or nil - local focustargetname = pfFocusTarget.label and UnitName(pfFocusTarget.label) or nil - - if pfFocusTarget.lastUnit ~= focustargetname then - pfFocusTarget.lastUnit = focustargetname - pfFocusTarget.instantRefresh = true - pfUI.uf:RefreshUnit(pfFocusTarget, "all") - end - end - - -- break here on unset focus frames - if not this.unitname or this.unitname == "focus" then return end - - -- focus unit detection - if this.unitname ~= strlower(unitname) then - -- invalid focus frame - for unit, bool in pairs(pfValidUnits) do - local scan = UnitName(unit) or "" - if this.unitname == strlower(scan) then - this.label = unit - if this.portrait then this.portrait.model.lastUnit = nil end - this.instantRefresh = true - pfUI.uf:RefreshUnit(this, "all") - return - end - this.label = nil - this.instantRefresh = true - this.hp.bar:SetStatusBarColor(.2,.2,.2) - end - end - end - - -- handle pseudo focus target visibility - if this.unitname and this == pfFocusTarget then - if pfFocus and not pfFocus.label or pfFocus.label == "" then - this.label = nil - return - end - end - - if not this.label then return end + if not this.label or this.label == "" then return end -- update portrait on first visible frame if this.portrait and this.portrait.model and this.portrait.model.update then diff --git a/env/tables.lua b/env/tables.lua index a4b84615..e2f6d457 100644 --- a/env/tables.lua +++ b/env/tables.lua @@ -52,20 +52,33 @@ pfValidUnits["player"] = true pfValidUnits["target"] = true pfValidUnits["mouseover"] = true +pfValidUnits["focus"] = true +pfValidUnits["focustarget"] = true +pfValidUnits["focustargettarget"] = true + pfValidUnits["pettarget"] = true pfValidUnits["playertarget"] = true pfValidUnits["targettarget"] = true pfValidUnits["mouseovertarget"] = true pfValidUnits["targettargettarget"] = true -for i=1,4 do pfValidUnits["party" .. i] = true end -for i=1,4 do pfValidUnits["partypet" .. i] = true end -for i=1,40 do pfValidUnits["raid" .. i] = true end -for i=1,40 do pfValidUnits["raidpet" .. i] = true end -for i=1,4 do pfValidUnits["party" .. i .. "target"] = true end -for i=1,4 do pfValidUnits["partypet" .. i .. "target"] = true end -for i=1,40 do pfValidUnits["raid" .. i .. "target"] = true end -for i=1,40 do pfValidUnits["raidpet" .. i .. "target"] = true end +for i=1,4 do + pfValidUnits["party" .. i] = true + pfValidUnits["party" .. i .. "target"] = true + pfValidUnits["partypet" .. i] = true + pfValidUnits["partypet" .. i .. "target"] = true +end + +for i=1,40 do + pfValidUnits["raid" .. i] = true + pfValidUnits["raid" .. i .. "target"] = true + + pfValidUnits["raidpet" .. i] = true + pfValidUnits["raidpet" .. i .. "target"] = true + + pfValidUnits["nameplate" .. i] = true + pfValidUnits["nameplate" .. i .. "target"] = true +end -- itemid = buyPrice (copper). Sell price comes from C_Item.GetItemSellPriceByID; -- this table only carries the curated vendor-buy price (which isn't a static diff --git a/modules/castbar.lua b/modules/castbar.lua index 7a25ec84..c01509bb 100644 --- a/modules/castbar.lua +++ b/modules/castbar.lua @@ -390,9 +390,6 @@ pfUI:RegisterModule("castbar", function () pfUI.castbar.focus.showrank = C.castbar.focus.showrank == "1" and true or nil pfUI.castbar.focus.spacing = default_border * 2 + tonumber(C.unitframes.focus.pspace) * GetPerfectPixel() - -- reset unitstr for vanilla focus frame emulation - pfUI.castbar.focus.unitstr = nil - local anchor = pfUI.uf.focus.portrait:GetHeight() > pfUI.uf.focus:GetHeight() and pfUI.uf.focus.power or pfUI.uf.focus local width = C.castbar.focus.width ~= "-1" and C.castbar.focus.width or anchor:GetWidth() pfUI.castbar.focus:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -pfUI.castbar.focus.spacing) @@ -402,31 +399,8 @@ pfUI:RegisterModule("castbar", function () pfUI.castbar.focus:SetHeight(C.castbar.focus.height) end - -- keep unit values in sync with focus unitframe - HookScript(pfUI.castbar.focus, "OnUpdate", function() - -- clear on empty focus frames - if pfUI.uf.focus.unitname == nil and pfUI.uf.focus.label == nil then - pfUI.castbar.focus.unitstr = nil - pfUI.castbar.focus.unitname = nil - return - end - - -- skip on initial values - if pfUI.uf.focus.unitname == "focus" then return end - if pfUI.uf.focus.unitname == "" then return end - - -- try to obtain a unitstr - pfUI.castbar.focus.unitstr = string.format("%s%s", (pfUI.uf.focus.label or ""), (pfUI.uf.focus.id or "")) - pfUI.castbar.focus.unitstr = pfUI.castbar.focus.unitstr == "" and nil or pfUI.castbar.focus.unitstr - - if pfUI.castbar.focus.unitstr then - -- read non-lowercase unitname when possible - pfUI.castbar.focus.unitname = UnitName(pfUI.castbar.focus.unitstr) or pfUI.castbar.focus.unitname - elseif strlower(pfUI.castbar.focus.unitname) ~= strlower(pfUI.uf.focus.unitname) then - -- sync unitname with focus frame's lowercase value - pfUI.castbar.focus.unitname = pfUI.uf.focus.unitname - end - end) + -- bind castbar to the "focus" unit token; the GUID resolves at read time + pfUI.castbar.focus.unitstr = "focus" UpdateMovable(pfUI.castbar.focus) end diff --git a/modules/focus.lua b/modules/focus.lua index d93c2fc4..b4753d0f 100644 --- a/modules/focus.lua +++ b/modules/focus.lua @@ -13,236 +13,117 @@ pfUI:RegisterModule("focus", function () pfUI.uf.focustarget:SetPoint("BOTTOMLEFT", pfUI.uf.focus, "TOP", 0, 10) UpdateMovable(pfUI.uf.focustarget) pfUI.uf.focustarget:Hide() + + -- PLAYER_FOCUS_CHANGED drives immediate refresh on focus assign / clear. + -- The frame's 0.2s tick keeps health/power/aura data fresh between events. + local refresher = CreateFrame("Frame") + refresher:RegisterEvent("PLAYER_FOCUS_CHANGED") + refresher:SetScript("OnEvent", function() + pfUI.uf.focus.instantRefresh = true + pfUI.uf:RefreshUnit(pfUI.uf.focus, "all") + pfUI.uf.focustarget.instantRefresh = true + pfUI.uf:RefreshUnit(pfUI.uf.focustarget, "all") + end) end) --- Helper: set focus frame to a GUID -local function SetFocusByGUID(guid) - pfUI.uf.focus.unitname = nil - pfUI.uf.focus.label = guid - pfUI.uf.focus.id = "" - - if pfUI.uf.focustarget then - pfUI.uf.focustarget.unitname = nil - pfUI.uf.focustarget.label = guid .. "target" - pfUI.uf.focustarget.id = "" - end - - C_Minimap.SetFocusByGUID(guid) -end - --- Helper: set focus frame by name (fallback, no Nampower) -local function SetFocusByName(name) - pfUI.uf.focus.unitname = strlower(name) - pfUI.uf.focus.label = nil - pfUI.uf.focus.id = nil - - if pfUI.uf.focustarget then - pfUI.uf.focustarget.unitname = strlower(name) .. "target" - pfUI.uf.focustarget.label = nil - pfUI.uf.focustarget.id = nil - end - - C_Minimap.SetFocusByName(name) -end - SLASH_PFFOCUS1, SLASH_PFFOCUS2 = '/focus', '/pffocus' function SlashCmdList.PFFOCUS(msg) - if not pfUI.uf or not pfUI.uf.focus then return end + if msg == "" then + FocusUnit("target") + return + end - if msg ~= "" then - -- Try to resolve GUID via short target swap - if UnitExists then - local prevGUID = UnitGUID("target") - local prevPlayer = UnitIsUnit("target", "player") + -- Resolve name → unit via short target-swap, capturing focus during the swap. + local prevGUID = UnitGUID("target") + local prevPlayer = UnitIsUnit("target", "player") - -- Suppress "Unknown unit" errors during targeting attempts (fired async) - UIErrorsFrame:UnregisterEvent("UI_ERROR_MESSAGE") + -- Suppress "Unknown unit" errors during targeting attempts (fired async) + UIErrorsFrame:UnregisterEvent("UI_ERROR_MESSAGE") - -- Try exact match first, then prefix match via /tar - TargetByName(msg, true) - local guid = UnitGUID("target") + -- Try exact match first, then prefix match via /tar + TargetByName(msg, true) + if not UnitExists("target") then + SlashCmdList.TARGET(msg) + end - if not guid or guid == "0x0000000000000000" then - -- Fallback: prefix match (like /tar storm -> Stormwind Guard) - SlashCmdList.TARGET(msg) - _, guid = UnitExists("target") - end + if UnitExists("target") then + FocusUnit("target") + end - -- Re-enable errors next frame (errors are fired async) - local restore = CreateFrame("Frame") - restore:SetScript("OnUpdate", function() - UIErrorsFrame:RegisterEvent("UI_ERROR_MESSAGE") - restore:SetScript("OnUpdate", nil) - end) + -- Re-enable errors next frame + local restore = CreateFrame("Frame") + restore:SetScript("OnUpdate", function() + UIErrorsFrame:RegisterEvent("UI_ERROR_MESSAGE") + restore:SetScript("OnUpdate", nil) + end) - -- Restore previous target - if prevGUID and prevGUID ~= "0x0000000000000000" then - TargetUnit(prevGUID) - elseif prevPlayer then - TargetUnit("player") - else - ClearTarget() - end - - if guid and guid ~= "0x0000000000000000" then - SetFocusByGUID(guid) - return - end - end - - -- Fallback: name-based (non-Nampower clients) - SetFocusByName(msg) + if prevGUID and prevGUID ~= "0x0000000000000000" then + TargetUnit(prevGUID) + elseif prevPlayer then + TargetUnit("player") else - -- No msg: use current target - if UnitExists then - local guid = UnitGUID("target") - if guid and guid ~= "0x0000000000000000" then - SetFocusByGUID(guid) - return - end - end - - -- Fallback: name-based - local name = UnitName("target") - if name then - SetFocusByName(name) - end + ClearTarget() end end SLASH_PFCLEARFOCUS1, SLASH_PFCLEARFOCUS2 = '/clearfocus', '/pfclearfocus' function SlashCmdList.PFCLEARFOCUS(msg) - if pfUI.uf and pfUI.uf.focus then - pfUI.uf.focus.unitname = nil - pfUI.uf.focus.label = nil - pfUI.uf.focus.id = nil - end - - if pfUI.uf and pfUI.uf.focustarget then - pfUI.uf.focustarget.unitname = nil - pfUI.uf.focustarget.label = nil - pfUI.uf.focustarget.id = nil - end - - C_Minimap.ClearFocus() + ClearFocus() end SLASH_PFCASTFOCUS1, SLASH_PFCASTFOCUS2 = '/castfocus', '/pfcastfocus' function SlashCmdList.PFCASTFOCUS(msg) - if not pfUI.uf.focus or not pfUI.uf.focus:IsShown() then + local focusGUID = UnitGUID("focus") + if not focusGUID or focusGUID == "0x0000000000000000" then UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1, 0, 0) return end local func = pfUI.api.TryMemoizedFuncLoadstringForSpellCasts(msg) - local focusGUID = pfUI.uf.focus.label - local hasGUID = focusGUID and focusGUID ~= "" and focusGUID ~= "0x0000000000000000" -- GUID-based cast (Nampower) - no target toggle needed - if hasGUID and CastSpellByName and not func then + if not func then CastSpellByName(msg, focusGUID) return end - -- For lua functions with GUID: short target swap via GUID - if hasGUID and func then - local currentGUID = UnitGUID("target") - local isPlayer = UnitIsUnit("target", "player") + -- Lua-function cast: short target swap via GUID + local prevGUID = UnitGUID("target") + local prevPlayer = UnitIsUnit("target", "player") - TargetUnit(focusGUID) - local newGUID = UnitGUID("target") - - if newGUID ~= focusGUID then - -- Could not target focus, restore and fail - if currentGUID and currentGUID ~= "0x0000000000000000" then - TargetUnit(currentGUID) - elseif isPlayer then - TargetUnit("player") - else - TargetLastTarget() - end - UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1, 0, 0) - return - end - - func() - - if currentGUID and currentGUID ~= "0x0000000000000000" then - TargetUnit(currentGUID) - elseif isPlayer then + TargetUnit(focusGUID) + if UnitGUID("target") ~= focusGUID then + if prevGUID and prevGUID ~= "0x0000000000000000" then + TargetUnit(prevGUID) + elseif prevPlayer then TargetUnit("player") else TargetLastTarget() end + UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1, 0, 0) return end - -- Fallback: name-based target swap (no Nampower / no GUID) - local skiptarget = false - local player = UnitIsUnit("target", "player") - local unitname = "" + func() - if pfUI.uf.focus.label and pfUI.uf.focus.id and - UnitIsUnit("target", pfUI.uf.focus.label .. pfUI.uf.focus.id) then - skiptarget = true + if prevGUID and prevGUID ~= "0x0000000000000000" then + TargetUnit(prevGUID) + elseif prevPlayer then + TargetUnit("player") else - pfScanActive = true - if pfUI.uf.focus.label and pfUI.uf.focus.id then - unitname = UnitName(pfUI.uf.focus.label .. pfUI.uf.focus.id) - TargetUnit(pfUI.uf.focus.label .. pfUI.uf.focus.id) - else - unitname = pfUI.uf.focus.unitname - TargetByName(pfUI.uf.focus.unitname, true) - end - - if strlower(UnitName("target") or "") ~= strlower(unitname or "") then - pfScanActive = nil - TargetLastTarget() - UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1, 0, 0) - return - end - end - - if func then - func() - else - CastSpellByName(msg) - end - - if skiptarget == false then - pfScanActive = nil - if player then - TargetUnit("player") - else - TargetLastTarget() - end + TargetLastTarget() end end SLASH_PFSWAPFOCUS1, SLASH_PFSWAPFOCUS2 = '/swapfocus', '/pfswapfocus' function SlashCmdList.PFSWAPFOCUS(msg) - if not pfUI.uf or not pfUI.uf.focus then return end + local targetGUID = UnitGUID("target") + local oldFocusGUID = UnitGUID("focus") - local _, guid = nil, nil - if UnitExists then - _, guid = UnitExists("target") - end - - if guid and guid ~= "0x0000000000000000" then - local oldGUID = pfUI.uf.focus.label - - SetFocusByGUID(guid) - - -- Target old focus if we had one - if oldGUID and oldGUID ~= "" and oldGUID ~= "0x0000000000000000" then - TargetUnit(oldGUID) - end - else - -- Fallback: name-based swap - local oldunit = UnitExists("target") and strlower(UnitName("target") or "") - if oldunit and pfUI.uf.focus.unitname then - TargetByName(pfUI.uf.focus.unitname, true) - pfUI.uf.focus.unitname = oldunit + if targetGUID and targetGUID ~= "0x0000000000000000" then + FocusUnit("target") + if oldFocusGUID and oldFocusGUID ~= "0x0000000000000000" then + TargetUnit(oldFocusGUID) end end end