4 Commits
Author SHA1 Message Date
Brues 60f953178f nameplates: validate unitstr before filling the per-unit cache
cache.player and cache.minion were filled from unitstr before the two guards
that validate it, so a stale identifier poisoned the cache -- and because the
fill is gated on `== nil`, the wrong answer was never recomputed. Players read
back as cache.player == false until the plate was hidden and shown again, which
only appeared to fix it because pool reuse tripped the name/guid wipe.

Two paths produce a stale unitstr: OnUpdate dispatches a targetUpdate to
OnDataChanged before it refreshes plate.istarget, and frameState.mouseoverGuid
is only updated on gaining mouseover, never on losing it.

Hoist the PLAYER_TARGET_CHANGED distrust and the UnitName mismatch check above
the cache fill so an unverified unitstr leaves the cache nil for the next tick
instead of locking in a wrong answer.
2026-09-02 10:56:46 -05:00
Brues 1dc10964a2 luarc: use nested config form so diagnostics settings apply 2026-09-02 10:31:03 -05:00
Brues 0fc08e77c0 Create .pkgmeta 2026-09-02 10:26:28 -05:00
Brues 7f3795b45b Add .luarc.json for Lua 5.1 language server diagnostics 2026-09-02 10:23:48 -05:00
6 changed files with 91 additions and 145 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"runtime": {
"version": "Lua 5.1"
},
"diagnostics": {
"disable": ["deprecated"],
"globals": [
"this",
"event",
"arg",
"arg1",
"arg2",
"arg3",
"arg4",
"arg5",
"arg6",
"arg7",
"arg8",
"arg9"
]
}
}
+4
View File
@@ -0,0 +1,4 @@
package-as: pfUI
ignore:
- .luarc.json
-104
View File
@@ -1,104 +0,0 @@
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 = 11301 -- (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"
local function FormatVersion(packed)
local x = math.floor(packed / 10000)
local y = math.floor(math.mod(packed, 10000) / 100)
local z = math.mod(packed, 100)
return string.format("v%d.%d.%d", x, y, z)
end
if not CLASSIC_API_VERSION or CLASSIC_API_VERSION < PFUI_CLASSIC_API_MIN then
local minVersion = FormatVersion(PFUI_CLASSIC_API_MIN)
pfUI_disabled = true
pfUI_locale = {}
pfUI_profiles = {}
pfUI_translation = {}
-- ClassicAPI is required. Rather than let the ~140 files that follow flood
-- errors as they load, stand up an inert stub so everything no-ops:
-- * modules/skins register their body into a no-op, so it never runs;
-- * the setfenv'd api/lib files run inside `env`, where CreateFrame and
-- any missing global (C_*, SuperWoW, ...) resolve to a null object --
-- so no real frames or live OnUpdate/OnEvent handlers get created and
-- missing API calls just return null instead of erroring.
local null -- forward declaration so the metamethods can return it
local nullmt = {
__index = function() return null end,
__newindex = function() end,
__call = function() return null end,
__concat = function() return "" end,
__tostring = function() return "" end,
}
for _, op in ipairs({ "__add","__sub","__mul","__div","__mod","__pow","__unm" }) do
nullmt[op] = function() return null end
end
for _, op in ipairs({ "__lt","__le","__eq" }) do nullmt[op] = function() return false end end
null = setmetatable({}, nullmt)
local realG = getfenv(0)
pfUI = setmetatable({}, { __index = function() return null end })
pfUI.env = setmetatable({}, {
__index = function(_, k)
if k == "CreateFrame" then return function() return null end end
local v = realG[k]
if v ~= nil then return v end
return null
end,
})
function pfUI:RegisterModule(_, _, _) end
function pfUI:RegisterSkin(_, _, _) end
function pfUI:GetEnvironment() return pfUI.env end
local detail
if not CLASSIC_API_VERSION then
detail = "The ClassicAPI DLL isn't loaded. The |cff33ffcc!!!ClassicAPI|r addon ships bundled with it -- delete your |cff33ffcc!!!ClassicAPI|r folder and install the latest release from:"
else
detail = "ClassicAPI " .. minVersion .. " or newer is required. Delete your |cff33ffcc!!!ClassicAPI|r folder and reinstall the latest release from:"
end
local function ShowRequiredPopup()
StaticPopupDialogs["PFUI_CLASSICAPI_REQUIRED"] = {
text = "|cff33ffccpf|cffffffffUI|r has been disabled.\n\n" .. detail,
button1 = OKAY,
hasEditBox = 1,
editBoxWidth = 280,
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
preferredIndex = 3,
OnShow = function()
local editBox = getglobal(this:GetName().."EditBox")
if editBox then
editBox:SetText(PFUI_CLASSIC_API_LATEST_URL)
editBox:HighlightText()
editBox:SetFocus()
end
end,
}
StaticPopup_Show("PFUI_CLASSICAPI_REQUIRED")
DEFAULT_CHAT_FRAME:AddMessage(
"|cff33ffccpf|cffffffffUI|r disabled: " .. detail .. " " .. PFUI_CLASSIC_API_LATEST_URL,
1, 0.3, 0.3
)
end
local loginFrame = CreateFrame("Frame")
loginFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
loginFrame:SetScript("OnEvent", function()
loginFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
ShowRequiredPopup()
end)
elseif CLASSIC_API_VERSION < PFUI_CLASSIC_API_LATEST then
EventUtil.ContinueOnPlayerLogin(function()
C_Timer.After(8, function()
DEFAULT_CHAT_FRAME:AddMessage(
"|cff33ffccpf|rUI: ClassicAPI " .. FormatVersion(PFUI_CLASSIC_API_LATEST) .. " is available — " .. PFUI_CLASSIC_API_LATEST_URL,
1, 0.85, 0.3
)
end)
end)
end
end
+37 -23
View File
@@ -296,23 +296,18 @@ pfUI:RegisterModule("nameplates", function ()
return plate.creatureType
end
-- Totem icon: UnitCreatedBySpell returns the totem-drop spell, whose icon IS
-- the totem's icon. It's a broadcast descriptor field the client has for every
-- summoned unit in range, so it resolves immediately for passive and active
-- totems alike -- no self-aura read or attack-cast capture needed.
--
-- Re-read the spell every call rather than caching the icon outright: a shaman
-- can swap the totem in place (same unit, new drop spell) with no plate re-add
-- to invalidate a cache, so key the cached texture on the spell id and refresh
-- it only when the spell changes.
-- Totem icon, read straight from the game:
-- * Passive totems self-cast their provided buff, so they carry exactly one
-- aura whose icon IS the totem's icon (Totem::Summon TOTEM_PASSIVE).
-- * Active totems (Searing/Magma/Fire Nova) cast at enemies and hold no
-- self-aura, so their icon arrives via UNIT_SPELLCAST_SUCCEEDED (cached
-- into plate.totemIcon by the event handler); nil here until then.
local function TotemPlate(plate)
if C.nameplates.totemicons ~= "1" then return nil end
if CreatureType(plate) ~= 11 then return nil end
local spellId = UnitCreatedBySpell(plate.cachedGuid)
if spellId ~= plate.totemSpell then
plate.totemSpell = spellId
plate.totemIcon = spellId and C_Spell.GetSpellTexture(spellId) or nil
end
if plate.totemIcon then return plate.totemIcon end
local aura = C_UnitAuras.GetBuffDataByIndex(plate.cachedGuid, 1)
if aura then plate.totemIcon = aura.icon end
return plate.totemIcon
end
@@ -504,6 +499,7 @@ nameplates:RegisterEvent("UNIT_SPELLCAST_START")
nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
nameplates:RegisterEvent("UNIT_SPELLCAST_STOP")
nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
nameplates:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
nameplates:SetScript("OnEvent", function()
@@ -592,7 +588,6 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
plate.nameplate.unit = arg1
plate.nameplate.creatureType = nil -- recompute for the new unit
plate.nameplate.totemIcon = nil
plate.nameplate.totemSpell = nil
if guid then
plateByGuid[guid] = plate.nameplate
-- Seed: the unit may already be mid-cast (its UNIT_SPELLCAST_START
@@ -670,6 +665,22 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
end
end
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
-- Active totems (Searing/Magma/Fire Nova) carry no self-aura, so their
-- attack cast is the only icon source. Capture it once per totem, gated on
-- creature type so a normal caster's spell never styles it as a totem.
if arg1 and strfind(arg1, "^nameplate") then
local guid = UnitGUID(arg1)
local plate = guid and plateByGuid[guid]
if plate and not plate.totemIcon and arg3 and CreatureType(plate) == 11 then
local tex = C_Spell.GetSpellTexture(arg3)
if tex then
plate.totemIcon = tex
plate.castUpdate = true -- re-render now so the icon shows
end
end
end
elseif event == "UNIT_AURA" then
-- ClassicAPI: fires with arg1 == "nameplateN" when a unit's aura set
-- changes (add/remove/modify). Flag the matching plate so OnUpdate does a
@@ -1028,6 +1039,16 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
local mouseover = plate.cachedGuid and plate.cachedGuid == frameState.mouseoverGuid or nil
local unitstr = target and "target" or mouseover and "mouseover" or plate.cachedGuid or nil
-- target event sometimes fires too quickly, where nameplate identifiers are not
-- yet updated. So while being inside this event, we cannot trust the unitstr.
if event == "PLAYER_TARGET_CHANGED" then unitstr = nil end
-- remove unitstr when it doesn't resolve to this plate's unit (stale istarget,
-- stale mouseover guid or a despawned unit). Must run before the cache fills
-- below -- an unrelated unitstr would poison cache.player/minion with the
-- *other* unit's answer, and the nil-gate would then never recompute it.
if unitstr and UnitName(unitstr) ~= name then unitstr = nil end
-- resolve player vs npc from plate's own unit so libunitscan can't return
-- a player record for an NPC sharing the same name (e.g. Chromie). Stored
-- as true/false/nil so it doubles as the GetUnitInfo hint.
@@ -1065,13 +1086,6 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
-- skip data updates on invisible frames
if not visible then return end
-- target event sometimes fires too quickly, where nameplate identifiers are not
-- yet updated. So while being inside this event, we cannot trust the unitstr.
if event == "PLAYER_TARGET_CHANGED" then unitstr = nil end
-- remove unitstr on unit name mismatch
if unitstr and UnitName(unitstr) ~= name then unitstr = nil end
-- always make sure to keep plate visible
plate:Show()
@@ -1105,7 +1119,7 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
local TotemIcon = TotemPlate(plate)
if TotemIcon then
-- icon resolved from the totem-drop spell (already a full path)
-- icon resolved from the totem's aura / attack cast (already a full path)
plate.totem.icon:SetTexture(TotemIcon)
plate.glow:Hide()
+28 -16
View File
@@ -3,10 +3,6 @@ function SlashCmdList.RELOAD(msg, editbox)
ReloadUI()
end
if pfUI_disabled then return end
local addonName = ...
SLASH_PFUI1 = '/pfui'
function SlashCmdList.PFUI(msg, editbox)
pfUI.gui:SetShown(not pfUI.gui:IsShown())
@@ -17,7 +13,7 @@ function SlashCmdList.GM(msg, editbox)
ToggleHelpFrame(1)
end
local pfUI = CreateFrame("Frame", addonName, UIParent)
pfUI = CreateFrame("Frame", nil, UIParent)
pfUI:RegisterEvent("ADDON_LOADED")
-- setup bootvar
@@ -27,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 = 11301 -- (X*10000 + Y*100 + Z)
local PFUI_CLASSIC_API_MIN = 11204 -- (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"
@@ -121,24 +117,35 @@ pfUI.env = {}
-- the flag; one that lacks it should drop the flag and get the safe fallback.
pfUI.handlesHookScript = true
pfUI.events = Mixin({}, CallbackRegistryMixin)
pfUI.events:OnLoad()
pfUI.events:SetUndefinedEventsAllowed(true)
if not pfUI.disabled then
pfUI.events = Mixin({}, CallbackRegistryMixin)
pfUI.events:OnLoad()
pfUI.events:SetUndefinedEventsAllowed(true)
end
-- check if macro addons are loaded (disables macrotweak/macroscan)
function pfUI:MacroAddonsLoaded()
return IsAddOnLoaded("Supermacro") or IsAddOnLoaded("SuperCleveRoidMacros") or IsAddOnLoaded("UltimaMacros")
end
pfUI.name = addonName
pfUI.path = "Interface\\AddOns\\" .. addonName
-- detect current addon path
local tocs = { "", "-master", "-tbc", "-wotlk" }
for _, name in pairs(tocs) do
local current = string.format("pfUI%s", name)
local title = C_AddOns.GetAddOnName(current)
if title then
pfUI.name = current
pfUI.path = "Interface\\AddOns\\" .. current
break
end
end
-- handle/convert media dir paths
pfUI.media = setmetatable({}, { __index = function(tab,key)
local value = tostring(key)
if value:find("img:") then
if strfind(value, "img:") then
value = string.gsub(value, "img:", pfUI.path .. "\\img\\")
elseif value:find("font:") then
elseif strfind(value, "font:") then
value = string.gsub(value, "font:", pfUI.path .. "\\fonts\\")
else
value = string.gsub(value, "Interface\\AddOns\\pfUI\\", pfUI.path .. "\\")
@@ -147,7 +154,9 @@ pfUI.media = setmetatable({}, { __index = function(tab,key)
return value
end})
pfUI.client = INTERFACE_VERSION
-- cache client version
local _, _, _, client = GetBuildInfo()
pfUI.client = client or 11200
-- setup pfUI namespace
setmetatable(pfUI.env, {__index = getfenv(0)})
@@ -381,13 +390,14 @@ function pfUI:CheckNewModules()
end
local function BackwardsCompatRegister(func, arg3)
if arg3 and type(func) == "string" and type(arg3) == "function" and func:find("vanilla") then
if arg3 and type(func) == "string" and type(arg3) == "function" and string.find(func, "vanilla") then
return arg3
end
return func
end
function pfUI:RegisterModule(name, func, arg3)
if pfUI.disabled then return end
if pfUI.module[name] then return end
func = BackwardsCompatRegister(func, arg3)
pfUI.module[name] = func
@@ -398,6 +408,7 @@ function pfUI:RegisterModule(name, func, arg3)
end
function pfUI:RegisterSkin(name, func, arg3)
if pfUI.disabled then return end
if pfUI.skin[name] then return end
func = BackwardsCompatRegister(func, arg3)
pfUI.skin[name] = func
@@ -418,6 +429,7 @@ function pfUI:LoadSkin(s)
end
pfUI:SetScript("OnEvent", function()
if pfUI.disabled then return end
-- make sure to initialize and set our fonts
-- each time an addon got loaded but only
@@ -431,7 +443,7 @@ pfUI:SetScript("OnEvent", function()
-- "@project-version@" until release tooling substitutes it — mark those
-- explicitly as "dev" instead of pretending they're a real numbered build.
local raw = tostring(GetAddOnMetadata(pfUI.name, "Version"))
if raw:find("@") then
if strfind(raw, "@") then
pfUI.version.major, pfUI.version.minor, pfUI.version.fix = 0, 0, 0
pfUI.version.string = "dev"
else
-2
View File
@@ -8,8 +8,6 @@
## SavedVariablesPerCharacter: pfUI_config, pfUI_init, pfUI_playerDB
## X-Website: https://github.com/brues-code/pfUI
API_Check.lua
pfUI.lua
init\env.xml