Files
benilla-octo/crates/benilla-app/assets/ui/BuffFrame.xml
T
SamandClaude Fable 5 820c406049 the interface labels announce their writes, and the idle tick goes quiet
- a text write names its own label, so the per-frame measure sweep drains a short ledger instead of hashing every font string twice — ~0.3 ms a frame at the Stormwind gates, gone
- the idle interface tick stops re-writing settled state: buff durations and the weapon-enchant frame write only on change, and the fade and unit-popup drivers park until woken
- the interface extract skips a hidden region before cloning its row, not after
- WOW_UPLOAD_BUDGET=<MB> brackets the arrival burst's upload pacing — a lever for measuring world entry, not a fix yet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 18:48:12 +07:00

830 lines
49 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui>
<!--
The player buff/debuff bar and the temporary-weapon-enchant row (decisions 0255/0257 — the
player aura arc). benilla's own code against the real 1.12 BuffFrame's contract: the
extracted Interface\FrameXML\BuffFrame.xml/.lua is the SPECIFICATION, and geometry,
textures, colours, every timing constant AND the data path are read off it.
THE INDEX SPACE — the thing to understand before touching a handler. There are TWO numbers
here and they are not the same number:
* a button's `id` is a 0-BASED ORDINAL WITHIN ITS OWN FILTER. BuffButton0..15 are
id="0".."15" under HELPFUL; BuffButton16..23 are id="0".."7" under HARMFUL. It is a
loop counter and nothing else.
* `GetPlayerBuff(id, filter)` turns that ordinal into a CACHE POSITION — the aura's
physical index in the player's display cache — plus `untilCancelled`. **Every** sibling
verb (GetPlayerBuffTexture/…DispelType/…Applications/…TimeLeft, CancelPlayerBuff,
GameTooltip:SetPlayerBuff) consumes that position, never the button's id.
So a button is a two-step: enumerate to a position on the event, then read the position. The
position is ABSOLUTE — the number a "HARMFUL" walk hands back is the number an unfiltered
walk would — which is why a debuff button's handle can never resolve against the buff list,
structurally, with no filter argument threaded through the read path. A miss is `-1`, never
nil, and the accessors all tolerate `-1` directly (byte-verified; the family's signature
table and the traps are `benilla_ui::script::aura`'s module header).
This file used to read the Era `UnitBuff`/`UnitDebuff` pair because the 1.12 verbs did not
exist. They landed in b2ede294, so the adaptation is gone: the handlers below run on the
reference's own verbs and the reference's own numbers.
SCOPE. The LOCAL PLAYER's auras, plus the reference's two temporary-weapon-enchant slots.
0255 left TemporaryEnchantFrame out ("there is no weapon-enchant wire yet"); there is —
`ITEM_FIELD_ENCHANTMENT`'s TEMP slot for the charges and `SMSG_ITEM_ENCHANT_TIME_UPDATE`
for the countdown, both already feeding the item tooltip (decisions 0915/0920) — so
`GetWeaponEnchantInfo` reads them and the row is here. The bar's resting place is
unchanged: right-aligned under the minimap at UIParent TOPRIGHT (-175,-13), which is where
BuffFrame_Enchant_OnUpdate's no-enchant branch parks it. With an enchant equipped the TOP
buff row alone slides left to clear the enchant icons (BuffFrame re-anchors to
TemporaryEnchantFrame's TOPLEFT); rows 2 and 3 hang off TempEnchant1/TemporaryEnchantFrame
and do not move. That is the reference's behaviour, not a choice made here.
DURATIONS ARE A SETTING (decision 1139). 0255 shipped the durations-shown geometry
unconditionally — the director's call, and there was no panel to hang a switch on — and said
in as many words that "if a CVar system later arrives, the geometry is already the branch it
would select". It arrived, as the saved-variable store rather than a CVar (1134): the store
below is SHOW_BUFF_DURATIONS, the Interface page's Buff Durations row drives it, and BOTH of
the reference's geometries are live again — rows at y = 0/−45/−90 with timers, 0/−35/−70
without (BuffButtons_UpdatePositions). Our default is "1", the reference's "0": the
director's look is what the bar comes up wearing, and what Defaults walks it back to.
ORDER IS STATE. The player's list the app pushes (crate::ui_aura, decision 0257) IS the
reference's insertion-ordered display cache: a new aura appends at the end, a dropped one
repacks — NOT ascending slot. That is precisely why a "cache position" needs no structure of
its own on our side; position N of that list is position N of the reference's cache.
DURATION / PERMANENCE is `untilCancelled`, GetPlayerBuff's second return — NOT
"expirationTime is 0", which is what this file tested before the verbs landed. They are
different questions and the difference is visible: untilCancelled is derived from the
SPELL (SpellDuration.dbc's infinite row, plus the area-aura override —
`benilla::ui_aura::until_cancelled`, byte-verified at 0x4e452e-0x4e45c5), so it is right on
the frame an aura appears, BEFORE any duration packet has landed. An aura that really is
timed but whose duration has not arrived yet therefore behaves as the reference does — it
counts from "0 s" and pulses for the frame or two before the stamp joins — instead of
silently showing no timer at all, which is what an expirationTime test cannot tell apart
from a permanent aura.
THE COUNTDOWN IS A POLL, NOT A CACHE. The aura EVENT is the discrete-change edge (icon,
stacks, dispel class); the remaining time is re-read every frame by every visible button.
That is the reference's own division of labour and this file keeps it exactly —
BuffButton_Update caches the buff POSITION on PLAYER_AURAS_CHANGED, BuffButton_OnUpdate
calls GetPlayerBuffTimeLeft with it each frame (ref BuffFrame.lua l.128-130) — and caching
the expiry on the event instead is what made refreshed buffs freeze at "0 s" and freshly
cast ones show no timer (decision 0846).
RIGHT-CLICK CANCEL. RegisterForClicks("RightButtonUp") + CancelPlayerBuff(this.buffIndex).
The server cancels by spell id and refuses anything the wire's cancelable bit didn't allow,
so a debuff right-click is a silent no-op, as in 1.12. `CancelUnitBuff` (the Era name) is
the same verb over the same cache behind the same gate and the same CMSG_CANCEL_AURA queue;
this file simply no longer needs the Era spelling.
BEHAVIOURAL DEPARTURES from the specification (noted, not bugs — everything else observable
is the reference's, however differently this file spells it):
- The count FontString is BLANKED ("") rather than hidden — an empty string draws nothing,
same look. A site that predates region Show/Hide; equivalent either way. (The DURATION
strings used the same shortcut until 1139 and no longer do: a hidden setting has to hide
a region, not write an empty string into a visible one.)
- The dispel-class lookup sits INSIDE the `if (debuffSlot)` guard. The reference runs
`GetPlayerBuffDispelType(GetPlayerBuff(this:GetID(), "HARMFUL"))` for all 24 buttons and
then throws the answer away for the 16 helpful ones, which have no $parentBorder to tint.
Provably invisible (the value's only consumer is that SetVertexColor), and it drops 16
cache walks per aura event.
- The colour is `DebuffTypeColor[debuffType] or DebuffTypeColor["none"]` where the
reference branches on `if (debuffType)`. Same answer for the five known classes; ours also
survives a dispel name the table has no row for, where the reference indexes nil.
- An icon path we cannot resolve falls back to BENILLA_FALLBACK_ICON rather than
SetTexture(nil) — carried forward from 0255, and the same fallback the action bar uses.
The reference has no such art; ours would otherwise draw an invisible button.
- BuffButtons_UpdatePositions is CALLED ONCE AT LOAD (from TemporaryEnchantFrame's OnLoad,
the first moment every frame it names exists). The reference never calls it at load
because its default is "0" and its static XML anchors already ARE the "0" geometry; ours
defaults to "1", so the load-time call is what makes the default honest.
- TempEnchant1/2 ship `hidden="true"`. The reference leaves them shown and lets the first
BuffFrame_Enchant_OnUpdate hide them — one frame of two empty enchant borders at the top
right, which is a flicker worth not having.
- BuffButtonTempEnchant is STANDALONE rather than inheriting BuffButtonTemplate and blanking
its handlers with empty elements — our loader keeps the inherited handler where the
reference clears it. Measured, not assumed; see the template's own note.
- `hasEnchant = 1` (ref BuffFrame.lua l.185/208) is dropped: a leaked local that becomes a
global, is never cleared, and is read by nothing.
- GameTooltip is nil-guarded. Engine tests load file subsets without GameTooltip.xml.
-->
<Script><![CDATA[
-- The timer switch (ref uvar, UIOptionsFrame.lua l.21/104 — its `default = "0"`). This
-- assignment is the ONLY statement of our default: the saved chunk executes over it at load
-- (1128) and the Interface page's row captures it as its Defaults value at OnLoad, which
-- runs first. "1" because the director asked for visible timers (0255) — the divergence is
-- written down once, here, and Defaults honours it rather than the reference's table.
SHOW_BUFF_DURATIONS = "1"
RegisterForSave("SHOW_BUFF_DURATIONS")
-- The reference's flash cadence + thresholds (BuffFrame.lua l.1-14). An aura in its last 31s
-- pulses between full and BUFF_MIN_ALPHA; its duration text turns highlight-white under 60s.
BUFF_FLASH_TIME_ON = 0.75
BUFF_FLASH_TIME_OFF = 0.75
BUFF_MIN_ALPHA = 0.3
BUFF_WARNING_TIME = 31
BUFF_DURATION_WARNING_TIME = 60
-- The dispel-class border tints (BuffFrame.lua l.9-14); "none" is the undispellable red the
-- debuff overlay wears by default. GetPlayerBuffDispelType's return keys this table — it
-- hands back the SpellDispelType.dbc name, or nil, never "" and never "none".
DebuffTypeColor = {
["none"] = { r = 0.80, g = 0.00, b = 0.00 },
["Magic"] = { r = 0.20, g = 0.60, b = 1.00 },
["Curse"] = { r = 0.60, g = 0.00, b = 1.00 },
["Disease"] = { r = 0.60, g = 0.40, b = 0.00 },
["Poison"] = { r = 0.00, g = 0.60, b = 0.00 },
}
-- SecondsToTimeAbbrev + its GlobalStrings (UIParent.lua l.1034 / GlobalStrings.lua). We load
-- no UIParent/GlobalStrings, so define them here (guarded, in case a later file ships them).
DAY_ONELETTER_ABBR = DAY_ONELETTER_ABBR or "%d d"
HOUR_ONELETTER_ABBR = HOUR_ONELETTER_ABBR or "%d h"
MINUTE_ONELETTER_ABBR = MINUTE_ONELETTER_ABBR or "%d m"
SECOND_ONELETTER_ABBR = SECOND_ONELETTER_ABBR or "%d s"
if not SecondsToTimeAbbrev then
function SecondsToTimeAbbrev(seconds)
if seconds > 86400 then
return format(DAY_ONELETTER_ABBR, ceil(seconds / 86400))
elseif seconds > 3600 then
return format(HOUR_ONELETTER_ABBR, ceil(seconds / 3600))
elseif seconds > 60 then
return format(MINUTE_ONELETTER_ABBR, ceil(seconds / 60))
end
return format(SECOND_ONELETTER_ABBR, seconds)
end
end
-- The frame-wide flash pulse (BuffFrame_OnLoad / _OnUpdate, BuffFrame.lua l.17-56). One
-- BUFF_ALPHA_VALUE is computed per frame and every low-on-time button reads it, so they blink
-- in unison. The bar also seats each duration string under its button here.
function BuffFrame_OnLoad()
BuffFrameUpdateTime = 0
BuffFrameFlashTime = 0
BuffFrameFlashState = 1
BUFF_ALPHA_VALUE = 1
for i = 0, 23 do
getglobal("BuffButton" .. i .. "Duration"):SetPoint("TOP", "BuffButton" .. i, "BOTTOM", 0, 0)
end
end
-- Half of the square wave, run down by one frame. BuffFrameFlashTime is the time LEFT in
-- the current half; when it goes under, the state flips and the counter reloads with the
-- new half's length. The overshoot is carried into that half — unless it is longer than
-- the half itself, in which case the whole period is kept rather than skipped, so a
-- stalled frame costs at most one beat of drift instead of racing the pulse forward.
local function buffFlashAdvance(elapsed)
BuffFrameFlashTime = BuffFrameFlashTime - elapsed
if BuffFrameFlashTime >= 0 then
return
end
local overtime = -BuffFrameFlashTime
if BuffFrameFlashState == 0 then
BuffFrameFlashState = 1
BuffFrameFlashTime = BUFF_FLASH_TIME_ON
else
BuffFrameFlashState = 0
BuffFrameFlashTime = BUFF_FLASH_TIME_OFF
end
if overtime < BuffFrameFlashTime then
BuffFrameFlashTime = BuffFrameFlashTime - overtime
end
end
function BuffFrame_OnUpdate(elapsed)
-- The tooltip re-poll's throttle (ref l.29-33): oscillates around zero with a
-- TOOLTIP_UPDATE_TIME period, and BuffButton_OnUpdate refreshes a hovered plate only on
-- the frames it is non-positive. ActionBar.xml declares TOOLTIP_UPDATE_TIME and loads
-- first (benilla.toc), so it is always a number by the time this runs.
if BuffFrameUpdateTime > 0 then
BuffFrameUpdateTime = BuffFrameUpdateTime - elapsed
else
BuffFrameUpdateTime = BuffFrameUpdateTime + TOOLTIP_UPDATE_TIME
end
-- The pulse's level for this frame: a 0..1 ramp up across the ON half and back down
-- across the OFF half (both scaled by BUFF_FLASH_TIME_ON — the reference's own choice,
-- and invisible while the two halves are the same length), squashed into
-- [BUFF_MIN_ALPHA, 1] so a warning icon dims rather than disappears.
--
-- Computed into a LOCAL and published once. BUFF_ALPHA_VALUE is read by every visible
-- button, so it is not scratch space: the reference assigns the raw ramp to it and then
-- squashes it in a second statement, which leaves the global holding an un-squashed
-- value for the length of one statement.
buffFlashAdvance(elapsed)
local level
if BuffFrameFlashState == 1 then
level = (BUFF_FLASH_TIME_ON - BuffFrameFlashTime) / BUFF_FLASH_TIME_ON
else
level = BuffFrameFlashTime / BUFF_FLASH_TIME_ON
end
BUFF_ALPHA_VALUE = (level * (1 - BUFF_MIN_ALPHA)) + BUFF_MIN_ALPHA
end
-- BuffButtons_UpdatePositions (BuffFrame.lua l.152-160): the ROW PITCH depends on whether a
-- timer sits under each icon. Shown, the three rows land at y = 0/−45/−90 (a 30px icon + a
-- 15px gutter); hidden, at 0/−35/−70 — the icons close up to the same 5px the columns use.
-- The anchors are the reference's own, through the TempEnchant chain: row 2 hangs off
-- TempEnchant1 and row 3 off TemporaryEnchantFrame, which is what keeps them still while the
-- top row slides left to clear an equipped enchant. SetPoint on a point already held
-- overwrites it (layout.rs — the client's own SetPoint law).
--
-- This is the Buff Durations row's `applyFunc`, and the ONLY thing the setting needs applied:
-- the timer text itself is re-decided every frame by the poll below, so a click changes what
-- the next frame draws without anyone telling it. Geometry is the part that is stated once.
function BuffButtons_UpdatePositions()
if SHOW_BUFF_DURATIONS == "1" then
BuffButton8:SetPoint("TOP", "TempEnchant1", "BOTTOM", 0, -15)
BuffButton16:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, -90)
else
BuffButton8:SetPoint("TOP", "TempEnchant1", "BOTTOM", 0, -5)
BuffButton16:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, -70)
end
end
-- Void a button's memory of what its duration region last had WRITTEN to it (the
-- benillaDur* fields BuffFrame_UpdateDuration gates on). Called by everything ELSE that
-- touches the region — the aura event's repaint here, the enchant row's clear — so the
-- per-frame gate can never skip a write against a region somebody moved under it; a
-- forgotten state just means the next poll writes once.
local function buffDurationForget(button)
button.benillaDurShown = nil
button.benillaDurText = nil
button.benillaDurWarn = nil
button.benillaDurSetting = nil
end
-- The button's aura, resolved on the discrete-change edge and CACHED AS A POSITION
-- (ref BuffButton_Update, l.58-107). `this:GetID()` is the 0-based ordinal within
-- this.buffFilter; buffIndex is the cache position every read below uses.
function BuffButton_Update()
local buffIndex, untilCancelled = GetPlayerBuff(this:GetID(), this.buffFilter)
this.buffIndex = buffIndex
this.untilCancelled = untilCancelled
local buffDuration = this.durationFS or getglobal(this:GetName() .. "Duration")
this.durationFS = buffDuration
-- -1, not nil: the miss sentinel the whole family agrees on.
if buffIndex < 0 then
this:Hide()
buffDuration:Hide()
buffDurationForget(this)
return
end
this:SetAlpha(1.0)
this.benillaLastAlpha = 1.0
this:Show()
if SHOW_BUFF_DURATIONS == "1" then
buffDuration:Show()
else
buffDuration:Hide()
end
buffDurationForget(this)
getglobal(this:GetName() .. "Icon"):SetTexture(GetPlayerBuffTexture(buffIndex) or BENILLA_FALLBACK_ICON)
-- Debuff border, tinted by dispel class (harmful buttons only — helpful ones have no
-- $parentBorder, so getglobal is nil and the whole lookup is skipped; see the header).
local debuffSlot = getglobal(this:GetName() .. "Border")
if debuffSlot then
local debuffType = GetPlayerBuffDispelType(GetPlayerBuff(this:GetID(), "HARMFUL"))
local color = DebuffTypeColor[debuffType] or DebuffTypeColor["none"]
debuffSlot:SetVertexColor(color.r, color.g, color.b)
end
-- Stack count, shown only above 1 (ref l.94-102). GetPlayerBuffApplications is a NUMBER
-- on every path — 1 for an absent position — so `> 1` needs no nil guard.
local buffCount = getglobal(this:GetName() .. "Count")
local count = GetPlayerBuffApplications(buffIndex)
if count > 1 then
buffCount:SetText(count)
else
buffCount:SetText("")
end
if GameTooltip and GameTooltip:IsOwned(this) then
GameTooltip:SetPlayerBuff(buffIndex)
end
end
-- A button takes right-clicks and listens for the aura rebuild. The first paint hides it
-- until the feed reports an aura. PLAYER_AURAS_CHANGED is the reference's event and the app
-- fires it beside the Era-shaped UNIT_AURA on the same rebuild (`ui_aura`).
function BuffButton_OnLoad()
-- Valid tokens for "buffFilter": HELPFUL, HARMFUL, CANCELABLE, NOT_CANCELABLE. Note the
-- mask starts at ZERO once a filter string is given, so a bare "CANCELABLE" would match
-- nothing — the sign must always be named.
BuffButton_Update()
this:RegisterForClicks("RightButtonUp")
this:RegisterEvent("PLAYER_AURAS_CHANGED")
end
function BuffButton_OnEvent(event)
if event == "PLAYER_AURAS_CHANGED" then
BuffButton_Update()
end
end
-- Per-frame: RE-READ the remaining time from the cached POSITION (never a cached duration),
-- flash in the last 31s, write the abbreviated timer, and tick a hovered tooltip.
--
-- The re-read every frame is the reference's own shape, and it is load-bearing: a duration
-- that changes without any discrete change — a recast, a DoT reapplied, cast pushback, the
-- server's replay of every duration at world entry — fires no aura event, so a button that
-- cached its expiry on the event would keep counting the OLD one down, sit at "0 s" while
-- the buff is still live, and never show a timer for an aura whose duration landed after
-- its icon did. Both were live defects; the poll is what makes them unrepresentable
-- (decision 0846).
--
-- The untilCancelled early-out (ref l.124-127) is what keeps a permanent aura from flashing
-- a "0 s" countdown while waiting for a duration packet that is never coming: it is answered
-- from the SPELL, so it is right before any packet arrives. This frame runs only on visible
-- frames (`UiScript::tick`), exactly as the client's does, so a hidden button's stale
-- buffIndex is never read.
function BuffButton_OnUpdate()
local button = this
-- A permanent aura has nothing to count down and nothing to flash toward, so it is
-- done in one line: UpdateDuration with no time left is exactly "hide the timer",
-- which is the whole of the reference's early-out here. (The reference reaches for the
-- $parentDuration region itself to do it, on every frame of every visible button, and
-- then throws that region away and lets UpdateDuration look it up again.)
if button.untilCancelled == 1 then
BuffFrame_UpdateDuration(button, nil)
return
end
local buffIndex = button.buffIndex
local timeLeft = GetPlayerBuffTimeLeft(buffIndex)
-- The flash is NOT gated on the timer setting: the reference pulses a button in its last
-- 31s whether or not a number is drawn under it, and with the timers off it is the only
-- thing left saying "about to drop".
--
-- Written only when it moves (decision 1396's class — a settled bar must not rewrite
-- per frame what changes only on an edge). Inside the warning window BUFF_ALPHA_VALUE
-- is a fresh ramp value every frame, so the pulse flows through the compare untouched;
-- outside it the value is the 1.0 that is already there, forever.
local alpha
if timeLeft < BUFF_WARNING_TIME then
alpha = BUFF_ALPHA_VALUE
else
alpha = 1.0
end
if alpha ~= button.benillaLastAlpha then
button.benillaLastAlpha = alpha
button:SetAlpha(alpha)
end
BuffFrame_UpdateDuration(button, timeLeft)
-- A tooltip we own is re-read on the throttle's beat, not every frame.
if BuffFrameUpdateTime <= 0 and GameTooltip and GameTooltip:IsOwned(button) then
GameTooltip:SetPlayerBuff(buffIndex)
end
end
-- Right-click cancels by CACHE POSITION (ref l.148-150). The app sends CMSG_CANCEL_AURA for
-- a cancelable aura and drops the rest, so a debuff right-click is a silent no-op.
function BuffButton_OnClick()
CancelPlayerBuff(this.buffIndex)
end
-- Clear enchant slots `first`..2 — the tail of the row nothing is using this frame. The
-- Hide moves the Duration region under the poll's write gate, so the gate's memory is
-- voided with it.
local function buffEnchantClear(first)
for slot = first, 2 do
local button = getglobal("TempEnchant" .. slot)
button:Hide()
getglobal("TempEnchant" .. slot .. "Duration"):Hide()
buffDurationForget(button)
end
end
-- One hand's six steps (the same block the reference spells out twice, ref l.180-207):
-- an enchanted hand packs into the next slot rightwards and keeps its countdown and
-- warning pulse live; a bare hand packs nothing. Returns the new fill count. A plain
-- function of its arguments so the per-frame path allocates nothing — this used to be a
-- two-row table built fresh every frame.
--
-- `slot` is the live API's inventory id, which is also what the button's hover reads
-- back off itself to ask for the ITEM's tooltip.
local function buffEnchantHand(filled, enchanted, expiration, slot)
if not enchanted then
return filled
end
filled = filled + 1
local button = getglobal("TempEnchant" .. filled)
button:SetID(slot)
getglobal(button:GetName() .. "Icon"):SetTexture(
GetInventoryItemTexture("player", slot) or BENILLA_FALLBACK_ICON)
button:Show()
-- The wire counts an enchant down in MILLISECONDS; everything the row draws is
-- in seconds. A nil expiration is "no countdown known", which the timer and the
-- warning flash both read as "nothing to warn about".
local timeLeft = expiration and (expiration / 1000)
BuffFrame_UpdateDuration(button, timeLeft)
if timeLeft and timeLeft < BUFF_WARNING_TIME then
button:SetAlpha(BUFF_ALPHA_VALUE)
else
button:SetAlpha(1.0)
end
return filled
end
-- The temporary-weapon-enchant row (ref l.162-233). GetWeaponEnchantInfo is the whole data
-- path: two flags, two remaining-MILLISECOND counts, two charge counts (the charges are
-- read by nobody here, and are named only to spell the API's shape out).
--
-- OFF HAND FIRST is the part that matters: the row packs from slot 1 rightwards, so with
-- both weapons enchanted the main hand lands in slot 2 and therefore sits to the LEFT of
-- the off hand.
function BuffFrame_Enchant_OnUpdate(elapsed)
local hasMainHandEnchant, mainHandExpiration, mainHandCharges,
hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo()
-- Nothing enchanted: clear the whole row and park BuffFrame back on its resting
-- point — ONCE per transition, latched on benillaCleared (decision 1396's class:
-- re-deriving per frame what changes only on an edge). The latch starts unset, so
-- the FIRST pass still writes — the load-time hide the header's TempEnchant note
-- leans on — and any enchant appearing clears it below.
if not hasMainHandEnchant and not hasOffHandEnchant then
if not TemporaryEnchantFrame.benillaCleared then
TemporaryEnchantFrame.benillaCleared = true
-- Forget the last row width too: after a park, the next enchanted pass must
-- re-write the width AND the bar's TOPLEFT anchor whatever its fill count is.
TemporaryEnchantFrame.benillaFilled = nil
buffEnchantClear(1)
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, 0)
end
return
end
TemporaryEnchantFrame.benillaCleared = nil
local filled = buffEnchantHand(0, hasOffHandEnchant, offHandExpiration, 17)
filled = buffEnchantHand(filled, hasMainHandEnchant, mainHandExpiration, 16)
buffEnchantClear(filled + 1)
-- Position buff frame: the row is `filled` icons wide, and the bar starts one 5px
-- gutter to its left. The geometry depends on nothing but `filled`, so it is written
-- only when that changes.
if TemporaryEnchantFrame.benillaFilled ~= filled then
TemporaryEnchantFrame.benillaFilled = filled
TemporaryEnchantFrame:SetWidth(filled * 32)
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPLEFT", -5, 0)
end
end
function BuffFrame_EnchantButton_OnUpdate()
if GameTooltip and GameTooltip:IsOwned(this) then
BuffFrame_EnchantButton_OnEnter()
end
end
-- The enchant hover is the ITEM's tooltip, not an aura's — the plate hangs below-left like
-- the buff buttons' (ref l.242-245).
function BuffFrame_EnchantButton_OnEnter()
if not GameTooltip then return end
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
GameTooltip:SetInventoryItem("player", this:GetID())
end
-- BuffFrame_UpdateDuration (ref l.247-260), gate and all: the timer draws only when the
-- setting is on AND there is a live one. `timeLeft` nil is the enchant row's "no countdown
-- known" case. Hidden, not blanked — see the header's deviation note.
--
-- The POLL stays per-frame (the re-read is load-bearing — decision 0846); only the WRITES
-- are gated, on what was last written: shown, the abbreviated text, the warning band, and
-- the SHOW_BUFF_DURATIONS value itself (so the settings toggle stays live). Deliberately
-- never on a measured extent (1396's trap: a fixture with no font engine measures 0
-- forever); the compare is against the STRING this function computed, nothing the host
-- had to shape. The benillaDur* memory is voided by whoever else touches the region
-- (buffDurationForget), so a stale skip is unrepresentable.
function BuffFrame_UpdateDuration(buffButton, timeLeft)
local duration = buffButton.durationFS or getglobal(buffButton:GetName() .. "Duration")
buffButton.durationFS = duration
local setting = SHOW_BUFF_DURATIONS
-- Two ways to have no number to draw: the setting is off, or nothing has told us how
-- long is left (the enchant row's "no countdown known").
if setting ~= "1" or not timeLeft then
-- == false, not falsey: nil is "no memory", and the first pass must write.
if buffButton.benillaDurShown == false and buffButton.benillaDurSetting == setting then
return
end
buffButton.benillaDurShown = false
buffButton.benillaDurText = nil
buffButton.benillaDurWarn = nil
buffButton.benillaDurSetting = setting
duration:Hide()
return
end
local text = SecondsToTimeAbbrev(timeLeft)
-- Gold most of the way, white inside the last minute — the number's own warning, a
-- half-minute ahead of the icon starting to pulse.
local warning = timeLeft < BUFF_DURATION_WARNING_TIME
if buffButton.benillaDurShown == true and buffButton.benillaDurText == text
and buffButton.benillaDurWarn == warning and buffButton.benillaDurSetting == setting then
return
end
buffButton.benillaDurShown = true
buffButton.benillaDurText = text
buffButton.benillaDurWarn = warning
buffButton.benillaDurSetting = setting
duration:SetText(text)
local color = NORMAL_FONT_COLOR
if warning then
color = HIGHLIGHT_FONT_COLOR
end
duration:SetVertexColor(color.r, color.g, color.b)
duration:Show()
end
]]></Script>
<!-- 30x30 icon + a bottom-right stack count (ref BuffButtonTemplate, BuffFrame.xml l.4-44).
The base template's own filter is the reference's HELPFUL|HARMFUL; the two concrete
variants below pin one sign each. -->
<Button name="BuffButtonTemplate" virtual="true">
<Size><AbsDimension x="30" y="30"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon"/>
<FontString name="$parentCount" inherits="NumberFontNormal">
<Anchors>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
this.buffFilter = "HELPFUL|HARMFUL"
BuffButton_OnLoad()
</OnLoad>
<OnEvent>BuffButton_OnEvent(event)</OnEvent>
<OnUpdate>BuffButton_OnUpdate()</OnUpdate>
<OnClick>BuffButton_OnClick()</OnClick>
<OnEnter>
if GameTooltip then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
GameTooltip:SetPlayerBuff(this.buffIndex)
end
</OnEnter>
<OnLeave>
if GameTooltip then GameTooltip:Hide() end
</OnLeave>
</Scripts>
</Button>
<!-- Helpful variant: filter HELPFUL, no border (ref l.50-57). -->
<Button name="BuffButtonHelpful" inherits="BuffButtonTemplate" virtual="true">
<Scripts>
<OnLoad>
this.buffFilter = "HELPFUL"
BuffButton_OnLoad()
</OnLoad>
</Scripts>
</Button>
<!-- Harmful variant: filter HARMFUL + the dispel-tinted border overlay (ref l.58-82).
33x32 UI-Debuff-Overlays, the exact reference TexCoords, tint set at runtime. -->
<Button name="BuffButtonHarmful" inherits="BuffButtonTemplate" virtual="true">
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentBorder" file="Interface\Buttons\UI-Debuff-Overlays">
<Size><AbsDimension x="33" y="32"/></Size>
<Anchors>
<Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.296875" right="0.5703125" top="0" bottom="0.515625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
this.buffFilter = "HARMFUL"
BuffButton_OnLoad()
</OnLoad>
</Scripts>
</Button>
<!-- Temp-enchant slot: the same 30x30 icon under a 32x32 UI-TempEnchant-Border (ref l.83-117).
It carries no filter and no buffIndex — its data is GetWeaponEnchantInfo and its hover is
the ITEM's tooltip.
**STANDALONE, where the reference inherits BuffButtonTemplate and then blanks the four aura
handlers with empty `<OnLoad></OnLoad>` / `<OnEvent>` / `<OnClick>` elements.** Our loader
does not implement that idiom: `Loader::compile_handler` returns None for an empty body, so
`apply_scripts` skips the SetScript entirely and the INHERITED handler survives. Written
the reference's way and MEASURED, TempEnchant1 ran `BuffButton_OnLoad`, registered
PLAYER_AURAS_CHANGED, and painted the player's first buff in the corner — and a right-click
on it would have cancelled that aura. Writing the three regions out removes the dependence
on a loader semantic we have not byte-verified; the engine gap is real and worth closing on
its own evidence, not silently here. -->
<Button name="BuffButtonTempEnchant" virtual="true">
<Size><AbsDimension x="30" y="30"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon"/>
<FontString name="$parentCount" inherits="NumberFontNormal">
<Anchors>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentBorder" file="Interface\Buttons\UI-TempEnchant-Border">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnUpdate>BuffFrame_EnchantButton_OnUpdate()</OnUpdate>
<OnEnter>BuffFrame_EnchantButton_OnEnter()</OnEnter>
<OnLeave>
if GameTooltip then GameTooltip:Hide() end
</OnLeave>
</Scripts>
</Button>
<!-- The duration text under a button: the ref routes these through a virtual FontString
template (BuffButtonDurationTemplate, l.45-49 — GameFontNormalSmall + a bare TOP anchor).
Region templates DO exist in the loader now (Loader::expand_region), but chaining to a FONT
OBJECT through one does not: `framexml::merge` lets the instance's own `inherits=` win, so
the spliced element still reads `inherits="BuffButtonDurationTemplate"` and
`apply_fontstring_font` looks that up in the FONT registry — a separate namespace — and
misses. Each instance therefore names GameFontNormalSmall directly; the anchor is re-seated
by BuffFrame_OnLoad / TemporaryEnchantFrame's OnLoad anyway. Same resolved font, no
indirection. -->
<!--
The bar. Right-aligned under the minimap (UIParent TOPRIGHT -175,-13 — the reference's XML
says -205 and lets the first Enchant_OnUpdate snap it here; ours starts where it rests, so
the bar is never one frame out of place). 16 helpful buttons in two rows of 8 (ids 0-15),
then 8 harmful (ids 0-7). The 24 duration strings live in the frame's ARTWORK layer (over the
icons' BACKGROUND), named to pair with their button. BuffButton8's and BuffButton16's static
anchors here are the no-timer geometry; BuffButtons_UpdatePositions re-seats both at load.
-->
<Frame name="BuffFrame" parent="UIParent" frameStrata="LOW" toplevel="true">
<Size><AbsDimension x="50" y="50"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="UIParent" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="-175" y="-13"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="BuffButton0Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton1Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton2Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton3Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton4Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton5Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton6Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton7Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton8Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton9Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton10Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton11Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton12Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton13Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton14Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton15Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton16Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton17Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton18Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton19Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton20Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton21Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton22Duration" inherits="GameFontNormalSmall"/>
<FontString name="BuffButton23Duration" inherits="GameFontNormalSmall"/>
</Layer>
</Layers>
<Frames>
<!-- Row 0: helpful ordinals 0-7, right to left. -->
<Button name="BuffButton0" inherits="BuffButtonHelpful" id="0">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton1" inherits="BuffButtonHelpful" id="1">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton0" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton2" inherits="BuffButtonHelpful" id="2">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton1" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton3" inherits="BuffButtonHelpful" id="3">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton2" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton4" inherits="BuffButtonHelpful" id="4">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton3" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton5" inherits="BuffButtonHelpful" id="5">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton4" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton6" inherits="BuffButtonHelpful" id="6">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton5" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton7" inherits="BuffButtonHelpful" id="7">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton6" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<!-- Row 1: helpful ordinals 8-15. Re-anchored to TempEnchant1 by BuffButtons_UpdatePositions. -->
<Button name="BuffButton8" inherits="BuffButtonHelpful" id="8">
<Anchors><Anchor point="TOP" relativeTo="BuffButton0" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton9" inherits="BuffButtonHelpful" id="9">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton8" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton10" inherits="BuffButtonHelpful" id="10">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton9" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton11" inherits="BuffButtonHelpful" id="11">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton10" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton12" inherits="BuffButtonHelpful" id="12">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton11" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton13" inherits="BuffButtonHelpful" id="13">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton12" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton14" inherits="BuffButtonHelpful" id="14">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton13" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton15" inherits="BuffButtonHelpful" id="15">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton14" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<!-- Row 2: harmful ordinals 0-7. Re-anchored to TemporaryEnchantFrame by
BuffButtons_UpdatePositions. (The reference's static anchor here names
relativePoint="BOTTOM" against a TOPRIGHT point — a typo it never notices because
UpdatePositions overwrites it; BOTTOMRIGHT is what it meant.) -->
<Button name="BuffButton16" inherits="BuffButtonHarmful" id="0">
<Anchors><Anchor point="TOPRIGHT" relativeTo="BuffButton8" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton17" inherits="BuffButtonHarmful" id="1">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton16" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton18" inherits="BuffButtonHarmful" id="2">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton17" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton19" inherits="BuffButtonHarmful" id="3">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton18" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton20" inherits="BuffButtonHarmful" id="4">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton19" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton21" inherits="BuffButtonHarmful" id="5">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton20" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton22" inherits="BuffButtonHarmful" id="6">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton21" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="BuffButton23" inherits="BuffButtonHarmful" id="7">
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton22" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>BuffFrame_OnLoad()</OnLoad>
<OnUpdate>BuffFrame_OnUpdate(arg1)</OnUpdate>
</Scripts>
</Frame>
<!-- The temporary-weapon-enchant row (ref BuffFrame.xml l.385-431). It owns the top-right
corner; BuffFrame hangs off it, which is why the bar's own anchor above is this frame's
point. Two slots, right to left, so with both weapons enchanted the main hand sits left of
the off hand.
Declared AFTER BuffFrame so that by the time this OnLoad runs every name
BuffButtons_UpdatePositions touches exists — the reference gets away with never calling it
at load because its default is the geometry its static anchors already hold; ours is not. -->
<Frame name="TemporaryEnchantFrame" parent="UIParent" frameStrata="LOW" toplevel="true">
<Size><AbsDimension x="36" y="36"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="UIParent" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="-175" y="-13"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString name="TempEnchant1Duration" inherits="GameFontNormalSmall"/>
<FontString name="TempEnchant2Duration" inherits="GameFontNormalSmall"/>
</Layer>
</Layers>
<Frames>
<Button name="TempEnchant1" inherits="BuffButtonTempEnchant" hidden="true">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="TempEnchant2" inherits="BuffButtonTempEnchant" hidden="true">
<Anchors><Anchor point="RIGHT" relativeTo="TempEnchant1" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>
TempEnchant1Duration:SetPoint("TOP", "TempEnchant1", "BOTTOM", 0, 0)
TempEnchant2Duration:SetPoint("TOP", "TempEnchant2", "BOTTOM", 0, 0)
BuffButtons_UpdatePositions()
</OnLoad>
<OnUpdate>BuffFrame_Enchant_OnUpdate(arg1)</OnUpdate>
</Scripts>
</Frame>
</Ui>