7 Commits
Author SHA1 Message Date
Brues df21f1f26c stable: capture per-iteration button in Enable/Disable closures
Lua 5.0 shares the generic-for loop variable across iterations, so the
closures all saw the final (nil) value.
2026-10-25 12:12:05 -05:00
Brues bd7bf3f39f Size aura cooldown text to its laid-out icon
Unit frame aura icons only get their size from the layout pass, but
RefreshUnit started their timers before it ran. A cooldown text created
on a never-laid-out icon saw a height of 0 and fell back to a 16pt font,
so some buff and debuff timers overflowed their icons. (Closes #64)

- RefreshUnit stores each aura's timer and starts them after
  LayoutAuras, once every icon has its final size.
- With dynamic size on, the cooldown text re-sizes its font whenever
  its frame's height changed since the last SetTimer, so icons that
  switch between the normal and own aura size keep a matching font.
2026-10-25 11:48:24 -05:00
Brues 987c337709 Revert "Remove CLINK parsing"
This reverts commit c03b74c3d1.
2026-10-25 11:39:39 -05:00
Brues cdaf9336a3 nameplates: skip destroyed plates on PLAYER_LOGOUT
Leaving the world destroys the units before PLAYER_LOGOUT fires, so some
plates in the registry are already dead: their overlay buttons dropped
their events and lost their methods, and UnregisterAllEvents errored as a
nil method. Only silence the plates that are still alive.
2026-10-25 11:33:51 -05:00
Brues e4f0849816 Pass the draw layer as CreateTexture's second argument
CreateTexture("OVERLAY") names the texture "OVERLAY" and leaves it on
the default ARTWORK layer. Pass nil for the name so the layer applies.
2026-10-25 11:21:01 -05:00
Brues c03b74c3d1 Remove CLINK parsing 2026-10-24 23:23:16 -05:00
Brues e583ac1e83 Drive mapreveal from MAP_EXPLORATION_UPDATED and C_MapExplorationInfo 2026-10-24 21:33:12 -05:00
13 changed files with 117 additions and 91 deletions
+35 -37
View File
@@ -69,6 +69,35 @@ local function LayoutAuras(unit, unitstr)
end
end
-- Returns the cooldown start and duration of an aura, 0, 0 when it has none.
-- Start is capped to now: talent-extended auras run past their Spell.dbc base
-- duration, and a start in the future trips the cooldown text's 2^32
-- wraparound branch.
local function AuraTimer(duration, expirationTime)
if expirationTime <= 0 then return 0, 0 end
local now = GetTime()
local start, dur = expirationTime - duration, duration
if start > now or dur <= 0 then
start, dur = now, expirationTime - now
end
if dur <= 0 then return 0, 0 end
return start, dur
end
-- Starts the timers RefreshUnit stored on the shown icons of an aura block.
local function StartAuraTimers(icons)
if not icons then return end
local i = 1
while icons[i] and icons[i]:IsShown() do
local dur = icons[i].auraDuration
CooldownFrame_SetTimer(icons[i].cd, icons[i].auraStart, dur, dur > 0 and 1 or 0)
i = i + 1
end
end
-- ============================================================================
-- GUID-based Roster Tracking for Smart Updates
-- Only updates frames where the unit actually changed, not ALL 40 frames
@@ -1711,24 +1740,7 @@ function pfUI.uf:RefreshUnit(unit, component)
unit.buffs[i].stacks:SetText("")
end
if expirationTime > 0 then
-- pfUI's cooldown text falls into a 2^32 wraparound branch when start > GetTime(),
-- which happens for talent-extended buffs where the real duration exceeds the
-- Spell.dbc base. Anchor start to now in that case to keep the remaining math sane.
local now = GetTime()
local start = expirationTime - duration
local dur = duration
if start > now or dur <= 0 then
start, dur = now, expirationTime - now
end
if dur > 0 then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, dur, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
unit.buffs[i].auraStart, unit.buffs[i].auraDuration = AuraTimer(duration, expirationTime)
else
unit.buffs[i]:Hide()
end
@@ -1771,24 +1783,7 @@ function pfUI.uf:RefreshUnit(unit, component)
if texture then
unit.debuffs[i]:Show()
if expirationTime > 0 then
-- Cap start to now so talent-extended debuffs (expirationTime past
-- the dbc base duration) don't push start into the future and trip
-- CooldownFrame_SetTimer's 2^32-wraparound branch.
local now = GetTime()
local start = expirationTime - duration
local dur = duration
if start > now or dur <= 0 then
start, dur = now, expirationTime - now
end
if dur > 0 then
CooldownFrame_SetTimer(unit.debuffs[i].cd, start, dur, 1)
else
CooldownFrame_SetTimer(unit.debuffs[i].cd, 0, 0, 0)
end
else
CooldownFrame_SetTimer(unit.debuffs[i].cd, 0, 0, 0)
end
unit.debuffs[i].auraStart, unit.debuffs[i].auraDuration = AuraTimer(duration, expirationTime)
if stacks > 1 then
unit.debuffs[i].stacks:SetText(stacks)
@@ -1806,8 +1801,11 @@ function pfUI.uf:RefreshUnit(unit, component)
end
end
-- timers start after the layout so each cooldown text is sized to its icon
if component == "all" or component == "aura" then
LayoutAuras(unit, unitstr)
StartAuraTimers(unit.buffs)
StartAuraTimers(unit.debuffs)
end
-- indicators
@@ -2238,7 +2236,7 @@ function pfUI.uf:AddIcon(frame, pos, icon, timeleft, stacks, start, duration)
if not frame.icon[pos] then
frame.icon[pos] = CreateFrame("Frame", nil, frame.icon)
frame.icon[pos]:SetParent(frame)
frame.icon[pos].tex = frame.icon[pos]:CreateTexture("OVERLAY")
frame.icon[pos].tex = frame.icon[pos]:CreateTexture(nil, "OVERLAY")
frame.icon[pos].tex:SetAllPoints(frame.icon[pos])
frame.icon[pos].tex:SetTexCoord(.08, .92, .08, .92)
frame.icon[pos].stacks = frame.icon[pos]:CreateFontString(nil, "OVERLAY")
+1 -1
View File
@@ -57,7 +57,7 @@ pfUI:RegisterModule("autovendor", function ()
-- Setup Autosell button
autovendor.button = CreateFrame("Button", "pfMerchantAutoVendorButton", MerchantFrame)
autovendor.button:SetSize(36, 36)
autovendor.button.icon = autovendor.button:CreateTexture("ARTWORK")
autovendor.button.icon = autovendor.button:CreateTexture(nil, "ARTWORK")
autovendor.button.icon:SetTexture("Interface\\Icons\\Spell_Shadow_SacrificialShield")
autovendor.button:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
+1 -1
View File
@@ -23,7 +23,7 @@ pfUI:RegisterModule("combopoints", function ()
pfUI.combopoints[point]:SetPoint("CENTER", UIParent, "CENTER", (point - 3) * (combo_width + border*3), 10 )
end
pfUI.combopoints[point].tex = pfUI.combopoints[point]:CreateTexture("OVERLAY")
pfUI.combopoints[point].tex = pfUI.combopoints[point]:CreateTexture(nil, "OVERLAY")
pfUI.combopoints[point].tex:SetAllPoints(pfUI.combopoints[point])
if point < 3 then
+26 -13
View File
@@ -59,18 +59,14 @@ pfUI:RegisterModule("cooldown", function ()
end
end
local height, size
local textcount = 0
local function pfCreateCoolDown(cooldown, start, duration)
textcount = textcount + 1
local name = cooldown.GetName and cooldown:GetName() or "pfCooldown" .. textcount
cooldown.pfCooldownText = CreateFrame("Frame", name .. "Text", cooldown:GetParent())
cooldown.pfCooldownText.cooldown = cooldown
cooldown.pfCooldownText:SetAllPoints(cooldown)
cooldown.pfCooldownText:SetFrameLevel(cooldown:GetParent():GetFrameLevel() + 2)
cooldown.pfCooldownText.text = cooldown.pfCooldownText:CreateFontString(name .. "TextString", "OVERLAY")
local function GetCooldownHeight(cooldown)
return cooldown:GetParent() and cooldown:GetParent():GetHeight() or cooldown:GetHeight() or 0
end
-- sizes the text to the cooldown's current height; the dynamic size follows
-- the frame, so SetCooldown calls this again whenever that height changes
local function pfUpdateCooldownFont(cooldown)
local size
if not cooldown.pfCooldownType then
size = tonumber(C.appearance.cd.font_size_foreign)
elseif cooldown.pfCooldownType == "BLIZZARD" then
@@ -82,12 +78,27 @@ pfUI:RegisterModule("cooldown", function ()
end
-- enforce dynamic font size
if C.appearance.cd.dynamicsize == "1" and cooldown.GetParent then
height = cooldown:GetParent() and cooldown:GetParent():GetHeight() or cooldown:GetHeight() or 0
if C.appearance.cd.dynamicsize == "1" then
local height = GetCooldownHeight(cooldown)
cooldown.pfCooldownText.height = height
size = math.max((height > 0 and height * .64 or 16), size)
end
cooldown.pfCooldownText.text:SetFont(pfUI.media[C.appearance.cd.font], size, "OUTLINE")
end
local textcount = 0
local function pfCreateCoolDown(cooldown, start, duration)
textcount = textcount + 1
local name = cooldown.GetName and cooldown:GetName() or "pfCooldown" .. textcount
cooldown.pfCooldownText = CreateFrame("Frame", name .. "Text", cooldown:GetParent())
cooldown.pfCooldownText.cooldown = cooldown
cooldown.pfCooldownText:SetAllPoints(cooldown)
cooldown.pfCooldownText:SetFrameLevel(cooldown:GetParent():GetFrameLevel() + 2)
cooldown.pfCooldownText.text = cooldown.pfCooldownText:CreateFontString(name .. "TextString", "OVERLAY")
pfUpdateCooldownFont(cooldown)
cooldown.pfCooldownText.text:SetPoint("CENTER", cooldown.pfCooldownText, "CENTER", 0, 0)
cooldown.pfCooldownText:SetScript("OnUpdate", pfCooldownOnUpdate)
end
@@ -136,6 +147,8 @@ pfUI:RegisterModule("cooldown", function ()
and start > 0 and duration > 0 and (not enable or enable > 0) then
if( not this.pfCooldownText ) then
pfCreateCoolDown(this, start, duration)
elseif C.appearance.cd.dynamicsize == "1" and GetCooldownHeight(this) ~= this.pfCooldownText.height then
pfUpdateCooldownFont(this)
end
this.pfCooldownText.start = start
+1 -1
View File
@@ -183,7 +183,7 @@ pfUI:RegisterModule("gui", function ()
frame.color:SetHeight(12)
CreateBackdrop(frame.color)
frame.color:SetPoint("RIGHT" , -5, 1)
frame.color.prev = frame.color.backdrop:CreateTexture("OVERLAY")
frame.color.prev = frame.color.backdrop:CreateTexture(nil, "OVERLAY")
frame.color.prev:SetAllPoints(frame.color)
local cr, cg, cb, ca = strsplit(",", category[config])
+35 -23
View File
@@ -31,7 +31,6 @@ pfUI:RegisterModule("mapreveal", function ()
end)
local explorecaches = {}
local alreadyknown = {} -- per-zone accumulator: { [zone] = { [texName] = true } }
-- Own texture pool - separate from Blizzard's WorldMapOverlay textures
local overlayPool = CreateTexturePool(WorldMapDetailFrame, "BORDER", nil, nil, function(_, tex)
@@ -93,43 +92,45 @@ pfUI:RegisterModule("mapreveal", function ()
-- clear stale caches
for k in pairs(explorecaches) do explorecaches[k] = nil end
-- hide all our textures from last frame
-- hide all our textures and icons from last frame
overlayPool:ReleaseAll()
explorePool:ReleaseAll()
local showExplore = C.appearance.worldmap.mapexploration == "1"
local showReveal = C.appearance.worldmap.mapreveal == "1"
if not showExplore and not showReveal then return end
local r,g,b,a = GetStringColor(C.appearance.worldmap.mapreveal_color)
local mapFileName = GetMapInfo()
if not mapFileName then mapFileName = "World" end
local numOverlays = GetNumMapOverlays()
-- accumulate explored overlays per zone (never clear, only add)
if not alreadyknown[mapFileName] then alreadyknown[mapFileName] = {} end
for i = 1, numOverlays do
local texName = GetMapOverlayInfo(i)
if texName then alreadyknown[mapFileName][string.upper(texName)] = true end
end
local zoneKnown = alreadyknown[mapFileName]
-- hide explore icons
explorePool:ReleaseAll()
-- ClassicAPI: full overlay list for the viewed zone (explored + unexplored),
-- read straight from WorldMapOverlay.dbc. Replaces the hand-measured pfMapOverlayData.
local zoneData = C_Map.GetMapOverlays() or {}
-- ClassicAPI: the unexplored subset of the viewed zone, using the engine's
-- own explored-bit test. Replaces the alreadyknown accumulator, which
-- replayed the explored-only GetMapOverlayInfo view. Keyed by the bare
-- texture name, the same key the icon cache below uses.
local unknown = {}
if showExplore then
for _, overlay in ipairs(C_MapExplorationInfo.GetUnexploredMapTextures() or {}) do
if overlay.textureName and overlay.textureName ~= "" then
unknown[string.upper(overlay.textureName)] = true
end
end
end
for _, overlay in ipairs(zoneData) do
local name = overlay.textureName -- bare, e.g. "DRYGULCHRAVINE"
local textureName = overlay.texturePath -- full engine path (for SetTexture)
local textureWidth = overlay.textureWidth
local textureHeight = overlay.textureHeight
local offsetX = overlay.offsetX
local offsetY = overlay.offsetY
-- explore magnifying glass icon. `alreadyknown` stores the FULL paths
-- GetMapOverlayInfo returns, so compare with the full path, not the bare
-- name.
if C.appearance.worldmap.mapexploration == "1" and not zoneKnown[string.upper(textureName)] then
-- explore magnifying glass icon for overlays the engine still
-- counts as unexplored.
if showExplore and unknown[string.upper(name)] then
local explore = explorePool:Acquire()
explore:SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT", offsetX + textureWidth/2, -offsetY - textureHeight/2)
explore.name = mapFileName .. " (" .. name .. ")"
@@ -147,7 +148,7 @@ pfUI:RegisterModule("mapreveal", function ()
-- disambiguated from the actual BLP dimensions. No 256px grid
-- math here — deriving the grid from textureWidth/Height is
-- exactly what shears quirky overlays (e.g. Icepoint's Kaneq'nuun).
if C.appearance.worldmap.mapreveal == "1" then
if showReveal then
for _, tile in ipairs(overlay.tiles) do
local tex = overlayPool:Acquire()
@@ -174,4 +175,15 @@ pfUI:RegisterModule("mapreveal", function ()
pfWorldMapFrame_Update()
end
end)
-- live refresh while the map is open: exploration changes arrive on this
-- event. Zone switches, zoom and (re)opens still come through the hook
-- above, so nothing is redrawn while the map is closed — the next open
-- rebuilds from the current state.
EventRegistry:RegisterFrameEventAndCallback("MAP_EXPLORATION_UPDATED", function()
if WorldMapFrame and WorldMapFrame:IsShown() then
pfWorldMapFrame_Update()
end
end)
end)
+1 -1
View File
@@ -103,7 +103,7 @@ pfUI:RegisterModule("minimap", function ()
this.highlight:SetAllPoints(this)
this.highlight:SetFrameLevel(this:GetFrameLevel() + 1)
this.highlight.tex = this.highlight:CreateTexture("OVERLAY")
this.highlight.tex = this.highlight:CreateTexture(nil, "OVERLAY")
this.highlight.tex:SetTexture(pfUI.media["img:mail"])
this.highlight.tex:SetPoint("TOPLEFT", MiniMapMailIcon, "TOPLEFT", -2, 2)
this.highlight.tex:SetPoint("BOTTOMRIGHT", MiniMapMailIcon, "BOTTOMRIGHT", 2, -2)
+5 -3
View File
@@ -524,9 +524,11 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
end
-- The plates hold their own unit subscriptions now, so silencing this
-- frame alone would leave them dispatching through logout -- exactly what
-- this branch exists to prevent.
-- this branch exists to prevent. Leaving the world destroys the units
-- before PLAYER_LOGOUT fires, and a destroyed plate already dropped its
-- events and lost its methods, so only the live ones need silencing.
for plate in pairs(registry) do
if plate.nameplate then
if plate.nameplate and plate.nameplate.UnregisterAllEvents then
plate.nameplate:UnregisterAllEvents()
plate.nameplate:SetScript("OnEvent", nil)
end
@@ -863,7 +865,7 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
combopoints[i] = CreateFrame("Frame", nil, nameplate)
combopoints[i]:Hide()
combopoints[i]:SetFrameLevel(8)
combopoints[i].tex = combopoints[i]:CreateTexture("OVERLAY")
combopoints[i].tex = combopoints[i]:CreateTexture(nil, "OVERLAY")
combopoints[i].tex:SetAllPoints()
if i < 3 then
+1 -1
View File
@@ -25,7 +25,7 @@ pfUI:RegisterModule("nampower", function ()
pfUI.spellqueue:SetPoint("CENTER", UIParent, "CENTER", 100, -100)
end
pfUI.spellqueue.icon = pfUI.spellqueue:CreateTexture("OVERLAY")
pfUI.spellqueue.icon = pfUI.spellqueue:CreateTexture(nil, "OVERLAY")
pfUI.spellqueue.icon:SetAllPoints(pfUI.spellqueue)
pfUI.spellqueue.icon:SetTexCoord(.08, .92, .08, .92)
+1 -1
View File
@@ -100,7 +100,7 @@ pfUI:RegisterModule("roll", function ()
f.icon:SetWidth(esize)
f.icon:SetHeight(esize)
f.icon.tex = f.icon:CreateTexture("OVERLAY")
f.icon.tex = f.icon:CreateTexture(nil, "OVERLAY")
f.icon.tex:SetTexCoord(.08, .92, .08, .92)
f.icon.tex:SetAllPoints(f.icon)
+1 -1
View File
@@ -30,7 +30,7 @@ pfUI:RegisterModule("tracking", function ()
pfUI.tracking:SetWidth(size)
pfUI.tracking:SetHeight(size)
pfUI.tracking.icon = pfUI.tracking:CreateTexture("BACKGROUND")
pfUI.tracking.icon = pfUI.tracking:CreateTexture(nil, "BACKGROUND")
pfUI.tracking.icon:SetTexCoord(.08, .92, .08, .92)
pfUI.tracking.icon:SetAllPoints(pfUI.tracking)
+2 -2
View File
@@ -28,12 +28,12 @@ pfUI:RegisterSkin("Petition", function ()
local texture = "StationeryTest"
if date("%m%d") == "0223" then texture = "Stationery_Val" end
local tex_Left = PetitionFrame:CreateTexture("BACKGROUND")
local tex_Left = PetitionFrame:CreateTexture(nil, "BACKGROUND")
tex_Left:SetTexture("Interface\\Stationery\\"..texture.."1")
tex_Left:SetPoint("TOPLEFT", 23, -81)
tex_Left:SetWidth(tex_Left:GetWidth() + 10)
tex_Left:SetHeight(330)
local tex_Right = PetitionFrame:CreateTexture("BACKGROUND")
local tex_Right = PetitionFrame:CreateTexture(nil, "BACKGROUND")
tex_Right:SetTexture("Interface\\Stationery\\"..texture.."2")
tex_Right:SetPoint("LEFT", tex_Left, "RIGHT", 0, 0)
tex_Right:SetWidth(tex_Right:GetWidth() + 10)
+7 -6
View File
@@ -35,13 +35,14 @@ pfUI:RegisterSkin("Pet Stable Master", function ()
StripTextures(button)
SkinButton(button, nil, nil, nil, _G[button:GetName().."IconTexture"])
button.Enable = function()
button:SetBackdropColor()
button.locked = false
local b = button
b.Enable = function()
b:SetBackdropColor()
b.locked = false
end
button.Disable = function()
button:SetBackdropColor(1, .1, .1, .3)
button.locked = true
b.Disable = function()
b:SetBackdropColor(1, .1, .1, .3)
b.locked = true
end
end