the addon arc: the AddOns folder opens

- benilla loads real 1.12 addons from Interface/AddOns — TOC, XML and Lua through one loader, with per-addon saved variables, Bindings.xml and the lifecycle events addons expect
- both AddOns screens are in: the character-select list with per-addon enable state, and the ESC menu's in-game list
- the API surface is measured, not guessed — a corpus harness runs hundreds of real 1.12 addons and ranks exactly which globals, templates and verbs they demand
- the Lua dialect is the client's own, read off the binary — 5.0 semantics with the 1.12 additions, and UI source files are bytes, not UTF-8

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-11 18:18:31 +07:00
co-authored by Claude Fable 5
parent bfb3e3addc
commit 44ff435304
231 changed files with 31316 additions and 5594 deletions
+154 -145
View File
@@ -8,13 +8,13 @@
ACTIONBAR_SLOT_CHANGED repaint, HasAction/GetActionTexture in, UseAction out.
Structure mirrors the real hierarchy so the draw order falls out of the frame tree:
BenillaActionBar (1024x53, BOTTOM of screen)
├─ BenillaExpBar (StatusBar, TOP) — drawn first ⇒ lowest
│ ├─ BenillaExhaustionLevelFillBar (BORDER) — the pale rested wash up to the tick
│ └─ BenillaExpBarOverlayFrame (DIALOG) — the hover "curr / max" numerals
├─ BenillaActionBarArtFrame (fills the bar) — dwarf strips + end caps over the XP bar
MainMenuBar (1024x53, BOTTOM of screen)
├─ MainMenuExpBar (StatusBar, TOP) — drawn first ⇒ lowest
│ ├─ ExhaustionLevelFillBar (BORDER) — the pale rested wash up to the tick
│ └─ MainMenuBarOverlayFrame (DIALOG) — the hover "curr / max" numerals
├─ MainMenuBarArtFrame (fills the bar) — dwarf strips + end caps over the XP bar
│ └─ 12 buttons + page up/down — child frames ⇒ over the art
└─ BenillaExhaustionTick (DIALOG) — the rested-boundary marker on the strip
└─ ExhaustionTick (DIALOG) — the rested-boundary marker on the strip
(The real client gets the same order via RaiseFrameLevel(MainMenuBarArtFrame); our loader's
later-sibling-is-higher + child-over-parent rules reproduce it without the explicit raise.)
@@ -47,12 +47,12 @@
- Exhaustion tick + rest fill, XP text (hover/cvar-gated), MaxLevelBar: OUT. The bar's
right-hand cluster is otherwise complete — the backpack toggle + four bag slots live in
BagFrame.xml and the eight micro buttons in MicroMenu.xml (both anchored into
BenillaActionBarArtFrame and seated over the art by BenillaActionBarArt_SeatAbove below),
MainMenuBarArtFrame and seated over the art by BenillaActionBarArt_SeatAbove below),
and the performance meter is at the foot of this file. The KEYRING landed with decision
0765: its button and container live in BagFrame.xml (the ref's own split —
MainMenuBarBagButtons.xml / ContainerFrame.lua), and the bar's half of it — swapping the
two right-hand strips to the keyring art and sliding the performance meter over to make
room — is BenillaMainMenuBar_UpdateKeyRing below, exactly where the ref keeps it
room — is MainMenuBar_UpdateKeyRing below, exactly where the ref keeps it
(MainMenuBar.lua l.174-183).
-->
<Script><![CDATA[
@@ -126,7 +126,7 @@
-- explicit level is the reliable lever.) Lives here, with the frame it's about, because two
-- files now need it — it began as BagFrame.xml's private BenillaBagBar_SeatAboveActionBar.
function BenillaActionBarArt_SeatAbove(frame)
local art = getglobal("BenillaActionBarArtFrame")
local art = getglobal("MainMenuBarArtFrame")
if art then frame:SetFrameLevel(art:GetFrameLevel() + 1) end
end
@@ -138,7 +138,7 @@
-- screen size and anchors, so only the file + TexCoords change (UI-MainMenuBar-KeyRing is
-- 256x128 and holds exactly the two bands the ref names: 0.3359375 * 128 = 43px each).
--
-- Called by BenillaKeyRingButton's own OnEvent (BagFrame.xml). Everything is nil-guarded:
-- Called by KeyRingButton's own OnEvent (BagFrame.xml). Everything is nil-guarded:
-- this file loads without BagFrame.xml in several harnesses, and BagFrame.xml loads without
-- this one in ~25 bag/bank/escape/merchant tests — either way the missing half no-ops.
--
@@ -150,17 +150,17 @@
-- unless you destroy your last key, which then un-shows the keyring rather than leaving an
-- empty one behind. The 60s pulse is out, consistently with decision 0304's "no button
-- pulse" for the micro buttons.
function BenillaMainMenuBar_UpdateKeyRing()
function MainMenuBar_UpdateKeyRing()
local show = HasKey and HasKey()
-- The two right-hand strips (BenillaActionBarTexture2 at x=+128, Texture3 at x=+384),
-- The two right-hand strips (MainMenuBarTexture2 at x=+128, Texture3 at x=+384),
-- keyring plate or the ordinary dwarf plate. The dwarf coords are this file's own
-- authored ones (see the strips below) — restated here because this is the only code
-- that ever changes them back.
local strips = {
{ name = "BenillaActionBarTexture2",
{ name = "MainMenuBarTexture2",
keyring = 0.6640625, keyringBottom = 1.0,
dwarf = 0.33203125, dwarfBottom = 0.5 },
{ name = "BenillaActionBarTexture3",
{ name = "MainMenuBarTexture3",
keyring = 0.1640625, keyringBottom = 0.5,
dwarf = 0.08203125, dwarfBottom = 0.25 },
}
@@ -178,12 +178,12 @@
end
-- The performance meter's recess moves with the plate: -227 without the keyring,
-- -235 with it (ref l.180). Its own comment at the foot of this file has the geometry.
local perf = getglobal("BenillaPerformanceBarFrame")
local bar = getglobal("BenillaActionBar")
local perf = getglobal("MainMenuBarPerformanceBarFrame")
local bar = getglobal("MainMenuBar")
if perf and bar then
perf:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", (show and -235) or -227, -10)
end
local button = getglobal("BenillaKeyRingButton")
local button = getglobal("KeyRingButton")
if button then
if show then button:Show() else button:Hide() end
end
@@ -307,20 +307,25 @@
-- mouse-click fork above, a keybind never places (decision 0216 §7: the ref itself never
-- routes ActionButtonUp through OnClick, so this no longer goes through Click() either —
-- the two entry points share the id formula, not the click path). The ref's
-- BonusActionBar fork and the SELFACTIONBUTTON onSelf argument are not modeled (no bonus
-- bar frame, no self-cast modifier binding).
function BenillaActionButtonDown(index)
local button = getglobal("BenillaActionButton" .. index)
-- BonusActionBar fork is not modeled (no bonus bar frame).
--
-- SIGNATURES ARE THE 1.12 ONES (decision 1199): `ActionButtonDown(id)` /
-- `ActionButtonUp(id, onSelf)`, the shapes an addon calls. These two never took a frame —
-- they take the bar INDEX in both clients — so nothing here changes but the names and the
-- restored `onSelf`, which rides through to UseAction's third argument exactly as the ref's
-- l.56 does (our binding accepts and ignores it — `script/action.rs`).
function ActionButtonDown(id)
local button = getglobal("ActionButton" .. id)
if ( button:GetButtonState() == "NORMAL" ) then
button:SetButtonState("PUSHED")
end
end
function BenillaActionButtonUp(index)
local button = getglobal("BenillaActionButton" .. index)
function ActionButtonUp(id, onSelf)
local button = getglobal("ActionButton" .. id)
if ( button:GetButtonState() == "PUSHED" ) then
button:SetButtonState("NORMAL")
UseAction(BenillaActionButton_Action(button))
UseAction(BenillaActionButton_Action(button), 0, onSelf)
-- Ref ActionButtonUp:55-59: the checked ring reflects the use immediately.
BenillaActionButton_UpdateState(button)
end
@@ -388,7 +393,8 @@
-- yet): the 0.4 s flash toggle, and the 0.2 s range recheck — a bound slot's label
-- tints red/grey, an unbound one wears the RANGE_INDICATOR dot (both legs the ref's,
-- unified in 1008 — the SCOPE note).
function BenillaActionButton_OnUpdate(button, elapsed)
function ActionButton_OnUpdate(elapsed)
local button = this
if ( button.flashing == 1 ) then
button.flashtime = button.flashtime - elapsed
if ( button.flashtime <= 0 ) then
@@ -610,7 +616,8 @@
-- The XP bar (ref-MainMenuBar.lua MainMenuExpBar_Update): value = current XP within the
-- level, max = XP-to-level. Both from the player-global UnitXP feed (PLAYER_XP /
-- PLAYER_NEXT_LEVEL_XP). currXP >= 0 so min(0,currXP) is just 0.
function BenillaExpBar_Update(bar)
function MainMenuExpBar_Update()
local bar = this
local currXP = UnitXP("player")
local nextXP = UnitXPMax("player")
bar:SetMinMaxValues(0, nextXP)
@@ -627,12 +634,12 @@
TextStatusBar_Initialize()
bar.textLockable = 1
end
BenillaExpBar_Update(bar)
MainMenuExpBar_Update()
end
-- ref-MainMenuBar.xml l.121-126: repaint, then hand arg1/arg2 to the CVAR_UPDATE watcher.
function BenillaExpBar_OnEvent(bar)
BenillaExpBar_Update(bar)
MainMenuExpBar_Update()
if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end
end
@@ -651,7 +658,7 @@
-- The ref OnEnter's full body (ref-MainMenuBar.xml l.136-141), now that its machinery
-- exists (decision 1082): the on-bar numerals (TextStatusBar.xml), the 1 s armed timer
-- that appends the rest-state line to the held-open tooltip, and canAddRestStateLine —
-- the handshake BenillaExhaustionToolTipText's newbie-tips branch reads. The trailing 1
-- the handshake ExhaustionToolTipText's newbie-tips branch reads. The trailing 1
-- is the ref's noNormalText: with detailed tips OFF this hover shows NOTHING rather than
-- a bare "XP Bar". Transcribed, though 1.12's default (0661) never takes it.
function BenillaExpBar_OnEnter(bar)
@@ -659,7 +666,7 @@
TextStatusBar_UpdateTextString(bar)
ShowTextStatusBarText(bar)
end
BenillaExhaustionTick.timer = 1
ExhaustionTick.timer = 1
if not GameTooltip_AddNewbieTip then return end
GameTooltip_AddNewbieTip(bar, XPBAR_LABEL, 1.0, 1.0, 1.0, NEWBIE_TOOLTIP_XPBAR, 1)
GameTooltip.canAddRestStateLine = 1
@@ -668,7 +675,7 @@
function BenillaExpBar_OnLeave(bar)
if HideTextStatusBarText then HideTextStatusBarText(bar) end
if GameTooltip then GameTooltip:Hide() end
BenillaExhaustionTick.timer = nil
ExhaustionTick.timer = nil
end
-- ── The max-level rail (ref-ReputationFrame.lua, the max-level slice) ─────────────────
@@ -692,12 +699,12 @@
newLevel = UnitLevel("player")
end
if newLevel == MAX_PLAYER_LEVEL then
BenillaExpBar:Hide()
BenillaMaxLevelBar:Show()
BenillaExhaustionTick:Hide()
MainMenuExpBar:Hide()
MainMenuBarMaxLevelBar:Show()
ExhaustionTick:Hide()
else
BenillaExpBar:Show()
BenillaMaxLevelBar:Hide()
MainMenuExpBar:Show()
MainMenuBarMaxLevelBar:Hide()
end
end
@@ -719,7 +726,8 @@
-- the rep-bar-replaced-the-xp-bar hide, l.69-71) reference the faction watch bar, not
-- built — its frame can't be shown, so the cut is an unreachable-code cut, not a
-- behavior cut. The MainMenuBarMaxLevelBar guard is live since 1094 (the rail below).
function BenillaExhaustionTick_OnLoad(tick)
function ExhaustionTick_OnLoad()
local tick = this
tick:RegisterEvent("PLAYER_ENTERING_WORLD")
tick:RegisterEvent("PLAYER_XP_UPDATE")
tick:RegisterEvent("UPDATE_EXHAUSTION")
@@ -727,7 +735,8 @@
tick:RegisterEvent("PLAYER_UPDATE_RESTING")
end
function BenillaExhaustionTick_Update(tick)
function ExhaustionTick_Update()
local tick = this
if event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_XP_UPDATE"
or event == "UPDATE_EXHAUSTION" or event == "PLAYER_LEVEL_UP" then
local playerCurrXP = UnitXP("player")
@@ -738,24 +747,24 @@
-- at the bar's RIGHT end. The live server never produces them (GetRestState's
-- binding note) — kept because the ref keeps it.
if exhaustionStateID >= 3 then
tick:SetPoint("CENTER", "BenillaExpBar", "RIGHT", 0, 0)
tick:SetPoint("CENTER", "MainMenuExpBar", "RIGHT", 0, 0)
end
if not exhaustionThreshold then
tick:Hide()
BenillaExhaustionLevelFillBar:Hide()
ExhaustionLevelFillBar:Hide()
else
local exhaustionTickSet = max(((playerCurrXP + exhaustionThreshold)
/ playerMaxXP) * BenillaExpBar:GetWidth(), 0)
/ playerMaxXP) * MainMenuExpBar:GetWidth(), 0)
tick:ClearAllPoints()
if exhaustionTickSet > BenillaExpBar:GetWidth()
or BenillaMaxLevelBar:IsShown() then
if exhaustionTickSet > MainMenuExpBar:GetWidth()
or MainMenuBarMaxLevelBar:IsShown() then
tick:Hide()
BenillaExhaustionLevelFillBar:Hide()
ExhaustionLevelFillBar:Hide()
else
tick:Show()
tick:SetPoint("CENTER", "BenillaExpBar", "LEFT", exhaustionTickSet, 0)
BenillaExhaustionLevelFillBar:Show()
BenillaExhaustionLevelFillBar:SetPoint("TOPRIGHT", "BenillaExpBar",
tick:SetPoint("CENTER", "MainMenuExpBar", "LEFT", exhaustionTickSet, 0)
ExhaustionLevelFillBar:Show()
ExhaustionLevelFillBar:SetPoint("TOPRIGHT", "MainMenuExpBar",
"TOPLEFT", exhaustionTickSet, 0)
end
end
@@ -765,13 +774,13 @@
if event == "PLAYER_ENTERING_WORLD" or event == "UPDATE_EXHAUSTION" then
local exhaustionStateID = GetRestState()
if exhaustionStateID == 1 then
BenillaExpBar:SetStatusBarColor(0.0, 0.39, 0.88, 1.0)
BenillaExhaustionLevelFillBar:SetVertexColor(0.0, 0.39, 0.88, 0.15)
BenillaExhaustionTickHighlight:SetVertexColor(0.0, 0.39, 0.88)
MainMenuExpBar:SetStatusBarColor(0.0, 0.39, 0.88, 1.0)
ExhaustionLevelFillBar:SetVertexColor(0.0, 0.39, 0.88, 0.15)
ExhaustionTickHighlight:SetVertexColor(0.0, 0.39, 0.88)
elseif exhaustionStateID == 2 then
BenillaExpBar:SetStatusBarColor(0.58, 0.0, 0.55, 1.0)
BenillaExhaustionLevelFillBar:SetVertexColor(0.58, 0.0, 0.55, 0.15)
BenillaExhaustionTickHighlight:SetVertexColor(0.58, 0.0, 0.55)
MainMenuExpBar:SetStatusBarColor(0.58, 0.0, 0.55, 1.0)
ExhaustionLevelFillBar:SetVertexColor(0.58, 0.0, 0.55, 0.15)
ExhaustionTickHighlight:SetVertexColor(0.58, 0.0, 0.55)
end
end
end
@@ -783,14 +792,14 @@
-- gated on canAddRestStateLine. EXHAUST_TOOLTIP4 (time to fully rested) needs
-- GetTimeToWellRested, which is nil on this wire — the line simply never renders,
-- faithfully (the binding's note).
function BenillaExhaustionToolTipText()
function ExhaustionToolTipText()
if SHOW_NEWBIE_TIPS ~= "1" then
local x, y = BenillaExhaustionTick:GetCenter()
if BenillaExhaustionTick:IsVisible() then
local x, y = ExhaustionTick:GetCenter()
if ExhaustionTick:IsVisible() then
if x >= (GetScreenWidth() / 2) then
GameTooltip:SetOwner(BenillaExhaustionTick, "ANCHOR_LEFT")
GameTooltip:SetOwner(ExhaustionTick, "ANCHOR_LEFT")
else
GameTooltip:SetOwner(BenillaExhaustionTick, "ANCHOR_RIGHT")
GameTooltip:SetOwner(ExhaustionTick, "ANCHOR_RIGHT")
end
else
GameTooltip_SetDefaultAnchor(GameTooltip, UIParent)
@@ -829,13 +838,13 @@
-- Driven from the XP BAR's OnUpdate (the ref wires it there, not on the tick — the timer
-- only ever counts while the mouse is on the bar, which is what arms it).
function BenillaExhaustionTick_OnUpdate(elapsed)
if BenillaExhaustionTick.timer then
if BenillaExhaustionTick.timer < 0 then
BenillaExhaustionToolTipText()
BenillaExhaustionTick.timer = nil
function ExhaustionTick_OnUpdate(elapsed)
if ExhaustionTick.timer then
if ExhaustionTick.timer < 0 then
ExhaustionToolTipText()
ExhaustionTick.timer = nil
else
BenillaExhaustionTick.timer = BenillaExhaustionTick.timer - elapsed
ExhaustionTick.timer = ExhaustionTick.timer - elapsed
end
end
end
@@ -845,8 +854,8 @@
-- materialized; the bar's lockShow starts at the not-hovered refcount.
function BenillaExpBarOverlay_OnLoad()
if SetTextStatusBarText then -- engine tests load file subsets
BenillaExpBar.lockShow = 0
SetTextStatusBarText(BenillaExpBar, BenillaExpBarText)
MainMenuExpBar.lockShow = 0
SetTextStatusBarText(MainMenuExpBar, MainMenuBarExpText)
end
end
@@ -897,11 +906,11 @@
frame.updateInterval = PERFORMANCEBAR_UPDATE_INTERVAL
local _, _, latency = GetNetStats()
if latency > PERFORMANCEBAR_MEDIUM_LATENCY then
BenillaPerformanceBar:SetVertexColor(1, 0, 0)
MainMenuBarPerformanceBar:SetVertexColor(1, 0, 0)
elseif latency > PERFORMANCEBAR_LOW_LATENCY then
BenillaPerformanceBar:SetVertexColor(1, 1, 0)
MainMenuBarPerformanceBar:SetVertexColor(1, 1, 0)
else
BenillaPerformanceBar:SetVertexColor(0, 1, 0)
MainMenuBarPerformanceBar:SetVertexColor(0, 1, 0)
end
-- Held-open tooltips re-read the number on the same 10 s beat (the ref's own refresh).
if frame.hover then
@@ -910,12 +919,12 @@
end
function BenillaPerformanceBar_OnEnter(button)
BenillaPerformanceBarFrame.hover = 1
MainMenuBarPerformanceBarFrame.hover = 1
BenillaPerformanceBar_ShowTooltip(button)
end
function BenillaPerformanceBar_OnLeave()
BenillaPerformanceBarFrame.hover = nil
MainMenuBarPerformanceBarFrame.hover = nil
if GameTooltip then GameTooltip:Hide() end
end
]]></Script>
@@ -982,14 +991,14 @@
<OnClick>BenillaActionButton_OnClick(self)</OnClick>
<OnDragStart>BenillaActionButton_OnDragStart(self)</OnDragStart>
<OnReceiveDrag>BenillaActionButton_OnReceiveDrag(self)</OnReceiveDrag>
<OnUpdate>BenillaActionButton_OnUpdate(self, arg1)</OnUpdate>
<OnUpdate>ActionButton_OnUpdate(arg1)</OnUpdate>
<OnEnter>BenillaActionButton_OnEnter(self)</OnEnter>
<OnLeave>if GameTooltip then GameTooltip:Hide() end</OnLeave>
</Scripts>
</CheckButton>
<!-- The bar: 1024x53 anchored BOTTOM of the screen (ref-MainMenuBar.xml l.4-10). -->
<Frame name="BenillaActionBar">
<Frame name="MainMenuBar">
<Size><AbsDimension x="1024" y="53"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
@@ -1005,7 +1014,7 @@
-->
<!-- enableMouse: the ref's MainMenuExpBar takes the mouse for its own hover (l.136-147).
The strip is the bar's top 13 px, clear of every button on it. -->
<StatusBar name="BenillaExpBar" enableMouse="true">
<StatusBar name="MainMenuExpBar" enableMouse="true">
<Size><AbsDimension x="1024" y="13"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
@@ -1019,33 +1028,33 @@
<Layer level="BORDER">
<!-- The rested fill (ref-MainMenuBar.xml l.20-33 ExhaustionLevelFillBar):
the pale wash from the bar's LEFT edge to the exhaustion tick. 0-wide
until BenillaExhaustionTick_Update stretches its TOPRIGHT to the tick
until ExhaustionTick_Update stretches its TOPRIGHT to the tick
and tints it (SetVertexColor a=0.15 over this authored white). -->
<Texture name="BenillaExhaustionLevelFillBar">
<Texture name="ExhaustionLevelFillBar">
<Size><AbsDimension x="0" y="13"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="BenillaExpBar"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
<Anchor point="TOPLEFT" relativeTo="MainMenuExpBar"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="1.0" b="1.0" a="1.0"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture name="BenillaXPBarTexture0" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuXPBarTexture0" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="-384" y="3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.79296875" bottom="0.83203125"/>
</Texture>
<Texture name="BenillaXPBarTexture1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuXPBarTexture1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="-128" y="3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.54296875" bottom="0.58203125"/>
</Texture>
<Texture name="BenillaXPBarTexture2" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuXPBarTexture2" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="128" y="3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.29296875" bottom="0.33203125"/>
</Texture>
<Texture name="BenillaXPBarTexture3" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuXPBarTexture3" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="384" y="3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.04296875" bottom="0.08203125"/>
@@ -1057,16 +1066,16 @@
a DIALOG-strata child so the "1234 / 5678" text draws over the dwarf-notch
art and the buttons; its own OnLoad wires the FontString to the bar
(BenillaExpBarOverlay_OnLoad's note). -->
<Frame name="BenillaExpBarOverlayFrame" frameStrata="DIALOG">
<Frame name="MainMenuBarOverlayFrame" frameStrata="DIALOG">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="BenillaActionBar" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="BenillaActionBar" relativePoint="BOTTOMRIGHT"/>
<Anchor point="TOPLEFT" relativeTo="MainMenuBar" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="MainMenuBar" relativePoint="BOTTOMRIGHT"/>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="BenillaExpBarText" inherits="TextStatusBarText">
<FontString name="MainMenuBarExpText" inherits="TextStatusBarText">
<Anchors>
<Anchor point="CENTER" relativeTo="BenillaExpBar"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor>
<Anchor point="CENTER" relativeTo="MainMenuExpBar"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
@@ -1083,7 +1092,7 @@
<OnEvent>BenillaExpBar_OnEvent(self)</OnEvent>
<OnEnter>BenillaExpBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaExpBar_OnLeave(self)</OnLeave>
<OnUpdate>BenillaExhaustionTick_OnUpdate(arg1)</OnUpdate>
<OnUpdate>ExhaustionTick_OnUpdate(arg1)</OnUpdate>
<OnValueChanged>
if this:IsShown() and TextStatusBar_OnValueChanged then
TextStatusBar_OnValueChanged()
@@ -1095,23 +1104,23 @@
<!--
The exhaustion tick (ref-MainMenuBar.xml l.415-442): a 32×32 button riding the XP
bar at the rested boundary, DIALOG strata so it draws over the bar art. Normal =
the brass marker, Highlight = the ADD glow BenillaExhaustionTick_Update tints to
the brass marker, Highlight = the ADD glow ExhaustionTick_Update tints to
the rest state. Starts at the bar's CENTER exactly like the ref (its own first
Update re-seats or hides it before anything is seen).
-->
<Button name="BenillaExhaustionTick" frameStrata="DIALOG">
<Button name="ExhaustionTick" frameStrata="DIALOG">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativeTo="BenillaExpBar"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
<Anchor point="CENTER" relativeTo="MainMenuExpBar"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>BenillaExhaustionTick_OnLoad(self)</OnLoad>
<OnEvent>BenillaExhaustionTick_Update(self)</OnEvent>
<OnEnter>BenillaExhaustionToolTipText()</OnEnter>
<OnLoad>ExhaustionTick_OnLoad()</OnLoad>
<OnEvent>ExhaustionTick_Update()</OnEvent>
<OnEnter>ExhaustionToolTipText()</OnEnter>
<OnLeave>if GameTooltip then GameTooltip:Hide() end</OnLeave>
</Scripts>
<NormalTexture name="BenillaExhaustionTickNormal" file="Interface\MainMenuBar\UI-ExhaustionTickNormal"/>
<HighlightTexture name="BenillaExhaustionTickHighlight" file="Interface\MainMenuBar\UI-ExhaustionTickHighlight" alphaMode="ADD"/>
<NormalTexture name="ExhaustionTickNormal" file="Interface\MainMenuBar\UI-ExhaustionTickNormal"/>
<HighlightTexture name="ExhaustionTickHighlight" file="Interface\MainMenuBar\UI-ExhaustionTickHighlight" alphaMode="ADD"/>
</Button>
<!--
@@ -1123,38 +1132,38 @@
OnHide call UIParent_ManageFramePositions, which benilla doesn't build
(DurabilityFrame.xml's note) — a named cut.
-->
<Frame name="BenillaMaxLevelBar" enableMouse="true" hidden="true">
<Frame name="MainMenuBarMaxLevelBar" enableMouse="true" hidden="true">
<Size><AbsDimension x="1024" y="7"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-11"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="BenillaMaxLevelBar0" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Texture name="MainMenuMaxLevelBar0" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Size><AbsDimension x="256" y="7"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativePoint="TOP"><Offset><AbsDimension x="-384" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.21875"/>
</Texture>
<Texture name="BenillaMaxLevelBar1" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Texture name="MainMenuMaxLevelBar1" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Size><AbsDimension x="256" y="7"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaMaxLevelBar0" relativePoint="RIGHT"/>
<Anchor point="LEFT" relativeTo="MainMenuMaxLevelBar0" relativePoint="RIGHT"/>
</Anchors>
<TexCoords left="0" right="1.0" top="0.25" bottom="0.46875"/>
</Texture>
<Texture name="BenillaMaxLevelBar2" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Texture name="MainMenuMaxLevelBar2" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Size><AbsDimension x="256" y="7"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaMaxLevelBar1" relativePoint="RIGHT"/>
<Anchor point="LEFT" relativeTo="MainMenuMaxLevelBar1" relativePoint="RIGHT"/>
</Anchors>
<TexCoords left="0" right="1.0" top="0.5" bottom="0.71875"/>
</Texture>
<Texture name="BenillaMaxLevelBar3" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Texture name="MainMenuMaxLevelBar3" file="Interface\MainMenuBar\UI-MainMenuBar-MaxLevel">
<Size><AbsDimension x="256" y="7"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaMaxLevelBar2" relativePoint="RIGHT"/>
<Anchor point="LEFT" relativeTo="MainMenuMaxLevelBar2" relativePoint="RIGHT"/>
</Anchors>
<TexCoords left="0" right="1.0" top="0.75" bottom="0.96875"/>
</Texture>
@@ -1171,31 +1180,31 @@
strips (ARTWORK) + the 2 end caps and page number (OVERLAY); the buttons are its
children so they draw over the art.
-->
<Frame name="BenillaActionBarArtFrame">
<Frame name="MainMenuBarArtFrame">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="BenillaActionBar" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="BenillaActionBar" relativePoint="BOTTOMRIGHT"/>
<Anchor point="TOPLEFT" relativeTo="MainMenuBar" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="MainMenuBar" relativePoint="BOTTOMRIGHT"/>
</Anchors>
<Layers>
<!-- 4 dwarf strips: 256x43, BOTTOM x=-384/-128/128/384 y=0, TexCoord bands
(ref-MainMenuBar.xml l.232-283). Adjacent atlas rows ⇒ the joins are seamless. -->
<Layer level="ARTWORK">
<Texture name="BenillaActionBarTexture0" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuBarTexture0" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="43"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="-384" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.83203125" bottom="1.0"/>
</Texture>
<Texture name="BenillaActionBarTexture1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuBarTexture1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="43"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="-128" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.58203125" bottom="0.75"/>
</Texture>
<Texture name="BenillaActionBarTexture2" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuBarTexture2" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="43"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="128" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.33203125" bottom="0.5"/>
</Texture>
<Texture name="BenillaActionBarTexture3" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="MainMenuBarTexture3" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="256" y="43"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="384" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0.08203125" bottom="0.25"/>
@@ -1207,16 +1216,16 @@
fix landed in 87c97d7). Page number: GameFontNormalSmall CENTER (30,-5),
static "1" (l.311-319). -->
<Layer level="OVERLAY">
<Texture name="BenillaActionBarLeftEndCap" file="Interface\MainMenuBar\UI-MainMenuBar-EndCap-Dwarf">
<Texture name="MainMenuBarLeftEndCap" file="Interface\MainMenuBar\UI-MainMenuBar-EndCap-Dwarf">
<Size><AbsDimension x="128" y="128"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="-544" y="0"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="BenillaActionBarRightEndCap" file="Interface\MainMenuBar\UI-MainMenuBar-EndCap-Dwarf">
<Texture name="MainMenuBarRightEndCap" file="Interface\MainMenuBar\UI-MainMenuBar-EndCap-Dwarf">
<Size><AbsDimension x="128" y="128"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="544" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="1.0" right="0.0" top="0.0" bottom="1.0"/>
</Texture>
<FontString name="BenillaActionBarPageNumber" inherits="GameFontNormalSmall" text="1">
<FontString name="MainMenuBarPageNumber" inherits="GameFontNormalSmall" text="1">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="30" y="-5"/></Offset></Anchor>
</Anchors>
@@ -1227,71 +1236,71 @@
<!-- The 12 buttons: Button1 at the art frame BOTTOMLEFT (8,4); 2..12 chain LEFT of
the previous's RIGHT +6,0 (ref-ActionBarFrame.xml l.51-158). Replaces our old
fixed 38px stride with the real 42px chain (36 wide + 6 gap). -->
<CheckButton name="BenillaActionButton1" inherits="BenillaActionButtonTemplate">
<CheckButton name="ActionButton1" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="8" y="4"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 1)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton2" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton2" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 2)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton3" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton3" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 3)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton4" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton4" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 4)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton5" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton5" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 5)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton6" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton6" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 6)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton7" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton7" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 7)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton8" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton8" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 8)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton9" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton9" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 9)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton10" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton10" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 10)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton11" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton11" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 11)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaActionButton12" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaActionButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ActionButton12" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ActionButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 12)</OnLoad></Scripts>
</CheckButton>
<!-- Page up/down: 32x32 at CENTER of the art frame's TOPLEFT +(522,-22)/(522,-42)
(ref-ActionBarFrame.xml l.159-217). ART ONLY — we have no bar paging, so the
clicks are no-ops (no OnClick handler). -->
<Button name="BenillaActionBarUpButton">
<Button name="ActionBarUpButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativeTo="BenillaActionBarArtFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="522" y="-22"/></Offset></Anchor>
<Anchor point="CENTER" relativeTo="MainMenuBarArtFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="522" y="-22"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollUpButton-Up"/>
<PushedTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollUpButton-Down"/>
<HighlightTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollUpButton-Highlight" alphaMode="ADD"/>
</Button>
<Button name="BenillaActionBarDownButton">
<Button name="ActionBarDownButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativeTo="BenillaActionBarArtFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="522" y="-42"/></Offset></Anchor>
<Anchor point="CENTER" relativeTo="MainMenuBarArtFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="522" y="-42"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollDownButton-Up"/>
<PushedTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollDownButton-Down"/>
@@ -1317,24 +1326,24 @@
tree, and its explicit strata survives the parent's LOW subtree-force (attributes
apply before nested <Frames> are built).
The -227 authored here is the NO-keyring position; BenillaMainMenuBar_UpdateKeyRing
The -227 authored here is the NO-keyring position; MainMenuBar_UpdateKeyRing
(top of this file) re-points it to -235 while the keyring is shown, the ref's own
shift (MainMenuBar.lua l.180). Both offsets live in that one function. -->
<Frame name="BenillaPerformanceBarFrame" frameStrata="LOW">
<Frame name="MainMenuBarPerformanceBarFrame" frameStrata="LOW">
<Size><AbsDimension x="16" y="64"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="BenillaActionBar" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-227" y="-10"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT" relativeTo="MainMenuBar" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-227" y="-10"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="BenillaPerformanceBar" file="Interface\MainMenuBar\UI-MainMenuBar-PerformanceBar">
<Texture name="MainMenuBarPerformanceBar" file="Interface\MainMenuBar\UI-MainMenuBar-PerformanceBar">
<Size><AbsDimension x="20" y="66"/></Size>
<Anchors><Anchor point="TOPRIGHT"/></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="BenillaPerformanceBarButton" setAllPoints="true" frameStrata="HIGH">
<Button name="MainMenuBarPerformanceBarFrameButton" setAllPoints="true" frameStrata="HIGH">
<Scripts>
<OnEnter>BenillaPerformanceBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaPerformanceBar_OnLeave()</OnLeave>
+366
View File
@@ -0,0 +1,366 @@
<Ui>
<!--
The in-game AddOns list (decision 1197) — the ESC-menu half of the AddOns UI, and the
button `GameMenuFrame.xml` has been carrying as `GameMenuButton_Pending(self, ADDONS)`
since 0997 ("goes live by deleting one Pending call and writing its OnClick").
SCOPE — this frame has no reference. 1.12 ships an AddOns list only on the GLUE side
(`GlueXML/AddonList.xml/.lua`, opened by CharacterSelectAddonsButton); in-world there is
no such window and the ESC menu has no such entry. This is a deliberate benilla addition
the director asked for, so nothing here is transcribed and nothing pretends to be. What IS
borrowed from the reference is the BEHAVIOUR of its glue twin, because it is the same job:
- the row shape and count: ADDON_BUTTON_HEIGHT = 16, MAX_ADDONS_DISPLAYED = 19
- AddonList_Update's three-colour title rule — gold loadable, red enabled-but-broken,
grey disabled (SetTextColor 1/0.78/0, 1/0.1/0.1, 0.5/0.5/0.5)
- the status column carrying `reason` (our GetAddOnInfo returns the reference's own
token set — decision 1191 §6)
- Okay saves, Cancel discards (SaveAddOns / ResetAddOns)
It is written entirely against the ELEVEN AddOn API verbs 1191 landed — GetNumAddOns,
GetAddOnInfo, GetAddOnDependencies, EnableAddOn, DisableAddOn, IsAddOnLoaded — with no
host support of its own. That is the point of it as much as the window is: if this panel
works, the addon API an addon author would use works, and it is exercised by anyone who
opens the ESC menu rather than by a test nobody runs.
WHEN CHANGES TAKE EFFECT — next login, not now. Enabling an addon mid-session would mean
running its files into a VM that has already fired ADDON_LOADED/VARIABLES_LOADED/
PLAYER_LOGIN, which is not a state the reference's own loader has a path for; disabling
one cannot unload it at all. The panel says so on its face rather than leaving a player to
discover it. The enable state is written at the session's end by the shutdown tail
(decision 1191 §7, the reference's own last step at 0x490bd0).
-->
<Script><![CDATA[
-- Strings. The `X = X or "..."` idiom is MicroMenu.xml's: a real GlobalStrings table
-- loaded later wins, and these are the fallbacks for a run without one.
ADDONS = ADDONS or "AddOns"
ADDON_LIST_ENABLE_ALL = ADDON_LIST_ENABLE_ALL or "Enable All"
ADDON_LIST_DISABLE_ALL = ADDON_LIST_DISABLE_ALL or "Disable All"
ADDON_LIST_APPLY_NOTE = ADDON_LIST_APPLY_NOTE or "Changes take effect at your next login."
OKAY = OKAY or "Okay"
CANCEL = CANCEL or "Cancel"
ADDON_BUTTON_HEIGHT = 16
MAX_ADDONS_DISPLAYED = 19
-- The staged edit. `AddonList.original` is the enable state as the panel found it, so
-- Cancel can put it back — the reference's `ResetAddOns` over the client's own records,
-- which we do not have an engine verb for (SaveAddOns/ResetAddOns are GLUE-namespace
-- globals and deliberately absent in-world, decision 1191 §6).
AddonList = { offset = 0, original = {} }
function AddonList_Toggle()
if AddonListFrame:IsVisible() then
AddonList_OnCancel()
else
AddonList_Show()
end
end
function AddonList_Show()
AddonList.offset = 0
AddonList.original = {}
for i = 1, GetNumAddOns() do
local name, _, _, _, loadable, reason = GetAddOnInfo(i)
-- "enabled" is everything except the player's own DISABLED — a dependency problem
-- is not a checkbox state, it is a status line.
AddonList.original[name] = not (reason == "DISABLED")
end
AddonListFrame:Show()
AddonList_Update()
end
-- One row's worth of facts, so the row painter below reads as a list of assignments
-- rather than as a pile of GetAddOnInfo returns.
local function rowFacts(index)
local name, title, notes, url, loadable, reason = GetAddOnInfo(index)
local enabled = not (reason == "DISABLED")
return name, (title or name), loadable, reason, enabled, notes, url
end
function AddonList_Update()
local total = GetNumAddOns()
-- Clamp first: a Disable All that shortens nothing still must not leave the offset
-- past the end after the list is re-read.
local maxOffset = total - MAX_ADDONS_DISPLAYED
if maxOffset < 0 then maxOffset = 0 end
if AddonList.offset > maxOffset then AddonList.offset = maxOffset end
if AddonList.offset < 0 then AddonList.offset = 0 end
for i = 1, MAX_ADDONS_DISPLAYED do
local entry = getglobal("AddonListEntry" .. i)
local index = AddonList.offset + i
if index > total then
entry:Hide()
else
local name, title, loadable, reason, enabled = rowFacts(index)
entry:SetID(index)
local check = getglobal("AddonListEntry" .. i .. "Check")
check:SetChecked(enabled)
-- AddonList_Update's own colour rule, its own three values.
local label = getglobal("AddonListEntry" .. i .. "Title")
label:SetText(title)
if loadable then
label:SetTextColor(1.0, 0.78, 0.0)
elseif enabled and reason ~= "DEP_DISABLED" then
label:SetTextColor(1.0, 0.1, 0.1)
else
label:SetTextColor(0.5, 0.5, 0.5)
end
-- The status column: the reference splices its token into a GlobalStrings
-- lookup (`getglobal("ADDON_"..reason)`), which is why the token spelling is
-- load-bearing (1191 §6). A token with no string falls back to the token, so
-- a player sees something rather than a blank.
local status = getglobal("AddonListEntry" .. i .. "Status")
if reason then
status:SetText(getglobal("ADDON_" .. reason) or reason)
else
status:SetText("")
end
entry:Show()
end
end
-- The scroll pair exists only while the list overflows — the reference's own
-- GlueScrollFrame_Update condition.
if total > MAX_ADDONS_DISPLAYED then
AddonListScrollUp:Show()
AddonListScrollDown:Show()
else
AddonListScrollUp:Hide()
AddonListScrollDown:Hide()
end
end
function AddonList_Scroll(delta)
AddonList.offset = AddonList.offset + delta
AddonList_Update()
end
function AddonList_ToggleEntry(entryIndex)
local name, _, _, _, _, reason = GetAddOnInfo(entryIndex)
if reason == "DISABLED" then
EnableAddOn(name)
else
DisableAddOn(name)
end
AddonList_Update()
end
function AddonList_SetAll(on)
if on then EnableAllAddOns() else DisableAllAddOns() end
AddonList_Update()
end
function AddonList_OnOk()
PlaySound("igMainMenuOption")
AddonListFrame:Hide()
end
-- Cancel restores what the panel found, addon by addon — `ResetAddOns` written out,
-- because that verb is glue-only and this window is not the glue.
function AddonList_OnCancel()
PlaySound("igMainMenuQuit")
for i = 1, GetNumAddOns() do
local name = GetAddOnInfo(i)
local was = AddonList.original[name]
if was == true then
EnableAddOn(name)
elseif was == false then
DisableAddOn(name)
end
end
AddonListFrame:Hide()
end
-- The hover tooltip: title, notes, dependencies — AddonTooltip_Update's three lines in
-- its order, on the shared GameTooltip rather than a private frame.
function AddonList_RowEnter(row)
local name, title, loadable, reason, enabled, notes = rowFacts(row:GetID())
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
GameTooltip:AddLine(title)
if notes and notes ~= "" then
GameTooltip:AddLine(notes, 1, 1, 1)
end
local deps = ""
local list = { GetAddOnDependencies(row:GetID()) }
for i = 1, getn(list) do
if i == 1 then
deps = (ADDON_DEPENDENCIES or "Dependencies: ") .. list[i]
else
deps = deps .. ", " .. list[i]
end
end
if deps ~= "" then
GameTooltip:AddLine(deps, 1, 0.82, 0)
end
GameTooltip:Show()
end
function AddonList_RowLeave()
GameTooltip:Hide()
end
]]></Script>
<!-- The panel button face. This engine ships no shared UIPanelButtonTemplate — every panel
defines its own (the MacroFrame/TrainerFrame precedent), and a local one also keeps this
window independent of any other file's template names. -->
<Button name="AddonListPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="90" y="22"/></Size>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<!-- The two scroll arrows, the client's own `UI-ScrollBar-ScrollUpButton-*` art. -->
<Button name="AddonListScrollUpTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Up"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Down"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Highlight" alphaMode="ADD"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></HighlightTexture>
</Button>
<Button name="AddonListScrollDownTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Up"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Down"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Highlight" alphaMode="ADD"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></HighlightTexture>
</Button>
<!-- One row: a checkbox, a title, a status column. 16 tall, the reference's own
ADDON_BUTTON_HEIGHT. -->
<Button name="AddonListEntryTemplate" virtual="true">
<Size><AbsDimension x="440" y="16"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="$parentTitle" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="$parentStatus" inherits="GameFontDisableSmall" justifyH="RIGHT">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<CheckButton name="$parentCheck">
<Size><AbsDimension x="20" y="20"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
<CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/>
<Scripts>
<OnClick>AddonList_ToggleEntry(this:GetParent():GetID())</OnClick>
</Scripts>
</CheckButton>
</Frames>
<Scripts>
<OnEnter>AddonList_RowEnter(this)</OnEnter>
<OnLeave>AddonList_RowLeave()</OnLeave>
<OnClick>AddonList_ToggleEntry(this:GetID())</OnClick>
</Scripts>
</Button>
<Frame name="AddonListFrame" frameStrata="DIALOG" enableMouse="true" hidden="true">
<Size><AbsDimension x="480" y="420"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentTitle" inherits="GameFontNormalLarge">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="$parentNote" inherits="GameFontDisableSmall">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="60"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="AddonListEntry1" inherits="AddonListEntryTemplate">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="20" y="-46"/></Offset></Anchor></Anchors>
</Button>
<Button name="AddonListEntry2" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry3" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry4" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry5" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry6" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry7" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry8" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry9" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry10" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry11" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry10" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry12" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry11" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry13" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry12" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry14" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry13" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry15" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry14" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry16" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry15" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry17" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry16" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry18" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry17" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry19" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry18" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListScrollUp" inherits="AddonListScrollUpTemplate" hidden="true">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-42"/></Offset></Anchor></Anchors>
<Scripts><OnClick>AddonList_Scroll(-1)</OnClick></Scripts>
</Button>
<Button name="AddonListScrollDown" inherits="AddonListScrollDownTemplate" hidden="true">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="86"/></Offset></Anchor></Anchors>
<Scripts><OnClick>AddonList_Scroll(1)</OnClick></Scripts>
</Button>
<Button name="AddonListEnableAll" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="104" y="22"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="20" y="20"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(ADDON_LIST_ENABLE_ALL)</OnLoad>
<OnClick>AddonList_SetAll(1)</OnClick>
</Scripts>
</Button>
<Button name="AddonListDisableAll" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="104" y="22"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="AddonListEnableAll" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(ADDON_LIST_DISABLE_ALL)</OnLoad>
<OnClick>AddonList_SetAll(nil)</OnClick>
</Scripts>
</Button>
<Button name="AddonListOkay" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="90" y="22"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-20" y="20"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(OKAY)</OnLoad>
<OnClick>AddonList_OnOk()</OnClick>
</Scripts>
</Button>
<Button name="AddonListCancel" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="90" y="22"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="AddonListOkay" relativePoint="LEFT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(CANCEL)</OnLoad>
<OnClick>AddonList_OnCancel()</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
AddonListFrameTitle:SetText(ADDONS)
AddonListFrameNote:SetText(ADDON_LIST_APPLY_NOTE)
</OnLoad>
<OnShow>AddonList_Update()</OnShow>
<OnMouseWheel>AddonList_Scroll(-arg1)</OnMouseWheel>
</Scripts>
</Frame>
</Ui>
+56 -45
View File
@@ -4,7 +4,7 @@
(dressing arc, decision 0084; whole-space generalization, decision 0216 slice 2): the
geometry and art below are QUOTED from the real `ContainerFrame.xml`/`ContainerFrame.lua`
(scratchpad `ref-framexml/ContainerFrame.xml`, `ContainerFrame.lua`; line citations inline),
turned onto our own C_Container feed/intents. FIVE windows now — the backpack (bag id 0, 16
turned onto our own container feed/intents. FIVE windows now — the backpack (bag id 0, 16
slots, its own hand-authored block below, unchanged in shape) plus the four equipped bags
(ids 1..4, `BenillaBagWindowTemplate`, up to 20 slots each) — the ContainerFrameTemplate
generalization this file's header always said was coming.
@@ -46,7 +46,7 @@
-->
<Script><![CDATA[
-- The five windows' shared slot count cap: the backpack is always exactly 16
-- (C_Container.GetContainerNumSlots(0) never varies — PACK_SLOTS is a wire constant); the
-- (GetContainerNumSlots(0) never varies — PACK_SLOTS is a wire constant); the
-- equipped-bag windows show up to this many (5 rows × 4 columns — the reference template's
-- true max is 36, but 20 is vanilla's largest actual equippable bag, and building out to 36
-- slot buttons per window for content that can never exist would be pure waste. Hides the
@@ -56,7 +56,7 @@
-- The keyring's container id (ref MainMenuBarBagButtons.lua:1) — decision 0765. It is an
-- ordinary container in every respect the seam cares about: the app feeds it like any other
-- bag (its slots are the player descriptor's own 81.., no container object), and every
-- C_Container verb, the cursor, the pending-op lock and the wire drains take it unchanged.
-- container verb, the cursor, the pending-op lock and the wire drains take it unchanged.
-- What makes it special is only what the reference makes special: the window's art, its
-- open/close sound, and the fact that its capacity is not a wire field.
KEYRING_CONTAINER = -2
@@ -67,7 +67,7 @@
-- and feeds it as the container's own size, so this reads it back rather than keeping a
-- second copy of the ladder that could drift from the one the feed uses.
function GetKeyRingSize()
return C_Container.GetContainerNumSlots(KEYRING_CONTAINER)
return GetContainerNumSlots(KEYRING_CONTAINER)
end
-- BenillaBagSlot_OnLoad(button, physIndex): physIndex is the button's CHAIN position (1 =
@@ -99,7 +99,10 @@
if button.slot == nil then return end -- a hidden button (physIndex past the bag's size)
local bagId = button:GetParent():GetID()
if mouseButton == "LeftButton" then
local info = C_Container.GetContainerItemInfo(bagId, button.slot)
-- The reference's own five returns (ContainerFrame.lua:241) — decision 1199
-- put this binding back on its 1.12 shape.
local texture, itemCount, locked, quality, readable =
GetContainerItemInfo(bagId, button.slot)
-- The modifier fork, the reference's own order (ContainerFrame.lua:563-579;
-- BankFrame.lua:141-152 for a bank slot, which is this same handler at container
-- -1). `ignoreModifiers` (the drag-trio calls, both true) suppresses it whole —
@@ -107,7 +110,7 @@
--
-- CTRL: preview the item on your own character in the dressing room (decision 1060).
if IsControlKeyDown() and not ignoreModifiers then
DressUpItemLink(C_Container.GetContainerItemLink(bagId, button.slot))
DressUpItemLink(GetContainerItemLink(bagId, button.slot))
return
end
-- SHIFT: post the item's link into the chat edit box if it's open, ELSE split the
@@ -119,10 +122,10 @@
-- arm never splits, even in the one benilla state where nothing can be posted
-- (an unresolved template — `BenillaChatEdit_InsertLink`'s own note).
if ChatFrameEditBox and ChatFrameEditBox:IsShown() then
BenillaChatEdit_InsertLink(C_Container.GetContainerItemLink(bagId, button.slot))
BenillaChatEdit_InsertLink(GetContainerItemLink(bagId, button.slot))
return
end
if info and not info.isLocked and (info.stackCount or 1) >= 2 then
if texture ~= nil and not locked and (itemCount or 1) >= 2 then
button.SplitStack = function(btn, n)
SplitContainerItem(bagId, btn.slot, n)
BenillaBagWindow_Repaint(btn:GetParent())
@@ -130,22 +133,23 @@
-- Ref ContainerFrameItemButton_OnLoad/_OnClick (ContainerFrame.lua:558-560,
-- 573-576): the owner's own SplitStack closure carries the bag/slot so
-- StackSplitFrameOkay_Click's generic `owner.SplitStack(owner, split)` call
-- needs no window-specific knowledge. SplitContainerItem is a TOP-LEVEL
-- global (cursor.rs — not under C_Container despite the container-verb
-- naming; GetContainerItemInfo/PickupContainerItem ARE namespaced).
OpenStackSplitFrame(info.stackCount, button, "BOTTOMRIGHT", "TOPRIGHT")
-- needs no window-specific knowledge. SplitContainerItem lives in cursor.rs
-- rather than container.rs — it is the one container-named verb whose
-- payload is the cursor's, not a bag's — but like every container verb it is
-- a plain top-level global (decision 1198).
OpenStackSplitFrame(itemCount, button, "BOTTOMRIGHT", "TOPRIGHT")
end
return
end
-- A plain click hides any open split frame (ref ContainerFrame.lua:581) before the
-- ordinary pickup/place proceeds.
local split = getglobal("BenillaStackSplitFrame")
local split = getglobal("StackSplitFrame")
if split then split:Hide() end
-- PickupContainerItem is the single drag entry point (real client): an empty cursor
-- picks this slot up; a held cursor places/swaps into it (same slot = cancel). Hide the
-- hover tooltip (the item just left the slot for the cursor) and repaint so the picked
-- slot dims / un-dims immediately — the swap itself settles when the server answers.
C_Container.PickupContainerItem(bagId, button.slot)
PickupContainerItem(bagId, button.slot)
GameTooltip:Hide()
BenillaBagWindow_Repaint(button:GetParent())
elseif mouseButton == "RightButton" then
@@ -154,7 +158,7 @@
ClearCursor()
BenillaBagWindow_Repaint(button:GetParent())
else
C_Container.UseContainerItem(bagId, button.slot)
UseContainerItem(bagId, button.slot)
end
end
end
@@ -169,11 +173,12 @@
function BenillaBagSlot_OnEnter(button)
if button.slot == nil then return end
local bagId = button:GetParent():GetID()
local info = C_Container.GetContainerItemInfo(bagId, button.slot)
local texture, itemCount, locked, quality, readable =
GetContainerItemInfo(bagId, button.slot)
-- An emptied slot drops its tooltip (ref ContainerFrame_Update's texture-nil branch,
-- ContainerFrame.lua:281-283) — without this the OnUpdate re-enter below would leave
-- a stale tooltip up after the item is sold or moved out from under the hover.
if not info then
if not texture then
if GameTooltip:IsOwned(button) then GameTooltip:Hide() end
return
end
@@ -195,10 +200,10 @@
-- loop). The ref's `IsControlKeyDown() and hasItem` magnifier arm (l.638) is the
-- dress-up preview's own cursor tell — it landed with the dressing room (decision
-- 1060); `info` is non-nil above, which is the ref's `hasItem`.
if BenillaMerchantFrame and BenillaMerchantFrame:IsShown()
and BenillaMerchantFrame.selectedTab == 1 and not InRepairMode() then
if MerchantFrame and MerchantFrame:IsShown()
and MerchantFrame.selectedTab == 1 and not InRepairMode() then
ShowContainerSellCursor(bagId, button.slot)
elseif info.isReadable or IsControlKeyDown() then
elseif readable or IsControlKeyDown() then
ShowInspectCursor()
else
ResetCursor()
@@ -224,10 +229,10 @@
end
-- The item's use-cooldown sweep (ref ContainerFrame_UpdateCooldown,
-- ContainerFrame.lua:295-299, verbatim shape over our C_Container dialect).
-- ContainerFrame.lua:295-299, verbatim shape over our container dialect).
function BenillaBagSlot_UpdateCooldown(button, bagId)
local cooldown = getglobal(button:GetName() .. "Cooldown")
local start, duration, enable = C_Container.GetContainerItemCooldown(bagId, button.slot)
local start, duration, enable = GetContainerItemCooldown(bagId, button.slot)
CooldownFrame_SetTimer(cooldown, start, duration, enable)
end
@@ -237,7 +242,7 @@
-- (l.248-253: occupied → UpdateCooldown, empty → Cooldown:Hide()).
function BenillaBagSlot_Update(button)
local bagId = button:GetParent():GetID()
local size = C_Container.GetContainerNumSlots(bagId)
local size = GetContainerNumSlots(bagId)
if button.physIndex > size then
button.slot = nil
button:Hide()
@@ -245,19 +250,19 @@
end
button.slot = size - button.physIndex + 1
button:Show()
local info = C_Container.GetContainerItemInfo(bagId, button.slot)
if info then
local texture, itemCount, locked = GetContainerItemInfo(bagId, button.slot)
if texture ~= nil or itemCount ~= nil then
BenillaBagSlot_UpdateCooldown(button, bagId)
button.icon:SetTexture(info.iconFileID or BENILLA_FALLBACK_ICON)
button.icon:SetTexture(texture or BENILLA_FALLBACK_ICON)
-- A locked slot (its item is up on the cursor, or a send is still pending
-- server-side, decision 0216 §4/0218 §3) dims, the real client's drag feedback.
if info.isLocked then
if locked then
button.icon:SetVertexColor(0.4, 0.4, 0.4)
else
button.icon:SetVertexColor(1, 1, 1)
end
if (info.stackCount or 1) > 1 then
button.count:SetText(info.stackCount)
if (itemCount or 1) > 1 then
button.count:SetText(itemCount)
else
button.count:SetText("")
end
@@ -269,7 +274,7 @@
end
-- The generic per-window repaint (decision 0216 slice 2): every slot up to this window's
-- cap, the title (C_Container.GetBagName — dynamic since a live bag swap changes it), and the money
-- cap, the title (GetBagName — dynamic since a live bag swap changes it), and the money
-- purse (backpack only, ContainerFrame.lua l.314). BenillaBagFrame_Update (below) is the
-- backpack's zero-arg convenience alias, kept for existing callers (UiPanels.xml's
-- OpenBackpack/CloseBackpack, and the tests that patch it directly).
@@ -292,7 +297,7 @@
if bagId == KEYRING_CONTAINER then
title:SetText(KEYRING)
else
title:SetText(C_Container.GetBagName(bagId) or "")
title:SetText(GetBagName(bagId) or "")
end
end
if bagId == 0 then
@@ -305,7 +310,7 @@
if portrait then
portrait:SetPortraitToTexture("Interface\\ContainerFrame\\KeyRing-Bag-Icon")
end
local size = C_Container.GetContainerNumSlots(bagId)
local size = GetContainerNumSlots(bagId)
if size > 0 then BenillaBagWindow_FitBackground(frame, size) end
else
-- The window's portrait is the bag's OWN item icon (the real client's
@@ -333,7 +338,7 @@
end
end
-- Equipped bags snug-fit their stitched background to the live row count.
local size = C_Container.GetContainerNumSlots(bagId)
local size = GetContainerNumSlots(bagId)
if size > 0 then BenillaBagWindow_FitBackground(frame, size) end
end
end
@@ -505,7 +510,7 @@
function BenillaOpenAllBags()
for bagId = 0, 4 do
-- The backpack always exists; an equipped-bag slot only when a bag is in it.
if bagId == 0 or C_Container.GetContainerNumSlots(bagId) > 0 then
if bagId == 0 or GetContainerNumSlots(bagId) > 0 then
local frame = getglobal(BENILLA_BAG_WINDOWS[bagId + 1])
if frame and not frame:IsShown() then
BenillaBagWindow_Repaint(frame) -- bag 0 routes through its observable alias
@@ -542,7 +547,7 @@
-- MainMenuBarBackpackButton, bags 1..4 → CharacterBag(N-1)Slot).
function BenillaBagWindow_BarButton(frame)
local name = frame:GetName()
if name == "BenillaKeyRingFrame" then return getglobal("BenillaKeyRingButton") end
if name == "BenillaKeyRingFrame" then return getglobal("KeyRingButton") end
for bagId = 0, 4 do
if name == BENILLA_BAG_WINDOWS[bagId + 1] then
if bagId == 0 then return getglobal("BenillaBagToggle") end
@@ -653,7 +658,7 @@
-- DEFERRAL: no drag-to-equip a bag onto the bar yet (that's the paper-doll drag arc, slice
-- 3's scope, not this one).
-- The bag bar (and the backpack toggle) are RELOCATED onto the action bar — anchored into
-- BenillaActionBarArtFrame's bottom-right — which means they need seating one frame level
-- MainMenuBarArtFrame's bottom-right — which means they need seating one frame level
-- above the bar art or it paints over their icons. That lever lives with the frame it's
-- about: BenillaActionBarArt_SeatAbove (ActionBar.xml, loaded before this file), which
-- carries the full explanation; the micro-button row is its second caller. Called
@@ -805,8 +810,14 @@
-- nothing when your keyring is full. We print the string that exists.
function PutKeyInKeyRing()
for i = 1, GetKeyRingSize() do
if not C_Container.GetContainerItemInfo(KEYRING_CONTAINER, i) then
C_Container.PickupContainerItem(KEYRING_CONTAINER, i)
-- Occupancy is `texture or itemCount`, not `texture` alone. The reference tests
-- the texture (ContainerFrame_Update) because ITS client always has the icon by
-- the time a slot is populated; ours can hold an item whose icon has not resolved
-- yet, and reading that as an empty slot would file the key on top of it
-- (decision 1199 — the transient our five-value shape exposed).
local texture, itemCount = GetContainerItemInfo(KEYRING_CONTAINER, i)
if texture == nil and itemCount == nil then
PickupContainerItem(KEYRING_CONTAINER, i)
return
end
end
@@ -816,7 +827,7 @@
end
-- ref KeyRingButton's OnLoad/OnClick/OnReceiveDrag (MainMenuBarBagButtons.xml l.142-199).
-- The button is authored HIDDEN and only ever revealed by BenillaMainMenuBar_UpdateKeyRing
-- The button is authored HIDDEN and only ever revealed by MainMenuBar_UpdateKeyRing
-- (ActionBar.xml), which is where the whole show/hide law and its divergence note live.
function BenillaKeyRingButton_OnLoad(button)
button:SetID(KEYRING_CONTAINER)
@@ -832,7 +843,7 @@
end
function BenillaKeyRingButton_OnEvent()
if BenillaMainMenuBar_UpdateKeyRing then BenillaMainMenuBar_UpdateKeyRing() end
if MainMenuBar_UpdateKeyRing then MainMenuBar_UpdateKeyRing() end
end
function BenillaKeyRingButton_OnClick(button)
@@ -1141,7 +1152,7 @@
</Texture>
<!-- Title: the bag name, GameFontHighlight, 112×12 at the frame TOPLEFT (47,-10)
(ref-ContainerFrame.xml l.120-131). BenillaBagWindow_Update keeps it current via
C_Container.GetBagName(0), which always answers "Backpack" — the authored text is just the
GetBagName(0), which always answers "Backpack" — the authored text is just the
pre-first-Update placeholder. -->
<FontString name="BenillaBagFrameName" inherits="GameFontHighlight" text="Backpack">
<Size><AbsDimension x="112" y="12"/></Size>
@@ -1555,13 +1566,13 @@
(the real client's MainMenuBarBackpackButton, ref-MainMenuBarBagButtons.xml l.54-64): a 37x37
button parented/anchored to the bar art frame at BOTTOMRIGHT (-6,2). Its behavior (toggle the
backpack window) is unchanged; only its seat moved from the free-floating screen corner onto
the bar. (BenillaActionBarArtFrame is defined in ActionBar.xml, which the loader loads before
the bar. (MainMenuBarArtFrame is defined in ActionBar.xml, which the loader loads before
this file; the anchor resolves by name.)
-->
<CheckButton name="BenillaBagToggle">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="BenillaActionBarArtFrame" relativePoint="BOTTOMRIGHT">
<Anchor point="BOTTOMRIGHT" relativeTo="MainMenuBarArtFrame" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-6" y="2"/></Offset>
</Anchor>
</Anchors>
@@ -1844,7 +1855,7 @@
<!--
The keyring button (decision 0765) — ref KeyRingButton, MainMenuBarBagButtons.xml l.142-199.
Sits at the FAR left of the bag row, past BenillaBagBarSlot4 (the ref's CharacterBag3Slot),
and is authored HIDDEN: nothing reveals it but BenillaMainMenuBar_UpdateKeyRing, and only
and is authored HIDDEN: nothing reveals it but MainMenuBar_UpdateKeyRing, and only
while HasKey() — the point being that a character with no key has no keyring at all, and the
bar's plate has no socket for one.
@@ -1857,7 +1868,7 @@
an $parentIcon layer, so Disable_BagButtons' SetDesaturation(b.icon) has nothing to grey —
and the ref leaves it out of its own Disable_BagButtons for the same reason.
-->
<CheckButton name="BenillaKeyRingButton" hidden="true">
<CheckButton name="KeyRingButton" hidden="true">
<Size><AbsDimension x="18" y="39"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="BenillaBagBarSlot4" relativePoint="LEFT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor>
+100 -98
View File
@@ -3,7 +3,7 @@
benilla's bank window (decision 0604 phase 4): the vault is already streamed (the 24 bank
slots + 6 bank bags ride the descriptor at login, exactly like the backpack — see the
decision's "pinned mechanism"), so this file is purely the window over the existing
C_Container/cursor plumbing. Geometry/art below are QUOTED from the real `BankFrame.xml`
container/cursor plumbing. Geometry/art below are QUOTED from the real `BankFrame.xml`
(`WoW/_extracted_framexml/BankFrame.xml`/`.lua`, read this session — line-cited inline),
turned onto OUR house idioms per decision 0068/0084: no `ItemButtonTemplate`/
`SmallMoneyFrameTemplate`/`UIPanelButtonTemplate` ship, so every one of those is either a
@@ -59,7 +59,8 @@
BENILLA_BANK_ITEM_SLOTS = 24 -- NUM_BANKGENERIC_SLOTS (ref BankFrame.lua:3)
BENILLA_BANK_BAG_SLOTS = 6 -- NUM_BANKBAGSLOTS (ref BankFrame.lua:4)
function BenillaBankFrame_OnLoad(frame)
function BankFrame_OnLoad()
local frame = this
SetPortraitTexture(getglobal(frame:GetName() .. "Portrait"), "npc")
frame:RegisterEvent("BANKFRAME_OPENED")
frame:RegisterEvent("BANKFRAME_CLOSED")
@@ -68,7 +69,7 @@
frame:RegisterEvent("PLAYER_MONEY")
-- Container -1 is a container like any other: the same lock/cooldown maintenance
-- events the bag windows register (BagFrame.xml's BenillaBagFrame_OnLoad), handled
-- below by BenillaBankFrame_OnEvent directly (not delegated — the shared window-level
-- below by BankFrame_OnEvent directly (not delegated — the shared window-level
-- repaint helper caps at BENILLA_BAG_WINDOW_MAX_SLOTS=20, short of the bank's 24).
frame:RegisterEvent("ITEM_LOCK_CHANGED")
frame:RegisterEvent("BAG_UPDATE_COOLDOWN")
@@ -81,14 +82,14 @@
if name == nil or name == "" then
name = "Banker"
end
getglobal("BenillaBankFrameTitleText"):SetText(name)
getglobal("BankFrameTitleText"):SetText(name)
end
-- Repaint every one of the 24 generic slots (PLAYERBANKSLOTS_CHANGED names ONE slot; the
-- spec allows repainting all 24 — cheap, and spares tracking a slot->button map).
function BenillaBankFrame_RepaintItems()
for i = 1, BENILLA_BANK_ITEM_SLOTS do
local button = getglobal("BenillaBankItem" .. i)
local button = getglobal("BankItem" .. i)
if button then BenillaBagSlot_Update(button) end
end
end
@@ -100,7 +101,7 @@
function BenillaBankFrame_UpdateBagRow()
local numSlots, full = GetNumBankSlots()
for i = 1, BENILLA_BANK_BAG_SLOTS do
local button = getglobal("BenillaBankBagButton" .. i)
local button = getglobal("BankBagButton" .. i)
if button then
local texture = BenillaGetBankBagTexture(i)
button.icon:SetTexture(texture or "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag")
@@ -115,15 +116,15 @@
end
local cost = GetBankSlotCost(numSlots)
BenillaBankFrame.nextSlotCost = cost
BenillaMoney_Set("BenillaBankFrameCostMoney", cost, true)
BankFrame.nextSlotCost = cost
BenillaMoney_Set("BankFrameCostMoney", cost, true)
if GetMoney() >= cost then
BenillaMoney_SetColor("BenillaBankFrameCostMoney", 1.0, 1.0, 1.0)
BenillaMoney_SetColor("BankFrameCostMoney", 1.0, 1.0, 1.0)
else
BenillaMoney_SetColor("BenillaBankFrameCostMoney", 1.0, 0.1, 0.1)
BenillaMoney_SetColor("BankFrameCostMoney", 1.0, 0.1, 0.1)
end
local purchaseFrame = getglobal("BenillaBankFramePurchaseInfo")
local purchaseFrame = getglobal("BankFramePurchaseInfo")
if full then
purchaseFrame:Hide()
else
@@ -131,7 +132,8 @@
end
end
function BenillaBankFrame_OnEvent(frame)
function BankFrame_OnEvent(event)
local frame = this
if event == "BANKFRAME_OPENED" then
BenillaBankFrame_SetTitle(arg1)
BenillaBankFrame_RepaintItems()
@@ -144,7 +146,7 @@
elseif event == "PLAYERBANKBAGSLOTS_CHANGED" then
BenillaBankFrame_UpdateBagRow()
elseif event == "PLAYER_MONEY" then
BenillaMoney_Set("BenillaBankFrameMoney", GetMoney())
BenillaMoney_Set("BankFrameMoney", GetMoney())
BenillaBankFrame_UpdateBagRow() -- the cost coins' afford color can flip too
elseif event == "ITEM_LOCK_CHANGED" then
if arg1 == -1 then
@@ -159,15 +161,15 @@
function BenillaBankFrame_CloseButton_OnClick()
CloseBankFrame()
HideUIPanel(getglobal("BenillaBankFrame"))
HideUIPanel(getglobal("BankFrame"))
end
-- CloseBankBagFrames (ref BankFrame.lua:119-123), over our popout naming.
function BenillaCloseBankBagFrames()
function CloseBankBagFrames()
for j = 1, BENILLA_BANK_BAG_SLOTS do
local frame = getglobal("BenillaBankBagFrame" .. j)
if frame and frame:IsShown() then frame:Hide() end
local button = getglobal("BenillaBankBagButton" .. j)
local button = getglobal("BankBagButton" .. j)
if button then button:SetChecked(nil) end
end
end
@@ -204,7 +206,7 @@
-- Only a slot actually holding a bag opens its popout — the reference's ToggleBag on
-- a bagless (or unpurchased) slot is a no-op beyond the click sound; the container
-- feed reports 0 slots exactly when no bag object sits in the slot.
if C_Container.GetContainerNumSlots(4 + id) == 0 then
if GetContainerNumSlots(4 + id) == 0 then
button:SetChecked(nil)
return
end
@@ -272,7 +274,7 @@
</Scripts>
</CheckButton>
<Frame name="BenillaBankFrame" id="-1" hidden="true">
<Frame name="BankFrame" id="-1" hidden="true">
<!-- The real 384×512 window (ref l.77-90). No <Anchors> — ShowUIPanel/SetLeftFrame SetPoints
this to the left slot on open, the same NPC-session convention as Merchant/Trainer/Mail.
id="-1" = BANK_CONTAINER: the 24 item buttons below read it via GetParent():GetID(), the
@@ -282,7 +284,7 @@
<Layer level="BACKGROUND">
<!-- NPC portrait 60×60 TOPLEFT (7,-6) (ref l.93-104), bound in OnLoad via
SetPortraitTexture(region, "npc") — the Merchant/Trainer/Gossip convention. -->
<Texture name="BenillaBankFramePortrait">
<Texture name="BankFramePortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
@@ -296,7 +298,7 @@
</Texture>
<!-- Title: the banker's name (ref BankFrameTitleText l.115-123, GameFontHighlight,
CENTER (6,233)); "Banker" until BANKFRAME_OPENED's arg1 lands. -->
<FontString name="BenillaBankFrameTitleText" inherits="GameFontHighlight" justifyH="CENTER" text="Banker">
<FontString name="BankFrameTitleText" inherits="GameFontHighlight" justifyH="CENTER" text="Banker">
<Anchors><Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="6" y="233"/></Offset></Anchor></Anchors>
</FontString>
<!-- Section labels (ref l.124-141: ITEMSLOTTEXT/BAGSLOTTEXT, GameFontNormal, CENTER
@@ -312,7 +314,7 @@
<Frames>
<!-- Close X: the house minimize-button convention (Merchant/Trainer/Mail), TOPRIGHT
(-30,-8) — see the header's deviation note on the reference's own offset. -->
<Button name="BenillaBankFrameCloseButton">
<Button name="BankFrameCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-30" y="-8"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
@@ -325,100 +327,100 @@
TOPLEFT(40,-73), 12px column gap, -7px row gap — QUOTED verbatim. Each is a
BenillaBagSlotTemplate button (BagFrame.xml) read against container -1; physIndex
is `25 - i` so button i resolves to slot i (see the header's deviation note). -->
<Button name="BenillaBankItem1" inherits="BenillaBagSlotTemplate">
<Button name="BankItem1" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="40" y="-73"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 24)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem2" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem2" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 23)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem3" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem2" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem3" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem2" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 22)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem4" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem4" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 21)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem5" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem4" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem5" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem4" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 20)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem6" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem6" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 19)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem7" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Button name="BankItem7" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 18)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem8" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem7" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem8" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem7" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 17)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem9" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem8" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem9" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem8" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 16)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem10" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem9" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem10" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem9" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 15)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem11" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem10" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem11" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem10" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 14)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem12" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem11" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem12" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem11" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 13)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem13" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Button name="BankItem13" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 12)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem14" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem13" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem14" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem13" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 11)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem15" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem14" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem15" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem14" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 10)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem16" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem15" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem16" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem15" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 9)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem17" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem16" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem17" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem16" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 8)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem18" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem17" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem18" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem17" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 7)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem19" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem13" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Button name="BankItem19" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem13" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 6)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem20" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem19" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem20" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem19" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 5)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem21" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem20" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem21" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem20" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 4)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem22" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem21" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem22" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem21" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 3)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem23" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem22" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem23" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem22" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 2)</OnLoad></Scripts>
</Button>
<Button name="BenillaBankItem24" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem23" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="BankItem24" inherits="BenillaBagSlotTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem23" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 1)</OnLoad></Scripts>
</Button>
@@ -426,28 +428,28 @@
column gap. Button id = the bag-button number (1..6, what BankButtonIDToInvSlotID
and BenillaGetBankBagTexture expect) — NOT the container id (5..10), which the
popout windows below carry instead via their own `id=`. -->
<CheckButton name="BenillaBankBagButton1" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankItem19" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-32"/></Offset></Anchor></Anchors>
<CheckButton name="BankBagButton1" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem19" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-32"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 1)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaBankBagButton2" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankBagButton1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="BankBagButton2" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 2)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaBankBagButton3" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankBagButton2" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="BankBagButton3" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton2" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 3)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaBankBagButton4" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankBagButton3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="BankBagButton4" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 4)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaBankBagButton5" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankBagButton4" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="BankBagButton5" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton4" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 5)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaBankBagButton6" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBankBagButton5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="BankBagButton6" inherits="BenillaBankBagButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 6)</OnLoad></Scripts>
</CheckButton>
@@ -455,46 +457,46 @@
window (-15,-105). House coin rig (BenillaMoney_Set) in place of the reference's
SmallMoneyFrameTemplate; the Purchase button is a local UI-Panel-Button redraw (no
UIPanelButtonTemplate ships — the house convention every NPC window uses). -->
<Frame name="BenillaBankFramePurchaseInfo">
<Frame name="BankFramePurchaseInfo">
<Size><AbsDimension x="300" y="72"/></Size>
<Anchors><Anchor point="CENTER" relativeTo="BenillaBankFrame" relativePoint="CENTER"><Offset><AbsDimension x="-15" y="-105"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="CENTER" relativeTo="BankFrame" relativePoint="CENTER"><Offset><AbsDimension x="-15" y="-105"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString inherits="GameFontHighlight" text="Do you wish to purchase space for an additional bag?" justifyH="LEFT">
<Size><AbsDimension x="280" y="0"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="10" y="20"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaBankFrameCostLabel" inherits="GameFontNormal" text="Cost:" justifyH="LEFT">
<FontString name="BankFrameCostLabel" inherits="GameFontNormal" text="Cost:" justifyH="LEFT">
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="30" y="-10"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="BenillaBankFrameCostMoneyCoin1">
<Frame name="BankFrameCostMoneyCoin1">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaBankFrameCostLabel" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="BankFrameCostLabel" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0" right="0.25" top="0" bottom="1"/></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<Frame name="BenillaBankFrameCostMoneyCoin2">
<Frame name="BankFrameCostMoneyCoin2">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaBankFrameCostMoneyCoin1" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="BankFrameCostMoneyCoin1" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0.25" right="0.5" top="0" bottom="1"/></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<Frame name="BenillaBankFrameCostMoneyCoin3">
<Frame name="BankFrameCostMoneyCoin3">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaBankFrameCostMoneyCoin2" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="BankFrameCostMoneyCoin2" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0.5" right="0.75" top="0" bottom="1"/></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<Button name="BenillaBankFramePurchaseButton" text="Purchase">
<Button name="BankFramePurchaseButton" text="Purchase">
<Size><AbsDimension x="124" y="21"/></Size>
<Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-20" y="-10"/></Offset></Anchor></Anchors>
<NormalFont inherits="GameFontNormal"/>
@@ -512,7 +514,7 @@
<!-- The player's own money (ref BankFrameMoneyFrame, l.501-509: BOTTOMRIGHT(-38,102)).
Same right-to-left coin chain as MerchantFrame's purse — copper-first, right-flush
off Coin1 (BenillaMoney_Set's own module doc, MerchantFrame.xml). -->
<Frame name="BenillaBankFrameMoneyCoin1">
<Frame name="BankFrameMoneyCoin1">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-51" y="102"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
@@ -520,17 +522,17 @@
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<Frame name="BenillaBankFrameMoneyCoin2">
<Frame name="BankFrameMoneyCoin2">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="BenillaBankFrameMoneyCoin1" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="RIGHT" relativeTo="BankFrameMoneyCoin1" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0.25" right="0.5" top="0" bottom="1"/></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<Frame name="BenillaBankFrameMoneyCoin3">
<Frame name="BankFrameMoneyCoin3">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="BenillaBankFrameMoneyCoin2" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="RIGHT" relativeTo="BankFrameMoneyCoin2" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0" right="0.25" top="0" bottom="1"/></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
@@ -538,14 +540,14 @@
</Frame>
</Frames>
<Scripts>
<OnLoad>BenillaBankFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaBankFrame_OnEvent(self)</OnEvent>
<OnLoad>BankFrame_OnLoad()</OnLoad>
<OnEvent>BankFrame_OnEvent(event)</OnEvent>
<!-- OnShow/OnHide (ref l.518-526): OpenBackpack/CloseBackpack per the 0561 contract
(MerchantFrame.xml's own placement is the model); the reference OnHide order
(CloseBankBagFrames(); CloseBankFrame(); PlaySound(...)) kept exactly, with
CloseBackpack folded in at the Merchant convention's spot. -->
<OnShow>OpenBackpack(); PlaySound("igMainMenuOpen")</OnShow>
<OnHide>BenillaCloseBankBagFrames(); CloseBankFrame(); CloseBackpack(); PlaySound("igMainMenuClose")</OnHide>
<OnHide>CloseBankBagFrames(); CloseBankFrame(); CloseBackpack(); PlaySound("igMainMenuClose")</OnHide>
</Scripts>
</Frame>
@@ -0,0 +1,134 @@
<Ui>
<!--
Interface\FrameXML\BasicControls.xml, transcribed — the reference's second file, right
after Fonts.xml, and one of the smallest in FrameXML. It carries four things an addon can
reach and we had none of:
TEXT(text) the identity function 1.12 wraps every displayed string in. **26 corpus
addons call it**, because half the shipped FrameXML does and they copied
the idiom. Missing, it is `attempt to call global 'TEXT'`.
message(text) the client's "show the player this" dialog. Its own popup, not a chat
line. Present in 64 corpus files — but read the line before ranking it
(1207): 55 of those are copies of `Dewdrop-2.0.lua`, and the genuine
callers are about ten addons.
_ERRORMESSAGE what `message` is implemented in terms of, and the reference's default
error handler. Two corpus addons replace it outright (ImprovedErrorFrame
is one), which they cannot do to a name that does not exist.
ScriptErrors the dialog both of the above put text into, plus DialogBoxFrame and the
DialogButton*Texture trio it is built from — three of which the corpus
also inherits from directly (`inherits_demand`, 2 addons each).
The three fonts this needs — GameFontRedLarge, DialogButtonNormalText,
DialogButtonHighlightText — are already in Fonts.xml, so this file is self-contained
against the one that loads before it, exactly as it is in the reference's own toc order.
-->
<Script><![CDATA[
-- ── _ERRORMESSAGE / message (ref BasicControls.xml l.5-15) ─────────────────────────────
--
-- VERBATIM, including `debuginfo()`. That line was guarded here when this file landed,
-- because `debuginfo` was an engine global we lacked and whose behaviour was uncarved —
-- and declaring a no-op under a real 1.12 name is exactly the "capability absent without a
-- failure" class 1203 named. wow-re has since carved it (2026-08-11): `0x7027f0` is
-- `33 c0 c3` — `xor eax,eax; ret`, three bytes, no call, no memory write. It genuinely
-- does nothing and returns nothing, and five of its `debug*` siblings are byte-identical.
-- So the no-op in `stdlib.rs` is the REFERENCE's no-op, the guard was measuring our own
-- ignorance rather than a real branch, and it is gone.
function _ERRORMESSAGE(msg)
debuginfo()
if not ScriptErrors:IsVisible() then
ScriptErrors_Message:SetText(msg)
ScriptErrors:Show()
end
return msg
end
function message(text)
_ERRORMESSAGE(text)
end
-- ── AND ONE LINE OF THE REFERENCE DELIBERATELY NOT TRANSCRIBED ─────────────────────────
-- The reference ends this block with `seterrorhandler(_ERRORMESSAGE);`. We do not.
--
-- benilla's default handler is `__benilla_script_error` (decision 1195), which pushes into
-- `UiScript::errors()` — the channel the corpus harness's whole blocker table is built on,
-- and the only place a Lua fault is visible to any instrument we own. Installing
-- `_ERRORMESSAGE` would route every runtime error into a modal dialog instead and blind
-- `blockers`, `demand` and every A/B that depends on them, to buy a dialog nobody has asked
-- for yet.
--
-- The addon-facing behaviour is unchanged either way: `geterrorhandler()` still answers a
-- callable (the 1195 pair), `geterrorhandler()(msg)` still reports, and an addon that
-- installs its OWN handler with `seterrorhandler` still wins outright. What differs is only
-- which handler is installed when nobody has chosen — and that is ours to choose while the
-- error channel is load-bearing for the arc's instruments. Revisit when a player-facing
-- Lua-error surface exists; the one line is the whole change.
-- ── TEXT (ref l.22-26, verbatim) ──────────────────────────────────────────────────────
-- "This function is used to translate text (actually it's already translated as-is)" is
-- the reference's own comment. It really is the identity function: 1.12 shipped one
-- localisation per client and the hook was never used.
function TEXT(text)
return text
end
]]></Script>
<!-- The dialog button faces (ref l.28-37). Same three-piece art as UIPanelButtonTemplate's,
with their own names because the reference's dialogs inherit these directly. -->
<Texture name="DialogButtonNormalTexture" file="Interface\Buttons\UI-Panel-Button-Up" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="DialogButtonPushedTexture" file="Interface\Buttons\UI-Panel-Button-Down" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="DialogButtonHighlightTexture" file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<!-- DialogBoxFrame (ref l.38-77): a centered tooltip-bordered box with one OKAY button that
hides its parent. Virtual — ScriptErrors below is the only thing in shipped FrameXML that
inherits it, but two corpus addons inherit it too. -->
<Frame name="DialogBoxFrame" virtual="true" toplevel="true" frameStrata="DIALOG" hidden="true">
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<EdgeSize><AbsValue val="32"/></EdgeSize>
<TileSize><AbsValue val="32"/></TileSize>
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
</Backdrop>
<Frames>
<Button name="$parentButton">
<Size><AbsDimension x="128" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="16"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
this:GetParent():Hide()
</OnClick>
</Scripts>
<NormalTexture inherits="DialogButtonNormalTexture"/>
<PushedTexture inherits="DialogButtonPushedTexture"/>
<HighlightTexture inherits="DialogButtonHighlightTexture"/>
<ButtonText text="OKAY"/>
<NormalFont inherits="DialogButtonNormalText"/>
<HighlightFont inherits="DialogButtonHighlightText"/>
</Button>
</Frames>
</Frame>
<!-- "This is the general scripting error dialog" (ref l.79-100), verbatim. -->
<Frame name="ScriptErrors" inherits="DialogBoxFrame">
<Size><AbsDimension x="384" y="128"/></Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="ScriptErrors_Message" inherits="GameFontRedLarge" justifyH="CENTER" justifyV="TOP" nonspacewrap="true">
<Size><AbsDimension x="360" y="0"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</Ui>
+459 -212
View File
@@ -1,56 +1,110 @@
<Ui>
<!--
The player buff/debuff bar (decisions 0255/0257 — the player aura arc). Geometry, textures,
colours, and every timing constant are QUOTED from the real 1.12 BuffFrame (extracted
Interface\FrameXML\BuffFrame.xml/.lua), turned onto benilla's own aura feed. Same shape as
UnitFrames.xml: the reference's data path was two 1.12-only calls (GetPlayerBuff +
GetPlayerBuffTexture/…Applications/…DispelType/…TimeLeft) that we did NOT implement; our
engine exposes the one Era aura function instead (decision 0068), so the handlers here read
UnitBuff/UnitDebuff and the countdown rides GetTime(), exactly as the cast bar does.
The player buff/debuff bar and the temporary-weapon-enchant row (decisions 0255/0257 — the
player aura arc). Geometry, textures, colours, every timing constant AND now the data path
are the real 1.12 BuffFrame's (extracted Interface\FrameXML\BuffFrame.xml/.lua).
SCOPE (decision 0255): the LOCAL PLAYER only. The reference's temporary-weapon-enchant frame
(TemporaryEnchantFrame / TempEnchant1-2) is deliberately out — there is no weapon-enchant wire
yet — so the bar is anchored to the reference's *no-enchant* steady state: 16 buff buttons
(two rows of 8) above 8 debuff buttons, right-aligned under the minimap at UIParent TOPRIGHT
(-175,-13), the point the reference's BuffFrame snaps to when no temp enchant is present
(BuffFrame_Enchant_OnUpdate's no-enchant branch).
THE INDEX SPACE — the thing to understand before touching a handler. There are TWO numbers
here and they are not the same number:
DURATIONS ARE A SETTING NOW (decision 1139). 0255 shipped the durations-shown geometry
* 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 are the
reference's, on the reference's 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 (BenillaBuffButtons_UpdatePositions). Our default is "1", the reference's "0": the
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 list UnitBuff/UnitDebuff enumerate is the app's insertion-ordered display
cache (crate::ui_aura, decision 0257): a new aura appends at the end, a dropped one repacks —
NOT ascending slot. Button N simply shows the Nth buff/debuff of that list; the bar's identity
is the app's, not this file's.
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. expirationTime (UnitBuff's 6th return) is 0 for a permanent aura — the
1.12 wire sends no duration packet for one (byte-verified, decision 0257), which is exactly the
reference's "until cancelled". So `expirationTime > 0` is our untilCancelled test: a timed aura
counts down and flashes under 31s; a permanent one shows no timer and never flashes.
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 from UnitBuff/UnitDebuff in OnUpdate, every frame,
by every visible button. That is the reference's own division of labour — BuffButton_Update
caches the buff INDEX 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 (0846).
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 it is now literally its code —
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") + CancelUnitBuff (the Era name for the
reference's CancelPlayerBuff) — resolved within the button's own filter so a debuff button's id
never resolves against the buff list. 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.
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.
DEVIATIONS from the reference .lua (noted, not bugs):
DEVIATIONS from the reference .lua/.xml (noted, not bugs):
- 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
@@ -70,7 +124,8 @@
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. UnitDebuff's debuffType return keys this table.
-- 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 },
@@ -102,36 +157,27 @@
-- 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 BenillaBuffFrame_OnLoad(frame)
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
BenillaBuffButtons_UpdatePositions()
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 reference reaches those two geometries by re-anchoring through its TempEnchant chain,
-- which we do not have; same two numbers, our own anchors, and 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 BenillaBuffButtons_UpdatePositions()
local gutter = -5
if SHOW_BUFF_DURATIONS == "1" then
gutter = -15
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
BuffButton8:SetPoint("TOP", "BuffButton0", "BOTTOM", 0, gutter)
BuffButton16:SetPoint("TOPRIGHT", "BuffButton8", "BOTTOMRIGHT", 0, gutter)
end
function BenillaBuffFrame_OnUpdate(elapsed)
BuffFrameFlashTime = BuffFrameFlashTime - elapsed
if BuffFrameFlashTime < 0 then
local overtime = -BuffFrameFlashTime
@@ -155,148 +201,250 @@
BUFF_ALPHA_VALUE = (BUFF_ALPHA_VALUE * (1 - BUFF_MIN_ALPHA)) + BUFF_MIN_ALPHA
end
-- A button caches its filter (set in the template OnLoad), its per-filter id (the XML `id`,
-- 1-based to match our UnitBuff/UnitDebuff index), takes right-clicks, and listens for the
-- aura change. The first paint hides it until the feed reports an aura.
function BenillaBuffButton_OnLoad(button)
button:RegisterForClicks("RightButtonUp")
button:RegisterEvent("UNIT_AURA")
BenillaBuffButton_Update(button)
end
function BenillaBuffButton_OnEvent(button)
if arg1 == "player" then
BenillaBuffButton_Update(button)
end
end
-- The button's aura: the Nth of its own filter, in the app's display order. The ONE lookup
-- both handlers use — the ref's BuffButton_Update and BuffButton_OnUpdate likewise resolve
-- the same buffIndex, one on the event and one per frame.
function BenillaBuffButton_Aura(button)
local id = button:GetID()
if button.buffFilter == "HARMFUL" then
return UnitDebuff("player", id)
end
return UnitBuff("player", id)
end
-- Pull the button's aura and repaint the discrete parts: icon, stack count, debuff-border
-- tint. Those are what the aura-change event exists for. The countdown is NOT cached here —
-- see BenillaBuffButton_OnUpdate. No aura ⇒ hide the button (which hides its icon/border
-- with it) and blank its duration string.
-- BuffFrame_UpdateDuration (BuffFrame.lua l.247-259), gate and all: the timer draws only when
-- the setting is on AND there is a live one. `timeLeft` nil covers both of the other cases —
-- no aura in this slot, and a permanent aura (no duration on the wire). Hidden, not blanked:
-- see the header's deviation note.
function BenillaBuffButton_SetDuration(button, timeLeft)
local durationText = getglobal(button:GetName() .. "Duration")
if SHOW_BUFF_DURATIONS ~= "1" or not timeLeft then
durationText:Hide()
return
end
durationText:SetText(SecondsToTimeAbbrev(timeLeft))
if timeLeft < BUFF_DURATION_WARNING_TIME then
durationText:SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
-- 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
durationText:SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
BuffButton8:SetPoint("TOP", "TempEnchant1", "BOTTOM", 0, -5)
BuffButton16:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, -70)
end
durationText:Show()
end
function BenillaBuffButton_Update(button)
local name, icon, count, debuffType = BenillaBuffButton_Aura(button)
-- 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 = getglobal(this:GetName() .. "Duration")
if not name then
button:Hide()
BenillaBuffButton_SetDuration(button, nil)
-- -1, not nil: the miss sentinel the whole family agrees on.
if buffIndex < 0 then
this:Hide()
buffDuration:Hide()
return
end
button:SetAlpha(1.0)
button:Show()
this:SetAlpha(1.0)
this:Show()
if SHOW_BUFF_DURATIONS == "1" then
buffDuration:Show()
else
buffDuration:Hide()
end
getglobal(button:GetName() .. "Icon"):SetTexture(icon or BENILLA_FALLBACK_ICON)
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 we skip it).
local border = getglobal(button:GetName() .. "Border")
if border then
-- $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"]
border:SetVertexColor(color.r, color.g, color.b)
debuffSlot:SetVertexColor(color.r, color.g, color.b)
end
-- Stack count, shown only above 1 (BuffButton_Update l.95-102).
local countText = getglobal(button:GetName() .. "Count")
if count and count > 1 then
countText:SetText(count)
-- 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
countText:SetText("")
buffCount:SetText("")
end
if GameTooltip and GameTooltip:IsOwned(this) then
GameTooltip:SetPlayerBuff(buffIndex)
end
end
-- Per-frame: RE-READ the expiration (never a cached one), count it down against GetTime(),
-- flash in the last 31s, and write the abbreviated timer (highlight-white under 60s).
-- expirationTime == 0 is a permanent aura (no duration on the wire) — our "until
-- cancelled": no timer, no flash.
-- 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: ref
-- BuffFrame.lua l.128-130 calls GetPlayerBuffTimeLeft(this.buffIndex) inside
-- BuffButton_OnUpdate, caching only the INDEX on PLAYER_AURAS_CHANGED. A duration that
-- changes without any discrete change — a recast, a DoT reapplied, cast pushback, the
-- 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).
function BenillaBuffButton_OnUpdate(button)
local name, _, _, _, _, expirationTime = BenillaBuffButton_Aura(button)
if not name or not expirationTime or expirationTime <= 0 then
button:SetAlpha(1.0)
BenillaBuffButton_SetDuration(button, nil)
--
-- 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 buffDuration = getglobal(this:GetName() .. "Duration")
if this.untilCancelled == 1 then
buffDuration:Hide()
return
end
local timeLeft = expirationTime - GetTime()
if timeLeft < 0 then
timeLeft = 0
end
-- The flash is NOT gated on the setting: the reference pulses a button in its last 31s
-- from BuffButton_OnUpdate, whether or not a timer is drawn under it (l.128-138), and
-- with the timers off it is the only thing left saying "about to drop".
local buffIndex = this.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".
if timeLeft < BUFF_WARNING_TIME then
button:SetAlpha(BUFF_ALPHA_VALUE)
this:SetAlpha(BUFF_ALPHA_VALUE)
else
button:SetAlpha(1.0)
this:SetAlpha(1.0)
end
BuffFrame_UpdateDuration(this, timeLeft)
if BuffFrameUpdateTime > 0 then
return
end
if GameTooltip and GameTooltip:IsOwned(this) then
GameTooltip:SetPlayerBuff(buffIndex)
end
BenillaBuffButton_SetDuration(button, timeLeft)
end
-- Right-click cancels (BuffButton_OnClick → CancelPlayerBuff). Resolve within the button's own
-- filter so a debuff button's id can't reach into the buff list; the app sends CMSG_CANCEL_AURA
-- for a cancelable buff and drops the rest.
-- Hover: the aura tooltip through the engine channel (decision 0274 P2 — the verified
-- aura variant: white AuraDescription + the SetPlayerBuff-only remaining line). The
-- OnUpdate re-poll keeps the remaining line ticking while hovered. Anchor: the ref's
-- BuffButton hangs the plate BELOW-left (ref BuffFrame.xml l.37 ANCHOR_BOTTOMLEFT —
-- the buff row sits at the screen's top-right, so the tooltip drops under it).
function BenillaBuffButton_OnEnter(button)
if not GameTooltip then return end -- engine tests load file subsets
GameTooltip:SetOwner(button, "ANCHOR_BOTTOMLEFT")
GameTooltip:SetPlayerBuff(button:GetID(), button.buffFilter)
-- 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
function BenillaBuffButton_OnLeave(button)
GameTooltip:Hide()
-- The temporary-weapon-enchant row (ref l.162-233). GetWeaponEnchantInfo is the whole data
-- path: two flags, two remaining-MILLISECOND counts, two charge counts. Off hand takes
-- TempEnchant1 and main hand the next free slot, so with both enchanted the main hand sits
-- to the LEFT of the off hand — the reference's own ordering.
function BuffFrame_Enchant_OnUpdate(elapsed)
local hasMainHandEnchant, mainHandExpiration, mainHandCharges, hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo()
-- No enchants, kick out early — and park BuffFrame back on its resting point.
if not hasMainHandEnchant and not hasOffHandEnchant then
TempEnchant1:Hide()
TempEnchant1Duration:Hide()
TempEnchant2:Hide()
TempEnchant2Duration:Hide()
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, 0)
return
end
local enchantButton
local textureName
local enchantIndex = 0
if hasOffHandEnchant then
enchantIndex = enchantIndex + 1
textureName = GetInventoryItemTexture("player", 17)
TempEnchant1:SetID(17)
TempEnchant1Icon:SetTexture(textureName or BENILLA_FALLBACK_ICON)
TempEnchant1:Show()
if offHandExpiration then
offHandExpiration = offHandExpiration / 1000
end
BuffFrame_UpdateDuration(TempEnchant1, offHandExpiration)
if offHandExpiration and offHandExpiration < BUFF_WARNING_TIME then
TempEnchant1:SetAlpha(BUFF_ALPHA_VALUE)
else
TempEnchant1:SetAlpha(1.0)
end
end
if hasMainHandEnchant then
enchantIndex = enchantIndex + 1
enchantButton = getglobal("TempEnchant" .. enchantIndex)
textureName = GetInventoryItemTexture("player", 16)
enchantButton:SetID(16)
getglobal(enchantButton:GetName() .. "Icon"):SetTexture(textureName or BENILLA_FALLBACK_ICON)
enchantButton:Show()
if mainHandExpiration then
mainHandExpiration = mainHandExpiration / 1000
end
BuffFrame_UpdateDuration(enchantButton, mainHandExpiration)
if mainHandExpiration and mainHandExpiration < BUFF_WARNING_TIME then
enchantButton:SetAlpha(BUFF_ALPHA_VALUE)
else
enchantButton:SetAlpha(1.0)
end
end
-- Hide unused enchants
for i = enchantIndex + 1, 2 do
getglobal("TempEnchant" .. i):Hide()
getglobal("TempEnchant" .. i .. "Duration"):Hide()
end
-- Position buff frame: the row is `enchantIndex` icons wide, and the bar starts one
-- 5px gutter to its left.
TemporaryEnchantFrame:SetWidth(enchantIndex * 32)
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPLEFT", -5, 0)
end
function BenillaBuffButton_OnClick(button)
CancelUnitBuff("player", button:GetID(), button.buffFilter)
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.
function BuffFrame_UpdateDuration(buffButton, timeLeft)
local duration = getglobal(buffButton:GetName() .. "Duration")
if SHOW_BUFF_DURATIONS == "1" and timeLeft then
duration:SetText(SecondsToTimeAbbrev(timeLeft))
if timeLeft < BUFF_DURATION_WARNING_TIME then
duration:SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
else
duration:SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
end
duration:Show()
else
duration:Hide()
end
end
]]></Script>
<!-- 30x30 icon + a bottom-right stack count. BuffButtonTemplate, BuffFrame.xml l.4-44 (minus the
tooltip OnEnter/OnLeave — a later arc). -->
<Button name="BenillaBuffButtonTemplate" virtual="true">
<!-- 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">
@@ -309,27 +457,38 @@
</Layer>
</Layers>
<Scripts>
<OnEvent>BenillaBuffButton_OnEvent(this)</OnEvent>
<OnUpdate>BenillaBuffButton_OnUpdate(this)</OnUpdate>
<OnClick>BenillaBuffButton_OnClick(this)</OnClick>
<OnEnter>BenillaBuffButton_OnEnter(this)</OnEnter>
<OnLeave>BenillaBuffButton_OnLeave(this)</OnLeave>
<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 (BuffButtonHelpful, l.50-57). -->
<Button name="BenillaBuffButtonHelpful" inherits="BenillaBuffButtonTemplate" virtual="true">
<!-- Helpful variant: filter HELPFUL, no border (ref l.50-57). -->
<Button name="BuffButtonHelpful" inherits="BuffButtonTemplate" virtual="true">
<Scripts>
<OnLoad>
this.buffFilter = "HELPFUL"
BenillaBuffButton_OnLoad(this)
BuffButton_OnLoad()
</OnLoad>
</Scripts>
</Button>
<!-- Harmful variant: filter HARMFUL + the dispel-tinted border overlay (BuffButtonHarmful,
l.58-82). 33x32 UI-Debuff-Overlays, the exact reference TexCoords, tint set at runtime. -->
<Button name="BenillaBuffButtonHarmful" inherits="BenillaBuffButtonTemplate" virtual="true">
<!-- 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">
@@ -344,23 +503,69 @@
<Scripts>
<OnLoad>
this.buffFilter = "HARMFUL"
BenillaBuffButton_OnLoad(this)
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),
but region TEMPLATES are not in the v1 loader's object model (inherits= on a FontString
resolves Font objects only), and the anchor is re-seated by BenillaBuffFrame_OnLoad
anyway — so each instance inherits the Font object directly. Same resolved font, no
indirection; the loader gap is the named v1 limit, warned once if reintroduced. -->
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). 16 helpful buttons in
two rows of 8 (ids 1-16), then 8 harmful (ids 1-8), rows 45px apart: button0 top-right;
buttons 1-7 step left (-5 gap); button8 opens row 2 under button0 (-15 gutter for the timer);
button16 opens the debuff row under button8. The 24 duration strings live in the frame's
ARTWORK layer (over the icons' BACKGROUND), named to pair with their button.
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>
@@ -398,85 +603,127 @@
</Layer>
</Layers>
<Frames>
<!-- Row 0: helpful buttons 0-7, right to left. -->
<Button name="BuffButton0" inherits="BenillaBuffButtonHelpful" id="1">
<!-- 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="BenillaBuffButtonHelpful" id="2">
<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="BenillaBuffButtonHelpful" id="3">
<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="BenillaBuffButtonHelpful" id="4">
<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="BenillaBuffButtonHelpful" id="5">
<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="BenillaBuffButtonHelpful" id="6">
<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="BenillaBuffButtonHelpful" id="7">
<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="BenillaBuffButtonHelpful" id="8">
<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 buttons 8-15. Button8 opens the row under button0, with the 15px timer gutter. -->
<Button name="BuffButton8" inherits="BenillaBuffButtonHelpful" id="9">
<Anchors><Anchor point="TOP" relativeTo="BuffButton0" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
<!-- 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="BenillaBuffButtonHelpful" id="10">
<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="BenillaBuffButtonHelpful" id="11">
<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="BenillaBuffButtonHelpful" id="12">
<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="BenillaBuffButtonHelpful" id="13">
<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="BenillaBuffButtonHelpful" id="14">
<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="BenillaBuffButtonHelpful" id="15">
<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="BenillaBuffButtonHelpful" id="16">
<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 buttons 16-23. Button16 opens the debuff row under button8, right-aligned. -->
<Button name="BuffButton16" inherits="BenillaBuffButtonHarmful" id="1">
<Anchors><Anchor point="TOPRIGHT" relativeTo="BuffButton8" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
<!-- 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="BenillaBuffButtonHarmful" id="2">
<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="BenillaBuffButtonHarmful" id="3">
<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="BenillaBuffButtonHarmful" id="4">
<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="BenillaBuffButtonHarmful" id="5">
<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="BenillaBuffButtonHarmful" id="6">
<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="BenillaBuffButtonHarmful" id="7">
<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="BenillaBuffButtonHarmful" id="8">
<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>BenillaBuffFrame_OnLoad(this)</OnLoad>
<OnUpdate>BenillaBuffFrame_OnUpdate(arg1)</OnUpdate>
<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>
File diff suppressed because it is too large Load Diff
+218 -40
View File
@@ -17,6 +17,173 @@
"Say:" with static insets.
-->
<Script><![CDATA[
-- ── SlashCmdList — the addon slash-command registry (decision 1195) ────────────────────
-- Declared HERE, not in Rust, because the 1.12 client declares it in ChatFrame.lua and the
-- captured `_G` attributes it `table framexml`. An addon's registration is three lines at
-- FILE SCOPE:
-- SLASH_MYADDON1 = "/myaddon"
-- SlashCmdList["MYADDON"] = function(msg) ... end
-- so a missing table is not a missing feature, it is `attempt to index global
-- 'SlashCmdList'` on the addon's very first chunk — 26 of a 218-addon corpus died there.
-- The dispatch walk is `benilla_ui::script::slash`, transcribing ChatEdit_ParseText's.
--
-- It starts EMPTY: benilla's own commands are dispatched host-side (decision 0881's table)
-- where the reference registers them here. That divergence is recorded in `slash.rs`.
SlashCmdList = {}
-- ── ChatTypeInfo — the per-chat-type table addons read colors out of ──────────────────
-- Declared HERE because the 1.12 client declares it at the top of ChatFrame.lua, and it
-- is an ADDON-FACING table before it is ours: `local info = ChatTypeInfo["SAY"]` then
-- `frame:AddMessage(text, info.r, info.g, info.b, info.id)` is the standard way an addon
-- prints a colored line.
--
-- Measured, and the raw count is misleading enough to be worth writing down: 234 corpus
-- sites, but **192 of them are one line** — AceEvent-2.0's
-- `ChatTypeInfo and ChatTypeInfo.WHISPER and ChatTypeInfo.WHISPER.r`, a GUARDED readiness
-- probe that copy-pastes into 64 addons and cannot raise. The reads that actually index
-- the table are 42 sites in 6 addons (ChatLog, oRA2/Participant's RaidWarn, Glory-2.0's
-- two FuBar hosts, FuBar_HeyFu) — those are `attempt to index global 'ChatTypeInfo'`
-- without it, and they are the reason this exists.
--
-- Three columns, three sources:
-- sticky the reference's own literals (ChatFrame.lua l.10-114) — the five sticky types
-- are SAY/PARTY/RAID/GUILD/BATTLEGROUND, which `ui_chat::edit` already quotes.
-- id GetChatTypeIndex's 1-based index into the engine's 94-entry color registry
-- (wow-re `chat-color-table.md`, static table 0x804710) — the ref sets it in
-- ChatFrame_OnLoad, and AddMessage's 5th argument is what consumes it.
-- r,g,b that registry's byte defaults, /255 — the engine hands Lua 0..1 floats
-- (it re-normalizes with the f32 1/255 at 0x8026c8).
--
-- ONE DIVERGENCE, and it is a shortcut through a two-step: the reference's OnLoad writes
-- 1.0/1.0/1.0 into every entry and waits for the engine's UPDATE_CHAT_COLOR storm to fill
-- the real values in (ChatFrame.lua l.1349 is the only writer in the whole default UI).
-- benilla has no chat-color settings surface and nothing that fires that event, so the
-- values are seeded at their END STATE directly. `ui_chat::event::default_color` is the
-- same wow-re table rendered host-side for our own feed, and
-- `chat_type_info_matches_the_host_color_table` fails if the two ever drift.
--
-- REPLY and COMBAT_ERROR are the two keys FrameXML declares that the engine's registry
-- does NOT contain, so GetChatTypeIndex answers 0 for both and no UPDATE_CHAT_COLOR is
-- ever fired for either name. They do NOT share a fate, and the first version of this
-- block got that wrong by generalising:
-- COMBAT_ERROR nothing writes it, ever — id 0, white, on the real client too.
-- REPLY the UPDATE_CHAT_COLOR handler MIRRORS WHISPER INTO IT by hand
-- (ChatFrame.lua l.1357-1365: `if strupper(arg1) == "WHISPER" then
-- info = ChatTypeInfo["REPLY"] ... end`). So its end state is WHISPER's
-- FF80FF, which is what the /r header shows, and seeding it white was a
-- bug the comment above it was busy justifying.
-- CHANNEL1..CHANNEL10 are the boot-seeded extras (indices 95-104), each colored from the
-- live CHANNEL entry — FFC0C0.
ChatTypeInfo = {}
local function chatType(name, sticky, id, r, g, b)
ChatTypeInfo[name] = { sticky = sticky, id = id, r = r / 255, g = g / 255, b = b / 255 }
end
chatType("SAY", 1, 1, 255, 255, 255)
chatType("PARTY", 1, 2, 170, 170, 255)
chatType("RAID", 1, 3, 255, 127, 0)
chatType("GUILD", 1, 4, 64, 255, 64)
chatType("OFFICER", 0, 5, 64, 192, 64)
chatType("YELL", 0, 6, 255, 64, 64)
chatType("WHISPER", 0, 7, 255, 128, 255)
chatType("WHISPER_INFORM", 0, 8, 255, 128, 255)
chatType("REPLY", 0, 0, 255, 128, 255)
chatType("EMOTE", 0, 9, 255, 128, 64)
chatType("TEXT_EMOTE", 0, 10, 255, 128, 64)
chatType("SYSTEM", 0, 11, 255, 255, 0)
chatType("MONSTER_WHISPER", 0, 27, 179, 179, 179)
chatType("MONSTER_SAY", 0, 12, 255, 255, 159)
chatType("MONSTER_YELL", 0, 13, 255, 64, 64)
chatType("MONSTER_EMOTE", 0, 14, 255, 128, 64)
chatType("CHANNEL", 0, 15, 255, 192, 192)
chatType("CHANNEL_JOIN", 0, 16, 192, 128, 128)
chatType("CHANNEL_LEAVE", 0, 17, 192, 128, 128)
chatType("CHANNEL_LIST", 0, 18, 192, 128, 128)
chatType("CHANNEL_NOTICE", 0, 19, 192, 192, 192)
chatType("CHANNEL_NOTICE_USER", 0, 20, 192, 192, 192)
chatType("AFK", 0, 21, 255, 128, 255)
chatType("DND", 0, 22, 255, 128, 255)
chatType("IGNORED", 0, 23, 255, 0, 0)
chatType("SKILL", 0, 24, 85, 85, 255)
chatType("LOOT", 0, 25, 0, 170, 0)
chatType("COMBAT_ERROR", 0, 0, 255, 255, 255)
chatType("COMBAT_MISC_INFO", 0, 26, 128, 128, 255)
chatType("CHANNEL1", 0, 95, 255, 192, 192)
chatType("CHANNEL2", 0, 96, 255, 192, 192)
chatType("CHANNEL3", 0, 97, 255, 192, 192)
chatType("CHANNEL4", 0, 98, 255, 192, 192)
chatType("CHANNEL5", 0, 99, 255, 192, 192)
chatType("CHANNEL6", 0, 100, 255, 192, 192)
chatType("CHANNEL7", 0, 101, 255, 192, 192)
chatType("CHANNEL8", 0, 102, 255, 192, 192)
chatType("CHANNEL9", 0, 103, 255, 192, 192)
chatType("CHANNEL10", 0, 104, 255, 192, 192)
chatType("COMBAT_SELF_HITS", 0, 28, 255, 255, 255)
chatType("COMBAT_SELF_MISSES", 0, 29, 255, 255, 255)
chatType("COMBAT_PET_HITS", 0, 30, 255, 255, 255)
chatType("COMBAT_PET_MISSES", 0, 31, 255, 255, 255)
chatType("COMBAT_PARTY_HITS", 0, 32, 255, 255, 255)
chatType("COMBAT_PARTY_MISSES", 0, 33, 255, 255, 255)
chatType("COMBAT_FRIENDLYPLAYER_HITS", 0, 34, 255, 255, 255)
chatType("COMBAT_FRIENDLYPLAYER_MISSES", 0, 35, 255, 255, 255)
chatType("COMBAT_HOSTILEPLAYER_HITS", 0, 36, 255, 255, 255)
chatType("COMBAT_HOSTILEPLAYER_MISSES", 0, 37, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_SELF_HITS", 0, 38, 255, 47, 47)
chatType("COMBAT_CREATURE_VS_SELF_MISSES", 0, 39, 255, 47, 47)
chatType("COMBAT_CREATURE_VS_PARTY_HITS", 0, 40, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_PARTY_MISSES", 0, 41, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_CREATURE_HITS", 0, 42, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_CREATURE_MISSES", 0, 43, 255, 255, 255)
chatType("COMBAT_FRIENDLY_DEATH", 0, 44, 255, 255, 255)
chatType("COMBAT_HOSTILE_DEATH", 0, 45, 255, 255, 255)
chatType("COMBAT_XP_GAIN", 0, 46, 111, 111, 255)
chatType("COMBAT_HONOR_GAIN", 0, 82, 224, 202, 10)
chatType("SPELL_SELF_DAMAGE", 0, 47, 255, 255, 0)
chatType("SPELL_SELF_BUFF", 0, 48, 255, 255, 0)
chatType("SPELL_PET_DAMAGE", 0, 49, 255, 255, 255)
chatType("SPELL_PET_BUFF", 0, 50, 255, 255, 255)
chatType("SPELL_PARTY_DAMAGE", 0, 51, 255, 255, 255)
chatType("SPELL_PARTY_BUFF", 0, 52, 255, 255, 255)
chatType("SPELL_FRIENDLYPLAYER_DAMAGE", 0, 53, 255, 255, 255)
chatType("SPELL_FRIENDLYPLAYER_BUFF", 0, 54, 255, 255, 255)
chatType("SPELL_HOSTILEPLAYER_DAMAGE", 0, 55, 255, 255, 255)
chatType("SPELL_HOSTILEPLAYER_BUFF", 0, 56, 255, 255, 255)
chatType("SPELL_CREATURE_VS_SELF_DAMAGE", 0, 57, 202, 76, 217)
chatType("SPELL_CREATURE_VS_SELF_BUFF", 0, 58, 255, 255, 255)
chatType("SPELL_CREATURE_VS_PARTY_DAMAGE", 0, 59, 255, 255, 255)
chatType("SPELL_CREATURE_VS_PARTY_BUFF", 0, 60, 255, 255, 255)
chatType("SPELL_CREATURE_VS_CREATURE_DAMAGE", 0, 61, 255, 255, 255)
chatType("SPELL_CREATURE_VS_CREATURE_BUFF", 0, 62, 255, 255, 255)
chatType("SPELL_TRADESKILLS", 0, 63, 255, 255, 255)
chatType("SPELL_DAMAGESHIELDS_ON_SELF", 0, 64, 255, 255, 255)
chatType("SPELL_DAMAGESHIELDS_ON_OTHERS", 0, 65, 255, 255, 255)
chatType("SPELL_AURA_GONE_SELF", 0, 66, 255, 255, 255)
chatType("SPELL_AURA_GONE_PARTY", 0, 67, 255, 255, 255)
chatType("SPELL_AURA_GONE_OTHER", 0, 68, 255, 255, 255)
chatType("SPELL_ITEM_ENCHANTMENTS", 0, 69, 255, 255, 255)
chatType("SPELL_BREAK_AURA", 0, 70, 255, 255, 255)
chatType("SPELL_PERIODIC_SELF_DAMAGE", 0, 71, 255, 255, 255)
chatType("SPELL_PERIODIC_SELF_BUFFS", 0, 72, 255, 255, 255)
chatType("SPELL_PERIODIC_PARTY_DAMAGE", 0, 73, 255, 255, 255)
chatType("SPELL_PERIODIC_PARTY_BUFFS", 0, 74, 255, 255, 255)
chatType("SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE", 0, 75, 255, 255, 255)
chatType("SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", 0, 76, 255, 255, 255)
chatType("SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", 0, 77, 255, 255, 255)
chatType("SPELL_PERIODIC_HOSTILEPLAYER_BUFFS", 0, 78, 255, 255, 255)
chatType("SPELL_PERIODIC_CREATURE_DAMAGE", 0, 79, 255, 255, 255)
chatType("SPELL_PERIODIC_CREATURE_BUFFS", 0, 80, 255, 255, 255)
chatType("SPELL_FAILED_LOCALPLAYER", 0, 81, 255, 255, 255)
chatType("BG_SYSTEM_NEUTRAL", 0, 83, 255, 120, 10)
chatType("BG_SYSTEM_ALLIANCE", 0, 84, 0, 174, 239)
chatType("BG_SYSTEM_HORDE", 0, 85, 255, 0, 0)
chatType("COMBAT_FACTION_CHANGE", 0, 86, 128, 128, 255)
chatType("MONEY", 0, 87, 255, 255, 0)
chatType("RAID_LEADER", 0, 88, 255, 219, 183)
chatType("RAID_WARNING", 0, 89, 255, 219, 183)
chatType("RAID_BOSS_EMOTE", 0, 91, 255, 219, 183)
chatType("FILTERED", 0, 92, 255, 0, 0)
chatType("BATTLEGROUND", 1, 93, 255, 127, 0)
chatType("BATTLEGROUND_LEADER", 0, 94, 255, 219, 183)
-- ── BenillaFCF: the FloatingChatFrame behaviors, transcribed (ref FloatingChatFrame.lua)
-- Quoted constants: CHAT_TAB_SHOW_DELAY=0.2, CHAT_FRAME_FADE_TIME=0.15,
-- DEFAULT_CHATFRAME_ALPHA=0.25, CHAT_BUTTON_FLASH_TIME=0.5,
@@ -111,7 +278,7 @@
-- fade over 0.15s, conceal on leave; a flashing tab forces the tabs visible; the
-- selected window's textures ride the same fade toward alpha 0.25 (the chat-cache
-- default window alpha is 0 — the box only exists on hover).
function BenillaFCF_OnUpdate(elapsed)
function FCF_OnUpdate(elapsed)
local s = BenillaFCF
local x, y = GetCursorPosition()
if overDock(x, y) then
@@ -171,7 +338,8 @@
-- ChatFrame_OnUpdate transcription: the bottom button's blink while scrolled up
-- (CHAT_BUTTON_FLASH_TIME = 0.5s toggle; hidden the moment the view is AtBottom).
function BenillaChatFrame_OnUpdate(frame, elapsed)
function ChatFrame_OnUpdate(elapsed)
local frame = this
local flash = getglobal(frame:GetName() .. "BottomButtonFlash")
if not flash then return end
if frame:AtBottom() then
@@ -235,7 +403,8 @@
BENILLA_UIMENU_NUMBUTTONS = 32
BENILLA_UIMENU_TIMEOUT = 2.0
function BenillaUIMenu_Initialize(menu)
function UIMenu_Initialize()
local menu = this
menu.numButtons = 0
menu.subMenu = ""
local name = menu:GetName()
@@ -251,7 +420,8 @@
end
-- Rows past the button pool are dropped (the ref message()s; nothing we author overflows).
function BenillaUIMenu_AddButton(menu, text, shortcut, func, nested)
function UIMenu_AddButton(text, shortcut, func, nested)
local menu = this
local id = menu.numButtons + 1
if id > BENILLA_UIMENU_NUMBUTTONS then return end
menu.numButtons = id
@@ -268,12 +438,12 @@
menu:SetHeight(id * BENILLA_UIMENU_BUTTON_HEIGHT + BENILLA_UIMENU_BORDER_HEIGHT * 2)
end
function BenillaUIMenu_OnShow()
function UIMenu_OnShow()
this.timeleft = BENILLA_UIMENU_TIMEOUT
this.counting = 1
end
function BenillaUIMenu_OnUpdate(elapsed)
function UIMenu_OnUpdate(elapsed)
if this.counting == 1 then
local timeleft = (this.timeleft or BENILLA_UIMENU_TIMEOUT) - elapsed
if timeleft <= 0 then
@@ -284,29 +454,29 @@
end
end
function BenillaUIMenu_StartCounting(menu)
function UIMenu_StartCounting(menu)
menu.counting = 1
if menu.parentMenu then BenillaUIMenu_StartCounting(getglobal(menu.parentMenu)) end
if menu.parentMenu then UIMenu_StartCounting(getglobal(menu.parentMenu)) end
end
function BenillaUIMenu_StopCounting(menu)
function UIMenu_StopCounting(menu)
menu.counting = 0
menu.timeleft = BENILLA_UIMENU_TIMEOUT
if menu.parentMenu then BenillaUIMenu_StopCounting(getglobal(menu.parentMenu)) end
if menu.parentMenu then UIMenu_StopCounting(getglobal(menu.parentMenu)) end
end
function BenillaUIMenuButton_OnLoad()
function UIMenuButton_OnLoad()
this:SetPoint("TOP", this:GetParent():GetName(), "TOP", 0,
-((this:GetID() - 1) * BENILLA_UIMENU_BUTTON_HEIGHT) - BENILLA_UIMENU_BORDER_HEIGHT)
end
function BenillaUIMenuButton_OnClick()
function UIMenuButton_OnClick()
if this.func then this.func() end
this:GetParent():Hide()
PlaySound("UChatScrollButton")
end
function BenillaUIMenuButton_OnEnter()
function UIMenuButton_OnEnter()
local nested = this.nested
if nested then
local menu = getglobal(nested)
@@ -317,15 +487,15 @@
menu:SetPoint("BOTTOMLEFT", this:GetName(), "BOTTOMRIGHT", 10, -12)
menu:Show()
end
BenillaUIMenu_StopCounting(menu)
UIMenu_StopCounting(menu)
else
BenillaUIMenu_StopCounting(this:GetParent())
UIMenu_StopCounting(this:GetParent())
end
end
function BenillaUIMenuButton_OnLeave()
BenillaUIMenu_StartCounting(this:GetParent())
if this.nested then BenillaUIMenu_StartCounting(getglobal(this.nested)) end
function UIMenuButton_OnLeave()
UIMenu_StartCounting(this:GetParent())
if this.nested then UIMenu_StartCounting(getglobal(this.nested)) end
end
-- ── The chat menu rows (ref ChatMenu_* in ChatFrame.lua, adapted): every type row
@@ -342,16 +512,16 @@
eb:SetText(prefix .. (eb:GetText() or ""))
end
function BenillaChatMenu_OnLoad()
BenillaUIMenu_Initialize(this)
BenillaUIMenu_AddButton(this, "Say", "/s", function() BenillaChatMenu_Type("/s ") end)
BenillaUIMenu_AddButton(this, "Party Chat", "/p", function() BenillaChatMenu_Type("/p ") end)
BenillaUIMenu_AddButton(this, "Guild Chat", "/g", function() BenillaChatMenu_Type("/g ") end)
BenillaUIMenu_AddButton(this, "Yell", "/y", function() BenillaChatMenu_Type("/y ") end)
BenillaUIMenu_AddButton(this, "Whisper", "/w", function() BenillaChatMenu_Type("/w ") end)
BenillaUIMenu_AddButton(this, "Emote", "/e", function() BenillaChatMenu_Type("/e ") end, "EmoteMenu")
BenillaUIMenu_AddButton(this, "Reply", "/r", function() BenillaChatMenu_Type("/r ") end)
BenillaUIMenu_AddButton(this, "Voice Emote", nil, nil, "VoiceMacroMenu")
function ChatMenu_OnLoad()
UIMenu_Initialize()
UIMenu_AddButton("Say", "/s", function() BenillaChatMenu_Type("/s ") end)
UIMenu_AddButton("Party Chat", "/p", function() BenillaChatMenu_Type("/p ") end)
UIMenu_AddButton("Guild Chat", "/g", function() BenillaChatMenu_Type("/g ") end)
UIMenu_AddButton("Yell", "/y", function() BenillaChatMenu_Type("/y ") end)
UIMenu_AddButton("Whisper", "/w", function() BenillaChatMenu_Type("/w ") end)
UIMenu_AddButton("Emote", "/e", function() BenillaChatMenu_Type("/e ") end, "EmoteMenu")
UIMenu_AddButton("Reply", "/r", function() BenillaChatMenu_Type("/r ") end)
UIMenu_AddButton("Voice Emote", nil, nil, "VoiceMacroMenu")
end
-- An emote row submits its /command through the REAL chat pipeline (SubmitChatInput →
@@ -379,11 +549,11 @@
end
function BenillaOnEmoteMenuLoad(list)
BenillaUIMenu_Initialize(this)
UIMenu_Initialize()
this.parentMenu = "ChatMenu"
for _, cmd in ipairs(list) do
local c = cmd
BenillaUIMenu_AddButton(this, cmd, nil, function() BenillaEmoteMenu_Click(c) end)
UIMenu_AddButton(cmd, nil, function() BenillaEmoteMenu_Click(c) end)
end
end
]]></Script>
@@ -560,8 +730,8 @@
if event == "EXECUTE_CHAT_LINE" then SubmitChatInput(arg1) end
</OnEvent>
<OnUpdate>
BenillaFCF_OnUpdate(arg1)
BenillaChatFrame_OnUpdate(this, arg1)
FCF_OnUpdate(arg1)
ChatFrame_OnUpdate(arg1)
</OnUpdate>
<OnMouseWheel>
-- WoW wheel convention: +1 per notch up, -1 down. Up = older messages.
@@ -694,16 +864,16 @@
<Size><AbsDimension x="128" y="16"/></Size>
<Scripts>
<OnLoad>
BenillaUIMenuButton_OnLoad()
UIMenuButton_OnLoad()
</OnLoad>
<OnClick>
BenillaUIMenuButton_OnClick()
UIMenuButton_OnClick()
</OnClick>
<OnEnter>
BenillaUIMenuButton_OnEnter()
UIMenuButton_OnEnter()
</OnEnter>
<OnLeave>
BenillaUIMenuButton_OnLeave()
UIMenuButton_OnLeave()
</OnLeave>
</Scripts>
<Layers>
@@ -762,13 +932,13 @@
</Frames>
<Scripts>
<OnShow>
BenillaUIMenu_OnShow()
UIMenu_OnShow()
PlaySound("UChatScrollButton")
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
this:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
</OnShow>
<OnUpdate>
BenillaUIMenu_OnUpdate(arg1)
UIMenu_OnUpdate(arg1)
</OnUpdate>
</Scripts>
</Frame>
@@ -819,10 +989,10 @@
</Frames>
<Scripts>
<OnLoad>
BenillaChatMenu_OnLoad()
ChatMenu_OnLoad()
</OnLoad>
<OnShow>
BenillaUIMenu_OnShow()
UIMenu_OnShow()
EmoteMenu:Hide()
VoiceMacroMenu:Hide()
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
@@ -903,6 +1073,14 @@
chat-cache window COLOR, which ships 0 0 0; the ChatFrameBackground art itself is
white, so untinted it reads as a white wash over the world). -->
<Script><![CDATA[
-- `DEFAULT_CHAT_FRAME = ChatFrame1` — the reference's own line (ChatFrame.lua), and the
-- name half of FrameXML actually prints through. It sits here rather than in the header
-- script because a <Script> block runs where it stands: at the top of this file
-- ChatFrame1 does not exist yet. Measured demand: 3 of the 218 vanilla-corpus addons
-- die on `attempt to index global 'DEFAULT_CHAT_FRAME'` without it, and many more reach
-- it on a later path.
DEFAULT_CHAT_FRAME = ChatFrame1
BenillaFCF_SelectDock(1)
for i = 1, 2 do
for _, t in ipairs(BenillaFCF_Textures(i)) do
@@ -0,0 +1,246 @@
<Ui>
<!--
Interface\FrameXML\ColorPickerFrame.xml, transcribed — the shared colour dialog. Line
numbers below are that file's (336 lines, XML only; it ships no `.lua`, its whole script is
inline and the one function that drives it, `UIDropDownMenuButton_OpenColorPicker`, lives in
UIDropDownMenu.lua).
WHY IT IS HERE, in the arc's own terms: **86 of the 218 corpus addons reach
`ColorPickerFrame`, 80 reach `OpacitySliderFrame`** — and 1207's lesson applies before those
numbers do, so read the lines. 64 of the files are copies of `Dewdrop-2.0.lua`, which does
not roll its own picker: it drives THIS one, with the exact block below (Dewdrop-2.0.lua
l.425-465, verbatim from a corpus copy):
ColorPickerFrame.func = function() … ColorPickerFrame:GetColorRGB() …
OpacitySliderFrame:GetValue() … end
ColorPickerFrame.hasOpacity = this.hasOpacity
ColorPickerFrame.opacityFunc = ColorPickerFrame.func
ColorPickerFrame.opacity = 1 - this.opacity
ColorPickerFrame:SetColorRGB(this.r, this.g, this.b)
ColorPickerFrame.cancelFunc = function() … end
ShowUIPanel(ColorPickerFrame)
Dewdrop is *the* Ace2 menu library, so that block is how EVERY colour option in EVERY Ace2
addon's config menu opens. On top of it sit ~14 addons that name the frame directly
(TipBuddy, CT_UnitFrames, CT_RaidAssist, CooldownCount, PowerAuras, Bongos_Options,
MikScrollingBattleText, EQL3, RangeColor, ColorPickerPlus, QuestHistory, Radar,
IgniteStatus, Bagnon_Core), and `AceConsole-2.0.lua` l.1402-1406, which does
`ColorPickerOkayButton:GetScript("OnClick")` and chains it — so the two BUTTON names are API,
not decoration.
The widget itself (`<ColorSelect>`, `SetColorRGB`/`GetColorRGB`/`OnColorSelect`) is
engine-side, in `benilla-ui/src/script/colorselect.rs` — read its module doc before touching
anything colour-valued here. Two things live there that are not obvious and are byte-verified
(wow-re `system/ui/scratch/colorselect-color-law.md`, a §5 cross-check dispatched from this
repo for exactly this window): the widget's state is **HSV floats**, not RGB, and the
set→read round trip is deliberately **not** the identity — 9.75 % of colours come back one
step low on a channel, and the FrameXML `GetColorRGB … SetColorRGB` idiom ratchets. It also
says why the widget could not be a few methods hung on this one frame (TipBuddy declares two
`<ColorSelect>` frames of its own).
── THE ONE THING THIS WINDOW CANNOT DO, AND IT IS THE BIG ONE ──────────────────────────────
**There is no colour wheel, so there is no way to CHOOSE a colour in it yet.** The reference
ends its `<ColorSelect>` with four sub-elements this file does not carry (ref l.186-221):
<ColorWheelTexture name="ColorPickerWheel"/> 128x128, TOPLEFT (16, -32)
<ColorWheelThumbTexture/> 10x10, Interface\Buttons\UI-ColorPicker-Buttons,
TexCoords 0 / 0.15625 / 0 / 0.625
<ColorValueTexture/> 32x128, LEFT of ColorPickerWheel's RIGHT at (24, 0)
<ColorValueThumbTexture/> 48x14, same sheet, TexCoords 0.25 / 1.0 / 0 / 0.875
Look at what the first and third do NOT have: a `file=`. The hue wheel and the value strip
are **generated by the client**, not loaded — there is no BLP anywhere in the MPQ chain that
is the colour wheel, only the two little thumb markers that ride on top of it. Rendering
them is an app-renderer job (an HSV wheel + a value ramp drawn for a `FrameKind::ColorSelect`
rect), not a FrameXML one, so the elements are omitted rather than shipped inert — their
geometry is recorded above, and dropping them back in is the day that renderer exists.
(RF-28 has the loader hooks: `<ColorWheelTexture>` `0x78b580`, `<ColorWheelThumbTexture>`
`0x78b850`, `<ColorValueTexture>` `0x78b8a0`, `<ColorValueThumbTexture>` `0x78ba90` — and the
INTERACTION half is already carved too, so that day is cheaper than it looks: wow-re's
`colorselect-color-law.md` §5 has the press/drag handler verbatim — a click jumps the colour
immediately, `H = (atan2(ny, nx) + π)·180/π`, `S = min(radius, 1)`, `V = clamp((y − bottom) /
(top − bottom), 0, 1)`, all raw f32 straight into the widget's HSV, no quantizer.)
Everything ELSE works, and it is most of what the corpus actually exercises: an addon sets a
colour and the swatch shows it, the opacity slider is live and drives `opacityFunc` on every
drag, Okay commits through `func` + `opacityFunc`, Cancel restores through `cancelFunc`, and
both buttons are real named buttons whose `OnClick` an addon can fetch and chain. The
reference's own geometry is kept exactly — including the 128x128 gap on the left where the
wheel belongs, and the 365-wide / 305-wide `OnShow` swap around it — because half a window at
the right size is honest and a re-designed window is not.
OTHER DEVIATIONS, each forced:
· **`<OnKeyDown>` (ref l.177-184) is not transcribed** — this engine gives plain frames no
raw key events (SCRIPT_KINDS has no `OnKeyDown`), the same wall AddonList.xml hit
(decision 1197). Its ESCAPE arm is `HideUIPanel(this) + cancelFunc(previousValues)`, i.e.
exactly what the Cancel button does, so the arm is carried in UiPanels.xml's ESC chain as
`ColorPickerCancelButton:Click()` — the reference's own idiom for this (its game-menu
chain cancel-clicks `OptionsFrameCancel` the same way). ESC therefore CANCELS, it does not
merely hide, which is the half people get wrong.
· **`OpacityFrame` (ref l.223-335) is not transcribed.** The second frame in the reference's
file: a standalone 80x180 opacity popup with its own slider and a fullscreen
click-off-to-close button, used by FloatingChatFrame's chat-window alpha. **Zero corpus
callers** and no benilla consumer, so it waits for its first customer (decision 1195). Its
globals are `OpacityFrame`, `OpacityFrameSlider`, `OpacityFrameCloseButton`, and
`OpacityFrame.opacityFunc`/`.saveOpacityFunc`.
· **The unguarded `ColorPickerFrame.func()` in Okay's OnClick (ref l.89) is KEPT unguarded.**
It is one of the few places quirk-for-quirk beats tidying: an addon that opened this window
without setting `func` errors in the real client too, and a guard here would swallow that
into a window that silently does nothing.
· `<ColorSelect>` carries no `<Layers>` `hidden` art beyond the reference's three regions;
`ColorSwatch` and `ColorPickerFrameHeader` keep their unprefixed reference names, which is
required — `ColorPickerFrame.xml`'s own `<OnColorSelect>` addresses `ColorSwatch` by name,
and so would any addon that re-skins the window.
-->
<Script><![CDATA[
-- ref GlobalStrings.lua l.770 / l.132 / l.1458 (enUS), in the `X = X or "…"` FALLBACK form
-- this file set uses throughout: the app runs the real localized GlobalStrings.lua off the
-- patch chain BEFORE the shipped XML, so these only bind on a surface without it (a headless
-- test). `text="COLOR_PICKER"` / `text="OKAY"` / `text="CANCEL"` below are global-string
-- KEYS, not literals — the loader resolves each through the Lua global (rf28 `0x703bf0`).
COLOR_PICKER = COLOR_PICKER or "Color Picker"
OKAY = OKAY or "Okay"
CANCEL = CANCEL or "Cancel"
]]></Script>
<!-- ref l.3-9. 365 wide is the WITH-opacity width; OnShow narrows it to 305 when an addon did
not ask for the slider. `movable="true"` is the reference's (this engine only honours it
once something calls StartMoving, which nothing here does — same as the reference, whose
frame has no drag handler either: the attribute is there for addons). -->
<ColorSelect name="ColorPickerFrame" toplevel="true" parent="UIParent" movable="true" enableMouse="true" enableKeyboard="true" hidden="true">
<Size><AbsDimension x="365" y="200"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- ref l.23-35. The live preview: a 32x32 solid quad repainted from OnColorSelect.
Unprefixed name, the reference's own — see the header. -->
<Texture name="ColorSwatch">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="225" y="-32"/></Offset></Anchor></Anchors>
<Color r="1.0" g="1.0" b="1.0" a="1.0"/>
</Texture>
<!-- ref l.36-47. The same header plate the game menu and every dialog wears. -->
<Texture name="ColorPickerFrameHeader" file="Interface\DialogFrame\UI-DialogBox-Header">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="12"/></Offset></Anchor></Anchors>
</Texture>
<!-- ref l.48-56. Unnamed in the reference too. -->
<FontString inherits="GameFontNormal" text="COLOR_PICKER">
<Anchors>
<Anchor point="TOP" relativeTo="ColorPickerFrameHeader"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- ref l.60-77. Cancel is declared FIRST because Okay anchors to it. Its OnClick is the
reference's, verbatim: hide, then hand the addon back what it had. -->
<Button name="ColorPickerCancelButton" inherits="GameMenuButtonTemplate" text="CANCEL">
<Anchors>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-10" y="10"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
HideUIPanel(this:GetParent());
if ( ColorPickerFrame.cancelFunc ) then
ColorPickerFrame.cancelFunc(ColorPickerFrame.previousValues);
end
PlaySound("igMainMenuOptionCheckBoxOn");
</OnClick>
</Scripts>
</Button>
<!-- ref l.78-96. `func()` unguarded, `opacityFunc()` guarded — the reference's own
asymmetry, kept (header). AceConsole-2.0 fetches THIS handler with
`ColorPickerOkayButton:GetScript("OnClick")` and calls it from its own replacement,
so the name and the script slot are both load-bearing. -->
<Button name="ColorPickerOkayButton" inherits="GameMenuButtonTemplate" text="OKAY">
<Anchors>
<Anchor point="RIGHT" relativeTo="ColorPickerCancelButton" relativePoint="LEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
HideUIPanel(this:GetParent());
ColorPickerFrame.func();
if ( ColorPickerFrame.opacityFunc ) then
ColorPickerFrame.opacityFunc();
end
PlaySound("igMainMenuOptionCheckBoxOn");
</OnClick>
</Scripts>
</Button>
<!-- ref l.97-158. A real Slider, so it drags: 0..1 in 0.01 steps, VERTICAL, seated to
the RIGHT of the swatch. Its `$parentText` label is EMPTY in the reference (only
OpacityFrame's copy carries text="OPACITY") — transcribed as written, not
"improved". Note the sense: 1 is FULLY OPAQUE, and every caller inverts
(`ColorPickerFrame.opacity = 1 - this.opacity`, and Dewdrop's func reads back
`1 - OpacitySliderFrame:GetValue()`). -->
<Slider name="OpacitySliderFrame" orientation="VERTICAL" minValue="0" maxValue="1" valueStep="0.01" defaultValue="1">
<Size><AbsDimension x="16" y="128"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ColorSwatch" relativePoint="TOPRIGHT"><Offset><AbsDimension x="32" y="0"/></Offset></Anchor>
</Anchors>
<Backdrop bgFile="Interface\Buttons\UI-SliderBar-Background" edgeFile="Interface\Buttons\UI-SliderBar-Border" tile="true">
<EdgeSize><AbsValue val="8"/></EdgeSize>
<TileSize><AbsValue val="8"/></TileSize>
<BackgroundInsets><AbsInset left="3" right="3" top="6" bottom="6"/></BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="BOTTOM" relativePoint="TOP"/></Anchors>
</FontString>
<FontString inherits="GameFontHighlightSmall" text="0%">
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="2" y="3"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString inherits="GameFontHighlightSmall" text="100%">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-2" y="3"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnValueChanged>
if ( ColorPickerFrame.opacityFunc ) then
ColorPickerFrame.opacityFunc();
end
</OnValueChanged>
</Scripts>
<ThumbTexture file="Interface\Buttons\UI-SliderBar-Button-Vertical">
<Size><AbsDimension x="32" y="32"/></Size>
</ThumbTexture>
</Slider>
</Frames>
<Scripts>
<!-- ref l.161-170. The opacity slider is opt-in per caller, and the window narrows by
the slider's 60 units when nobody asked for it. -->
<OnShow>
if ( this.hasOpacity ) then
OpacitySliderFrame:Show();
OpacitySliderFrame:SetValue(this.opacity);
this:SetWidth(365);
else
OpacitySliderFrame:Hide();
this:SetWidth(305);
end
</OnShow>
<!-- ref l.171-176. The widget's own slot, fired by SetColorRGB with the colour the
widget now holds (colorselect.rs). This is the ONLY thing that paints the swatch —
no caller touches it — and it is also where an addon's live-preview `func` runs. -->
<OnColorSelect>
ColorSwatch:SetTexture(arg1, arg2, arg3);
if ( this.func ) then
this.func();
end
</OnColorSelect>
</Scripts>
</ColorSelect>
</Ui>
+2 -2
View File
@@ -197,10 +197,10 @@
end
elseif event == "UNIT_POWER_UPDATE" then
-- Ref: UNIT_MANA + UnitMana/UnitManaMax (header deviation — the Era surface).
local mana = UnitPower("player")
local mana = UnitMana("player")
local powerType = UnitPowerType("player")
if arg1 == "player" then
if mana / UnitPowerMax("player") <= COMBAT_TEXT_LOW_MANA_THRESHOLD and powerType == 0 then
if mana / UnitManaMax("player") <= COMBAT_TEXT_LOW_MANA_THRESHOLD and powerType == 0 then
if not CombatText.lowMana then
messageType = "MANA_LOW"
CombatText.lowMana = 1
+27 -27
View File
@@ -58,46 +58,46 @@
</Layers>
</Frame>
<Frame name="BenillaComboFrame" frameStrata="MEDIUM" toplevel="true" parent="UIParent" hidden="true" alpha="0">
<Frame name="ComboFrame" frameStrata="MEDIUM" toplevel="true" parent="UIParent" hidden="true" alpha="0">
<Size><AbsDimension x="256" y="32"/></Size>
<!-- ref l.56-62: TOPRIGHT of the target frame, (-44,-9). -->
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="BenillaTargetFrame">
<Anchor point="TOPRIGHT" relativeTo="TargetFrame">
<Offset><AbsDimension x="-44" y="-9"/></Offset>
</Anchor>
</Anchors>
<Frames>
<Frame name="BenillaComboPoint1" inherits="BenillaComboPointTemplate">
<Frame name="ComboPoint1" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="BenillaComboPoint2" inherits="BenillaComboPointTemplate">
<Frame name="ComboPoint2" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="BenillaComboPoint1" relativePoint="BOTTOM">
<Anchor point="TOP" relativeTo="ComboPoint1" relativePoint="BOTTOM">
<Offset><AbsDimension x="7" y="4"/></Offset>
</Anchor>
</Anchors>
</Frame>
<Frame name="BenillaComboPoint3" inherits="BenillaComboPointTemplate">
<Frame name="ComboPoint3" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="BenillaComboPoint2" relativePoint="BOTTOM">
<Anchor point="TOP" relativeTo="ComboPoint2" relativePoint="BOTTOM">
<Offset><AbsDimension x="5" y="2"/></Offset>
</Anchor>
</Anchors>
</Frame>
<Frame name="BenillaComboPoint4" inherits="BenillaComboPointTemplate">
<Frame name="ComboPoint4" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="BenillaComboPoint3" relativePoint="BOTTOM">
<Anchor point="TOP" relativeTo="ComboPoint3" relativePoint="BOTTOM">
<Offset><AbsDimension x="2" y="1"/></Offset>
</Anchor>
</Anchors>
</Frame>
<!-- The fifth is the odd one out: the reference sizes it 15×18 (ref l.101-104). -->
<Frame name="BenillaComboPoint5" inherits="BenillaComboPointTemplate">
<Frame name="ComboPoint5" inherits="BenillaComboPointTemplate">
<Size><AbsDimension x="15" y="18"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="BenillaComboPoint4" relativePoint="BOTTOM">
<Anchor point="TOP" relativeTo="ComboPoint4" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="1"/></Offset>
</Anchor>
</Anchors>
@@ -107,11 +107,11 @@
<OnLoad>
this:RegisterEvent("PLAYER_TARGET_CHANGED");
this:RegisterEvent("PLAYER_COMBO_POINTS");
BenillaComboPoint1Highlight:SetAlpha(0);
BenillaComboPoint1Shine:SetAlpha(0);
ComboPoint1Highlight:SetAlpha(0);
ComboPoint1Shine:SetAlpha(0);
</OnLoad>
<OnEvent>
BenillaComboPointsFrame_OnEvent();
ComboPointsFrame_OnEvent();
</OnEvent>
</Scripts>
</Frame>
@@ -130,22 +130,22 @@
-- ref ComboFrame.lua `ComboPointsFrame_OnEvent`. Only points ABOVE the last count animate
-- (`i > COMBO_FRAME_LAST_NUM_POINTS`) — a repaint at the same count must not re-flare every
-- dot, which is why the last count is remembered across events rather than recomputed.
function BenillaComboPointsFrame_OnEvent()
function ComboPointsFrame_OnEvent()
local comboPoints = GetComboPoints();
if comboPoints > 0 then
if not BenillaComboFrame:IsVisible() then
BenillaComboFrame:Show();
UIFrameFadeIn(BenillaComboFrame, COMBOFRAME_FADE_IN);
if not ComboFrame:IsVisible() then
ComboFrame:Show();
UIFrameFadeIn(ComboFrame, COMBOFRAME_FADE_IN);
end
for i = 1, MAX_COMBO_POINTS do
local comboPointHighlight = getglobal("BenillaComboPoint"..i.."Highlight");
local comboPointShine = getglobal("BenillaComboPoint"..i.."Shine");
local comboPointHighlight = getglobal("ComboPoint"..i.."Highlight");
local comboPointShine = getglobal("ComboPoint"..i.."Shine");
if i <= comboPoints then
if i > COMBO_FRAME_LAST_NUM_POINTS then
local fadeInfo = {};
fadeInfo.mode = "IN";
fadeInfo.timeToFade = COMBOFRAME_HIGHLIGHT_FADE_IN;
fadeInfo.finishedFunc = BenillaComboPointShineFadeIn;
fadeInfo.finishedFunc = ComboPointShineFadeIn;
fadeInfo.finishedArg1 = comboPointShine;
UIFrameFade(comboPointHighlight, fadeInfo);
end
@@ -155,25 +155,25 @@
end
end
else
BenillaComboPoint1Highlight:SetAlpha(0);
BenillaComboPoint1Shine:SetAlpha(0);
BenillaComboFrame:Hide();
ComboPoint1Highlight:SetAlpha(0);
ComboPoint1Shine:SetAlpha(0);
ComboFrame:Hide();
end
COMBO_FRAME_LAST_NUM_POINTS = comboPoints;
end
function BenillaComboPointShineFadeIn(frame)
function ComboPointShineFadeIn(frame)
local fadeInfo = {};
fadeInfo.mode = "IN";
fadeInfo.timeToFade = COMBOFRAME_SHINE_FADE_IN;
fadeInfo.finishedFunc = BenillaComboPointShineFadeOut;
fadeInfo.finishedFunc = ComboPointShineFadeOut;
fadeInfo.finishedArg1 = frame:GetName();
UIFrameFade(frame, fadeInfo);
end
-- The name round-trip is the reference's own workaround ("hack since a frame can't have a
-- reference to itself in it") — kept, so the chain reads as its source does.
function BenillaComboPointShineFadeOut(frameName)
function ComboPointShineFadeOut(frameName)
UIFrameFadeOut(getglobal(frameName), COMBOFRAME_SHINE_FADE_OUT);
end
]]>
+8 -8
View File
@@ -31,7 +31,7 @@
session), the UIPanel slot system (carries NO hardcoded <Anchors> — ShowUIPanel/SetLeftFrame
SetPoints it), OnShow/OnHide PlaySound pair.
- TradeSkillFrame.xml — the FLAT layout constraint (below), the faux-scroll list kit
(BenillaFauxScrollFrameTemplate, ScrollTemplates.xml), the locally-reconstructed
(FauxScrollFrameTemplate, ScrollTemplates.xml), the locally-reconstructed
UIPanelButtonTemplate/close-button/item-button idioms (no such templates ship in this house
dialect — TalentFrame.xml's own ENGINE API note, restated per-window, StackSplit.xml's own
"own local virtual template, not a cross-file inherits" convention), the persistent
@@ -316,8 +316,8 @@
end
local scrollFrame = getglobal("BenillaCraftListScrollFrame")
BenillaFauxScrollFrame_Update(scrollFrame, numCrafts, CRAFTS_DISPLAYED, CRAFT_SKILL_HEIGHT)
local craftOffset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
FauxScrollFrame_Update(scrollFrame, numCrafts, CRAFTS_DISPLAYED, CRAFT_SKILL_HEIGHT)
local craftOffset = FauxScrollFrame_GetOffset(scrollFrame)
-- The skill-rank StatusBar (ref l.107-123): Show/Hide are real here (real widgets, not
-- flattened Layer regions).
@@ -437,7 +437,7 @@
ExpandCraftSkillLine(0)
else
self.collapsed = true
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
CollapseCraftSkillLine(0)
end
end
@@ -513,7 +513,7 @@
else
BenillaCraftFrame_SetSelection(GetCraftSelectionIndex())
end
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
SetPortraitTexture(getglobal("BenillaCraftFramePortrait"), "player")
BenillaCraftFrame_Update()
ShowUIPanel(frame)
@@ -529,7 +529,7 @@
BenillaCraftFrame_SetSelection(selection)
elseif GetNumCrafts() > 0 then
BenillaCraftFrame_SetSelection(1)
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
end
BenillaCraftFrame_Update()
elseif event == "CRAFT_CLOSE" then
@@ -839,11 +839,11 @@
<Button name="BenillaCraft7" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaCraft8" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- The faux-scroll frame (BenillaFauxScrollFrameTemplate, ScrollTemplates.xml): invisible
<!-- The faux-scroll frame (FauxScrollFrameTemplate, ScrollTemplates.xml): invisible
container over the 8 rows; only its right-hand Slider scroll bar renders. Sized to the
8-row span (CRAFTS_DISPLAYED × CRAFT_SKILL_HEIGHT), TradeSkillFrame.xml's own
same-TOPLEFT-as-row1 convention. updateFunc set in BenillaCraftFrame_OnLoad. -->
<Frame name="BenillaCraftListScrollFrame" inherits="BenillaFauxScrollFrameTemplate">
<Frame name="BenillaCraftListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-96"/></Offset></Anchor></Anchors>
</Frame>
+1 -1
View File
@@ -229,7 +229,7 @@
end
end
-- The ref hides the gossip window under the confirm (UIParent.lua l.412).
local gossip = getglobal("BenillaGossipFrame")
local gossip = getglobal("GossipFrame")
if gossip and gossip:IsVisible() then
HideUIPanel(gossip)
end
+32 -32
View File
@@ -46,8 +46,8 @@
-- tries the clicked item on. Both halves ride ONE ordered intent queue app-side, so the
-- reset can never overtake the try-on (dressup.rs's module doc).
function DressUpItem(item)
if not BenillaDressUpFrame:IsVisible() then
ShowUIPanel(BenillaDressUpFrame)
if not DressUpFrame:IsVisible() then
ShowUIPanel(DressUpFrame)
BenillaDressUpModel_Dress() -- ref: DressUpModel:SetUnit("player")
end
BenillaDressUpModel_TryOn(item) -- ref: DressUpModel:TryOn(item)
@@ -86,10 +86,10 @@
-- character window's level line documents).
function SetDressUpBackground()
local texture = DressUpTexturePath()
BenillaDressUpBackgroundTopLeft:SetTexture(texture .. 1)
BenillaDressUpBackgroundTopRight:SetTexture(texture .. 2)
BenillaDressUpBackgroundBotLeft:SetTexture(texture .. 3)
BenillaDressUpBackgroundBotRight:SetTexture(texture .. 4)
DressUpBackgroundTopLeft:SetTexture(texture .. 1)
DressUpBackgroundTopRight:SetTexture(texture .. 2)
DressUpBackgroundBotLeft:SetTexture(texture .. 3)
DressUpBackgroundBotRight:SetTexture(texture .. 4)
end
-- The pane's facing — the paper doll's pair of handlers, over this room's own booth yaw
@@ -147,7 +147,7 @@
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<Frame name="BenillaDressUpFrame" toplevel="true" hidden="true">
<Frame name="DressUpFrame" toplevel="true" hidden="true">
<!-- The ref's 384×512 window. No <Anchors>: ShowUIPanel seats it in the left slot, this
house's convention for every panel window (MerchantFrame.xml/CraftFrame.xml). -->
<Size><AbsDimension x="384" y="512"/></Size>
@@ -156,7 +156,7 @@
<!-- The "player" portrait, 60×60 at TOPLEFT (7,-6) (ref l.20-33) — BACKGROUND so the
window art's ring frames it; bound in OnShow, like every other portrait here. -->
<Layer level="BACKGROUND">
<Texture name="BenillaDressUpFramePortrait">
<Texture name="DressUpFramePortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
@@ -180,61 +180,61 @@
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="256" y="-256"/></Offset></Anchor></Anchors>
</Texture>
<FontString name="BenillaDressUpFrameTitleText" inherits="GameFontHighlight" text="DRESSUP_FRAME">
<FontString name="DressUpFrameTitleText" inherits="GameFontHighlight" text="DRESSUP_FRAME">
<Size><AbsDimension x="300" y="14"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaDressUpFrameDescriptionText" inherits="GameFontNormalSmall" text="DRESSUP_FRAME_INSTRUCTIONS">
<FontString name="DressUpFrameDescriptionText" inherits="GameFontNormalSmall" text="DRESSUP_FRAME_INSTRUCTIONS">
<Size><AbsDimension x="260" y="36"/></Size>
<Anchors>
<Anchor point="CENTER" relativeTo="BenillaDressUpFrameTitleText" relativePoint="BOTTOM"><Offset><AbsDimension x="10" y="-22"/></Offset></Anchor>
<Anchor point="CENTER" relativeTo="DressUpFrameTitleText" relativePoint="BOTTOM"><Offset><AbsDimension x="10" y="-22"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<!-- The race backdrop behind the model, four quadrants (ref l.73-118); the files are
chosen per race by SetDressUpBackground above. -->
<Layer level="OVERLAY">
<Texture name="BenillaDressUpBackgroundTopLeft">
<Texture name="DressUpBackgroundTopLeft">
<Size><AbsDimension x="256" y="255"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-76"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="BenillaDressUpBackgroundTopRight">
<Texture name="DressUpBackgroundTopRight">
<Size><AbsDimension x="62" y="255"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaDressUpBackgroundTopLeft" relativePoint="TOPRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft" relativePoint="TOPRIGHT"/></Anchors>
</Texture>
<Texture name="BenillaDressUpBackgroundBotLeft">
<Texture name="DressUpBackgroundBotLeft">
<Size><AbsDimension x="256" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaDressUpBackgroundTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="BenillaDressUpBackgroundBotRight">
<Texture name="DressUpBackgroundBotRight">
<Size><AbsDimension x="62" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaDressUpBackgroundTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The corner close X: the ref's UIPanelCloseButton seat (CENTER of TOPRIGHT, -46,-25 —
ref l.120-127) on the minimize-button art every close button in this house uses. -->
<Button name="BenillaDressUpFrameCloseButton">
<Button name="DressUpFrameCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-46" y="-25"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts>
<OnClick>HideUIPanel(BenillaDressUpFrame)</OnClick>
<OnClick>HideUIPanel(DressUpFrame)</OnClick>
</Scripts>
</Button>
<!-- Close (ref l.128-146) and Reset (l.147-165, chained off Close's LEFT). Reset is the
ref's `DressUpModel:Dress()` + its gsTitleOptionOK click. -->
<Button name="BenillaDressUpFrameCancelButton" inherits="BenillaDressUpPanelButtonTemplate" text="CLOSE">
<Button name="DressUpFrameCancelButton" inherits="BenillaDressUpPanelButtonTemplate" text="CLOSE">
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="305" y="-422"/></Offset></Anchor></Anchors>
<Scripts>
<OnClick>HideUIPanel(BenillaDressUpFrame)</OnClick>
<OnClick>HideUIPanel(DressUpFrame)</OnClick>
</Scripts>
</Button>
<Button name="BenillaDressUpFrameResetButton" inherits="BenillaDressUpPanelButtonTemplate" text="RESET">
<Anchors><Anchor point="RIGHT" relativeTo="BenillaDressUpFrameCancelButton" relativePoint="LEFT"/></Anchors>
<Button name="DressUpFrameResetButton" inherits="BenillaDressUpPanelButtonTemplate" text="RESET">
<Anchors><Anchor point="RIGHT" relativeTo="DressUpFrameCancelButton" relativePoint="LEFT"/></Anchors>
<Scripts>
<OnClick>
BenillaDressUpModel_Dress()
@@ -245,7 +245,7 @@
<!-- The model pane (ref the <DressUpModel> widget, l.166-207): 316×351 anchored BOTTOM
(-11,+85). A plain Frame sampling the "dressup" booth bake (header). -->
<Frame name="BenillaDressUpModelFrame">
<Frame name="DressUpModelFrame">
<Size><AbsDimension x="316" y="351"/></Size>
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="-11" y="85"/></Offset></Anchor></Anchors>
<Layers>
@@ -267,26 +267,26 @@
then right chained off its TOPRIGHT. Both edges registered, so one tap fires
OnClick twice and holding hands the spin to the pane's OnUpdate — the ref's
own tap-and-hold (decision 0638 §3). -->
<Button name="BenillaDressUpModelFrameRotateLeftButton">
<Button name="DressUpModelFrameRotateLeftButton">
<Size><AbsDimension x="35" y="35"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaDressUpFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="21" y="-75"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="21" y="-75"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-RotationLeft-Button-Up"/>
<PushedTexture file="Interface\Buttons\UI-RotationLeft-Button-Down"/>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
<Scripts>
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
<OnClick>BenillaDressUpModel_RotateLeft(getglobal("BenillaDressUpModelFrame"))</OnClick>
<OnClick>BenillaDressUpModel_RotateLeft(getglobal("DressUpModelFrame"))</OnClick>
</Scripts>
</Button>
<Button name="BenillaDressUpModelFrameRotateRightButton">
<Button name="DressUpModelFrameRotateRightButton">
<Size><AbsDimension x="35" y="35"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaDressUpModelFrameRotateLeftButton" relativePoint="TOPRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpModelFrameRotateLeftButton" relativePoint="TOPRIGHT"/></Anchors>
<NormalTexture file="Interface\Buttons\UI-RotationRight-Button-Up"/>
<PushedTexture file="Interface\Buttons\UI-RotationRight-Button-Down"/>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
<Scripts>
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
<OnClick>BenillaDressUpModel_RotateRight(getglobal("BenillaDressUpModelFrame"))</OnClick>
<OnClick>BenillaDressUpModel_RotateRight(getglobal("DressUpModelFrame"))</OnClick>
</Scripts>
</Button>
</Frames>
@@ -303,7 +303,7 @@
<OnLoad>SetDressUpBackground()</OnLoad>
<OnShow>
SetDressUpBackground()
SetPortraitTexture(BenillaDressUpFramePortrait, "player")
SetPortraitTexture(DressUpFramePortrait, "player")
PlaySound("igCharacterInfoOpen")
</OnShow>
<!-- Closing empties the booth (dressup.rs's `Close` intent): the ref's widget keeps its
+20 -15
View File
@@ -19,7 +19,7 @@
BENILLA_ERRORS_FADE = 1.0 -- INTERIM (comment above)
function BenillaErrorsFrame_AddMessage(text, r, g, b)
local frame = getglobal("BenillaErrorsFrame")
local frame = getglobal("UIErrorsFrame")
-- insertMode="TOP": newest first, older lines shift down and off the list.
table.insert(frame.messages, 1, { text = text, r = r, g = g, b = b, age = 0 })
while table.getn(frame.messages) > BENILLA_ERRORS_LINES do
@@ -31,7 +31,7 @@
function BenillaErrorsFrame_Paint(frame)
for i = 1, BENILLA_ERRORS_LINES do
local line = getglobal("BenillaErrorsFrameLine" .. i)
local line = getglobal("UIErrorsFrameLine" .. i)
local msg = frame.messages[i]
if msg then
line:SetText(msg.text)
@@ -49,19 +49,24 @@
end
end
function BenillaErrorsFrame_OnLoad(frame)
function UIErrorsFrame_OnLoad()
local frame = this
frame.messages = {}
frame:RegisterEvent("UI_INFO_MESSAGE")
frame:RegisterEvent("UI_ERROR_MESSAGE")
frame:Hide()
end
function BenillaErrorsFrame_OnEvent(frame)
if not arg1 then return end
-- ref UIErrorsFrame_OnEvent(event, message) (UIErrorsFrame.lua:8-15) — the message is the
-- DECLARED second parameter, not the `arg1` global: the XML passes `arg1` into it exactly
-- as the reference's own does, and an addon calling the 1.12 arity is then read correctly.
function UIErrorsFrame_OnEvent(event, message)
local frame = this
if not message then return end
if event == "UI_INFO_MESSAGE" then
BenillaErrorsFrame_AddMessage(arg1, 1.0, 1.0, 0.0) -- ref UIErrorsFrame.lua:12
BenillaErrorsFrame_AddMessage(message, 1.0, 1.0, 0.0) -- ref UIErrorsFrame.lua:12
elseif event == "UI_ERROR_MESSAGE" then
BenillaErrorsFrame_AddMessage(arg1, 1.0, 0.1, 0.1) -- ref UIErrorsFrame.lua:14
BenillaErrorsFrame_AddMessage(message, 1.0, 0.1, 0.1) -- ref UIErrorsFrame.lua:14
end
end
@@ -86,7 +91,7 @@
own content is flat FontString REGIONS at level 0 while those panels carry dozens of nested
child frames at level 1+, so left in the default MEDIUM its toasts draw UNDER whichever
panel is open — invisible in exactly the state you most want to read them. Decision 0597. -->
<Frame name="BenillaErrorsFrame" frameStrata="HIGH">
<Frame name="UIErrorsFrame" frameStrata="HIGH">
<Size><AbsDimension x="512" y="60"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="TOP">
@@ -95,23 +100,23 @@
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString name="BenillaErrorsFrameLine1" inherits="GameFontNormalLarge" justifyH="CENTER">
<FontString name="UIErrorsFrameLine1" inherits="GameFontNormalLarge" justifyH="CENTER">
<Size><AbsDimension x="512" y="0"/></Size>
<Anchors><Anchor point="TOP" relativePoint="TOP"/></Anchors>
</FontString>
<FontString name="BenillaErrorsFrameLine2" inherits="GameFontNormalLarge" justifyH="CENTER">
<FontString name="UIErrorsFrameLine2" inherits="GameFontNormalLarge" justifyH="CENTER">
<Size><AbsDimension x="512" y="0"/></Size>
<Anchors><Anchor point="TOP" relativeTo="BenillaErrorsFrameLine1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOP" relativeTo="UIErrorsFrameLine1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaErrorsFrameLine3" inherits="GameFontNormalLarge" justifyH="CENTER">
<FontString name="UIErrorsFrameLine3" inherits="GameFontNormalLarge" justifyH="CENTER">
<Size><AbsDimension x="512" y="0"/></Size>
<Anchors><Anchor point="TOP" relativeTo="BenillaErrorsFrameLine2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOP" relativeTo="UIErrorsFrameLine2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>BenillaErrorsFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaErrorsFrame_OnEvent(self)</OnEvent>
<OnLoad>UIErrorsFrame_OnLoad()</OnLoad>
<OnEvent>UIErrorsFrame_OnEvent(event, arg1)</OnEvent>
<OnUpdate>BenillaErrorsFrame_OnUpdate(self, arg1)</OnUpdate>
</Scripts>
</Frame>
+69 -12
View File
@@ -28,6 +28,23 @@
the two color tables tooltip AddLine calls read. Defined here (the ref's own home, loaded
first) so every window shares one verified set. -->
<Script><![CDATA[
-- ── The four font-PATH globals (ref Fonts.xml l.4-7) ──────────────────────────────────
-- This file quoted the reference's colour block (l.8-19) and skipped the four lines above
-- it. `STANDARD_TEXT_FONT` is the one that matters: **68 corpus addons name it**, and per
-- 1207's rule that number is read rather than quoted — 64 of them are copies of
-- `Dewdrop-2.0.lua` and only 4 are addon code. But Dewdrop is *the* Ace2 menu library, and
-- it calls `button.text:SetFont(STANDARD_TEXT_FONT, height)` **unguarded** at two of its
-- three sites (l.207 and l.496; only l.2294 writes `STANDARD_TEXT_FONT or "Fonts\\..."`),
-- so a nil global is a failed SetFont on every menu button the Ace2 ecosystem draws.
--
-- The other three come with it because they are the same four-line block and cost nothing:
-- UNIT_NAME_FONT (0 corpus addons), DAMAGE_TEXT_FONT (1), NAMEPLATE_FONT (1). All four are
-- the same face in 1.12 — the reference assigns FRIZQT__ to every one — and that identity
-- is the reference's, not a simplification of ours.
STANDARD_TEXT_FONT = "Fonts\\FRIZQT__.TTF" -- ref Fonts.xml l.4
UNIT_NAME_FONT = "Fonts\\FRIZQT__.TTF" -- l.5
DAMAGE_TEXT_FONT = "Fonts\\FRIZQT__.TTF" -- l.6
NAMEPLATE_FONT = "Fonts\\FRIZQT__.TTF" -- l.7
NORMAL_FONT_COLOR_CODE = "|cffffd200" -- ref Fonts.xml l.8
HIGHLIGHT_FONT_COLOR_CODE = "|cffffffff" -- l.9
RED_FONT_COLOR_CODE = "|cffff2020" -- l.10
@@ -43,19 +60,59 @@
-- one color this file was missing: a passive spell's name text (SpellBookFrame.lua:385).
PASSIVE_SPELL_FONT_COLOR = { r = 0.77, g = 0.64, b = 0 } -- l.20
-- The 1.12 item-quality palette (ref Constants.lua's ITEM_QUALITY_COLORS, the real
-- global name) — promoted here on its second customer (the unit popup's loot-threshold
-- rows; LootFrame.xml aliases its old private copy to this). Indexed 0..6; values match
-- GetItemQualityColor and ui_items.rs's quality_color.
ITEM_QUALITY_COLORS = {
[0] = { r = 0.62, g = 0.62, b = 0.62 }, -- Poor (gray)
[1] = { r = 1.00, g = 1.00, b = 1.00 }, -- Common (white)
[2] = { r = 0.12, g = 1.00, b = 0.00 }, -- Uncommon (green)
[3] = { r = 0.00, g = 0.44, b = 0.87 }, -- Rare (blue)
[4] = { r = 0.64, g = 0.21, b = 0.93 }, -- Epic (purple)
[5] = { r = 1.00, g = 0.50, b = 0.00 }, -- Legendary (orange)
[6] = { r = 0.90, g = 0.80, b = 0.50 }, -- Artifact (light gold)
-- The item-quality palette — **built from the engine verb, exactly as the reference
-- builds it** (ref UIParent.lua l.65-72, verbatim shape; decision 1199):
--
-- ITEM_QUALITY_COLORS = { }
-- for i = -1, 6 do … = GetItemQualityColor(i) end
--
-- It used to be seven hand-typed rows here. They were CLOSE and not exact — 0.62 where
-- the client's byte is 0x9d/255 = 0.6157, 0.64/0.21/0.93 where Epic is
-- 0xa3/0x35/0xee — and they carried no `.hex`, which the reference's table has and
-- which addons splice straight into a chat link. `GetItemQualityColor` is now a real
-- engine binding over wow-5875-re RF-0055's byte-verified table, so the rounding and
-- the second source both go.
--
-- The loop starts at -1 like the reference's: `UIParent.lua` reads one index before the
-- array there, and our binding answers Poor for it rather than reproducing that.
ITEM_QUALITY_COLORS = { }
for i = -1, 6 do
ITEM_QUALITY_COLORS[i] = { }
ITEM_QUALITY_COLORS[i].r,
ITEM_QUALITY_COLORS[i].g,
ITEM_QUALITY_COLORS[i].b,
ITEM_QUALITY_COLORS[i].hex = GetItemQualityColor(i)
end
-- ── RAID_CLASS_COLORS (ref Fonts.xml l.43-53), transcribed verbatim ───────────────────
--
-- The per-class colour every raid/party addon paints names with. **22 corpus addons** reach
-- it — nine of them through `Babble-Class-2.0`, but seven more by hand (KLHThreatMeter,
-- CT_RaidAssist, CT_ExpenseHistory, _LazyPig, FuBar_oRA2CooldownFu, and BigWigs in two
-- plugins) — and absent it is `bad argument #1 to 'pairs' (table expected, got nil)` at
-- file scope. That was the whole of the harness's remaining `table expected` row: three
-- addons, one missing table, found by asking the blocker table which addons were behind it.
--
-- TWO THINGS THAT LOOK WRONG AND ARE THE REFERENCE'S: `SHAMAN` carries the same pink as
-- `PALADIN` (shamans were Horde-only and paladins Alliance-only in 1.12, so the two never
-- shared a raid frame and Blizzard never gave shamans their own), and there is no `ROGUE`
-- yellow anywhere else in the client to reconcile it with. Transcribed as found; a later
-- client's palette is not this one's.
RAID_CLASS_COLORS = {
["HUNTER"] = { r = 0.67, g = 0.83, b = 0.45 },
["WARLOCK"] = { r = 0.58, g = 0.51, b = 0.79 },
["PRIEST"] = { r = 1.0, g = 1.0, b = 1.0 },
["PALADIN"] = { r = 0.96, g = 0.55, b = 0.73 },
["MAGE"] = { r = 0.41, g = 0.8, b = 0.94 },
["ROGUE"] = { r = 1.0, g = 0.96, b = 0.41 },
["DRUID"] = { r = 1.0, g = 0.49, b = 0.04 },
["SHAMAN"] = { r = 0.96, g = 0.55, b = 0.73 },
["WARRIOR"] = { r = 0.78, g = 0.61, b = 0.43 },
}
-- The three OTHER tables the reference's Fonts.xml `<Script>` block carries —
-- CHAT_FONT_HEIGHTS, MATERIAL_TEXT_COLOR_TABLE, MATERIAL_TITLETEXT_COLOR_TABLE — are
-- deliberately not here: **zero corpus addons name any of them**, and nothing benilla ships
-- reads them. They cost nine lines each the day something does.
]]></Script>
<!-- MasterFont: the shadow-only root the GameFont* families chain through (ref l.56). -->
File diff suppressed because it is too large Load Diff
+28 -21
View File
@@ -81,9 +81,14 @@
- The Options label stays a literal "Options": 1.12 GlobalStrings has no era
GAMEMENU_OPTIONS (checked; its OPTIONS_MENU is "Options Menu", a different string).
- The ref frame is `toplevel="true" movable="true" parent="UIParent"`. Dropped: this engine has
no cross-file `parent=` attachment (SkillFrame.xml's header comment), no frame-level raise,
and `movable` is inert in the ref too (GameMenuFrame defines no drag scripts, so it never
actually moves). `frameStrata="DIALOG"` is what puts it over the world UI, and that is kept.
no cross-file `parent=` attachment (SkillFrame.xml's header comment), and `movable` is inert
in the ref too (GameMenuFrame defines no drag scripts, so it never actually moves).
`frameStrata="DIALOG"` is what puts it over the world UI, and that is kept.
**The "no frame-level raise" half of that reason is gone**: the raise law is built
(`script::object::toplevel`, wow-re `ui/scratch/toplevel-raise.md`) and the loader honours
`toplevel="true"`. Re-adding it here is a one-word change and would make the Esc menu come
to the front over any other DIALOG-strata frame it overlaps; left off pending the
director's call on the look, not on the mechanism.
-->
<Script><![CDATA[
-- The reference GlobalStrings (patch-2.MPQ Interface\FrameXML\GlobalStrings.lua), quoted
@@ -179,23 +184,14 @@
<FontHeight><AbsValue val="13"/></FontHeight>
</Font>
<!-- Era MainMenuFrameButtonTemplate (Classic/Frame/MainMenuFrameTemplates.xml l.11-16) =
UIPanelButtonTemplate at 144x21 with a white/white/grey label trio — byte-for-byte the
1.12 GameMenuButtonTemplate (UIPanelTemplates.xml l.403-410): same art, same trio, same
size. UIPanelButtonTemplate itself is not transcribed as a shared virtual (no other
benilla file inherits it — each open-codes the same four-state art, e.g. QuestFrame.xml
l.753); its textures are inlined here with the ref TexCoords. -->
<Button name="GameMenuButtonTemplate" virtual="true">
<Size><AbsDimension x="144" y="21"/></Size>
<ButtonText name="$parentText"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
<NormalFont inherits="GameFontHighlight"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
</Button>
<!-- `GameMenuButtonTemplate` MOVED OUT, to UIPanelTemplates.xml — the reference's own file for
it (ref UIPanelTemplates.xml l.403-410), and now ColorPickerFrame.xml's Okay/Cancel need it
too, from far above this file's LAST-in-the-toc seat. It lived here, with its four state
textures inlined, only because `UIPanelButtonTemplate` had not been transcribed yet; it now
has been, so the reference's actual body (`inherits="UIPanelButtonTemplate"`, 144x21, the
white/white/grey label trio) replaces the private copy verbatim. Era's
MainMenuFrameButtonTemplate (Classic/Frame/MainMenuFrameTemplates.xml l.11-16) is that same
button — same art, same trio, same size — so the header's ART BYTES note is unchanged. -->
<Frame name="GameMenuFrame" frameStrata="DIALOG" enableMouse="true" hidden="true">
<!-- The era layout engine's own numbers (header SCOPE note): 28+144+28 wide, 32-top /
@@ -257,7 +253,18 @@
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonOptions" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>GameMenuButton_Pending(self, ADDONS)</OnLoad></Scripts>
<!-- LIVE since decision 1197 (AddonList.xml). The Macros entry's own body shape:
hide the menu, show the window. This entry has no reference — 1.12's ESC menu
has no AddOns row, its list is glue-side only — so it is a deliberate benilla
addition, and AddonList.xml's header says so rather than implying otherwise. -->
<Scripts>
<OnLoad>self:SetText(ADDONS)</OnLoad>
<OnClick>
PlaySound("igMainMenuOption")
HideUIPanel(getglobal("GameMenuFrame"))
AddonList_Toggle()
</OnClick>
</Scripts>
</Button>
<Button name="GameMenuButtonEditMode" inherits="GameMenuButtonTemplate">
<Anchors>
+130 -70
View File
@@ -4,14 +4,23 @@
SetOwner anchor law, auto-size + right-flush, IsOwned, FadeOut, and the OnTooltipCleared/
OnTooltipAddMoney script protocol all live engine-side (benilla-ui script/tooltip.rs); the
line pairs are engine-created named regions (GameTooltipTextLeftN/RightN — real globals,
like the template pairs reference Lua addresses by name). This file declares the INSTANCE:
the plate (backdrop art + colors QUOTED from ref-GameTooltip.xml/.lua, line cites inline),
the money coin trio, and the Lua that is genuinely FrameXML's in the real client
(GameTooltip_SetDefaultAnchor, GameTooltip_AddNewbieTip, SetTooltipMoney) — plus the P0 item-stat renderer, which
like the template pairs reference Lua addresses by name). This file declares the two
TEMPLATES and their instances: the plate (backdrop art + colors QUOTED from
ref-GameTooltip.xml/.lua, line cites inline), the money coin trio, and the Lua that is
genuinely FrameXML's in the real client (GameTooltip_SetDefaultAnchor,
GameTooltip_AddNewbieTip, SetTooltipMoney) — plus the P0 item-stat renderer, which
moves engine-side in 0274 P1 with the full template feed.
The frame's global name is the real one — GameTooltip — so transcribed reference code
(MinimapCluster's zone hover, UnitFrame's TextLeft1 recolor) works verbatim.
The reference splits this across two files — Interface\FrameXML\GameTooltipTemplate.xml
holds the virtual GameTooltipTemplate (l.3-627) and ShoppingTooltipTemplate (l.628-1217);
GameTooltip.xml <Include>s it and declares only the instances. Both templates live here
instead, ABOVE their instances, because that is the whole of their reach: the tooltip file
already loads before every window that hovers, and keeping them in one file means the
template is registered by the time the instances (and, later, ItemRef.xml's) expand it
without a second manifest entry to keep in order. Same registry, same splice, one file.
-->
<Script><![CDATA[
-- Tooltip default colours (VERIFIED, ref-GameTooltip.lua l.3-4). The border is white, the
@@ -114,7 +123,14 @@
-- ref GameTooltip_OnHide (ref-GameTooltip.lua l.100-104, quoted): the plate colours
-- reset for the next owner and the default-corner flag drops. The ref's OnHide also
-- hides ShoppingTooltip1/2 - that pair lands with the compare-tooltip slice (0274 P4).
--
-- `self or this`: the corpus calls this the REFERENCE way, with no argument at all
-- (TipBuddy.xml l.1412, atsw.xml l.3420, and TipBuddy.lua l.2520 which REPLACES the global
-- and calls the saved original bare). The signature note above explains the corpus-wide
-- shift to a leading parameter; nothing lets us edit an addon, and `this` is exactly what
-- the reference reads here, so the two spellings mean the same thing.
function GameTooltip_OnHide(self)
self = self or this
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
self.default = nil
@@ -147,6 +163,7 @@
-- ships in MerchantFrame.xml (the money kit's home) — guard for engine tests that load
-- this file alone; in the app every file is loaded.
function GameTooltip_ClearMoney(frame)
frame = frame or this
if BenillaMoney_Clear then
BenillaMoney_Clear(frame:GetName() .. "Money")
end
@@ -214,23 +231,57 @@
this:SetStatusBarColor(r, g, b)
end
-- `self or this` for the same reason as GameTooltip_OnHide: an addon inheriting
-- GameTooltipTemplate re-declares <OnLoad> to add its own line and opens it with the
-- reference's bare `GameTooltip_OnLoad();` (TipBuddy.xml l.1402, atsw.xml l.3417) — a nil
-- self there is a load ERROR, and the plate it was about to tint never gets its colours.
-- The body is the ref's two lines and NOTHING else. It used to end with a `self:Hide()` we
-- had added, which the reference (l.79-82, the whole function) does not have — inert for
-- our four tooltips, since every one of them carries `hidden="true"` and the loader's
-- attribute pass runs Hide() long before OnLoad fires, but NOT inert for an addon: two of
-- the 27 declare `hidden="false"` on purpose (PowerAuras.xml l.20, SimpleActionSets.xml
-- l.349) and never call Show(), so the extra line hid a tooltip the real client leaves up.
-- A helper that quietly does more than the reference's is exactly the kind of thing that
-- only shows up when someone else's code calls it.
function GameTooltip_OnLoad(self)
self = self or this
-- The plate colours (ref GameTooltip_OnLoad, ref-GameTooltip.lua l.79-82).
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
self:Hide()
end
]]></Script>
<GameTooltip name="GameTooltip" frameStrata="TOOLTIP" hidden="true">
<!-- A default size so the plate resolves the first frame it is shown; the engine's
auto-size pass takes over once the lines are measured (one frame's latency). NO
load-time anchor (the ref instance carries none): every show path seats the plate
first - SetOwner's anchor law for owned hovers, OnTooltipSetDefaultAnchor for world
hovers - and a leftover static anchor is exactly what parked the world tooltip at
screen center when the default-anchor script wasn't wired. -->
<Size><AbsDimension x="120" y="32"/></Size>
<!-- The tooltip plate (ref-GameTooltip.xml l.33-43 / ref-GameTooltipTemplate.xml l.4-14):
<!--
GameTooltipTemplate (ref-GameTooltipTemplate.xml l.3-627) — the shared tooltip SHAPE, and
the single most-wanted template in the addon corpus: 27 addons name it in an XML
`inherits=` (TipBuddy, AtlasLoot, CT_BarMod, MikScrollingBattleText, TheoryCraft, …) and
two more reach it through CreateFrame("GameTooltip", name, nil, "GameTooltipTemplate").
Every one of those declarations is a <GameTooltip> element, so the template's job is to
hand them the PLATE and the STATUS BAR; the line pairs they then read
(`getglobal(tip:GetName().."TextLeft1")`) are engine-created per instance and can never
carry the template's name.
What is here is what the reference's template carries and all our instances shared, moved
verbatim from the GameTooltip instance below: the backdrop plate, the Thicken deviation
(its rationale note travels WITH it — this is the shared plate, not one window's), the
$parentStatusBar, and the OnLoad/OnHide pair (ref l.611-616).
What is deliberately NOT here, and stays on the GameTooltip instance:
- The money coin trio and the OnTooltipAddMoney/OnTooltipCleared pair. The reference's
template carries a $parentMoneyFrame (l.598-608, a SmallMoneyFrameTemplate), so its
handler is safe on every instance; ours is a bespoke three-coin row that only
GameTooltip declares, so a templated OnTooltipAddMoney would index a MoneyCoin1 that
does not exist. Moving the row up would give ItemRefTooltip and every addon tooltip a
coin line they render today without — a visible change, so it waits for the director.
- OnTooltipSetDefaultAnchor (ref l.617-619). The engine fires it by name on `GameTooltip`
alone (script/tooltip_unit.rs looks the frame up), so on any other instance it is dead
weight.
`clampedToScreen="true"` is the reference's own attribute and is a no-op for us: decision
0352 made the flag default-ON for every GameTooltip-kind frame, because the reference plate
observably clamps though its XML never sets it. Kept because it is what the reference says.
-->
<GameTooltip name="GameTooltipTemplate" frameStrata="TOOLTIP" clampedToScreen="true" hidden="true" virtual="true">
<!-- The tooltip plate (ref-GameTooltipTemplate.xml l.4-14 / ref-GameTooltip.xml l.33-43):
UI-Tooltip-Background tiled, UI-Tooltip-Border edge, EdgeSize 16, TileSize 16, insets
5/5/5/5. Colours are set in OnLoad (SetBackdropColor/BorderColor) — no <Color> here, so
the plate starts ctor-white and OnLoad tints it. -->
@@ -255,6 +306,46 @@
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The mouseover health bar (ref-GameTooltipTemplate.xml l.575-597, quoted): 8px
tall, hanging below the plate, UI-TargetingFrame-BarFill, colored by
HealthBar_OnValueChanged. The ENGINE drives min/max/value from the live unit
snapshot (the byte law's HEALTH watcher — SetUnit + every set_unit push), finding
it by the `<name>StatusBar` global — so it has to be the TEMPLATE's, not one
instance's: TipBuddy replaces the whole unit tooltip and both `parent=` and
`relativeTo=` a `TipBuddyTooltipStatusBar` it never declares
(TipBuddy.xml l.2320, l.2591) — a name only this element can publish. -->
<StatusBar name="$parentStatusBar" hidden="true">
<Size><AbsDimension x="0" y="8"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="-1"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnValueChanged>HealthBar_OnValueChanged(arg1)</OnValueChanged>
</Scripts>
<BarTexture name="$parentTexture" file="Interface\TargetingFrame\UI-TargetingFrame-BarFill"/>
</StatusBar>
</Frames>
<Scripts>
<!-- ref-GameTooltipTemplate.xml l.611-616. An instance that needs more re-declares the
handler and calls the same helper first (the reference's own shape, and ours:
GameTooltip adds its event registration, ItemRefTooltip its padding) — the loader
walks every <Scripts> block in splice order, so the instance's SetScript lands
last and wins. -->
<OnLoad>GameTooltip_OnLoad(self)</OnLoad>
<OnHide>GameTooltip_OnHide(this)</OnHide>
</Scripts>
</GameTooltip>
<GameTooltip name="GameTooltip" inherits="GameTooltipTemplate" frameStrata="TOOLTIP" hidden="true">
<!-- A default size so the plate resolves the first frame it is shown; the engine's
auto-size pass takes over once the lines are measured (one frame's latency). NO
load-time anchor (the ref instance carries none): every show path seats the plate
first - SetOwner's anchor law for owned hovers, OnTooltipSetDefaultAnchor for world
hovers - and a leftover static anchor is exactly what parked the world tooltip at
screen center when the default-anchor script wasn't wired. -->
<Size><AbsDimension x="120" y="32"/></Size>
<Frames>
<!-- The money row's coin trio (the real $parentMoneyFrame — GameTooltipTemplate.xml,
patch.MPQ — as our shared coin-slot shape; MerchantFrame.xml's BenillaMoney_Set
@@ -315,21 +406,6 @@
</Layer>
</Layers>
</Frame>
<!-- The mouseover health bar (ref-GameTooltipTemplate.xml l.575-597, quoted): 8px
tall, hanging below the plate, UI-TargetingFrame-BarFill, colored by
HealthBar_OnValueChanged. The ENGINE drives min/max/value from the live unit
snapshot (the byte law's HEALTH watcher — SetUnit + every set_unit push). -->
<StatusBar name="$parentStatusBar" hidden="true">
<Size><AbsDimension x="0" y="8"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="-1"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnValueChanged>HealthBar_OnValueChanged(arg1)</OnValueChanged>
</Scripts>
<BarTexture name="$parentTexture" file="Interface\TargetingFrame\UI-TargetingFrame-BarFill"/>
</StatusBar>
</Frames>
<Scripts>
<OnLoad>
@@ -363,47 +439,25 @@
</Scripts>
</GameTooltip>
<!-- The shopping-compare pair (ref GameTooltip.xml l.25-26 / ShoppingTooltipTemplate,
GameTooltipTemplate.xml l.628+): two more GameTooltip widgets the engine seats on the
paperdoll slot when SHOW_COMPARE_TOOLTIP fires (CharacterFrame.xml's listener). The
declared TextLeft/Right1-4 pairs are the template's own small-font ladder, faces quoted
verbatim (including its odd GameFontNormal cells) — the engine ADOPTS declared pairs and
clones the last pair's faces for lines it grows past them (tooltip.rs ensure_lines).
Same plate as the main tooltip, including the Thicken deviation (its rationale is on the
GameTooltip instance above); no coin trio — the compare cut ends before money. -->
<GameTooltip name="ShoppingTooltip1" frameStrata="TOOLTIP" hidden="true">
<Size><AbsDimension x="120" y="32"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize><AbsValue val="16"/></EdgeSize>
<TileSize><AbsValue val="16"/></TileSize>
<BackgroundInsets><AbsInset left="5" right="5" top="5" bottom="5"/></BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentThicken">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="5" y="-5"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="5"/></Offset></Anchor>
</Anchors>
<Color r="0.09" g="0.09" b="0.19" a="0.4"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentTextLeft1" inherits="GameFontNormalSmall" hidden="true"/>
<FontString name="$parentTextRight1" inherits="GameFontNormal" hidden="true"/>
<FontString name="$parentTextLeft2" inherits="GameFontNormal" hidden="true"/>
<FontString name="$parentTextRight2" inherits="GameFontHighlightSmall" hidden="true"/>
<FontString name="$parentTextLeft3" inherits="GameFontHighlightSmall" hidden="true"/>
<FontString name="$parentTextRight3" inherits="GameFontHighlightSmall" hidden="true"/>
<FontString name="$parentTextLeft4" inherits="GameTooltipTextSmall" hidden="true"/>
<FontString name="$parentTextRight4" inherits="GameFontHighlightSmall" hidden="true"/>
</Layer>
</Layers>
<Scripts>
<OnLoad>GameTooltip_OnLoad(self)</OnLoad>
</Scripts>
</GameTooltip>
<GameTooltip name="ShoppingTooltip2" frameStrata="TOOLTIP" hidden="true">
<!--
ShoppingTooltipTemplate (ref-GameTooltipTemplate.xml l.628-1217) — the shopping-compare
pair's own template, a SEPARATE virtual from GameTooltipTemplate exactly as the reference
has it (its instances, ref-GameTooltip.xml l.24-25, inherit this and not the main one).
It repeats the plate rather than inheriting it because the reference's does, and because
inheriting GameTooltipTemplate would hand the compare plates its OnHide, its status bar and
(later) its money row — none of which the compare cut has.
The declared TextLeft/Right1-4 pairs are this template's own small-font ladder, faces
quoted verbatim (including its odd GameFontNormal cells) — the engine ADOPTS declared pairs
and clones the last pair's faces for lines it grows past them (tooltip.rs ensure_lines).
Same plate as the main tooltip, including the Thicken deviation (its rationale is on
GameTooltipTemplate above); no coin trio — the compare cut ends before money.
No addon in the corpus names this template (0 of 218, against GameTooltipTemplate's 27), so
it earns its keep as OUR de-duplication, not as an addon surface: the two instances below
were byte-identical apart from the name.
-->
<GameTooltip name="ShoppingTooltipTemplate" frameStrata="TOOLTIP" hidden="true" virtual="true">
<Size><AbsDimension x="120" y="32"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize><AbsValue val="16"/></EdgeSize>
@@ -435,4 +489,10 @@
<OnLoad>GameTooltip_OnLoad(self)</OnLoad>
</Scripts>
</GameTooltip>
<!-- The shopping-compare pair (ref GameTooltip.xml l.24-25, quoted down to the one-line shape):
two more GameTooltip widgets the engine seats on the paperdoll slot when
SHOW_COMPARE_TOOLTIP fires (CharacterFrame.xml's listener). -->
<GameTooltip name="ShoppingTooltip1" inherits="ShoppingTooltipTemplate" frameStrata="TOOLTIP" hidden="true"/>
<GameTooltip name="ShoppingTooltip2" inherits="ShoppingTooltipTemplate" frameStrata="TOOLTIP" hidden="true"/>
</Ui>
+57 -55
View File
@@ -3,8 +3,8 @@
benilla's gossip dialog (decision 0081 phase 3), RE-SKINNED to the real 1.12 gossip/quest
greeting window (dressing arc, decision 0084): the geometry and art below are QUOTED from the
real `GossipFrame.xml` (scratchpad `ref-GossipFrame.xml`; line citations inline), turned onto
our own feeds/intents. The name `BenillaGossipFrame`, its panel registration
(UiPanels.xml's UIPanelWindows["BenillaGossipFrame"], area="left", pushable=0), the Era gossip
our own feeds/intents. The name `GossipFrame`, its panel registration
(UiPanels.xml's UIPanelWindows["GossipFrame"], area="left", pushable=0), the Era gossip
feeds (GetGossipText/GetGossipOptions/IsGossipOptionCoded/SelectGossipOption/CloseGossip/
GetNumGossipQuests/GetGossipQuestInfo/SelectGossipQuest), and the intent functions are
UNCHANGED — only the window's dressing is real now.
@@ -24,7 +24,7 @@
over our OWN flatter seam (`benilla-ui` `script/gossip.rs`): the app already resolves the wire's
available/active split and delivers ONE ordered `GetNumGossipQuests()`/`GetGossipQuestInfo(i)`
list (quest rows first, in wire order), so `BenillaGossipFrame_Update` below runs a single loop
over the shared `BenillaGossipRow1..8` pool — quest rows fill from i=1, option rows fill after
over the shared `GossipRow1..8` pool — quest rows fill from i=1, option rows fill after
them — instead of the ref's three separate counters; a row's `.kind` ("quest"/"option") plus its
`.itemIndex` (the row's 1-based position within ITS OWN list) is all `BenillaGossipRow_OnClick`
needs to call the right one of `SelectGossipQuest`/`SelectGossipOption` (ref's three intents
@@ -62,7 +62,7 @@
-->
<Script><![CDATA[
-- The shared row pool's cap (decision 0088 §3): quest rows fill from slot 1, option rows fill
-- after them, both drawing from the SAME BenillaGossipRow1..32 buttons (ref's single
-- after them, both drawing from the SAME GossipRow1..32 buttons (ref's single
-- GossipTitleButton pool, l.24-29/63-128). This IS the reference's number
-- (`NUMGOSSIPBUTTONS = 32`, ref-GossipFrame.lua l.1); benilla ran a cap of 8 only because an
-- overflowing menu had nowhere to go — with the scroll frame below, it does.
@@ -72,8 +72,8 @@
-- measure (BenillaScroll_ResizeChild, ScrollTemplates.xml). Built once at load, like the
-- quest window's per-panel lists.
BENILLA_GOSSIP_SCROLL_WIDGETS = (function()
local t = { "BenillaGossipGreetingText" }
for i = 1, BENILLA_GOSSIP_MAX do table.insert(t, "BenillaGossipRow" .. i) end
local t = { "GossipGreetingText" }
for i = 1, BENILLA_GOSSIP_MAX do table.insert(t, "GossipRow" .. i) end
return t
end)()
@@ -125,7 +125,7 @@
-- (the ref grows its child with the 15px GossipSpacerFrame under the last row instead —
-- ref-GossipFrame.lua l.39-44; the shared kit's BENILLA_SCROLL_PAD is that breathing room).
function BenillaGossipFrame_SizeScrollChild()
BenillaScroll_ResizeChild("BenillaGossipGreetingScroll", BENILLA_GOSSIP_SCROLL_WIDGETS)
BenillaScroll_ResizeChild("GossipGreetingScroll", BENILLA_GOSSIP_SCROLL_WIDGETS)
end
-- The convergence driver (the tab-fit / quest-panel idiom): while the menu is up, re-fit
@@ -135,7 +135,7 @@
-- so the scroll range sees their settled heights in the same pass.
function BenillaGossipFrame_OnUpdate()
for i = 1, BENILLA_GOSSIP_MAX do
local button = getglobal("BenillaGossipRow" .. i)
local button = getglobal("GossipRow" .. i)
if button:IsShown() then
BenillaGossipRow_Resize(button)
end
@@ -156,13 +156,13 @@
end
function BenillaGossipFrame_Update()
getglobal("BenillaGossipGreetingText"):SetText(GetGossipText() or "")
getglobal("GossipGreetingText"):SetText(GetGossipText() or "")
local numQuests = GetNumGossipQuests()
-- GetGossipOptions() returns a flat vararg of (label, type) pairs (the Era shape).
local opts = { GetGossipOptions() }
local numOptions = #opts / 2
for i = 1, BENILLA_GOSSIP_MAX do
local button = getglobal("BenillaGossipRow" .. i)
local button = getglobal("GossipRow" .. i)
if i <= numQuests then
-- Quest rows fill first (ref l.27-28: available then active, already ordered by
-- the app before it reaches GetGossipQuestInfo) — the row's OWN 1-based position
@@ -201,11 +201,12 @@
end
-- The ref's GossipFrameUpdate tail (ref-GossipFrame.lua l.46-48): a repaint always lands
-- back at the TOP of the menu, then re-ranges the bar off the new content.
getglobal("BenillaGossipGreetingScroll"):SetVerticalScroll(0)
getglobal("GossipGreetingScroll"):SetVerticalScroll(0)
BenillaGossipFrame_SizeScrollChild()
end
function BenillaGossipFrame_OnLoad(frame)
function GossipFrame_OnLoad()
local frame = this
-- The NPC portrait: the "npc" unit token → the interaction NPC's live model bake
-- (crates/benilla/src/portrait, InteractNpc). Bound once; the app re-resolves it every
-- frame, so a revisit to a different gossip NPC shows that face.
@@ -217,10 +218,11 @@
-- The gossip NPC's name arrives as arg1 of GOSSIP_SHOW (the app resolves the NPC guid through
-- the NameCache); nil/empty until it lands → a blank banner (mirrors the merchant title).
function BenillaGossipFrame_SetTitle(name)
getglobal("BenillaGossipNpcNameText"):SetText(name or "")
getglobal("GossipNpcNameText"):SetText(name or "")
end
function BenillaGossipFrame_OnEvent(frame)
function GossipFrame_OnEvent()
local frame = this
if event == "GOSSIP_SHOW" then
BenillaGossipFrame_SetTitle(arg1)
BenillaGossipFrame_Update()
@@ -234,7 +236,7 @@
-- vanilla's client-side close sends no packet; the app clears the menu on this intent
-- (which fires GOSSIP_CLOSED), and we hide immediately so there's no one-frame lag.
CloseGossip()
HideUIPanel(getglobal("BenillaGossipFrame"))
HideUIPanel(getglobal("GossipFrame"))
end
]]></Script>
@@ -274,7 +276,7 @@
</Scripts>
</Button>
<Frame name="BenillaGossipFrame" hidden="true">
<Frame name="GossipFrame" hidden="true">
<!-- The real 384×512 window (ref l.128). No <Anchors> — ShowUIPanel/SetLeftFrame SetPoints this
to the left slot on open. -->
<Size><AbsDimension x="384" y="512"/></Size>
@@ -322,7 +324,7 @@
on the window at TOP -23. The app delivers the name as arg1 of GOSSIP_SHOW; blank
until it lands. CENTER + the single-point (~zero-width) rect keeps the name centred
regardless of length, exactly like the merchant's title. -->
<FontString name="BenillaGossipNpcNameText" inherits="GameFontHighlight" justifyH="CENTER">
<FontString name="GossipNpcNameText" inherits="GameFontHighlight" justifyH="CENTER">
<Anchors>
<Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-23"/></Offset></Anchor>
</Anchors>
@@ -348,7 +350,7 @@
re-anchored per update by GossipFrame.lua l.39-44); ours is the shared kit's
BENILLA_SCROLL_PAD, the same breathing room by a plainer mechanism.
-->
<ScrollFrame name="BenillaGossipGreetingScroll">
<ScrollFrame name="GossipGreetingScroll">
<Size><AbsDimension x="300" y="334"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="23" y="-81"/></Offset></Anchor></Anchors>
<Scripts>
@@ -378,7 +380,7 @@
<Layer level="ARTWORK">
<!-- Greeting body: the real GossipGreetingText (ref l.241-252) — QuestFont,
LEFT, 270 wide so the engine wraps it, at the child's +(10,-10). -->
<FontString name="BenillaGossipGreetingText" inherits="QuestFont" justifyH="LEFT">
<FontString name="GossipGreetingText" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="10" y="-10"/></Offset></Anchor></Anchors>
</FontString>
@@ -392,38 +394,38 @@
the rest off the previous row's BOTTOMLEFT (ref l.256-269) — one static
chain, so rows re-flow off each other's measured heights with no runtime
repositioning. -->
<Button name="BenillaGossipRow1" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipGreetingText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-10" y="-20"/></Offset></Anchor></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow2" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow1" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow3" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow2" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow4" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow3" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow5" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow4" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow6" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow5" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow7" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow6" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow8" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow7" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow9" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow8" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow10" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow9" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow11" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow10" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow12" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow11" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow13" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow12" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow14" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow13" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow15" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow14" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow16" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow15" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow17" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow16" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow18" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow17" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow19" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow18" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow20" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow19" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow21" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow20" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow22" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow21" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow23" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow22" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow24" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow23" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow25" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow24" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow26" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow25" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow27" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow26" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow28" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow27" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow29" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow28" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow30" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow29" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow31" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow30" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="BenillaGossipRow32" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow31" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow1" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipGreetingText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-10" y="-20"/></Offset></Anchor></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow2" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow1" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow3" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow2" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow4" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow3" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow5" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow4" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow6" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow5" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow7" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow6" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow8" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow7" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow9" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow8" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow10" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow9" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow11" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow10" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow12" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow11" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow13" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow12" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow14" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow13" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow15" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow14" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow16" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow15" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow17" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow16" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow18" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow17" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow19" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow18" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow20" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow19" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow21" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow20" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow22" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow21" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow23" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow22" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow24" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow23" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow25" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow24" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow26" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow25" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow27" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow26" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow28" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow27" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow29" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow28" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow30" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow29" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow31" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow30" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
<Button name="GossipRow32" inherits="BenillaGossipRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipRow31" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
</Frames>
</Frame>
</Frames>
@@ -431,7 +433,7 @@
<!-- Close: the real GossipFrameCloseButton position (CENTER on the window's TOPRIGHT,
-42,-31; ref l.179-187), on the minimize-button art our close already used (the real
UIPanelCloseButton art rides the tooltip/dialog arc). -->
<Button name="BenillaGossipCloseButton">
<Button name="GossipCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-42" y="-31"/></Offset></Anchor>
@@ -451,7 +453,7 @@
(GlobalStrings GOODBYE = "Goodbye") is an OVERLAY FontString centred on the button (the
real ButtonText draws in the OVERLAY layer). Wired to the same close path as the close
button (CloseGossip → HideUIPanel), mirroring the ref's OnClick HideUIPanel(GossipFrame). -->
<Button name="BenillaGossipGoodbyeButton">
<Button name="GossipGoodbyeButton">
<Size><AbsDimension x="78" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-39" y="73"/></Offset></Anchor>
@@ -487,8 +489,8 @@
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaGossipFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaGossipFrame_OnEvent(self)</OnEvent>
<OnLoad>GossipFrame_OnLoad()</OnLoad>
<OnEvent>GossipFrame_OnEvent()</OnEvent>
<!-- The row auto-height's settle pass (see BenillaGossipRow_Resize): our text measure
is a frame late, so the fit re-runs here until every visible row's label measure
lands. Fires only while the window is effectively visible. -->
+31 -17
View File
@@ -62,12 +62,24 @@
may be `nil` while the item template is still in flight). `GetLootRollItemLink` DOES exist now
(decision 1059) but is the click's, not the hover's: the tooltip wants the id, not the markup.
`toplevel="true"` (ref l.243) IS OMITTED — `SetToplevel` isn't in the v1 object model yet
(`loader/geometry.rs`'s `apply_attrs`: a warn-once "gap", same as `movable`/`resizable`); other
benilla toplevel popups (BuffFrame.xml, CastingBar.xml) carry the attribute anyway and simply
eat the warning, but it has ZERO engine effect either way today, so keeping this file's own
load warning-free (this assignment's own bar) costs nothing real — trivially re-added the day
`SetToplevel` lands.
`toplevel="true"` (ref l.243) IS STILL OMITTED, but for a NEW reason — the old one ("`SetToplevel`
isn't in the v1 object model yet ... trivially re-added the day `SetToplevel` lands") has expired.
`SetToplevel` landed: the loader calls the real method and the raise law — occlusion-gated,
confined to this frame's own stratum — is `script::object::toplevel` (wow-re
`ui/scratch/toplevel-raise.md`).
What adding it back does, measured rather than guessed: `GroupLootFrame1`'s rect
(390.5,60)-(633.5,144) overlaps `ExhaustionTick` — `ActionBar.xml` l.1111, `frameStrata="DIALOG"`
transcribed verbatim from ref-MainMenuBar.xml l.415-442 — in a ~2.5 px band, so showing roll
frame 1 raises its whole subtree to DIALOG level 3 while frame 2 (which overlaps nothing, the
four stack with a 15 px gap) stays at level 0. That is exactly what the reference does, and the
only pixels it moves are the sliver where the roll frame meets the tick. It does, however, break
`group_loot_tests::start_loot_roll_claims_frames_in_order_and_paints_the_roll`, whose
`quad_center("UI-GroupLoot-Dice-Up")` picks the FIRST dice quad in painter order as a stand-in
for "frame 1's Need button" — an unstated assumption that declaration order is draw order among
the four, which the raise legitimately ends. That is a fixture proxy, not the test's assertion
(the BoP-confirm gate it proves is untouched), so re-adding the attribute is one word plus a
two-line fix to that helper — a director's call on the look, not a blocked mechanism.
NAMED FONT OBJECTS ARE SUPPORTED on this branch (the loader's top-level `<Font>` handling,
`loader/mod.rs`'s `do_font`, plus the full `Fonts.xml` transcription, both landed since
@@ -75,9 +87,9 @@
`inherits="GameFontNormalSmall"` below resolves for real; no stale caveat needed here.
IN-FLIGHT ITEMS: `GetLootRollItemInfo`'s `name`/`texture`/`quality` are `nil` while the item
template query hasn't landed yet (`loot_roll.rs`). `BenillaGroupLootFrame_OnShow` falls back to
template query hasn't landed yet (`loot_roll.rs`). `GroupLootFrame_OnShow` falls back to
`BENILLA_FALLBACK_ICON` / an empty name / common (quality 1) — the same fallback shape
`BenillaLootFrame_Update` uses for an in-flight loot row.
`LootFrame_Update` uses for an in-flight loot row.
The Era API this drives (`benilla-ui/src/script/loot_roll.rs`, decision 0591):
`GetLootRollItemInfo(rollID)` -> texture,name,count,quality,bindOnPickUp,itemID;
@@ -125,7 +137,7 @@
-- of the four frames for a freshly opened roll, seed its rollID/rollTime, and prime the
-- Timer StatusBar's max before showing it (fields set BEFORE Show(), so OnShow below can
-- already read frame.rollID).
function BenillaGroupLootFrame_OpenNewFrame(rollId, rollTime)
function GroupLootFrame_OpenNewFrame(rollId, rollTime)
for i = 1, BENILLA_NUM_GROUP_LOOT_FRAMES do
local frame = getglobal("GroupLootFrame" .. i)
if frame and not frame:IsVisible() then
@@ -143,7 +155,7 @@
-- GroupLootFrame_OnShow's body (ref l.260-277): resolve the roll's display fields and paint
-- the icon/name/backdrop. name/texture/quality may be nil while the item template query is
-- still in flight (loot_roll.rs) — fall back to the fallback icon / blank name / common (1)
-- quality rather than erroring, the same fallback shape BenillaLootFrame_Update uses for an
-- quality rather than erroring, the same fallback shape LootFrame_Update uses for an
-- in-flight loot row (LootFrame.xml).
--
-- SPLIT OUT OF OnShow, unlike the reference, because it must be re-runnable: the ref's
@@ -182,7 +194,8 @@
nameText:SetVertexColor(color.r, color.g, color.b)
end
function BenillaGroupLootFrame_OnShow(frame)
function GroupLootFrame_OnShow()
local frame = this
BenillaGroupLootFrame_Paint(frame)
end
@@ -195,7 +208,8 @@
frame:RegisterEvent("UPDATE_LOOT_ROLL")
end
function BenillaGroupLootFrame_OnEvent(frame)
function GroupLootFrame_OnEvent()
local frame = this
if arg1 ~= frame.rollID then
return
end
@@ -320,7 +334,7 @@
</Layer>
<Layer level="OVERLAY">
<!-- The gold BoP dragon flourish (ref l.287-298): hidden by default, shown only for a
bind-on-pickup roll (BenillaGroupLootFrame_OnShow). -->
bind-on-pickup roll (GroupLootFrame_OnShow). -->
<Texture name="$parentDecoration" file="Interface\DialogFrame\UI-DialogBox-Gold-Dragon" hidden="true">
<Size><AbsDimension x="120" y="120"/></Size>
<Anchors>
@@ -363,7 +377,7 @@
</Button>
<!-- Pass (ref $parentPassButton, l.364-384): the ref inherits UIPanelCloseButton, which
this engine ships no template for — reconstructed on the same minimize-button art
BenillaLootFrame's own close button uses (LootFrame.xml's identical "no
LootFrame's own close button uses (LootFrame.xml's identical "no
UIPanelCloseButton template ships" precedent), at the ref's own 32x32/position. -->
<Button name="$parentPassButton">
<Size><AbsDimension x="32" y="32"/></Size>
@@ -448,8 +462,8 @@
</Backdrop>
<Scripts>
<OnLoad>BenillaGroupLootFrame_OnLoad(self)</OnLoad>
<OnShow>BenillaGroupLootFrame_OnShow(self)</OnShow>
<OnEvent>BenillaGroupLootFrame_OnEvent(self)</OnEvent>
<OnShow>GroupLootFrame_OnShow()</OnShow>
<OnEvent>GroupLootFrame_OnEvent()</OnEvent>
</Scripts>
</Frame>
@@ -495,7 +509,7 @@
</OnLoad>
<OnEvent>
if event == "START_LOOT_ROLL" then
BenillaGroupLootFrame_OpenNewFrame(arg1, arg2)
GroupLootFrame_OpenNewFrame(arg1, arg2)
elseif event == "CONFIRM_LOOT_ROLL" then
-- Ref UIParent.lua:517-524 verbatim, minus the central router: a Need/Greed on a
-- BIND-ON-PICKUP roll sends nothing and asks first (decision 0594). arg1 = rollID,
@@ -136,7 +136,7 @@
-- ref InspectFrame_OnHide (l.53-59): drop the token, play the close sound, and tell the
-- engine to stop inspecting (which stops the app's per-frame slot resolve). No subframe
-- hide-loop is needed (unlike BenillaCharacterFrame_OnHide's): the one page is a true
-- hide-loop is needed (unlike CharacterFrame_OnHide's): the one page is a true
-- engine child of this frame, so it hides on the parent's cascade.
function BenillaInspectFrame_OnHide(frame)
frame.unit = nil
+13 -21
View File
@@ -2,11 +2,15 @@
<!--
The chat-link tooltip (decision 0274 P4) - ref ItemRef.xml/ItemRef.lua, quoted: a
standalone GameTooltip instance parked above the bottom center that SetItemRef fills via
SetHyperlink when a chat item link is clicked, plus its corner close button. The plate is
the shared tooltip plate (GameTooltip.xml has the art + Thicken rationale); the ref gets
it from GameTooltipTemplate.
SetHyperlink when a chat item link is clicked, plus its corner close button. The plate,
the status bar and the OnHide come from GameTooltipTemplate — the same inheritance the ref
uses (ref ItemRef.xml l.4) — declared in GameTooltip.xml, which the manifest loads first.
Departures, each named:
- No coin row (ref: the template's $parentMoneyFrame): our money row is the coin trio the
GameTooltip instance declares, and its OnTooltipAddMoney/OnTooltipCleared pair stays
there with it, so a chat-link tooltip renders no price line. The reasoning is on
GameTooltipTemplate.
- No drag (ref: movable + RegisterForDrag + StartMoving/StopMovingOrSizing +
ValidateFramePosition): benilla has no user-moved-frame affordance yet - the window
sits on its declared anchor.
@@ -87,27 +91,11 @@
end
]]></Script>
<GameTooltip name="ItemRefTooltip" frameStrata="HIGH" hidden="true">
<GameTooltip name="ItemRefTooltip" inherits="GameTooltipTemplate" frameStrata="HIGH" hidden="true">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="80"/></Offset></Anchor>
</Anchors>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize><AbsValue val="16"/></EdgeSize>
<TileSize><AbsValue val="16"/></TileSize>
<BackgroundInsets><AbsInset left="5" right="5" top="5" bottom="5"/></BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentThicken">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="5" y="-5"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="5"/></Offset></Anchor>
</Anchors>
<Color r="0.09" g="0.09" b="0.19" a="0.4"/>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- ref ItemRefCloseButton (ItemRef.xml l.17-36, quoted): the corner minimize-style
close; OnLoad's SetPadding(16) keeps it off the text. -->
@@ -125,11 +113,15 @@
</Button>
</Frames>
<Scripts>
<!-- The template's own OnLoad, re-declared to add the padding (ref ItemRef.xml
l.38-42's shape, minus its drag arming — see the no-drag departure above); the
loader's later SetScript wins, so GameTooltip_OnLoad has to be called here too.
OnHide is the template's unchanged, so it is not re-declared — ref ItemRef.xml
declares none either. -->
<OnLoad>
GameTooltip_OnLoad(self)
self:SetPadding(16)
</OnLoad>
<OnHide>GameTooltip_OnHide(this)</OnHide>
</Scripts>
</GameTooltip>
+66 -66
View File
@@ -2,7 +2,7 @@
<!--
The item-text reader — letters, books, plaques (the mail arc's "permanent copy" lands
here). Transcribed from the reference ItemTextFrame.xml (354 l) + ItemTextFrame.lua
(104 l); geometry/art quoted verbatim, names Benilla-prefixed. Driven by the engine's
(104 l); geometry/art quoted verbatim, names the reference's own. Driven by the engine's
item-text seam (benilla-ui/src/script/item_text.rs) + crate::ui_item_text's feed/drain:
the app fires the reference event flow ITEM_TEXT_BEGIN → ITEM_TEXT_READY → ITEM_TEXT_CLOSED.
@@ -45,7 +45,7 @@
["Bronze"] = {0.93, 0.82, 0}
}
-- ref UIParent.lua l.1880-1888.
function BenillaGetMaterialTextColors(material)
function GetMaterialTextColors(material)
local textColor = BENILLA_MATERIAL_TEXT_COLOR_TABLE[material]
local titleColor = BENILLA_MATERIAL_TITLETEXT_COLOR_TABLE[material]
if not (textColor and titleColor) then
@@ -56,7 +56,7 @@
end
-- ref ItemTextFrame.lua l.1-8.
function BenillaItemTextFrame_OnLoad()
function ItemTextFrame_OnLoad()
this:RegisterEvent("ITEM_TEXT_BEGIN")
this:RegisterEvent("ITEM_TEXT_TRANSLATION")
this:RegisterEvent("ITEM_TEXT_READY")
@@ -64,26 +64,26 @@
end
-- ref l.10-96.
function BenillaItemTextFrame_OnEvent(event)
function ItemTextFrame_OnEvent(event)
if event == "ITEM_TEXT_BEGIN" then
BenillaItemTextTitleText:SetText(ItemTextGetItem())
BenillaItemTextScrollFrame:Hide()
BenillaItemTextCurrentPage:Hide()
BenillaItemTextStatusBar:Hide()
BenillaItemTextPrevPageButton:Hide()
BenillaItemTextNextPageButton:Hide()
ItemTextTitleText:SetText(ItemTextGetItem())
ItemTextScrollFrame:Hide()
ItemTextCurrentPage:Hide()
ItemTextStatusBar:Hide()
ItemTextPrevPageButton:Hide()
ItemTextNextPageButton:Hide()
local material = ItemTextGetMaterial()
if not material then material = "Parchment" end
local textColor = BenillaGetMaterialTextColors(material)
BenillaItemTextPageText:SetTextColor(textColor[1], textColor[2], textColor[3])
local textColor = GetMaterialTextColors(material)
ItemTextPageText:SetTextColor(textColor[1], textColor[2], textColor[3])
return
end
if event == "ITEM_TEXT_TRANSLATION" then
BenillaItemTextPrevPageButton:Hide()
BenillaItemTextNextPageButton:Hide()
ItemTextPrevPageButton:Hide()
ItemTextNextPageButton:Hide()
this.translationElapsed = 0
BenillaItemTextStatusBar:SetMinMaxValues(0, arg1)
BenillaItemTextStatusBar:Show()
ItemTextStatusBar:SetMinMaxValues(0, arg1)
ItemTextStatusBar:Show()
ShowUIPanel(this)
if not this:IsVisible() then CloseItemText() end
return
@@ -92,45 +92,45 @@
local creator = ItemTextGetCreator()
if creator then
creator = "\n\n".."From,".."\n"..creator.."\n\n" -- ITEM_TEXT_FROM
BenillaItemTextPageText:SetText("\n"..ItemTextGetText()..creator)
ItemTextPageText:SetText("\n"..ItemTextGetText()..creator)
else
BenillaItemTextPageText:SetText("\n"..ItemTextGetText().."\n")
ItemTextPageText:SetText("\n"..ItemTextGetText().."\n")
end
BenillaItemTextScrollFrame:Show()
ItemTextScrollFrame:Show()
local page = ItemTextGetPage()
local next = ItemTextHasNextPage()
local material = ItemTextGetMaterial()
if not material then material = "Parchment" end
if material == "Parchment" then
BenillaItemTextMaterialTopLeft:Hide()
BenillaItemTextMaterialTopRight:Hide()
BenillaItemTextMaterialBotLeft:Hide()
BenillaItemTextMaterialBotRight:Hide()
ItemTextMaterialTopLeft:Hide()
ItemTextMaterialTopRight:Hide()
ItemTextMaterialBotLeft:Hide()
ItemTextMaterialBotRight:Hide()
else
BenillaItemTextMaterialTopLeft:Show()
BenillaItemTextMaterialTopRight:Show()
BenillaItemTextMaterialBotLeft:Show()
BenillaItemTextMaterialBotRight:Show()
BenillaItemTextMaterialTopLeft:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-TopLeft")
BenillaItemTextMaterialTopRight:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-TopRight")
BenillaItemTextMaterialBotLeft:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-BotLeft")
BenillaItemTextMaterialBotRight:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-BotRight")
ItemTextMaterialTopLeft:Show()
ItemTextMaterialTopRight:Show()
ItemTextMaterialBotLeft:Show()
ItemTextMaterialBotRight:Show()
ItemTextMaterialTopLeft:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-TopLeft")
ItemTextMaterialTopRight:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-TopRight")
ItemTextMaterialBotLeft:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-BotLeft")
ItemTextMaterialBotRight:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-BotRight")
end
if (page > 1) or next then
BenillaItemTextCurrentPage:SetText(page)
BenillaItemTextCurrentPage:Show()
ItemTextCurrentPage:SetText(page)
ItemTextCurrentPage:Show()
if page > 1 then
BenillaItemTextPrevPageButton:Show()
ItemTextPrevPageButton:Show()
else
BenillaItemTextPrevPageButton:Hide()
ItemTextPrevPageButton:Hide()
end
if next then
BenillaItemTextNextPageButton:Show()
ItemTextNextPageButton:Show()
else
BenillaItemTextNextPageButton:Hide()
ItemTextNextPageButton:Hide()
end
end
BenillaItemTextStatusBar:Hide()
ItemTextStatusBar:Hide()
ShowUIPanel(this)
if not this:IsVisible() then CloseItemText() end
return
@@ -142,15 +142,15 @@
end
-- ref l.98-104.
function BenillaItemTextFrame_OnUpdate(elapsed)
if BenillaItemTextStatusBar:IsVisible() then
function ItemTextFrame_OnUpdate(elapsed)
if ItemTextStatusBar:IsVisible() then
elapsed = this.translationElapsed + elapsed
BenillaItemTextStatusBar:SetValue(elapsed)
ItemTextStatusBar:SetValue(elapsed)
this.translationElapsed = elapsed
end
end
]]></Script>
<Frame name="BenillaItemTextFrame" toplevel="true" movable="true" enableMouse="true" hidden="true" parent="UIParent">
<Frame name="ItemTextFrame" toplevel="true" movable="true" enableMouse="true" hidden="true" parent="UIParent">
<Size><AbsDimension x="384" y="512"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-104"/></Offset></Anchor></Anchors>
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="70"/></HitRectInsets>
@@ -182,29 +182,29 @@
<Layer level="ARTWORK">
<!-- The non-parchment material corners (stone/marble/…): textures swapped in by
ITEM_TEXT_READY; hidden entirely for parchment (ref l.56-70). -->
<Texture name="BenillaItemTextMaterialTopLeft" hidden="true">
<Texture name="ItemTextMaterialTopLeft" hidden="true">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="21" y="-75"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="BenillaItemTextMaterialTopRight" hidden="true">
<Texture name="ItemTextMaterialTopRight" hidden="true">
<Size><AbsDimension x="64" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaItemTextMaterialTopLeft" relativePoint="TOPRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="ItemTextMaterialTopLeft" relativePoint="TOPRIGHT"/></Anchors>
</Texture>
<Texture name="BenillaItemTextMaterialBotLeft" hidden="true">
<Texture name="ItemTextMaterialBotLeft" hidden="true">
<Size><AbsDimension x="256" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaItemTextMaterialTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="ItemTextMaterialTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="BenillaItemTextMaterialBotRight" hidden="true">
<Texture name="ItemTextMaterialBotRight" hidden="true">
<Size><AbsDimension x="64" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaItemTextMaterialTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="ItemTextMaterialTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="BenillaItemTextTitleText" inherits="GameFontNormal">
<FontString name="ItemTextTitleText" inherits="GameFontNormal">
<Size><AbsDimension x="224" y="14"/></Size>
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="6" y="230"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaItemTextCurrentPage" inherits="GameFontNormal">
<FontString name="ItemTextCurrentPage" inherits="GameFontNormal">
<Size><AbsDimension x="192" y="0"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="10" y="-50"/></Offset></Anchor></Anchors>
</FontString>
@@ -213,7 +213,7 @@
<Frames>
<!-- ref ItemTextScrollFrame l.145-219: the plain-Frame stand-in (see the header) with
the ref's own flanking scrollbar art kept verbatim. -->
<Frame name="BenillaItemTextScrollFrame" hidden="true">
<Frame name="ItemTextScrollFrame" hidden="true">
<Size><AbsDimension x="280" y="355"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-66" y="-76"/></Offset></Anchor></Anchors>
<Layers>
@@ -222,28 +222,28 @@
SetPoint time — a target declared later falls back to the parent (and
the black track lands on the parchment). -->
<Layer level="ARTWORK">
<Texture name="BenillaItemTextScrollFrameTop" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Texture name="ItemTextScrollFrameTop" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size><AbsDimension x="28" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="0" y="5"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.484375" top="0" bottom="1.0"/>
</Texture>
<Texture name="BenillaItemTextScrollFrameBottom" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Texture name="ItemTextScrollFrameBottom" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size><AbsDimension x="28" y="107"/></Size>
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors>
<TexCoords left="0.515625" right="1.0" top="0" bottom="0.4140625"/>
</Texture>
</Layer>
<Layer level="BACKGROUND">
<Texture name="BenillaItemTextScrollFrameMiddle">
<Texture name="ItemTextScrollFrameMiddle">
<Size><AbsDimension x="25" y="365"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="BenillaItemTextScrollFrameTop"/></Anchors>
<Anchors><Anchor point="TOPRIGHT" relativeTo="ItemTextScrollFrameTop"/></Anchors>
<Color r="0" g="0" b="0" a="0.8"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- ref ItemTextPageText (SimpleHTML 270×304 at 0,-15 in the scroll child)
as a FontString at the same rect. -->
<FontString name="BenillaItemTextPageText" inherits="ItemTextFontNormal" justifyH="LEFT" justifyV="TOP">
<FontString name="ItemTextPageText" inherits="ItemTextFontNormal" justifyH="LEFT" justifyV="TOP">
<Size><AbsDimension x="270" y="304"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
</FontString>
@@ -252,9 +252,9 @@
</Frame>
<!-- ref ItemTextStatusBar l.220-251 — the ITEM_TEXT_TRANSLATION progress bar; never
fires on our letters, kept for fidelity. -->
<StatusBar name="BenillaItemTextStatusBar" hidden="true">
<StatusBar name="ItemTextStatusBar" hidden="true">
<Size><AbsDimension x="250" y="21"/></Size>
<Anchors><Anchor point="BOTTOM" relativeTo="BenillaItemTextFrame"><Offset><AbsDimension x="-5" y="128"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="BOTTOM" relativeTo="ItemTextFrame"><Offset><AbsDimension x="-5" y="128"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorder">
@@ -270,7 +270,7 @@
<BarTexture file="Interface\TargetingFrame\UI-TargetingFrame-BarFill"/>
<BarColor r="0.25" g="0.75" b="0.25"/>
</StatusBar>
<Button name="BenillaItemTextPrevPageButton" hidden="true">
<Button name="ItemTextPrevPageButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="90" y="-56"/></Offset></Anchor></Anchors>
<Layers>
@@ -291,7 +291,7 @@
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="BenillaItemTextNextPageButton" hidden="true">
<Button name="ItemTextNextPageButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-55" y="-56"/></Offset></Anchor></Anchors>
<Layers>
@@ -312,11 +312,11 @@
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="BenillaItemTextCloseButton">
<Button name="ItemTextCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-45" y="-26"/></Offset></Anchor></Anchors>
<Scripts>
<OnClick>HideUIPanel(BenillaItemTextFrame)</OnClick>
<OnClick>HideUIPanel(ItemTextFrame)</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
@@ -324,9 +324,9 @@
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaItemTextFrame_OnLoad()</OnLoad>
<OnEvent>BenillaItemTextFrame_OnEvent(event)</OnEvent>
<OnUpdate>BenillaItemTextFrame_OnUpdate(arg1)</OnUpdate>
<OnLoad>ItemTextFrame_OnLoad()</OnLoad>
<OnEvent>ItemTextFrame_OnEvent(event)</OnEvent>
<OnUpdate>ItemTextFrame_OnUpdate(arg1)</OnUpdate>
<OnShow>PlaySound("igMainMenuOpen")</OnShow>
<OnHide>
CloseItemText()
@@ -302,7 +302,7 @@
function KeyBindingsPage_Update()
local page = KeyBindingsPage
local scroll = getglobal("OptionsFrameContainerBodyKeybindingsScrollFrame")
local offset = BenillaFauxScrollFrame_GetOffset(scroll)
local offset = FauxScrollFrame_GetOffset(scroll)
for i = 1, KEYBINDINGS_DISPLAYED do
local name = KEYBINDINGS_ROW_BASE .. i
local row = getglobal(name)
@@ -317,7 +317,7 @@
row:Hide()
end
end
BenillaFauxScrollFrame_Update(scroll, table.getn(page.items),
FauxScrollFrame_Update(scroll, table.getn(page.items),
KEYBINDINGS_DISPLAYED, KEYBINDINGS_ROW_HEIGHT)
KeyBindings_UpdateUnbindKey()
end
+45 -43
View File
@@ -2,7 +2,7 @@
<!--
benilla's loot window (decision 0084), RE-SKINNED to the real 1.12 loot window: the geometry
and art below are QUOTED from the real `LootFrame.xml` (scratchpad `LootFrame.xml`; line
citations inline), turned onto our own feeds/intents. The name `BenillaLootFrame`, its panel
citations inline), turned onto our own feeds/intents. The name `LootFrame`, its panel
registration (UiPanels.xml area="left", pushable=7), the loot feeds, the 4-rows-per-page +
pager logic, and the intent functions are UNCHANGED — only the window's dressing is real now.
@@ -163,8 +163,8 @@
end
-- LootFrame_Update (LootFrame.lua l.67-119): fill the visible rows for the current page.
function BenillaLootFrame_Update()
local frame = getglobal("BenillaLootFrame")
function LootFrame_Update()
local frame = getglobal("LootFrame")
local numItems = GetNumLootItems()
-- Rows per page: 4, or 3 when there's a pager to make room for (l.70-73).
local perPage = BENILLA_LOOT_NUMBUTTONS
@@ -185,7 +185,7 @@
end
for index = 1, BENILLA_LOOT_NUMBUTTONS do
local button = getglobal("BenillaLootButton" .. index)
local button = getglobal("LootButton" .. index)
local slot = (perPage * (frame.page - 1)) + index
if slot <= numItems and index <= perPage
and (LootSlotIsItem(slot) or LootSlotIsCoin(slot)) then
@@ -210,52 +210,54 @@
-- Pager visibility (LootFrame.lua l.105-118): Up when past page 1, Down when more remain.
if frame.page > 1 then
getglobal("BenillaLootUpButton"):Show()
getglobal("LootUpButton"):Show()
else
getglobal("BenillaLootUpButton"):Hide()
getglobal("LootUpButton"):Hide()
end
if frame.page < maxPage then
getglobal("BenillaLootDownButton"):Show()
getglobal("LootDownButton"):Show()
else
getglobal("BenillaLootDownButton"):Hide()
getglobal("LootDownButton"):Hide()
end
end
function BenillaLootFrame_PageDown()
local frame = getglobal("BenillaLootFrame")
function LootFrame_PageDown()
local frame = getglobal("LootFrame")
frame.page = frame.page + 1
BenillaLootFrame_Update()
LootFrame_Update()
end
function BenillaLootFrame_PageUp()
local frame = getglobal("BenillaLootFrame")
function LootFrame_PageUp()
local frame = getglobal("LootFrame")
frame.page = frame.page - 1
BenillaLootFrame_Update()
LootFrame_Update()
end
function BenillaLootFrame_OnLoad(frame)
function LootFrame_OnLoad()
local frame = this
frame.page = 1
frame:RegisterEvent("LOOT_OPENED")
frame:RegisterEvent("LOOT_UPDATE")
frame:RegisterEvent("LOOT_CLOSED")
end
function BenillaLootFrame_OnEvent(frame)
function LootFrame_OnEvent(event)
local frame = this
if event == "LOOT_OPENED" then
-- A fresh loot: reset to page 1, paint, show (LootFrame.lua l.14-16).
frame.page = 1
BenillaLootFrame_Update()
LootFrame_Update()
ShowUIPanel(frame)
elseif event == "LOOT_UPDATE" then
-- A content change while open (name/quality landed, a row/coin removed): repaint,
-- keeping the current page.
BenillaLootFrame_Update()
LootFrame_Update()
elseif event == "LOOT_CLOSED" then
HideUIPanel(frame)
end
end
function BenillaLootFrame_OnShow()
function LootFrame_OnShow()
-- Open sound — the window-sound convention (decision 0090), wrapper form (this handler
-- has real logic beyond the sound, so it can't be a bare inline PlaySound).
-- (LootFrame.lua LootFrame_OnShow, l.131-141). ONLY the empty-roll fork and the fishing
@@ -267,16 +269,16 @@
-- so we stay silent for it — there is no kit to guess. (DR 0086: vmangos masks the
-- lootable flag for empty rolls, so a live empty window is rare — but the branch must
-- exist.)
getglobal("BenillaLootPortraitOverlay"):SetTexture("Interface\\TargetingFrame\\TargetDead")
getglobal("LootPortraitOverlay"):SetTexture("Interface\\TargetingFrame\\TargetDead")
if GetNumLootItems() == 0 then
PlaySound("LOOTWINDOWOPENEMPTY")
elseif IsFishingLoot() then
PlaySound("FISHING REEL IN")
getglobal("BenillaLootPortraitOverlay"):SetTexture("Interface\\LootFrame\\FishingLoot-Icon")
getglobal("LootPortraitOverlay"):SetTexture("Interface\\LootFrame\\FishingLoot-Icon")
end
end
function BenillaLootFrame_OnHide()
function LootFrame_OnHide()
-- Closing the window releases the loot (LootFrame.lua l.143-145). The app only sends the
-- release when a loot is actually open, so a hide after the state already cleared is a
-- harmless no-op. No close sound here: LootFrame_OnHide plays none (the loot-close kit is
@@ -286,7 +288,7 @@
function BenillaLootCloseButton_OnClick()
-- Hide immediately (client-authoritative); OnHide's CloseLoot() sends the release.
HideUIPanel(getglobal("BenillaLootFrame"))
HideUIPanel(getglobal("LootFrame"))
end
]]></Script>
@@ -353,7 +355,7 @@
</Scripts>
</Button>
<Frame name="BenillaLootFrame" hidden="true">
<Frame name="LootFrame" hidden="true">
<!-- The real 256×256 window (ref l.59-60). No <Anchors> here — ShowUIPanel SetPoints this to
its slot. -->
<Size><AbsDimension x="256" y="256"/></Size>
@@ -364,7 +366,7 @@
OnShow resets it to the skull and swaps a fishing loot to FishingLoot-Icon
(decision 1086), exactly as the ref's OnShow does (l.133-140). -->
<Layer level="BORDER">
<Texture name="BenillaLootPortraitOverlay" file="Interface\TargetingFrame\TargetDead">
<Texture name="LootPortraitOverlay" file="Interface\TargetingFrame\TargetDead">
<Size><AbsDimension x="58" y="58"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="10" y="-8"/></Offset></Anchor>
@@ -376,7 +378,7 @@
<Layer level="ARTWORK">
<Texture file="Interface\LootFrame\UI-LootPanel"/>
<!-- Title: the real ITEMS label (ref l.89-97) — GameFontNormal at CENTER +(-12,102). -->
<FontString name="BenillaLootTitle" inherits="GameFontNormal" text="Items">
<FontString name="LootTitle" inherits="GameFontNormal" text="Items">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-12" y="102"/></Offset></Anchor>
</Anchors>
@@ -386,27 +388,27 @@
<Frames>
<!-- The real row stack (ref l.139-174): row 1 at the window TOPLEFT +(24,-80), each next
row centred on the prior row's BOTTOM -4. -->
<Button name="BenillaLootButton1" inherits="BenillaLootButtonTemplate">
<Button name="LootButton1" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="24" y="-80"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 1)</OnLoad></Scripts>
</Button>
<Button name="BenillaLootButton2" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaLootButton1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Button name="LootButton2" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="LootButton1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 2)</OnLoad></Scripts>
</Button>
<Button name="BenillaLootButton3" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaLootButton2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Button name="LootButton3" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="LootButton2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 3)</OnLoad></Scripts>
</Button>
<Button name="BenillaLootButton4" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaLootButton3" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Button name="LootButton4" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="LootButton3" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 4)</OnLoad></Scripts>
</Button>
<!-- Pager (ref l.175-216): Up (page back) at the window BOTTOMLEFT +(25,16), Down (page
forward) at the Up button's RIGHT +86 — both in the bottom-left of the panel. The
ChatIcon scroll arrows are the real art (ref l.191-193, 212-214). -->
<Button name="BenillaLootUpButton" hidden="true">
<Button name="LootUpButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="25" y="16"/></Offset></Anchor>
@@ -415,24 +417,24 @@
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
<Scripts><OnClick>BenillaLootFrame_PageUp()</OnClick></Scripts>
<Scripts><OnClick>LootFrame_PageUp()</OnClick></Scripts>
</Button>
<Button name="BenillaLootDownButton" hidden="true">
<Button name="LootDownButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaLootUpButton" relativePoint="RIGHT"><Offset><AbsDimension x="86" y="0"/></Offset></Anchor>
<Anchor point="LEFT" relativeTo="LootUpButton" relativePoint="RIGHT"><Offset><AbsDimension x="86" y="0"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
<Scripts><OnClick>BenillaLootFrame_PageDown()</OnClick></Scripts>
<Scripts><OnClick>LootFrame_PageDown()</OnClick></Scripts>
</Button>
<!-- Close: the real LootCloseButton position (CENTER on the window's TOPRIGHT, -81,-26;
ref l.119-129), on the minimize-button art it already used (the real LootCloseButton
uses this very art, ref l.135-137). -->
<Button name="BenillaLootCloseButton">
<Button name="LootCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-81" y="-26"/></Offset></Anchor>
@@ -444,10 +446,10 @@
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaLootFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaLootFrame_OnEvent(self)</OnEvent>
<OnShow>BenillaLootFrame_OnShow()</OnShow>
<OnHide>BenillaLootFrame_OnHide()</OnHide>
<OnLoad>LootFrame_OnLoad()</OnLoad>
<OnEvent>LootFrame_OnEvent(event)</OnEvent>
<OnShow>LootFrame_OnShow()</OnShow>
<OnHide>LootFrame_OnHide()</OnHide>
</Scripts>
</Frame>
</Ui>
+6 -6
View File
@@ -350,7 +350,7 @@
local popup = getglobal("BenillaMacroPopupFrame")
local scrollFrame = getglobal("BenillaMacroPopupScrollFrame")
local numMacroIcons = GetNumMacroIcons()
local macroPopupOffset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
local macroPopupOffset = FauxScrollFrame_GetOffset(scrollFrame)
if resetName then
if popup.mode == "new" then
@@ -379,7 +379,7 @@
end
end
BenillaFauxScrollFrame_Update(scrollFrame,
FauxScrollFrame_Update(scrollFrame,
math.ceil(numMacroIcons / NUM_ICONS_PER_ROW), NUM_ICON_ROWS, MACRO_ICON_ROW_HEIGHT)
end
@@ -404,7 +404,7 @@
local popup = getglobal("BenillaMacroPopupFrame")
local scrollFrame = getglobal("BenillaMacroPopupScrollFrame")
popup.selectedIcon = button:GetID()
+ (BenillaFauxScrollFrame_GetOffset(scrollFrame) * NUM_ICONS_PER_ROW)
+ (FauxScrollFrame_GetOffset(scrollFrame) * NUM_ICONS_PER_ROW)
getglobal("BenillaMacroFrameSelectedMacroButtonIcon")
:SetTexture(GetMacroIconInfo(popup.selectedIcon))
BenillaMacroPopupOkayButton_Update()
@@ -892,13 +892,13 @@
<!-- The icon grid's faux scroll (ref a ClassTrainerListScrollFrameTemplate, l.759-775).
The frame itself is invisible; its ONE visible consequence is where the bar seats
(BenillaFauxScrollFrameTemplate hangs it off the frame's right edge +6, inset 16
(FauxScrollFrameTemplate hangs it off the frame's right edge +6, inset 16
top and bottom). So its rect is the reference's VERBATIM — 296x195 at TOPRIGHT
(-39,-67) — which puts the bar at popup x 264..280, on the plate's groove and
bracketing the 4-row grid. 0983 sized it to the grid instead (252x152 at TOPLEFT
24,-85) and the bar came out at x 282..298: hanging half off the plate's right
edge, over the world. -->
<Frame name="BenillaMacroPopupScrollFrame" inherits="BenillaFauxScrollFrameTemplate">
<Frame name="BenillaMacroPopupScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="195"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-39" y="-67"/></Offset></Anchor>
@@ -907,7 +907,7 @@
<!-- The faux kit's contract (ScrollTemplates.xml): the owner sets updateFunc,
the shared Slider handler calls it whenever the row offset moves. -->
<OnLoad>
BenillaFauxScrollFrame_OnLoad(self)
ScrollFrame_OnLoad()
self.updateFunc = BenillaMacroPopupFrame_Update
</OnLoad>
</Scripts>
File diff suppressed because it is too large Load Diff
+141 -139
View File
@@ -3,8 +3,8 @@
benilla's vendor window (decision 0081 phase 4), RE-SKINNED to the real 1.12 merchant window
(dressing arc, decision 0084): the geometry and art below are QUOTED from the real
`MerchantFrame.xml` (scratchpad `ref-MerchantFrame.xml`; line citations inline), turned onto our
own feeds/intents. The name `BenillaMerchantFrame`, its panel registration
(UiPanels.xml's UIPanelWindows["BenillaMerchantFrame"], area="left"), the Era vendor feeds
own feeds/intents. The name `MerchantFrame`, its panel registration
(UiPanels.xml's UIPanelWindows["MerchantFrame"], area="left"), the Era vendor feeds
(GetMerchantNumItems/GetMerchantItemInfo/BuyMerchantItem/CloseMerchant, GetMoney), and the
intent functions are UNCHANGED — only the window's dressing is real now.
@@ -199,14 +199,14 @@
-- The merchant-list index a row button shows on the current page (ref index arithmetic,
-- MerchantFrame.lua l.74).
function BenillaMerchantFrame_ItemIndex(button)
return ((BenillaMerchantFrame.page or 1) - 1) * BENILLA_MERCHANT_MAX + button.merchantIndex
return ((MerchantFrame.page or 1) - 1) * BENILLA_MERCHANT_MAX + button.merchantIndex
end
function BenillaMerchantItem_OnClick(button, mouseButton)
-- Buyback tab: either click buys the slot back (ref MerchantItemButton_OnClick's else
-- arm, l.358-361) — and, as there, with NO modifier branch of any kind: 1.12 ships no
-- GetBuybackItemLink, so a buyback row carries no link at all (ui_merchant.rs).
if BenillaMerchantFrame.selectedTab == 2 then
if MerchantFrame.selectedTab == 2 then
BuybackItem(button.merchantIndex)
return
end
@@ -254,7 +254,7 @@
-- offset back to the icon's TOPRIGHT (37 − 153 = −116). No buy-price line — the price is
-- on the row, and the real vendor tooltip doesn't repeat it.
function BenillaMerchantItem_OnEnter(button)
if BenillaMerchantFrame.selectedTab == 2 then
if MerchantFrame.selectedTab == 2 then
if not GetBuybackItemInfo(button.merchantIndex) then return end
GameTooltip:SetOwner(button, "ANCHOR_RIGHT", -116, 0)
GameTooltip:SetBuybackItem(button.merchantIndex)
@@ -272,14 +272,14 @@
-- Buy tab: remember the row so the frame's OnUpdate arms the coin every frame — Buy
-- vs the grayed UnableBuy by affordability, or the Inspect magnifier while Ctrl is held
-- (ref MerchantFrame.itemHover + OnUpdate l.82/725-731).
BenillaMerchantFrame.itemHover = index
MerchantFrame.itemHover = index
end
function BenillaMerchantItem_OnLeave(button)
GameTooltip:Hide()
-- Drop the coin/inspect override and stop the OnUpdate arming (ref OnLeave l.87-90).
ResetCursor()
BenillaMerchantFrame.itemHover = nil
MerchantFrame.itemHover = nil
end
-- One row's item fill — the shape both pages share (ref fills name/texture/count/money
@@ -337,13 +337,13 @@
-- The merchant page (ref MerchantFrame_UpdateMerchantInfo l.66-193): the paged 10 rows,
-- the repair pair, the single most-recent buyback slot, the page buttons, the merchant
-- bottom border — and the buyback page's pieces hidden.
function BenillaMerchantFrame_UpdateMerchantInfo()
local frame = BenillaMerchantFrame
function MerchantFrame_UpdateMerchantInfo()
local frame = MerchantFrame
local page = frame.page or 1
local numItems = GetMerchantNumItems()
for i = 1, BENILLA_MERCHANT_MAX do
local button = getglobal("BenillaMerchantItem" .. i)
local button = getglobal("MerchantItem" .. i)
local index = (page - 1) * BENILLA_MERCHANT_MAX + i
if index <= numItems then
local name, texture, price, quantity, numAvailable, isUsable = GetMerchantItemInfo(index)
@@ -355,32 +355,32 @@
-- Rows back to the merchant spacing (ref l.188-192; the buyback page spreads them to
-- -15). SetPoint replaces the same-point anchor, the real client's semantics.
getglobal("BenillaMerchantItem3"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem1"), "BOTTOMLEFT", 0, -8)
getglobal("BenillaMerchantItem5"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem3"), "BOTTOMLEFT", 0, -8)
getglobal("BenillaMerchantItem7"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem5"), "BOTTOMLEFT", 0, -8)
getglobal("BenillaMerchantItem9"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem7"), "BOTTOMLEFT", 0, -8)
getglobal("MerchantItem3"):SetPoint("TOPLEFT", getglobal("MerchantItem1"), "BOTTOMLEFT", 0, -8)
getglobal("MerchantItem5"):SetPoint("TOPLEFT", getglobal("MerchantItem3"), "BOTTOMLEFT", 0, -8)
getglobal("MerchantItem7"):SetPoint("TOPLEFT", getglobal("MerchantItem5"), "BOTTOMLEFT", 0, -8)
getglobal("MerchantItem9"):SetPoint("TOPLEFT", getglobal("MerchantItem7"), "BOTTOMLEFT", 0, -8)
-- Buyback-page-only pieces off (ref l.180-186), merchant-only pieces on (ref l.175-178).
getglobal("BenillaMerchantItem11"):Hide()
getglobal("BenillaMerchantItem12"):Hide()
getglobal("BenillaBuybackFrameTopLeft"):Hide()
getglobal("BenillaBuybackFrameTopRight"):Hide()
getglobal("BenillaBuybackFrameBotLeft"):Hide()
getglobal("BenillaBuybackFrameBotRight"):Hide()
getglobal("BenillaMerchantFrameBottomLeftBorder"):Show()
getglobal("BenillaMerchantFrameBottomRightBorder"):Show()
getglobal("MerchantItem11"):Hide()
getglobal("MerchantItem12"):Hide()
getglobal("BuybackFrameTopLeft"):Hide()
getglobal("BuybackFrameTopRight"):Hide()
getglobal("BuybackFrameBotLeft"):Hide()
getglobal("BuybackFrameBotRight"):Hide()
getglobal("MerchantFrameBottomLeftBorder"):Show()
getglobal("MerchantFrameBottomRightBorder"):Show()
-- The repair pair shows iff this vendor repairs (ref l.121-130); the all-button
-- enables iff there is damage to pay for (ref MerchantFrame_OnShow l.37-46 — our
-- vertex-color gray stands in for its SetDesaturation, which the engine lacks).
if CanMerchantRepair() then
getglobal("BenillaMerchantRepairText"):Show()
local allButton = getglobal("BenillaMerchantRepairAllButton")
local itemButton = getglobal("BenillaMerchantRepairItemButton")
getglobal("MerchantRepairText"):Show()
local allButton = getglobal("MerchantRepairAllButton")
local itemButton = getglobal("MerchantRepairItemButton")
allButton:Show()
itemButton:Show()
local repairAllCost, canRepair = GetRepairAllCost()
local icon = getglobal("BenillaMerchantRepairAllIcon")
local icon = getglobal("MerchantRepairAllIcon")
if canRepair then
icon:SetVertexColor(1, 1, 1)
allButton:Enable()
@@ -389,14 +389,14 @@
allButton:Disable()
end
else
getglobal("BenillaMerchantRepairText"):Hide()
getglobal("BenillaMerchantRepairAllButton"):Hide()
getglobal("BenillaMerchantRepairItemButton"):Hide()
getglobal("MerchantRepairText"):Hide()
getglobal("MerchantRepairAllButton"):Hide()
getglobal("MerchantRepairItemButton"):Hide()
end
-- The single buyback slot: the MOST RECENT sale (ref l.132-152 —
-- GetBuybackItemInfo(GetNumBuybackItems())).
local slot = getglobal("BenillaMerchantBuyBackItem")
local slot = getglobal("MerchantBuyBackItem")
local bbName, bbTexture, bbPrice, bbQuantity = GetBuybackItemInfo(GetNumBuybackItems())
if bbName then
slot.icon:SetTexture(bbTexture or BENILLA_FALLBACK_ICON)
@@ -406,13 +406,13 @@
else
slot.count:SetText("")
end
BenillaMoney_Set("BenillaMerchantBuyBackItemPrice", bbPrice, true)
BenillaMoney_Set("MerchantBuyBackItemPrice", bbPrice, true)
slot.slot:SetVertexColor(1, 1, 1)
else
slot.icon:SetTexture(nil)
slot.name:SetText("")
slot.count:SetText("")
BenillaMoney_Clear("BenillaMerchantBuyBackItemPrice")
BenillaMoney_Clear("MerchantBuyBackItemPrice")
slot.slot:SetVertexColor(0.4, 0.4, 0.4)
-- Hide the tooltip upon sale of the last slot (ref l.148-151).
if GameTooltip:IsOwned(slot) then
@@ -422,9 +422,9 @@
slot:Show()
-- Paging (ref l.154-173): buttons + label only when the stock overflows one page.
local prevButton = getglobal("BenillaMerchantPrevPageButton")
local nextButton = getglobal("BenillaMerchantNextPageButton")
local pageText = getglobal("BenillaMerchantPageText")
local prevButton = getglobal("MerchantPrevPageButton")
local nextButton = getglobal("MerchantNextPageButton")
local pageText = getglobal("MerchantPageText")
if numItems > BENILLA_MERCHANT_MAX then
pageText:SetText(string.format("Page %d", page)) -- PAGE_NUMBER
if page == 1 then prevButton:Disable() else prevButton:Enable() end
@@ -445,34 +445,34 @@
-- Title + portrait back to the vendor (ref l.67-68): re-bind the "npc" model bake the
-- buyback page's raw SetTexture(UI-BuyBack-Icon) dropped.
BenillaMerchantFrame_SetTitle(frame.vendorName)
SetPortraitTexture(getglobal("BenillaMerchantFramePortrait"), "npc")
SetPortraitTexture(getglobal("MerchantFramePortrait"), "npc")
end
-- The buyback page (ref MerchantFrame_UpdateBuybackInfo l.195-273): 12 rows over the
-- buyback slots, the wider -15 row spacing, the buyback corner art — and every
-- merchant-only piece hidden.
function BenillaMerchantFrame_UpdateBuybackInfo()
getglobal("BenillaMerchantNameText"):SetText("Merchant Buyback") -- MERCHANT_BUYBACK
function MerchantFrame_UpdateBuybackInfo()
getglobal("MerchantNameText"):SetText("Merchant Buyback") -- MERCHANT_BUYBACK
-- The buyback icon is drawn for the portrait ring — the ref sets it RAW (SetTexture,
-- l.197), not through the portrait mask.
getglobal("BenillaMerchantFramePortrait"):SetTexture("Interface\\MerchantFrame\\UI-BuyBack-Icon")
getglobal("MerchantFramePortrait"):SetTexture("Interface\\MerchantFrame\\UI-BuyBack-Icon")
getglobal("BenillaMerchantItem11"):Show()
getglobal("BenillaMerchantItem12"):Show()
getglobal("BenillaBuybackFrameTopLeft"):Show()
getglobal("BenillaBuybackFrameTopRight"):Show()
getglobal("BenillaBuybackFrameBotLeft"):Show()
getglobal("BenillaBuybackFrameBotRight"):Show()
getglobal("MerchantItem11"):Show()
getglobal("MerchantItem12"):Show()
getglobal("BuybackFrameTopLeft"):Show()
getglobal("BuybackFrameTopRight"):Show()
getglobal("BuybackFrameBotLeft"):Show()
getglobal("BuybackFrameBotRight"):Show()
-- The wider buyback row spacing (ref l.208-211).
getglobal("BenillaMerchantItem3"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem1"), "BOTTOMLEFT", 0, -15)
getglobal("BenillaMerchantItem5"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem3"), "BOTTOMLEFT", 0, -15)
getglobal("BenillaMerchantItem7"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem5"), "BOTTOMLEFT", 0, -15)
getglobal("BenillaMerchantItem9"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem7"), "BOTTOMLEFT", 0, -15)
getglobal("MerchantItem3"):SetPoint("TOPLEFT", getglobal("MerchantItem1"), "BOTTOMLEFT", 0, -15)
getglobal("MerchantItem5"):SetPoint("TOPLEFT", getglobal("MerchantItem3"), "BOTTOMLEFT", 0, -15)
getglobal("MerchantItem7"):SetPoint("TOPLEFT", getglobal("MerchantItem5"), "BOTTOMLEFT", 0, -15)
getglobal("MerchantItem9"):SetPoint("TOPLEFT", getglobal("MerchantItem7"), "BOTTOMLEFT", 0, -15)
local numBuyback = GetNumBuybackItems()
for i = 1, BENILLA_BUYBACK_MAX do
local button = getglobal("BenillaMerchantItem" .. i)
local button = getglobal("MerchantItem" .. i)
if i <= numBuyback then
local name, texture, price, quantity, numAvailable, isUsable = GetBuybackItemInfo(i)
-- Never soldOut here: the ref's buyback sold-out leg tests its merchant-page
@@ -485,45 +485,45 @@
end
-- Merchant-only pieces off (ref l.263-272).
getglobal("BenillaMerchantRepairText"):Hide()
getglobal("BenillaMerchantRepairAllButton"):Hide()
getglobal("BenillaMerchantRepairItemButton"):Hide()
getglobal("BenillaMerchantBuyBackItem"):Hide()
getglobal("BenillaMerchantPrevPageButton"):Hide()
getglobal("BenillaMerchantNextPageButton"):Hide()
getglobal("BenillaMerchantPageText"):Hide()
getglobal("BenillaMerchantFrameBottomLeftBorder"):Hide()
getglobal("BenillaMerchantFrameBottomRightBorder"):Hide()
getglobal("MerchantRepairText"):Hide()
getglobal("MerchantRepairAllButton"):Hide()
getglobal("MerchantRepairItemButton"):Hide()
getglobal("MerchantBuyBackItem"):Hide()
getglobal("MerchantPrevPageButton"):Hide()
getglobal("MerchantNextPageButton"):Hide()
getglobal("MerchantPageText"):Hide()
getglobal("MerchantFrameBottomLeftBorder"):Hide()
getglobal("MerchantFrameBottomRightBorder"):Hide()
end
function BenillaMerchantFrame_Update()
function MerchantFrame_Update()
-- The tab dispatch (ref MerchantFrame_Update l.57-64) + the purse (the ref's money
-- frame updates itself on PLAYER_MONEY; ours repaints with the window).
if BenillaMerchantFrame.selectedTab == 2 then
BenillaMerchantFrame_UpdateBuybackInfo()
if MerchantFrame.selectedTab == 2 then
MerchantFrame_UpdateBuybackInfo()
else
BenillaMerchantFrame_UpdateMerchantInfo()
MerchantFrame_UpdateMerchantInfo()
end
BenillaMoney_Set("BenillaMerchantMoney", GetMoney())
BenillaMoney_Set("MerchantMoney", GetMoney())
end
-- The tab pair (ref MerchantFrameTab1/2 OnClick l.682-685/704-707): select + repaint.
function BenillaMerchantFrameTab_OnClick(id)
PanelTemplates_SetTab(BenillaMerchantFrame, id)
BenillaMerchantFrame_Update()
PanelTemplates_SetTab(MerchantFrame, id)
MerchantFrame_Update()
end
-- Paging clicks (ref MerchantPrevPageButton_OnClick/Next l.275-285).
function BenillaMerchantPrevPageButton_OnClick()
function MerchantPrevPageButton_OnClick()
PlaySound("igMainMenuOptionCheckBoxOn")
BenillaMerchantFrame.page = (BenillaMerchantFrame.page or 1) - 1
BenillaMerchantFrame_Update()
MerchantFrame.page = (MerchantFrame.page or 1) - 1
MerchantFrame_Update()
end
function BenillaMerchantNextPageButton_OnClick()
function MerchantNextPageButton_OnClick()
PlaySound("igMainMenuOptionCheckBoxOn")
BenillaMerchantFrame.page = (BenillaMerchantFrame.page or 1) + 1
BenillaMerchantFrame_Update()
MerchantFrame.page = (MerchantFrame.page or 1) + 1
MerchantFrame_Update()
end
-- The single buyback slot's handlers (ref MerchantBuyBackItemItemButton l.516-541): a
@@ -558,7 +558,8 @@
end
end
function BenillaMerchantFrame_OnLoad(frame)
function MerchantFrame_OnLoad()
local frame = this
-- The NPC portrait: the "npc" unit token → the vendor's live model bake
-- (crates/benilla/src/portrait, InteractNpc). The buyback page overrides this ring with a
-- raw SetTexture (which drops the binding); the vendor page restores it (see below).
@@ -582,26 +583,27 @@
if name == nil or name == "" then
name = "Merchant"
end
getglobal("BenillaMerchantNameText"):SetText(name)
getglobal("MerchantNameText"):SetText(name)
end
function BenillaMerchantFrame_OnEvent(frame)
function MerchantFrame_OnEvent()
local frame = this
if event == "PLAYER_MONEY" then
BenillaMoney_Set("BenillaMerchantMoney", GetMoney())
BenillaMoney_Set("MerchantMoney", GetMoney())
elseif event == "MERCHANT_SHOW" then
-- Fresh session: first page, merchant tab (ref OnEvent l.29 / OnShow l.47).
frame.page = 1
frame.vendorName = arg1
PanelTemplates_SetTab(frame, 1)
BenillaMerchantFrame_SetTitle(arg1)
BenillaMerchantFrame_Update()
MerchantFrame_Update()
ShowUIPanel(frame)
elseif event == "MERCHANT_UPDATE" then
frame.vendorName = arg1
if frame.selectedTab ~= 2 then
BenillaMerchantFrame_SetTitle(arg1)
end
BenillaMerchantFrame_Update()
MerchantFrame_Update()
elseif event == "MERCHANT_CLOSED" then
HideUIPanel(frame)
end
@@ -611,7 +613,7 @@
-- vanilla's client-side close sends no packet; the app clears the merchant on this intent
-- (which fires MERCHANT_CLOSED), and we hide immediately so there's no one-frame lag.
CloseMerchant()
HideUIPanel(getglobal("BenillaMerchantFrame"))
HideUIPanel(getglobal("MerchantFrame"))
end
]]></Script>
@@ -622,7 +624,7 @@
the icon, and the dark label plate $parentNameFrame (Interface\MerchantFrame\UI-Merchant-
LabelSlots, 128×78, LEFT of the slot -9,-18 — ref l.22-33) behind the name+price. Every one of
the 10 slots always renders these; an empty slot dims the socket to 0.4 and clears its item
(BenillaMerchantFrame_Update, ref MerchantFrame.lua l.112-118). The name (GameFontNormalSmall,
(MerchantFrame_Update, ref MerchantFrame.lua l.112-118). The name (GameFontNormalSmall,
LEFT — ref l.34) sits above the price coins, the real name-over-money stack. The pushed-state
depress ring the ref repair buttons use (l.419) is a square-button texture that would smear
across our wide whole-row button, so it is OMITTED; the highlight is the square ADD glow.
@@ -786,7 +788,7 @@
</Scripts>
</Button>
<Frame name="BenillaMerchantFrame" hidden="true">
<Frame name="MerchantFrame" hidden="true">
<!-- The real 384×512 window (ref l.117-119). No <Anchors> — ShowUIPanel/SetLeftFrame SetPoints
this to the left slot on open. -->
<Size><AbsDimension x="384" y="512"/></Size>
@@ -830,20 +832,20 @@
OnEvent handler sets it here, falling back to "Merchant" until it lands. CENTER +
the (unsized, single-point) ~zero-width rect keeps the name centered on the
window regardless of length. -->
<FontString name="BenillaMerchantNameText" inherits="GameFontNormal" justifyH="CENTER" text="Merchant">
<FontString name="MerchantNameText" inherits="GameFontNormal" justifyH="CENTER" text="Merchant">
<Anchors>
<Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- "Repair Items" label beside the repair pair (ref MerchantRepairText l.199-207,
REPAIR_ITEMS); shown/hidden with the pair by Update. -->
<FontString name="BenillaMerchantRepairText" inherits="GameFontHighlightSmall" text="Repair Items" hidden="true">
<FontString name="MerchantRepairText" inherits="GameFontHighlightSmall" text="Repair Items" hidden="true">
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="26" y="103"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- "Page N" between the page buttons (ref MerchantPageText l.187-198). -->
<FontString name="BenillaMerchantPageText" inherits="GameFontNormal" justifyH="CENTER" hidden="true">
<FontString name="MerchantPageText" inherits="GameFontNormal" justifyH="CENTER" hidden="true">
<Size><AbsDimension x="104" y="0"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativePoint="BOTTOM"><Offset><AbsDimension x="-14" y="150"/></Offset></Anchor>
@@ -853,34 +855,34 @@
<!-- The buyback page's parchment (ref BuybackFrame* l.210-245): four slabs over the
merchant page's item-grid art, shown only on the buyback tab. -->
<Layer level="ARTWORK">
<Texture name="BenillaBuybackFrameTopLeft" file="Interface\MerchantFrame\UI-BuyBack-TopLeft" hidden="true">
<Texture name="BuybackFrameTopLeft" file="Interface\MerchantFrame\UI-BuyBack-TopLeft" hidden="true">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="19" y="-71"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="BenillaBuybackFrameTopRight" file="Interface\MerchantFrame\UI-BuyBack-TopRight" hidden="true">
<Texture name="BuybackFrameTopRight" file="Interface\MerchantFrame\UI-BuyBack-TopRight" hidden="true">
<Size><AbsDimension x="64" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBuybackFrameTopLeft" relativePoint="TOPRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="BuybackFrameTopLeft" relativePoint="TOPRIGHT"/></Anchors>
</Texture>
<Texture name="BenillaBuybackFrameBotLeft" file="Interface\MerchantFrame\UI-BuyBack-BotLeft" hidden="true">
<Texture name="BuybackFrameBotLeft" file="Interface\MerchantFrame\UI-BuyBack-BotLeft" hidden="true">
<Size><AbsDimension x="256" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBuybackFrameTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="BuybackFrameTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="BenillaBuybackFrameBotRight" file="Interface\MerchantFrame\UI-BuyBack-BotRight" hidden="true">
<Texture name="BuybackFrameBotRight" file="Interface\MerchantFrame\UI-BuyBack-BotRight" hidden="true">
<Size><AbsDimension x="64" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaBuybackFrameTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="BuybackFrameTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer>
<!-- The bottom band's engraved border over the repair/buyback strip (ref
MerchantFrameBottomLeftBorder/RightBorder l.248-269) — merchant tab only. -->
<Layer level="OVERLAY">
<Texture name="BenillaMerchantFrameBottomLeftBorder" file="Interface\MerchantFrame\UI-Merchant-BottomBorder">
<Texture name="MerchantFrameBottomLeftBorder" file="Interface\MerchantFrame\UI-Merchant-BottomBorder">
<Size><AbsDimension x="256" y="61"/></Size>
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="14" y="85"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1" top="0" bottom="0.4765625"/>
</Texture>
<Texture name="BenillaMerchantFrameBottomRightBorder" file="Interface\MerchantFrame\UI-Merchant-BottomBorder">
<Texture name="MerchantFrameBottomRightBorder" file="Interface\MerchantFrame\UI-Merchant-BottomBorder">
<Size><AbsDimension x="76" y="61"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaMerchantFrameBottomLeftBorder" relativePoint="RIGHT"/></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="MerchantFrameBottomLeftBorder" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.296875" top="0.4765625" bottom="0.953125"/>
</Texture>
</Layer>
@@ -892,8 +894,8 @@
starts at iconWidth — MoneyFrame.lua l.219), so the COIN edge the ref frame anchor
implies is -40-13 = -53, bottom 67. Three position-slots chained right-to-left with
the real 4px spacing, shrunk to content by the same BenillaMoney_Set fill as the row
prices (prefix "BenillaMerchantMoney") — copper-first, right-flush off Coin1. -->
<Frame name="BenillaMerchantMoneyCoin1">
prices (prefix "MerchantMoney") — copper-first, right-flush off Coin1. -->
<Frame name="MerchantMoneyCoin1">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-53" y="67"/></Offset></Anchor>
@@ -912,10 +914,10 @@
</Layer>
</Layers>
</Frame>
<Frame name="BenillaMerchantMoneyCoin2">
<Frame name="MerchantMoneyCoin2">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="BenillaMerchantMoneyCoin1" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="MerchantMoneyCoin1" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
@@ -931,10 +933,10 @@
</Layer>
</Layers>
</Frame>
<Frame name="BenillaMerchantMoneyCoin3">
<Frame name="MerchantMoneyCoin3">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="BenillaMerchantMoneyCoin2" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="MerchantMoneyCoin2" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
@@ -953,54 +955,54 @@
<!-- The real 2-column × 5-row grid (ref l.273-362): item 1 at (24, -80); the right column
anchors to its left neighbour's TOPRIGHT +12; each new row to the left item's
BOTTOMLEFT -8. Odd rows = left column, even rows = right column. -->
<Button name="BenillaMerchantItem1" inherits="BenillaMerchantItemTemplate">
<Button name="MerchantItem1" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="24" y="-80"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 1)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem2" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="MerchantItem2" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 2)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem3" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Button name="MerchantItem3" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 3)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem4" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="MerchantItem4" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 4)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem5" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Button name="MerchantItem5" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 5)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem6" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="MerchantItem6" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 6)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem7" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Button name="MerchantItem7" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 7)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem8" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem7" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="MerchantItem8" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem7" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 8)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem9" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Button name="MerchantItem9" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 9)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem10" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem9" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="MerchantItem10" inherits="BenillaMerchantItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem9" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 10)</OnLoad></Scripts>
</Button>
<!-- Rows 11/12 exist for the BUYBACK page only (ref MerchantItem11/12 l.363-380; the
merchant page hides them, the buyback page's 12 slots need them). -->
<Button name="BenillaMerchantItem11" inherits="BenillaMerchantItemTemplate" hidden="true">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
<Button name="MerchantItem11" inherits="BenillaMerchantItemTemplate" hidden="true">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 11)</OnLoad></Scripts>
</Button>
<Button name="BenillaMerchantItem12" inherits="BenillaMerchantItemTemplate" hidden="true">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem11" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Button name="MerchantItem12" inherits="BenillaMerchantItemTemplate" hidden="true">
<Anchors><Anchor point="TOPLEFT" relativeTo="MerchantItem11" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaMerchantItem_OnLoad(self, 12)</OnLoad></Scripts>
</Button>
<!-- The repair pair (ref MerchantRepairAllButton l.381-421 / MerchantRepairItemButton
@@ -1008,14 +1010,14 @@
UI-Merchant-RepairIcons sheet. Shown only when the vendor repairs. The ref's
anchor: the ALL button's BOTTOMRIGHT at frame BOTTOMLEFT +(172,91); the ITEM
button chains LEFT of it (-2). -->
<Button name="BenillaMerchantRepairAllButton" hidden="true">
<Button name="MerchantRepairAllButton" hidden="true">
<Size><AbsDimension x="36" y="36"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="172" y="91"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BORDER">
<Texture name="BenillaMerchantRepairAllIcon" file="Interface\MerchantFrame\UI-Merchant-RepairIcons">
<Texture name="MerchantRepairAllIcon" file="Interface\MerchantFrame\UI-Merchant-RepairIcons">
<TexCoords left="0.28125" right="0.5625" top="0" bottom="0.5625"/>
</Texture>
</Layer>
@@ -1036,10 +1038,10 @@
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<Button name="BenillaMerchantRepairItemButton" hidden="true">
<Button name="MerchantRepairItemButton" hidden="true">
<Size><AbsDimension x="36" y="36"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="BenillaMerchantRepairAllButton" relativePoint="LEFT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="MerchantRepairAllButton" relativePoint="LEFT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BORDER">
@@ -1064,10 +1066,10 @@
MerchantBuyBackItem l.460-559): the same socket+plate row shape at 153×37, the
shorter 64-tall label plate, anchored under row 10 (0,-53). One wide click
button, our whole-row model. -->
<Button name="BenillaMerchantBuyBackItem" hidden="true">
<Button name="MerchantBuyBackItem" hidden="true">
<Size><AbsDimension x="153" y="37"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="BenillaMerchantItem10" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-53"/></Offset></Anchor>
<Anchor point="TOPLEFT" relativeTo="MerchantItem10" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-53"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
@@ -1190,7 +1192,7 @@
<!-- Page turners (ref MerchantPrevPageButton l.569-610 / MerchantNextPageButton
l.611-656): the spellbook page-corner buttons over the page background disc, with
the Prev/Next labels beside them. Shown only when the stock overflows a page. -->
<Button name="BenillaMerchantPrevPageButton" hidden="true">
<Button name="MerchantPrevPageButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="37" y="156"/></Offset></Anchor>
@@ -1211,10 +1213,10 @@
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
<Scripts>
<OnClick>BenillaMerchantPrevPageButton_OnClick()</OnClick>
<OnClick>MerchantPrevPageButton_OnClick()</OnClick>
</Scripts>
</Button>
<Button name="BenillaMerchantNextPageButton" hidden="true">
<Button name="MerchantNextPageButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="324" y="156"/></Offset></Anchor>
@@ -1235,7 +1237,7 @@
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
<Scripts>
<OnClick>BenillaMerchantNextPageButton_OnClick()</OnClick>
<OnClick>MerchantNextPageButton_OnClick()</OnClick>
</Scripts>
</Button>
<!-- The Merchant / Buyback tab pair at the window's bottom edge (ref MerchantFrameTab1/2
@@ -1259,7 +1261,7 @@
</Button>
<!-- Close: the real UIPanelCloseButton position (TOPRIGHT -30, -8; ref l.657-664), on the
minimize-button art UIPanelCloseButton itself uses (pin § "The re-skin pass"). -->
<Button name="BenillaMerchantCloseButton">
<Button name="MerchantCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-30" y="-8"/></Offset></Anchor>
@@ -1273,19 +1275,19 @@
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaMerchantFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaMerchantFrame_OnEvent(self)</OnEvent>
<OnLoad>MerchantFrame_OnLoad()</OnLoad>
<OnEvent>MerchantFrame_OnEvent()</OnEvent>
<!-- While a buy-tab row is hovered, re-arm its cursor every frame (ref MerchantFrame
OnUpdate l.725-731): the Inspect magnifier while Ctrl is held, else the coin —
Buy if affordable, the grayed UnableBuy if not (ShowMerchantSellCursor's own gate).
Polling (not a one-shot on enter) is what makes the Ctrl swap and a live money
change track under a stationary cursor. The buyback slot arms in its OnEnter. -->
<OnUpdate>
if BenillaMerchantFrame.itemHover then
if MerchantFrame.itemHover then
if IsControlKeyDown() then
ShowInspectCursor()
else
ShowMerchantSellCursor(BenillaMerchantFrame.itemHover)
ShowMerchantSellCursor(MerchantFrame.itemHover)
end
end
</OnUpdate>
@@ -1307,7 +1309,7 @@
<OnShow>OpenBackpack(); PlaySound("igCharacterInfoOpen")</OnShow>
<!-- HideRepairCursor: closing the window must drop the repair-mode latch (the ref's
OnHide ResetCursor, l.54 — its cursor semantics land with the sell-cursor slice). -->
<OnHide>CloseMerchant(); CloseBackpack(); HideRepairCursor(); ResetCursor(); BenillaMerchantFrame.itemHover = nil; PlaySound("igCharacterInfoClose")</OnHide>
<OnHide>CloseMerchant(); CloseBackpack(); HideRepairCursor(); ResetCursor(); MerchantFrame.itemHover = nil; PlaySound("igCharacterInfoClose")</OnHide>
</Scripts>
</Frame>
</Ui>
+6 -6
View File
@@ -23,7 +23,7 @@
the action bar behind it, and an unguarded call would abort the rest of that OnShow. Same
posture as UiPanels.xml's `CloseMenus and CloseMenus()` and the `if GameTooltip` guards.
Structure: the buttons are TOP-LEVEL frames anchored by name into BenillaActionBarArtFrame
Structure: the buttons are TOP-LEVEL frames anchored by name into MainMenuBarArtFrame
(this engine has no cross-file `parent=` attachment — SkillFrame.xml's header comment), then
seated one frame level above the art by BenillaActionBarArt_SeatAbove (ActionBar.xml) so the
bar plate doesn't paint over them. Same shape the bag bar next door already uses.
@@ -153,10 +153,10 @@
-- reference lists first. Its keyring arm has no frame here; the still-unwired Help button
-- simply never pushes.
function UpdateMicroButtons()
BenillaMicroButton_SetPushed(CharacterMicroButton, BenillaMicro_PanelShown("BenillaCharacterFrame"))
BenillaMicroButton_SetPushed(SpellbookMicroButton, BenillaMicro_PanelShown("BenillaSpellBookFrame"))
BenillaMicroButton_SetPushed(CharacterMicroButton, BenillaMicro_PanelShown("CharacterFrame"))
BenillaMicroButton_SetPushed(SpellbookMicroButton, BenillaMicro_PanelShown("SpellBookFrame"))
BenillaMicroButton_SetPushed(TalentMicroButton, BenillaMicro_PanelShown("BenillaTalentFrame"))
BenillaMicroButton_SetPushed(QuestLogMicroButton, BenillaMicro_PanelShown("BenillaQuestLogFrame"))
BenillaMicroButton_SetPushed(QuestLogMicroButton, BenillaMicro_PanelShown("QuestLogFrame"))
BenillaMicroButton_SetPushed(WorldMapMicroButton, BenillaMicro_PanelShown("WorldMapFrame"))
BenillaMicroButton_SetPushed(MainMenuMicroButton, BenillaMicro_PanelShown("GameMenuFrame"))
end
@@ -249,7 +249,7 @@
button's TOP -28. -->
<Button name="CharacterMicroButton" inherits="BenillaMicroButtonTemplate">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="BenillaActionBarArtFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="552" y="2"/></Offset></Anchor>
<Anchor point="BOTTOMLEFT" relativeTo="MainMenuBarArtFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="552" y="2"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
@@ -262,7 +262,7 @@
</Layers>
<Scripts>
<OnLoad>CharacterMicroButton_OnLoad(self)</OnLoad>
<OnClick>ToggleCharacter("BenillaPaperDollFrame")</OnClick>
<OnClick>ToggleCharacter("PaperDollFrame")</OnClick>
<OnUpdate>CharacterMicroButton_OnUpdate(self)</OnUpdate>
</Scripts>
</Button>
+601
View File
@@ -0,0 +1,601 @@
<Ui>
<!--
The reference's MONEY FRAME kit — `MoneyFrameTemplate` / `SmallMoneyFrameTemplate` plus the
`MoneyTypeInfo` table and the `MoneyFrame_*` API that drives them. Transcribed from the
extracted 1.12 `Interface\FrameXML\MoneyFrame.lua` (356 lines) and `MoneyFrame.xml` (297),
line numbers cited below as `l.N`.
WHY IT IS HERE: it is FrameXML's, not Rust's (decision 1190 — a name the shipped 1.12 UI
defines is transcribed, never hardcoded), and the corpus asks for it by name.
`MoneyFrame_SetType` is wanted by 11 addons, `MoneyFrame_Update` by 9, `SmallMoneyFrame_OnLoad`
by 9, and 13 declare a frame `inherits="SmallMoneyFrameTemplate"`. `MoneyTypeInfo` was one of
the top runtime walls the frame rename left standing.
── THE OTHER COIN KIT, and why there are two ────────────────────────────────────────────────
benilla's own eight windows (merchant, bank, trade, mail, quest, quest log, taxi, trainer) do
NOT use this file. They render coins through `BenillaMoney_Set`/`_Clear`/`_SetColor` in
MerchantFrame.xml: a three-SLOT chain (`<prefix>Coin1..3`) that fills only the non-zero
denominations in display order and re-flows, rather than three fixed GoldButton/SilverButton/
CopperButton frames that hide themselves. That kit was quoted FROM this one (its header cites
these same line numbers for the coin TexCoords, the collapse and the shrink) and its layout is
the one the director has signed off across those eight windows.
So the two coexist deliberately, and the split is by AUDIENCE, not by accident:
· `BenillaMoney_*` + `<prefix>Coin1..3` — what our own transcribed windows are laid out on.
· `MoneyFrame_*` + `MoneyTypeInfo` here — the reference's names and structure, which is
what an addon writes against and what an addon's `inherits="SmallMoneyFrameTemplate"`
resolves to.
The reconciliation — migrating those eight windows onto `SmallMoneyFrameTemplate` +
`MoneyFrame_Update` and retiring `BenillaMoney_*` — is what the reference itself does and is
the right end state; it is a re-layout of hand-authored art in eight files (an approved look),
so it is named here rather than done in passing.
── WHAT IS NOT CARRIED, and why (each is guarded at its own site below) ─────────────────────
· THE COIN PICKUP. `OpenCoinPickupFrame`/`UpdateCoinPickupFrame`/`CoinPickupFrame` live in the
reference's CoinPickupFrame.lua/.xml, which we do not ship — and the engine verbs underneath
it (`PickupPlayerMoney`, `DropCursorMoney`, `PickupTradeMoney`, `AddTradeMoney`) are not
built either, so a picked-up coin would have nowhere to go. The templates' `<OnClick>` is
the reference's, guarded on the function existing; a `canPickup` money frame therefore
enables its mouse (the reference's own state) and clicking it does nothing.
· `GetCursorMoney` (PLAYER's UpdateFunc, l.17) — the engine verb for coins held on the cursor.
With no coin pickup there is no cursor money, so its term is not approximated away: it is
structurally zero. Stated at the spot.
· `GetSendMailMoney` / `GetSendMailCOD` (l.73, l.90) — 1.12 engine verbs we have not built. We
ship their SETTERS (`SetSendMailMoney`/`SetSendMailCOD`, which MailFrame.xml drives); nothing
reads the amount back yet. Guarded to 0 rather than left as a nil call.
· `message()` (l.145, l.173, l.181) — the reference's one-line StaticPopup shim, which lives in
BasicControls.xml, a file family we have not transcribed. Guarded, so a misuse of the API
stays silent instead of erroring on the diagnostic.
· `Button:GetTextWidth()` (l.202 etc.) — our Button has no such method yet. The button's own
`<ButtonText name="$parentText">` FontString carries `GetStringWidth`, which is the same
measurement, and that is what is called.
· The four `Pickup*`/`Add*` mail verbs the reference's own MoneyTypeInfo closures name are not
in 1.12's `_G` either (they are absent from reference/1.12-globals.tsv) — so those bodies are
transcribed verbatim and are as unreachable here as they are there.
-->
<Script><![CDATA[
-- l.2-12, verbatim.
MONEY_ICON_WIDTH = 19
MONEY_ICON_WIDTH_SMALL = 13
MONEY_BUTTON_SPACING = -4
MONEY_BUTTON_SPACING_SMALL = -4
COPPER_PER_SILVER = 100
SILVER_PER_GOLD = 100
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD
COIN_BUTTON_WIDTH = 32
-- l.14-103: the money TYPES. A money frame carries no amount of its own — it carries a type,
-- and the type says where to read the number from (`UpdateFunc`, called with `this` set to the
-- frame), whether zero denominations collapse away, whether the smaller coins show anyway, and
-- whether the coins are click-to-pick-up. Addons read this table directly, so its shape is the
-- contract, not just its behaviour.
MoneyTypeInfo = { }
MoneyTypeInfo["PLAYER"] = {
UpdateFunc = function()
-- Reference l.17: `GetMoney() - GetCursorMoney() - GetPlayerTradeMoney()`. The
-- cursor term is dropped, not approximated: `GetCursorMoney` is the amount lifted onto
-- the cursor by the coin-pickup frame, and benilla ships neither that frame nor the
-- `PickupPlayerMoney` verb behind it, so it is structurally 0 here. The term returns
-- the moment either lands.
return GetMoney() - GetPlayerTradeMoney()
end,
PickupFunc = function(amount)
PickupPlayerMoney(amount)
end,
DropFunc = function()
DropCursorMoney()
end,
collapse = 1,
canPickup = 1,
showSmallerCoins = "Backpack"
}
MoneyTypeInfo["STATIC"] = {
UpdateFunc = function()
return this.staticMoney
end,
collapse = 1,
}
MoneyTypeInfo["AUCTION"] = {
UpdateFunc = function()
return this.staticMoney
end,
showSmallerCoins = "Backpack",
fixedWidth = 1,
collapse = 1,
truncateSmallCoins = nil,
}
MoneyTypeInfo["PLAYER_TRADE"] = {
UpdateFunc = function()
return GetPlayerTradeMoney()
end,
PickupFunc = function(amount)
PickupTradeMoney(amount)
end,
DropFunc = function()
AddTradeMoney()
end,
collapse = 1,
canPickup = 1,
}
MoneyTypeInfo["TARGET_TRADE"] = {
UpdateFunc = function()
return GetTargetTradeMoney()
end,
collapse = 1,
}
MoneyTypeInfo["SEND_MAIL"] = {
UpdateFunc = function()
-- `GetSendMailMoney` (ref l.73) is an engine verb we have not built — see the header.
if not GetSendMailMoney then return 0 end
return GetSendMailMoney()
end,
PickupFunc = function(amount)
PickupSendMailMoney(amount)
end,
DropFunc = function()
AddSendMailMoney()
end,
collapse = nil,
canPickup = 1,
showSmallerCoins = "Backpack",
}
MoneyTypeInfo["SEND_MAIL_COD"] = {
UpdateFunc = function()
-- `GetSendMailCOD` (ref l.90) — same gap as SEND_MAIL above.
if not GetSendMailCOD then return 0 end
return GetSendMailCOD()
end,
PickupFunc = function(amount)
PickupSendMailCOD(amount)
end,
DropFunc = function()
AddSendMailCOD()
end,
collapse = 1,
canPickup = 1,
}
-- l.105-122: the two OnLoads. Identical but for `this.small`, which is what picks the 13px
-- coin + tight spacing over the 19px one in MoneyFrame_Update below. Both then take type
-- PLAYER, so a frame that is never told otherwise shows the purse.
function MoneyFrame_OnLoad()
this:RegisterEvent("PLAYER_MONEY")
this:RegisterEvent("PLAYER_TRADE_MONEY")
this:RegisterEvent("TRADE_MONEY_CHANGED")
this:RegisterEvent("SEND_MAIL_MONEY_CHANGED")
this:RegisterEvent("SEND_MAIL_COD_CHANGED")
MoneyFrame_SetType("PLAYER")
end
function SmallMoneyFrame_OnLoad()
this:RegisterEvent("PLAYER_MONEY")
this:RegisterEvent("PLAYER_TRADE_MONEY")
this:RegisterEvent("TRADE_MONEY_CHANGED")
this:RegisterEvent("SEND_MAIL_MONEY_CHANGED")
this:RegisterEvent("SEND_MAIL_COD_CHANGED")
this.small = 1
MoneyFrame_SetType("PLAYER")
end
-- l.124-140, verbatim: every money frame hears every money event, and each decides for itself
-- whether the one that arrived is about the number IT is showing.
function MoneyFrame_OnEvent()
if ( not this.info or not this:IsVisible() ) then
return
end
if ( event == "PLAYER_MONEY" and this.moneyType == "PLAYER" ) then
MoneyFrame_UpdateMoney()
elseif ( event == "PLAYER_TRADE_MONEY" and (this.moneyType == "PLAYER" or this.moneyType == "PLAYER_TRADE") ) then
MoneyFrame_UpdateMoney()
elseif ( event == "TRADE_MONEY_CHANGED" and this.moneyType == "TARGET_TRADE" ) then
MoneyFrame_UpdateMoney()
elseif ( event == "SEND_MAIL_MONEY_CHANGED" and (this.moneyType == "PLAYER" or this.moneyType == "SEND_MAIL") ) then
MoneyFrame_UpdateMoney()
elseif ( event == "SEND_MAIL_COD_CHANGED" and (this.moneyType == "PLAYER" or this.moneyType == "SEND_MAIL_COD") ) then
MoneyFrame_UpdateMoney()
end
end
-- l.142-162: point a frame at a money type. Operates on `this`, not on a passed frame — the
-- reference's convention here, and what the eleven corpus callers write.
function MoneyFrame_SetType(type)
local info = MoneyTypeInfo[type]
if ( not info ) then
-- ref l.145 `message("Invalid money type: "..type)` — `message` is BasicControls.xml's
-- StaticPopup shim, not transcribed (see the header).
if ( message ) then message("Invalid money type: "..type) end
return
end
this.info = info
this.moneyType = type
local frameName = this:GetName()
if ( info.canPickup ) then
getglobal(frameName.."GoldButton"):EnableMouse(true)
getglobal(frameName.."SilverButton"):EnableMouse(true)
getglobal(frameName.."CopperButton"):EnableMouse(true)
else
getglobal(frameName.."GoldButton"):EnableMouse(false)
getglobal(frameName.."SilverButton"):EnableMouse(false)
getglobal(frameName.."CopperButton"):EnableMouse(false)
end
MoneyFrame_UpdateMoney()
end
-- l.164-175: re-read the number from the type and repaint. `this` again.
function MoneyFrame_UpdateMoney()
if ( this.info ) then
local money = this.info.UpdateFunc()
MoneyFrame_Update(this:GetName(), money)
-- ref l.169-171 also drives the coin-pickup frame while one is open. `hasPickup` is
-- only ever set by the templates' OnClick, which is guarded off (see the header), so
-- this branch is unreachable today rather than removed.
if ( this.hasPickup == 1 and UpdateCoinPickupFrame ) then
UpdateCoinPickupFrame(money)
end
else
if ( message ) then message("Error moneyType not set") end
end
end
-- The button's laid-out text width. The reference calls `Button:GetTextWidth()` (l.202 and
-- kin); our Button has no such method, but the `<ButtonText name="$parentText">` FontString
-- the templates below name does carry `GetStringWidth`, which measures the same string with
-- the same font object. Falls back to 0 when the label has not resolved (headless, no atlas),
-- which leaves the coin at exactly its icon width — the same shape, without the digits.
local function moneyButtonTextWidth(buttonName)
local text = getglobal(buttonName.."Text")
if ( not text or not text.GetStringWidth ) then return 0 end
return text:GetStringWidth() or 0
end
-- l.177-270: THE PAINTER. Splits a copper amount into the three denominations, sizes each coin
-- to its own digits + icon, and then — if the type collapses — hides the leading zeros and
-- re-chains what is left so the group packs tight and the frame reports its true width.
--
-- Note that this one takes a frame NAME, not `this`: it is the only entry point in the family
-- an owner can call about a frame that is not the one currently dispatching (`MoneyFrame_Update
-- (frameName, money)` is how an addon paints a STATIC frame it just filled).
function MoneyFrame_Update(frameName, money)
local frame = getglobal(frameName)
local info = frame.info
if ( not info ) then
if ( message ) then message("Error moneyType not set") end
end
-- Breakdown the money into denominations
local gold = floor(money / (COPPER_PER_SILVER * SILVER_PER_GOLD))
local silver = floor((money - (gold * COPPER_PER_SILVER * SILVER_PER_GOLD)) / COPPER_PER_SILVER)
local copper = mod(money, COPPER_PER_SILVER)
local goldButton = getglobal(frameName.."GoldButton")
local silverButton = getglobal(frameName.."SilverButton")
local copperButton = getglobal(frameName.."CopperButton")
local iconWidth = MONEY_ICON_WIDTH
local spacing = MONEY_BUTTON_SPACING
if ( frame.small ) then
iconWidth = MONEY_ICON_WIDTH_SMALL
spacing = MONEY_BUTTON_SPACING_SMALL
end
-- Set values for each denomination
goldButton:SetText(gold)
goldButton:SetWidth(moneyButtonTextWidth(frameName.."GoldButton") + iconWidth)
goldButton:Show()
silverButton:SetText(silver)
silverButton:SetWidth(moneyButtonTextWidth(frameName.."SilverButton") + iconWidth)
silverButton:Show()
copperButton:SetText(copper)
copperButton:SetWidth(moneyButtonTextWidth(frameName.."CopperButton") + iconWidth)
copperButton:Show()
-- Store how much money the frame is displaying
frame.staticMoney = money
-- If not collapsable don't need to continue
if ( not info.collapse ) then
return
end
local width = iconWidth
local showLowerDenominations, truncateCopper
if ( gold > 0 ) then
width = width + goldButton:GetWidth()
if ( info.showSmallerCoins ) then
showLowerDenominations = 1
end
if ( info.truncateSmallCoins ) then
truncateCopper = 1
end
else
goldButton:Hide()
end
if ( silver > 0 or showLowerDenominations ) then
-- Exception if showLowerDenominations and fixedWidth
if ( showLowerDenominations and info.fixedWidth ) then
silverButton:SetWidth(COIN_BUTTON_WIDTH)
end
width = width + silverButton:GetWidth()
goldButton:SetPoint("RIGHT", frameName.."SilverButton", "LEFT", spacing, 0)
if ( goldButton:IsVisible() ) then
width = width - spacing
end
if ( info.showSmallerCoins ) then
showLowerDenominations = 1
end
else
silverButton:Hide()
goldButton:SetPoint("RIGHT", frameName.."SilverButton", "RIGHT", 0, 0)
end
-- Used if we're not showing lower denominations
if ( (copper > 0 or showLowerDenominations or info.showSmallerCoins == "Backpack") and not truncateCopper) then
-- Exception if showLowerDenominations and fixedWidth
if ( showLowerDenominations and info.fixedWidth ) then
copperButton:SetWidth(COIN_BUTTON_WIDTH)
end
width = width + copperButton:GetWidth()
silverButton:SetPoint("RIGHT", frameName.."CopperButton", "LEFT", spacing, 0)
if ( silverButton:IsVisible() ) then
width = width - spacing
end
else
copperButton:Hide()
silverButton:SetPoint("RIGHT", frameName.."CopperButton", "RIGHT", 0, 0)
end
frame:SetWidth(width)
end
-- l.272-346: a NO-OP in 1.12 — the reference's whole body is inside a `--[[ ]]` block, an
-- earlier painter that MoneyFrame_Update replaced. Kept because the name is in the shipped
-- client's `_G` and an addon that calls it gets exactly this there: nothing.
function RefreshMoneyFrame(frameName, money, small, collapse, showSmallerCoins)
end
-- l.348-356, verbatim: recolour the DIGITS (the coin icons keep their own texture) — how the
-- reference reddens a price you cannot afford.
function SetMoneyFrameColor(frameName, r, g, b)
local goldButton = getglobal(frameName.."GoldButton")
local silverButton = getglobal(frameName.."SilverButton")
local copperButton = getglobal(frameName.."CopperButton")
goldButton:SetTextColor(r, g, b)
silverButton:SetTextColor(r, g, b)
copperButton:SetTextColor(r, g, b)
end
]]></Script>
<!--
MoneyFrameTemplate (ref MoneyFrame.xml l.3-149) — the LARGE kit: 192x28, 19px coins, the
NumberFontNormalLarge digits. The three buttons chain RIGHT-to-LEFT from the copper end, which
is what lets MoneyFrame_Update above re-point them past a hidden denomination.
The `<OnClick>` bodies are the reference's, guarded on `OpenCoinPickupFrame` — the coin-pickup
frame is not transcribed (see the header). `<PushedTextOffset>` is carried for provenance; our
loader does not model it and ignores it.
-->
<Frame name="MoneyFrameTemplate" virtual="true">
<Size><AbsDimension x="192" y="28"/></Size>
<Frames>
<Button name="$parentCopperButton">
<Size><AbsDimension x="47" y="19"/></Size>
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
if ( OpenCoinPickupFrame ) then
OpenCoinPickupFrame(1, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
parent.hasPickup = 1
end
</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="19" y="19"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-19" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormalLarge" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentSilverButton">
<Size><AbsDimension x="47" y="19"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentCopperButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
if ( OpenCoinPickupFrame ) then
OpenCoinPickupFrame(COPPER_PER_SILVER, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
parent.hasPickup = 1
end
</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="19" y="19"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-19" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormalLarge" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentGoldButton">
<Size><AbsDimension x="47" y="19"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentSilverButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
if ( OpenCoinPickupFrame ) then
OpenCoinPickupFrame(COPPER_PER_GOLD, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
parent.hasPickup = 1
end
</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="19" y="19"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-19" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormalLarge" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
</Frames>
<Scripts>
<OnLoad>MoneyFrame_OnLoad()</OnLoad>
<OnEvent>MoneyFrame_OnEvent()</OnEvent>
<OnShow>MoneyFrame_UpdateMoney()</OnShow>
<OnHide>
if ( this.hasPickup == 1 and CoinPickupFrame ) then
CoinPickupFrame:Hide()
this.hasPickup = 0
end
</OnHide>
</Scripts>
</Frame>
<!--
SmallMoneyFrameTemplate (ref MoneyFrame.xml l.150-296) — the same kit at 128x13 with 13px
coins and NumberFontNormal digits, and `SmallMoneyFrame_OnLoad` in place of `MoneyFrame_OnLoad`
so `this.small` is set. This is the one the shipped UI uses nearly everywhere (merchant prices,
the purse, mail postage, trade offers) and the one 13 corpus addons inherit.
-->
<Frame name="SmallMoneyFrameTemplate" virtual="true">
<Size><AbsDimension x="128" y="13"/></Size>
<Frames>
<Button name="$parentCopperButton">
<Size><AbsDimension x="32" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
if ( OpenCoinPickupFrame ) then
OpenCoinPickupFrame(1, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
parent.hasPickup = 1
end
</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormal" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentSilverButton">
<Size><AbsDimension x="32" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentCopperButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
if ( OpenCoinPickupFrame ) then
OpenCoinPickupFrame(COPPER_PER_SILVER, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
parent.hasPickup = 1
end
</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormal" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentGoldButton">
<Size><AbsDimension x="32" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentSilverButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
if ( OpenCoinPickupFrame ) then
OpenCoinPickupFrame(COPPER_PER_GOLD, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
parent.hasPickup = 1
end
</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormal" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
</Frames>
<Scripts>
<OnLoad>SmallMoneyFrame_OnLoad()</OnLoad>
<OnEvent>MoneyFrame_OnEvent()</OnEvent>
<OnShow>MoneyFrame_UpdateMoney()</OnShow>
<OnHide>
if ( this.hasPickup == 1 and CoinPickupFrame ) then
CoinPickupFrame:Hide()
this.hasPickup = 0
end
</OnHide>
</Scripts>
</Frame>
</Ui>
+68 -62
View File
@@ -15,7 +15,7 @@
as per-button data.
- Buttons reuse BenillaActionButtonTemplate + the whole ActionBar.xml handler set: same
click/drag/keyless-use, same feedback layer (state/usable/cooldown/flash), fanned from
BenillaActionBar's bar-level events via the BENILLA_ACTION_BUTTONS roster each button
MainMenuBar's bar-level events via the BENILLA_ACTION_BUTTONS roster each button
joins at OnLoad.
Ref divergences (deliberate, mirroring ActionBar.xml's SCOPE notes):
@@ -36,24 +36,30 @@
BENILLA_MULTIBAR_BOTTOMLEFT_BASE = 60
BENILLA_MULTIBAR_BOTTOMRIGHT_BASE = 48
-- The MULTIACTIONBAR keybind pair — ref MultiActionBars.lua:24-45 MultiActionButtonDown/
-- Up, transcribed onto our frame names (`bar` is "BottomLeft"/"BottomRight", the ref
-- passes "MultiBarBottomLeft"); the same NORMAL→PUSHED debounce dance as the main bar's
-- BenillaActionButtonDown/Up (ActionBar.xml — the ref's MacroFrame_SaveMacro hook and
-- onSelf argument are not modeled there either). Driven by the binding dispatch
-- (crate::bindings, Kind::EdgeUpDown — decision 1008).
function BenillaMultiActionButtonDown(bar, id)
local button = getglobal("BenillaMultiBar" .. bar .. "Button" .. id)
-- The MULTIACTIONBAR keybind pair — ref MultiActionBars.lua:24-45 MultiActionButtonDown/Up,
-- the same NORMAL→PUSHED debounce dance as the main bar's ActionButtonDown/Up
-- (ActionBar.xml — the ref's MacroFrame_SaveMacro hook is not modeled there either).
-- Driven by the binding dispatch (crate::bindings, Kind::EdgeUpDown — decision 1008).
--
-- `bar` IS THE WHOLE FRAME-NAME PREFIX, not a suffix (decision 1199). The reference resolves
-- `getglobal(bar.."Button"..id)` and its own Bindings.xml passes "MultiBarBottomLeft"; ours
-- used to pass "BottomLeft" and splice "MultiBar" on in here. Same arity, different MEANING
-- — the silent kind of wrong shape: zBar calls `MultiActionButtonDown("zBar"..barid, id)`
-- and under the old splice built the nonexistent frame name "MultiBarzBar1Button1". The
-- caller now names the bar in full, exactly as the reference's binding does, so a third-
-- party bar can address these at all. `onSelf` rides to UseAction like ActionButtonUp's.
function MultiActionButtonDown(bar, id)
local button = getglobal(bar .. "Button" .. id)
if ( button:GetButtonState() == "NORMAL" ) then
button:SetButtonState("PUSHED")
end
end
function BenillaMultiActionButtonUp(bar, id)
local button = getglobal("BenillaMultiBar" .. bar .. "Button" .. id)
function MultiActionButtonUp(bar, id, onSelf)
local button = getglobal(bar .. "Button" .. id)
if ( button:GetButtonState() == "PUSHED" ) then
button:SetButtonState("NORMAL")
UseAction(BenillaActionButton_Action(button))
UseAction(BenillaActionButton_Action(button), 0, onSelf)
-- Ref MultiActionButtonUp:39-43: the checked ring reflects the use immediately.
BenillaActionButton_UpdateState(button)
end
@@ -65,116 +71,116 @@
both sides by design — and both sides carry deep nested button trees, so co-tenanting one
stratum is exactly the level-outranks-insertion collision decision 0597 fixed for the party
frame. HIGH is what keeps the bars above an open bag. -->
<Frame name="BenillaMultiBarBottomLeft" frameStrata="HIGH">
<Frame name="MultiBarBottomLeft" frameStrata="HIGH">
<Size><AbsDimension x="500" y="38"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="BenillaActionButton1" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="17"/></Offset></Anchor>
<Anchor point="BOTTOMLEFT" relativeTo="ActionButton1" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="17"/></Offset></Anchor>
</Anchors>
<Frames>
<CheckButton name="BenillaMultiBarBottomLeftButton1" inherits="BenillaActionButtonTemplate">
<CheckButton name="MultiBarBottomLeftButton1" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton2" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton2" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton3" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton3" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton4" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton4" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton5" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton5" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton6" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton6" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton7" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton7" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton8" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton8" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton9" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton9" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton10" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton10" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton11" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton11" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomLeftButton12" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeftButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomLeftButton12" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
</CheckButton>
</Frames>
</Frame>
<!-- HIGH, same as BottomLeft above (ref `MultiActionBars.xml` l.151). -->
<Frame name="BenillaMultiBarBottomRight" frameStrata="HIGH">
<Frame name="MultiBarBottomRight" frameStrata="HIGH">
<Size><AbsDimension x="500" y="38"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaMultiBarBottomLeft" relativePoint="RIGHT"><Offset><AbsDimension x="10" y="0"/></Offset></Anchor>
<Anchor point="LEFT" relativeTo="MultiBarBottomLeft" relativePoint="RIGHT"><Offset><AbsDimension x="10" y="0"/></Offset></Anchor>
</Anchors>
<Frames>
<CheckButton name="BenillaMultiBarBottomRightButton1" inherits="BenillaActionButtonTemplate">
<CheckButton name="MultiBarBottomRightButton1" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton2" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton2" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton3" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton3" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton4" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton4" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton5" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton5" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton6" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton6" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton7" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton7" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton8" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton8" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton9" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton9" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton10" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton10" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton11" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton11" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaMultiBarBottomRightButton12" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMultiBarBottomRightButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="MultiBarBottomRightButton12" inherits="BenillaActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
</CheckButton>
</Frames>
@@ -326,7 +326,7 @@
function OptionsSearch_Words(text)
local up = string.upper(text)
local words = { up }
for w in string.gmatch(up, "([^, ]+)") do
for w in string.gfind(up, "([^, ]+)") do
table.insert(words, w)
end
return words
@@ -2373,7 +2373,7 @@
<Scripts><OnLoad>
OptionsGlobalRow_OnLoad(self, "SHOW_BUFF_DURATIONS",
"Buff Durations", "OPTION_TOOLTIP_SHOW_BUFF_DURATION")
self.applyFunc = BenillaBuffButtons_UpdatePositions
self.applyFunc = BuffButtons_UpdatePositions
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowAutoQuestWatch" inherits="OptionsCheckboxRowTemplate">
@@ -2685,14 +2685,14 @@
seating). The wheel handler is NOT here: a spin over a row
bubbles up the PARENT chain (pointer.rs), and this frame is
the rows' sibling — it lives on the body below. -->
<Frame name="$parentScrollFrame" inherits="BenillaFauxScrollFrameTemplate">
<Frame name="$parentScrollFrame" inherits="FauxScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-47"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-32" y="28"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>
BenillaFauxScrollFrame_OnLoad(this)
ScrollFrame_OnLoad()
this.updateFunc = KeyBindingsPage_Update
</OnLoad>
</Scripts>
@@ -0,0 +1,127 @@
<Ui>
<!--
The reference's shared OPTIONS widget kit — Interface\FrameXML\OptionsFrameTemplates.xml,
transcribed from the extracted 1.12.1 file (line numbers below are that file's), plus the
one template from UIOptionsFrame.xml that addons actually name.
These are UIPanelTemplates.xml's descendants: `OptionsCheckButtonTemplate` inherits
`UICheckButtonTemplate` and `OptionsButtonTemplate` inherits `UIPanelButtonTemplate`, so
this file must load after it (the .toc seats them together).
WHY THE UIOptionsFrame ONE LIVES HERE. `UIOptionsCheckButtonTemplate` is 8 `CreateFrame`
call sites in the corpus — the fourth most-wanted template we lacked — and in the reference
it is declared in UIOptionsFrame.xml (l.6-10) as a two-line size override on
`OptionsCheckButtonTemplate`. We do not transcribe UIOptionsFrame (benilla's own options
window is OptionsFrame.xml, a different design), so the one template that file contributes
to the shared namespace sits with its parent rather than in a file of its own. Its
provenance is on the declaration.
Every name here is in `reference/1.12-globals.tsv`; none is invented. What this supersedes
but does not delete: nothing — OptionsFrame.xml's own `Options*Template` family
(`OptionsRedButtonTemplate`, `OptionsCheckboxRowTemplate`, `OptionsSliderRowTemplate`, …)
are benilla's row widgets for benilla's window, distinct names and a distinct shape.
-->
<!-- ref l.3-34. The titled group box: a tooltip-border backdrop with its caption hung above the
top-left corner, greyed by its own OnLoad. -->
<Frame name="OptionFrameBoxTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentTitle" inherits="GameFontHighlight">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="9" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize><AbsValue val="16"/></EdgeSize>
<TileSize><AbsValue val="16"/></TileSize>
<BackgroundInsets><AbsInset left="5" right="5" top="5" bottom="5"/></BackgroundInsets>
</Backdrop>
<Scripts>
<OnLoad>
this:SetBackdropBorderColor(0.4, 0.4, 0.4)
this:SetBackdropColor(0.5, 0.5, 0.5)
</OnLoad>
</Scripts>
</Frame>
<!-- ref l.35-39. The panel button at the options window's own 90x21. -->
<Button name="OptionsButtonTemplate" inherits="UIPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="90" y="21"/></Size>
</Button>
<!-- ref l.40-66. The checkbox with the options window's manners: a 100px hit rect to the right
so the LABEL is clickable too, the on/off click sounds, and the tooltipText /
tooltipRequirement hover pair an instance sets as plain fields. -->
<CheckButton name="OptionsCheckButtonTemplate" inherits="UICheckButtonTemplate" virtual="true">
<HitRectInsets><AbsInset left="0" right="-100" top="0" bottom="0"/></HitRectInsets>
<Scripts>
<OnClick>
if (this:GetChecked()) then
PlaySound("igMainMenuOptionCheckBoxOn")
else
PlaySound("igMainMenuOptionCheckBoxOff")
end
</OnClick>
<OnEnter>
if (this.tooltipText) then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(this.tooltipText, nil, nil, nil, nil, 1)
end
if (this.tooltipRequirement) then
GameTooltip:AddLine(this.tooltipRequirement, "", 1.0, 1.0, 1.0)
GameTooltip:Show()
end
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</CheckButton>
<!-- ref UIOptionsFrame.xml l.6-10 — see the header. A 26x26 override of the above, and the
template the corpus's nameplate addons build their whole options panel out of. -->
<CheckButton name="UIOptionsCheckButtonTemplate" inherits="OptionsCheckButtonTemplate" virtual="true">
<Size><AbsDimension x="26" y="26"/></Size>
</CheckButton>
<!-- ref l.67-132. The horizontal options slider: a SliderBar backdrop with three labels
(caption above, LOW/HIGH under the ends) and the horizontal knob. `text="LOW"`/`"HIGH"` are
global-string keys, resolved by the loader's `resolve_text` against GlobalStrings. -->
<Slider name="OptionsSliderTemplate" orientation="HORIZONTAL" virtual="true" enableMouse="true">
<Size><AbsDimension x="128" y="17"/></Size>
<HitRectInsets><AbsInset left="0" right="0" top="-10" bottom="-10"/></HitRectInsets>
<Backdrop bgFile="Interface\Buttons\UI-SliderBar-Background" edgeFile="Interface\Buttons\UI-SliderBar-Border" tile="true">
<EdgeSize><AbsValue val="8"/></EdgeSize>
<TileSize><AbsValue val="8"/></TileSize>
<BackgroundInsets><AbsInset left="3" right="3" top="6" bottom="6"/></BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="BOTTOM" relativePoint="TOP"/></Anchors>
</FontString>
<FontString name="$parentLow" inherits="GameFontHighlightSmall" text="LOW">
<Anchors><Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="2" y="3"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="$parentHigh" inherits="GameFontHighlightSmall" text="HIGH">
<Anchors><Anchor point="TOPRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnter>
if (this.tooltipText) then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(this.tooltipText, nil, nil, nil, nil, 1)
end
if (this.tooltipRequirement) then
GameTooltip:AddLine(this.tooltipRequirement, "", 1.0, 1.0, 1.0)
GameTooltip:Show()
end
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
<ThumbTexture name="$parentThumb" file="Interface\Buttons\UI-SliderBar-Button-Horizontal">
<Size><AbsDimension x="32" y="32"/></Size>
</ThumbTexture>
</Slider>
</Ui>
+3 -3
View File
@@ -144,14 +144,14 @@
frame.healthBar:SetMinMaxValues(0, UnitHealthMax(unit))
frame.healthBar:SetValue(UnitHealth(unit))
local maxPower = UnitPowerMax(unit)
local maxPower = UnitManaMax(unit)
if maxPower > 0 then
frame.manaBar:Show()
local _, token = UnitPowerType(unit)
local c = BENILLA_POWER_COLORS[token] or BENILLA_POWER_COLORS.MANA
frame.manaBar:SetStatusBarColor(c.r, c.g, c.b)
frame.manaBar:SetMinMaxValues(0, maxPower)
frame.manaBar:SetValue(UnitPower(unit))
frame.manaBar:SetValue(UnitMana(unit))
else
frame.manaBar:Hide()
end
@@ -201,7 +201,7 @@
end
local hp, hpMax = UnitHealth(unit), UnitHealthMax(unit)
if hpMax > 0 and hp > 0 and hp / hpMax <= 0.2 then
local t = math.fmod(frame.flashTimer + elapsed, 1.0)
local t = mod(frame.flashTimer + elapsed, 1.0)
frame.flashTimer = t
local a
if t < 0.5 then
+65 -54
View File
@@ -25,7 +25,7 @@
strips (256x44 at TOPLEFT with TexCoords 0..1 / 0.015625..0.359375, then 184x44 to its
right with 0..0.71875 / 0.375..0.71875); buttons 30x30, button1 at frame BOTTOMLEFT +(36,2),
chained +8 (with the ref's own +7 between 6 and 7); the 54x54 UI-Quickslot2 ring CENTER
(0,-1). The bar rides `BenillaActionBar` BOTTOMLEFT — our MainMenuBar, the same 1024x53
(0,-1). The bar rides `MainMenuBar` BOTTOMLEFT — our MainMenuBar, the same 1024x53
frame at screen BOTTOM the ref anchors to — at the managed PETACTIONBAR_YPOS (UIParent.xml
declares the row already).
@@ -222,13 +222,14 @@
-- and the reference immediately clears that (`this:SetChecked(0)`) — nothing about this bar
-- is locally latched, because the server owns every bit of it. Left = run the slot, unless
-- the pet is already on the attack, in which case it calls off; right = flip autocast.
function BenillaPetActionButton_OnClick(button, mouseButton)
function PetActionButton_OnClick(mouseButton)
local button = this
button:SetChecked(0)
-- Shift outranks the button fork entirely (ref l.253-255): a shift-click on the pet bar
-- is a pick-up whichever mouse button it came from, and it never toggles autocast.
if IsShiftKeyDown() then
PickupPetAction(button.index)
BenillaPetActionBar_Update()
PetActionBar_Update()
elseif mouseButton == "RightButton" then
TogglePetAutocast(button.index)
elseif IsPetAttackActive(button.index) then
@@ -255,15 +256,15 @@
-- latch lit, so click and key both simply order the attack. The split is visible on a
-- POSSESS bar (Mind Control, Eye of Kilrogg, Eyes of the Beast): there a click sends
-- PetStopAttack and CTRL-1 re-issues the order instead.
function BenillaPetActionButtonDown(id)
local button = getglobal("BenillaPetActionButton" .. id)
function PetActionButtonDown(id)
local button = getglobal("PetActionButton" .. id)
if ( button:GetButtonState() == "NORMAL" ) then
button:SetButtonState("PUSHED")
end
end
function BenillaPetActionButtonUp(id)
local button = getglobal("BenillaPetActionButton" .. id)
function PetActionButtonUp(id)
local button = getglobal("PetActionButton" .. id)
if ( button:GetButtonState() == "PUSHED" ) then
button:SetButtonState("NORMAL")
CastPetAction(id)
@@ -278,22 +279,25 @@
-- Both also honour the lock (the ref wraps each body in `if LOCK_ACTIONBAR ~= "1" then …`).
-- The shift-click in OnClick above is unguarded on this bar too, exactly as the reference
-- leaves it (l.253-255): the lock stops the drag, shift-click stays the way through.
function BenillaPetActionButton_OnDragStart(button)
function PetActionButton_OnDragStart()
local button = this
if LOCK_ACTIONBAR == "1" then return end
PickupPetAction(button.index)
BenillaPetActionBar_Update()
PetActionBar_Update()
end
function BenillaPetActionButton_OnReceiveDrag(button)
function PetActionButton_OnReceiveDrag()
local button = this
if LOCK_ACTIONBAR == "1" then return end
PickupPetAction(button.index)
BenillaPetActionBar_Update()
PetActionBar_Update()
end
-- Ref PetActionButton_OnEnter. A token slot has no spell to look up, so its tooltip is
-- built from the two strings the info call already returned (the ref's own `isToken` fork);
-- a spell slot goes through the engine's tooltip channel.
function BenillaPetActionButton_OnEnter(button)
function PetActionButton_OnEnter()
local button = this
if not GameTooltip then return end -- engine tests load file subsets
if not button.tooltipName then return end
GameTooltip_SetDefaultAnchor(GameTooltip, button)
@@ -308,7 +312,8 @@
end
end
function BenillaPetActionButton_OnLeave(button)
function PetActionButton_OnLeave()
local button = this
if GameTooltip then GameTooltip:Hide() end
end
@@ -318,10 +323,10 @@
-- header). Every branch below is the reference's, including the two that are easy to lose:
-- an unnamed slot HIDES its button, and a slot with no texture swaps the button's ring from
-- the filled UI-Quickslot2 to the empty UI-Quickslot.
function BenillaPetActionBar_Update()
function PetActionBar_Update()
local usable = GetPetActionsUsable()
for i = 1, BENILLA_NUM_PET_ACTION_SLOTS do
local button = getglobal("BenillaPetActionButton" .. i)
local button = getglobal("PetActionButton" .. i)
local name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled = GetPetActionInfo(i)
button.isToken = isToken
button.tooltipSubtext = subtext
@@ -341,7 +346,7 @@
-- An unnamed slot hides — UNLESS the drop grid is up, which is the whole point of
-- the grid: while a pet action rides the cursor every slot shows, so you can see
-- where it may land (ref PetActionBar_Update l.122-128 + PetActionBar_ShowGrid).
if name or (BenillaPetActionBarFrame.showgrid or 0) > 0 then
if name or (PetActionBarFrame.showgrid or 0) > 0 then
button:Show()
else
button:Hide()
@@ -361,7 +366,8 @@
end
end
function BenillaPetActionBar_OnLoad(frame)
function PetActionBar_OnLoad()
local frame = this
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PET_BAR_UPDATE")
-- The drop grid (decision 1010). A counter rather than a flag, ref-verbatim
@@ -372,21 +378,26 @@
frame:RegisterEvent("PET_BAR_HIDEGRID")
frame.showgrid = 0
frame.clock = 0
BenillaPetActionBar_OnEvent(frame)
PetActionBar_OnEvent()
end
-- The bar exists exactly while PetHasActionBar() does. Its show/hide re-fires the managed
-- bottom-stack pass (UIParent.xml, decision 0272) — the pet term of that stack is what
-- lifts the cast bar and the chat frame clear of it, which is the ref's own
-- PetActionBarFrame.xml:205-210.
function BenillaPetActionBar_OnEvent(frame, eventName)
if eventName == "PET_BAR_SHOWGRID" then
-- Declares nothing and reads the ambient `event`, exactly as the reference does
-- (PetActionBarFrame.lua:33 / .xml's bare `PetActionBar_OnEvent();`). PetActionBar_OnLoad's
-- own tail call therefore lands with `event` nil, which is the no-grid-edge path — the same
-- thing the old explicit `eventName` argument did when omitted.
function PetActionBar_OnEvent()
local frame = this
if event == "PET_BAR_SHOWGRID" then
frame.showgrid = frame.showgrid + 1
elseif eventName == "PET_BAR_HIDEGRID" and frame.showgrid > 0 then
elseif event == "PET_BAR_HIDEGRID" and frame.showgrid > 0 then
frame.showgrid = frame.showgrid - 1
end
if PetHasActionBar() then
BenillaPetActionBar_Update()
PetActionBar_Update()
frame:Show()
else
frame:Hide()
@@ -401,7 +412,7 @@
frame.clock = frame.clock - BENILLA_PET_AUTOCAST_PERIOD
end
for i = 1, BENILLA_NUM_PET_ACTION_SLOTS do
BenillaPetAutocast_Update(getglobal("BenillaPetActionButton" .. i), frame.clock)
BenillaPetAutocast_Update(getglobal("PetActionButton" .. i), frame.clock)
end
end
@@ -519,15 +530,15 @@
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\CheckButtonHilight" alphaMode="ADD"/>
<Scripts>
<OnClick>BenillaPetActionButton_OnClick(self, arg1)</OnClick>
<OnDragStart>BenillaPetActionButton_OnDragStart(self)</OnDragStart>
<OnReceiveDrag>BenillaPetActionButton_OnReceiveDrag(self)</OnReceiveDrag>
<OnEnter>BenillaPetActionButton_OnEnter(self)</OnEnter>
<OnLeave>BenillaPetActionButton_OnLeave(self)</OnLeave>
<OnClick>PetActionButton_OnClick(arg1)</OnClick>
<OnDragStart>PetActionButton_OnDragStart()</OnDragStart>
<OnReceiveDrag>PetActionButton_OnReceiveDrag()</OnReceiveDrag>
<OnEnter>PetActionButton_OnEnter()</OnEnter>
<OnLeave>PetActionButton_OnLeave()</OnLeave>
</Scripts>
</CheckButton>
<Frame name="BenillaPetActionBarFrame" hidden="true">
<Frame name="PetActionBarFrame" hidden="true">
<Size><AbsDimension x="509" y="43"/></Size>
<!-- This anchor is the LOAD-TIME value only: UIParent_ManageFramePositions owns the real
one and re-seats the frame on every pass (OnShow/OnHide below fire it), because the
@@ -540,68 +551,68 @@
the same arriving seated as it does arriving in 90 ms (the stance bar's own posture,
decision 0270). -->
<Anchors>
<Anchor point="TOPLEFT" relativeTo="BenillaActionBar" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="36" y="97"/></Offset></Anchor>
<Anchor point="TOPLEFT" relativeTo="MainMenuBar" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="36" y="97"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="BenillaSlidingActionBarTexture0" file="Interface\PetActionBar\UI-PetBar">
<Texture name="SlidingActionBarTexture0" file="Interface\PetActionBar\UI-PetBar">
<Size><AbsDimension x="256" y="44"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.015625" bottom="0.359375"/>
</Texture>
<Texture name="BenillaSlidingActionBarTexture1" file="Interface\PetActionBar\UI-PetBar">
<Texture name="SlidingActionBarTexture1" file="Interface\PetActionBar\UI-PetBar">
<Size><AbsDimension x="184" y="44"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaSlidingActionBarTexture0" relativePoint="RIGHT"/></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="SlidingActionBarTexture0" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.71875" top="0.375" bottom="0.71875"/>
</Texture>
</Layer>
</Layers>
<Frames>
<CheckButton name="BenillaPetActionButton1" inherits="BenillaPetActionButtonTemplate">
<CheckButton name="PetActionButton1" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="36" y="2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 1)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton2" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton1" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton2" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton1" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 2)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton3" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton2" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton3" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton2" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 3)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton4" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton3" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton4" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton3" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 4)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton5" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton4" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton5" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton4" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 5)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton6" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton5" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton6" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton5" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 6)</OnLoad></Scripts>
</CheckButton>
<!-- The ref's own +7 here, not +8 (PetActionBarFrame.xml l.158-166). -->
<CheckButton name="BenillaPetActionButton7" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton6" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton7" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton6" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 7)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton8" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton7" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton8" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton7" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 8)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton9" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton8" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton9" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton8" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 9)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaPetActionButton10" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetActionButton9" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="PetActionButton10" inherits="BenillaPetActionButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton9" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 10)</OnLoad></Scripts>
</CheckButton>
</Frames>
<Scripts>
<OnLoad>BenillaPetActionBar_OnLoad(self)</OnLoad>
<OnEvent>BenillaPetActionBar_OnEvent(self, event)</OnEvent>
<OnLoad>PetActionBar_OnLoad()</OnLoad>
<OnEvent>PetActionBar_OnEvent()</OnEvent>
<OnUpdate>BenillaPetActionBar_OnUpdate(self, arg1)</OnUpdate>
<OnShow>UIParent_ManageFramePositions()</OnShow>
<OnHide>UIParent_ManageFramePositions()</OnHide>
+134 -132
View File
@@ -7,7 +7,7 @@
**Its own top-level file, loaded straight after `CharacterFrame.xml`** — the SkillFrame.xml
precedent: this engine reads no XML `parent=` attribute, so a page ships as a sibling pinned
to `BenillaCharacterFrame`'s TOPLEFT by name, and `BENILLA_CHARACTERFRAME_SUBFRAMES` (which
to `CharacterFrame`'s TOPLEFT by name, and `BENILLA_CHARACTERFRAME_SUBFRAMES` (which
this page joins as entry 2) is what hides it when the whole window closes.
**Almost none of the page's Lua lives here, and that is the point.** The reference's own
@@ -19,10 +19,10 @@
l.470-473/l.541-544 — the pet page has no ranged rows, so the ref no-ops rather than
nil-indexing). So this file adds only what is genuinely pet-only: the stats/resistances pair
that reads the pet's own frames, the XP bar, loyalty, training points and the diet icon.
`prefix` here is `"BenillaPet"`, so `prefix.."ArmorFrame"` resolves `BenillaPetArmorFrame`,
exactly as `"BenillaCharacter"` resolves the sibling page's.
`prefix` here is `"Pet"`, so `prefix.."ArmorFrame"` resolves `PetArmorFrame`,
exactly as `"Character"` resolves the sibling page's.
**The tab is conditional, and `BenillaPetTab_Update` is the whole mechanism** (ref
**The tab is conditional, and `PetTab_Update` is the whole mechanism** (ref
`PetPaperDollFrame.lua:189-198`): no pet UI ⇒ tab 2 hides and tab 3 re-anchors onto tab 2's
LEFT, closing the gap; a pet ⇒ tab 2 shows and tab 3 goes back to the -16 chain. Ours is the
ref's arithmetic unchanged; only WHICH page tab 3 is differs (Skills here, Reputation there —
@@ -55,7 +55,7 @@
below dropped their existence guards. The ref's OWN `if ( UnitCreatureFamily("pet") )`
guard stays — nil is still reachable (a template with no family; the creature query not
yet answered) and a blank level line is the ref's answer to it.
- `BenillaPetPaperDollFrame_SetResistances` reads the school ids from the shared
- `PetPaperDollFrame_SetResistances` reads the school ids from the shared
`BENILLA_MAGICRES_IDS` table where the ref reads each frame's own `id=` attr
(`frame:GetID()`, ref l.112). Same five ids in the same order (6/2/3/4/5); the table is
what the sibling page already uses, and one convention across the two pages beats
@@ -90,12 +90,13 @@
-- page's OnLoad carries the why: this engine's template expansion APPENDS children, so a
-- re-declared $parentLabel makes a second anchorless FontString instead of merging).
-- `SetTextStatusBarTextPrefix(…, XP)` is dropped with the bar's numerals (header).
function BenillaPetPaperDollFrame_OnLoad(frame)
getglobal("BenillaPetAttackFrameLabel"):SetText(ATTACK_COLON)
getglobal("BenillaPetDamageFrameLabel"):SetText(DAMAGE_COLON)
getglobal("BenillaPetAttackPowerFrameLabel"):SetText(ATTACK_POWER_COLON)
getglobal("BenillaPetDefenseFrameLabel"):SetText(DEFENSE_COLON)
getglobal("BenillaPetArmorFrameLabel"):SetText(ARMOR_COLON)
function PetPaperDollFrame_OnLoad()
local frame = this
getglobal("PetAttackFrameLabel"):SetText(ATTACK_COLON)
getglobal("PetDamageFrameLabel"):SetText(DAMAGE_COLON)
getglobal("PetAttackPowerFrameLabel"):SetText(ATTACK_POWER_COLON)
getglobal("PetDefenseFrameLabel"):SetText(DEFENSE_COLON)
getglobal("PetArmorFrameLabel"):SetText(ARMOR_COLON)
frame:RegisterEvent("PET_UI_UPDATE")
frame:RegisterEvent("PET_BAR_UPDATE")
frame:RegisterEvent("PET_UI_CLOSE")
@@ -113,55 +114,56 @@
frame:RegisterEvent("UNIT_DEFENSE")
frame:RegisterEvent("UNIT_ATTACK")
frame:RegisterEvent("UNIT_PET_TRAINING_POINTS")
BenillaPetTab_Update()
PetTab_Update()
end
-- ref PetPaperDollFrame_OnEvent (l.31-48), all four arms. NOTE the shape: unlike the
-- character page, this one does NOT gate on visibility — the first arm has to run while the
-- page is HIDDEN, because that is what keeps the tab itself in step with whether a pet
-- exists. Only the last arm (a plain "the pet's numbers moved") is unit-token gated.
function BenillaPetPaperDollFrame_OnEvent(frame)
function PetPaperDollFrame_OnEvent()
local frame = this
if event == "PET_UI_UPDATE" or event == "PET_BAR_UPDATE"
or (event == "UNIT_PET" and arg1 == "player") then
-- The pet left while its own page was open: close the window (ToggleCharacter on
-- the visible page is the ref's own way of saying HideUIPanel), then re-fit the tab.
if frame:IsVisible() and not HasPetUI() then
ToggleCharacter("BenillaPetPaperDollFrame")
ToggleCharacter("PetPaperDollFrame")
end
BenillaPetTab_Update()
BenillaPetPaperDollFrame_Update()
PetTab_Update()
PetPaperDollFrame_Update()
elseif event == "PET_UI_CLOSE" then
if frame:IsVisible() then
ToggleCharacter("BenillaPetPaperDollFrame")
ToggleCharacter("PetPaperDollFrame")
end
BenillaPetTab_Update()
PetTab_Update()
elseif event == "UNIT_PET_EXPERIENCE" then
BenillaPetExpBar_Update()
PetExpBar_Update()
elseif arg1 == "pet" then
BenillaPetPaperDollFrame_Update()
PetPaperDollFrame_Update()
end
end
-- ref PetPaperDollFrame_OnShow/_OnHide (l.50-60): the window's ONE name line is shared —
-- the pet page borrows it and puts the player's back on the way out.
function BenillaPetPaperDollFrame_OnShow()
getglobal("BenillaCharacterNameText"):Hide()
local petName = getglobal("BenillaPetNameText")
function PetPaperDollFrame_OnShow()
getglobal("CharacterNameText"):Hide()
local petName = getglobal("PetNameText")
petName:Show()
petName:SetText(UnitName("pet"))
BenillaPetPaperDollFrame_Update()
PetPaperDollFrame_Update()
end
function BenillaPetPaperDollFrame_OnHide()
getglobal("BenillaCharacterNameText"):Show()
getglobal("BenillaPetNameText"):Hide()
function PetPaperDollFrame_OnHide()
getglobal("CharacterNameText"):Show()
getglobal("PetNameText"):Hide()
end
-- ref PetPaperDollFrame_Update (l.62-94). The `hasPetUI` early-out is load-bearing, not
-- defensive: every setter below reads the "pet" token, and with no pet those all answer the
-- absent shape — repainting from it would blank a page the ref leaves showing its last state
-- until the tab itself goes.
function BenillaPetPaperDollFrame_Update()
function PetPaperDollFrame_Update()
local hasPetUI, canGainXP = HasPetUI()
if not hasPetUI then return end
-- ref l.67 `PetModelFrame:SetUnit("pet")` — no analogue: the app's `petdoll` booth
@@ -174,34 +176,34 @@
-- the binding twice for one answer.
local family = UnitCreatureFamily("pet")
if family then
getglobal("BenillaPetLevelText"):SetText(
getglobal("PetLevelText"):SetText(
format(UNIT_LEVEL_TEMPLATE, UnitLevel("pet")) .. " " .. family)
end
getglobal("BenillaPetLoyaltyText"):SetText(GetPetLoyalty())
BenillaPetExpBar_Update()
BenillaPetPaperDollFrame_SetResistances()
BenillaPetPaperDollFrame_SetStats()
BenillaPaperDollFrame_SetDamage("pet", "BenillaPet")
BenillaPaperDollFrame_SetRangedDamage("pet", "BenillaPet")
BenillaPaperDollFrame_SetAttackPower("pet", "BenillaPet")
BenillaPaperDollFrame_SetRangedAttackPower("pet", "BenillaPet")
BenillaPaperDollFrame_SetArmor("pet", "BenillaPet")
BenillaPaperDollFrame_SetAttackBothHands("pet", "BenillaPet")
BenillaPaperDollFrame_SetDefense("pet", "BenillaPet")
getglobal("PetLoyaltyText"):SetText(GetPetLoyalty())
PetExpBar_Update()
PetPaperDollFrame_SetResistances()
PetPaperDollFrame_SetStats()
PaperDollFrame_SetDamage("pet", "Pet")
PaperDollFrame_SetRangedDamage("pet", "Pet")
PaperDollFrame_SetAttackPower("pet", "Pet")
PaperDollFrame_SetRangedAttackPower("pet", "Pet")
PaperDollFrame_SetArmor("pet", "Pet")
PaperDollFrame_SetAttackBothHands("pet", "Pet")
PaperDollFrame_SetDefense("pet", "Pet")
-- The hunter-only block (ref l.83-93): the diet icon and the training-point line are
-- the two things a warlock's minion never has. `canGainXP` is HasPetUI's second return.
if canGainXP then
getglobal("BenillaPetPaperDollPetInfo"):Show()
getglobal("PetPaperDollPetInfo"):Show()
local totalPoints, spent = GetPetTrainingPoints()
local text = getglobal("BenillaPetTrainingPointText")
local text = getglobal("PetTrainingPointText")
text:SetText(totalPoints - spent)
text:Show()
getglobal("BenillaPetTrainingPointLabel"):Show()
getglobal("PetTrainingPointLabel"):Show()
else
getglobal("BenillaPetPaperDollPetInfo"):Hide()
getglobal("BenillaPetTrainingPointText"):Hide()
getglobal("BenillaPetTrainingPointLabel"):Hide()
getglobal("PetPaperDollPetInfo"):Hide()
getglobal("PetTrainingPointText"):Hide()
getglobal("PetTrainingPointLabel"):Hide()
end
end
@@ -209,11 +211,11 @@
-- version: the pet's has no resistance-quality subtext (no `RESISTANCE_TOOLTIP_SUBTEXT`
-- line at all) and colours the NUMBER off its own sign rather than off pos-vs-neg
-- magnitude — two real differences in the reference, transcribed rather than unified.
function BenillaPetPaperDollFrame_SetResistances()
function PetPaperDollFrame_SetResistances()
for i = 1, BENILLA_NUM_PET_RESISTANCE_TYPES do
local resistId = BENILLA_MAGICRES_IDS[i]
local text = getglobal("BenillaPetMagicResText" .. i)
local frame = getglobal("BenillaPetMagicResFrame" .. i)
local text = getglobal("PetMagicResText" .. i)
local frame = getglobal("PetMagicResFrame" .. i)
local base, resistance, positive, negative = UnitResistance("pet", resistId)
frame.tooltip = getglobal("RESISTANCE" .. resistId .. "_NAME")
-- Resistances can be negative: red below zero, green above, plain white at zero.
@@ -241,11 +243,11 @@
-- _SetStats with the pet's own frame names and the "pet" token (the ref carries the same
-- duplication; unifying them would mean inventing a prefix the reference's own
-- `PetStatFrame`/`CharacterStatFrame` naming doesn't share).
function BenillaPetPaperDollFrame_SetStats()
function PetPaperDollFrame_SetStats()
for i = 1, BENILLA_NUM_PET_STATS do
local label = getglobal("BenillaPetStatFrame" .. i .. "Label")
local text = getglobal("BenillaPetStatFrame" .. i .. "StatText")
local frame = getglobal("BenillaPetStatFrame" .. i)
local label = getglobal("PetStatFrame" .. i .. "Label")
local text = getglobal("PetStatFrame" .. i .. "StatText")
local frame = getglobal("PetStatFrame" .. i)
local statName = getglobal("SPELL_STAT" .. (i - 1) .. "_NAME")
label:SetText(statName .. ":")
local stat, effectiveStat, posBuff, negBuff = UnitStat("pet", i)
@@ -279,9 +281,9 @@
-- ref PetExpBar_Update (l.183-187). `min(0, currXP)` is the ref's own; XP can't be negative,
-- so it is just 0 — quoted, not simplified.
function BenillaPetExpBar_Update()
function PetExpBar_Update()
local currXP, nextXP = GetPetExperience()
local bar = getglobal("BenillaPetPaperDollFrameExpBar")
local bar = getglobal("PetPaperDollFrameExpBar")
bar:SetMinMaxValues(math.min(0, currXP), nextXP)
bar:SetValue(currXP)
end
@@ -289,27 +291,27 @@
-- ref PetTab_Update (l.189-198) — the conditional tab, whole. The re-anchor is what keeps
-- the row gapless: with tab 2 hidden, tab 3 sits at tab 2's own LEFT (i.e. exactly where
-- tab 2 would have started) instead of -16 past its RIGHT.
function BenillaPetTab_Update()
local tab2 = getglobal("BenillaCharacterFrameTab2")
local tab3 = getglobal("BenillaCharacterFrameTab3")
function PetTab_Update()
local tab2 = getglobal("CharacterFrameTab2")
local tab3 = getglobal("CharacterFrameTab3")
-- Nil-guarded like CharacterFrame_ShowSubFrame's own loop: a test subset may load this
-- file without the window's tab row.
if not tab2 or not tab3 then return end
if not HasPetUI() then
tab2:Hide()
tab3:SetPoint("LEFT", "BenillaCharacterFrameTab2", "LEFT", 0, 0)
tab3:SetPoint("LEFT", "CharacterFrameTab2", "LEFT", 0, 0)
else
tab2:Show()
tab3:SetPoint("LEFT", "BenillaCharacterFrameTab2", "RIGHT", -16, 0)
tab3:SetPoint("LEFT", "CharacterFrameTab2", "RIGHT", -16, 0)
end
end
]]></Script>
<Frame name="BenillaPetPaperDollFrame" enableMouse="true" parent="BenillaCharacterFrame" hidden="true" id="2">
<Frame name="PetPaperDollFrame" enableMouse="true" parent="CharacterFrame" hidden="true" id="2">
<!-- The real 384×512 page (ref l.4, `setAllPoints="true"`); pinned to the window's TOPLEFT
by name, the SkillFrame.xml precedent (no cross-file `parent=` in this engine). -->
<Size><AbsDimension x="384" y="512"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCharacterFrame" relativePoint="TOPLEFT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="CharacterFrame" relativePoint="TOPLEFT"/></Anchors>
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="75"/></HitRectInsets>
<Layers>
<!-- The quadrant background (ref l.9-57): the "General" top halves every
@@ -334,28 +336,28 @@
</Texture>
<!-- The pet's name (ref l.58-69), sitting exactly where the window's own player
name line does — the two swap on this page's Show/Hide. -->
<FontString name="BenillaPetNameText" inherits="GameFontHighlight">
<FontString name="PetNameText" inherits="GameFontHighlight">
<Size><AbsDimension x="109" y="16"/></Size>
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="6" y="232"/></Offset></Anchor></Anchors>
</FontString>
<!-- "Level %d <Family>" (ref l.70-78) and the loyalty level under it (l.79-87). -->
<FontString name="BenillaPetLevelText" inherits="GameFontNormalSmall">
<FontString name="PetLevelText" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="TOP" relativeTo="BenillaPetNameText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor>
<Anchor point="TOP" relativeTo="PetNameText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="BenillaPetLoyaltyText" inherits="GameFontNormalSmall">
<FontString name="PetLoyaltyText" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="TOP" relativeTo="BenillaPetLevelText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
<Anchor point="TOP" relativeTo="PetLevelText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Unspent training points (ref l.88-105) — shown for hunter pets only. -->
<FontString name="BenillaPetTrainingPointText" inherits="GameFontHighlightSmall">
<FontString name="PetTrainingPointText" inherits="GameFontHighlightSmall">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-135" y="86"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaPetTrainingPointLabel" inherits="GameFontNormalSmall" text="TRAINING_POINTS">
<FontString name="PetTrainingPointLabel" inherits="GameFontNormalSmall" text="TRAINING_POINTS">
<Anchors>
<Anchor point="RIGHT" relativeTo="BenillaPetTrainingPointText" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="PetTrainingPointText" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
@@ -367,19 +369,19 @@
<!-- No `enableMouse`: the ref's bar takes the mouse only to show its numerals on hover
(l.166-172), and those are the deferral above — a mouse-enabled bar with nothing to
show would just eat clicks. It comes back with the numerals. -->
<StatusBar name="BenillaPetPaperDollFrameExpBar">
<StatusBar name="PetPaperDollFrameExpBar">
<Size><AbsDimension x="319" y="11"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="23" y="105"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="BenillaPetPaperDollXPBar1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Texture name="PetPaperDollXPBar1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="160" y="13"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0.203125" right="0.8046875" top="0.2890625" bottom="0.33984375"/>
</Texture>
<Texture file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
<Size><AbsDimension x="159" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetPaperDollXPBar1" relativePoint="RIGHT"/></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="PetPaperDollXPBar1" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.203125" right="0.8046875" top="0.2890625" bottom="0.33984375"/>
</Texture>
</Layer>
@@ -390,14 +392,14 @@
<!-- The ref's OnEvent is the TextStatusBar CVar hook plus PetExpBar_Update; only
the latter has anything to do here, and the page's own UNIT_PET_EXPERIENCE
arm already drives it, so no per-bar registration ships. -->
<OnLoad>BenillaPetExpBar_Update()</OnLoad>
<OnLoad>PetExpBar_Update()</OnLoad>
</Scripts>
</StatusBar>
<!-- The model pane (ref PlayerModel PetModelFrame, l.177-244): 318×224 at TOPLEFT
(25,-78) — wider than the character page's 233 because there is no equipment column
beside it. A plain Frame sampling the `petdoll` booth bake (header). -->
<Frame name="BenillaPetModelFrame">
<Frame name="PetModelFrame">
<Size><AbsDimension x="318" y="224"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="25" y="-78"/></Offset></Anchor></Anchors>
<Layers>
@@ -423,26 +425,26 @@
<!-- Rotate buttons (ref l.201-242): 35×35. NOTE the ref anchors the left one to
the PAGE, not to the pane (l.206) — (23,-76), two pixels up and left of the
pane's own corner — and the right one chains off its TOPRIGHT. -->
<Button name="BenillaPetModelFrameRotateLeftButton">
<Button name="PetModelFrameRotateLeftButton">
<Size><AbsDimension x="35" y="35"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetPaperDollFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="23" y="-76"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetPaperDollFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="23" y="-76"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-RotationLeft-Button-Up"/>
<PushedTexture file="Interface\Buttons\UI-RotationLeft-Button-Down"/>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
<Scripts>
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
<OnClick>BenillaPaperDollModel_RotateLeft(getglobal("BenillaPetModelFrame"))</OnClick>
<OnClick>BenillaPaperDollModel_RotateLeft(getglobal("PetModelFrame"))</OnClick>
</Scripts>
</Button>
<Button name="BenillaPetModelFrameRotateRightButton">
<Button name="PetModelFrameRotateRightButton">
<Size><AbsDimension x="35" y="35"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetModelFrameRotateLeftButton" relativePoint="TOPRIGHT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetModelFrameRotateLeftButton" relativePoint="TOPRIGHT"/></Anchors>
<NormalTexture file="Interface\Buttons\UI-RotationRight-Button-Up"/>
<PushedTexture file="Interface\Buttons\UI-RotationRight-Button-Down"/>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
<Scripts>
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
<OnClick>BenillaPaperDollModel_RotateRight(getglobal("BenillaPetModelFrame"))</OnClick>
<OnClick>BenillaPaperDollModel_RotateRight(getglobal("PetModelFrame"))</OnClick>
</Scripts>
</Button>
</Frames>
@@ -467,10 +469,10 @@
(its tooltip lists what it eats), not a happiness readout; the happiness face that
does move lives on the pet unit frame (decision 1005/0990). Shown for hunter pets
only. -->
<Frame name="BenillaPetPaperDollPetInfo">
<Frame name="PetPaperDollPetInfo">
<Size><AbsDimension x="24" y="23"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="BenillaPetModelFrameRotateLeftButton" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor>
<Anchor point="TOPLEFT" relativeTo="PetModelFrameRotateLeftButton" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
@@ -505,7 +507,7 @@
<!-- The page's own Close button (ref PetPaperDollCloseButton, l.276-292) — the panel
button art/size/font inline, since no UIPanelButtonTemplate ships (header). -->
<Button name="BenillaPetPaperDollCloseButton" text="CLOSE">
<Button name="PetPaperDollCloseButton" text="CLOSE">
<Size><AbsDimension x="80" y="22"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="305" y="-422"/></Offset></Anchor></Anchors>
<NormalFont inherits="GameFontNormal"/>
@@ -519,7 +521,7 @@
cross-file parenting, so the page is not really the window's child (header).
CLOSE is the ref's own literal `text="CLOSE"` attribute — the GlobalStrings
key, resolved by the loader's text lookup like every other one here. -->
<OnClick>HideUIPanel(getglobal("BenillaCharacterFrame"))</OnClick>
<OnClick>HideUIPanel(getglobal("CharacterFrame"))</OnClick>
</Scripts>
</Button>
@@ -527,7 +529,7 @@
(67,-300), two 114×78 stat-background plates side by side. The pet's plate is the
SIMPLE two-column one — the character page's is three stacked 3-piece columns
because it also carries a ranged block, which a pet has none of. -->
<Frame name="BenillaPetAttributesFrame">
<Frame name="PetAttributesFrame">
<Size><AbsDimension x="230" y="78"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="67" y="-300"/></Offset></Anchor></Anchors>
<Layers>
@@ -547,51 +549,51 @@
<Frames>
<!-- The 5 primaries (ref l.335-419): TOPLEFT (6,-5), chained BOTTOMLEFT with a
1px gap. Labels are the template's $parentLabel, filled by _SetStats. -->
<Frame name="BenillaPetStatFrame1" inherits="BenillaStatRowTemplate">
<Frame name="PetStatFrame1" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-5"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>BenillaPaperDollStatTooltip(self, "STRENGTH", "pet")</OnEnter>
<OnEnter>PaperDollStatTooltip("pet", "STRENGTH")</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetStatFrame2" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetStatFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetStatFrame2" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>BenillaPaperDollStatTooltip(self, "AGILITY", "pet")</OnEnter>
<OnEnter>PaperDollStatTooltip("pet", "AGILITY")</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetStatFrame3" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetStatFrame2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetStatFrame3" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>BenillaPaperDollStatTooltip(self, "STAMINA", "pet")</OnEnter>
<OnEnter>PaperDollStatTooltip("pet", "STAMINA")</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetStatFrame4" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetStatFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetStatFrame4" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>BenillaPaperDollStatTooltip(self, "INTELLECT", "pet")</OnEnter>
<OnEnter>PaperDollStatTooltip("pet", "INTELLECT")</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetStatFrame5" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetStatFrame4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetStatFrame5" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>BenillaPaperDollStatTooltip(self, "SPIRIT", "pet")</OnEnter>
<OnEnter>PaperDollStatTooltip("pet", "SPIRIT")</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<!-- The right-hand column (ref l.420-514): attack rating, attack power, damage,
defense, armor — the second plate's own chain, rooted at (122,-5) of the
attributes frame. -->
<Frame name="BenillaPetAttackFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetAttributesFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="122" y="-5"/></Offset></Anchor></Anchors>
<Frame name="PetAttackFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetAttributesFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="122" y="-5"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<!-- ref l.429-433: the pet's attack row shows the BARE tooltip title, with
@@ -605,8 +607,8 @@
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetAttackPowerFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetAttackFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetAttackPowerFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetAttackFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>
@@ -618,19 +620,19 @@
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetDamageFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetAttackPowerFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetDamageFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetAttackPowerFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<!-- ref l.468-470: the pet's damage row reuses the CHARACTER damage
hover verbatim — it reads `self.damage`/`self.dps`, which
_SetDamage filled with the pet's numbers. -->
<OnEnter>BenillaCharacterDamageFrame_OnEnter(self)</OnEnter>
<OnEnter>CharacterDamageFrame_OnEnter()</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetDefenseFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetDamageFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetDefenseFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetDamageFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>
@@ -641,8 +643,8 @@
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="BenillaPetArmorFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaPetDefenseFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Frame name="PetArmorFrame" inherits="BenillaStatRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="PetDefenseFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:EnableMouse(true)</OnLoad>
<OnEnter>
@@ -662,72 +664,72 @@
297, because the pet page's wider model pane pushes it out. The five icons carry
their own texcoords into the shared ResistanceIcons sheet, in the ref's own school
order (arcane/fire/nature/frost/shadow = BENILLA_MAGICRES_IDS). -->
<Frame name="BenillaPetResistanceFrame">
<Frame name="PetResistanceFrame">
<Size><AbsDimension x="32" y="160"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="BenillaPetPaperDollFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="347" y="-77"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="PetPaperDollFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="347" y="-77"/></Offset></Anchor>
</Anchors>
<Frames>
<Frame name="BenillaPetMagicResFrame1" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaPetResistanceFrame" relativePoint="TOP"/></Anchors>
<Frame name="PetMagicResFrame1" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="PetResistanceFrame" relativePoint="TOP"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
<TexCoords left="0" right="1.0" top="0.2265625" bottom="0.33984375"/>
</Texture>
<FontString name="BenillaPetMagicResText1" inherits="GameFontHighlightSmall" text="X">
<FontString name="PetMagicResText1" inherits="GameFontHighlightSmall" text="X">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<Frame name="BenillaPetMagicResFrame2" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaPetMagicResFrame1" relativePoint="BOTTOM"/></Anchors>
<Frame name="PetMagicResFrame2" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame1" relativePoint="BOTTOM"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
<TexCoords left="0" right="1.0" top="0" bottom="0.11328125"/>
</Texture>
<FontString name="BenillaPetMagicResText2" inherits="GameFontHighlightSmall" text="X">
<FontString name="PetMagicResText2" inherits="GameFontHighlightSmall" text="X">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<Frame name="BenillaPetMagicResFrame3" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaPetMagicResFrame2" relativePoint="BOTTOM"/></Anchors>
<Frame name="PetMagicResFrame3" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame2" relativePoint="BOTTOM"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
<TexCoords left="0" right="1.0" top="0.11328125" bottom="0.2265625"/>
</Texture>
<FontString name="BenillaPetMagicResText3" inherits="GameFontHighlightSmall" text="X">
<FontString name="PetMagicResText3" inherits="GameFontHighlightSmall" text="X">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<Frame name="BenillaPetMagicResFrame4" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaPetMagicResFrame3" relativePoint="BOTTOM"/></Anchors>
<Frame name="PetMagicResFrame4" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame3" relativePoint="BOTTOM"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
<TexCoords left="0" right="1.0" top="0.33984375" bottom="0.453125"/>
</Texture>
<FontString name="BenillaPetMagicResText4" inherits="GameFontHighlightSmall" text="X">
<FontString name="PetMagicResText4" inherits="GameFontHighlightSmall" text="X">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<Frame name="BenillaPetMagicResFrame5" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="BenillaPetMagicResFrame4" relativePoint="BOTTOM"/></Anchors>
<Frame name="PetMagicResFrame5" inherits="BenillaMagicResistanceFrameTemplate">
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame4" relativePoint="BOTTOM"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
<TexCoords left="0" right="1.0" top="0.453125" bottom="0.56640625"/>
</Texture>
<FontString name="BenillaPetMagicResText5" inherits="GameFontHighlightSmall" text="X">
<FontString name="PetMagicResText5" inherits="GameFontHighlightSmall" text="X">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
@@ -737,10 +739,10 @@
</Frame>
</Frames>
<Scripts>
<OnLoad>BenillaPetPaperDollFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaPetPaperDollFrame_OnEvent(self)</OnEvent>
<OnShow>BenillaPetPaperDollFrame_OnShow()</OnShow>
<OnHide>BenillaPetPaperDollFrame_OnHide()</OnHide>
<OnLoad>PetPaperDollFrame_OnLoad()</OnLoad>
<OnEvent>PetPaperDollFrame_OnEvent()</OnEvent>
<OnShow>PetPaperDollFrame_OnShow()</OnShow>
<OnHide>PetPaperDollFrame_OnHide()</OnHide>
</Scripts>
</Frame>
</Ui>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -24,14 +24,18 @@
getglobal(name), and the ref's own Lua below reaches its rows by
getglobal("QuestTimer"..i.."Text"). Prefixing would silently unseat the frame and break
the row lookup at once.
- Lua 5.0 `arg`/`arg.n` varargs replaced with select("#", ...)/select(i, ...) —
LUA_COMPAT_VARARG isn't shipped (TalentFrame.xml / WorldMapFrame.xml's precedent).
- Lua 5.0 `arg`/`arg.n` varargs written as select("#", ...)/select(i, ...), the 5.1-native
spelling and this tree's house style (TalentFrame.xml / WorldMapFrame.xml's precedent).
- The ref's XML anchor (TOPRIGHT of MinimapCluster +10,0) is transcribed and, like every
managed HUD frame, DEAD ON ARRIVAL: UIParent_ManageFramePositions re-seats it at
-CONTAINER_OFFSET_X on every pass. Kept verbatim anyway so the file reads as the ref's.
- `toplevel="true"` is dropped: it is the ref's raise-on-click behaviour for movable
dialogs, which this engine has no verb for and this frame never uses (it is not movable
and its click opens the quest log rather than raising anything).
- `toplevel="true"` is dropped, and the reason given here was half wrong: the raise's real
triggers are **Show**, drag-start and mouse-down, not "raise-on-click for movable dialogs"
(wow-re `ui/scratch/toplevel-raise.md`). The verb now exists (`script::object::toplevel`),
so the standing reason is only the second one — this frame is a managed HUD strip that
UIParent_ManageFramePositions re-seats and that overlaps nothing in its own stratum, so
the occlusion gate would decline every raise anyway. Re-adding it is one word if that
stops being true.
- QuestTimerFrame_OnShow/OnHide call the manage pass, as the ref does — that is what lets
the durability guy and the quest tracker drop below this window when a timer appears.
- The ref's own commented-out QuestTimerFrame_UpdatePosition (its pre-manage-pass
@@ -112,7 +116,7 @@
if not index then
return
end
ShowUIPanel(getglobal("BenillaQuestLogFrame"))
ShowUIPanel(getglobal("QuestLogFrame"))
SelectQuestLogEntry(index)
BenillaQuestLogFrame_Update()
end
+133 -20
View File
@@ -64,6 +64,25 @@
-- repaint calls _Update below, which SetValue()s the thumb back to offset*itemHeight — a value
-- that maps to the SAME offset, so this handler no-ops and the cycle stops after one hop (the
-- Slider's own SetValue change-gate stops the value side, decision 0250).
--
-- THE SECOND HALF IS FOR ADDONS, and it is the reference's own wiring. The ref's
-- `UIPanelScrollBarTemplate` routes its value through the ScrollFrame it rides
-- (`<OnValueChanged>this:GetParent():SetVerticalScroll(arg1)</OnValueChanged>`,
-- ref-UIPanelTemplates.xml l.156-158), and that frame's `<OnVerticalScroll>` is where an
-- addon puts `FauxScrollFrame_OnVerticalScroll(itemHeight, MyUpdate)`. Our OWN faux frames
-- are plain `<Frame>`s (the 0251 divergence in the header above), so they have no
-- SetVerticalScroll and this line never fires for them — but an addon's is declared
-- `<ScrollFrame ... inherits="FauxScrollFrameTemplate">`, and the instance's own tag wins
-- over the template's on inherit (framexml.rs `merge`), so it really is one. Duck-typed for
-- exactly that reason.
--
-- GAP, stated where it bites: `SetVerticalScroll` clamps into `[0, GetVerticalScrollRange()]`
-- (scrollframe.rs), and that range comes from the frame's SCROLL CHILD — which the ref's
-- `FauxScrollFrameTemplate` declares as `<ScrollChild><Frame name="$parentScrollChildFrame">`
-- and our XML loader has no `<ScrollChild>` element for. Until it does, an addon frame's
-- range reads 0, so the handler fires with arg1 = 0 and its list stays at the top. The
-- offset our own kit computes above is unaffected — it comes straight off the Slider, which
-- is the whole point of 0251.
function BenillaFauxScrollBar_OnValueChanged(scrollBar, value)
local frame = scrollBar:GetParent()
local step = scrollBar:GetValueStep()
@@ -74,25 +93,53 @@
if frame.updateFunc then frame.updateFunc() end
end
BenillaScrollBar_UpdateArrows(scrollBar)
if frame.SetVerticalScroll then frame:SetVerticalScroll(value) end
end
-- OnLoad: a fresh faux frame is at the top with an empty range (ref ScrollFrame_OnLoad).
function BenillaFauxScrollFrame_OnLoad(frame)
frame.offset = 0
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
-- OnLoad: a fresh scroll frame is at the top with an empty range and both arrows greyed
-- (ref UIPanelTemplates.lua l.244-252, verbatim but for the one added line). The ref's own
-- name, because this is the ref's own function: `FauxScrollFrameTemplate` inherits
-- `UIPanelScrollFrameTemplate`, whose `<OnLoad>` is exactly this.
--
-- The added line is `SetValueStep(1)`: our bar's step is also its ARROW/WHEEL step
-- (BenillaScrollBar_Step), so a bar that has not been through an _Update yet would step by
-- nothing. The ref has no such coupling — its arrows move by half the bar's height.
function ScrollFrame_OnLoad()
getglobal(this:GetName() .. "ScrollBarScrollDownButton"):Disable()
getglobal(this:GetName() .. "ScrollBarScrollUpButton"):Disable()
local scrollBar = getglobal(this:GetName() .. "ScrollBar")
scrollBar:SetMinMaxValues(0, 0)
scrollBar:SetValueStep(1)
scrollBar:SetValue(0)
this.offset = 0
end
-- The offset getters/setters the owner's list code reads (ref FauxScrollFrame_GetOffset /
-- _SetOffset). _SetOffset syncs the thumb too, so a programmatic reset (top-of-list on open)
-- moves the bar.
function BenillaFauxScrollFrame_GetOffset(frame)
-- The frame's `<OnVerticalScroll>` body, as the reference writes it (UIPanelTemplates.lua
-- l.228-233, verbatim): the frame scrolled, so seat the bar at the new pixel value, round it
-- to a row, and repaint. `this`/`arg1` are the handler globals our dispatcher sets
-- (script/event.rs `invoke_with_globals`), so the ref's body runs unchanged.
--
-- Nothing in benilla's own FrameXML calls this — our six faux lists take the offset straight
-- off the Slider (BenillaFauxScrollBar_OnValueChanged above). It is here because it is the
-- shape 27 corpus addons write into their own XML, and it is reached through the
-- SetVerticalScroll passthrough there, whose one gap is documented at that spot.
function FauxScrollFrame_OnVerticalScroll(itemHeight, updateFunction)
local scrollbar = getglobal(this:GetName() .. "ScrollBar")
scrollbar:SetValue(arg1)
this.offset = floor((arg1 / itemHeight) + 0.5)
updateFunction()
end
-- The offset getters/setters the owner's list code reads (ref UIPanelTemplates.lua
-- l.235-241). _SetOffset diverges by one line, deliberately: the ref writes `frame.offset`
-- and leaves the thumb where it was, which is a latent desync until the next _Update; ours
-- syncs the bar, so a programmatic reset (top-of-list on open) moves it there and then.
function FauxScrollFrame_GetOffset(frame)
return frame.offset or 0
end
function BenillaFauxScrollFrame_SetOffset(frame, offset)
function FauxScrollFrame_SetOffset(frame, offset)
frame.offset = offset
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
scrollBar:SetValue(offset * scrollBar:GetValueStep())
@@ -152,27 +199,93 @@
end
end
-- Reconcile the bar to a freshly-sized list (ref FauxScrollFrame_Update): clamp the offset into
-- range, set the pixel range + step, seat the thumb at the current offset, and show the bar only
-- when the list overflows the visible rows. Called by the owner's list-repaint each pass.
function BenillaFauxScrollFrame_Update(frame, numItems, numToDisplay, itemHeight)
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
-- Reconcile the bar to a freshly-sized list — the reference's own `FauxScrollFrame_Update`
-- (UIPanelTemplates.lua l.160-226), whose signature and return value are transcribed whole
-- because 28 corpus addons call it with all ten arguments and read the result:
--
-- frame, numItems, numToDisplay, valueStep the list itself (our four)
-- button, smallWidth, bigWidth a `<name>1..numToDisplay` row family to
-- NARROW when the bar takes its gutter
-- highlightFrame, smallHighlightWidth, and the moving highlight over them
-- bigHighlightWidth
-- → showScrollBar 1 when the bar is up, nil when it is not
--
-- `valueStep` is what benilla called `itemHeight` before this landed; same number, the ref's
-- name. The shrink/widen tail is pure ref and no benilla window passes it — our lists are
-- laid out to their with-bar width from the start.
--
-- TWO DELIBERATE DIVERGENCES from the ref body, both named where they are:
--
-- 1 · The ref gates its whole middle section on `frame:IsVisible()` right after showing or
-- hiding the frame — reading back a state it just wrote. That is a quirk, not a
-- mechanism: it is true exactly when `showScrollBar` is, EXCEPT when an ancestor is
-- hidden, in which case the range/step/value silently do not get set and the bar is
-- stale the next time the window opens. benilla repaints lists while their window is
-- closed (a feed arriving is not a click), so we gate on `showScrollBar` — what the ref
-- means — and the offset clamp runs unconditionally.
-- 2 · The bar is shown/hidden through BenillaScrollBar_SetShown, not by the frame alone:
-- ours also carries the TROUGH (see its own note above; the ref has no trough in this
-- kit at all). The frame's own Show/Hide is kept as well, because it is what an addon
-- reads back — and because for us it is the same pixels either way, the bar being the
-- only thing a faux frame contains.
function FauxScrollFrame_Update(frame, numItems, numToDisplay, valueStep, button, smallWidth, bigWidth, highlightFrame, smallHighlightWidth, bigHighlightWidth)
local frameName = frame:GetName()
local scrollBar = getglobal(frameName .. "ScrollBar")
local maxOffset = numItems - numToDisplay
if maxOffset < 0 then maxOffset = 0 end
if not frame.offset or frame.offset > maxOffset then frame.offset = maxOffset end
if frame.offset < 0 then frame.offset = 0 end
scrollBar:SetValueStep(itemHeight)
scrollBar:SetMinMaxValues(0, maxOffset * itemHeight)
scrollBar:SetValue(frame.offset * itemHeight)
local showScrollBar
if numItems > numToDisplay then showScrollBar = 1 end
scrollBar:SetValueStep(valueStep)
scrollBar:SetMinMaxValues(0, maxOffset * valueStep)
scrollBar:SetValue(frame.offset * valueStep)
BenillaScrollBar_UpdateArrows(scrollBar)
BenillaScrollBar_SetShown(scrollBar, numItems > numToDisplay)
if showScrollBar then frame:Show() else frame:Hide() end
BenillaScrollBar_SetShown(scrollBar, showScrollBar)
-- The ref sizes the scroll child to the whole list (l.179-191) — which is what gives a
-- real ScrollFrame its scroll range. GUARDED, because our loader has no `<ScrollChild>`
-- element, so `$parentScrollChildFrame` exists only if the owner declared one itself.
-- (This is the same gap BenillaFauxScrollBar_OnValueChanged documents; closing it is a
-- loader change, not a FrameXML one.)
local scrollChildFrame = getglobal(frameName .. "ScrollChildFrame")
if scrollChildFrame then
if numItems > 0 then
scrollChildFrame:SetHeight(numItems * valueStep)
scrollChildFrame:Show()
else
scrollChildFrame:Hide()
end
end
-- Shrink because the bar is shown / widen because it is hidden (ref l.205-223).
if showScrollBar then
if highlightFrame then highlightFrame:SetWidth(smallHighlightWidth) end
if button then
for i = 1, numToDisplay do getglobal(button .. i):SetWidth(smallWidth) end
end
else
if highlightFrame then highlightFrame:SetWidth(bigHighlightWidth) end
if button then
for i = 1, numToDisplay do getglobal(button .. i):SetWidth(bigWidth) end
end
end
return showScrollBar
end
-- A wheel spin over the list → one row (WoW convention: +1 up, −1 down). Called from the
-- owner window's OnMouseWheel (the engine bubbles the spin up the parent chain, so the handler
-- lives on the window, not a child — TrainerFrame.xml's own note).
--
-- KEEPS the `Benilla` prefix on purpose: the reference has no faux-scroll wheel function to
-- take the name of. Its `ScrollFrameTemplate_OnMouseWheel` is the REAL-scroll one and moves
-- by half the bar's height; its faux owners each write a wheel body of their own inline.
function BenillaFauxScrollFrame_OnMouseWheel(frame, delta)
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
BenillaScrollBar_Step(scrollBar, -delta)
@@ -388,7 +501,7 @@
UIPanelScrollFrameTemplate's scrollbar anchors). An owner instantiates this and, in its own
OnLoad, sets `frame.updateFunc` to its list-repaint.
-->
<Frame name="BenillaFauxScrollFrameTemplate" virtual="true">
<Frame name="FauxScrollFrameTemplate" virtual="true">
<Frames>
<Slider name="$parentScrollBar" inherits="BenillaScrollBarTemplate">
<Anchors>
@@ -398,7 +511,7 @@
</Slider>
</Frames>
<Scripts>
<OnLoad>BenillaFauxScrollFrame_OnLoad(self)</OnLoad>
<OnLoad>ScrollFrame_OnLoad()</OnLoad>
</Scripts>
</Frame>
</Ui>
+81 -79
View File
@@ -8,20 +8,20 @@
`Expand`/`CollapseSkillHeader`/`SetSelectedSkill`/`GetSelectedSkill`, decision 0437's skills
seam, `crates/benilla-ui/src/script/skills.rs`, built alongside this file).
A CHILD PAGE of `BenillaCharacterFrame` (ref: `parent="CharacterFrame"`, `id="4"`), shown/
hidden by `CharacterFrame_ShowSubFrame` exactly like `BenillaPaperDollFrame` — but NOT a
A CHILD PAGE of `CharacterFrame` (ref: `parent="CharacterFrame"`, `id="4"`), shown/
hidden by `CharacterFrame_ShowSubFrame` exactly like `PaperDollFrame` — but NOT a
physically nested `<Frame>` the way PaperDollFrame is: PaperDollFrame is declared INSIDE
CharacterFrame.xml's own `<Frames>` tree (a real engine parent/child), while this page is a
SEPARATE file's top-level `<Frame>` (CharacterFrame.xml is shared-tree with another session
this pass, so only its tab machinery is touched here — not its frame tree). `parent=` is
transcribed for fidelity but is a NO-OP in this engine (grepped `benilla-ui/src/loader`: the
XML `parent` attribute is read nowhere — only physical `<Frames>` nesting creates a real
parent). Position instead rides `<Anchors relativeTo="BenillaCharacterFrame">` (a NAMED
parent). Position instead rides `<Anchors relativeTo="CharacterFrame">` (a NAMED
lookup, works regardless of tree position, the same mechanism every sibling-anchor in this
codebase already uses) — loaded right after `CharacterFrame.xml` in the load list so it
paints ON TOP of it (later file = higher z, the established house convention every top-level
window already relies on implicitly). Because a true engine Hide-cascade doesn't reach a
non-child, `CharacterFrame.xml`'s own `BenillaCharacterFrame_OnHide` was extended (its own
non-child, `CharacterFrame.xml`'s own `CharacterFrame_OnHide` was extended (its own
header comment) to loop-hide every `BENILLA_CHARACTERFRAME_SUBFRAMES` entry — the fix this
page's non-nested shape requires, done once so later non-nested tabs (Pet/Reputation/Honor)
inherit it for free.
@@ -29,7 +29,7 @@
NO OWN PORTRAIT (matches the ref: `ref-ui/SkillFrame.xml` l.130-213 draws no portrait Texture
at all): the shared quadrant background art below (`UI-Character-General-TopLeft/TopRight`,
the SAME "General" variant the ref uses, l.136-159) carries the SAME baked-in portrait-ring
cutout every PaperDollInfoFrame quadrant variant shares, so `BenillaCharacterFrame`'s own
cutout every PaperDollInfoFrame quadrant variant shares, so `CharacterFrame`'s own
portrait/name/level (drawn at a lower z, underneath) shows through unchanged — exactly the
ref's own real-client compositing (SkillFrame is a nested child there too, relying on the
same cutout rather than redrawing the portrait itself).
@@ -42,10 +42,10 @@
v1 DEVIATIONS (each re-cited at its own site below):
1. THE DETAIL PANE IS MINIMAL-PLUS-DESCRIPTION-PLUS-ABANDON: the selected skill's rank
StatusBar + name + the DESCRIPTION BODY (`BenillaSkillDetailDescriptionText` — ref
StatusBar + name + the DESCRIPTION BODY (`SkillDetailDescriptionText` — ref
`SkillDetailDescriptionText`/`SKILL_DESCRIPTION`, real since `GetSkillLineInfo`'s 13th
return carries `SkillLine.dbc` col 12) + the ABANDON BUTTON
(`BenillaSkillDetailUnlearnButton` → the `UNLEARN_SKILL` confirm → `AbandonSkill` →
(`SkillDetailUnlearnButton` → the `UNLEARN_SKILL` confirm → `AbandonSkill` →
`CMSG_UNLEARN_SKILL`; the isAbandonable predicate is `SkillRaceClassInfo.flags & 0x20`
— the server's own gate, real 5875 data: primary professions only). STILL OMITTED:
the training-point spend row (the ref's `LeftArrow`/`RightArrow`/`LearnSkillButton`/
@@ -58,8 +58,8 @@
3. NO ACCEPT/CANCEL BUTTONS (ref l.318-357's `SkillFrameAcceptButton`/`SkillFrameCancelButton`,
the latter ALSO commented dead in the ref): both back the training-point economy (Accept/
CancelSkillUps) this pane doesn't ship, AND this page has no window of its own to close —
`BenillaCharacterFrame`'s single shared close X (CharacterFrame.xml) already covers every
tab, matching `BenillaPaperDollFrame`'s own lack of a per-page close button.
`CharacterFrame`'s single shared close X (CharacterFrame.xml) already covers every
tab, matching `PaperDollFrame`'s own lack of a per-page close button.
4. THE RANK TEXT FORMAT IS THE REF'S BYTE-EXACT ONE, not the task brief's own paraphrase:
`ref-ui/SkillFrame.lua` l.178-186 builds `rank .. " (" .. color .. modifier .. close ..
")/" .. max` (e.g. "57 (|cff20ff20+3|r)/75") — transcribed verbatim (the
@@ -75,7 +75,7 @@
7. `LockHighlight`/`UnlockHighlight` ENGINE GAP (TalentFrame.xml/TradeSkillFrame.xml's own
"no engine equivalent" posture, grepped `benilla-ui/src/script/button.rs` — ships
texture-pairs/SetText/Enable/Disable/RegisterForClicks/SetButtonState/Click only): the
persistent selection glow is `BenillaSkillHighlight`, the separate overlay Frame
persistent selection glow is `SkillHighlight`, the separate overlay Frame
Trainer/TradeSkillFrame already use, moved onto the selected row's StatusBar.
8. GlobalStrings this window itself needs: NONE beyond the tab label `SKILLS` (already
declared in CharacterFrame.xml, quoted there byte-exact with its GlobalStrings.lua line
@@ -182,9 +182,9 @@
-- same fill (ref l.220-226): shown only for an abandonable line, carrying the name the
-- confirm popup formats and the index its accept abandons.
function BenillaSkillFrame_UpdateDetail()
local bar = getglobal("BenillaSkillDetailBar")
local desc = getglobal("BenillaSkillDetailDescriptionText")
local unlearn = getglobal("BenillaSkillDetailUnlearnButton")
local bar = getglobal("SkillDetailBar")
local desc = getglobal("SkillDetailDescriptionText")
local unlearn = getglobal("SkillDetailUnlearnButton")
local index = GetSelectedSkill()
if index == 0 then
bar:Hide()
@@ -252,8 +252,8 @@
-- skillCostType coloring + stepCost/rankCost training-up arrows never apply here, since
-- GetSkillLineInfo always reports them nil/0, this file's header comment deviation 1).
function BenillaSkillFrame_SetRow(i, index, numRows)
local bar = getglobal("BenillaSkillRankFrame" .. i)
local label = getglobal("BenillaSkillTypeLabel" .. i)
local bar = getglobal("SkillRankFrame" .. i)
local label = getglobal("SkillTypeLabel" .. i)
if index > numRows then
bar:Hide()
@@ -292,11 +292,11 @@
-- TradeSkillFrame.xml "ref's own dead code is dropped" precedent).
function BenillaSkillFrame_Update()
local numRows = GetNumSkillLines()
local scrollFrame = getglobal("BenillaSkillListScrollFrame")
BenillaFauxScrollFrame_Update(scrollFrame, numRows, SKILLFRAME_SKILLS_DISPLAYED, SKILLFRAME_SKILL_HEIGHT)
local offset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
local scrollFrame = getglobal("SkillListScrollFrame")
FauxScrollFrame_Update(scrollFrame, numRows, SKILLFRAME_SKILLS_DISPLAYED, SKILLFRAME_SKILL_HEIGHT)
local offset = FauxScrollFrame_GetOffset(scrollFrame)
local highlight = getglobal("BenillaSkillHighlight")
local highlight = getglobal("SkillHighlight")
highlight:Hide()
local selected = GetSelectedSkill()
@@ -304,7 +304,7 @@
local index = i + offset
BenillaSkillFrame_SetRow(i, index, numRows)
if selected == index and index <= numRows then
highlight:SetPoint("TOPLEFT", getglobal("BenillaSkillRankFrame" .. i), "TOPLEFT", -5, 0)
highlight:SetPoint("TOPLEFT", getglobal("SkillRankFrame" .. i), "TOPLEFT", -5, 0)
highlight:Show()
end
end
@@ -317,7 +317,7 @@
allExpanded = false
end
end
local collapseAll = getglobal("BenillaSkillCollapseAllButton")
local collapseAll = getglobal("SkillCollapseAllButton")
if numRows == 0 then
collapseAll:Disable()
else
@@ -370,12 +370,12 @@
-- driven the TrainerFrame.xml BenillaTrainerCollapseAllButton way since this engine's
-- CollapseSkillHeader(0)/ExpandSkillHeader(0) already fold/unfold every group at once).
function BenillaSkillCollapseAllButton_OnClick()
local b = getglobal("BenillaSkillCollapseAllButton")
local b = getglobal("SkillCollapseAllButton")
if b.collapsed then
ExpandSkillHeader(0)
else
CollapseSkillHeader(0)
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaSkillListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("SkillListScrollFrame"), 0)
end
BenillaSkillFrame_Update()
end
@@ -383,27 +383,29 @@
-- The window's wheel handler (bubbled up the parent chain from a row) → one row via the faux
-- kit (TrainerFrame.xml's own note: the engine bubbles a spin up the parent chain).
function BenillaSkillFrame_OnMouseWheel(delta)
BenillaFauxScrollFrame_OnMouseWheel(getglobal("BenillaSkillListScrollFrame"), delta)
BenillaFauxScrollFrame_OnMouseWheel(getglobal("SkillListScrollFrame"), delta)
end
-- ── Lifecycle (ref SkillFrame_OnLoad/_OnShow/_OnEvent, l.4-22, collapsed to the events this
-- page's feed actually fires: CHARACTER_POINTS_CHANGED backs the training-point economy this
-- pane doesn't ship, this file's header comment deviation 1). ─────────────────────────────────
function BenillaSkillFrame_OnLoad(frame)
function SkillFrame_OnLoad()
local frame = this
frame:RegisterEvent("SKILL_LINES_CHANGED")
getglobal("BenillaSkillListScrollFrame").updateFunc = BenillaSkillFrame_Update
getglobal("SkillListScrollFrame").updateFunc = BenillaSkillFrame_Update
-- The description body hangs under the detail bar — anchored here, not in XML,
-- because the bar is a child frame declared after the page's layers and anchors
-- resolve at SetPoint time (an XML forward reference silently lands on the page;
-- the MailFrame error-text OnLoad precedent, ref MailFrame.xml l.1040).
BenillaSkillDetailDescriptionText:SetPoint("TOPLEFT", "BenillaSkillDetailBar", "BOTTOMLEFT", -2, -10)
SkillDetailDescriptionText:SetPoint("TOPLEFT", "SkillDetailBar", "BOTTOMLEFT", -2, -10)
end
function BenillaSkillFrame_OnShow()
function SkillFrame_OnShow()
BenillaSkillFrame_Update()
end
function BenillaSkillFrame_OnEvent(frame)
function SkillFrame_OnEvent(event)
local frame = this
if event == "SKILL_LINES_CHANGED" and frame:IsVisible() then
BenillaSkillFrame_Update()
end
@@ -470,11 +472,11 @@
Reputation (the ref's 3) is not built — the same deviation CharacterFrame.xml's tab row
already flags. `character_tests.rs` pins every page's id against its slot in
BENILLA_CHARACTERFRAME_SUBFRAMES, so the day Reputation lands this is loud, not silent. -->
<Frame name="BenillaSkillFrame" enableMouse="true" parent="BenillaCharacterFrame" hidden="true" id="3">
<Frame name="SkillFrame" enableMouse="true" parent="CharacterFrame" hidden="true" id="3">
<!-- The real 384×512 page. No <Anchors relativeTo> substitute for true parenting exists
(this file's header comment) — pinned to BenillaCharacterFrame's own TOPLEFT by name. -->
(this file's header comment) — pinned to CharacterFrame's own TOPLEFT by name. -->
<Size><AbsDimension x="384" y="512"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCharacterFrame" relativePoint="TOPLEFT"/></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="CharacterFrame" relativePoint="TOPLEFT"/></Anchors>
<!-- Transcribed for fidelity (ref l.131-133); no loader wiring exists yet for the hit-rect
shrink (TradeSkillFrame.xml's own "transcribe the hook even if nothing drives it yet"
posture, TalentFrame.xml's precedent). -->
@@ -506,14 +508,14 @@
last row sits slightly lower than the ref's fixed-slot geometry — a pixel-fit
adjustment, not a structural deviation). -->
<Layer level="ARTWORK">
<Texture name="BenillaSkillHorizontalBarLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Texture name="SkillHorizontalBarLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="256" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-305"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.25"/>
</Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="75" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaSkillHorizontalBarLeft" relativePoint="RIGHT"/></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="SkillHorizontalBarLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.29296875" top="0.25" bottom="0.5"/>
</Texture>
<!-- The detail pane's description body (ref SkillDetailDescriptionText, l.579-591:
@@ -521,9 +523,9 @@
its SkillDetailScrollChildFrame; this page's flat layout (header comment) has
no such child, so it hangs under the detail bar instead — the same pane
position, minus the (never-needed for one sentence) scroll machinery. The
anchor is set in BenillaSkillFrame_OnLoad, NOT here: the bar is a child frame
anchor is set in SkillFrame_OnLoad, NOT here: the bar is a child frame
declared below and an XML forward reference silently lands on the page. -->
<FontString name="BenillaSkillDetailDescriptionText" inherits="GameFontHighlightSmall" justifyH="LEFT" justifyV="TOP" hidden="true">
<FontString name="SkillDetailDescriptionText" inherits="GameFontHighlightSmall" justifyH="LEFT" justifyV="TOP" hidden="true">
<Size><AbsDimension x="275" y="0"/></Size>
</FontString>
</Layer>
@@ -532,11 +534,11 @@
<!-- The scroll-wheel catcher (TrainerFrame.xml's own note) — MUST be first so every real
row below out-ranks it in hit-testing, and sized to the LIST REGION ONLY (rows span
y -79..-295, x 22..309), NEVER the whole page: unlike the trainer's standalone
window, this page is BenillaCharacterFrame's LAST child, so a full-page catcher
window, this page is CharacterFrame's LAST child, so a full-page catcher
out-ranks every EARLIER CharacterFrame child — it ate the tab row and the close X
whenever Skills was up (the 2026-07-17 "can't switch back to Character" report;
character_tests.rs's tab_round_trip_with_a_selected_skill_by_point is the repro). -->
<Button name="BenillaSkillWheelCatcher">
<Button name="SkillWheelCatcher">
<Size><AbsDimension x="290" y="222"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="20" y="-76"/></Offset></Anchor></Anchors>
<Scripts><OnMouseWheel>BenillaSkillFrame_OnMouseWheel(arg1)</OnMouseWheel></Scripts>
@@ -544,18 +546,18 @@
<!-- The expand/collapse-all tab (ref SkillFrameExpandButtonFrame, l.242-317 — the SAME
QuestLogSortTab art TrainerFrame.xml's own BenillaTrainerExpandButtonFrame uses). -->
<Frame name="BenillaSkillExpandButtonFrame">
<Frame name="SkillExpandButtonFrame">
<Size><AbsDimension x="54" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="70" y="-49"/></Offset></Anchor></Anchors>
<Layers><Layer level="BACKGROUND">
<Texture name="BenillaSkillExpandTabLeft" file="Interface\QuestFrame\UI-QuestLogSortTab-Left"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors></Texture>
<Texture name="BenillaSkillExpandTabMiddle" file="Interface\QuestFrame\UI-QuestLogSortTab-Middle"><Size><AbsDimension x="38" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillExpandTabLeft" relativePoint="RIGHT"/></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLogSortTab-Right"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillExpandTabMiddle" relativePoint="RIGHT"/></Anchors></Texture>
<Texture name="SkillExpandTabLeft" file="Interface\QuestFrame\UI-QuestLogSortTab-Left"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors></Texture>
<Texture name="SkillExpandTabMiddle" file="Interface\QuestFrame\UI-QuestLogSortTab-Middle"><Size><AbsDimension x="38" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="SkillExpandTabLeft" relativePoint="RIGHT"/></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLogSortTab-Right"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="SkillExpandTabMiddle" relativePoint="RIGHT"/></Anchors></Texture>
</Layer></Layers>
<Frames>
<Button name="BenillaSkillCollapseAllButton" text="ALL">
<Button name="SkillCollapseAllButton" text="ALL">
<Size><AbsDimension x="40" y="22"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaSkillExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="SkillExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-MinusButton-Up"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></NormalTexture>
<HighlightTexture file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></HighlightTexture>
<ButtonText><Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="18" y="0"/></Offset></Anchor></Anchors></ButtonText>
@@ -568,11 +570,11 @@
<!-- The persistent selection highlight (Trainer/TradeSkillFrame's own precedent, this
file's header comment deviation 7): 281×15, ARTWORK-layer UI-Listbox-Highlight2.
Declared BEFORE the rows so the row text paints OVER the glow (same z-tiebreak). -->
<Frame name="BenillaSkillHighlightFrame" hidden="true">
<Frame name="SkillHighlightFrame" hidden="true">
<Size><AbsDimension x="281" y="15"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers><Layer level="ARTWORK">
<Texture name="BenillaSkillHighlight" file="Interface\Buttons\UI-Listbox-Highlight2">
<Texture name="SkillHighlight" file="Interface\Buttons\UI-Listbox-Highlight2">
<Anchors><Anchor point="TOPLEFT"/><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer></Layers>
@@ -580,44 +582,44 @@
<!-- 12 header-row slots (ref SkillTypeLabel1..12, l.358-465): 285×14, row1 LEFT of
TOPLEFT(22,-86); rows 2-12 chain by (0,-18). -->
<Button name="BenillaSkillTypeLabel1" inherits="BenillaSkillHeaderTemplate">
<Button name="SkillTypeLabel1" inherits="BenillaSkillHeaderTemplate">
<Anchors><Anchor point="LEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-86"/></Offset></Anchor></Anchors>
</Button>
<Button name="BenillaSkillTypeLabel2" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel1"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel3" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel2"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel4" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel3"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel5" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel4"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel6" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel5"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel7" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel6"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel8" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel7"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel9" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel8"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel10" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel9"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel11" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel10"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="BenillaSkillTypeLabel12" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="BenillaSkillTypeLabel11"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel2" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel1"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel3" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel2"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel4" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel3"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel5" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel4"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel6" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel5"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel7" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel6"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel8" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel7"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel9" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel8"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel10" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel9"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel11" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel10"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<Button name="SkillTypeLabel12" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel11"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
<!-- 12 rank-bar slots (ref SkillRankFrame1..12, l.746-853): 271×15, row1 TOPLEFT(38,-79);
rows 2-12 chain BOTTOMLEFT→TOPLEFT by (0,-3) — the SAME 18px pitch as the label chain
(15 bar height + 3 gap). -->
<StatusBar name="BenillaSkillRankFrame1" inherits="BenillaSkillRankTemplate">
<StatusBar name="SkillRankFrame1" inherits="BenillaSkillRankTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="38" y="-79"/></Offset></Anchor></Anchors>
</StatusBar>
<StatusBar name="BenillaSkillRankFrame2" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame3" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame4" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame5" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame6" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame7" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame8" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame9" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame8" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame10" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame11" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame10" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="BenillaSkillRankFrame12" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSkillRankFrame11" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame2" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame3" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame4" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame5" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame6" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame7" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame8" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame9" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame8" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame10" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame11" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame10" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<StatusBar name="SkillRankFrame12" inherits="BenillaSkillRankTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame11" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
<!-- The faux-scroll frame (BenillaFauxScrollFrameTemplate, ScrollTemplates.xml): invisible
<!-- The faux-scroll frame (FauxScrollFrameTemplate, ScrollTemplates.xml): invisible
container over the 12 rows; only its right-hand Slider scroll bar renders. Sized to the
12-row span (SKILLFRAME_SKILLS_DISPLAYED × 18px pitch), TrainerFrame.xml's own
same-TOPLEFT-as-row1 convention. updateFunc set in BenillaSkillFrame_OnLoad. -->
<Frame name="BenillaSkillListScrollFrame" inherits="BenillaFauxScrollFrameTemplate">
same-TOPLEFT-as-row1 convention. updateFunc set in SkillFrame_OnLoad. -->
<Frame name="SkillListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="216"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-79"/></Offset></Anchor></Anchors>
</Frame>
@@ -626,7 +628,7 @@
skill's own rank StatusBar + name — no description/abandon/spend row. Reuses the SAME
rank template the list rows use (its Border click is harmless here: no .skillIndex is
ever set on this instance, so BenillaSkillRankFrame_OnClick's guard no-ops it). -->
<StatusBar name="BenillaSkillDetailBar" inherits="BenillaSkillRankTemplate" hidden="true">
<StatusBar name="SkillDetailBar" inherits="BenillaSkillRankTemplate" hidden="true">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="38" y="-325"/></Offset></Anchor></Anchors>
<Frames>
<!-- The unlearn (abandon) button (ref $parentUnlearnButton, ref SkillFrame.xml
@@ -635,10 +637,10 @@
transcribed-inert (no loader wiring — CharacterFrame.xml's engine-gap note).
Handlers are chunk functions (the chunk-boundary rule — OnEnter reads the
chunk-local UNLEARN_SKILL_TOOLTIP). -->
<Button name="BenillaSkillDetailUnlearnButton" hidden="true">
<Button name="SkillDetailUnlearnButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaSkillDetailBarBorder" relativePoint="RIGHT">
<Anchor point="LEFT" relativeTo="SkillDetailBarBorder" relativePoint="RIGHT">
<Offset><AbsDimension x="-2" y="-1"/></Offset>
</Anchor>
</Anchors>
@@ -656,9 +658,9 @@
</StatusBar>
</Frames>
<Scripts>
<OnLoad>BenillaSkillFrame_OnLoad(self)</OnLoad>
<OnShow>BenillaSkillFrame_OnShow()</OnShow>
<OnEvent>BenillaSkillFrame_OnEvent(self)</OnEvent>
<OnLoad>SkillFrame_OnLoad()</OnLoad>
<OnShow>SkillFrame_OnShow()</OnShow>
<OnEvent>SkillFrame_OnEvent(event)</OnEvent>
<OnMouseWheel>BenillaSkillFrame_OnMouseWheel(arg1)</OnMouseWheel>
</Scripts>
</Frame>
+181 -171
View File
@@ -8,16 +8,17 @@
line cites below are into that extraction's own `ref-SpellBookFrame.xml`/`.lua`).
Docks the left UIPanel slot exactly like Character/Merchant/Gossip/Quest/QuestLog
(`UiPanels.xml`'s `UIPanelWindows["BenillaSpellBookFrame"] = { area = "left", pushable =
(`UiPanels.xml`'s `UIPanelWindows["SpellBookFrame"] = { area = "left", pushable =
0 }`, mirroring the ref's own `SpellBookFrame` row) — so it competes for/replaces those
windows, never stacks with them. `ToggleSpellBook(bookType)` (ref `SpellBookFrame.lua:
9-50`, kept BARE — not Benilla-prefixed — matching this codebase's own `ToggleCharacter`/
9-50`) is the addon-facing entry point; like every frame name here it is the reference's own
`ToggleQuestLog` precedent) is the show/hide entry point; the app's 'P' binding calls it
with `BOOKTYPE_SPELL` (`ui_script/input.rs`, ref `Bindings.xml:582-583`).
ONE deliberate naming deviation from the ref, applied consistently: every frame this file
defines is Benilla-prefixed (`BenillaSpellBookFrame`, `BenillaSpellButton1`, …) — the
same posture every other window in this codebase already takes.
Every frame this file defines wears the REFERENCE's name (`SpellBookFrame`, `SpellButton1`,
…), because addons address them by it — the `Benilla` prefix these carried until the
reference-naming pass is gone. A child with no counterpart in the 1.12 namespace keeps the
bare stripped name so `$parent`/`GetName()..` composition inside the family stays coherent.
The book MODEL (tabs = skill lines, the flat slot list, offsets) is built entirely by the
app from `PlayerActions.spells` through the `Spell.dbc`/`SkillLine.dbc` join
@@ -36,7 +37,7 @@
`crate::ui_pet_book`'s feed of `SMSG_PET_SPELLS`' own spell list. Two reference QUIRKS are
reproduced on purpose here and each carries a do-not-fix note at its site (the standing
rule from decision 1030): the pet page **ignores its page number**, and its page arrows
write the *spell* book's page counter. See `BenillaSpellBook_GetSpellID` and the two
write the *spell* book's page counter. See `SpellBook_GetSpellID` and the two
page-turn handlers.
Named deferrals/OMITs (each re-cited at its own site below):
@@ -96,7 +97,7 @@
-- codebase's own ToggleCharacter/ToggleQuestLog precedent. The ref's own DUPLICATED
-- prev/next enable/disable (once inside the `currentPage > maxPages` clamp branch, once
-- again unconditionally right after — both produce the identical end state) collapses to
-- the ONE BenillaSpellBook_UpdatePageArrows() call already shared by every other
-- the ONE SpellBook_UpdatePageArrows() call already shared by every other
-- page-turn site below — a redundant-code cleanup, not a behavior change.
function ToggleSpellBook(bookType)
-- ref l.11-14: asking for a pet book you haven't got does NOTHING — not even close a
@@ -104,29 +105,30 @@
if bookType == BOOKTYPE_PET and not HasPetSpells() then
return
end
local isVisible = BenillaSpellBookFrame:IsVisible()
HideUIPanel(BenillaSpellBookFrame)
if (not isVisible or (BenillaSpellBookFrame.bookType ~= bookType)) then
BenillaSpellBookFrame.bookType = bookType
ShowUIPanel(BenillaSpellBookFrame)
local isVisible = SpellBookFrame:IsVisible()
HideUIPanel(SpellBookFrame)
if (not isVisible or (SpellBookFrame.bookType ~= bookType)) then
SpellBookFrame.bookType = bookType
ShowUIPanel(SpellBookFrame)
end
BenillaSpellBook_ClampPage()
BenillaSpellBook_UpdatePageArrows()
BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, BenillaSpellBook_GetCurrentPage()))
SpellBook_UpdatePageArrows()
SpellBookPageText:SetText(format(PAGE_NUMBER, SpellBook_GetCurrentPage()))
end
-- Clamp the selected tab's page back onto range (ref's own inline logic inside
-- ToggleSpellBook, l.22-26) — split out so both ToggleSpellBook and a future re-entry
-- share it, rather than inlining it twice.
function BenillaSpellBook_ClampPage()
local currentPage, maxPages = BenillaSpellBook_GetCurrentPage()
local currentPage, maxPages = SpellBook_GetCurrentPage()
if currentPage > maxPages then
SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] = maxPages
SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] = maxPages
BenillaUpdateSpells()
end
end
function BenillaSpellBookFrame_OnLoad(frame)
function SpellBookFrame_OnLoad()
local frame = this
frame:RegisterEvent("SPELLS_CHANGED")
-- LEARNED_SPELL_IN_TAB OMITTED — this file's header comment.
frame.bookType = BOOKTYPE_SPELL
@@ -134,45 +136,46 @@
SPELLBOOK_PAGENUMBERS[i] = 1
end
SPELLBOOK_PAGENUMBERS["pet"] = 1 -- ref l.66; read by GetCurrentPage, written by nothing
BenillaSpellBookSkillLineTab_OnClick(1)
SpellBookSkillLineTab_OnClick(1)
end
-- ref SpellBookFrame_OnEvent (l.75-92), the SPELLS_CHANGED branch only (LEARNED_SPELL_IN_TAB
-- OMITTED, header comment).
function BenillaSpellBookFrame_OnEvent(frame)
function SpellBookFrame_OnEvent()
local frame = this
if event == "SPELLS_CHANGED" and frame:IsVisible() then
BenillaSpellBookFrame_Update()
BenillaSpellBook_UpdatePageArrows()
SpellBookFrame_Update()
SpellBook_UpdatePageArrows()
end
end
-- ref SpellBookFrame_OnShow (l.94-105): MultiActionBar_ShowAllGrids OMITTED (header
-- comment); the tab-flash UIFrameFlash call goes with LEARNED_SPELL_IN_TAB.
-- UpdateMicroButtons is LIVE as of the micro-menu slice (MicroMenu.xml).
function BenillaSpellBookFrame_OnShow()
function SpellBookFrame_OnShow()
if UpdateMicroButtons then UpdateMicroButtons() end
BenillaSpellBookFrame_Update(1)
SpellBookFrame_Update(1)
end
-- ref SpellBookFrame_Update (l.107-164), whole. Three things happen here and they are
-- easy to conflate: the skill-line strip is gated on the book TYPE (the pet book has no
-- tabs at all — GetSpellTabInfo never sees it), the Spell/Pet toggle row is gated on
-- HasPetSpells(), and the title + open sound fork on which book is up.
function BenillaSpellBookFrame_Update(showing)
function SpellBookFrame_Update(showing)
if showing then
BenillaSpellBookSkillLineTab_OnClick(BenillaSpellBookFrame.selectedSkillLine)
SpellBookSkillLineTab_OnClick(SpellBookFrame.selectedSkillLine)
BenillaUpdateSpells()
end
local isPet = BenillaSpellBookFrame.bookType == BOOKTYPE_PET
local isPet = SpellBookFrame.bookType == BOOKTYPE_PET
local numSkillLineTabs = GetNumSpellTabs()
for i = 1, MAX_SKILLLINE_TABS do
local skillLineTab = getglobal("BenillaSpellBookSkillLineTab" .. i)
local skillLineTab = getglobal("SpellBookSkillLineTab" .. i)
if i <= numSkillLineTabs and not isPet then
local name, texture = GetSpellTabInfo(i)
skillLineTab:SetNormalTexture(texture)
skillLineTab.tooltip = name
skillLineTab:Show()
skillLineTab:SetChecked(BenillaSpellBookFrame.selectedSkillLine == i)
skillLineTab:SetChecked(SpellBookFrame.selectedSkillLine == i)
else
skillLineTab:Hide()
end
@@ -181,26 +184,26 @@
-- ref l.108-111 + l.141-151: hide the row first, then raise exactly the tabs that
-- exist. Tab 3 never appears in 1.12 (no third book) and is hidden by the template's
-- own `hidden="true"`; it is kept so the anchor chain reads like the ref's.
BenillaSpellBookFrameTabButton1:Hide()
BenillaSpellBookFrameTabButton2:Hide()
SpellBookFrameTabButton1:Hide()
SpellBookFrameTabButton2:Hide()
local hasPetSpells, petToken = HasPetSpells()
BenillaSpellBookFrame.petTitle = nil
SpellBookFrame.petTitle = nil
if hasPetSpells then
BenillaSpellBookFrame_SetTabType(BenillaSpellBookFrameTabButton1, BOOKTYPE_SPELL)
BenillaSpellBookFrame_SetTabType(BenillaSpellBookFrameTabButton2, BOOKTYPE_PET, petToken)
SpellBookFrame_SetTabType(SpellBookFrameTabButton1, BOOKTYPE_SPELL)
SpellBookFrame_SetTabType(SpellBookFrameTabButton2, BOOKTYPE_PET, petToken)
elseif isPet then
-- The pet died/was dismissed while its book was open: the ref closes the window
-- and reverts, rather than showing an empty pet page (l.147-151).
HideUIPanel(BenillaSpellBookFrame)
BenillaSpellBookFrame.bookType = BOOKTYPE_SPELL
HideUIPanel(SpellBookFrame)
SpellBookFrame.bookType = BOOKTYPE_SPELL
isPet = false
end
if not isPet then
BenillaSpellBookTitleText:SetText(SPELLBOOK)
SpellBookTitleText:SetText(SPELLBOOK)
if showing then PlaySound("igSpellBookOpen") end
else
BenillaSpellBookTitleText:SetText(BenillaSpellBookFrame.petTitle)
SpellBookTitleText:SetText(SpellBookFrame.petTitle)
-- ref l.161: the pet book opens on the ABILITY kit, not the spellbook one. Both
-- ship in the real SoundEntries.dbc.
if showing then PlaySound("igAbilityOpen") end
@@ -210,7 +213,7 @@
-- ref SpellBookFrame_SetTabType (l.166-183). `binding` is what the tab's tooltip appends
-- the bound key to (MicroButtonTooltipText); `petTitle` is set here as a side effect,
-- exactly as the ref does, because this is the only place the token is in hand.
function BenillaSpellBookFrame_SetTabType(tabButton, bookType, token)
function SpellBookFrame_SetTabType(tabButton, bookType, token)
if bookType == BOOKTYPE_SPELL then
tabButton.bookType = BOOKTYPE_SPELL
tabButton:SetText(SPELLBOOK)
@@ -220,12 +223,12 @@
local label = getglobal("PET_TYPE_" .. (token or "PET")) or PET_TYPE_PET
tabButton:SetText(label)
tabButton.binding = "TOGGLEPETBOOK"
BenillaSpellBookFrame.petTitle = label
SpellBookFrame.petTitle = label
end
-- The tab for the book you are ON is disabled, which is also what draws it as the
-- selected (Tab1-Selected) plate — the ref uses <DisabledTexture> for the "current"
-- look rather than a checked state.
if BenillaSpellBookFrame.bookType == bookType then
if SpellBookFrame.bookType == bookType then
tabButton:Disable()
else
tabButton:Enable()
@@ -244,16 +247,16 @@
frame.clock = frame.clock - BENILLA_PET_AUTOCAST_PERIOD
end
for i = 1, SPELLS_PER_PAGE do
BenillaPetAutocast_Update(getglobal("BenillaSpellButton" .. i), frame.clock)
BenillaPetAutocast_Update(getglobal("SpellButton" .. i), frame.clock)
end
end
-- ref SpellBookFrame_OnHide (l.186-204): MultiActionBar_HideAllGrids and the tab-flash
-- cleanup OMITTED (header comment); always the spell-book close kit (no pet book to branch
-- on). UpdateMicroButtons is LIVE as of the micro-menu slice (MicroMenu.xml).
function BenillaSpellBookFrame_OnHide()
function SpellBookFrame_OnHide()
if UpdateMicroButtons then UpdateMicroButtons() end
if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
if SpellBookFrame.bookType == BOOKTYPE_PET then
PlaySound("igAbilityClose") -- ref l.191
else
PlaySound("igSpellBookClose")
@@ -266,7 +269,7 @@
-- page (this file's own convention: explicit button params, not ambient `this`).
function BenillaUpdateSpells()
for i = 1, SPELLS_PER_PAGE do
BenillaSpellButton_UpdateButton(getglobal("BenillaSpellButton" .. i))
BenillaSpellButton_UpdateButton(getglobal("SpellButton" .. i))
end
end
@@ -277,7 +280,8 @@
-- PET_BAR_UPDATE is LIVE (ref l.214 + l.227-231) and it is the pet page's whole repaint
-- wire: the pet book's cooldowns, autocast bits and active ring all move without the BOOK
-- changing, so SPELLS_CHANGED alone would leave a stale page.
function BenillaSpellButton_OnLoad(button)
function SpellButton_OnLoad()
local button = this
button:RegisterEvent("SPELLS_CHANGED")
button:RegisterEvent("CURRENT_SPELL_CAST_CHANGED")
button:RegisterEvent("SPELL_UPDATE_COOLDOWN")
@@ -293,13 +297,14 @@
BenillaSpellButton_UpdateButton(button)
end
function BenillaSpellButton_OnEvent(button)
function SpellButton_OnEvent(event)
local button = this
if event == "SPELLS_CHANGED" or event == "SPELL_UPDATE_COOLDOWN" then
BenillaSpellButton_UpdateButton(button)
elseif event == "CURRENT_SPELL_CAST_CHANGED" then
BenillaSpellButton_UpdateSelection(button)
elseif event == "PET_BAR_UPDATE" then
if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
if SpellBookFrame.bookType == BOOKTYPE_PET then
BenillaSpellButton_UpdateButton(button)
end
end
@@ -312,8 +317,8 @@
-- The ref's out-of-tab bounds fork collapses into the one SetChecked: IsCurrentCast
-- answers nil past the book's end already.
function BenillaSpellButton_UpdateSelection(button)
local id = BenillaSpellBook_GetSpellID(button:GetID())
button:SetChecked(IsCurrentCast(id, BenillaSpellBookFrame.bookType))
local id = SpellBook_GetSpellID(button:GetID())
button:SetChecked(IsCurrentCast(id, SpellBookFrame.bookType))
end
-- ref SpellButton_OnEnter (l.235-244): the engine's SetSpell — the byte-verified spell
@@ -322,12 +327,12 @@
-- verified showRank=0).
function BenillaSpellButton_OnEnter(button)
if not GameTooltip then return end -- engine tests load file subsets
local id = BenillaSpellBook_GetSpellID(button:GetID())
if not GetSpellName(id, BenillaSpellBookFrame.bookType) then
local id = SpellBook_GetSpellID(button:GetID())
if not GetSpellName(id, SpellBookFrame.bookType) then
return
end
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
GameTooltip:SetSpell(id, BenillaSpellBookFrame.bookType)
GameTooltip:SetSpell(id, SpellBookFrame.bookType)
end
-- ref SpellButton_OnClick (l.263-290): the MacroFrame branch and the right-click pet
@@ -336,17 +341,22 @@
-- `id > MAX_SPELLS` guard is dropped too: GetSpellName/CastSpell/PickupSpell already
-- answer nil/no-op for any id past the book's real length, so the extra guard checks
-- nothing a MAX_SPELLS constant would add here.
function BenillaSpellButton_OnClick(button, drag, mouseButton)
local id = BenillaSpellBook_GetSpellID(button:GetID())
function SpellButton_OnClick(drag)
local button = this
-- The mouse button is the ambient `arg1`, exactly as the reference reads it (l.284) —
-- its XML passes nothing on OnClick and a bare `1` on the two drag slots. Only the
-- `drag` falsy arm below reaches it, so the drag slots never read a stale arg1.
local mouseButton = arg1
local id = SpellBook_GetSpellID(button:GetID())
button:SetChecked("false")
if drag then
PickupSpell(id, BenillaSpellBookFrame.bookType)
PickupSpell(id, SpellBookFrame.bookType)
elseif not IsShiftKeyDown() and mouseButton and mouseButton ~= "LeftButton"
and BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
and SpellBookFrame.bookType == BOOKTYPE_PET then
-- ref l.284-285: on the PET page a non-left click flips autocast instead of
-- casting. Shift still outranks it — the ref tests shift first, so a shift
-- right-click on a pet spell picks it up.
ToggleSpellAutocast(id, BenillaSpellBookFrame.bookType)
ToggleSpellAutocast(id, SpellBookFrame.bookType)
elseif IsShiftKeyDown() then
-- ref SpellButton_OnClick l.271-283 (LIVE since decision 0983): with the macro
-- editor open a shift-click APPENDS `/cast <name>[(<rank>)]` to the body instead of
@@ -355,8 +365,8 @@
-- The `/cast` alias comes from the shipped strings (SLASH_CAST1), never typed here.
local macroFrame = getglobal("BenillaMacroFrame")
if macroFrame and macroFrame:IsVisible() then
local spellName, subSpellName = GetSpellName(id, BenillaSpellBookFrame.bookType)
if spellName and not IsSpellPassive(id, BenillaSpellBookFrame.bookType) then
local spellName, subSpellName = GetSpellName(id, SpellBookFrame.bookType)
if spellName and not IsSpellPassive(id, SpellBookFrame.bookType) then
local line = (SLASH_CAST1 or "/cast") .. " " .. spellName
if subSpellName and string.len(subSpellName) > 0 then
line = line .. "(" .. subSpellName .. ")"
@@ -364,10 +374,10 @@
BenillaMacroFrame_AddMacroLine(line)
end
else
PickupSpell(id, BenillaSpellBookFrame.bookType)
PickupSpell(id, SpellBookFrame.bookType)
end
else
CastSpell(id, BenillaSpellBookFrame.bookType)
CastSpell(id, SpellBookFrame.bookType)
BenillaSpellButton_UpdateSelection(button) -- ref l.288: the ring re-reads at once
end
end
@@ -383,15 +393,15 @@
if GameTooltip:IsOwned(button) then
BenillaSpellButton_OnEnter(button)
end
if not BenillaSpellBookFrame.selectedSkillLine then
BenillaSpellBookFrame.selectedSkillLine = 1
if not SpellBookFrame.selectedSkillLine then
SpellBookFrame.selectedSkillLine = 1
end
local isPet = BenillaSpellBookFrame.bookType == BOOKTYPE_PET
local temp, tabTexture, offset, numSpells = GetSpellTabInfo(BenillaSpellBookFrame.selectedSkillLine)
local isPet = SpellBookFrame.bookType == BOOKTYPE_PET
local temp, tabTexture, offset, numSpells = GetSpellTabInfo(SpellBookFrame.selectedSkillLine)
offset = offset or 0
numSpells = numSpells or 0
BenillaSpellBookFrame.selectedSkillLineOffset = offset
local id = BenillaSpellBook_GetSpellID(button:GetID())
SpellBookFrame.selectedSkillLineOffset = offset
local id = SpellBook_GetSpellID(button:GetID())
local name = button:GetName()
local iconTexture = getglobal(name .. "IconTexture")
local spellString = getglobal(name .. "SpellName")
@@ -416,7 +426,7 @@
else
button:Enable()
end
local texture = GetSpellTexture(id, BenillaSpellBookFrame.bookType)
local texture = GetSpellTexture(id, SpellBookFrame.bookType)
local highlightTexture = getglobal(name .. "Highlight")
local normalTexture = getglobal(name .. "NormalTexture")
if (not texture) or (texture == "") then
@@ -434,7 +444,7 @@
-- The cooldown sweep + the enable-keyed icon dim (ref l.359-365): enable == 0 is an
-- on-hold cooldown (Stealth/Feign Death parked until SMSG_COOLDOWN_EVENT) — the
-- widget stays hidden (CooldownFrame_SetTimer's own gate) and the icon reads 40%.
local start, duration, enable = GetSpellCooldown(id, BenillaSpellBookFrame.bookType)
local start, duration, enable = GetSpellCooldown(id, SpellBookFrame.bookType)
CooldownFrame_SetTimer(cooldown, start, duration, enable)
if enable == 1 then
iconTexture:SetVertexColor(1.0, 1.0, 1.0)
@@ -444,11 +454,11 @@
-- ref l.367-377: the autocast pair. `GetSpellAutocast` answers (nil, nil) for the
-- player book by the engine's own short-circuit, so this needs no isPet guard — the
-- overlay and the sparkle simply never come on outside the pet page.
local autoCastAllowed, autoCastEnabled = GetSpellAutocast(id, BenillaSpellBookFrame.bookType)
local autoCastAllowed, autoCastEnabled = GetSpellAutocast(id, SpellBookFrame.bookType)
if autoCastAllowed then autocastable:Show() else autocastable:Hide() end
button.autocasting = autoCastEnabled
local spellName, subSpellName = GetSpellName(id, BenillaSpellBookFrame.bookType)
local isPassive = IsSpellPassive(id, BenillaSpellBookFrame.bookType)
local spellName, subSpellName = GetSpellName(id, SpellBookFrame.bookType)
local isPassive = IsSpellPassive(id, SpellBookFrame.bookType)
if isPassive then
normalTexture:SetVertexColor(0, 0, 0)
highlightTexture:SetTexture("Interface\\Buttons\\UI-PassiveHighlight")
@@ -479,47 +489,47 @@
-- reads `SPELLBOOK_PAGENUMBERS["pet"]` for the pet one — a key nothing writes. So on the
-- pet page the arrows turn nothing, the page text says a number the page does not honour,
-- and the *spell* book's page counter moves behind your back. All three are the reference's
-- and all three are reproduced; `BenillaSpellBook_GetSpellID`'s note is the other half.
-- and all three are reproduced; `SpellBook_GetSpellID`'s note is the other half.
function BenillaSpellBookPrevPageButton_OnClick()
local pageNum = BenillaSpellBook_GetCurrentPage() - 1
SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] = pageNum
BenillaSpellBook_UpdatePageArrows()
BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
local pageNum = SpellBook_GetCurrentPage() - 1
SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] = pageNum
SpellBook_UpdatePageArrows()
SpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
BenillaUpdateSpells()
-- ref l.414-417: the pet page re-asserts its own title, because the spell book's page
-- text handler is shared and the title is not.
if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
BenillaSpellBookTitleText:SetText(BenillaSpellBookFrame.petTitle)
if SpellBookFrame.bookType == BOOKTYPE_PET then
SpellBookTitleText:SetText(SpellBookFrame.petTitle)
end
PlaySound("igAbiliityPageTurn")
end
function BenillaSpellBookNextPageButton_OnClick()
local pageNum = BenillaSpellBook_GetCurrentPage() + 1
SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] = pageNum
BenillaSpellBook_UpdatePageArrows()
BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
local pageNum = SpellBook_GetCurrentPage() + 1
SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] = pageNum
SpellBook_UpdatePageArrows()
SpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
BenillaUpdateSpells()
if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
BenillaSpellBookTitleText:SetText(BenillaSpellBookFrame.petTitle)
if SpellBookFrame.bookType == BOOKTYPE_PET then
SpellBookTitleText:SetText(SpellBookFrame.petTitle)
end
PlaySound("igAbiliityPageTurn")
end
-- ── The skill-line tabs (ref SpellBookSkillLineTab_OnClick, l.436-457) ───────────────────
function BenillaSpellBookSkillLineTab_OnClick(id)
function SpellBookSkillLineTab_OnClick(id)
local update
if not id then
update = 1
id = this:GetID()
end
BenillaSpellBookFrame.selectedSkillLine = id
SpellBookFrame.selectedSkillLine = id
local name, texture, offset, numSpells = GetSpellTabInfo(id)
BenillaSpellBookFrame.selectedSkillLineOffset = offset or 0
BenillaSpellBookFrame.selectedSkillLineNumSpells = numSpells or 0
BenillaSpellBook_UpdatePageArrows()
BenillaSpellBookFrame_Update()
BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, BenillaSpellBook_GetCurrentPage()))
SpellBookFrame.selectedSkillLineOffset = offset or 0
SpellBookFrame.selectedSkillLineNumSpells = numSpells or 0
SpellBook_UpdatePageArrows()
SpellBookFrame_Update()
SpellBookPageText:SetText(format(PAGE_NUMBER, SpellBook_GetCurrentPage()))
if update then
BenillaUpdateSpells()
end
@@ -533,26 +543,26 @@
-- always shows book entries 1..12 no matter what page number the window is displaying. A
-- well-trained hunter pet can know more than twelve spells, so this is visible, and it is
-- still the reference's behaviour. Its sibling quirk is in the two page-turn handlers.
function BenillaSpellBook_GetSpellID(id)
if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
function SpellBook_GetSpellID(id)
if SpellBookFrame.bookType == BOOKTYPE_PET then
return id
end
return id + (BenillaSpellBookFrame.selectedSkillLineOffset or 0)
+ (SPELLS_PER_PAGE * ((SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] or 1) - 1))
return id + (SpellBookFrame.selectedSkillLineOffset or 0)
+ (SPELLS_PER_PAGE * ((SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] or 1) - 1))
end
-- ref SpellBook_UpdatePageArrows (l.467-479).
function BenillaSpellBook_UpdatePageArrows()
local currentPage, maxPages = BenillaSpellBook_GetCurrentPage()
function SpellBook_UpdatePageArrows()
local currentPage, maxPages = SpellBook_GetCurrentPage()
if currentPage == 1 then
BenillaSpellBookPrevPageButton:Disable()
SpellBookPrevPageButton:Disable()
else
BenillaSpellBookPrevPageButton:Enable()
SpellBookPrevPageButton:Enable()
end
if currentPage == maxPages then
BenillaSpellBookNextPageButton:Disable()
SpellBookNextPageButton:Disable()
else
BenillaSpellBookNextPageButton:Enable()
SpellBookNextPageButton:Enable()
end
end
@@ -564,15 +574,15 @@
-- The pet branch reads `SPELLBOOK_PAGENUMBERS["pet"]`, which is the counter NOTHING ever
-- writes — see the page-turn handlers. So it is always 1, and with it `maxPages > 1` the
-- Next arrow stays enabled forever on a big pet book. Reference behaviour; see the
-- do-not-fix note on `BenillaSpellBook_GetSpellID`.
function BenillaSpellBook_GetCurrentPage()
-- do-not-fix note on `SpellBook_GetSpellID`.
function SpellBook_GetCurrentPage()
local numPetSpells = HasPetSpells()
if numPetSpells and BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
if numPetSpells and SpellBookFrame.bookType == BOOKTYPE_PET then
return SPELLBOOK_PAGENUMBERS["pet"] or 1,
max(ceil(numPetSpells / SPELLS_PER_PAGE), 1)
end
local currentPage = SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] or 1
local temp, texture, offset, numSpells = GetSpellTabInfo(BenillaSpellBookFrame.selectedSkillLine)
local currentPage = SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] or 1
local temp, texture, offset, numSpells = GetSpellTabInfo(SpellBookFrame.selectedSkillLine)
local maxPages = max(ceil((numSpells or 0) / SPELLS_PER_PAGE), 1)
return currentPage, maxPages
end
@@ -601,7 +611,7 @@
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\CheckButtonHilight" alphaMode="ADD"/>
<Scripts>
<OnClick>BenillaSpellBookSkillLineTab_OnClick()</OnClick>
<OnClick>SpellBookSkillLineTab_OnClick()</OnClick>
<OnEnter>
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
if self.tooltip then
@@ -671,14 +681,14 @@
</Cooldown>
</Frames>
<Scripts>
<OnLoad>BenillaSpellButton_OnLoad(self)</OnLoad>
<OnEvent>BenillaSpellButton_OnEvent(self)</OnEvent>
<OnClick>BenillaSpellButton_OnClick(self, nil, arg1)</OnClick>
<OnLoad>SpellButton_OnLoad()</OnLoad>
<OnEvent>SpellButton_OnEvent(event)</OnEvent>
<OnClick>SpellButton_OnClick()</OnClick>
<OnShow>BenillaSpellButton_UpdateButton(self)</OnShow>
<!-- The reference routes both drag entries to the plain click path with drag=1
(SpellButtonTemplate l.169-174) — the bag/doll/action slots' own precedent. -->
<OnDragStart>BenillaSpellButton_OnClick(self, 1)</OnDragStart>
<OnReceiveDrag>BenillaSpellButton_OnClick(self, 1)</OnReceiveDrag>
<OnDragStart>SpellButton_OnClick(1)</OnDragStart>
<OnReceiveDrag>SpellButton_OnClick(1)</OnReceiveDrag>
<OnEnter>BenillaSpellButton_OnEnter(self)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
@@ -726,7 +736,7 @@
<HighlightTexture file="Interface\SpellBook\UI-SpellbookPanel-Tab-Highlight" alphaMode="ADD"/>
</Button>
<Frame name="BenillaSpellBookFrame" hidden="true">
<Frame name="SpellBookFrame" hidden="true">
<!-- The real 384x512 window (ref l.202-205). No <Anchors> — ShowUIPanel/SetLeftFrame
SetPoints this to the left slot on open (UiPanels.xml's own convention). -->
<Size><AbsDimension x="384" y="512"/></Size>
@@ -762,12 +772,12 @@
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<FontString name="BenillaSpellBookTitleText" inherits="GameFontNormal" text="SPELLBOOK">
<FontString name="SpellBookTitleText" inherits="GameFontNormal" text="SPELLBOOK">
<Anchors>
<Anchor point="CENTER"><Offset><AbsDimension x="6" y="230"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="BenillaSpellBookPageText" inherits="GameFontNormal">
<FontString name="SpellBookPageText" inherits="GameFontNormal">
<Size><AbsDimension x="102" y="0"/></Size>
<Anchors>
<Anchor point="BOTTOM"><Offset><AbsDimension x="-14" y="96"/></Offset></Anchor>
@@ -779,53 +789,53 @@
<!-- Close: the house minimize-button art (no UIPanelCloseButton template ships —
Gossip/Bag/Merchant/CharacterFrame's own precedent), the same TOPRIGHT(-44,-25)
position as every other window's close button. -->
<Button name="BenillaSpellBookFrameCloseButton">
<Button name="SpellBookFrameCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-44" y="-25"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts>
<OnClick>HideUIPanel(BenillaSpellBookFrame)</OnClick>
<OnClick>HideUIPanel(SpellBookFrame)</OnClick>
</Scripts>
</Button>
<!-- ref l.288-317. Tab 3 exists in the ref's own XML and is never raised in 1.12
(there is no third book); kept so the anchor chain reads like the ref's. -->
<Button name="BenillaSpellBookFrameTabButton1" inherits="BenillaSpellBookFrameTabButtonTemplate">
<Button name="SpellBookFrameTabButton1" inherits="BenillaSpellBookFrameTabButtonTemplate">
<Anchors>
<Anchor point="CENTER" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="79" y="61"/></Offset></Anchor>
</Anchors>
<DisabledTexture file="Interface\SpellBook\UI-SpellBook-Tab1-Selected"/>
</Button>
<Button name="BenillaSpellBookFrameTabButton2" inherits="BenillaSpellBookFrameTabButtonTemplate">
<Button name="SpellBookFrameTabButton2" inherits="BenillaSpellBookFrameTabButtonTemplate">
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaSpellBookFrameTabButton1" relativePoint="RIGHT">
<Anchor point="LEFT" relativeTo="SpellBookFrameTabButton1" relativePoint="RIGHT">
<Offset><AbsDimension x="-20" y="0"/></Offset>
</Anchor>
</Anchors>
<DisabledTexture file="Interface\SpellBook\UI-SpellBook-Tab1-Selected"/>
</Button>
<Button name="BenillaSpellBookFrameTabButton3" inherits="BenillaSpellBookFrameTabButtonTemplate">
<Button name="SpellBookFrameTabButton3" inherits="BenillaSpellBookFrameTabButtonTemplate">
<Anchors>
<Anchor point="LEFT" relativeTo="BenillaSpellBookFrameTabButton2" relativePoint="RIGHT">
<Anchor point="LEFT" relativeTo="SpellBookFrameTabButton2" relativePoint="RIGHT">
<Offset><AbsDimension x="-20" y="0"/></Offset>
</Anchor>
</Anchors>
<DisabledTexture file="Interface\SpellBook\UI-SpellBook-Tab3-Selected"/>
</Button>
<Button name="BenillaSpellBookPrevPageButton">
<Button name="SpellBookPrevPageButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativeTo="BenillaSpellBookFrame" relativePoint="BOTTOMLEFT">
<Anchor point="CENTER" relativeTo="SpellBookFrame" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="50" y="105"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString inherits="GameFontNormal" justifyH="LEFT" text="PREV">
<Anchors><Anchor point="LEFT" relativeTo="BenillaSpellBookPrevPageButton" relativePoint="RIGHT"/></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="SpellBookPrevPageButton" relativePoint="RIGHT"/></Anchors>
</FontString>
</Layer>
</Layers>
@@ -835,17 +845,17 @@
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="BenillaSpellBookNextPageButton">
<Button name="SpellBookNextPageButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativeTo="BenillaSpellBookFrame" relativePoint="BOTTOMLEFT">
<Anchor point="CENTER" relativeTo="SpellBookFrame" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="314" y="105"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString inherits="GameFontNormal" justifyH="RIGHT" text="NEXT">
<Anchors><Anchor point="RIGHT" relativeTo="BenillaSpellBookNextPageButton" relativePoint="LEFT"/></Anchors>
<Anchors><Anchor point="RIGHT" relativeTo="SpellBookNextPageButton" relativePoint="LEFT"/></Anchors>
</FontString>
</Layer>
</Layers>
@@ -860,74 +870,74 @@
ref's own column-major pattern (col1 ids 1-6, col2 ids 7-12) — SpellBook_GetSpellID
reads a button's `id=` as its within-page/within-tab 1-based position, not its
screen order. -->
<CheckButton name="BenillaSpellButton1" inherits="BenillaSpellButtonTemplate" id="1">
<CheckButton name="SpellButton1" inherits="BenillaSpellButtonTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="34" y="-85"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton2" inherits="BenillaSpellButtonTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton1"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton2" inherits="BenillaSpellButtonTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton1"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton3" inherits="BenillaSpellButtonTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton3" inherits="BenillaSpellButtonTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton4" inherits="BenillaSpellButtonTemplate" id="8">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton3"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton4" inherits="BenillaSpellButtonTemplate" id="8">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton3"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton5" inherits="BenillaSpellButtonTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton5" inherits="BenillaSpellButtonTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton6" inherits="BenillaSpellButtonTemplate" id="9">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton5"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton6" inherits="BenillaSpellButtonTemplate" id="9">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton5"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton7" inherits="BenillaSpellButtonTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton7" inherits="BenillaSpellButtonTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton8" inherits="BenillaSpellButtonTemplate" id="10">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton7"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton8" inherits="BenillaSpellButtonTemplate" id="10">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton7"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton9" inherits="BenillaSpellButtonTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton9" inherits="BenillaSpellButtonTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton10" inherits="BenillaSpellButtonTemplate" id="11">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton9"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton10" inherits="BenillaSpellButtonTemplate" id="11">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton9"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton11" inherits="BenillaSpellButtonTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton11" inherits="BenillaSpellButtonTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellButton12" inherits="BenillaSpellButtonTemplate" id="12">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellButton11"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="SpellButton12" inherits="BenillaSpellButtonTemplate" id="12">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellButton11"><Offset><AbsDimension x="157" y="0"/></Offset></Anchor></Anchors>
</CheckButton>
<!-- The right-edge skill-line tabs, up to MAX_SKILLLINE_TABS (8) — ref l.495-566. -->
<CheckButton name="BenillaSpellBookSkillLineTab1" inherits="BenillaSpellBookSkillLineTabTemplate" id="1">
<CheckButton name="SpellBookSkillLineTab1" inherits="BenillaSpellBookSkillLineTabTemplate" id="1">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-32" y="-65"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellBookSkillLineTab2" inherits="BenillaSpellBookSkillLineTabTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellBookSkillLineTab1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
<CheckButton name="SpellBookSkillLineTab2" inherits="BenillaSpellBookSkillLineTabTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellBookSkillLineTab1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellBookSkillLineTab3" inherits="BenillaSpellBookSkillLineTabTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellBookSkillLineTab2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
<CheckButton name="SpellBookSkillLineTab3" inherits="BenillaSpellBookSkillLineTabTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellBookSkillLineTab2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellBookSkillLineTab4" inherits="BenillaSpellBookSkillLineTabTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellBookSkillLineTab3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
<CheckButton name="SpellBookSkillLineTab4" inherits="BenillaSpellBookSkillLineTabTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellBookSkillLineTab3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellBookSkillLineTab5" inherits="BenillaSpellBookSkillLineTabTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellBookSkillLineTab4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
<CheckButton name="SpellBookSkillLineTab5" inherits="BenillaSpellBookSkillLineTabTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellBookSkillLineTab4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellBookSkillLineTab6" inherits="BenillaSpellBookSkillLineTabTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellBookSkillLineTab5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
<CheckButton name="SpellBookSkillLineTab6" inherits="BenillaSpellBookSkillLineTabTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellBookSkillLineTab5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellBookSkillLineTab7" inherits="BenillaSpellBookSkillLineTabTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellBookSkillLineTab6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
<CheckButton name="SpellBookSkillLineTab7" inherits="BenillaSpellBookSkillLineTabTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellBookSkillLineTab6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="BenillaSpellBookSkillLineTab8" inherits="BenillaSpellBookSkillLineTabTemplate" id="8">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaSpellBookSkillLineTab7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
<CheckButton name="SpellBookSkillLineTab8" inherits="BenillaSpellBookSkillLineTabTemplate" id="8">
<Anchors><Anchor point="TOPLEFT" relativeTo="SpellBookSkillLineTab7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</CheckButton>
</Frames>
<Scripts>
<OnLoad>BenillaSpellBookFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaSpellBookFrame_OnEvent(self)</OnEvent>
<OnShow>BenillaSpellBookFrame_OnShow()</OnShow>
<OnHide>BenillaSpellBookFrame_OnHide()</OnHide>
<OnLoad>SpellBookFrame_OnLoad()</OnLoad>
<OnEvent>SpellBookFrame_OnEvent()</OnEvent>
<OnShow>SpellBookFrame_OnShow()</OnShow>
<OnHide>SpellBookFrame_OnHide()</OnHide>
<OnUpdate>BenillaSpellBookFrame_OnUpdate(self, arg1)</OnUpdate>
</Scripts>
</Frame>
+29 -29
View File
@@ -39,7 +39,7 @@
-- ContainerFrameItemButton_OnClick's own closure). maxStack < 2 hides instead of showing (a
-- 1-stack has nothing to split) — the ref's own guard.
function OpenStackSplitFrame(maxStack, owner, anchor, anchorTo)
local frame = BenillaStackSplitFrame
local frame = StackSplitFrame
if frame.owner then
frame.owner.hasStackSplit = nil
end
@@ -51,9 +51,9 @@
frame.owner = owner
owner.hasStackSplit = true
frame.split = 1
BenillaStackSplitText:SetText(frame.split)
BenillaStackSplitLeftButton:Disable()
BenillaStackSplitRightButton:Enable()
StackSplitText:SetText(frame.split)
StackSplitLeftButton:Disable()
StackSplitRightButton:Enable()
-- ref l.21-22: ClearAllPoints first — SetPoint only replaces an anchor of the SAME
-- point, and the anchor callers choose (BagFrame.xml passes "BOTTOMRIGHT") differs from
-- this frame's authored placeholder ("CENTER"), so skipping this would leave both live.
@@ -67,7 +67,7 @@
-- (no caller has a live reason to resize an open spinner today) — kept so a later caller
-- (the paper doll / bank slice) drops in without a new function.
function UpdateStackSplitFrame(maxStack)
local frame = BenillaStackSplitFrame
local frame = StackSplitFrame
frame.maxStack = maxStack
if frame.maxStack < 2 then
if frame.owner then
@@ -78,44 +78,44 @@
end
if frame.split > frame.maxStack then
frame.split = frame.maxStack
BenillaStackSplitText:SetText(frame.split)
StackSplitText:SetText(frame.split)
end
if frame.split == frame.maxStack then
BenillaStackSplitRightButton:Disable()
StackSplitRightButton:Disable()
else
BenillaStackSplitRightButton:Enable()
StackSplitRightButton:Enable()
end
if frame.split == 1 then
BenillaStackSplitLeftButton:Disable()
StackSplitLeftButton:Disable()
else
BenillaStackSplitLeftButton:Enable()
StackSplitLeftButton:Enable()
end
end
function BenillaStackSplitLeft_Click()
local frame = BenillaStackSplitFrame
local frame = StackSplitFrame
if frame.split == 1 then return end
frame.split = frame.split - 1
BenillaStackSplitText:SetText(frame.split)
StackSplitText:SetText(frame.split)
if frame.split == 1 then
BenillaStackSplitLeftButton:Disable()
StackSplitLeftButton:Disable()
end
BenillaStackSplitRightButton:Enable()
StackSplitRightButton:Enable()
end
function BenillaStackSplitRight_Click()
local frame = BenillaStackSplitFrame
local frame = StackSplitFrame
if frame.split == frame.maxStack then return end
frame.split = frame.split + 1
BenillaStackSplitText:SetText(frame.split)
StackSplitText:SetText(frame.split)
if frame.split == frame.maxStack then
BenillaStackSplitRightButton:Disable()
StackSplitRightButton:Disable()
end
BenillaStackSplitLeftButton:Enable()
StackSplitLeftButton:Enable()
end
function BenillaStackSplitOkay_Click()
local frame = BenillaStackSplitFrame
local frame = StackSplitFrame
if frame.owner then
frame.owner.SplitStack(frame.owner, frame.split)
end
@@ -123,7 +123,7 @@
end
function BenillaStackSplitCancel_Click()
BenillaStackSplitFrame:Hide()
StackSplitFrame:Hide()
end
-- OnHide (ref l.158-162): ANY hide — Okay, Cancel, or ToggleGameMenu's chain hook below —
@@ -131,8 +131,8 @@
-- l.35-39, checks `this.hasStackSplit` to auto-close the spinner if the SLOT itself goes away
-- mid-split — no consumer needs that half here, but the flag's own clear is this function's
-- job regardless of which button caused the hide).
function BenillaStackSplitFrame_OnHide()
local frame = BenillaStackSplitFrame
function StackSplitFrame_OnHide()
local frame = StackSplitFrame
if frame.owner then
frame.owner.hasStackSplit = nil
end
@@ -152,7 +152,7 @@
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<Frame name="BenillaStackSplitFrame" frameStrata="HIGH" hidden="true">
<Frame name="StackSplitFrame" frameStrata="HIGH" hidden="true">
<!-- ref StackSplitFrame.xml l.4-7: 172×96. The CENTER anchor here is only the pre-first-open
placeholder — OpenStackSplitFrame always repositions before the first Show. -->
<Size><AbsDimension x="172" y="96"/></Size>
@@ -167,7 +167,7 @@
<TexCoords left="0" right="0.671875" top="0" bottom="0.75"/>
</Texture>
<!-- ref l.16-24: the count, GameFontHighlight, right-justified, RIGHT -50/+18. -->
<FontString name="BenillaStackSplitText" inherits="GameFontHighlight" justifyH="RIGHT">
<FontString name="StackSplitText" inherits="GameFontHighlight" justifyH="RIGHT">
<Anchors>
<Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-50" y="18"/></Offset></Anchor>
</Anchors>
@@ -176,7 +176,7 @@
</Layers>
<Frames>
<!-- ref l.28-47: the left/right arrows, 16×16, RIGHT/LEFT-of-CENTER ±59/64, +18. -->
<Button name="BenillaStackSplitLeftButton">
<Button name="StackSplitLeftButton">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="RIGHT" relativePoint="CENTER"><Offset><AbsDimension x="-59" y="18"/></Offset></Anchor>
@@ -188,7 +188,7 @@
<OnClick>BenillaStackSplitLeft_Click()</OnClick>
</Scripts>
</Button>
<Button name="BenillaStackSplitRightButton">
<Button name="StackSplitRightButton">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativePoint="CENTER"><Offset><AbsDimension x="64" y="18"/></Offset></Anchor>
@@ -201,7 +201,7 @@
</Scripts>
</Button>
<!-- ref l.68-84: Okay, RIGHT-of-BOTTOM -3/+32. -->
<Button name="BenillaStackSplitButton1" inherits="BenillaStackSplitButtonTemplate" text="OKAY">
<Button name="StackSplitButton1" inherits="BenillaStackSplitButtonTemplate" text="OKAY">
<Anchors>
<Anchor point="RIGHT" relativePoint="BOTTOM"><Offset><AbsDimension x="-3" y="32"/></Offset></Anchor>
</Anchors>
@@ -210,7 +210,7 @@
</Scripts>
</Button>
<!-- ref l.85-101: Cancel, LEFT-of-BOTTOM +5/+32. -->
<Button name="BenillaStackSplitButton2" inherits="BenillaStackSplitButtonTemplate" text="CANCEL">
<Button name="StackSplitButton2" inherits="BenillaStackSplitButtonTemplate" text="CANCEL">
<Anchors>
<Anchor point="LEFT" relativePoint="BOTTOM"><Offset><AbsDimension x="5" y="32"/></Offset></Anchor>
</Anchors>
@@ -220,7 +220,7 @@
</Button>
</Frames>
<Scripts>
<OnHide>BenillaStackSplitFrame_OnHide()</OnHide>
<OnHide>StackSplitFrame_OnHide()</OnHide>
</Scripts>
</Frame>
</Ui>
+33 -31
View File
@@ -78,15 +78,15 @@
-- bar to the form count (hide the whole frame at 0 — a formless class never shows it),
-- then paint each button — icon, checked (active), the 1.0/0.4 castable tint (ref l.210-214),
-- cooldown swipe (CooldownFrame_SetTimer, Cooldown.xml's shared transcription).
function BenillaShapeshiftBar_Update()
function ShapeshiftBar_Update()
local numForms = GetNumShapeshiftForms()
if numForms > 0 then
BenillaShapeshiftBarFrame:Show()
ShapeshiftBarFrame:Show()
else
BenillaShapeshiftBarFrame:Hide()
ShapeshiftBarFrame:Hide()
end
for i = 1, NUM_SHAPESHIFT_SLOTS do
local button = getglobal("BenillaShapeshiftButton" .. i)
local button = getglobal("ShapeshiftButton" .. i)
if i <= numForms then
local texture, name, isActive, isCastable = GetShapeshiftFormInfo(i)
button.icon:SetTexture(texture or BENILLA_FALLBACK_ICON)
@@ -109,14 +109,16 @@
end
end
function BenillaShapeshiftBar_OnLoad(frame)
function ShapeshiftBar_OnLoad()
local frame = this
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("UPDATE_SHAPESHIFT_FORMS")
BenillaShapeshiftBar_Update()
ShapeshiftBar_Update()
end
function BenillaShapeshiftBar_OnEvent(frame)
BenillaShapeshiftBar_Update()
function ShapeshiftBar_OnEvent()
local frame = this
ShapeshiftBar_Update()
end
]]></Script>
@@ -157,56 +159,56 @@
</Scripts>
</CheckButton>
<Frame name="BenillaShapeshiftBarFrame" hidden="true">
<Frame name="ShapeshiftBarFrame" hidden="true">
<Size><AbsDimension x="29" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="BenillaActionBar" relativePoint="TOPLEFT"><Offset><AbsDimension x="30" y="45"/></Offset></Anchor>
<Anchor point="BOTTOMLEFT" relativeTo="MainMenuBar" relativePoint="TOPLEFT"><Offset><AbsDimension x="30" y="45"/></Offset></Anchor>
</Anchors>
<Frames>
<CheckButton name="BenillaShapeshiftButton1" inherits="BenillaShapeshiftButtonTemplate">
<CheckButton name="ShapeshiftButton1" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="11" y="3"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 1)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton2" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton1" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton2" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton1" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 2)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton3" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton2" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton3" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton2" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 3)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton4" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton3" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton4" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton3" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 4)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton5" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton4" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton5" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton4" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 5)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton6" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton5" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton6" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton5" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 6)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton7" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton6" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton7" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton6" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 7)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton8" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton7" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton8" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton7" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 8)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton9" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton8" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton9" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton8" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 9)</OnLoad></Scripts>
</CheckButton>
<CheckButton name="BenillaShapeshiftButton10" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaShapeshiftButton9" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<CheckButton name="ShapeshiftButton10" inherits="BenillaShapeshiftButtonTemplate">
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton9" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 10)</OnLoad></Scripts>
</CheckButton>
</Frames>
<Scripts>
<OnLoad>BenillaShapeshiftBar_OnLoad(self)</OnLoad>
<OnEvent>BenillaShapeshiftBar_OnEvent(self)</OnEvent>
<OnLoad>ShapeshiftBar_OnLoad()</OnLoad>
<OnEvent>ShapeshiftBar_OnEvent()</OnEvent>
<!-- The stance bar is the pet/stance term of the managed bottom-stack positions
(UIParent.xml, decision 0272): its show/hide re-fires the pass, exactly the
ref's BonusActionBarFrame.xml:350-355. -->
+3 -3
View File
@@ -147,8 +147,8 @@
- Bare `min`/`max`/`select` (used verbatim in `_DrawLines`/`_SetPrereqs` below): CONFIRMED
available — `min`/`max` are house-stdlib aliases for `math.min`/`math.max`
(`benilla-ui/src/script/stdlib.rs`), `select` is stock Lua 5.1. The ref's own `arg`/`arg.n`
Lua-5.0 vararg table (`_SetPrereqs`) has no equivalent here — `LUA_COMPAT_VARARG` isn't
shipped (WorldMapFrame.xml's own identical note) — replaced with `select("#", ...)`/
Lua-5.0 vararg table (`_SetPrereqs`) is written the 5.1-native way
(WorldMapFrame.xml's own identical note) — replaced with `select("#", ...)`/
`select(i, ...)` over a trailing `...`, WorldMapFrame.xml's own precedent for the identical
substitution.
- `GetTalentTabInfo` called TWICE in a row by the ref's own `TalentFrame_Update` (l.152-154:
@@ -1049,7 +1049,7 @@
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-65" y="-77"/></Offset></Anchor></Anchors>
<Frames>
<!-- The scrollbar: BenillaScrollBarTemplate's own geometry (ScrollTemplates.xml's
BenillaFauxScrollFrameTemplate, whose Slider anchor shape is reused directly
FauxScrollFrameTemplate, whose Slider anchor shape is reused directly
here without the Frame wrapper around it — this file's header comment). Its
inherited OnValueChanged is REPLACED below (not chained — verified). -->
<Slider name="BenillaTalentFrameScrollBar" inherits="BenillaScrollBarTemplate">
+81 -79
View File
@@ -29,7 +29,7 @@
demand. `BENILLA_TAXI_MAX_NODES = 50` below comfortably covers the real 5875
`TaxiNodes.dbc`'s per-continent max (43 nodes on Eastern Kingdoms, 39 on Kalimdor —
counted this session, `crate::ui_taxi`'s module doc). The route-line textures stay
genuinely dynamic (`BenillaTaxiRouteMap:CreateTexture(...)`, ref l.101/142) — plain
genuinely dynamic (`TaxiRouteMap:CreateTexture(...)`, ref l.101/142) — plain
region creation carries no template/inherits, so THAT half of the reference runs
completely unmodified.
2. THE TITLE READS AN EVENT ARG, not `UnitName("npc")` (ref TaxiFrame.lua:27). This
@@ -41,13 +41,13 @@
3. THE "NO SINGLE-HOP DESTINATION" REFUSAL (`DrawOneHopLines`, ref l.161-164) calls
`BenillaErrorsFrame_AddMessage` (`ErrorsFrame.xml`'s real Lua-callable seam, byte-exact
`ERR_TAXINOPATHS` text below) rather than the reference's `UIErrorsFrame:AddMessage` —
this engine's error toast is `BenillaErrorsFrame`, not an intrinsic `MessageFrame`
this engine's error toast is `UIErrorsFrame`, not an intrinsic `MessageFrame`
object, so the ref's OO call becomes the plain function call. `ErrorsFrame.xml` loads
well before this file in the shipped list (`ui_script/mod.rs`), so the global already
exists by the time this ever runs.
`<TaxiRouteFrame>` (an intrinsic type this engine doesn't have) becomes a plain `<Frame>`
(`BenillaTaxiRouteMap`) per the house rule for every non-shipped intrinsic (decision 0246's
(`TaxiRouteMap`) per the house rule for every non-shipped intrinsic (decision 0246's
patch-chain rule; the `Cooldown`/`GameTooltip`/etc. precedents) — it carries no behavior of
its own beyond being a Frame that owns child regions/buttons, so the plain type loses
nothing.
@@ -86,7 +86,8 @@
-- (U+2019, "don\226\128\153t"), not a straight one.
local ERR_TAXINOPATHS = "You don\226\128\153t know any flight locations connected to this one."
function BenillaTaxiFrame_OnLoad(frame)
function TaxiFrame_OnLoad()
local frame = this
-- The "npc" portrait token (decision 0421): the open taxi session's flight master
-- bakes into the ring, resolved exactly like Gossip/Merchant/Trainer's own OnLoad.
SetPortraitTexture(getglobal(frame:GetName() .. "Portrait"), "npc")
@@ -97,29 +98,30 @@
-- Deviation 2 above: the flight master's name rides TAXIMAP_OPENED's arg1 (the app
-- resolves it through the NameCache) rather than a live UnitName("npc") read.
function BenillaTaxiFrame_SetTitle(name)
getglobal("BenillaTaxiNameText"):SetText(name or "")
getglobal("TaxiNameText"):SetText(name or "")
end
-- ref TaxiFrame.lua:24-80, restructured only at the node-button loop (deviation 1 — a
-- fixed pool walked 1..BENILLA_TAXI_MAX_NODES instead of grown via CreateFrame).
function BenillaTaxiFrame_OnEvent(frame)
function TaxiFrame_OnEvent(event)
local frame = this
if event == "TAXIMAP_OPENED" then
BenillaTaxiFrame_SetTitle(arg1)
-- Set the texture coords on the map, then hand the region to the app's art pick.
BenillaTaxiMap:SetTexCoord(0, 1, 0, 1)
SetTaxiMap(BenillaTaxiMap)
TaxiMap:SetTexCoord(0, 1, 0, 1)
SetTaxiMap(TaxiMap)
-- Draw nodes: every pool slot up to the pushed count shows its type's icon at its
-- pushed position; the rest hide (ref l.44-63, the static-pool shape).
local num_nodes = NumTaxiNodes()
for index = 1, BENILLA_TAXI_MAX_NODES do
local button = getglobal("BenillaTaxiButton" .. index)
local button = getglobal("TaxiButton" .. index)
local ntype = TaxiNodeGetType(index)
if index <= num_nodes and ntype ~= "NONE" then
local x, y = TaxiNodePosition(button:GetID())
button:ClearAllPoints()
button:SetPoint("CENTER", "BenillaTaxiMap", "BOTTOMLEFT", x * TAXI_MAP_WIDTH, y * TAXI_MAP_HEIGHT)
button:SetPoint("CENTER", "TaxiMap", "BOTTOMLEFT", x * TAXI_MAP_WIDTH, y * TAXI_MAP_HEIGHT)
button:SetNormalTexture(TaxiButtonTypes[ntype].file)
button:Show()
else
@@ -153,8 +155,8 @@
local numRoutes = GetNumRoutes(index)
local line
local sX, sY, dX, dY
local w = BenillaTaxiRouteMap:GetWidth()
local h = BenillaTaxiRouteMap:GetHeight()
local w = TaxiRouteMap:GetWidth()
local h = TaxiRouteMap:GetHeight()
local ntype = TaxiNodeGetType(index)
if ntype == "REACHABLE" then
@@ -163,7 +165,7 @@
if numRoutes > NUM_TAXI_ROUTES then
for i = NUM_TAXI_ROUTES + 1, numRoutes do
line = BenillaTaxiRouteMap:CreateTexture("TaxiRoute" .. i, "BACKGROUND")
line = TaxiRouteMap:CreateTexture("TaxiRoute" .. i, "BACKGROUND")
line:SetTexture("Interface\\TaxiFrame\\UI-Taxi-Line")
end
NUM_TAXI_ROUTES = numRoutes
@@ -176,7 +178,7 @@
sY = TaxiGetSrcY(index, i) * h
dX = TaxiGetDestX(index, i) * w
dY = TaxiGetDestY(index, i) * h
DrawRouteLine(line, "BenillaTaxiRouteMap", sX, sY, dX, dY, 32)
DrawRouteLine(line, "TaxiRouteMap", sX, sY, dX, dY, 32)
line:Show()
else
line:Hide()
@@ -193,14 +195,14 @@
NUM_TAXI_ROUTES = 0
-- Draw all flightpaths within one hop of current location — ref TaxiFrame.lua:129-165,
-- verbatim except the canvas name ("TaxiRouteMap" -> "BenillaTaxiRouteMap") and the
-- verbatim except the canvas name ("TaxiRouteMap" -> "TaxiRouteMap") and the
-- no-single-hop refusal (deviation 3 above: BenillaErrorsFrame_AddMessage, not
-- UIErrorsFrame:AddMessage).
function DrawOneHopLines()
local line
local sX, sY, dX, dY
local w = BenillaTaxiRouteMap:GetWidth()
local h = BenillaTaxiRouteMap:GetHeight()
local w = TaxiRouteMap:GetWidth()
local h = TaxiRouteMap:GetHeight()
local numNodes = NumTaxiNodes()
local numLines = 0
local numSingleHops = 0
@@ -209,7 +211,7 @@
numSingleHops = numSingleHops + 1
numLines = numLines + 1
if numLines > NUM_TAXI_ROUTES then
line = BenillaTaxiRouteMap:CreateTexture("TaxiRoute" .. numLines, "BACKGROUND")
line = TaxiRouteMap:CreateTexture("TaxiRoute" .. numLines, "BACKGROUND")
line:SetTexture("Interface\\TaxiFrame\\UI-Taxi-Line")
NUM_TAXI_ROUTES = numLines
else
@@ -220,7 +222,7 @@
sY = TaxiGetSrcY(i, 1) * h
dX = TaxiGetDestX(i, 1) * w
dY = TaxiGetDestY(i, 1) * h
DrawRouteLine(line, "BenillaTaxiRouteMap", sX, sY, dX, dY, 32)
DrawRouteLine(line, "TaxiRouteMap", sX, sY, dX, dY, 32)
line:Show()
end
end
@@ -230,7 +232,7 @@
end
if numSingleHops == 0 then
BenillaErrorsFrame_AddMessage(ERR_TAXINOPATHS, 1.0, 0.1, 0.1)
HideUIPanel(BenillaTaxiFrame)
HideUIPanel(TaxiFrame)
end
end
@@ -303,7 +305,7 @@
-- vanilla's client-side close sends no packet; the app clears TaxiState on this
-- intent (which fires TAXIMAP_CLOSED), and we hide immediately so there's no lag.
CloseTaxiMap()
HideUIPanel(BenillaTaxiFrame)
HideUIPanel(TaxiFrame)
end
]]></Script>
@@ -347,7 +349,7 @@
</HighlightTexture>
</Button>
<Frame name="BenillaTaxiFrame" hidden="true">
<Frame name="TaxiFrame" hidden="true">
<!-- The real 384×512 window (ref l.34-36). No <Anchors> — ShowUIPanel/SetLeftFrame
SetPoints this to the left slot on open, exactly like Gossip/Merchant/Trainer. -->
<Size><AbsDimension x="384" y="512"/></Size>
@@ -384,7 +386,7 @@
</Texture>
<!-- The flight master's name banner (ref TaxiMerchant, l.104-115): GameFontNormal,
CENTER, TOP -17, 192 wide — deviation 2 above (arg1, not UnitName("npc")). -->
<FontString name="BenillaTaxiNameText" inherits="GameFontNormal" justifyH="CENTER">
<FontString name="TaxiNameText" inherits="GameFontNormal" justifyH="CENTER">
<Size><AbsDimension x="192" y="0"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor>
@@ -394,7 +396,7 @@
<!-- The continent map art (ref TaxiMap, l.117-129): 316×352, TOP -13,-75. No file —
SetTaxiMap assigns the app's TAXIMAP<continentId> pick (byte-verified — decision 0496). -->
<Layer level="OVERLAY">
<Texture name="BenillaTaxiMap">
<Texture name="TaxiMap">
<Size><AbsDimension x="316" y="352"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="-13" y="-75"/></Offset></Anchor>
@@ -406,7 +408,7 @@
<!-- Close: the real UIPanelCloseButton position (TOPRIGHT -29,-8; ref l.133-141), on
the minimize-button art (this engine ships no UIPanelCloseButton — the Gossip/
Trainer/Merchant precedent). -->
<Button name="BenillaTaxiCloseButton">
<Button name="TaxiCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-29" y="-8"/></Offset></Anchor>
@@ -419,73 +421,73 @@
<!-- `<TaxiRouteFrame>` (ref l.142-153) — an intrinsic type this engine doesn't have —
becomes a plain <Frame> (see the header note): 316×352, TOP -13,-75, exactly
overlaid on BenillaTaxiMap. Houses the node-button pool + the dynamically-created
overlaid on TaxiMap. Houses the node-button pool + the dynamically-created
route-line textures, both parented here exactly like the reference (ref l.39,101). -->
<Frame name="BenillaTaxiRouteMap">
<Frame name="TaxiRouteMap">
<Size><AbsDimension x="316" y="352"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="-13" y="-75"/></Offset></Anchor>
</Anchors>
<Frames>
<Button name="BenillaTaxiButton1" inherits="BenillaTaxiButtonTemplate" id="1"/>
<Button name="BenillaTaxiButton2" inherits="BenillaTaxiButtonTemplate" id="2"/>
<Button name="BenillaTaxiButton3" inherits="BenillaTaxiButtonTemplate" id="3"/>
<Button name="BenillaTaxiButton4" inherits="BenillaTaxiButtonTemplate" id="4"/>
<Button name="BenillaTaxiButton5" inherits="BenillaTaxiButtonTemplate" id="5"/>
<Button name="BenillaTaxiButton6" inherits="BenillaTaxiButtonTemplate" id="6"/>
<Button name="BenillaTaxiButton7" inherits="BenillaTaxiButtonTemplate" id="7"/>
<Button name="BenillaTaxiButton8" inherits="BenillaTaxiButtonTemplate" id="8"/>
<Button name="BenillaTaxiButton9" inherits="BenillaTaxiButtonTemplate" id="9"/>
<Button name="BenillaTaxiButton10" inherits="BenillaTaxiButtonTemplate" id="10"/>
<Button name="BenillaTaxiButton11" inherits="BenillaTaxiButtonTemplate" id="11"/>
<Button name="BenillaTaxiButton12" inherits="BenillaTaxiButtonTemplate" id="12"/>
<Button name="BenillaTaxiButton13" inherits="BenillaTaxiButtonTemplate" id="13"/>
<Button name="BenillaTaxiButton14" inherits="BenillaTaxiButtonTemplate" id="14"/>
<Button name="BenillaTaxiButton15" inherits="BenillaTaxiButtonTemplate" id="15"/>
<Button name="BenillaTaxiButton16" inherits="BenillaTaxiButtonTemplate" id="16"/>
<Button name="BenillaTaxiButton17" inherits="BenillaTaxiButtonTemplate" id="17"/>
<Button name="BenillaTaxiButton18" inherits="BenillaTaxiButtonTemplate" id="18"/>
<Button name="BenillaTaxiButton19" inherits="BenillaTaxiButtonTemplate" id="19"/>
<Button name="BenillaTaxiButton20" inherits="BenillaTaxiButtonTemplate" id="20"/>
<Button name="BenillaTaxiButton21" inherits="BenillaTaxiButtonTemplate" id="21"/>
<Button name="BenillaTaxiButton22" inherits="BenillaTaxiButtonTemplate" id="22"/>
<Button name="BenillaTaxiButton23" inherits="BenillaTaxiButtonTemplate" id="23"/>
<Button name="BenillaTaxiButton24" inherits="BenillaTaxiButtonTemplate" id="24"/>
<Button name="BenillaTaxiButton25" inherits="BenillaTaxiButtonTemplate" id="25"/>
<Button name="BenillaTaxiButton26" inherits="BenillaTaxiButtonTemplate" id="26"/>
<Button name="BenillaTaxiButton27" inherits="BenillaTaxiButtonTemplate" id="27"/>
<Button name="BenillaTaxiButton28" inherits="BenillaTaxiButtonTemplate" id="28"/>
<Button name="BenillaTaxiButton29" inherits="BenillaTaxiButtonTemplate" id="29"/>
<Button name="BenillaTaxiButton30" inherits="BenillaTaxiButtonTemplate" id="30"/>
<Button name="BenillaTaxiButton31" inherits="BenillaTaxiButtonTemplate" id="31"/>
<Button name="BenillaTaxiButton32" inherits="BenillaTaxiButtonTemplate" id="32"/>
<Button name="BenillaTaxiButton33" inherits="BenillaTaxiButtonTemplate" id="33"/>
<Button name="BenillaTaxiButton34" inherits="BenillaTaxiButtonTemplate" id="34"/>
<Button name="BenillaTaxiButton35" inherits="BenillaTaxiButtonTemplate" id="35"/>
<Button name="BenillaTaxiButton36" inherits="BenillaTaxiButtonTemplate" id="36"/>
<Button name="BenillaTaxiButton37" inherits="BenillaTaxiButtonTemplate" id="37"/>
<Button name="BenillaTaxiButton38" inherits="BenillaTaxiButtonTemplate" id="38"/>
<Button name="BenillaTaxiButton39" inherits="BenillaTaxiButtonTemplate" id="39"/>
<Button name="BenillaTaxiButton40" inherits="BenillaTaxiButtonTemplate" id="40"/>
<Button name="BenillaTaxiButton41" inherits="BenillaTaxiButtonTemplate" id="41"/>
<Button name="BenillaTaxiButton42" inherits="BenillaTaxiButtonTemplate" id="42"/>
<Button name="BenillaTaxiButton43" inherits="BenillaTaxiButtonTemplate" id="43"/>
<Button name="BenillaTaxiButton44" inherits="BenillaTaxiButtonTemplate" id="44"/>
<Button name="BenillaTaxiButton45" inherits="BenillaTaxiButtonTemplate" id="45"/>
<Button name="BenillaTaxiButton46" inherits="BenillaTaxiButtonTemplate" id="46"/>
<Button name="BenillaTaxiButton47" inherits="BenillaTaxiButtonTemplate" id="47"/>
<Button name="BenillaTaxiButton48" inherits="BenillaTaxiButtonTemplate" id="48"/>
<Button name="BenillaTaxiButton49" inherits="BenillaTaxiButtonTemplate" id="49"/>
<Button name="BenillaTaxiButton50" inherits="BenillaTaxiButtonTemplate" id="50"/>
<Button name="TaxiButton1" inherits="BenillaTaxiButtonTemplate" id="1"/>
<Button name="TaxiButton2" inherits="BenillaTaxiButtonTemplate" id="2"/>
<Button name="TaxiButton3" inherits="BenillaTaxiButtonTemplate" id="3"/>
<Button name="TaxiButton4" inherits="BenillaTaxiButtonTemplate" id="4"/>
<Button name="TaxiButton5" inherits="BenillaTaxiButtonTemplate" id="5"/>
<Button name="TaxiButton6" inherits="BenillaTaxiButtonTemplate" id="6"/>
<Button name="TaxiButton7" inherits="BenillaTaxiButtonTemplate" id="7"/>
<Button name="TaxiButton8" inherits="BenillaTaxiButtonTemplate" id="8"/>
<Button name="TaxiButton9" inherits="BenillaTaxiButtonTemplate" id="9"/>
<Button name="TaxiButton10" inherits="BenillaTaxiButtonTemplate" id="10"/>
<Button name="TaxiButton11" inherits="BenillaTaxiButtonTemplate" id="11"/>
<Button name="TaxiButton12" inherits="BenillaTaxiButtonTemplate" id="12"/>
<Button name="TaxiButton13" inherits="BenillaTaxiButtonTemplate" id="13"/>
<Button name="TaxiButton14" inherits="BenillaTaxiButtonTemplate" id="14"/>
<Button name="TaxiButton15" inherits="BenillaTaxiButtonTemplate" id="15"/>
<Button name="TaxiButton16" inherits="BenillaTaxiButtonTemplate" id="16"/>
<Button name="TaxiButton17" inherits="BenillaTaxiButtonTemplate" id="17"/>
<Button name="TaxiButton18" inherits="BenillaTaxiButtonTemplate" id="18"/>
<Button name="TaxiButton19" inherits="BenillaTaxiButtonTemplate" id="19"/>
<Button name="TaxiButton20" inherits="BenillaTaxiButtonTemplate" id="20"/>
<Button name="TaxiButton21" inherits="BenillaTaxiButtonTemplate" id="21"/>
<Button name="TaxiButton22" inherits="BenillaTaxiButtonTemplate" id="22"/>
<Button name="TaxiButton23" inherits="BenillaTaxiButtonTemplate" id="23"/>
<Button name="TaxiButton24" inherits="BenillaTaxiButtonTemplate" id="24"/>
<Button name="TaxiButton25" inherits="BenillaTaxiButtonTemplate" id="25"/>
<Button name="TaxiButton26" inherits="BenillaTaxiButtonTemplate" id="26"/>
<Button name="TaxiButton27" inherits="BenillaTaxiButtonTemplate" id="27"/>
<Button name="TaxiButton28" inherits="BenillaTaxiButtonTemplate" id="28"/>
<Button name="TaxiButton29" inherits="BenillaTaxiButtonTemplate" id="29"/>
<Button name="TaxiButton30" inherits="BenillaTaxiButtonTemplate" id="30"/>
<Button name="TaxiButton31" inherits="BenillaTaxiButtonTemplate" id="31"/>
<Button name="TaxiButton32" inherits="BenillaTaxiButtonTemplate" id="32"/>
<Button name="TaxiButton33" inherits="BenillaTaxiButtonTemplate" id="33"/>
<Button name="TaxiButton34" inherits="BenillaTaxiButtonTemplate" id="34"/>
<Button name="TaxiButton35" inherits="BenillaTaxiButtonTemplate" id="35"/>
<Button name="TaxiButton36" inherits="BenillaTaxiButtonTemplate" id="36"/>
<Button name="TaxiButton37" inherits="BenillaTaxiButtonTemplate" id="37"/>
<Button name="TaxiButton38" inherits="BenillaTaxiButtonTemplate" id="38"/>
<Button name="TaxiButton39" inherits="BenillaTaxiButtonTemplate" id="39"/>
<Button name="TaxiButton40" inherits="BenillaTaxiButtonTemplate" id="40"/>
<Button name="TaxiButton41" inherits="BenillaTaxiButtonTemplate" id="41"/>
<Button name="TaxiButton42" inherits="BenillaTaxiButtonTemplate" id="42"/>
<Button name="TaxiButton43" inherits="BenillaTaxiButtonTemplate" id="43"/>
<Button name="TaxiButton44" inherits="BenillaTaxiButtonTemplate" id="44"/>
<Button name="TaxiButton45" inherits="BenillaTaxiButtonTemplate" id="45"/>
<Button name="TaxiButton46" inherits="BenillaTaxiButtonTemplate" id="46"/>
<Button name="TaxiButton47" inherits="BenillaTaxiButtonTemplate" id="47"/>
<Button name="TaxiButton48" inherits="BenillaTaxiButtonTemplate" id="48"/>
<Button name="TaxiButton49" inherits="BenillaTaxiButtonTemplate" id="49"/>
<Button name="TaxiButton50" inherits="BenillaTaxiButtonTemplate" id="50"/>
</Frames>
</Frame>
</Frames>
<Scripts>
<OnLoad>
BenillaTaxiFrame_OnLoad(self)
TaxiFrame_OnLoad()
</OnLoad>
<OnEvent>
BenillaTaxiFrame_OnEvent(self)
TaxiFrame_OnEvent(event)
</OnEvent>
<!-- ref l.162-169, verbatim: the MainMenu sound kit (not Merchant/Trainer's
CharacterInfo kit — the reference genuinely uses a different pair here). -->
+110 -108
View File
@@ -24,7 +24,8 @@
BENILLA_MAX_TRADE_ITEMS = 7 -- MAX_TRADE_ITEMS (ref TradeFrame.lua l.1)
-- ── Load / event dispatch (ref TradeFrame_OnLoad l.4 / TradeFrame_OnEvent l.18) ───────────
function BenillaTradeFrame_OnLoad(frame)
function TradeFrame_OnLoad()
local frame = this
frame:RegisterEvent("TRADE_SHOW")
frame:RegisterEvent("TRADE_UPDATE")
frame:RegisterEvent("TRADE_CLOSED")
@@ -37,71 +38,72 @@
frame:RegisterEvent("PLAYER_TRADE_MONEY")
end
function BenillaTradeFrame_OnEvent(frame)
function TradeFrame_OnEvent()
local frame = this
if event == "TRADE_SHOW" or event == "TRADE_UPDATE" then
ShowUIPanel(frame)
if not frame:IsVisible() then
CloseTrade()
return
end
BenillaTradeFrameTradeButton:Enable()
BenillaTradeFrame_Update()
TradeFrameTradeButton:Enable()
TradeFrame_Update()
elseif event == "PLAYER_TRADE_MONEY" then
BenillaMoneyInput_SetCopper("BenillaTradePlayerInputMoney", GetPlayerTradeMoney())
BenillaMoneyInput_SetCopper("TradePlayerInputMoney", GetPlayerTradeMoney())
elseif event == "TRADE_CLOSED" then
HideUIPanel(frame)
elseif event == "TRADE_PLAYER_ITEM_CHANGED" then
BenillaTradeFrame_UpdatePlayerItem(arg1)
TradeFrame_UpdatePlayerItem(arg1)
elseif event == "TRADE_TARGET_ITEM_CHANGED" then
BenillaTradeFrame_UpdateTargetItem(arg1)
TradeFrame_UpdateTargetItem(arg1)
elseif event == "TRADE_ACCEPT_UPDATE" then
BenillaTradeFrame_SetAcceptState(arg1, arg2)
TradeFrame_SetAcceptState(arg1, arg2)
end
end
function BenillaTradeFrame_OnShow()
BenillaTradeFrame.acceptState = 0
function TradeFrame_OnShow()
TradeFrame.acceptState = 0
end
function BenillaTradeFrame_OnHide()
function TradeFrame_OnHide()
-- The client-side close verb (ref TradeFrame_OnHide l.144) → CMSG_CANCEL_TRADE + a local
-- clear (the app's drain); clear our money entry so a later trade opens fresh (ref l.146).
BenillaMoneyInput_ResetMoney("BenillaTradePlayerInputMoney")
BenillaMoneyInput_ResetMoney("TradePlayerInputMoney")
CloseTrade()
end
-- A keystroke in our money input (ref TradeFrame_UpdateMoney l.162): read the copper total,
-- clamp to what we actually carry, and offer it → SetTradeMoney → CMSG_SET_TRADE_GOLD.
function BenillaTradeFrame_UpdateMoney()
local copper = BenillaMoneyInput_GetCopper("BenillaTradePlayerInputMoney")
function TradeFrame_UpdateMoney()
local copper = BenillaMoneyInput_GetCopper("TradePlayerInputMoney")
if GetMoney and copper > GetMoney() then
copper = GetPlayerTradeMoney()
BenillaMoneyInput_SetCopper("BenillaTradePlayerInputMoney", copper)
BenillaMoneyInput_SetCopper("TradePlayerInputMoney", copper)
end
SetTradeMoney(copper)
end
-- ── Full repaint (ref TradeFrame_Update l.39): portraits, names, all 7 slots each side, the
-- two money displays, the accept highlights hidden. ────────────────────────────────────
function BenillaTradeFrame_Update()
SetPortraitTexture(BenillaTradeFramePlayerPortrait, "player")
function TradeFrame_Update()
SetPortraitTexture(TradeFramePlayerPortrait, "player")
-- The partner rides the shared "npc" portrait booth (the app points it at the partner
-- entity, decision 0592); the name comes from GetTradePartnerName() in place of the
-- reference's UnitName("NPC").
SetPortraitTexture(BenillaTradeFrameRecipientPortrait, "npc")
BenillaTradeFramePlayerNameText:SetText(UnitName("player") or "")
BenillaTradeFrameRecipientNameText:SetText(GetTradePartnerName() or "")
SetPortraitTexture(TradeFrameRecipientPortrait, "npc")
TradeFramePlayerNameText:SetText(UnitName("player") or "")
TradeFrameRecipientNameText:SetText(GetTradePartnerName() or "")
for i = 1, BENILLA_MAX_TRADE_ITEMS do
BenillaTradeFrame_UpdatePlayerItem(i)
BenillaTradeFrame_UpdateTargetItem(i)
TradeFrame_UpdatePlayerItem(i)
TradeFrame_UpdateTargetItem(i)
end
-- Our gold is the editable input, reflected via PLAYER_TRADE_MONEY (not repainted here, so
-- the repaint never clobbers mid-typing); only the partner's read-only gold repaints.
BenillaMoney_Set("BenillaTradeRecipientMoneyFrame", GetTargetTradeMoney())
BenillaTradeHighlightPlayer:Hide()
BenillaTradeHighlightRecipient:Hide()
BenillaTradeHighlightPlayerEnchant:Hide()
BenillaTradeHighlightRecipientEnchant:Hide()
BenillaMoney_Set("TradeRecipientMoneyFrame", GetTargetTradeMoney())
TradeHighlightPlayer:Hide()
TradeHighlightRecipient:Hide()
TradeHighlightPlayerEnchant:Hide()
TradeHighlightRecipientEnchant:Hide()
end
-- One slot's paint, shared by both columns (ref TradeFrame_UpdatePlayerItem l.54 /
@@ -152,14 +154,14 @@
end
end
function BenillaTradeFrame_UpdatePlayerItem(id)
function TradeFrame_UpdatePlayerItem(id)
local name, texture, numItems, isUsable, enchantment = GetTradePlayerItemInfo(id)
BenillaTradeSlot_SetItem("BenillaTradePlayerItem"..id, id, name, texture, numItems, isUsable, enchantment, nil)
BenillaTradeSlot_SetItem("TradePlayerItem"..id, id, name, texture, numItems, isUsable, enchantment, nil)
end
function BenillaTradeFrame_UpdateTargetItem(id)
function TradeFrame_UpdateTargetItem(id)
local name, texture, numItems, quality, isUsable, enchantment = GetTradeTargetItemInfo(id)
BenillaTradeSlot_SetItem("BenillaTradeRecipientItem"..id, id, name, texture, numItems, isUsable, enchantment, 1)
BenillaTradeSlot_SetItem("TradeRecipientItem"..id, id, name, texture, numItems, isUsable, enchantment, 1)
end
-- A trade slot's item button as a cursor drop target (decision 0592 P2, ref ClickTradeButton /
@@ -178,33 +180,33 @@
-- The accept glow (ref TradeFrame_SetAcceptState l.116): each side's column + enchant-slot
-- highlight tracks who has pressed Trade; our own accept disables the Trade button.
function BenillaTradeFrame_SetAcceptState(playerState, targetState)
BenillaTradeFrame.acceptState = playerState
function TradeFrame_SetAcceptState(playerState, targetState)
TradeFrame.acceptState = playerState
if playerState == 1 then
BenillaTradeHighlightPlayer:Show()
BenillaTradeHighlightPlayerEnchant:Show()
BenillaTradeFrameTradeButton:Disable()
TradeHighlightPlayer:Show()
TradeHighlightPlayerEnchant:Show()
TradeFrameTradeButton:Disable()
else
BenillaTradeHighlightPlayer:Hide()
BenillaTradeHighlightPlayerEnchant:Hide()
BenillaTradeFrameTradeButton:Enable()
TradeHighlightPlayer:Hide()
TradeHighlightPlayerEnchant:Hide()
TradeFrameTradeButton:Enable()
end
if targetState == 1 then
BenillaTradeHighlightRecipient:Show()
BenillaTradeHighlightRecipientEnchant:Show()
TradeHighlightRecipient:Show()
TradeHighlightRecipientEnchant:Show()
else
BenillaTradeHighlightRecipient:Hide()
BenillaTradeHighlightRecipientEnchant:Hide()
TradeHighlightRecipient:Hide()
TradeHighlightRecipientEnchant:Hide()
end
end
-- Cancel (ref TradeFrameCancelButton_OnClick l.136): with our accept up, drop it (stay in the
-- trade); otherwise close the window (→ OnHide → CloseTrade).
function BenillaTradeFrameCancelButton_OnClick()
if BenillaTradeFrame.acceptState == 1 then
function TradeFrameCancelButton_OnClick()
if TradeFrame.acceptState == 1 then
CancelTradeAccept()
else
HideUIPanel(BenillaTradeFrame)
HideUIPanel(TradeFrame)
end
end
]]></Script>
@@ -304,18 +306,18 @@
</Frames>
</Frame>
<Frame name="BenillaTradeFrame" toplevel="true" movable="true" enableMouse="true" hidden="true">
<Frame name="TradeFrame" toplevel="true" movable="true" enableMouse="true" hidden="true">
<!-- ref TradeFrame l.164: 384×512; ShowUIPanel slots it left. -->
<Size><AbsDimension x="384" y="512"/></Size>
<HitRectInsets><AbsInset left="0" right="35" top="0" bottom="72"/></HitRectInsets>
<Layers>
<Layer level="BACKGROUND">
<!-- ref l.180: the two 60×60 unit portraits (recipient right, player left). -->
<Texture name="BenillaTradeFrameRecipientPortrait">
<Texture name="TradeFrameRecipientPortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="183" y="-6"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="BenillaTradeFramePlayerPortrait">
<Texture name="TradeFramePlayerPortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
@@ -338,92 +340,92 @@
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<FontString name="BenillaTradeFrameRecipientNameText" inherits="GameFontNormal" text="">
<FontString name="TradeFrameRecipientNameText" inherits="GameFontNormal" text="">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeFrame"><Offset><AbsDimension x="245" y="-17"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="245" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTradeFramePlayerNameText" inherits="GameFontNormal" text="">
<FontString name="TradeFramePlayerNameText" inherits="GameFontNormal" text="">
<Size><AbsDimension x="100" y="12"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeFrame"><Offset><AbsDimension x="75" y="-17"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="75" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<!-- ref l.262: the "Will Be Enchanted" column labels above the enchant slots. -->
<FontString name="BenillaTradeFramePlayerEnchantText" inherits="GameFontHighlightSmall" text="Will Be Enchanted">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeFrame"><Offset><AbsDimension x="26" y="-374"/></Offset></Anchor></Anchors>
<FontString name="TradeFramePlayerEnchantText" inherits="GameFontHighlightSmall" text="Will Be Enchanted">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="26" y="-374"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTradeFrameRecipientEnchantText" inherits="GameFontHighlightSmall" text="Will Be Enchanted">
<Anchors><Anchor point="LEFT" relativeTo="BenillaTradeFramePlayerEnchantText"><Offset><AbsDimension x="170" y="0"/></Offset></Anchor></Anchors>
<FontString name="TradeFrameRecipientEnchantText" inherits="GameFontHighlightSmall" text="Will Be Enchanted">
<Anchors><Anchor point="LEFT" relativeTo="TradeFramePlayerEnchantText"><Offset><AbsDimension x="170" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The accept-glow highlights, one per column + its enchant slot (ref l.283). -->
<Frame name="BenillaTradeHighlightPlayer" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Frame name="TradeHighlightPlayer" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="266"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeFrame"><Offset><AbsDimension x="19" y="-100"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="19" y="-100"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeHighlightRecipient" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Frame name="TradeHighlightRecipient" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="266"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="189" y="-100"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeHighlightPlayerEnchant" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Frame name="TradeHighlightPlayerEnchant" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="61"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeHighlightPlayer" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeHighlightPlayer" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeHighlightRecipientEnchant" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Frame name="TradeHighlightRecipientEnchant" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="61"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeHighlightRecipient" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeHighlightRecipient" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
</Frame>
<!-- The partner's 6 trade slots + the 7th enchant slot (ref TradeRecipientItem1..7 l.331). -->
<Frame name="BenillaTradeRecipientItem1" inherits="BenillaTradeItemTemplate" id="1">
<Frame name="TradeRecipientItem1" inherits="BenillaTradeItemTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="195" y="-104"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeRecipientItem2" inherits="BenillaTradeItemTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeRecipientItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradeRecipientItem2" inherits="BenillaTradeItemTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeRecipientItem3" inherits="BenillaTradeItemTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeRecipientItem2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradeRecipientItem3" inherits="BenillaTradeItemTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeRecipientItem4" inherits="BenillaTradeItemTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeRecipientItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradeRecipientItem4" inherits="BenillaTradeItemTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeRecipientItem5" inherits="BenillaTradeItemTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeRecipientItem4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradeRecipientItem5" inherits="BenillaTradeItemTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeRecipientItem6" inherits="BenillaTradeItemTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeRecipientItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradeRecipientItem6" inherits="BenillaTradeItemTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradeRecipientItem7" inherits="BenillaTradeEnchantItemTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeRecipientItem6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-28"/></Offset></Anchor></Anchors>
<Frame name="TradeRecipientItem7" inherits="BenillaTradeEnchantItemTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-28"/></Offset></Anchor></Anchors>
</Frame>
<!-- Our 6 trade slots + the 7th enchant slot (ref TradePlayerItem1..7 l.410). -->
<Frame name="BenillaTradePlayerItem1" inherits="BenillaTradeItemTemplate" id="1">
<Frame name="TradePlayerItem1" inherits="BenillaTradeItemTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="-104"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradePlayerItem2" inherits="BenillaTradeItemTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradePlayerItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradePlayerItem2" inherits="BenillaTradeItemTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradePlayerItem3" inherits="BenillaTradeItemTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradePlayerItem2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradePlayerItem3" inherits="BenillaTradeItemTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradePlayerItem4" inherits="BenillaTradeItemTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradePlayerItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradePlayerItem4" inherits="BenillaTradeItemTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradePlayerItem5" inherits="BenillaTradeItemTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradePlayerItem4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradePlayerItem5" inherits="BenillaTradeItemTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradePlayerItem6" inherits="BenillaTradeItemTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradePlayerItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
<Frame name="TradePlayerItem6" inherits="BenillaTradeItemTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTradePlayerItem7" inherits="BenillaTradeEnchantItemTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradePlayerItem6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-28"/></Offset></Anchor></Anchors>
<Frame name="TradePlayerItem7" inherits="BenillaTradeEnchantItemTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-28"/></Offset></Anchor></Anchors>
</Frame>
<!-- The Trade button (ref l.502, UIPanelButtonTemplate inlined): OnClick → AcceptTrade. -->
<Button name="BenillaTradeFrameTradeButton" text="Trade">
<Button name="TradeFrameTradeButton" text="Trade">
<Size><AbsDimension x="85" y="22"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="BenillaTradeFrame" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-113" y="55"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="TradeFrame" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-113" y="55"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
@@ -432,32 +434,32 @@
<NormalFont inherits="GameFontNormal"/><HighlightFont inherits="GameFontHighlight"/><DisabledFont inherits="GameFontDisable"/>
<Scripts><OnClick>AcceptTrade()</OnClick></Scripts>
</Button>
<Button name="BenillaTradeFrameCancelButton" text="Cancel">
<Button name="TradeFrameCancelButton" text="Cancel">
<Size><AbsDimension x="77" y="22"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeFrameTradeButton" relativePoint="TOPRIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrameTradeButton" relativePoint="TOPRIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
<ButtonText name="$parentText"/>
<NormalFont inherits="GameFontNormal"/><HighlightFont inherits="GameFontHighlight"/><DisabledFont inherits="GameFontDisable"/>
<Scripts><OnClick>BenillaTradeFrameCancelButton_OnClick()</OnClick></Scripts>
<Scripts><OnClick>TradeFrameCancelButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The window close X (ref l.544, UIPanelCloseButton inlined). -->
<Button name="BenillaTradeFrameCloseButton">
<Button name="TradeFrameCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="BenillaTradeFrame" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-25" y="-8"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPRIGHT" relativeTo="TradeFrame" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-25" y="-8"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts><OnClick>HideUIPanel(BenillaTradeFrame)</OnClick></Scripts>
<Scripts><OnClick>HideUIPanel(TradeFrame)</OnClick></Scripts>
</Button>
<!-- Our gold — EDITABLE (decision 0592 P2, ref TradePlayerInputMoneyFrame l.553): the shared
BenillaMoneyInput trio (three numeric edit boxes Gold/Silver/Copper). A keystroke drives
BenillaTradeFrame_UpdateMoney → SetTradeMoney → CMSG_SET_TRADE_GOLD; the server's echo is
TradeFrame_UpdateMoney → SetTradeMoney → CMSG_SET_TRADE_GOLD; the server's echo is
reflected back through PLAYER_TRADE_MONEY (below). Box layout copied from the proven
send-mail widget (MailFrame.xml). -->
<Frame name="BenillaTradePlayerInputMoney">
<Frame name="TradePlayerInputMoney">
<Size><AbsDimension x="176" y="18"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="-73"/></Offset></Anchor></Anchors>
<Frames>
@@ -476,7 +478,7 @@
<OnTabPressed>getglobal(this:GetParent():GetName().."Silver"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName().."Silver"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>if ( BenillaMoneyInput_Consume("BenillaTradePlayerInputMoney") ) then BenillaTradeFrame_UpdateMoney() end</OnTextChanged>
<OnTextChanged>if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() end</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
@@ -497,7 +499,7 @@
<OnTabPressed>getglobal(this:GetParent():GetName().."Copper"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName().."Copper"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>if ( BenillaMoneyInput_Consume("BenillaTradePlayerInputMoney") ) then BenillaTradeFrame_UpdateMoney() end</OnTextChanged>
<OnTextChanged>if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() end</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
@@ -516,7 +518,7 @@
</Layers>
<Scripts>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>if ( BenillaMoneyInput_Consume("BenillaTradePlayerInputMoney") ) then BenillaTradeFrame_UpdateMoney() end</OnTextChanged>
<OnTextChanged>if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() end</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
@@ -525,7 +527,7 @@
</Frames>
</Frame>
<!-- The partner's gold (read-only — GetTargetTradeMoney; ref TradeRecipientMoneyFrame l.574). -->
<Frame name="BenillaTradeRecipientMoneyFrame">
<Frame name="TradeRecipientMoneyFrame">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-40" y="-80"/></Offset></Anchor></Anchors>
<Frames>
@@ -548,10 +550,10 @@
</Frame>
</Frames>
<Scripts>
<OnLoad>BenillaTradeFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaTradeFrame_OnEvent(self)</OnEvent>
<OnShow>BenillaTradeFrame_OnShow(); PlaySound("igCharacterInfoOpen")</OnShow>
<OnHide>BenillaTradeFrame_OnHide(); PlaySound("igCharacterInfoClose")</OnHide>
<OnLoad>TradeFrame_OnLoad()</OnLoad>
<OnEvent>TradeFrame_OnEvent()</OnEvent>
<OnShow>TradeFrame_OnShow(); PlaySound("igCharacterInfoOpen")</OnShow>
<OnHide>TradeFrame_OnHide(); PlaySound("igCharacterInfoClose")</OnHide>
</Scripts>
</Frame>
</Ui>
@@ -24,7 +24,7 @@
token those NPC-session windows use), the UIPanel slot system (carries NO hardcoded
<Anchors> — ShowUIPanel/SetLeftFrame SetPoints it), OnShow/OnHide PlaySound pair.
- TrainerFrame.xml — the FLAT layout constraint (below), the faux-scroll list kit
(BenillaFauxScrollFrameTemplate, ScrollTemplates.xml), the locally-reconstructed
(FauxScrollFrameTemplate, ScrollTemplates.xml), the locally-reconstructed
UIPanelButtonTemplate/close-button/item-button idioms (no such templates ship in this house
dialect — TalentFrame.xml's own ENGINE API note), the persistent selection-highlight Frame.
@@ -127,8 +127,8 @@
`GetText("*_ABBR", nil, n)` locale-pluralization dispatch collapses to English
singular/plural directly — `UiPanels.xml`'s `StaticPopupTimeUnit` is the identical-shaped
precedent for this SAME `GetText` call elsewhere in the reference.
- Lua-5.0 `arg`/`arg.n` varargs have no equivalent (`LUA_COMPAT_VARARG` isn't shipped,
WorldMapFrame.xml/TalentFrame.xml's own identical note) — `BuildColoredListString`'s
- Lua-5.0 `arg`/`arg.n` varargs are written the 5.1-native way
(WorldMapFrame.xml/TalentFrame.xml's own identical note) — `BuildColoredListString`'s
varargs use `select("#", ...)`/`select(i, ...)`, their own precedent for the
substitution; the new `TradeSkillFilterFrame_LoadSubClasses`/`_LoadInvSlots` (ref l.331/
377) use the same substitution.
@@ -412,8 +412,8 @@
end
local scrollFrame = getglobal("BenillaTradeSkillListScrollFrame")
BenillaFauxScrollFrame_Update(scrollFrame, numTradeSkills, TRADE_SKILLS_DISPLAYED, TRADE_SKILL_HEIGHT)
local skillOffset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
FauxScrollFrame_Update(scrollFrame, numTradeSkills, TRADE_SKILLS_DISPLAYED, TRADE_SKILL_HEIGHT)
local skillOffset = FauxScrollFrame_GetOffset(scrollFrame)
local highlight = getglobal("BenillaTradeSkillHighlight")
highlight:Hide()
@@ -545,7 +545,7 @@
-- doc), using the button's own `.collapsed` flag (re-derived every Update() from the real
-- header states above, so it stays honest regardless of how a fold happened). The ref's raw
-- `TradeSkillListScrollFrameScrollBar:SetValue(0)` becomes this file's own
-- `BenillaFauxScrollFrame_SetOffset(...,0)` wrapper (already this file's own Show/OnEvent
-- `FauxScrollFrame_SetOffset(...,0)` wrapper (already this file's own Show/OnEvent
-- convention). No direct `BenillaTradeSkillFrame_Update()` call — the ref doesn't call one
-- either; `CollapseTradeSkillSubClass`/`ExpandTradeSkillSubClass` raise the engine's touched
-- flag, answered with `TRADE_SKILL_UPDATE` the same frame (header comment, deviation 1). ──────
@@ -555,7 +555,7 @@
ExpandTradeSkillSubClass(0)
else
self.collapsed = true
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
CollapseTradeSkillSubClass(0)
end
end
@@ -781,7 +781,7 @@
else
BenillaTradeSkillFrame_SetSelection(GetTradeSkillSelectionIndex())
end
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
SetPortraitTexture(getglobal("BenillaTradeSkillFramePortrait"), "player")
BenillaTradeSkillFrame_Update()
ShowUIPanel(frame)
@@ -797,7 +797,7 @@
BenillaTradeSkillFrame_SetSelection(selection)
elseif GetNumTradeSkills() > 0 then
BenillaTradeSkillFrame_SetSelection(GetFirstTradeSkill())
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
end
BenillaTradeSkillFrame_Update()
elseif event == "TRADE_SKILL_CLOSE" then
@@ -1131,11 +1131,11 @@
<Button name="BenillaTradeSkillSkill7" inherits="BenillaTradeSkillSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeSkillSkill6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTradeSkillSkill8" inherits="BenillaTradeSkillSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTradeSkillSkill7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- The faux-scroll frame (BenillaFauxScrollFrameTemplate, ScrollTemplates.xml): invisible
<!-- The faux-scroll frame (FauxScrollFrameTemplate, ScrollTemplates.xml): invisible
container over the 8 rows; only its right-hand Slider scroll bar renders. Sized to the
8-row span (TRADE_SKILLS_DISPLAYED × TRADE_SKILL_HEIGHT), TrainerFrame.xml's own
same-TOPLEFT-as-row1 convention. updateFunc set in BenillaTradeSkillFrame_OnLoad. -->
<Frame name="BenillaTradeSkillListScrollFrame" inherits="BenillaFauxScrollFrameTemplate">
<Frame name="BenillaTradeSkillListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-96"/></Offset></Anchor></Anchors>
</Frame>
@@ -31,7 +31,7 @@
the shipped 1.12.1 Blizzard_TrainerUI.lua:6-9's own values — restored from
benilla/saved-variables.lua at load, and pushed into the engine on show because the list
builder resets its mask on every packet.
- THE SCROLL BAR IS REAL. BenillaTrainerListScrollFrame (BenillaFauxScrollFrameTemplate): a
- THE SCROLL BAR IS REAL. BenillaTrainerListScrollFrame (FauxScrollFrameTemplate): a
draggable Slider bar + up/down step arrows over the fixed 11 rows (the faux-scroll re-binds
rows to a moving offset — it never really scrolls a child). The wheel still lives on the
WINDOW (the engine bubbles a spin up the parent chain, so a spin over a row reaches here).
@@ -258,8 +258,8 @@
-- The faux-scroll bar reconciles to the row count, then hands back the top-row offset.
local scrollFrame = getglobal("BenillaTrainerListScrollFrame")
BenillaFauxScrollFrame_Update(scrollFrame, numServices, BENILLA_TRAINER_ROWS, BENILLA_TRAINER_ROW_HEIGHT)
local skillOffset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
FauxScrollFrame_Update(scrollFrame, numServices, BENILLA_TRAINER_ROWS, BENILLA_TRAINER_ROW_HEIGHT)
local skillOffset = FauxScrollFrame_GetOffset(scrollFrame)
getglobal("BenillaTrainerSkillHighlight"):Hide()
@@ -445,7 +445,7 @@
end
setglobal("TRAINER_FILTER_" .. strupper(this.value), on)
SetTrainerServiceTypeFilter(this.value, on)
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
BenillaTrainerFrame_Update()
end
@@ -459,7 +459,7 @@
ExpandTrainerSkillLine(0)
else
b.collapsed = 1
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
CollapseTrainerSkillLine(0)
end
BenillaTrainerFrame_Update()
@@ -503,7 +503,7 @@
-- Fresh session: top of the list, a first selection (ref ClassTrainerFrame_Show).
BenillaTrainerFrame_ApplyFilter()
BenillaTrainerFrame_SetTitle(arg1)
BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
if GetTrainerSelectionIndex() == 0 then
selectFirstService()
end
@@ -750,11 +750,11 @@
<Button name="BenillaTrainerService10" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService11" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService10" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- The faux-scroll frame (BenillaFauxScrollFrameTemplate, ScrollTemplates.xml): invisible
<!-- The faux-scroll frame (FauxScrollFrameTemplate, ScrollTemplates.xml): invisible
container over the 11 rows; only its right-hand Slider scroll bar renders. Sized to the
11-row span so the bar sits in the window's right gutter. updateFunc is set in
BenillaTrainerFrame_OnLoad. -->
<Frame name="BenillaTrainerListScrollFrame" inherits="BenillaFauxScrollFrameTemplate">
<Frame name="BenillaTrainerListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="176"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
</Frame>
@@ -15,9 +15,17 @@
CloseDropDownMenus(level), Disable/EnableButton, SetButtonText, SetAnchor, JustifyText,
GetCurrentDropDown, the 2-second hover-away auto-close counting through the level chain.
THE COLOR SWATCH ROW came in with the colour picker (ColorPickerFrame.xml): the full
`hasColorSwatch` / `r` / `g` / `b` / `swatchFunc` / `hasOpacity` / `opacity` / `opacityFunc` /
`cancelFunc` field set, the `$parentColorSwatch` button on the row template, and
`UIDropDownMenuButton_OpenColorPicker` (ref UIDropDownMenu.lua l.801-815). It is the second half
of one feature, not a separate one: `hasColorSwatch` draws the square, clicking the square is
the ONLY way the reference's colour picker ever opens, and `hasColorSwatch` is **145 files /
82 corpus addons**.
OUT (each waits for its first customer): menu level 3 (UIDROPDOWNMENU_MAXLEVELS is 2 — the
unit popups' deepest law; the XML gains DropDownList3 the day a customer needs it), color
swatches + the color picker + opacity, info.textHeight's per-row SetFont, the newbie
unit popups' deepest law; the XML gains DropDownList3 the day a customer needs it),
info.textHeight's per-row SetFont, the newbie
tooltips on rows (the ref gates them behind a default-off cvar), message() on the row/level
overflow guards (no dev-error dialog surface; the guard returns silently). The uiScale
correction came IN with its first scaled customer (0992, the options window's dropdown
@@ -41,8 +49,9 @@
- MENU displayMode hides the capsule art trio when present but skips the ref's toggle-button
texture blanking + re-anchor (every MENU customer here is a born-hidden 10x10 frame whose
capsule never draws).
- The disabled-row InvisibleButton spans to the row's RIGHT (the ref stops it at the color
swatch, which is OUT).
- (Was: "the disabled-row InvisibleButton spans to the row's RIGHT — the ref stops it at the
color swatch, which is OUT". The swatch is IN now, so the ref's own anchor is restored:
`RIGHT relativeTo="$parentColorSwatch" relativePoint="LEFT"`.)
- The list OnShow resets showTimer AND isCounting (the ref writes `showTime`, a field
nothing reads — transcribed as the intent, not the typo).
-->
@@ -97,8 +106,10 @@
end
-- The ref's width law, run from the list's OnUpdate (the header's WIDTH SETTLE): rows
-- measure text + 60, +10 for an arrow, +10 for an icon, -30 when notCheckable; the list
-- takes max + 15. Waits until every text row has a real (nonzero) measure.
-- measure text + 60, +10 for an arrow OR a colour swatch, +10 for an icon, -30 when
-- notCheckable; the list takes max + 15. Waits until every text row has a real (nonzero)
-- measure. (The ref's `if ( info.hasArrow or info.hasColorSwatch )` is ONE +10, not two —
-- the two ride the same right-hand slot, and a row never has both.)
function UIDropDownMenu_SettleWidth(listFrame)
local listFrameName = listFrame:GetName();
if ( listFrame.fixedWidth ) then
@@ -116,7 +127,7 @@
if ( width > 0 ) then
seen = seen + 1;
width = width + 60;
if ( button.hasArrow ) then
if ( button.hasArrow or button.hasColorSwatch ) then
width = width + 10;
end
if ( button.notCheckable ) then
@@ -296,6 +307,15 @@
button.func = info.func;
button.owner = info.owner;
-- The colour-swatch half of the info table (ref l.229-233), passed to the row so the
-- swatch's OnClick can hand it all to ColorPickerFrame. `swatchFunc` becomes the
-- picker's `func`, which is why it is the picker's LIVE-preview callback and not an
-- on-accept one.
button.hasOpacity = info.hasOpacity;
button.opacity = info.opacity;
button.opacityFunc = info.opacityFunc;
button.cancelFunc = info.cancelFunc;
button.swatchFunc = info.swatchFunc;
button.keepShownOnClick = info.keepShownOnClick;
button.tooltipTitle = info.tooltipTitle;
button.tooltipText = info.tooltipText;
@@ -376,6 +396,20 @@
end
button.checked = info.checked;
-- The colour square (ref l.318-328). Its NormalTexture is the swatch art, tinted to the
-- row's colour; the row keeps r/g/b so the swatch's OnClick can seed the picker.
local colorSwatch = getglobal(listFrameName.."Button"..index.."ColorSwatch");
if ( info.hasColorSwatch ) then
getglobal(colorSwatch:GetName().."NormalTexture"):SetVertexColor(info.r, info.g, info.b);
button.r = info.r;
button.g = info.g;
button.b = info.b;
colorSwatch:Show();
else
colorSwatch:Hide();
end
button.hasColorSwatch = info.hasColorSwatch;
-- notClickable disables white; isTitle disables gold (ref order — title wins).
if ( info.notClickable ) then
info.disabled = 1;
@@ -743,6 +777,26 @@
return getglobal(this:GetName().."NormalText"):GetText();
end
-- ref l.801-815, verbatim. THE entry point to the colour picker: every colour option in
-- every FrameXML dropdown and every Ace2/Dewdrop menu row lands here. Note what it does
-- that the picker itself does not — `previousValues` is captured HERE, and it is the table
-- `cancelFunc` is handed, so Cancel restoring the old colour is this line's doing.
function UIDropDownMenuButton_OpenColorPicker(button)
CloseMenus();
if ( not button ) then
button = this;
end
UIDROPDOWNMENU_MENU_VALUE = button.value;
ColorPickerFrame.func = button.swatchFunc;
ColorPickerFrame.hasOpacity = button.hasOpacity;
ColorPickerFrame.opacityFunc = button.opacityFunc;
ColorPickerFrame.opacity = button.opacity;
ColorPickerFrame:SetColorRGB(button.r, button.g, button.b);
ColorPickerFrame.previousValues = {r = button.r, g = button.g, b = button.b, opacity = button.opacity};
ColorPickerFrame.cancelFunc = button.cancelFunc;
ShowUIPanel(ColorPickerFrame);
end
function UIDropDownMenu_DisableButton(level, id)
getglobal("DropDownList"..level.."Button"..id):Disable();
end
@@ -781,6 +835,41 @@
</Layer>
</Layers>
<Frames>
<!-- ref UIDropDownMenuTemplates.xml l.38-83. The colour square: a 16x16 button whose
NormalTexture is the ChatFrameColorSwatch art, tinted per row, over a 14x14
`$parentSwatchBg` that the hover recolours (gold in, white out) — that background
is the row's only hover feedback, since the square itself is the row's colour.
Clicking it is the ONLY thing in the whole reference UI that opens the colour
picker. -->
<Button name="$parentColorSwatch" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentSwatchBg">
<Size><AbsDimension x="14" y="14"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Color r="1.0" g="1.0" b="1.0"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnClick>
CloseMenus();
UIDropDownMenuButton_OpenColorPicker(this:GetParent());
</OnClick>
<OnEnter>
CloseDropDownMenus(this:GetParent():GetParent():GetID() + 1);
getglobal(this:GetName().."SwatchBg"):SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b);
UIDropDownMenu_StopCounting(this:GetParent():GetParent());
</OnEnter>
<OnLeave>
getglobal(this:GetName().."SwatchBg"):SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b);
UIDropDownMenu_StartCounting(this:GetParent():GetParent());
</OnLeave>
</Scripts>
<NormalTexture name="$parentNormalTexture" file="Interface\ChatFrame\ChatFrameColorSwatch"/>
</Button>
<Button name="$parentExpandArrow" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor></Anchors>
@@ -799,12 +888,13 @@
<NormalTexture file="Interface\ChatFrame\ChatFrameExpandArrow"/>
</Button>
<!-- Disabled rows don't get mouse events; this transparent cover keeps the hover
alive (timeout + deeper-level close). Spans to the row's RIGHT (header note). -->
alive (timeout + deeper-level close). It stops at the colour swatch's LEFT edge
(ref l.111-113) so a disabled row's swatch is still clickable. -->
<Button name="$parentInvisibleButton" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMLEFT"/>
<Anchor point="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentColorSwatch" relativePoint="LEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnEnter>
@@ -0,0 +1,433 @@
<Ui>
<!--
The reference's SHARED widget kit — Interface\FrameXML\UIPanelTemplates.xml, transcribed
from the extracted 1.12.1 file (line numbers below are that file's).
WHY THIS FILE EXISTS AT ALL. Every window benilla has built so far declared its own private
copy of these — `AddonListPanelButtonTemplate`, `BenillaTrainerPanelButtonTemplate`,
`BenillaMacroPanelButtonTemplate` and a dozen siblings, each the same four state textures on
the same UI-Panel-Button art. That was fine while the only consumer was our own XML. It
stopped being fine the day `CreateFrame`'s fourth argument started working (decision 1203):
an addon writes `CreateFrame("Button", "MyBtn", UIParent, "UIPanelButtonTemplate")` and
nothing else, and 185 of the corpus's 218 addons' template call sites name templates we had
never declared. These are those templates, under the reference's own names — the 1190 rule
(a name the shipped 1.12 UI defines is FrameXML's, and belongs in `assets/ui`) applied to
the ranked list `addon_harness` now prints.
Every name here is in `reference/1.12-globals.tsv`; none is invented.
NO DEVIATIONS — the state textures below are the reference's, `<NormalTexture
inherits="UIPanelButtonUpTexture"/>` and all. That is worth a note only because it was not
true when this file first landed: the loader expanded `inherits=` on `<Layers>` regions and
on nested `<Frames>` children but not on a Button's state-texture or a Slider's thumb, so a
faithful transcription produced buttons with no art AND NO ERROR. The first version carried
the resolved art inline as a workaround; both gaps are now fixed in
`loader/widgets.rs` (`apply_button`/`apply_slider` call `expand_region`, the same call a
`<Layers>` region gets) and the workaround is gone. Reverting either fix makes
`a_panel_button_from_the_template_labels_and_paints` and
`a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep` fail.
NOT CARRIED, deliberately:
· `FauxScrollFrameTemplate` (ref l.213-225) — benilla already ships one under the reference's
own name in ScrollTemplates.xml, built as a plain `<Frame>` over `BenillaScrollBarTemplate`
(decisions 0247/0250/0251). Two declarations of one name would race; re-pointing ours at
the reference's `<ScrollFrame>` shape is its own job with its own risk, not this pass's.
· `TabButtonTemplate` (ref l.303-402) — already transcribed verbatim in UiPanels.xml, with
its fit/highlight deviations recorded there (decision 1004).
(`GameMenuButtonTemplate`, ref l.403-410, was on this list as "already in GameMenuFrame.xml"
until the colour picker needed it from above that file's LAST-in-the-toc seat. It is below
now, in the reference's own file for it, and GameMenuFrame.xml's private copy is gone.)
AND WHAT THIS SUPERSEDES BUT DOES NOT DELETE: `UIPanelButtonTemplate` below is the shared
original of `AddonListPanelButtonTemplate`, `BenillaQuestPanelButtonTemplate`,
`BenillaCraftPanelButtonTemplate`, `BenillaQuestLogPanelButtonTemplate`,
`BenillaFriendsPanelButtonTemplate`, `BenillaTrainerPanelButtonTemplate`,
`BenillaMacroPanelButtonTemplate`, `BenillaTradeSkillPanelButtonTemplate` and
`BenillaDressUpPanelButtonTemplate` — nine copies of the same eight lines. Re-pointing those
windows at this template is a separate, mechanical pass; nothing here changes them.
-->
<Script><![CDATA[
-- The two scroll-frame handlers `UIPanelScrollFrameTemplate` below calls by name (ref
-- UIPanelTemplates.lua l.150-157 and l.254-305). `ScrollFrame_OnLoad` and the whole
-- `FauxScrollFrame_*` family already live in ScrollTemplates.xml, which loads immediately
-- before this file; these two are the remainder of that kit.
-- ref l.150-157, verbatim.
function ScrollFrameTemplate_OnMouseWheel(value)
local scrollBar = getglobal(this:GetName() .. "ScrollBar")
if (value > 0) then
scrollBar:SetValue(scrollBar:GetValue() - (scrollBar:GetHeight() / 2))
else
scrollBar:SetValue(scrollBar:GetValue() + (scrollBar:GetHeight() / 2))
end
end
-- ref l.254-305, verbatim — including `getglobal(scrollbar:GetName().."ThumbTexture")`,
-- which is what `<ThumbTexture name="$parentThumbTexture">` publishes. (That publication
-- did not exist when this file was first written: `apply_slider` applied a named thumb's
-- layout and never set the global, the one asymmetry with a Button's state textures. It
-- was the reference's own line that found the gap, and the gap is fixed rather than
-- routed around.)
function ScrollFrame_OnScrollRangeChanged(scrollrange)
local scrollbar = getglobal(this:GetName() .. "ScrollBar")
if (not scrollrange) then
scrollrange = this:GetVerticalScrollRange()
end
local value = scrollbar:GetValue()
if (value > scrollrange) then
value = scrollrange
end
scrollbar:SetMinMaxValues(0, scrollrange)
scrollbar:SetValue(value)
if (floor(scrollrange) == 0) then
if (this.scrollBarHideable) then
scrollbar:Hide()
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Hide()
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Hide()
else
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Disable()
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Disable()
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Show()
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Show()
end
getglobal(scrollbar:GetName() .. "ThumbTexture"):Hide()
else
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Show()
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Show()
scrollbar:Show()
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Enable()
getglobal(scrollbar:GetName() .. "ThumbTexture"):Show()
end
-- Hide/show scrollframe borders (ref l.285-304). Named regions an OWNER declares, so
-- all three are optional.
local top = getglobal(this:GetName() .. "Top")
local bottom = getglobal(this:GetName() .. "Bottom")
local middle = getglobal(this:GetName() .. "Middle")
if (top and bottom and this.scrollBarHideable) then
if (this:GetVerticalScrollRange() == 0) then
top:Hide()
bottom:Hide()
else
top:Show()
bottom:Show()
end
end
if (middle and this.scrollBarHideable) then
if (this:GetVerticalScrollRange() == 0) then
middle:Hide()
else
middle:Show()
end
end
end
]]></Script>
<!-- The five shared panel-button faces (ref l.3-17). Declared under their own names because
they ARE 1.12 globals an addon's XML may inherit; the state textures below also carry the
resolved art inline, for the reason in the header. -->
<Texture name="UIPanelButtonUpTexture" file="Interface\Buttons\UI-Panel-Button-Up" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonDownTexture" file="Interface\Buttons\UI-Panel-Button-Down" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonDisabledTexture" file="Interface\Buttons\UI-Panel-Button-Disabled" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonDisabledDownTexture" file="Interface\Buttons\UI-Panel-Button-Disabled-Down" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonHighlightTexture" file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<!-- ref l.18-27. THE panel button: 19 call sites across 3 corpus addons, and the shared
original of nine private copies in our own assets/ui. Note it declares no `<Size>` — an
instance sets its own, and a bare CreateFrame of it comes out 0x0 until the addon does. -->
<Button name="UIPanelButtonTemplate" virtual="true">
<ButtonText name="$parentText"/>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture inherits="UIPanelButtonUpTexture"/>
<PushedTexture inherits="UIPanelButtonDownTexture"/>
<DisabledTexture inherits="UIPanelButtonDisabledTexture"/>
<HighlightTexture inherits="UIPanelButtonHighlightTexture"/>
</Button>
<!-- ref l.403-410, verbatim. The ESC menu's button: a 144x21 UIPanelButtonTemplate whose label
trio is overridden to GameFontHighlight/GameFontHighlight/GameFontDisable — the menu's
labels are WHITE, not the gold a normal panel button wears. It lived in GameMenuFrame.xml
with its art inlined until ColorPickerFrame.xml needed it too (the reference's own colour
picker builds its Okay/Cancel from this template, and loads far ahead of the game menu,
which is deliberately LAST in the toc). Moving it here is where the reference has it. -->
<Button name="GameMenuButtonTemplate" inherits="UIPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="144" y="21"/></Size>
<NormalFont inherits="GameFontHighlight"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
</Button>
<!-- ref l.28-81. The THREE-SLICE panel button: same art, cut 12/stretch/12 so it can be any
width without the end caps smearing, and its own OnMouseDown/OnMouseUp swap the three
slices to the Down sheet (there is no PushedTexture on this one — the layer regions ARE
the pressed state). The slices are `<Layers>` regions, so their `$parent` names publish
and the handlers' getglobal finds them. -->
<Button name="UIPanelButtonTemplate2" virtual="true">
<Size><AbsDimension x="40" y="22"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Buttons\UI-Panel-Button-Up">
<Size><AbsDimension x="12" y="22"/></Size>
<Anchors><Anchor point="LEFT"/></Anchors>
<TexCoords left="0" right="0.09375" top="0" bottom="0.6875"/>
</Texture>
<Texture name="$parentRight" file="Interface\Buttons\UI-Panel-Button-Up">
<Size><AbsDimension x="12" y="22"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.53125" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Buttons\UI-Panel-Button-Up">
<Size><AbsDimension x="12" y="22"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.09375" right="0.53125" top="0" bottom="0.6875"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>
getglobal(this:GetName() .. "Left"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
getglobal(this:GetName() .. "Middle"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
getglobal(this:GetName() .. "Right"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
</OnMouseDown>
<OnMouseUp>
getglobal(this:GetName() .. "Left"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
getglobal(this:GetName() .. "Middle"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
getglobal(this:GetName() .. "Right"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
</OnMouseUp>
</Scripts>
<ButtonText name="$parentText"/>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<HighlightTexture inherits="UIPanelButtonHighlightTexture"/>
</Button>
<!-- ref l.82-90. The permanently-greyed variant: the Disabled sheet in the Normal slot, white
label. Used for a button that reads as unavailable but still takes clicks. -->
<Button name="UIPanelButtonGrayTemplate" virtual="true">
<NormalFont inherits="GameFontHighlight"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture inherits="UIPanelButtonDisabledTexture"/>
<PushedTexture inherits="UIPanelButtonDisabledDownTexture"/>
<DisabledTexture inherits="UIPanelButtonDisabledTexture"/>
<HighlightTexture inherits="UIPanelButtonHighlightTexture"/>
</Button>
<!-- ref l.91-103. The round X in a window's top-right corner — 8 call sites across 4 corpus
addons, the most-wanted template after the checkbox and the panel button. Note the name has
no `Template` suffix; that is the reference's own spelling, and the reference-globals table
agrees. Its OnClick is `HideUIPanel(this:GetParent())`, the verb UiPanels.xml defines: a
Lua global resolved at click time, so the load order between the two files is free. -->
<Button name="UIPanelCloseButton" virtual="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Scripts>
<OnClick>HideUIPanel(this:GetParent())</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
</Button>
<!-- ref l.104-124. The scroll-arrow kit. `UIPanelScrollBarButton` is the shared crop — the
1.12 arrow art is 32x32 with the glyph in the centre 16, which is what the 0.25..0.75
TexCoords take (ScrollTemplates.xml's own bar applies the identical crop). -->
<Texture name="UIPanelScrollBarButton" virtual="true">
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</Texture>
<Button name="UIPanelScrollUpButtonTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Up"/>
<PushedTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Down"/>
<DisabledTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Disabled"/>
<HighlightTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Highlight" alphaMode="ADD"/>
</Button>
<Button name="UIPanelScrollDownButtonTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Up"/>
<PushedTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Down"/>
<DisabledTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Disabled"/>
<HighlightTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Highlight" alphaMode="ADD"/>
</Button>
<!-- ref l.125-165. The scroll bar itself: a Slider carrying its two step buttons as nested
frames, so `$parentScrollUpButton`/`$parentScrollDownButton` publish under the OWNER's
name two levels down (`MyScrollScrollBarScrollUpButton`) — which is exactly what
`ScrollFrame_OnLoad` and `FauxScrollFrame_Update` getglobal for. The value model here is
the REFERENCE's: `OnValueChanged` pushes the value straight into the parent ScrollFrame's
`SetVerticalScroll`, i.e. pixels of pan. (benilla's own `BenillaScrollBarTemplate` in
ScrollTemplates.xml is the faux twin of this — same art and same child names, but its
OnValueChanged drives a row offset instead. Both are needed: ours serves our windows, this
serves an addon that names the reference template.) -->
<Slider name="UIPanelScrollBarTemplate" virtual="true">
<Size><AbsDimension x="16" y="0"/></Size>
<Frames>
<Button name="$parentScrollUpButton" inherits="UIPanelScrollUpButtonTemplate">
<Anchors><Anchor point="BOTTOM" relativePoint="TOP"/></Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
parent:SetValue(parent:GetValue() - (parent:GetHeight() / 2))
PlaySound("UChatScrollButton")
</OnClick>
</Scripts>
</Button>
<Button name="$parentScrollDownButton" inherits="UIPanelScrollDownButtonTemplate">
<Anchors><Anchor point="TOP" relativePoint="BOTTOM"/></Anchors>
<Scripts>
<OnClick>
local parent = this:GetParent()
parent:SetValue(parent:GetValue() + (parent:GetHeight() / 2))
PlaySound("UChatScrollButton")
</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnValueChanged>this:GetParent():SetVerticalScroll(arg1)</OnValueChanged>
</Scripts>
<ThumbTexture name="$parentThumbTexture" inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-Knob">
<Size><AbsDimension x="16" y="16"/></Size>
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</ThumbTexture>
</Slider>
<!-- ref l.166-212. The REAL scroll frame: an engine `<ScrollFrame>` that pans and clips a
scroll child, with the bar above pinned down its right edge (inset 16 top and bottom so
the arrow buttons sit inside the track span). 6 call sites across 2 corpus addons.
`ScrollFrame_OnLoad` is ScrollTemplates.xml's (the reference's own function, already
transcribed there); the other two handlers are this file's script block. An instance
supplies its own `<ScrollChild>` — without one the frame has no scroll range at all, which
the loader now says out loud. -->
<ScrollFrame name="UIPanelScrollFrameTemplate" virtual="true">
<Frames>
<Slider name="$parentScrollBar" inherits="UIPanelScrollBarTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="6" y="-16"/></Offset></Anchor>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="6" y="16"/></Offset></Anchor>
</Anchors>
</Slider>
</Frames>
<Scripts>
<OnLoad>ScrollFrame_OnLoad()</OnLoad>
<OnScrollRangeChanged>ScrollFrame_OnScrollRangeChanged(arg2)</OnScrollRangeChanged>
<!-- The reference's own comment here reads "The code to disable the ScrollDownButton is
pretty hacked, but it works". Transcribed as it stands. -->
<OnVerticalScroll>
local scrollbar = getglobal(this:GetName() .. "ScrollBar")
scrollbar:SetValue(arg1)
local min
local max
min, max = scrollbar:GetMinMaxValues()
if (arg1 == 0) then
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Disable()
else
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Enable()
end
if ((scrollbar:GetValue() - max) == 0) then
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Disable()
else
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Enable()
end
</OnVerticalScroll>
<OnMouseWheel>ScrollFrameTemplate_OnMouseWheel(arg1)</OnMouseWheel>
</Scripts>
</ScrollFrame>
<!-- ref l.226-279. The standard text box: a three-slice Common-Input-Border under an EditBox,
with the reference's escape/focus trio. 5 call sites across 3 corpus addons. The
direct-child `<FontString>` is the box's TEXT region (the engine's LoadXML slot, not a
layer) — `loader/regions.rs`'s `apply_special_fontstrings`. -->
<EditBox name="InputBoxTemplate" enableMouse="true" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<!-- ref l.280-302. THE most-wanted template in the whole corpus: 69 `CreateFrame` call sites.
A 32x32 checkbox with its label hung off the right edge, and no scripts of its own — the
checked flag toggles in the engine BEFORE `OnClick` fires (script/button.rs), so an addon
that sets only an OnClick reads the new state there, which is what every one of those 69
sites assumes. -->
<CheckButton name="UICheckButtonTemplate" virtual="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
<HighlightTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/>
<DisabledCheckedTexture file="Interface\Buttons\UI-CheckBox-Check-Disabled"/>
</CheckButton>
<!-- ref l.411-437. The radio button — the same CheckButton machinery on the four-frame
UI-RadioButton strip (normal / checked / highlight are three crops of one 64x16 sheet).
Carried because it is the checkbox's sibling in the same file and costs nothing; no corpus
addon names it today. -->
<CheckButton name="UIRadioButtonTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="5" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<NormalTexture file="Interface\Buttons\UI-RadioButton">
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</NormalTexture>
<HighlightTexture file="Interface\Buttons\UI-RadioButton" alphaMode="ADD">
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</HighlightTexture>
<CheckedTexture file="Interface\Buttons\UI-RadioButton">
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</CheckedTexture>
</CheckButton>
</Ui>
+123 -29
View File
@@ -17,6 +17,29 @@
-->
<Frame name="UIParent" setAllPoints="true" frameStrata="MEDIUM"/>
<!--
WorldFrame — the reference's full-screen, bottom-of-the-strata frame that the 3D world
renders into. **Ours renders nothing**: benilla draws the world through Bevy's render
graph, not through a FrameScript frame, so this is a NAMED HANDLE and nothing more. That
is a disclosed divergence, not a pretence — and it is what makes the two things addons
actually do with it work: `WorldFrame:GetWidth()`/`GetHeight()` (the screen rect, which
this frame really has) and `frame:SetParent(WorldFrame)` / `SetPoint(..., WorldFrame,
...)` (parking a tooltip or an overlay under everything). Measured demand: 29 of the 218
vanilla-corpus addons die on their first load with `attempt to index global 'WorldFrame'`
without it (the corpus harness, `benilla_app::addon_harness`).
**No `enableMouse`, deliberately** — the same reason UIParent above ships without it, and
here it is load-bearing rather than incidental. benilla's `PointerOverUi` is "is the
topmost mouse-enabled frame under the cursor a UI frame", and it gates camera look, world
clicks and hover targeting (`player/camera.rs`, `target/hover.rs`); a full-screen
mouse-enabled plate would answer yes for every world pixel and kill all three. The
reference gets away with it because *its* WorldFrame IS the world, so a click landing
there is a world click by construction. Reproducing that here means teaching the arbiter
that a WorldFrame hit is not a UI hit — engine work with its own tests, not a rename — so
until then the addon-facing `OnMouseDown`/`OnMouseUp` hooks on this frame never fire.
-->
<Frame name="WorldFrame" setAllPoints="true" frameStrata="BACKGROUND"/>
<!--
The UIParent bottom-stack position manager - UIPARENT_MANAGED_FRAME_POSITIONS and
UIParent_ManageFramePositions(), transcribed from the extracted 1.12 UIParent.lua
@@ -36,17 +59,22 @@
SHOW_MULTI_ACTIONBAR_1/2; benilla's bottom bars are always-on with no options state
(decision 0270), so the flags read the bars' own visibility - identical values, no
parallel global to drift.
- Frame names: our stance bar is BenillaShapeshiftBarFrame (0270) and our pet bar is
BenillaPetActionBarFrame (0982), both substituted in the pet-bar visibility check —
either one showing raises the stack by the ref's "pet" delta, which is the ref's own
rule (the two bars share that row). Rows whose frames don't exist here (GroupLootFrame1,
TutorialFrameParent, FramerateLabel, ChatFrame2, MultiBarBottomLeft - ours is
BenillaMultiBarBottomLeft, statically anchored per 0270, like ShapeshiftBarFrame)
no-op through the ref's own `if ( frame )` guard and self-activate if the frames
ever land under those names.
- MultiBarLeft/MultiBarRight (the vertical bars),
ReputationWatchBar, MainMenuExpBar, and MainMenuBarMaxLevelBar don't exist yet:
nil-guarded (the ref reads them bare only because it always creates them).
- Frame names: our stance bar, pet bar and bottom multibars wear the reference's own
names (ShapeshiftBarFrame, PetActionBarFrame, MultiBarBottomLeft/Right), so their
rows and flags in the table below are LIVE, not the no-ops they were while those
frames carried a `Benilla` prefix. That is the payoff the bare names buy — the same
one `group_loot_tests::managed_positions_engage_for_the_bare_frame_name` pins for
GroupLootFrame1 — and it costs nothing on screen: each frame's static XML anchor was
already written to the value the row computes (the stance bar's XML (30,45) is the
row's xOffset 30 + baseY 0 + bottomLeft 45, and MultiBarBottomLeft's XML (0,17) is
its row's baseY 17), so the pass now re-derives what the XML asserted. TutorialFrameParent
and FramerateLabel still have no frame here and no-op through the ref's `if ( frame )`.
- MultiBarLeft/MultiBarRight (the vertical bars) and ReputationWatchBar don't exist yet:
nil-guarded (the ref reads them bare only because it always creates them). MainMenuExpBar
and MainMenuBarMaxLevelBar DO exist now, so the `reputation` and `maxLevel` flags are
real reads — `reputation` still cannot fire (it needs ReputationWatchBar too) and
`maxLevel` fires exactly when the level-60 rail replaces the XP bar, which is the ref's
own rule.
- No IsUserPlaced/IsObjectType/SIMPLE_CHAT legs on the SetPoint: benilla has no
user-placed frames and no simple-chat option, so the ref's guarded SetPoint
collapses to the plain one.
@@ -161,8 +189,8 @@
-- Set up flags. Departure: the bottom flags read the always-on bars' visibility,
-- not the ref's SHOW_MULTI_ACTIONBAR_1/2 option globals (0270 ships no such state).
local bottomLeftShown = BenillaMultiBarBottomLeft and BenillaMultiBarBottomLeft:IsShown()
local bottomRightShown = BenillaMultiBarBottomRight and BenillaMultiBarBottomRight:IsShown()
local bottomLeftShown = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
local bottomRightShown = MultiBarBottomRight and MultiBarBottomRight:IsShown()
if ( bottomLeftShown or bottomRightShown ) then
table.insert(yOffsetFrames, "bottomEither")
end
@@ -179,7 +207,7 @@
table.insert(xOffsetFrames, "rightRight")
end
if ( ( BenillaPetActionBarFrame and BenillaPetActionBarFrame:IsShown() ) or ( BenillaShapeshiftBarFrame and BenillaShapeshiftBarFrame:IsShown() ) ) then
if ( ( PetActionBarFrame and PetActionBarFrame:IsShown() ) or ( ShapeshiftBarFrame and ShapeshiftBarFrame:IsShown() ) ) then
table.insert(yOffsetFrames, "pet")
end
if ( ReputationWatchBar and MainMenuExpBar and ReputationWatchBar:IsShown() and MainMenuExpBar:IsShown() ) then
@@ -280,21 +308,21 @@
-- while the stance bar is up starts 20px past the last stance button. That case is
-- rare but real — a possessed or charmed unit gives a pet bar to a class that also
-- has forms.
local bottomLeftUp = BenillaMultiBarBottomLeft and BenillaMultiBarBottomLeft:IsShown()
if ( BenillaSlidingActionBarTexture0 and BenillaSlidingActionBarTexture1 ) then
local bottomLeftUp = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
if ( SlidingActionBarTexture0 and SlidingActionBarTexture1 ) then
if ( bottomLeftUp ) then
BenillaSlidingActionBarTexture0:Hide()
BenillaSlidingActionBarTexture1:Hide()
SlidingActionBarTexture0:Hide()
SlidingActionBarTexture1:Hide()
else
BenillaSlidingActionBarTexture0:Show()
BenillaSlidingActionBarTexture1:Show()
SlidingActionBarTexture0:Show()
SlidingActionBarTexture1:Show()
end
end
if ( BenillaPetActionBarFrame ) then
if ( PetActionBarFrame ) then
PETACTIONBAR_XPOS = 36
local forms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
if ( forms > 0 and BenillaShapeshiftBarFrame and BenillaShapeshiftBarFrame:IsShown() ) then
local last = getglobal("BenillaShapeshiftButton"..forms)
if ( forms > 0 and ShapeshiftBarFrame and ShapeshiftBarFrame:IsShown() ) then
local last = getglobal("ShapeshiftButton"..forms)
local right = last and last:GetRight()
if ( right ) then
PETACTIONBAR_XPOS = right + 20
@@ -303,7 +331,7 @@
-- The ref guards this on IsShown() because a hidden bar of its is mid-slide and
-- owns its own anchor; ours has no slide, so re-seating a hidden frame is a
-- no-op and the guard would only add an ordering trap (OnShow fires the pass).
BenillaPetActionBarFrame:SetPoint("TOPLEFT", "BenillaActionBar", "BOTTOMLEFT", PETACTIONBAR_XPOS, PETACTIONBAR_YPOS)
PetActionBarFrame:SetPoint("TOPLEFT", "MainMenuBar", "BOTTOMLEFT", PETACTIONBAR_XPOS, PETACTIONBAR_YPOS)
end
-- The under-minimap right stack (ref l.1740-1770): anchorY walks down capture bars →
@@ -339,10 +367,10 @@
-- QuestWatchFrame is last in the ref's walk (l.1770), seated at the running anchorY so
-- it drops below the durability guy when he shows instead of overlapping him (both
-- anchor this same MinimapCluster corner — director-caught). benilla's tracker is
-- BenillaQuestWatchFrame; nil-guarded because the durability-only test harness (and
-- QuestWatchFrame; nil-guarded because the durability-only test harness (and
-- any config without a quest log) ships no such frame.
if ( BenillaQuestWatchFrame ) then
BenillaQuestWatchFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X, anchorY)
if ( QuestWatchFrame ) then
QuestWatchFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X, anchorY)
end
end
@@ -429,8 +457,8 @@
-- that can pass nothing (a warlock minion's empty diet) must be gated by its own UI, and
-- it is — the diet icon is hunter-only. Quietly returning "" here would hide that.
--
-- Lua-5.0 `arg`/`arg.n` has no equivalent (LUA_COMPAT_VARARG isn't shipped) —
-- `select("#", ...)`/`select(i, ...)`, TradeSkillFrame.xml's own substitution.
-- Lua-5.0 `arg`/`arg.n` written as `select("#", ...)`/`select(i, ...)`, the 5.1-native
-- spelling — TradeSkillFrame.xml's own substitution.
function BuildListString(...)
local n = select("#", ...);
if ( n == 0 ) then
@@ -473,5 +501,71 @@
ChatFrameEditBox:Insert(link)
return true
end
-- ── LowerFrameLevel / RaiseFrameLevel (ref UIParent.lua l.1890-1896), verbatim ────────
--
-- Two lines each, and the reason to have them is that they are FRAMEXML, not engine: an
-- addon calling `RaiseFrameLevel(f)` is calling a function the client's own UI defines, and
-- without it that is `attempt to call global` at load (BetterCharacterStats dies there).
-- Our own ActionBar.xml already describes this operation in prose at three sites while
-- doing it by hand; those can move onto the real name whenever that file is next opened.
function LowerFrameLevel(frame)
frame:SetFrameLevel(frame:GetFrameLevel() - 1)
end
function RaiseFrameLevel(frame)
frame:SetFrameLevel(frame:GetFrameLevel() + 1)
end
-- ── MouseIsOver (ref UIParent.lua l.1388-1417), verbatim ──────────────────────────────
--
-- "is the cursor inside this frame, optionally with per-edge padding" — the idiom every
-- hover-driven addon writes, and **9 corpus addons call it**. Missing, each is
-- `attempt to call global 'MouseIsOver'`.
--
-- Transcribed rather than approximated, including the two things that look like bugs and
-- are not:
-- · the offsets are ADDED to every edge, so a positive `bottomOffset` shrinks the box
-- from below while a positive `topOffset` grows it upward. The reference's own caller
-- proves the intent — `MouseIsOver(chatFrame, 45, -10, -5, 5)` means 45 up into the
-- tab strip, 10 in from the bottom, 5 out each side.
-- · a nil `topOffset` zeroes ALL FOUR, so `MouseIsOver(f, nil, 5)` silently ignores the 5.
-- That is the reference's `if ( not topOffset ) then` block exactly; an addon written
-- against 1.12 may depend on it.
-- The reference's `-- Hack to fix a symptom not the real issue` guard on an unresolved rect
-- is kept with its comment, because a frame that has never been laid out really does answer
-- nil here and returning nil is what callers expect.
--
-- ONE DIVERGENCE, and it is a no-op: the reference divides the cursor by
-- `frame:GetEffectiveScale()` because 1.12 hands Lua screen pixels. benilla's UI space is
-- 1:1 logical px and `GetEffectiveScale` is the constant 1 (`script/pointer.rs`,
-- `object.rs` l.271), so the division is written out rather than left in to divide by one
-- forever. If a real UI scale ever lands, this is one of the sites that needs it back.
function MouseIsOver(frame, topOffset, bottomOffset, leftOffset, rightOffset)
local x, y = GetCursorPosition()
local left = frame:GetLeft()
local right = frame:GetRight()
local top = frame:GetTop()
local bottom = frame:GetBottom()
if not topOffset then
topOffset = 0
bottomOffset = 0
leftOffset = 0
rightOffset = 0
end
-- Hack to fix a symptom not the real issue
if not left then
return
end
left = left + leftOffset
right = right + rightOffset
top = top + topOffset
bottom = bottom + bottomOffset
if (x > left and x < right) and (y > bottom and y < top) then
return 1
else
return nil
end
end
]]></Script>
</Ui>
+87 -24
View File
@@ -8,9 +8,11 @@
Ground truth: the 1.12 `UIParent.lua` extract (scratchpad/UIParent.lua, patch-2.MPQ), quoted
verbatim in the line-numbered comments below — never re-derived. v1 scope (pin §3): the
**left + center two-slot logic only**. The real client's `full`/`doublewide` areas, the
`CanOpenPanels`/full-screen/native-center gates, `UIChildWindows`, and `UISpecialFrames` are
all out of scope — no benilla panel uses them yet — but the registration table keeps their
exact shape (`area`, `pushable`, `whileDead`) so a later row drops in unchanged.
`CanOpenPanels`/full-screen/native-center gates and `UIChildWindows` are all out of scope —
no benilla panel uses them yet — but the registration table keeps their exact shape
(`area`, `pushable`, `whileDead`) so a later row drops in unchanged. **`UISpecialFrames`
left that list in decision 1206**: it is not ours to use, it is the table an ADDON writes
its own window into, and 34 corpus call sites do.
Frame names here are OUR shipped names (`Benilla*`); UIParent.lua's names (`GossipFrame`,
`MerchantFrame`, `LootFrame`) are the semantic reference the pin cites, not what we register.
@@ -20,50 +22,52 @@
-- rows benilla ships/anticipates today (pin §1): Gossip + Merchant are live; Loot registers
-- now (pushable=7) so the coming loot window lands on an already-wired system.
UIPanelWindows = {}
UIPanelWindows["BenillaGossipFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["BenillaMerchantFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["GossipFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["MerchantFrame"] = { area = "left", pushable = 0 }
-- The mail window (decision 0544): the reference UIParent.lua l.37 registers MailFrame
-- area="left", pushable=0. The OpenMailFrame is NOT a panel — it is a special-frame toplevel
-- (UIParent.lua l.46 UISpecialFrames), closed by ESC in ToggleGameMenu below (this v1 drops
-- the UISpecialFrames table, so escapable toplevels are hard-listed there, like WorldMapFrame).
UIPanelWindows["BenillaMailFrame"] = { area = "left", pushable = 0 }
-- (UIParent.lua l.46 UISpecialFrames), closed by ESC in ToggleGameMenu below. Our own
-- escapable toplevels stay hard-listed in that chain rather than pushed into
-- UISpecialFrames — the table exists now (1206) but it is the ADDONS' list, and mixing our
-- windows into it would make an addon's `tremove` able to break the client's own ESC.
UIPanelWindows["MailFrame"] = { area = "left", pushable = 0 }
-- The trade window (decision 0592 P1, TradeFrame.xml): the ref's own UIParent.lua l.27
-- registration (`UIPanelWindows["TradeFrame"] = { area = "left", pushable = 1 }`). Without
-- this row ShowUIPanel takes its unregistered branch — a bare frame:Show() with no left-slot
-- placement — so the window flips IsShown() but never lands on screen (the bug: OPEN_WINDOW
-- arrived on both clients, yet no window). pushable=1 lets a pushable=0 panel (a merchant)
-- shove it to center, the reference's browse-while-trading arbitration.
UIPanelWindows["BenillaTradeFrame"] = { area = "left", pushable = 1 }
UIPanelWindows["TradeFrame"] = { area = "left", pushable = 1 }
-- The trainer window (decision 0237 phase 3, TrainerFrame.xml): an NPC session exactly like
-- Gossip/Merchant above (area "left", pushable 0) — competes for/replaces them at the slot.
UIPanelWindows["BenillaTrainerFrame"] = { area = "left", pushable = 0 }
-- The bank window (decision 0604 phase 4, BankFrame.xml): the ref's own UIParent.lua l.28
-- registration (`UIPanelWindows["BankFrame"] = { area = "left", pushable = 6 }`), quoted
-- verbatim — the same left-slot competition as the other NPC-session windows above.
UIPanelWindows["BenillaBankFrame"] = { area = "left", pushable = 6 }
UIPanelWindows["BankFrame"] = { area = "left", pushable = 6 }
-- The taxi-map window (decision 0484 phase 2, TaxiFrame.xml): the ref's own UIParent.lua
-- registration line (`UIPanelWindows["TaxiFrame"] = { area = "left", pushable = 0 }`),
-- quoted verbatim — same left-slot competition as Gossip/Merchant/Trainer above.
UIPanelWindows["BenillaTaxiFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["TaxiFrame"] = { area = "left", pushable = 0 }
-- The dressing room (decision 1060, DressUpFrame.xml): the ref's own UIParent.lua l.41
-- registration (`UIPanelWindows["DressUpFrame"] = { area = "left", pushable = 2 }`), quoted
-- verbatim — pushable=2 lets the NPC-session windows (pushable=0) shove it aside, which is
-- what makes "ctrl-click a merchant's item" show both.
UIPanelWindows["BenillaDressUpFrame"] = { area = "left", pushable = 2 }
UIPanelWindows["BenillaQuestFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["BenillaQuestLogFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["BenillaLootFrame"] = { area = "left", pushable = 7 }
UIPanelWindows["DressUpFrame"] = { area = "left", pushable = 2 }
UIPanelWindows["QuestFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["QuestLogFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["LootFrame"] = { area = "left", pushable = 7 }
-- The character window (decision 0208 phase 1a, CharacterFrame.xml): the ref's own
-- CharacterFrame row (area "left", pushable 0) — competes for/replaces the other left-slot
-- windows exactly like Gossip/Merchant/Quest/QuestLog above.
UIPanelWindows["BenillaCharacterFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["CharacterFrame"] = { area = "left", pushable = 0 }
-- The inspect window (decision 0631, InspectFrame.xml): the ref's own InspectFrame row
-- (`Blizzard_InspectUI.lua:4` — area "left", pushable 0), so inspecting swaps out whichever
-- left-slot window was open, exactly as the character window does.
UIPanelWindows["BenillaInspectFrame"] = { area = "left", pushable = 0 }
-- The spellbook (decision 0216 §8, SpellBookFrame.xml): the ref's own SpellBookFrame row
-- (area "left", pushable 0) — same left-slot competition as every window above.
UIPanelWindows["BenillaSpellBookFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["SpellBookFrame"] = { area = "left", pushable = 0 }
-- The talent window (decision 0304 §4, TalentFrame.xml): the ref's own TalentFrame row is
-- `{ area = "left", pushable = 6, whileDead = 1 }` — pushable 6 kept (one tier above the
-- plain NPC-session/player-state windows' 0, matching the ref exactly); `whileDead` is
@@ -74,7 +78,7 @@
-- The social window (decision 0668) — the ref's own registration line verbatim
-- (`UIPanelWindows["FriendsFrame"] = { area = "left", pushable = 0, whileDead = 1 }`,
-- UIParent.lua l.29). whileDead: a corpse run is exactly when you want to whisper someone.
UIPanelWindows["BenillaFriendsFrame"] = { area = "left", pushable = 0, whileDead = 1 }
UIPanelWindows["FriendsFrame"] = { area = "left", pushable = 0, whileDead = 1 }
-- The macro window (decision 0983, MacroFrame.xml) — the ref's own registration line
-- verbatim (`UIPanelWindows["MacroFrame"] = { area = "left", pushable = 5, whileDead = 1 }`,
-- Blizzard_MacroUI.lua:7). whileDead is the ref's and it earns its keep: writing a macro
@@ -375,9 +379,33 @@
frame:Hide()
end
-- CloseWindows(ignoreCenter) — UIParent.lua l.928-957, v1 left+center slice (the
-- doublewide/full-screen/UISpecialFrames handling, l.932-954, is dropped: none exist yet).
-- Implemented per the pin so the function exists; wiring it to ESC is a later slice.
-- UISpecialFrames (UIParent.lua l.52-55) — **the addon's own ESC list**, and the reason
-- this stopped being "dropped: none exist yet" (decision 1206).
--
-- `tinsert(UISpecialFrames, "MyFrame")` is THE 1.12 idiom for "ESC should close my
-- window", and it is 34 call sites across the corpus. With the table absent that line is
-- `bad argument #1 to 'tinsert' (table expected, got nil)` — which killed 11 addons at
-- load, the second-largest wall in the survey after the Lua-5.0 generic-for.
--
-- Seeded with the reference's own two entries — **both** of them now. 1206 seeded only
-- `ItemRefTooltip`, and its Rejected section wrote down why, verbatim: *"Listing
-- `ColorPickerFrame`. The reference seeds it; we do not ship one, and a name that resolves
-- to nothing is skipped harmlessly — but seeding a frame we lack would be writing a claim
-- we cannot honour."* ColorPickerFrame.xml ships it, so the claim is honourable now, and
-- the entry is the reference's own rather than one of ours.
--
-- Note it does NOT contradict the paragraph above (our own escapable toplevels stay
-- hard-listed in the ESC chain). The concern there is an addon's `tremove` breaking the
-- CLIENT's own ESC; here the ESC chain carries the colour picker on its own rung — and has
-- to, because ESC on this window must CANCEL (restore the colour), which this walk's plain
-- `Hide` cannot do. So the table entry is not what makes ESC work; it is what makes
-- `CloseWindows` put the picker away when something ELSE closes the world's windows —
-- opening the game menu, a `CloseAllWindows()` — which is exactly the reference's own
-- behaviour on that path, cancel-less Hide and all.
UISpecialFrames = { "ItemRefTooltip", "ColorPickerFrame" }
-- CloseWindows(ignoreCenter) — UIParent.lua l.928-957. The doublewide slice stays dropped
-- (we have none); the UISpecialFrames loop at l.947-954 is now carried.
function CloseWindows(ignoreCenter)
local leftFrame = GetLeftFrame()
local centerFrame = GetCenterFrame()
@@ -393,6 +421,18 @@
end
end
-- The special frames, the reference's own loop (l.947-954): a plain Hide, never
-- HideUIPanel, because these hold no panel slot. Its `for index, value in
-- UISpecialFrames do` is Lua 5.0's iterator-less generic-for, which our VM cannot run
-- (decision 1202) — `ipairs` is the same walk in the dialect we have.
for _, name in ipairs(UISpecialFrames) do
local frame = getglobal(name)
if frame and frame:IsVisible() then
frame:Hide()
found = 1
end
end
return found
end
@@ -467,7 +507,7 @@
-- The backpack (bag 0) always exists; an equipped-bag window only when a bag is in
-- the slot (the same existence test as BagFrame.xml's BenillaOpenAllBags).
local exists = bagId == 0
or (C_Container and C_Container.GetContainerNumSlots(bagId) > 0)
or (GetContainerNumSlots and GetContainerNumSlots(bagId) > 0)
local bag = exists and getglobal(BAG_WINDOWS[bagId + 1])
if bag then
if bag:IsShown() then
@@ -557,6 +597,29 @@
if StaticPopup_EscapePressed() then
return
end
-- The colour picker next (ColorPickerFrame.xml). The reference hooks ESC on the frame
-- itself (`<OnKeyDown>` l.177-184, whose ESCAPE arm is `HideUIPanel(this)` +
-- `cancelFunc(previousValues)`); this engine gives plain frames no raw key events, so
-- it routes through this chain exactly like the AddOns list on the next rung.
-- **Cancel-CLICK, not hide** — the ref's own arm restores the previous colour, and
-- clicking the real button is how the options rung below already spells this (the ref's
-- `OptionsFrameCancel:Click()`), so there is one cancel path, not two.
local picker = getglobal("ColorPickerFrame")
if picker and picker:IsVisible() then
ColorPickerCancelButton:Click()
return
end
-- The AddOns list next (decision 1197). The reference's glue twin hooks ESC directly
-- (`AddonList_OnKeyDown`, whose ESCAPE arm is `AddonList_OnCancel`); this engine gives
-- plain frames no raw key events, so it routes through this shared chain like the
-- stack-split spinner and the world map do. **Cancel, not hide** — ESC on that window
-- means "discard what I just ticked", which is the reference's own arm and the reason
-- the panel stages its edits at all.
local addons = getglobal("AddonListFrame")
if addons and addons:IsVisible() then
AddonList_OnCancel()
return
end
-- The options window next (l.1483-1484, live since decision 0950 — this rung sat
-- unfilled while GameMenuFrame.xml's disabled buttons were the gap). The ref
-- cancel-clicks (`OptionsFrameCancel:Click()`); ours hides: benilla applies changes
@@ -585,7 +648,7 @@
-- — this engine has no plain-frame keyboard capture (StackSplit.xml's own header comment),
-- so it routes through this shared chain instead. Just hides (StackSplitFrame_OnHide
-- clears the owner's hasStackSplit flag) — no accept, matching a real Cancel click.
local split = getglobal("BenillaStackSplitFrame")
local split = getglobal("StackSplitFrame")
if split and split:IsVisible() then
split:Hide()
return
@@ -604,7 +667,7 @@
-- UISpecialFrames (UIParent.lua l.46), so ESC closes it BEFORE the mail panel itself —
-- routed through this chain since this v1 drops the UISpecialFrames table. Its OnHide
-- runs the taken-letter auto-delete + close sound (MailFrame.lua OpenMailFrame_OnHide).
local openmail = getglobal("BenillaOpenMailFrame")
local openmail = getglobal("OpenMailFrame")
if openmail and openmail:IsVisible() then
HideUIPanel(openmail)
return
+107 -98
View File
@@ -59,7 +59,7 @@
branch on UnitPlayerControlled (which also covers pets); we resolve its player half via
UnitIsPlayer, so a player-controlled PET takes the NPC reaction branch until that flag lands.
- The classification border swap (elite/rare art, ref-TargetFrame.lua l.205-218) LANDED with
decision 0782 — UnitClassification is bound and BenillaTargetFrame_CheckClassification
decision 0782 — UnitClassification is bound and TargetFrame_CheckClassification
below runs the ref's branch table. Two things worth knowing at this altitude. The rank
`UnitClassification` reads is the client's GATED rank (`0x605620`): a pet or charmed unit
reports rank 0, so an enslaved elite wears the plain border, and because the tooltip's
@@ -163,9 +163,9 @@
end
end
-- The hover seam (1146): who this bar belongs to, and what its click means.
local click = BenillaTargetFrame_OnClick
local click = TargetFrame_OnClick
if unit == "player" then
click = BenillaPlayerFrame_OnClick
click = PlayerFrame_OnClick
end
frame.healthBar.owner = frame
frame.powerBar.owner = frame
@@ -225,7 +225,7 @@
-- The status flash (ref PlayerFrame_OnLoad, decision 1082): the resting zzz /
-- auto-attack swords under the portrait, the gold/red ring, and the pulse
-- counters. Handles cached like pvpIcon above; their presence is what gates
-- BenillaPlayerFrame_UpdateStatus in the shared Update.
-- PlayerFrame_UpdateStatus in the shared Update.
frame.statusTexture = getglobal(tf .. "StatusTexture")
frame.attackBackground = getglobal(tf .. "AttackBackground")
frame.restIcon = getglobal(tf .. "RestIcon")
@@ -249,7 +249,7 @@
-- A plain Frame isn't mouse-enabled by construction (only Button/EditBox are); enable it so
-- the hover tooltip fires (the real client's unit frames take the mouse to target/tooltip).
frame:EnableMouse(true)
BenillaUnitFrame_Update(frame)
UnitFrame_Update()
end
-- Hover: the engine's SetUnit — the byte-verified unit line law (decision 0276: name/
@@ -324,7 +324,7 @@
if not UnitExists(unit) then return end
GameTooltip_SetDefaultAnchor(GameTooltip, frame)
if SHOW_NEWBIE_TIPS == "1" then
if frame:GetName() == "BenillaPlayerFrame" then
if frame:GetName() == "PlayerFrame" then
GameTooltip_AddNewbieTip(frame, PARTY_OPTIONS_LABEL, 1.0, 1.0, 1.0,
NEWBIE_TOOLTIP_PARTYOPTIONS)
return
@@ -356,7 +356,8 @@
end
end
function BenillaUnitFrame_OnEvent(frame)
function UnitFrame_OnEvent(event)
local frame = this
-- UNIT_* events name their unit in arg1; ignore other units' updates.
if string.sub(event, 1, 5) == "UNIT_" and arg1 ~= frame.unit then
return
@@ -383,7 +384,7 @@
elseif event == "PLAYER_REGEN_ENABLED" then
frame.onHateList = nil
end
BenillaUnitFrame_Update(frame)
UnitFrame_Update()
-- The player's flag-change sound (decision 0646 §2/§4). The reference plays
-- igPVPUpdate inside PlayerFrame_UpdatePvPStatus's two show branches, which it only
-- ever reaches from UNIT_FACTION; our repaint runs from the shared Update on every
@@ -549,7 +550,8 @@
-- sound because all three BLPs are 256x128 (checked, not assumed), so the region's
-- XML-declared TexCoords (0.09375..1.0 x 0..0.78125 = the 232x100 crop) frame all three
-- identically, and our SetTexture touches only the path (it leaves tex_coords alone).
function BenillaTargetFrame_CheckClassification(frame)
function TargetFrame_CheckClassification()
local frame = this
local classification = UnitClassification(frame.unit)
if classification == "worldboss" or classification == "rareelite" or classification == "elite" then
frame.frameTexture:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Elite")
@@ -560,7 +562,8 @@
end
end
function BenillaUnitFrame_Update(frame)
function UnitFrame_Update()
local frame = this
local unit = frame.unit
if not UnitExists(unit) then
frame:Hide()
@@ -571,7 +574,7 @@
frame.healthBar:SetMinMaxValues(0, UnitHealthMax(unit))
frame.healthBar:SetValue(UnitHealth(unit))
local maxPower = UnitPowerMax(unit)
local maxPower = UnitManaMax(unit)
if maxPower > 0 then
frame.powerBar:Show()
local _, token = UnitPowerType(unit)
@@ -584,7 +587,7 @@
-- mechanism stays — SetTextStatusBarTextPrefix is live and one call brings any
-- label back — only the calls are gone.
frame.powerBar:SetMinMaxValues(0, maxPower)
frame.powerBar:SetValue(UnitPower(unit))
frame.powerBar:SetValue(UnitMana(unit))
else
frame.powerBar:Hide()
end
@@ -660,7 +663,7 @@
-- The classification border (target only) — the ref's TargetFrame_Update calls
-- CheckClassification right here, between CheckFaction and CheckDead.
if frame.frameTexture then
BenillaTargetFrame_CheckClassification(frame)
TargetFrame_CheckClassification()
end
-- DEAD text (target only): shown at zero health (ref TargetFrame_CheckDead). Blanked
@@ -679,7 +682,7 @@
-- The status flash, player frame only (handle-gated like pvpIcon; decision 1082).
if frame.statusTexture then
BenillaPlayerFrame_UpdateStatus(frame)
PlayerFrame_UpdateStatus()
end
-- The aura rows (target only) — the ref's TargetFrame_Update →
@@ -695,7 +698,7 @@
-- TargetDebuffButton_Update (ref-TargetFrame.lua l.263-387), transcribed onto the Era API
-- (decision 0255: UnitBuff returns name,icon,count,debuffType,… — the 1.12 original returned
-- the texture first). Only called with a live target (BenillaUnitFrame_Update guards).
-- the texture first). Only called with a live target (UnitFrame_Update guards).
-- DebuffTypeColor is defined by BuffFrame.xml, which loads AFTER this file — safe, because
-- this function only runs on events, never at load (no target exists at load).
function BenillaTargetAuras_Update()
@@ -703,7 +706,7 @@
local numBuffs = 0
for i = 1, MAX_TARGET_BUFFS do
local name, icon = UnitBuff("target", i)
button = getglobal("BenillaTargetFrameBuff" .. i)
button = getglobal("TargetFrameBuff" .. i)
if name then
getglobal(button:GetName() .. "Icon"):SetTexture(icon or BENILLA_FALLBACK_ICON)
button:Show()
@@ -716,7 +719,7 @@
local numDebuffs = 0
for i = 1, MAX_TARGET_DEBUFFS do
local name, icon, count, debuffType = UnitDebuff("target", i)
button = getglobal("BenillaTargetFrameDebuff" .. i)
button = getglobal("TargetFrameDebuff" .. i)
if name then
getglobal(button:GetName() .. "Icon"):SetTexture(icon or BENILLA_FALLBACK_ICON)
local color = DebuffTypeColor[debuffType] or DebuffTypeColor["none"]
@@ -740,11 +743,11 @@
local debuffWrap = 6
if UnitIsFriend("player", "target") then
BenillaTargetFrameBuff1:SetPoint("TOPLEFT", "BenillaTargetFrame", "BOTTOMLEFT", 5, 32)
BenillaTargetFrameDebuff1:SetPoint("TOPLEFT", "BenillaTargetFrameBuff1", "BOTTOMLEFT", 0, -2)
TargetFrameBuff1:SetPoint("TOPLEFT", "TargetFrame", "BOTTOMLEFT", 5, 32)
TargetFrameDebuff1:SetPoint("TOPLEFT", "TargetFrameBuff1", "BOTTOMLEFT", 0, -2)
else
BenillaTargetFrameDebuff1:SetPoint("TOPLEFT", "BenillaTargetFrame", "BOTTOMLEFT", 5, 32)
BenillaTargetFrameBuff1:SetPoint("TOPLEFT", "BenillaTargetFrameDebuff7", "BOTTOMLEFT", 0, -2)
TargetFrameDebuff1:SetPoint("TOPLEFT", "TargetFrame", "BOTTOMLEFT", 5, 32)
TargetFrameBuff1:SetPoint("TOPLEFT", "TargetFrameDebuff7", "BOTTOMLEFT", 0, -2)
end
-- Shrink when the debuffs would overlap the frame (ref l.340-347): 21→17 icons,
@@ -759,13 +762,13 @@
end
for i = 1, 5 do
button = getglobal("BenillaTargetFrameBuff" .. i)
button = getglobal("TargetFrameBuff" .. i)
button:SetWidth(debuffSize)
button:SetHeight(debuffSize)
end
-- The ref resizes only the FIRST ROW's debuffs (l.358-368) — rows 2+ stay 17px always.
for i = 1, 6 do
button = getglobal("BenillaTargetFrameDebuff" .. i)
button = getglobal("TargetFrameDebuff" .. i)
local border = getglobal(button:GetName() .. "Border")
border:SetWidth(debuffFrameSize)
border:SetHeight(debuffFrameSize)
@@ -775,18 +778,18 @@
-- Re-seat the wrap points (ref l.370-385) — constant with wrap pinned at 6, kept
-- verbatim so the ToT wrap-5 leg drops in unchanged.
getglobal("BenillaTargetFrameDebuff" .. debuffWrap):ClearAllPoints()
getglobal("BenillaTargetFrameDebuff" .. debuffWrap):SetPoint("LEFT", getglobal("BenillaTargetFrameDebuff" .. (debuffWrap - 1)), "RIGHT", 3, 0)
getglobal("BenillaTargetFrameDebuff" .. (debuffWrap + 1)):ClearAllPoints()
getglobal("BenillaTargetFrameDebuff" .. (debuffWrap + 1)):SetPoint("TOPLEFT", "BenillaTargetFrameDebuff1", "BOTTOMLEFT", 0, -2)
getglobal("BenillaTargetFrameDebuff" .. (debuffWrap + 2)):ClearAllPoints()
getglobal("BenillaTargetFrameDebuff" .. (debuffWrap + 2)):SetPoint("LEFT", getglobal("BenillaTargetFrameDebuff" .. (debuffWrap + 1)), "RIGHT", 3, 0)
getglobal("TargetFrameDebuff" .. debuffWrap):ClearAllPoints()
getglobal("TargetFrameDebuff" .. debuffWrap):SetPoint("LEFT", getglobal("TargetFrameDebuff" .. (debuffWrap - 1)), "RIGHT", 3, 0)
getglobal("TargetFrameDebuff" .. (debuffWrap + 1)):ClearAllPoints()
getglobal("TargetFrameDebuff" .. (debuffWrap + 1)):SetPoint("TOPLEFT", "TargetFrameDebuff1", "BOTTOMLEFT", 0, -2)
getglobal("TargetFrameDebuff" .. (debuffWrap + 2)):ClearAllPoints()
getglobal("TargetFrameDebuff" .. (debuffWrap + 2)):SetPoint("LEFT", getglobal("TargetFrameDebuff" .. (debuffWrap + 1)), "RIGHT", 3, 0)
if debuffWrap == 5 then
BenillaTargetFrameDebuff11:ClearAllPoints()
BenillaTargetFrameDebuff11:SetPoint("TOPLEFT", "BenillaTargetFrameDebuff6", "BOTTOMLEFT", 0, -2)
TargetFrameDebuff11:ClearAllPoints()
TargetFrameDebuff11:SetPoint("TOPLEFT", "TargetFrameDebuff6", "BOTTOMLEFT", 0, -2)
else
BenillaTargetFrameDebuff11:ClearAllPoints()
BenillaTargetFrameDebuff11:SetPoint("LEFT", "BenillaTargetFrameDebuff10", "RIGHT", 3, 0)
TargetFrameDebuff11:ClearAllPoints()
TargetFrameDebuff11:SetPoint("LEFT", "TargetFrameDebuff10", "RIGHT", 3, 0)
end
end
@@ -806,7 +809,8 @@
-- style; decision 1082). Priority is the ref's: RESTING wins over combat — sitting in an
-- inn mid-auto-attack shows the zzz — then the auto-attack state (gold ring/zzz vs red
-- ring/swords/red disc), then bare swords for on-someone's-aggro-list, then nothing.
function BenillaPlayerFrame_UpdateStatus(frame)
function PlayerFrame_UpdateStatus()
local frame = this
if IsResting() then
frame.statusTexture:SetVertexColor(1.0, 0.88, 0.25, 1.0)
frame.statusTexture:Show()
@@ -843,7 +847,8 @@
-- 55/255 and 255/255 on the ADD-blend status ring and the icon glow, run only while the
-- ring is visible (resting or auto-attacking); the hit-indicator fade rides the same
-- tick, exactly as before.
function BenillaPlayerFrame_OnUpdate(frame, elapsed)
function PlayerFrame_OnUpdate(elapsed)
local frame = this
if frame.statusTexture:IsVisible() then
local counter = frame.statusCounter + elapsed
local sign = frame.statusSign
@@ -872,9 +877,9 @@
-- with arg1 = the button. The self-target itself is the byte-verified way the client
-- selects its own unit; the world-model click is a separate path, deliberately left out
-- (director's call).
function BenillaPlayerFrame_OnClick(button)
function PlayerFrame_OnClick(button)
if button == "RightButton" then
ToggleDropDownMenu(1, nil, PlayerFrameDropDown, "BenillaPlayerFrame", 106, 27)
ToggleDropDownMenu(1, nil, PlayerFrameDropDown, "PlayerFrame", 106, 27)
return
end
if button ~= "LeftButton" then return end
@@ -929,9 +934,9 @@
-- ref TargetFrame_OnClick: right-click opens the unit menu at (120, 10). The ref's
-- LeftButton branch (SpellTargetUnit / DropItemOnUnit) rides the spell-cursor and
-- item-drop slices — a bare left-click is a no-op on the ref too.
function BenillaTargetFrame_OnClick(button)
function TargetFrame_OnClick(button)
if button == "RightButton" then
ToggleDropDownMenu(1, nil, TargetFrameDropDown, "BenillaTargetFrame", 120, 10)
ToggleDropDownMenu(1, nil, TargetFrameDropDown, "TargetFrame", 120, 10)
end
end
]]></Script>
@@ -945,7 +950,7 @@
BELOW the MEDIUM panel windows on purpose, and the player frame shares the top-left corner
with the left-slot panel. See the note on PartyFrame.xml's template (decision 0597) for why
the default MEDIUM makes a unit frame's bar children draw through a window's background. -->
<Frame name="BenillaPlayerFrame" frameStrata="BACKGROUND">
<Frame name="PlayerFrame" frameStrata="BACKGROUND">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="-19" y="-4"/></Offset></Anchor>
@@ -1167,17 +1172,17 @@
</Frames>
<Scripts>
<OnLoad><![CDATA[BenillaUnitFrame_OnLoad(this, "player")]]></OnLoad>
<OnEvent><![CDATA[BenillaUnitFrame_OnEvent(this)]]></OnEvent>
<OnUpdate><![CDATA[BenillaPlayerFrame_OnUpdate(this, arg1)]]></OnUpdate>
<OnEvent><![CDATA[UnitFrame_OnEvent(event)]]></OnEvent>
<OnUpdate><![CDATA[PlayerFrame_OnUpdate(arg1)]]></OnUpdate>
<OnEnter><![CDATA[BenillaUnitFrame_OnEnter(this)]]></OnEnter>
<OnLeave><![CDATA[BenillaUnitFrame_OnLeave(this)]]></OnLeave>
<OnClick><![CDATA[BenillaPlayerFrame_OnClick(arg1)]]></OnClick>
<OnClick><![CDATA[PlayerFrame_OnClick(arg1)]]></OnClick>
</Scripts>
</Frame>
<!-- The SELF-menu dropdown host (ref $parentDropDown, PlayerFrame.xml): hidden state-holder
only — the list anchors to the player frame by name, never to this. -->
<Frame name="PlayerFrameDropDown" parent="BenillaPlayerFrame" inherits="UIDropDownMenuTemplate" hidden="true">
<Frame name="PlayerFrameDropDown" parent="PlayerFrame" inherits="UIDropDownMenuTemplate" hidden="true">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="-20" y="-10"/></Offset></Anchor></Anchors>
<Scripts>
@@ -1235,7 +1240,7 @@
every login. Steady-state behavior is identical — Update Shows/Hides on target changes. -->
<!-- frameStrata LOW is the ref's own (`TargetFrame.xml` l.88) — one above the player frame's
BACKGROUND, still below the MEDIUM panels. Same reasoning as PlayerFrame above. -->
<Frame name="BenillaTargetFrame" hidden="true" frameStrata="LOW">
<Frame name="TargetFrame" hidden="true" frameStrata="LOW">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="250" y="-4"/></Offset></Anchor>
@@ -1452,7 +1457,7 @@
</Frames>
<Scripts>
<OnLoad><![CDATA[BenillaUnitFrame_OnLoad(this, "target")]]></OnLoad>
<OnEvent><![CDATA[BenillaUnitFrame_OnEvent(this)]]></OnEvent>
<OnEvent><![CDATA[UnitFrame_OnEvent(event)]]></OnEvent>
<OnEnter><![CDATA[BenillaUnitFrame_OnEnter(this)]]></OnEnter>
<OnLeave><![CDATA[BenillaUnitFrame_OnLeave(this)]]></OnLeave>
<!-- The select/deselect pair rides the frame's visibility, exactly the ref
@@ -1474,13 +1479,13 @@
PlaySound("INTERFACESOUND_LOSTTARGETUNIT")
CloseDropDownMenus()
]]></OnHide>
<OnClick><![CDATA[BenillaTargetFrame_OnClick(arg1)]]></OnClick>
<OnClick><![CDATA[TargetFrame_OnClick(arg1)]]></OnClick>
</Scripts>
</Frame>
<!-- The target-menu dropdown host (ref $parentDropDown, TargetFrame.xml): hidden
state-holder only, like the player's above. -->
<Frame name="TargetFrameDropDown" parent="BenillaTargetFrame" inherits="UIDropDownMenuTemplate" hidden="true">
<Frame name="TargetFrameDropDown" parent="TargetFrame" inherits="UIDropDownMenuTemplate" hidden="true">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="-20" y="-10"/></Offset></Anchor></Anchors>
<Scripts>
@@ -1514,30 +1519,31 @@
frames' own deferred buff panel, not here.
-->
<Script><![CDATA[
function BenillaPetFrame_OnLoad(frame)
frame.portrait = BenillaPetFrameTextureFramePortrait
frame.healthBar = BenillaPetFrameHealthBar
frame.manaBar = BenillaPetFrameManaBar
frame.nameText = BenillaPetFrameTextureFrameName
function PetFrame_OnLoad()
local frame = this
frame.portrait = PetFrameTextureFramePortrait
frame.healthBar = PetFrameHealthBar
frame.manaBar = PetFrameManaBar
frame.nameText = PetFrameTextureFrameName
-- The recessed-bar look, the same mechanism the player/target/party frames use
-- (decision 0884): push the bars one frame level BELOW the art's TextureFrame, because
-- the draw layer is bucket-wide and the frame level is the one key term above it.
local level = BenillaPetFrameTextureFrame:GetFrameLevel()
local level = PetFrameTextureFrame:GetFrameLevel()
frame.healthBar:SetFrameLevel(level - 1)
frame.manaBar:SetFrameLevel(level - 1)
-- The pet's numerals, textLockable like the ref's (PetFrame.xml l.139/164; 1143).
if TextStatusBar_Initialize then -- engine tests load file subsets
TextStatusBar_Initialize(frame.healthBar)
TextStatusBar_Initialize(frame.manaBar)
SetTextStatusBarText(frame.healthBar, BenillaPetFrameTextureFrameHealthBarText)
SetTextStatusBarText(frame.manaBar, BenillaPetFrameTextureFrameManaBarText)
SetTextStatusBarText(frame.healthBar, PetFrameTextureFrameHealthBarText)
SetTextStatusBarText(frame.manaBar, PetFrameTextureFrameManaBarText)
frame.healthBar.textLockable = 1
frame.manaBar.textLockable = 1
end
-- The pet's bars hover for their numbers, but hand nothing on: the pet frame has no
-- OnEnter of its own, and its click is its own handler (1146).
frame.healthBar.ownerClick = BenillaPetFrame_OnClick
frame.manaBar.ownerClick = BenillaPetFrame_OnClick
frame.healthBar.ownerClick = PetFrame_OnClick
frame.manaBar.ownerClick = PetFrame_OnClick
frame.attackModeCounter = 0
frame.attackModeSign = -1
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
@@ -1553,33 +1559,35 @@
frame:RegisterEvent("PET_ATTACK_STOP")
frame:RegisterEvent("UNIT_HAPPINESS")
frame:EnableMouse(true)
BenillaPetFrame_Update(frame)
PetFrame_Update()
end
-- ref PetFrame_OnEvent, with both of its argument gates kept: UNIT_PET names the OWNER
-- ("player" — wow-re §9 carves the arg), every other UNIT_* names the pet itself. Getting
-- those backwards is how a pet frame ends up repainting off the player's health.
function BenillaPetFrame_OnEvent(frame)
function PetFrame_OnEvent(event)
local frame = this
if event == "PET_ATTACK_START" then
BenillaPetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, 1.0)
BenillaPetFrameAttackMode:Show()
PetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, 1.0)
PetFrameAttackMode:Show()
return
elseif event == "PET_ATTACK_STOP" then
BenillaPetFrameAttackMode:Hide()
PetFrameAttackMode:Hide()
return
elseif event == "UNIT_HAPPINESS" then
-- The ref's own last arm: repaint the icon alone, not the whole frame.
BenillaPetFrame_SetHappiness()
PetFrame_SetHappiness()
return
elseif event == "UNIT_PET" then
if arg1 ~= "player" then return end
elseif string.sub(event, 1, 5) == "UNIT_" and arg1 ~= "pet" then
return
end
BenillaPetFrame_Update(frame)
PetFrame_Update()
end
function BenillaPetFrame_Update(frame)
function PetFrame_Update()
local frame = this
if not UnitExists("pet") then
frame:Hide()
return
@@ -1593,22 +1601,22 @@
-- The ref's art swap (PetFrame.lua's Update): a powerless pet wears the -NoMana plate,
-- which has no mana rail to leave empty. The bar hides with it, as on every other frame.
local maxPower = UnitPowerMax("pet")
local maxPower = UnitManaMax("pet")
if maxPower > 0 then
BenillaPetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame")
PetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame")
frame.manaBar:Show()
local _, token = UnitPowerType("pet")
local c = BENILLA_POWER_COLORS[token] or BENILLA_POWER_COLORS.MANA
frame.manaBar:SetStatusBarColor(c.r, c.g, c.b)
frame.manaBar:SetMinMaxValues(0, maxPower)
frame.manaBar:SetValue(UnitPower("pet"))
frame.manaBar:SetValue(UnitMana("pet"))
else
BenillaPetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame-NoMana")
PetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame-NoMana")
frame.manaBar:Hide()
end
BenillaPetFrame_UpdateDebuffs()
BenillaPetFrame_SetHappiness()
PetFrame_SetHappiness()
end
-- ref PetFrame_SetHappiness (PetFrame.lua l.132-156), quoted (decision 1005).
@@ -1622,29 +1630,29 @@
-- * the three texcoord cuts run 3/2/1 left-to-right across the strip — happy is at x=0.
-- * the loyalty line is chosen by the SIGN of loyaltyRate, which is why that return is
-- unscaled and allowed to be negative.
function BenillaPetFrame_SetHappiness()
function PetFrame_SetHappiness()
local happiness, damagePercentage, loyaltyRate = GetPetHappiness()
local hasPetUI, isHunterPet = HasPetUI()
if not happiness or not isHunterPet then
BenillaPetFrameHappiness:Hide()
PetFrameHappiness:Hide()
return
end
BenillaPetFrameHappiness:Show()
PetFrameHappiness:Show()
if happiness == 1 then
BenillaPetFrameHappinessTexture:SetTexCoord(0.375, 0.5625, 0, 0.359375)
PetFrameHappinessTexture:SetTexCoord(0.375, 0.5625, 0, 0.359375)
elseif happiness == 2 then
BenillaPetFrameHappinessTexture:SetTexCoord(0.1875, 0.375, 0, 0.359375)
PetFrameHappinessTexture:SetTexCoord(0.1875, 0.375, 0, 0.359375)
elseif happiness == 3 then
BenillaPetFrameHappinessTexture:SetTexCoord(0, 0.1875, 0, 0.359375)
PetFrameHappinessTexture:SetTexCoord(0, 0.1875, 0, 0.359375)
end
BenillaPetFrameHappiness.tooltip = getglobal("PET_HAPPINESS" .. happiness)
BenillaPetFrameHappiness.tooltipDamage = format(PET_DAMAGE_PERCENTAGE, damagePercentage)
PetFrameHappiness.tooltip = getglobal("PET_HAPPINESS" .. happiness)
PetFrameHappiness.tooltipDamage = format(PET_DAMAGE_PERCENTAGE, damagePercentage)
if loyaltyRate < 0 then
BenillaPetFrameHappiness.tooltipLoyalty = getglobal("LOSING_LOYALTY")
PetFrameHappiness.tooltipLoyalty = getglobal("LOSING_LOYALTY")
elseif loyaltyRate > 0 then
BenillaPetFrameHappiness.tooltipLoyalty = getglobal("GAINING_LOYALTY")
PetFrameHappiness.tooltipLoyalty = getglobal("GAINING_LOYALTY")
else
BenillaPetFrameHappiness.tooltipLoyalty = nil
PetFrameHappiness.tooltipLoyalty = nil
end
end
@@ -1653,7 +1661,7 @@
-- so these are icon + border + stack count, the party rows' shape exactly.
function BenillaPetFrame_UpdateDebuffs()
for i = 1, 4 do
local button = getglobal("BenillaPetFrameDebuff" .. i)
local button = getglobal("PetFrameDebuff" .. i)
local _, icon, count = UnitDebuff("pet", i)
if icon then
getglobal(button:GetName() .. "Icon"):SetTexture(icon)
@@ -1673,15 +1681,16 @@
-- ref PetFrame_OnUpdate: the attack overlay's alpha ramp — a 0.5 s-per-leg triangle between
-- 55/255 and 1, sign-flipped at each end (the ref's own counter shape, transcribed).
function BenillaPetFrame_OnUpdate(frame, elapsed)
if not BenillaPetFrameAttackMode:IsVisible() then return end
function PetFrame_OnUpdate(elapsed)
local frame = this
if not PetFrameAttackMode:IsVisible() then return end
local counter = frame.attackModeCounter + elapsed
local sign = frame.attackModeSign
if counter > 0.5 then
sign = -sign
frame.attackModeSign = sign
end
counter = math.fmod(counter, 0.5)
counter = mod(counter, 0.5)
frame.attackModeCounter = counter
local alpha
if sign == 1 then
@@ -1689,13 +1698,13 @@
else
alpha = (255 - (counter * 400)) / 255
end
BenillaPetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, alpha)
PetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, alpha)
end
-- ref PetFrame_OnClick, now whole (decision 1066 built the right branch). Its FIRST leg is
-- the one that is easy to drop: with a spell armed, a RIGHT click disarms rather than
-- opening the menu, and it precedes both branches.
function BenillaPetFrame_OnClick(button)
function PetFrame_OnClick(button)
if SpellIsTargeting() and button == "RightButton" then
SpellStopTargeting()
return
@@ -1780,7 +1789,7 @@
<!-- frameStrata LOW matches the party frames' and the target's — above the player frame's
BACKGROUND (which it overlaps), still below the MEDIUM panels. -->
<Frame name="BenillaPetFrame" parent="BenillaPlayerFrame" hidden="true" frameStrata="LOW">
<Frame name="PetFrame" parent="PlayerFrame" hidden="true" frameStrata="LOW">
<Size><AbsDimension x="128" y="53"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="80" y="-60"/></Offset></Anchor>
@@ -1840,7 +1849,7 @@
<Layer level="BORDER">
<!-- 128×64 art at (0,-2) — it hangs 13 px below the frame rect, as
authored. The file is swapped at update (see the header). -->
<Texture name="BenillaPetFrameTexture" file="Interface\TargetingFrame\UI-SmallTargetingFrame">
<Texture name="PetFrameTexture" file="Interface\TargetingFrame\UI-SmallTargetingFrame">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
</Texture>
@@ -1848,7 +1857,7 @@
<Layer level="ARTWORK">
<!-- The attacking overlay (ref PetAttackModeTexture, l.104-118): the right
29.7% of UI-Player-AttackStatus, ADD-blended, pulsed by OnUpdate. -->
<Texture name="BenillaPetFrameAttackMode" file="Interface\TargetingFrame\UI-Player-AttackStatus" alphaMode="ADD" hidden="true">
<Texture name="PetFrameAttackMode" file="Interface\TargetingFrame\UI-Player-AttackStatus" alphaMode="ADD" hidden="true">
<Size><AbsDimension x="76" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-9"/></Offset></Anchor></Anchors>
<TexCoords left="0.703125" right="1.0" top="0" bottom="1.0"/>
@@ -1919,22 +1928,22 @@
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="48" y="-42"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff2" inherits="BenillaPetDebuffTemplate" id="2">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetFrameDebuff1" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="PetFrameDebuff1" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff3" inherits="BenillaPetDebuffTemplate" id="3">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetFrameDebuff2" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="PetFrameDebuff2" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff4" inherits="BenillaPetDebuffTemplate" id="4">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetFrameDebuff3" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="LEFT" relativeTo="PetFrameDebuff3" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaPetFrame_OnLoad(this)</OnLoad>
<OnEvent>BenillaPetFrame_OnEvent(this)</OnEvent>
<OnUpdate>BenillaPetFrame_OnUpdate(this, arg1)</OnUpdate>
<OnLoad>PetFrame_OnLoad()</OnLoad>
<OnEvent>PetFrame_OnEvent(event)</OnEvent>
<OnUpdate>PetFrame_OnUpdate(arg1)</OnUpdate>
<OnEnter>BenillaPetFrame_OnEnter(this)</OnEnter>
<OnLeave>if SpellIsTargeting() then SetCursor("CAST_ERROR_CURSOR") end GameTooltip:FadeOut()</OnLeave>
<OnClick>BenillaPetFrame_OnClick(arg1)</OnClick>
<OnClick>PetFrame_OnClick(arg1)</OnClick>
</Scripts>
</Frame>
</Ui>
+1 -1
View File
@@ -624,7 +624,7 @@
elseif ( button == "PET_ABANDON" ) then
StaticPopup_Show("ABANDON_PET");
elseif ( button == "PET_PAPERDOLL" ) then
ToggleCharacter("BenillaPetPaperDollFrame");
ToggleCharacter("PetPaperDollFrame");
elseif ( button == "PET_RENAME" ) then
StaticPopup_Show("RENAME_PET");
end
@@ -157,8 +157,8 @@
end
-- ── The continent/zone dropdowns (ref verbatim, except the vararg walk: the reference's
-- `arg.n` needs LUA_COMPAT_VARARG, which stock 5.1 — and ours — doesn't ship; select()
-- is the 5.1-native spelling). ─────────────────────────────────────────────────────────
-- `arg.n` is written as select("#", ...), the 5.1-native spelling and this tree's house
-- style for it). ──────────────────────────────────────────────────────────────────────
function WorldMapContinentsDropDown_OnLoad()
UIDropDownMenu_Initialize(this, WorldMapContinentsDropDown_Initialize);
UIDropDownMenu_SetWidth(130);
+5 -2
View File
@@ -170,8 +170,11 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
end
]]></Script>
<!-- These frames are shown when the current zone or subzone changes.
(ref carries toplevel="true" on both; dropped here — SetToplevel is a v1 object-model
gap and click-raise is meaningless on mouse-less splash text.) -->
(ref carries toplevel="true" on both; still dropped here, but the reason has changed.
`SetToplevel` is no longer a gap — the raise law is built, `script::object::toplevel` —
and its real trigger is Show, not a click, so "click-raise is meaningless on mouse-less
splash text" was the wrong argument. What holds now: these are LOW-strata splash frames
that overlap nothing in LOW, so the occlusion gate declines. One word to re-add.) -->
<Frame name="ZoneTextFrame" parent="UIParent" frameStrata="LOW">
<Anchors>
<Anchor point="BOTTOM">
+65 -13
View File
@@ -20,6 +20,11 @@
# The named virtual Font objects (decision 0084) — must load first so any FontString's
# `inherits="GameFontNormal"` resolves against a registered object.
Fonts.xml
# The reference's own second file, in its own position: TEXT/message/_ERRORMESSAGE, the
# ScriptErrors dialog, and the DialogBoxFrame + DialogButton*Texture kit it is built from.
# Straight after Fonts.xml because those are the only things it needs (GameFontRedLarge,
# DialogButtonNormalText/HighlightText) and everything above it in FrameXML.toc is engine.
BasicControls.xml
# The UIPanel slot manager (decision 0084 §2): script-only, no frames of its own — must load
# before any panel frame (Gossip/Merchant today) so ShowUIPanel/HideUIPanel already exist
# when those frames' OnLoad/OnEvent reference them.
@@ -32,7 +37,10 @@ UiPanels.xml
UIParent.xml
# The tooltip: a hover window the bag/merchant/loot/unit frames drive. Loaded before them so
# `GameTooltip` + its verbs exist when their OnEnter handlers fire (runtime, so order
# is not strictly required — but it keeps the dependency legible).
# is not strictly required — but it keeps the dependency legible). It also declares the two
# virtual templates (GameTooltipTemplate, ShoppingTooltipTemplate), which IS an order
# requirement: ItemRef.xml below expands one, and so do the 27 corpus addons that name
# GameTooltipTemplate in an XML inherits=.
GameTooltip.xml
# The dropdown kit (0203 phase 2's closing sub-slice, widened for 0434 phase 5):
# UIDropDownMenuTemplate + DropDownList1/2 + the UIDropDownMenu_* API — before its
@@ -43,9 +51,11 @@ UIDropDownMenu.xml
# children's OnLoad initialize into it.
UnitPopup.xml
# The chat-link click router (SetItemRef): item links → ItemRefTooltip:SetHyperlink; a
# player name → left-click whisper / right-click FRIEND dropdown. After GameTooltip.xml
# (GameTooltip_OnLoad) AND the dropdown kit + UnitPopup — its FriendsDropDown inherits
# UIDropDownMenuTemplate and opens a UnitPopup FRIEND menu.
# player name → left-click whisper / right-click FRIEND dropdown. After GameTooltip.xml —
# ItemRefTooltip inherits=GameTooltipTemplate, so this order is now REQUIRED, not just legible
# (GameTooltip_OnLoad was the old, runtime-only reason) — AND after the dropdown kit +
# UnitPopup, since its FriendsDropDown inherits UIDropDownMenuTemplate and opens a UnitPopup
# FRIEND menu.
ItemRef.xml
# The shared status-bar numerals machinery (decision 1082): TextStatusBar_* + the opt-in
# template — before its customers (the XP bar in ActionBar.xml today; the unit-frame
@@ -53,7 +63,7 @@ ItemRef.xml
# loads TextStatusBar right before its unit frames, same seat as here.
TextStatusBar.xml
UnitFrames.xml
# The combo-point dots (decision 0869) — anchored to BenillaTargetFrame, so after
# The combo-point dots (decision 0869) — anchored to TargetFrame, so after
# UnitFrames.xml; its fade chain needs UiPanels.xml's UIFrameFade above.
ComboFrame.xml
# The center-screen scrolling combat text (decision 0578) — the Blizzard_CombatText
@@ -79,26 +89,26 @@ Cooldown.xml
ActionBar.xml
# The two always-on bottom multibars (actions 61-72 / 49-60): pure XML/Lua over
# ActionBar.xml's shared button template + handler set — loads right after it
# (inherits BenillaActionButtonTemplate, anchors to BenillaActionButton1, and its
# (inherits BenillaActionButtonTemplate, anchors to ActionButton1, and its
# buttons join the BENILLA_ACTION_BUTTONS roster ActionBar.xml declares).
MultiBars.xml
# The stance/shapeshift bar: hidden until `crate::ui_shapeshift`'s feed pushes a
# non-empty form list (a formless class never shows it). After Cooldown.xml
# (CooldownFrame_SetTimer) + ActionBar.xml (BENILLA_FALLBACK_ICON, the BenillaActionBar
# (CooldownFrame_SetTimer) + ActionBar.xml (BENILLA_FALLBACK_ICON, the MainMenuBar
# anchor target).
StanceBar.xml
# The pet action bar (decision 0982): hidden until `crate::ui_pet`'s feed reports a bar,
# i.e. until the server sends `SMSG_PET_SPELLS` for a live pet or charm — so a class that
# never has one never sees it. Right after the stance bar, whose row it shares (either
# one showing raises UIParent.xml's managed "pet" delta), and after Cooldown.xml
# (CooldownFrame_SetTimer), ActionBar.xml (the `BenillaActionBar` anchor target) and
# (CooldownFrame_SetTimer), ActionBar.xml (the `MainMenuBar` anchor target) and
# GameTooltip.xml (its hover). It is also the pet ARC's file, so decision 1066's three
# right-click-menu dialogs (ABANDON_PET / RENAME_PET / PETRENAMECONFIRM) register here —
# which adds UiPanels.xml to its dependency set, already far above.
PetActionBar.xml
# The micro-button row in the bar's right-hand recess (the ref's own file split, its TOC
# loads MainMenuBarMicroButtons right after MainMenuBar). After ActionBar.xml: it anchors
# into BenillaActionBarArtFrame by name and seats itself with that file's
# into MainMenuBarArtFrame by name and seats itself with that file's
# BenillaActionBarArt_SeatAbove. It must come BEFORE the panels it toggles, because each of
# their OnShow/OnHide calls the `UpdateMicroButtons` this file defines (the panel frames
# themselves are read back through getglobal, so the reverse dependency is call-time).
@@ -117,9 +127,47 @@ GameTime.xml
# other consumer of the zone-text host globals.
ZoneText.xml
# The faux-scroll kit (decisions 0247/0250/0251): BenillaScrollBarTemplate (the draggable
# Slider scroll bar) + BenillaFauxScrollFrameTemplate + the BenillaFauxScrollFrame_* API —
# Slider scroll bar) + FauxScrollFrameTemplate + the reference's own FauxScrollFrame_*
# API (Update/GetOffset/SetOffset/OnVerticalScroll + ScrollFrame_OnLoad) —
# before its first customer (the trainer list) below.
ScrollTemplates.xml
# The reference's SHARED widget kit (decision 1203's queue): UIPanelButtonTemplate,
# UICheckButtonTemplate, UIPanelCloseButton, InputBoxTemplate, the UIPanelScroll* family and
# their siblings, under the reference's own names — the templates a third-party addon names in
# `CreateFrame(kind, name, parent, "Template")`, which nothing in benilla's own XML consumes
# yet (each of our windows still carries its private copy; see the file's header). After
# ScrollTemplates.xml because UIPanelScrollFrameTemplate's OnLoad is that file's
# ScrollFrame_OnLoad, and this file supplies the two remaining handlers of that same kit
# (ScrollFrameTemplate_OnMouseWheel, ScrollFrame_OnScrollRangeChanged).
UIPanelTemplates.xml
# The reference's shared OPTIONS widgets: OptionsCheckButtonTemplate /
# UIOptionsCheckButtonTemplate / OptionsSliderTemplate / OptionsButtonTemplate /
# OptionFrameBoxTemplate. Straight after UIPanelTemplates.xml — two of them inherit its
# UICheckButtonTemplate and UIPanelButtonTemplate. Not benilla's own options window
# (OptionsFrame.xml, far below): these are the shared names addons build panels out of.
OptionsFrameTemplates.xml
# The reference's money-frame kit: MoneyFrameTemplate/SmallMoneyFrameTemplate + MoneyTypeInfo
# + the MoneyFrame_* API. Needs only Fonts (NumberFontNormal/NumberFontNormalLarge), and
# nothing benilla ships consumes it yet — our own eight coin displays run on MerchantFrame.xml's
# BenillaMoney_* slot kit, and MoneyFrame.xml's header is where the two are reconciled. Seated
# beside the scroll kit (the other shared widget kit) and ahead of every window that shows
# coins, which is where the reference's own TOC puts it too.
MoneyFrame.xml
# The shared colour dialog + its opacity slider (`ColorPickerFrame`, `OpacitySliderFrame`,
# `ColorPickerOkayButton`, `ColorPickerCancelButton`). Last of the "shared FrameXML surface with no
# benilla consumer yet" cluster that runs from UIPanelTemplates.xml down — the same posture as
# MoneyFrame.xml above, and for the same reason: 86 of the 218 corpus addons reach this window (64
# of those files are `Dewdrop-2.0.lua`, which drives THIS one rather than rolling its own), and
# nothing we ship opens it. The seat is a REQUIREMENT, not tidiness: its Okay/Cancel buttons
# `inherits="GameMenuButtonTemplate"`, which is resolved at LOAD, and that template lives in
# UIPanelTemplates.xml six lines up (it moved there from GameMenuFrame.xml — the reference's own
# file for it — precisely because GameMenuFrame.xml is deliberately LAST). Everything else it needs
# is far above (Fonts' GameFontNormal/…Small trio; UiPanels' ShowUIPanel/HideUIPanel, its
# UISpecialFrames entry and the ESC rung that cancel-clicks it). UIDropDownMenu.xml, which owns the
# colour-swatch row that OPENS this window, sits above too — but that direction is call-time only
# (`UIDropDownMenuButton_OpenColorPicker` reaches `ColorPickerFrame` by name when the swatch is
# clicked), so the two could sit either way round.
ColorPickerFrame.xml
# The fullscreen world map (decision 0203 phase 2), over the worldmap host API
# (`crate::ui_world_map` feeds it); the 'M' binding (ui_script/input.rs) calls its
# ToggleWorldMap().
@@ -160,8 +208,8 @@ PetPaperDollFrame.xml
# The Skills tab (decision 0437 phase 4): a CharacterFrame child page, but a separate
# top-level file (SkillFrame.xml's own header comment — this engine has no cross-file
# `parent=` attachment, so it's positioned by name via `<Anchors relativeTo=
# "BenillaCharacterFrame">` instead). Loaded immediately after CharacterFrame.xml so it
# paints ON TOP of it (later file = higher z) and so `BenillaCharacterFrame` already exists
# "CharacterFrame">` instead). Loaded immediately after CharacterFrame.xml so it
# paints ON TOP of it (later file = higher z) and so `CharacterFrame` already exists
# for that anchor to resolve against.
SkillFrame.xml
# The inspect window (decision 0631): another player's paper doll. Beside the character
@@ -218,7 +266,7 @@ ItemTextFrame.xml
# session windows below.
TrainerFrame.xml
# The bank window (decision 0604 phase 4): the vault, purchase ladder, and the 6 bank-bag
# popouts, over the C_Container feed already carrying container -1/5..10 (ui_items.rs) +
# popouts, over the container feed already carrying container -1/5..10 (ui_items.rs) +
# the bank Lua surface (benilla-ui/src/script/bank.rs) + crate::ui_bank's feed/drain.
# Loaded AFTER MerchantFrame.xml (reuses its BenillaMoney_* coin helpers for the purchase
# row + purse) and AFTER BagFrame.xml, already above (its BenillaBagSlotTemplate/
@@ -289,6 +337,10 @@ KeyBindingsPage.xml
# (all far above); sits right before GameMenuFrame so the menu — which reaches it by
# name at click time — stays the last, outermost layer.
OptionsFrame.xml
# The in-game AddOns list (decision 1197) — the ESC menu's AddOns entry. Before GameMenuFrame,
# which reaches AddonList_Toggle by name at click time; it depends only on the AddOn API verbs
# (1191) and defines its own templates, so it can sit anywhere after the fonts.
AddonList.xml
# The game menu (decision 0674): the frame ESC opens, plus the CAMP/QUIT dialogs and their
# event driver. LAST on purpose — it is the outermost layer of the shell, it depends on
# everything it touches being loaded (UiPanels' popup engine + panel manager, MicroMenu's
@@ -0,0 +1,219 @@
//! `addon_harness` — load a folder of addons, one per VM, and print what happened.
//!
//! ```text
//! cargo run -q -p benilla-app --example addon_harness -- <folder of addons> [--verbose] [--why <substr>]
//! ```
//!
//! The instrument decision 1188 phase 6 asks for: *"which addons work" is a number that can be
//! re-read on any day*. The mechanics, and what the numbers are and are not worth, are in
//! [`benilla_app::addon_harness`]'s module doc — read it before quoting anything from here.
//!
//! **Expect a long tail and do not chase 100 %** (1188's own instruction). The report is a
//! distribution, not a pass/fail: a handful of addons will always want features we have not built.
use benilla_app::addon_harness;
fn main() {
let mut args = std::env::args().skip(1);
let Some(root) = args.next() else {
eprintln!(
"usage: addon_harness <folder of addons> [--verbose] [--why <blocker substring>]"
);
std::process::exit(2);
};
let rest: Vec<String> = args.collect();
let verbose = rest.iter().any(|a| a == "--verbose");
// `--why <substring>` — the addons behind one blocker row, with their verbatim first errors.
// The ranked table collapses quoted names by design (1193); this is the read-back, and two of
// this arc's decisions came from doing it by hand (1206, 1210).
let why = rest
.iter()
.position(|a| a == "--why")
.and_then(|i| rest.get(i + 1))
.cloned();
let root = std::path::PathBuf::from(root);
let reports = addon_harness::survey(&root);
if reports.is_empty() {
eprintln!(
"no addons under {} — is that an AddOns folder?",
root.display()
);
std::process::exit(1);
}
let loaded = reports.iter().filter(|r| r.loaded).count();
let clean = reports
.iter()
.filter(|r| r.loaded && r.missing_globals.is_empty())
.count();
let blocked = reports
.iter()
.filter(|r| !r.missing_deps.is_empty())
.count();
println!("\n{} addon(s) under {}", reports.len(), root.display());
// Which VM the survey ran against. Without an install there is no GlobalStrings.lua, ~5,000
// globals are missing, and every number below is worse for a reason that has nothing to do
// with the client — say so rather than letting two machines' numbers be compared in silence.
println!(
" VM: our whole FrameXML + a seated session{}\n",
if addon_harness::seated_with_global_strings() {
" + the real GlobalStrings.lua"
} else {
" ** no install found: GlobalStrings absent, these numbers are NOT comparable **"
}
);
// The tree these numbers came from. Two runs are comparable only if this matches: in a dev
// build `assets/ui` is read from the SOURCE TREE, so anything else editing the checkout moves
// the headline with no rebuild. Quoting a delta across two different digests is how a wrong
// attribution got into a decision record (1209).
println!(
" FrameXML digest : {}",
addon_harness::framexml_digest()
);
println!(
" loaded without a single load error : {loaded}/{}",
reports.len()
);
println!(
" ...and calling nothing we lack : {clean}/{}",
reports.len()
);
println!(" with a dependency not installed : {blocked}");
// The stricter column, and the one that answers what the survey is really asking. Every other
// number here is LOAD-time; this one drives the client's own session start
// (ADDON_LOADED -> VARIABLES_LOADED -> PLAYER_LOGIN -> PLAYER_ENTERING_WORLD, then a second of
// ticks) and reports what the addon's HANDLERS raised. Four decision records in this arc end
// with "the headline cannot see this"; this is the number that can.
let survived = reports
.iter()
.filter(|r| r.loaded && r.session_errors.is_empty())
.count();
println!(
" ...and survived a session start : {survived}/{}",
reports.len()
);
// What the session start broke, ranked the way `blockers` ranks load failures.
let mut session: std::collections::BTreeMap<String, usize> = Default::default();
for r in reports.iter().filter(|r| r.loaded) {
if let Some(e) = r.session_errors.first() {
*session.entry(addon_harness::normalise(e)).or_default() += 1;
}
}
if !session.is_empty() {
let mut rows: Vec<(String, usize)> = session.into_iter().collect();
rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
println!("\n what broke at SESSION START (addons that loaded clean, first error each):");
for (err, count) in rows.into_iter().take(12) {
println!(" {count:>4} {err}");
}
}
// The distribution, because a mean would hide the shape.
let mut buckets = [0usize; 5];
for r in &reports {
let n = r.missing_globals.len();
buckets[match n {
0 => 0,
1..=2 => 1,
3..=5 => 2,
6..=15 => 3,
_ => 4,
}] += 1;
}
println!("\n missing-global count per addon:");
for (label, n) in ["0", "1-2", "3-5", "6-15", "16+"].iter().zip(buckets) {
println!(" {label:>5} {n:>4} {}", "#".repeat(n.min(60)));
}
// What actually STOPPED them — the ranked first error. Read this before the demand list: a
// wall 60 addons hit is worth more than a verb 60 addons would like (decision 1193).
println!("\n what stopped them (addons whose FIRST load error was each):");
for (err, count) in addon_harness::blockers(&reports).into_iter().take(12) {
println!(" {count:>4} {err}");
}
// Templates an addon names in `CreateFrame(..., "Template")` that we have never declared
// (decision 1203). Its own list because it is invisible to every other number here: an
// unresolved template raises no load error, so the addon scores as a pass and paints nothing.
let templates = addon_harness::template_demand(&reports);
if !templates.is_empty() {
println!("\n most-wanted missing TEMPLATES (addons naming each in CreateFrame):");
for (name, count) in templates.into_iter().take(12) {
println!(" {count:>4} {name}");
}
}
// The same question over the OTHER axis, and the one that actually moves the headline: a
// template named in an addon's own XML `inherits=`. Unlike the list above this failure is
// usually LOUD — the element's `<OnLoad>` fires at load and its first line is normally
// `getglobal(this:GetName().."Text")` — which is why transcribing the reference's shared kit
// was worth twelve addons while the CreateFrame list predicted none of them. Printed second
// and separately because merging the two would hide exactly that difference.
let inherits = addon_harness::inherits_demand(&reports);
if !inherits.is_empty() {
println!("\n most-wanted missing TEMPLATES (addons naming each in an XML inherits=):");
for (name, count) in inherits.into_iter().take(12) {
println!(" {count:>4} {name}");
}
}
// Frames and tables, ranked separately: a missing function is a Rust verb to write, a missing
// frame is FrameXML to transcribe, and the two queues go to different people. The scan was
// blind to this whole shape until 2026-08-11 — a window 86 addons reach scored 0.
let tables = addon_harness::table_demand(&reports);
if !tables.is_empty() {
println!("\n most-wanted missing FRAMES/TABLES (addons indexing each):");
for (name, count) in tables.into_iter().take(16) {
println!(" {count:>4} {name}");
}
}
println!("\n most-wanted missing globals (addons wanting each):");
for (name, count) in addon_harness::demand(&reports).into_iter().take(30) {
println!(" {count:>4} {name}");
}
if let Some(pattern) = &why {
let hits = addon_harness::blocked_by(&reports, pattern);
println!(
"\n addons whose first error matches {pattern:?} ({}):",
hits.len()
);
for (name, err) in &hits {
println!(" {name}");
// Two lines, not one. The first is the message; the SECOND is mlua's first traceback
// frame, and for the row this instrument is most often pointed at that frame is the
// whole answer — `in local '(for generator)'` is what tells a generic-for
// (decision 1202) apart from any other call of a table value.
for line in err.lines().take(2) {
println!(" {}", line.trim());
}
}
if hits.is_empty() {
println!(" (none — the pattern is matched against the NORMALISED row as printed)");
}
}
if verbose {
println!("\n per addon:");
for r in &reports {
let iface = r
.interface
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(",");
println!(
" {:<28} iface={:<12} {:<8} missing={:<4} {}",
r.name,
if iface.is_empty() { "-".into() } else { iface },
if r.loaded { "loaded" } else { "ERRORS" },
r.missing_globals.len(),
r.errors.first().map(String::as_str).unwrap_or("")
);
}
}
println!();
}
File diff suppressed because it is too large Load Diff
+379 -40
View File
@@ -21,6 +21,13 @@
//! - [`Kind::Host`] lands in [`BindingsState::fired`] for engine consumers (chat open, TAB
//! targeting, nameplates, autorun, camera zoom, …).
//!
//! **An addon's `Bindings.xml` rows dispatch here too** (decision 1188 phase 4). They are not in
//! [`SPECS`] — they are read off disk at addon load ([`benilla_ui::bindings_xml`]) — so a resolved
//! chord names a [`Bound`], which is either a registry [`Cmd`] or an index into this frame's
//! addon table. That enum is the whole design: an addon row is a *runtime* `String` body run with
//! the reference's `keystate` global, and every `SPECS[cmd]` in this file would be a latent panic
//! if it were instead an index past the end of the static table.
//!
//! While the Keybindings page (the Options window's category since 1008) has a capsule
//! selected it arms the **capture seam** (`BenillaBindCapture`): raw input is swallowed here,
//! canonicalized (`ALT-CTRL-SHIFT-<TOKEN>`), and handed back to the page's own capture handler
@@ -36,7 +43,7 @@ use bevy::input::mouse::AccumulatedMouseScroll;
use bevy::input::ButtonState;
use bevy::prelude::*;
use benilla_ui::script::keybind::{KeybindCommand, KeybindRequest};
use benilla_ui::script::keybind::{AddonBindingBody, KeybindCommand, KeybindRequest};
use benilla_ui::script::UiScript;
use crate::char_select::ClientState;
@@ -50,11 +57,32 @@ use chord::{BindKey, Chord};
pub(crate) use commands::cmd;
use commands::{Cmd, Kind, SPECS};
/// The derived chord→command map — rebuilt whenever the engine table's generation moves. Probed
/// What a bound chord names — a registry command, or an addon's `Bindings.xml` body
/// (decision 1188 phase 4).
///
/// **An enum rather than one index space with a sentinel in it.** The tempting shape is
/// "`Cmd(u16)`, and anything `>= SPECS.len()` is an addon" — which would turn every one of this
/// file's eleven `SPECS[cmd.0 as usize]` reads into a panic waiting for the first addon binding to
/// be pressed. The compiler is what should be enforcing that split, so it does: `state.fired` /
/// `state.just` / `state.amounts` stay [`Cmd`]-typed (a host command is always a built-in and
/// there is nothing for an addon to fire into), and only the map and the latches carry a `Bound`.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Bound {
/// A registry command — indexes [`SPECS`].
Spec(Cmd),
/// An addon-declared binding — indexes [`BindingDispatch::addons`].
Addon(u16),
}
/// The derived chord→binding map — rebuilt whenever the engine table's generation moves. Probed
/// through [`BindingDispatch::resolve`], never directly: the lookup is two probes, not one.
#[derive(Resource, Default)]
struct BindingDispatch {
map: std::collections::HashMap<Chord, Cmd>,
map: std::collections::HashMap<Chord, Bound>,
/// The addon-declared bindings [`Bound::Addon`] indexes, in the engine table's registration
/// order — re-snapshotted with the map, so the two can never disagree about what an index
/// means. Empty in every session with no addon bindings, which is every session today.
addons: Vec<AddonBindingBody>,
seen_generation: Option<u64>,
}
@@ -76,9 +104,9 @@ impl BindingDispatch {
/// there are untouched. Only the plane's fallback shadow is ours. Mouse and wheel are not
/// suppressed — every dev instrument is a letter, so a modified click is nobody's but the
/// game's.
fn resolve(&self, chord: Chord, dev_plane: bool) -> Option<Cmd> {
if let Some(&cmd) = self.map.get(&chord) {
return Some(cmd);
fn resolve(&self, chord: Chord, dev_plane: bool) -> Option<Bound> {
if let Some(&bound) = self.map.get(&chord) {
return Some(bound);
}
if dev_plane {
return None;
@@ -90,9 +118,9 @@ impl BindingDispatch {
/// This frame's binding activity — what the engine-side consumers read instead of raw keys.
#[derive(Resource, Default)]
pub(crate) struct BindingsState {
/// Live latches: (base key, command) — a [`Kind::Held`] or [`Kind::EdgeUpDown`] press that
/// has not released yet.
latched: Vec<(BindKey, Cmd)>,
/// Live latches: (base key, binding) — a [`Kind::Held`], [`Kind::EdgeUpDown`] or `runOnUp`
/// addon press that has not released yet.
latched: Vec<(BindKey, Bound)>,
/// Commands whose first latch began this frame (the press edge).
just: Vec<Cmd>,
/// Host-edge commands fired this frame.
@@ -105,9 +133,11 @@ pub(crate) struct BindingsState {
}
impl BindingsState {
/// Is a held command latched right now? (The reference's held movement bit.)
/// Is a held command latched right now? (The reference's held movement bit.) Registry
/// commands only — an addon's latch exists to deliver its release half, and no engine system
/// reads it.
pub(crate) fn pressed(&self, c: Cmd) -> bool {
self.latched.iter().any(|&(_, l)| l == c)
self.latched.iter().any(|&(_, l)| l == Bound::Spec(c))
}
/// Did this command's latch begin this frame? (The key-DOWN edge — the autorun cancel set.)
pub(crate) fn just_pressed(&self, c: Cmd) -> bool {
@@ -289,24 +319,37 @@ fn sync_dispatch(script: Option<NonSendMut<UiScript>>, mut dispatch: ResMut<Bind
}
dispatch.seen_generation = Some(generation);
dispatch.map.clear();
let by_name: std::collections::HashMap<&str, Cmd> = SPECS
// The addon table first: it is what the names the registry does not know resolve into. Before
// 1188 phase 4 an unknown name hit the `continue` below and the binding silently never fired
// — it registered, listed in the window, saved and loaded, and did nothing.
let addons = script.addon_binding_bodies();
let mut by_name: std::collections::HashMap<&str, Bound> = SPECS
.iter()
.enumerate()
.map(|(i, s)| (s.name, Cmd(i as u16)))
.map(|(i, s)| (s.name, Bound::Spec(Cmd(i as u16))))
.collect();
for (i, a) in addons.iter().enumerate() {
// A registry name is never overwritten — the engine table already refuses to register an
// addon row over one, so this only ever adds. The `try_from` is the honest form of the
// cast: 65k addon bindings is not a real case, and a wrapped index would dispatch the
// wrong body rather than none.
let Ok(i) = u16::try_from(i) else { break };
by_name.entry(a.name.as_str()).or_insert(Bound::Addon(i));
}
for (name, keys) in script.keybind_snapshot() {
let Some(&cmd) = by_name.get(name.as_str()) else {
let Some(&bound) = by_name.get(name.as_str()) else {
continue;
};
for key in keys {
match Chord::parse(&key) {
Some(ch) => {
dispatch.map.insert(ch, cmd);
dispatch.map.insert(ch, bound);
}
None => warn!("bindings: {name}: unpressable chord '{key}' (unknown token)"),
}
}
}
dispatch.addons = addons;
script.fire_event("UPDATE_BINDINGS", vec![]);
}
@@ -400,9 +443,12 @@ fn latch_and_dispatch(
// the up of a pressed binding regardless).
let typing = capture.0;
if typing && !state.was_typing {
state
.latched
.retain(|&(_, c)| !matches!(SPECS[c.0 as usize].kind, Kind::Held));
state.latched.retain(|&(_, b)| match b {
Bound::Spec(c) => !matches!(SPECS[c.0 as usize].kind, Kind::Held),
// An addon's `runOnUp` latch is an EdgeUpDown pair by another name — its release half
// is a Lua body that must still run, so it stays armed for the same reason.
Bound::Addon(_) => true,
});
}
state.was_typing = typing;
@@ -424,12 +470,25 @@ fn latch_and_dispatch(
shift,
key: BindKey::Key(key),
};
if let Some(cmd) = dispatch.resolve(chord, dev_plane) {
press(&mut state, &mut script, run_lua, cmd, BindKey::Key(key));
if let Some(bound) = dispatch.resolve(chord, dev_plane) {
press(
&mut state,
&mut script,
run_lua,
&dispatch,
bound,
BindKey::Key(key),
);
}
}
ButtonState::Released => {
release(&mut state, &mut script, run_lua, BindKey::Key(key));
release(
&mut state,
&mut script,
run_lua,
&dispatch,
BindKey::Key(key),
);
}
}
}
@@ -455,12 +514,25 @@ fn latch_and_dispatch(
shift,
key: BindKey::Mouse(b),
};
if let Some(cmd) = dispatch.resolve(chord, false) {
press(&mut state, &mut script, run_lua, cmd, BindKey::Mouse(b));
if let Some(bound) = dispatch.resolve(chord, false) {
press(
&mut state,
&mut script,
run_lua,
&dispatch,
bound,
BindKey::Mouse(b),
);
}
}
if buttons.just_released(b) {
release(&mut state, &mut script, run_lua, BindKey::Mouse(b));
release(
&mut state,
&mut script,
run_lua,
&dispatch,
BindKey::Mouse(b),
);
}
}
@@ -486,8 +558,8 @@ fn latch_and_dispatch(
shift,
key,
};
if let Some(cmd) = dispatch.resolve(chord, false) {
match &SPECS[cmd.0 as usize].kind {
match dispatch.resolve(chord, false) {
Some(Bound::Spec(cmd)) => match &SPECS[cmd.0 as usize].kind {
Kind::Edge(lua) => run_lua(&mut script, lua, SPECS[cmd.0 as usize].name),
Kind::Host => {
state.fired.push(cmd);
@@ -496,7 +568,18 @@ fn latch_and_dispatch(
// Unreachable by construction (SetBinding refuses these); harmless if a
// hand-edited file smuggles one in.
Kind::Held | Kind::EdgeUpDown(..) => {}
},
// An addon binding on the wheel is a one-shot press, the `Kind::Edge` case. A
// `runOnUp` one cannot be here at all — its entry carries the same press+release flag
// a Held/EdgeUpDown command does, and the table's wheel refusal reads exactly that.
Some(Bound::Addon(i)) => {
if let Some(a) = dispatch.addons.get(i as usize) {
if !a.run_on_up {
run_addon(&mut script, a, "down");
}
}
}
None => {}
}
}
@@ -517,22 +600,28 @@ fn latch_and_dispatch(
}
}
for k in stuck {
release(&mut state, &mut script, run_lua, k);
release(&mut state, &mut script, run_lua, &dispatch, k);
}
}
#[cfg(test)]
impl BindingDispatch {
/// A dispatch seeded straight from the registry defaults — the no-VM test seam.
/// A dispatch seeded straight from the registry defaults — the no-VM test seam. No addon
/// bindings: an addon body is Lua, and a harness with no VM could only assert it vacuously
/// (the with-VM harness below is where those live).
fn test_defaults() -> Self {
let mut map = std::collections::HashMap::new();
for (i, s) in SPECS.iter().enumerate() {
for d in [s.d1, s.d2].into_iter().flatten() {
map.insert(Chord::parse(d).expect("default parses"), Cmd(i as u16));
map.insert(
Chord::parse(d).expect("default parses"),
Bound::Spec(Cmd(i as u16)),
);
}
}
Self {
map,
addons: Vec::new(),
seen_generation: None,
}
}
@@ -543,21 +632,37 @@ fn press(
state: &mut BindingsState,
script: &mut Option<NonSendMut<UiScript>>,
run_lua: impl Fn(&mut Option<NonSendMut<UiScript>>, &str, &str),
cmd: Cmd,
dispatch: &BindingDispatch,
bound: Bound,
key: BindKey,
) {
let cmd = match bound {
Bound::Spec(cmd) => cmd,
// An addon's press: run its one body with `keystate = "down"`, and latch it only if it
// asked for the release half — the `Kind::Edge` / `Kind::EdgeUpDown` fork, decided by the
// file's `runOnUp` instead of by our own registry.
Bound::Addon(i) => {
if let Some(a) = dispatch.addons.get(i as usize) {
run_addon(script, a, "down");
if a.run_on_up {
state.latched.push((key, bound));
}
}
return;
}
};
let spec = &SPECS[cmd.0 as usize];
match &spec.kind {
Kind::Held => {
if !state.pressed(cmd) {
state.just.push(cmd);
}
state.latched.push((key, cmd));
state.latched.push((key, bound));
}
Kind::Edge(lua) => run_lua(script, lua, spec.name),
Kind::EdgeUpDown(down, _) => {
run_lua(script, down, spec.name);
state.latched.push((key, cmd));
state.latched.push((key, bound));
}
Kind::Host => {
state.fired.push(cmd);
@@ -575,14 +680,25 @@ fn release(
state: &mut BindingsState,
script: &mut Option<NonSendMut<UiScript>>,
run_lua: impl Fn(&mut Option<NonSendMut<UiScript>>, &str, &str),
dispatch: &BindingDispatch,
key: BindKey,
) {
let mut i = 0;
while i < state.latched.len() {
if state.latched[i].0 == key {
let (_, cmd) = state.latched.remove(i);
if let Kind::EdgeUpDown(_, up) = &SPECS[cmd.0 as usize].kind {
run_lua(script, up, SPECS[cmd.0 as usize].name);
match state.latched.remove(i).1 {
Bound::Spec(cmd) => {
if let Kind::EdgeUpDown(_, up) = &SPECS[cmd.0 as usize].kind {
run_lua(script, up, SPECS[cmd.0 as usize].name);
}
}
// The same body again, this time with `keystate = "up"` — the one law that makes
// an addon's `runOnUp` binding one chunk instead of two.
Bound::Addon(a) => {
if let Some(a) = dispatch.addons.get(a as usize) {
run_addon(script, a, "up");
}
}
}
} else {
i += 1;
@@ -590,6 +706,42 @@ fn release(
}
}
/// Run one addon binding's body with the reference's `keystate` global set **for the duration of
/// the call, and restored after**.
///
/// A `Bindings.xml` body is **one Lua chunk read twice**: `runOnUp="true"` means it runs on the
/// press and again on the release, and every shipped body forks on the bare global `keystate`
/// (`if ( keystate == "down" ) then MoveForwardStart(); else MoveForwardStop(); end`). So the
/// global is set in `_G` where the chunk will look for it — not prepended to the source, which
/// would shift every line number in the addon's own error messages.
///
/// **Restoring it is not tidiness.** `keystate` is absent from the 1.12.1 client's in-world `_G`
/// (`reference/1.12-globals.tsv`) even though its own `Bindings.xml` bodies read it as a bare
/// global — which is only possible if the reference sets it transiently around the call, exactly
/// as it does `this`/`event`/`arg1` (`invoke_with_globals`, RF-0025). Leaving it set would hand
/// every addon a global the reference does not have, and an addon that feature-detects it would
/// take a path we cannot honour — decision 1189's "a superset is not free", one call deeper.
///
/// Save-and-restore rather than set-and-delete, because these bodies nest: a binding whose Lua
/// fires another binding must not clear the outer one's `keystate` on the way out.
fn run_addon(script: &mut Option<NonSendMut<UiScript>>, bind: &AddonBindingBody, keystate: &str) {
let Some(s) = script.as_mut() else { return };
let globals = s.lua().globals();
// `Option<String>` rather than a raw Lua value: benilla-app does not depend on mlua, and
// `keystate` is a string or nothing — `None` converts back to nil on the way out.
let prior: Option<String> = globals.get("keystate").unwrap_or(None);
if let Err(e) = globals.set("keystate", keystate) {
warn!("bindings({}): setting keystate: {e}", bind.name);
return;
}
if let Err(e) = s.run(&bind.body) {
warn!("bindings({}): {e}", bind.name);
}
if let Err(e) = s.lua().globals().set("keystate", prior) {
warn!("bindings({}): restoring keystate: {e}", bind.name);
}
}
#[cfg(test)]
mod tests {
use bevy::input::keyboard::{Key, KeyboardInput};
@@ -633,6 +785,190 @@ mod tests {
app.world().resource::<BindingsState>()
}
/// One addon's `Bindings.xml`, in the reference's own shape: a `runOnUp` binding whose single
/// body forks on `keystate`, and a one-shot beside it. Each half counts itself in a global, so
/// the assertions below read what the VM actually ran rather than what we told it to run.
const PROBE_BINDINGS: &str = r#"<Bindings>
<Binding name="PROBEHOLD" runOnUp="true" header="PROBE">
if ( keystate == "down" ) then
PROBE_DOWN = (PROBE_DOWN or 0) + 1;
else
PROBE_UP = (PROBE_UP or 0) + 1;
end
PROBE_LAST = keystate;
</Binding>
<Binding name="PROBEEDGE">
PROBE_EDGE = (PROBE_EDGE or 0) + 1;
</Binding>
</Bindings>"#;
/// The **with-VM** harness: a real engine table (the whole registry plus one addon's parsed
/// `Bindings.xml`) with the real [`sync_dispatch`] chained in front of [`latch_and_dispatch`].
///
/// Deliberately not [`BindingDispatch::test_defaults`]: the bug 1188 phase 4 removes lived in
/// the *derivation* — `sync_dispatch` dropped every name it could not find in `SPECS`, so a
/// hand-built map would assert past exactly the code that was wrong.
fn vm_harness(script: UiScript) -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, bevy::input::InputPlugin))
.init_resource::<UiKeyboardCapture>()
.init_resource::<PlayerUiHover>()
.init_resource::<PointerOverUi>()
.init_resource::<BindingsState>()
.init_resource::<BindingDispatch>()
.add_systems(Update, (sync_dispatch, latch_and_dispatch).chain());
app.insert_non_send_resource(script);
app
}
/// A counter a binding body left in the VM — `0` when the body never ran.
fn lua_count(app: &App, global: &str) -> i64 {
app.world()
.non_send_resource::<UiScript>()
.eval::<i64>(&format!("return {global} or 0"))
.expect("eval")
}
/// A string a binding body left in the VM — `""` when it never ran.
fn lua_str(app: &App, global: &str) -> String {
app.world()
.non_send_resource::<UiScript>()
.eval::<String>(&format!(r#"return {global} or """#))
.expect("eval")
}
/// **An addon's binding actually fires** (decision 1188 phase 4) — the assertion the whole
/// phase exists for. Before it, an addon's `Bindings.xml` registered, listed in the Key
/// Bindings window, saved and loaded, and then did *nothing*: `sync_dispatch` silently dropped
/// every name that was not in [`SPECS`], so no chord ever reached the body.
///
/// The `runOnUp` half is the part that cannot be guessed from the built-in path, because it is
/// shaped differently: our own registry holds two Lua strings ([`Kind::EdgeUpDown`]), an
/// addon holds **one chunk run twice** with the global `keystate` set to `"down"` then
/// `"up"`. A dispatch that ran the body once, or twice with the same `keystate`, leaves the
/// player's key held down forever — which is what every shipped `runOnUp` body's `else` branch
/// is there to prevent.
#[test]
fn an_addon_binding_fires_its_lua_and_runs_again_on_release_when_it_asked_to() {
let mut script = UiScript::new().expect("VM");
script.register_bindings(&registry_commands());
script.register_addon_bindings(
"ProbeAddon",
&benilla_ui::bindings_xml::parse(PROBE_BINDINGS).expect("well-formed"),
);
// A `<Binding>` ships no default chord in 1.12 — the shipped defaults live in the engine's
// own table — so an addon binding starts unbound and the player binds it, which is what
// the Key Bindings window does through exactly this call.
script
.run(r#"SetBinding("J", "PROBEHOLD"); SetBinding("G", "PROBEEDGE")"#)
.expect("bind");
let mut app = vm_harness(script);
// Press: one run, `keystate == "down"`.
press_key(&mut app, KeyCode::KeyJ);
app.update();
assert_eq!(
lua_count(&app, "PROBE_DOWN"),
1,
"the press must reach the addon's body — this is the phase-4 bug"
);
assert_eq!(lua_str(&app, "PROBE_LAST"), "down");
assert_eq!(
lua_count(&app, "PROBE_UP"),
0,
"no release has happened yet"
);
// Release: the SAME body again, with `keystate == "up"`.
release_key(&mut app, KeyCode::KeyJ);
app.update();
assert_eq!(lua_count(&app, "PROBE_UP"), 1);
assert_eq!(lua_str(&app, "PROBE_LAST"), "up");
assert_eq!(
lua_count(&app, "PROBE_DOWN"),
1,
"the release runs the chunk with keystate=up, not the down half a second time"
);
// The one-shot binding: press runs it, release does not — the `Kind::Edge` behaviour,
// decided here by the file's missing `runOnUp` rather than by our registry.
press_key(&mut app, KeyCode::KeyG);
app.update();
assert_eq!(lua_count(&app, "PROBE_EDGE"), 1);
release_key(&mut app, KeyCode::KeyG);
app.update();
assert_eq!(
lua_count(&app, "PROBE_EDGE"),
1,
"no runOnUp, no second run — an addon that toggled here would toggle back"
);
// And the registry dispatches unchanged beside them: the enum routes, it does not divert.
press_key(&mut app, KeyCode::KeyW);
app.update();
assert!(state(&app).pressed(cmd::MOVE_FORWARD));
assert_eq!(
lua_count(&app, "PROBE_DOWN"),
1,
"a built-in press is nobody else's"
);
// **`keystate` does not outlive the call.** It is absent from the 1.12.1 client's in-world
// `_G` (`reference/1.12-globals.tsv`) even though its own binding bodies read it, so the
// reference sets it transiently — as it does `this`/`arg1`. Leaving it set would hand
// every addon a global the reference lacks, and one that feature-detects `if keystate`
// would take a branch we cannot honour (decision 1189's "a superset is not free").
assert_eq!(
lua_str(&app, "tostring(keystate)"),
"nil",
"keystate must be restored after a binding body runs, not left standing in _G"
);
}
/// A `runOnUp` addon latch survives a chat box taking focus, and its release half still runs.
///
/// The typing edge clears [`Kind::Held`] latches (the reference's focus handler releasing the
/// direction bits) and deliberately leaves [`Kind::EdgeUpDown`] armed, because the up half of
/// a *pressed* binding is delivered regardless of focus. An addon's `runOnUp` binding is that
/// same pair wearing one chunk, so it must follow the same rule — dropped on the focus edge,
/// it would leave whatever its down half started running forever, with no key left to press
/// to stop it.
#[test]
fn a_run_on_up_addon_latch_survives_the_typing_edge_and_still_releases() {
let mut script = UiScript::new().expect("VM");
script.register_bindings(&registry_commands());
script.register_addon_bindings(
"ProbeAddon",
&benilla_ui::bindings_xml::parse(PROBE_BINDINGS).expect("well-formed"),
);
script.run(r#"SetBinding("J", "PROBEHOLD")"#).expect("bind");
let mut app = vm_harness(script);
press_key(&mut app, KeyCode::KeyW);
press_key(&mut app, KeyCode::KeyJ);
app.update();
assert!(state(&app).pressed(cmd::MOVE_FORWARD));
assert_eq!(lua_count(&app, "PROBE_DOWN"), 1);
// A box takes focus: movement stops, the addon's latch stays.
app.world_mut().resource_mut::<UiKeyboardCapture>().0 = true;
app.update();
assert!(!state(&app).pressed(cmd::MOVE_FORWARD));
assert_eq!(
lua_count(&app, "PROBE_UP"),
0,
"the focus edge is not a release — nothing has run the up half yet"
);
release_key(&mut app, KeyCode::KeyJ);
app.update();
assert_eq!(
lua_count(&app, "PROBE_UP"),
1,
"the up half is delivered even while typing, like every other pressed binding's"
);
}
#[test]
fn held_commands_latch_across_frames_and_release_per_base_key() {
let mut app = harness();
@@ -755,7 +1091,7 @@ mod tests {
// is 0585's original "one key did two things" bug, reborn one strip further along...
assert_eq!(
dispatch.resolve(plane_p, false),
Some(pet_paper_doll),
Some(Bound::Spec(pet_paper_doll)),
"without the plane rule the retry does reach SHIFT-P — this is what is being blocked"
);
// ...so on the plane, the retry is what it costs.
@@ -764,15 +1100,18 @@ mod tests {
// shipped default proves it (1.12's own two, CTRL-SHIFT-TAB and CTRL-SHIFT-PAGEDOWN,
// belong to commands the honest tree doesn't carry yet), so the entry is made here: this
// is the case a player creates the moment they bind anything on the plane.
dispatch.map.insert(plane_p, pet_paper_doll);
dispatch.map.insert(plane_p, Bound::Spec(pet_paper_doll));
assert_eq!(
dispatch.resolve(plane_p, true),
Some(pet_paper_doll),
Some(Bound::Spec(pet_paper_doll)),
"the plane spends the retry, never the exact probe"
);
// And SHIFT-P itself is still the pet paper doll: the plane costs nothing outside itself.
let shift_p = Chord::parse("SHIFT-P").expect("parses");
assert_eq!(dispatch.resolve(shift_p, false), Some(pet_paper_doll));
assert_eq!(
dispatch.resolve(shift_p, false),
Some(Bound::Spec(pet_paper_doll))
);
}
/// **The pet lane routes on the CTRL digits, and the number row is untouched** (B218,
@@ -793,7 +1132,7 @@ mod tests {
"the modifier decides: CTRL-1 is not the number row's"
);
// The runOnUp half: the latch drops on the BASE key's release (which is what runs
// BenillaPetActionButtonUp → CastPetAction in the app's VM), Ctrl still held.
// PetActionButtonUp → CastPetAction in the app's VM), Ctrl still held.
release_key(&mut app, KeyCode::Digit1);
app.update();
assert!(!state(&app).pressed(by_name("BONUSACTIONBUTTON1")));
+83 -113
View File
@@ -165,84 +165,84 @@ pub(crate) static SPECS: &[Spec] = &[
spec!(
"ACTIONBUTTON1",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(1)", "BenillaActionButtonUp(1)"),
Kind::EdgeUpDown("ActionButtonDown(1)", "ActionButtonUp(1)"),
Some("1"),
None
),
spec!(
"ACTIONBUTTON2",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(2)", "BenillaActionButtonUp(2)"),
Kind::EdgeUpDown("ActionButtonDown(2)", "ActionButtonUp(2)"),
Some("2"),
None
),
spec!(
"ACTIONBUTTON3",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(3)", "BenillaActionButtonUp(3)"),
Kind::EdgeUpDown("ActionButtonDown(3)", "ActionButtonUp(3)"),
Some("3"),
None
),
spec!(
"ACTIONBUTTON4",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(4)", "BenillaActionButtonUp(4)"),
Kind::EdgeUpDown("ActionButtonDown(4)", "ActionButtonUp(4)"),
Some("4"),
None
),
spec!(
"ACTIONBUTTON5",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(5)", "BenillaActionButtonUp(5)"),
Kind::EdgeUpDown("ActionButtonDown(5)", "ActionButtonUp(5)"),
Some("5"),
None
),
spec!(
"ACTIONBUTTON6",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(6)", "BenillaActionButtonUp(6)"),
Kind::EdgeUpDown("ActionButtonDown(6)", "ActionButtonUp(6)"),
Some("6"),
None
),
spec!(
"ACTIONBUTTON7",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(7)", "BenillaActionButtonUp(7)"),
Kind::EdgeUpDown("ActionButtonDown(7)", "ActionButtonUp(7)"),
Some("7"),
None
),
spec!(
"ACTIONBUTTON8",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(8)", "BenillaActionButtonUp(8)"),
Kind::EdgeUpDown("ActionButtonDown(8)", "ActionButtonUp(8)"),
Some("8"),
None
),
spec!(
"ACTIONBUTTON9",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(9)", "BenillaActionButtonUp(9)"),
Kind::EdgeUpDown("ActionButtonDown(9)", "ActionButtonUp(9)"),
Some("9"),
None
),
spec!(
"ACTIONBUTTON10",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(10)", "BenillaActionButtonUp(10)"),
Kind::EdgeUpDown("ActionButtonDown(10)", "ActionButtonUp(10)"),
Some("0"),
None
),
spec!(
"ACTIONBUTTON11",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(11)", "BenillaActionButtonUp(11)"),
Kind::EdgeUpDown("ActionButtonDown(11)", "ActionButtonUp(11)"),
Some("-"),
None
),
spec!(
"ACTIONBUTTON12",
ACTIONBAR,
Kind::EdgeUpDown("BenillaActionButtonDown(12)", "BenillaActionButtonUp(12)"),
Kind::EdgeUpDown("ActionButtonDown(12)", "ActionButtonUp(12)"),
Some("="),
None
),
@@ -252,7 +252,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON1",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton1"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton1"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F1"),
None
@@ -261,7 +261,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON2",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton2"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton2"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F2"),
None
@@ -270,7 +270,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON3",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton3"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton3"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F3"),
None
@@ -279,7 +279,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON4",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton4"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton4"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F4"),
None
@@ -288,7 +288,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON5",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton5"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton5"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F5"),
None
@@ -297,7 +297,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON6",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton6"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton6"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F6"),
None
@@ -306,7 +306,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON7",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton7"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton7"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F7"),
None
@@ -315,7 +315,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON8",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton8"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton8"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F8"),
None
@@ -324,7 +324,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON9",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton9"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton9"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F9"),
None
@@ -333,7 +333,7 @@ pub(crate) static SPECS: &[Spec] = &[
"SHAPESHIFTBUTTON10",
ACTIONBAR,
Kind::Edge(
r#"local b = getglobal("BenillaShapeshiftButton10"); if b and b:IsVisible() then b:Click() end"#
r#"local b = getglobal("ShapeshiftButton10"); if b and b:IsVisible() then b:Click() end"#
),
Some("CTRL-F10"),
None
@@ -349,100 +349,70 @@ pub(crate) static SPECS: &[Spec] = &[
spec!(
"BONUSACTIONBUTTON1",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(1)",
"BenillaPetActionButtonUp(1)"
),
Kind::EdgeUpDown("PetActionButtonDown(1)", "PetActionButtonUp(1)"),
Some("CTRL-1"),
None
),
spec!(
"BONUSACTIONBUTTON2",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(2)",
"BenillaPetActionButtonUp(2)"
),
Kind::EdgeUpDown("PetActionButtonDown(2)", "PetActionButtonUp(2)"),
Some("CTRL-2"),
None
),
spec!(
"BONUSACTIONBUTTON3",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(3)",
"BenillaPetActionButtonUp(3)"
),
Kind::EdgeUpDown("PetActionButtonDown(3)", "PetActionButtonUp(3)"),
Some("CTRL-3"),
None
),
spec!(
"BONUSACTIONBUTTON4",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(4)",
"BenillaPetActionButtonUp(4)"
),
Kind::EdgeUpDown("PetActionButtonDown(4)", "PetActionButtonUp(4)"),
Some("CTRL-4"),
None
),
spec!(
"BONUSACTIONBUTTON5",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(5)",
"BenillaPetActionButtonUp(5)"
),
Kind::EdgeUpDown("PetActionButtonDown(5)", "PetActionButtonUp(5)"),
Some("CTRL-5"),
None
),
spec!(
"BONUSACTIONBUTTON6",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(6)",
"BenillaPetActionButtonUp(6)"
),
Kind::EdgeUpDown("PetActionButtonDown(6)", "PetActionButtonUp(6)"),
Some("CTRL-6"),
None
),
spec!(
"BONUSACTIONBUTTON7",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(7)",
"BenillaPetActionButtonUp(7)"
),
Kind::EdgeUpDown("PetActionButtonDown(7)", "PetActionButtonUp(7)"),
Some("CTRL-7"),
None
),
spec!(
"BONUSACTIONBUTTON8",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(8)",
"BenillaPetActionButtonUp(8)"
),
Kind::EdgeUpDown("PetActionButtonDown(8)", "PetActionButtonUp(8)"),
Some("CTRL-8"),
None
),
spec!(
"BONUSACTIONBUTTON9",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(9)",
"BenillaPetActionButtonUp(9)"
),
Kind::EdgeUpDown("PetActionButtonDown(9)", "PetActionButtonUp(9)"),
Some("CTRL-9"),
None
),
spec!(
"BONUSACTIONBUTTON10",
ACTIONBAR,
Kind::EdgeUpDown(
"BenillaPetActionButtonDown(10)",
"BenillaPetActionButtonUp(10)"
),
Kind::EdgeUpDown("PetActionButtonDown(10)", "PetActionButtonUp(10)"),
Some("CTRL-0"),
None
),
@@ -580,7 +550,7 @@ pub(crate) static SPECS: &[Spec] = &[
spec!(
"TOGGLECHARACTER0",
INTERFACE,
Kind::Edge(r#"ToggleCharacter("BenillaPaperDollFrame")"#),
Kind::Edge(r#"ToggleCharacter("PaperDollFrame")"#),
Some("C"),
None
),
@@ -597,7 +567,7 @@ pub(crate) static SPECS: &[Spec] = &[
spec!(
"TOGGLECHARACTER1",
INTERFACE,
Kind::Edge(r#"ToggleCharacter("BenillaSkillFrame")"#),
Kind::Edge(r#"ToggleCharacter("SkillFrame")"#),
Some("K"),
None
),
@@ -610,7 +580,7 @@ pub(crate) static SPECS: &[Spec] = &[
spec!(
"TOGGLECHARACTER3",
INTERFACE,
Kind::Edge(r#"ToggleCharacter("BenillaPetPaperDollFrame")"#),
Kind::Edge(r#"ToggleCharacter("PetPaperDollFrame")"#),
Some("SHIFT-P"),
None
),
@@ -769,8 +739,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON1",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 1)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 1)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 1)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 1)"#
),
None,
None
@@ -779,8 +749,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON2",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 2)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 2)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 2)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 2)"#
),
None,
None
@@ -789,8 +759,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON3",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 3)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 3)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 3)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 3)"#
),
None,
None
@@ -799,8 +769,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON4",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 4)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 4)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 4)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 4)"#
),
None,
None
@@ -809,8 +779,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON5",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 5)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 5)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 5)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 5)"#
),
None,
None
@@ -819,8 +789,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON6",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 6)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 6)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 6)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 6)"#
),
None,
None
@@ -829,8 +799,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON7",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 7)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 7)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 7)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 7)"#
),
None,
None
@@ -839,8 +809,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON8",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 8)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 8)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 8)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 8)"#
),
None,
None
@@ -849,8 +819,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON9",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 9)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 9)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 9)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 9)"#
),
None,
None
@@ -859,8 +829,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON10",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 10)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 10)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 10)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 10)"#
),
None,
None
@@ -869,8 +839,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON11",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 11)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 11)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 11)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 11)"#
),
None,
None
@@ -879,8 +849,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR1BUTTON12",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomLeft", 12)"#,
r#"BenillaMultiActionButtonUp("BottomLeft", 12)"#
r#"MultiActionButtonDown("MultiBarBottomLeft", 12)"#,
r#"MultiActionButtonUp("MultiBarBottomLeft", 12)"#
),
None,
None
@@ -889,8 +859,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON1",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 1)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 1)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 1)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 1)"#
),
None,
None
@@ -899,8 +869,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON2",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 2)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 2)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 2)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 2)"#
),
None,
None
@@ -909,8 +879,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON3",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 3)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 3)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 3)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 3)"#
),
None,
None
@@ -919,8 +889,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON4",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 4)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 4)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 4)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 4)"#
),
None,
None
@@ -929,8 +899,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON5",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 5)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 5)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 5)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 5)"#
),
None,
None
@@ -939,8 +909,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON6",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 6)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 6)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 6)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 6)"#
),
None,
None
@@ -949,8 +919,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON7",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 7)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 7)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 7)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 7)"#
),
None,
None
@@ -959,8 +929,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON8",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 8)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 8)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 8)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 8)"#
),
None,
None
@@ -969,8 +939,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON9",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 9)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 9)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 9)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 9)"#
),
None,
None
@@ -979,8 +949,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON10",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 10)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 10)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 10)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 10)"#
),
None,
None
@@ -989,8 +959,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON11",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 11)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 11)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 11)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 11)"#
),
None,
None
@@ -999,8 +969,8 @@ pub(crate) static SPECS: &[Spec] = &[
"MULTIACTIONBAR2BUTTON12",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"BenillaMultiActionButtonDown("BottomRight", 12)"#,
r#"BenillaMultiActionButtonUp("BottomRight", 12)"#
r#"MultiActionButtonDown("MultiBarBottomRight", 12)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 12)"#
),
None,
None
+18 -6
View File
@@ -96,8 +96,15 @@ pub(crate) fn resolve(diff: &[(String, Vec<String>)]) -> Vec<(String, Vec<String
held.retain(|h| h != k);
}
}
if let Some((_, slot)) = table.iter_mut().find(|(n, _)| n == name) {
*slot = keys.clone();
match table.iter_mut().find(|(n, _)| n == name) {
Some((_, slot)) => *slot = keys.clone(),
// **A name that is not in SPECS is an ADDON's command, and it is kept** (decision
// 1201). Dropping it here is what made an addon binding forget its key every restart:
// the row was written by `to_diff`, read back by `from_diff`, had its chord stolen
// from whoever else held it by the pass above — and was then thrown away, so the
// binding registered at world entry with nothing to restore. `Bindings.txt` is a flat
// list in the reference too; an addon's row is a row like any other.
None => table.push((name.clone(), keys.clone())),
}
}
table
@@ -180,11 +187,16 @@ mod tests {
("BOGUSCMD".to_string(), vec!["Q".to_string()]),
]
);
// Unknown commands pass through parse (kept for forward compat); resolve just drops
// what the registry doesn't know — after stealing its keys, so the file's intent
// ("Q belongs to BOGUSCMD, not STRAFELEFT") still holds for the commands we do have.
// **A command the registry does not know is KEPT** (decision 1201). It used to be
// dropped after its keys were stolen, which was right for a genuinely bogus line and
// catastrophic for the case that actually occurs: an ADDON's command, whose
// `Bindings.xml` registers at world entry, hours after this file was read. Dropping it
// meant the binding registered with nothing to restore and forgot the player's chord
// every restart (1192 §4). The steal still happens either way, so the file's intent
// ("Q belongs to that command, not STRAFELEFT") holds for the commands we do have.
let resolved = resolve(&parsed);
assert!(!resolved.iter().any(|(n, _)| n == "BOGUSCMD"));
let bogus = &resolved.iter().find(|(n, _)| n == "BOGUSCMD").unwrap().1;
assert_eq!(bogus, &vec!["Q".to_string()]);
let strafe = &resolved.iter().find(|(n, _)| n == "STRAFELEFT").unwrap().1;
assert!(
strafe.is_empty(),
+6 -6
View File
@@ -346,10 +346,10 @@ pub(super) fn seed_ui_fixture(
// frames — the window's OnUpdate polls, flag-guarded, exactly once).
if let Some(s) = script.as_mut() {
if let Err(e) = s.run(
"BenillaBankFrame:SetScript(\"OnUpdate\", function()\n\
if not benillaBankPopoutDone and C_Container.GetContainerNumSlots(5) > 0 then\n\
"BankFrame:SetScript(\"OnUpdate\", function()\n\
if not benillaBankPopoutDone and GetContainerNumSlots(5) > 0 then\n\
benillaBankPopoutDone = 1\n\
BenillaBankBagButton_OnClick(getglobal(\"BenillaBankBagButton1\"))\n\
BenillaBankBagButton_OnClick(getglobal(\"BankBagButton1\"))\n\
end\n\
end)",
) {
@@ -866,7 +866,7 @@ pub(super) fn seed_ui_fixture(
// Open the window (the C binding's path). The feed pushes on the following frames of
// the settle window; OnShow + the UNIT_* events repaint exactly as live.
if let Some(s) = script.as_mut() {
if let Err(e) = s.run("ToggleCharacter(\"BenillaPaperDollFrame\")") {
if let Err(e) = s.run("ToggleCharacter(\"PaperDollFrame\")") {
warn!("capture: ui-char seed failed to open the window: {e}");
}
}
@@ -1138,7 +1138,7 @@ pub(super) fn seed_ui_fixture(
// opaque-gray selection highlight (ctor 0xFF606060) under the glyphs AND the white
// caret at the selection's end — the whole text-UI overlay stack in one golden.
// …then pin the hover-revealed chrome: in-game the tab + the black window textures
// follow the OS cursor (BenillaFCF_OnUpdate), which a headless capture can't hover —
// follow the OS cursor (FCF_OnUpdate), which a headless capture can't hover —
// replace the OnUpdate with a fixed-reveal one so the golden also locks the window
// tint (chat-cache COLOR 0 0 0) and the text-sized tab (BenillaFCF_TabResize, which
// must keep retrying until the label's measure lands).
@@ -1261,7 +1261,7 @@ pub(super) fn seed_ui_fixture(
}
/// Seed + open the backpack window with a fixed item set — shared by the `Bag` and `Tooltip`
/// fixtures. The bag is a standalone addon (no `ShowUIPanel` path): drive the `C_Container` snapshot
/// fixtures. The bag is a standalone addon (no `ShowUIPanel` path): drive the container snapshot
/// and purse directly, then open and paint. The feed (`crate::ui_items`) leaves bag 0 alone when
/// there is no `SelfPlayer` (net is disabled in capture), so this manual snapshot is not clobbered.
fn seed_bag_window(
@@ -86,10 +86,10 @@ struct ProbeCastCancel {
/// probe's target) and use it. `false` = not resolved yet — a fresh character's first login
/// still has the item-query round trip in flight, so the caller retries per frame.
const USE_HEARTH_CHUNK: &str = "return (function()\n\
for s = 1, C_Container.GetContainerNumSlots(0) do\n\
local link = C_Container.GetContainerItemLink(0, s)\n\
for s = 1, GetContainerNumSlots(0) do\n\
local link = GetContainerItemLink(0, s)\n\
if link and string.find(link, \"item:6948\", 1, true) then\n\
C_Container.UseContainerItem(0, s)\n\
UseContainerItem(0, s)\n\
return true\n\
end\n\
end\n\
+7 -14
View File
@@ -474,17 +474,15 @@ fn mail_probe(
// itself uses; encoded bag*100+slot (-1 = not there yet).
let found = script
.eval::<i64>(
"for bag=0,4 do local n=C_Container.GetContainerNumSlots(bag) or 0 \
for slot=1,n do local link=C_Container.GetContainerItemLink(bag,slot) \
"for bag=0,4 do local n=GetContainerNumSlots(bag) or 0 \
for slot=1,n do local link=GetContainerItemLink(bag,slot) \
if link and string.find(link,'item:8383',1,true) then return bag*100+slot end end end \
return -1",
)
.unwrap_or(-1);
if found >= 0 {
let (bag, lslot) = (found / 100, found % 100);
if let Err(e) =
script.run(&format!("C_Container.UseContainerItem({bag}, {lslot})"))
{
if let Err(e) = script.run(&format!("UseContainerItem({bag}, {lslot})")) {
error!("PROBE_MAIL: FAIL (b3) — UseContainerItem({bag}, {lslot}) errored: {e}");
probe.fails += 1;
probe.phase = Phase::TakeMoney {
@@ -510,23 +508,18 @@ fn mail_probe(
Some((bag, lslot)) => {
let painted = script
.eval::<bool>(
"return BenillaItemTextFrame:IsShown() \
and string.find(BenillaItemTextPageText:GetText() or '', 'hello', 1, true) ~= nil",
"return ItemTextFrame:IsShown() \
and string.find(ItemTextPageText:GetText() or '', 'hello', 1, true) ~= nil",
)
.unwrap_or(false);
if painted {
info!("PROBE_MAIL: PASS (b3) — the reader painted the letter body (title/creator/text via ITEM_TEXT_BEGIN→READY)");
probe.passes += 1;
// Cleanup: close the reader, destroy the copy (else bags gain one per run).
crate::ui_script::run_or_warn(&script, "ItemTextCloseButton:Click()");
crate::ui_script::run_or_warn(
&script,
"BenillaItemTextCloseButton:Click()",
);
crate::ui_script::run_or_warn(
&script,
&format!(
"C_Container.PickupContainerItem({bag}, {lslot}) DeleteCursorItem()"
),
&format!("PickupContainerItem({bag}, {lslot}) DeleteCursorItem()"),
);
probe.phase = Phase::TakeMoney {
since: now,
+1 -1
View File
@@ -39,7 +39,7 @@ pub(super) enum UiFixture {
/// render): vault slots with icons, bag buttons (owned icon / bought-empty / red unpurchased),
/// and the purchase row's DBC cost.
Bank,
/// The multi-quest greeting panel (`QUEST_GREETING`, `BenillaQuestGreetingPanel`): a greeting
/// The multi-quest greeting panel (`QUEST_GREETING`, `QuestGreetingPanel`): a greeting
/// line plus an "Available Quests" list of `UI-Quest-BulletPoint` title rows — the frame the
/// gossip-vs-greeting confusion turned on, and previously uncaptured (bullet/title seating had
/// no regression baseline).
@@ -0,0 +1,730 @@
//! The **character-select AddOns screen** (decision 1197) — the glue half of the director's ask,
//! and the button 1191 §5 recorded as *"not hidden; never built"*.
//!
//! The reference's is `Interface\GlueXML\AddonList.xml/.lua`, opened by
//! `CharacterSelectAddonsButton`. We have that Lua (read off the patch chain) and it is the
//! authority for **behaviour**; we do not have its XML, so the geometry here is ours, derived from
//! the two constants the Lua does carry: `ADDON_BUTTON_HEIGHT = 16` and
//! `MAX_ADDONS_DISPLAYED = 19` — a 19-row list at 16 px pitch, 304 px of list. Everything else is
//! sized around that and stated as ours rather than passed off as transcribed.
//!
//! ## What the reference does, and what we do
//!
//! `AddonList_Update` per row: a tri-state checkbox, the title (`## Title` else the folder name)
//! coloured **gold when loadable, red when enabled-but-broken, grey when disabled**, the status
//! token (`getglobal("ADDON_"..reason)`), a URL/update button, and a security icon. Hovering a row
//! raises `AddonTooltip` with title + notes + `ADDON_DEPENDENCIES`.
//!
//! Carried: the list, the checkbox, the three-colour title rule, the status column, the
//! hover tooltip with notes and dependencies, scrolling, Enable All / Disable All, and
//! Okay/Cancel with Cancel meaning *discard* (the reference's `ResetAddOns`).
//!
//! **Not carried, each for a stated reason:**
//! - the **security icon** — `## Secure: 1` is Blizzard's own signature marker and nothing a
//! player installs carries it honestly (1191 §6 already refused to treat it as granting
//! anything). An icon that is always "insecure" tells nobody anything.
//! - the **URL / update buttons** — they launch a browser. That is an outward-facing action from a
//! game client, and it is the director's call to make, not this screen's. `## URL` is shown in
//! the tooltip instead, where it is information rather than a trigger.
//! - the **character dropdown** (`AddonCharacterDropDown`, the "All" vs per-character switch).
//! Ours edits the **selected** character's enable file, because that is the file the world-entry
//! walk will read (1191 §7). "All characters" needs a glue dropdown widget we do not have and a
//! fan-out write across every roster file; it is queued, not forgotten.
//! - `SetScriptMemory` / `GetScriptMemory` — a 1.12 Lua-heap dial with no equivalent here.
//!
//! ## Why it reads the folder rather than the VM
//!
//! At character select no addon has been *loaded* — `load_third_party` is a world-entry step
//! (1191 §2) — so `GetNumAddOns()` would answer 0. The screen asks
//! [`crate::ui_script::addons::installed`] instead: the same discovery, the same manifest parser,
//! the same enable file. Two views of one folder, never two folders.
use bevy::prelude::*;
use crate::glue::art::{GlueArt, DIM, GOLD};
use crate::glue::backdrop::{backdrop_border, tiled_bg_node};
use crate::glue::widgets::{glue_button, outlined_text, overlay, GlueBtnKind, GlueText, Hilight};
use crate::glue_strings::GlueStrings;
use crate::ui_script::addons::{self, InstalledAddOn};
use super::wow_font;
/// `MAX_ADDONS_DISPLAYED` (AddonList.lua l.2) — the reference's own row count.
const MAX_ROWS: usize = 19;
/// `ADDON_BUTTON_HEIGHT` (AddonList.lua l.1).
const ROW_H: f32 = 16.0;
/// Ours: the list is 19×16 = 304 tall, and the panel is that plus a title and a button row.
const PANEL_W: f32 = 560.0;
const PANEL_H: f32 = 420.0;
const LIST_TOP: f32 = 56.0;
const LIST_LEFT: f32 = 24.0;
const CHECK: f32 = 14.0;
/// A red that reads as "this is enabled and will not load" — `SetTextColor(1.0, 0.1, 0.1)`,
/// AddonList.lua's own value.
const BROKEN: Color = Color::srgb(1.0, 0.1, 0.1);
/// The screen's state. Opened by the select screen's AddOns button, driven by
/// [`drive_addons_panel`], cleared on leaving character select.
///
/// `staged` is the whole point of Okay/Cancel: edits go here, and only Okay writes the file. The
/// reference does the same with `SaveAddOns`/`ResetAddOns` over the client's in-memory records.
#[derive(Resource, Default)]
pub(super) struct AddonsPanel {
pub(super) open: bool,
/// The installed list as read at open — never re-read while up, so a row's index is stable.
list: Vec<InstalledAddOn>,
/// The edited enable state, parallel to `list`.
staged: Vec<bool>,
/// First visible row (`AddonList.offset`).
offset: usize,
/// The row the cursor is over, for the tooltip.
hovered: Option<usize>,
root: Option<Entity>,
spawned_s: f32,
/// Set while the spawned tree no longer matches `staged`/`offset`/`hovered` — one flag rather
/// than a diff, because a 19-row list is cheaper to respawn than to reconcile.
dirty: bool,
}
impl AddonsPanel {
/// Open for `identity`, reading the folder fresh. The reference's `AddonList_OnShow` also
/// re-reads (`AddonList_Update`), so a folder that changed under us is picked up.
pub(super) fn open_for(&mut self, identity: Option<&(String, String)>) {
self.list = addons::installed(identity);
self.staged = self.list.iter().map(|a| a.enabled).collect();
self.offset = 0;
self.hovered = None;
self.open = true;
self.dirty = true;
}
pub(super) fn close(&mut self) {
self.open = false;
self.list.clear();
self.staged.clear();
self.hovered = None;
self.dirty = true;
}
/// Are there any installed addons at all? The reference hides its button when not
/// (`UpdateAddonButton`), and so do we — a button that opens an empty list is worse than none.
///
/// **Answered once per process, not per call.** The screen asks this while it is spawning its
/// tree, and the tree respawns on every glue-scale change — which during a window drag-resize
/// is *every frame*. Reading the folder there means a `read_dir` plus a `.toc` parse per addon
/// per frame for as long as the director holds the mouse down. Installing or removing an addon
/// mid-session is not a thing that happens, so the cache never needs invalidating; the panel
/// itself re-reads the folder on every open ([`Self::open_for`]), which is where freshness
/// actually matters.
pub(super) fn any_installed() -> bool {
static ANY: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ANY.get_or_init(|| !addons::installed(None).is_empty())
}
/// The rows currently visible, as `(index, addon, enabled)`.
fn visible(&self) -> impl Iterator<Item = (usize, &InstalledAddOn, bool)> {
self.list
.iter()
.enumerate()
.skip(self.offset)
.take(MAX_ROWS)
.map(|(i, a)| (i, a, self.staged.get(i).copied().unwrap_or(a.enabled)))
}
/// The highest first-row index that still fills the list.
fn max_offset(&self) -> usize {
self.list.len().saturating_sub(MAX_ROWS)
}
/// **Why a row will not load, as the reference's `reason` token** — the string the status
/// column shows and the colour rule keys on.
///
/// `DEP_DISABLED`/`DEP_MISSING` come from the dependency walk; `DISABLED` from the checkbox;
/// `INCOMPATIBLE` from `## Interface`. Reported and never enforced for the last one (1191 §6),
/// which is why an out-of-date addon still shows gold-if-enabled: the status column says so
/// and the client still loads it, and those two facts must not contradict each other.
fn reason(&self, i: usize) -> Option<&'static str> {
let addon = self.list.get(i)?;
if !self.staged.get(i).copied().unwrap_or(addon.enabled) {
return Some("Disabled");
}
for dep in &addon.dependencies {
match self
.list
.iter()
.position(|a| a.name.eq_ignore_ascii_case(dep))
{
None => return Some("Dependency missing"),
Some(j) if !self.staged.get(j).copied().unwrap_or(true) => {
return Some("Dependency disabled")
}
Some(_) => {}
}
}
addon.out_of_date().then_some("Out of date")
}
}
/// The panel's clickable parts.
#[derive(Component, Clone, Copy, PartialEq, Eq)]
pub(super) enum AddonsAction {
/// A list row — toggles its checkbox (`AddonList_Enable`).
Row(usize),
EnableAll,
DisableAll,
Okay,
Cancel,
ScrollUp,
ScrollDown,
}
/// The panel root (despawned whole on close).
#[derive(Component)]
struct AddonsUi;
/// Spawn/despawn the panel, run its flows, and repaint when anything it shows has changed.
///
/// Ordered before the select screen's own click handling so a click that lands on the panel is
/// never also read as a click on the screen behind it.
#[allow(clippy::too_many_arguments)]
pub(super) fn drive_addons_panel(
mut commands: Commands,
mut panel: ResMut<AddonsPanel>,
art: Res<GlueArt>,
assets: Res<AssetServer>,
strings: Option<Res<GlueStrings>>,
identity: Res<crate::ui_script::AddOnIdentity>,
roster: Res<super::Roster>,
keys: Res<ButtonInput<KeyCode>>,
mut wheel: MessageReader<bevy::input::mouse::MouseWheel>,
presses: Query<(&AddonsAction, &Interaction), Changed<Interaction>>,
hovers: Query<(&AddonsAction, &Interaction)>,
window: Query<&Window, With<bevy::window::PrimaryWindow>>,
) {
if !panel.open {
if let Some(root) = panel.root.take() {
commands.entity(root).despawn();
}
wheel.clear();
return;
}
// ── the flows ─────────────────────────────────────────────────────────────────────────────
let mut close_and_save = false;
let mut close_and_discard = false;
for (action, interaction) in &presses {
if *interaction != Interaction::Pressed {
continue;
}
match *action {
AddonsAction::Row(i) => {
if let Some(slot) = panel.staged.get_mut(i) {
*slot = !*slot;
panel.dirty = true;
}
}
AddonsAction::EnableAll => {
panel.staged.iter_mut().for_each(|s| *s = true);
panel.dirty = true;
}
AddonsAction::DisableAll => {
panel.staged.iter_mut().for_each(|s| *s = false);
panel.dirty = true;
}
AddonsAction::Okay => close_and_save = true,
AddonsAction::Cancel => close_and_discard = true,
AddonsAction::ScrollUp => scroll(&mut panel, -1),
AddonsAction::ScrollDown => scroll(&mut panel, 1),
}
}
// `AddonList_OnKeyDown`: ESCAPE cancels, ENTER accepts.
if keys.just_pressed(KeyCode::Escape) {
close_and_discard = true;
}
if keys.just_pressed(KeyCode::Enter) || keys.just_pressed(KeyCode::NumpadEnter) {
close_and_save = true;
}
for ev in wheel.read() {
if ev.y != 0.0 {
scroll(&mut panel, if ev.y > 0.0 { -1 } else { 1 });
}
}
// The hovered row, for the tooltip — read every frame rather than on change, because a row
// that scrolls out from under a stationary cursor changes what is hovered without an event.
let hovered = hovers.iter().find_map(|(a, i)| match (a, i) {
(AddonsAction::Row(r), Interaction::Hovered | Interaction::Pressed) => Some(*r),
_ => None,
});
if panel.hovered != hovered {
panel.hovered = hovered;
panel.dirty = true;
}
if close_and_save {
let states: Vec<(String, bool)> = panel
.list
.iter()
.zip(panel.staged.iter())
.map(|(a, &on)| (a.name.clone(), on))
.collect();
// The identity the world-entry walk will read. `AddOnIdentity` is only filled once a
// character has entered the world, so at first launch the roster's own selection is the
// one that matters — the same resolution `load_ingame_ui_on_world_entry` performs.
let who = identity
.0
.clone()
.or_else(|| crate::ui_macro::identity(&roster));
addons::write_enable_state(who.as_ref(), &states);
panel.close();
return;
}
if close_and_discard {
panel.close();
return;
}
// ── the tree ──────────────────────────────────────────────────────────────────────────────
let s = crate::glue::screen_scale(window.single().ok());
let stale = panel.root.is_some() && (panel.spawned_s != s || panel.dirty);
if stale {
if let Some(root) = panel.root.take() {
commands.entity(root).despawn();
}
}
if panel.root.is_none() {
let empty = GlueStrings::default();
let strings = strings.as_deref().unwrap_or(&empty);
panel.root = Some(spawn_panel(
&mut commands,
&art,
&assets,
strings,
&panel,
s,
));
panel.spawned_s = s;
panel.dirty = false;
}
}
/// Move the first visible row by `delta`, clamped — the reference's `AddonList.offset`.
fn scroll(panel: &mut AddonsPanel, delta: i32) {
let max = panel.max_offset();
let next = (panel.offset as i32 + delta).clamp(0, max as i32) as usize;
if next != panel.offset {
panel.offset = next;
panel.dirty = true;
}
}
fn spawn_panel(
commands: &mut Commands,
art: &GlueArt,
assets: &AssetServer,
strings: &GlueStrings,
panel: &AddonsPanel,
s: f32,
) -> Entity {
let px = |v: f32| Val::Px(v * s);
let font = wow_font(assets);
let title = strings.text("ADDONS", "AddOns");
commands
.spawn((
AddonsUi,
GlobalZIndex(1200), // over the select screen's 1100, like the delete dialog
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
))
.with_children(|overlay_ui| {
let mut boxed = overlay_ui.spawn(Node {
width: px(PANEL_W),
height: px(PANEL_H),
..default()
});
boxed.with_children(|b| {
// The dialog backdrop, exactly as the delete dialog builds it.
if let (Some(bg), Some(border)) = (&art.dialog_bg, &art.dialog_border) {
b.spawn((
tiled_bg_node(bg.clone(), 32.0, s, Color::WHITE),
Node {
position_type: PositionType::Absolute,
left: px(11.0),
right: px(12.0),
top: px(12.0),
bottom: px(11.0),
..default()
},
));
backdrop_border(b, border, 32.0, Color::WHITE);
} else {
b.spawn((
BackgroundColor(Color::srgba(0.05, 0.05, 0.08, 0.95)),
overlay(),
));
}
// Title.
outlined_text(
b,
Node {
position_type: PositionType::Absolute,
left: px(0.0),
right: px(0.0),
top: px(22.0),
justify_content: JustifyContent::Center,
..default()
},
(),
(),
GlueText {
text: title,
size: 18.0,
color: GOLD,
wrap: false,
},
&font,
s,
);
// The rows.
for (slot, (index, addon, enabled)) in panel.visible().enumerate() {
let top = LIST_TOP + slot as f32 * ROW_H;
let reason = panel.reason(index);
// AddonList.lua's three-colour rule: gold when it will load, red when the
// player has it ON and it still will not, grey when they turned it off.
let colour = match (enabled, reason) {
(true, None) => GOLD,
(true, Some("Out of date")) => GOLD,
(true, _) => BROKEN,
(false, _) => DIM,
};
let mut row = b.spawn((
AddonsAction::Row(index),
Button,
Node {
position_type: PositionType::Absolute,
left: px(LIST_LEFT),
top: px(top),
width: px(PANEL_W - LIST_LEFT * 2.0),
height: px(ROW_H),
align_items: AlignItems::Center,
..default()
},
));
row.with_children(|r| {
// The checkbox. The glue art's own `UI-CheckBox-*` when we have it, a
// drawn box when we do not — the same fallback every glue control takes.
let check = &art.checkbox;
let face = match (check.as_ref(), enabled) {
(Some(c), true) => Some(c.checked.clone()),
(Some(c), false) => Some(c.up.clone()),
(None, _) => None,
};
let mut cb = r.spawn(Node {
position_type: PositionType::Absolute,
left: px(0.0),
top: px((ROW_H - CHECK) / 2.0),
width: px(CHECK),
height: px(CHECK),
..default()
});
match face {
Some(image) => {
cb.insert(ImageNode::new(image));
}
None => {
cb.insert(BackgroundColor(if enabled { GOLD } else { DIM }));
}
}
// Title.
outlined_text(
r,
Node {
position_type: PositionType::Absolute,
left: px(CHECK + 8.0),
top: px(0.0),
..default()
},
(),
(),
GlueText {
text: addon.display_title(),
size: 12.0,
color: colour,
wrap: false,
},
&font,
s,
);
// Status — the reference's `getglobal("ADDON_"..reason)` column.
if let Some(reason) = reason {
outlined_text(
r,
Node {
position_type: PositionType::Absolute,
right: px(0.0),
top: px(0.0),
..default()
},
(),
(),
GlueText {
text: reason,
size: 11.0,
color: DIM,
wrap: false,
},
&font,
s,
);
}
if let Some(hi) = &art.hilight {
r.spawn((
Hilight,
Visibility::Hidden,
MaterialNode(hi.clone()),
overlay(),
));
}
});
}
// The scroll pair, shown only when the list is longer than the window — the
// reference's `GlueScrollFrame_Update` hides its bar on the same condition.
if panel.max_offset() > 0 {
for (action, caption, top) in [
(AddonsAction::ScrollUp, "-", LIST_TOP),
(
AddonsAction::ScrollDown,
"+",
LIST_TOP + (MAX_ROWS as f32 - 1.0) * ROW_H,
),
] {
b.spawn((
action,
Button,
BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.10)),
Node {
position_type: PositionType::Absolute,
right: px(6.0),
top: px(top),
width: px(14.0),
height: px(14.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
))
.with_children(|sb| {
outlined_text(
sb,
Node::default(),
(),
(),
GlueText {
text: caption,
size: 11.0,
color: GOLD,
wrap: false,
},
&font,
s,
);
});
}
}
// The hover tooltip: title, notes, dependencies — `AddonTooltip_Update`'s three
// lines, in its order.
if let Some(i) = panel.hovered.and_then(|i| panel.list.get(i)) {
let deps = if i.dependencies.is_empty() {
String::new()
} else {
format!("Dependencies: {}", i.dependencies.join(", "))
};
let body = [
i.notes.clone().unwrap_or_default(),
deps,
i.url.clone().unwrap_or_default(),
]
.into_iter()
.filter(|l| !l.is_empty())
.collect::<Vec<_>>()
.join("\n");
if !body.is_empty() {
outlined_text(
b,
Node {
position_type: PositionType::Absolute,
left: px(LIST_LEFT),
right: px(LIST_LEFT),
bottom: px(58.0),
..default()
},
(),
(),
GlueText {
text: &body,
size: 11.0,
color: Color::WHITE,
wrap: true,
},
&font,
s,
);
}
}
// The button row.
b.spawn(Node {
position_type: PositionType::Absolute,
left: px(24.0),
right: px(24.0),
bottom: px(20.0),
height: px(28.0),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
..default()
})
.with_children(|row| {
for (action, caption) in [
(AddonsAction::EnableAll, "Enable All"),
(AddonsAction::DisableAll, "Disable All"),
(AddonsAction::Cancel, "Cancel"),
(AddonsAction::Okay, "Okay"),
] {
glue_button(
row,
art,
&font,
action,
caption,
118.0,
28.0,
GlueBtnKind::Small,
s,
);
}
});
});
})
.id()
}
#[cfg(test)]
mod tests {
use super::*;
fn addon(name: &str, deps: &[&str], iface: &[u32]) -> InstalledAddOn {
InstalledAddOn {
name: name.into(),
title: None,
notes: None,
url: None,
dependencies: deps.iter().map(|d| (*d).to_string()).collect(),
interface: iface.to_vec(),
load_on_demand: false,
enabled: true,
}
}
fn panel(list: Vec<InstalledAddOn>) -> AddonsPanel {
let staged = list.iter().map(|a| a.enabled).collect();
AddonsPanel {
open: true,
list,
staged,
..Default::default()
}
}
/// The status column's rule, which is also the title-colour rule — the reference's own
/// `reason` token set, in its own precedence.
///
/// The ordering is the part worth pinning: a row the player turned OFF reads `Disabled` and
/// nothing else, even when its dependency is also missing. The reference does the same
/// (`GetAddOnInfo` reports `DISABLED` before the dependency walk runs), and the alternative
/// tells a player to go fix a dependency for an addon they deliberately switched off.
#[test]
fn the_status_column_reports_the_references_reason_in_its_own_precedence() {
let mut p = panel(vec![
addon("Solo", &[], &[11200]),
addon("Lib", &[], &[11200]),
addon("Needs", &["Lib"], &[11200]),
addon("Orphan", &["Nowhere"], &[11200]),
addon("Old", &[], &[11100]),
addon("Silent", &[], &[]),
]);
assert_eq!(p.reason(0), None, "nothing wrong: no status, gold title");
assert_eq!(p.reason(2), None, "its dependency is installed and enabled");
assert_eq!(p.reason(3), Some("Dependency missing"));
assert_eq!(
p.reason(4),
Some("Out of date"),
"reported — 1191 §6 does not ENFORCE it, and the two must not contradict"
);
assert_eq!(
p.reason(5),
None,
"a manifest with no `## Interface` is silent, not out of date"
);
// Turning the library off makes its dependent unloadable, and the dependent says so.
p.staged[1] = false;
assert_eq!(p.reason(1), Some("Disabled"));
assert_eq!(p.reason(2), Some("Dependency disabled"));
// ...and a row the player turned off reports only that, even with a broken dependency.
p.staged[3] = false;
assert_eq!(p.reason(3), Some("Disabled"));
}
/// Okay/Cancel is a **staged** edit: nothing the panel does touches the enable file until
/// Okay. `Cancel` is the reference's `ResetAddOns`, and `close` is what implements it.
#[test]
fn edits_are_staged_and_cancel_discards_them() {
let mut p = panel(vec![addon("A", &[], &[11200]), addon("B", &[], &[11200])]);
assert_eq!(p.staged, vec![true, true]);
p.staged[0] = false;
assert!(
p.list[0].enabled,
"the read-in list is never mutated — only `staged` is, which is what makes Cancel free"
);
p.close();
assert!(!p.open);
assert!(
p.staged.is_empty(),
"a cancelled edit leaves nothing behind"
);
}
/// Scrolling clamps at both ends and never moves a list that fits.
#[test]
fn the_offset_clamps_to_the_list() {
let mut short = panel((0..5).map(|i| addon(&format!("A{i}"), &[], &[])).collect());
assert_eq!(short.max_offset(), 0);
scroll(&mut short, 1);
assert_eq!(short.offset, 0, "a list that fits does not scroll");
let mut long = panel(
(0..MAX_ROWS + 4)
.map(|i| addon(&format!("A{i}"), &[], &[]))
.collect(),
);
assert_eq!(long.max_offset(), 4);
scroll(&mut long, 10);
assert_eq!(long.offset, 4, "clamped at the bottom, not past it");
assert_eq!(long.visible().count(), MAX_ROWS);
scroll(&mut long, -100);
assert_eq!(long.offset, 0);
}
}
+9 -1
View File
@@ -34,13 +34,15 @@ pub(super) fn select_input(
mut roster: ResMut<Roster>,
pick: Res<CharPick>,
mut dialog: ResMut<DeleteDialog>,
mut panel: ResMut<super::addons::AddonsPanel>,
mut next: ResMut<NextState<ClientState>>,
mut sounds: MessageWriter<GlueSound>,
mut intent: ResMut<crate::login::LoginIntent>,
time: Res<Time>,
mut last_click: Local<Option<(usize, f32)>>,
) {
if dialog.open {
// A modal owns the input while it is up — the delete confirm, or the AddOns list.
if dialog.open || panel.open {
return;
}
let now = time.elapsed_secs();
@@ -76,6 +78,12 @@ pub(super) fn select_input(
sounds.write(GlueSound("gsCharacterSelectionCreateNew"));
next.set(ClientState::CharCreate);
}
SelectAction::Addons => {
// The reference plays `gsCharacterSelectionOpen`-family click here; ours reuses
// the create-screen open sound rather than inventing a name the client lacks.
sounds.write(GlueSound("gsCharacterSelectionCreateNew"));
panel.open_for(crate::ui_macro::identity(&roster).as_ref());
}
SelectAction::Back => back_to_login = true,
_ => {}
}
@@ -20,6 +20,7 @@
//! [`screen`] (the authored layout), [`refresh`] (list/banner/booth-feed refresh), [`input`]
//! (clicks, keys, rotation, the flows), [`dialog`] (the delete confirm).
mod addons;
mod dialog;
mod input;
mod refresh;
@@ -93,6 +94,7 @@ impl Plugin for CharSelectPlugin {
publish_world_live.before(benilla_world::schedule::WorldStage::Net),
)
.init_resource::<dialog::DeleteDialog>()
.init_resource::<addons::AddonsPanel>()
.add_systems(OnEnter(ClientState::CharSelect), screen::enter_select)
.add_systems(OnExit(ClientState::CharSelect), screen::exit_select)
.add_systems(Update, (debug_glue_roundtrip, debug_logout_smoke))
@@ -108,6 +110,9 @@ impl Plugin for CharSelectPlugin {
input::rotate_model,
debug_select_dialog,
dialog::drive_delete_dialog,
// Before the list refresh, and before `select_input` reads a click that
// landed on the panel rather than the screen (decision 1196).
addons::drive_addons_panel,
refresh::refresh_list,
refresh::refresh_banner_and_buttons,
refresh::feed_glue_preview,
@@ -43,6 +43,8 @@ pub(super) enum SelectAction {
CreateChar,
/// Rendered disabled — realm choice is out of scope (decision 0465 §6).
ChangeRealm,
/// Open the AddOns list (decision 1197) — the reference's `CharacterSelectAddonsButton`.
Addons,
RotateLeft,
RotateRight,
}
@@ -254,6 +256,34 @@ fn spawn_screen(
rotate_cluster(ui, art, &font, s);
// **AddOns** (decision 1197) — the reference's `CharacterSelectAddonsButton`, which 1191
// §5 recorded as never built because there was no list behind it. There is now. Bottom
// LEFT, out of the way of the Enter World / Back cluster; the reference sits it in the
// same lower band. Shown only when something is installed, exactly as the reference's
// `UpdateAddonButton` hides it on `GetNumAddOns() == 0`.
if super::addons::AddonsPanel::any_installed() {
ui.spawn((Node {
position_type: PositionType::Absolute,
left: px(30.0),
bottom: px(25.0),
flex_direction: FlexDirection::Row,
..default()
},))
.with_children(|actions| {
glue_button(
actions,
art,
&font,
SelectAction::Addons,
strings.text("ADDONS", "AddOns"),
140.0,
35.0,
GlueBtnKind::Small,
s,
);
});
}
// Back (100×35 at BOTTOMRIGHT (−30,25)) and Delete Character (165×35 at its LEFT).
ui.spawn((Node {
position_type: PositionType::Absolute,
+18
View File
@@ -198,6 +198,24 @@ impl Items {
(!left.is_zero()).then_some(left.as_millis() as u64)
}
/// The same deadline read **without** the tooltip's expired-is-absent collapse: `Some(0)` for a
/// timer that has run out, `None` only when the slot never had one.
///
/// `GetWeaponEnchantInfo` needs the two apart where the tooltip does not. Its expiration return
/// is `max(0, deadline − now)` from the client-local deadline array (`0x5d9d00`, subtract on
/// read — VERIFIED wow-re `system/ui/scratch/weapon-enchant-info.md`), so an enchant whose
/// timer has elapsed answers the NUMBER 0, and `BuffFrame_Enchant_OnUpdate` then draws "0 s"
/// and pulses the icon. Collapsing that to nil would silently hide a expiring enchant's last
/// state. The wire carries **seconds** and this returns **milliseconds**, which is the
/// conversion `set_enchant_deadline` already performs.
pub(crate) fn enchant_deadline_ms(&self, guid: u64, slot: u32) -> Option<u64> {
let at = self.enchant_deadlines.get(&(guid, slot))?;
Some(
at.saturating_duration_since(std::time::Instant::now())
.as_millis() as u64,
)
}
/// A tracked item object's merged descriptor fields.
pub(crate) fn object(&self, guid: u64) -> Option<&ObjectFields> {
self.objects.get(&guid)
+1
View File
@@ -31,6 +31,7 @@
// spent its whole diff clearing it out of. Dev builds still warn normally.
#![cfg_attr(not(feature = "dev"), allow(dead_code))]
pub mod addon_harness;
mod area;
mod area_trigger;
#[cfg(feature = "dev")]
+33
View File
@@ -157,6 +157,39 @@ pub(crate) fn saved_variables_path() -> Option<PathBuf> {
home().map(|h| h.join("saved-variables.lua"))
}
/// `benilla-config/addons/<Realm>-<Character>.txt` — the AddOn **enable state** (1188 phase 2).
///
/// Per character, like the reference: it writes `WTF/Account/<ACC>/<Realm>/<Char>/AddOns.txt` as
/// the last step of its UI shutdown (`0x490bd0`'s tail, after the saved-variables files). The
/// file's own format is the reference's too — one `<AddOnName>: enabled|disabled` per line,
/// confirmed against a real 1.12 install rather than remembered. An addon absent from the file is
/// enabled, which is what makes a freshly-dropped-in folder just work.
pub(crate) fn addons_state_path(realm: &str, character: &str) -> Option<PathBuf> {
let key = format!("{}-{}", file_token(realm), file_token(character));
home().map(|h| h.join("addons").join(format!("{key}.txt")))
}
/// `benilla-config/saved/` — **per-addon** saved variables, account scope (1188 phase 3): one
/// `<Addon>.lua` per addon that declares `## SavedVariables`.
///
/// The reference's shape, one level flatter: it writes
/// `WTF/Account/<ACC>/SavedVariables/<Addon>.lua`, and our whole state folder is already
/// per-install, so the folder IS the account scope (0954, and 1128 reserved this exact path —
/// spelled `benilla/saved/` before 1180 renamed the folder).
///
/// Distinct from [`saved_variables_path`], which is the *flat* channel our own FrameXML uses
/// through `RegisterForSave`. The reference has both too, and they are not the same mechanism.
pub(crate) fn addon_saved_account_dir() -> Option<PathBuf> {
home().map(|h| h.join("saved"))
}
/// `benilla-config/saved/<Realm>-<Character>/` — per-addon saved variables, character scope
/// (`## SavedVariablesPerCharacter`). Loaded **after** the account file, so it wins.
pub(crate) fn addon_saved_character_dir(realm: &str, character: &str) -> Option<PathBuf> {
let key = format!("{}-{}", file_token(realm), file_token(character));
home().map(|h| h.join("saved").join(key))
}
/// `benilla-config/camera/<realm>-<character>.txt` — the third-person camera pose (decision 1131; the
/// `<Char>/camera-settings.txt` analog, and character-scoped for the same reason it is there: a
/// gnome rogue and a tauren warrior want different zooms). Two lines, the reference's own keys and
+14
View File
@@ -106,6 +106,20 @@ pub(crate) use swim::swim_enter_depth;
/// [`crate::creature_anim::MovementState::stunned`] (decision 0880).
pub(crate) const UNIT_FLAG_STUNNED: u32 = 0x0004_0000;
/// `UNIT_FLAG_IN_COMBAT` — the same `UNIT_FIELD_FLAGS` word, **bit 19** (vmangos
/// `UnitDefines.h`; the client reads it as `shr reg,0x13; test rl,1`).
///
/// Its readers are deliberately unrelated to each other — the spell-usability walk's leg 8, the
/// probe preflight banner, and `UnitAffectingCombat`'s snapshot field — which is exactly why the
/// constant lives here beside its neighbour rather than being declared a fourth time: three
/// private copies of one bit is how a mask silently drifts.
///
/// **There is no player-specific combat latch to pair it with.** wow-re censused the
/// `shr reg,0x13` + `test rl,1` idiom image-wide (7 hits, 6 on this field+bit) and found the two
/// hardcoded *local-player* readers going through this same flag; `UnitAffectingCombat("player")`
/// takes a GUID fast path and lands on the identical word.
pub(crate) const UNIT_FLAG_IN_COMBAT: u32 = 0x0008_0000;
/// Ask for a **stand state** — the client's `SetStandState(newState)` (`0x5ed430`: send
/// `CMSG_STANDSTATECHANGE` + apply locally through `0x6127b0`), as a message so every path that
/// wants a posture funnels through the ONE setter in [`control`] (the [`crate::creature_anim::
+7
View File
@@ -29,6 +29,13 @@ use benilla_world::view::CAM_NEAR;
/// || (elapsed < 800ms && yaw_travel < 2.25° && pitch_travel < 2.0°)
/// ```
///
/// **The `ms` on those two numbers is now VERIFIED, where 1122 could only infer it** (wow-re
/// `ui/scratch/button-doubleclick-law.md`, 2026-08-11, a side effect of the `OnDoubleClick` §5):
/// the clock both constants are compared against is `0x42c010` → `0x42b790`, whose counter is the
/// `KERNEL32!GetTickCount` import at `[0x7ff310]` and whose scale is stored as `1.0/freq × 1000.0`
/// — milliseconds in either counter mode. Nothing here changes; the units simply stopped being a
/// guess.
///
/// **A press under 200 world selects however far the mouse swept.** That arm is the entire bug report
/// (ledger B226): flick the cursor across a mob and click on arrival with the hand still moving, and
/// the reference targets it — the camera has been orbiting since the first motion sample and selects
+4 -3
View File
@@ -74,9 +74,10 @@ const MOVE_BLOCKERS: &[(u32, &str)] = &[
(0x0100_0000, "POSSESSED (another unit holds the reins)"),
];
/// `UNIT_FLAG_IN_COMBAT` — worth naming on entry: an unattended probe that logs in already fighting
/// is the exact shape of the accident the unattended-combat ban exists for (method.md).
const UNIT_FLAG_IN_COMBAT: u32 = 0x0008_0000;
/// Worth naming on entry: an unattended probe that logs in already fighting is the exact shape of
/// the accident the unattended-combat ban exists for (method.md). The bit itself is declared once
/// ([`crate::player`]).
use crate::player::UNIT_FLAG_IN_COMBAT;
/// `UNIT_FLAG_SILENCED` — casts silently refuse.
const UNIT_FLAG_SILENCED: u32 = 0x0000_2000;
+42
View File
@@ -420,6 +420,48 @@ pub(super) fn target_by_name_requests(
}
}
/// Drain the **Lua** `TargetByName(name, exactMatch)` asks — the binding half of the same
/// resolver [`target_by_name_requests`] runs for `/target`.
///
/// A separate system rather than a second writer of [`TargetByNameRequest`] because the binding
/// carries a second argument the slash command has no way to supply: `0x489d8e` fetches Lua arg #2
/// with default 0 and hands it to `0x493aa0` as the **exact-only** flag (`ctx+0x0c`, consumed at
/// `0x493cab`). Shipped FrameXML never passes it — its own usage string documents one argument —
/// so a stock `/target` and a stock addon call both run with prefix matching live; an addon that
/// already knows the exact name can turn tier 2 off, exactly as `UnitPopup.lua` does for
/// `FollowByName(name, 1)`.
///
/// Everything else is identical to the slash path, deliberately: typemask 8 (creatures *and*
/// players), filter mode 0 (accept unconditionally — no range, cone, dead, reaction or
/// self-exclusion test), commit through [`SelectCommit`], and a miss that leaves the current
/// target untouched and says nothing (the reference's two game-message ids are `0x127`/`0xb8`,
/// whose strings are runtime-populated BSS and are not statically recoverable — the known
/// deviation this path already carries).
pub(super) fn script_target_by_name_requests(
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
scan_params: ByNameScan,
mut commit: SelectCommit,
) {
let Some(mut script) = script else {
return;
};
for (name, exact) in script.take_target_by_name_requests() {
let Some((entity, guid, _)) = scan_params.resolve(
&name,
NameSearch::AnyUnit,
Filter::AcceptAll,
if exact {
Match::ExactOnly
} else {
Match::PrefixOk
},
) else {
continue;
};
commit.commit(entity, guid);
}
}
/// Drain `/assist [name]`: find the **basis** unit, read its `UNIT_FIELD_TARGET` (+0x28 off the unit
/// block — wow-re proved the offset arithmetic against HEALTH and DYNAMIC_FLAGS on the same line),
/// and select whatever it is pointing at.
+4
View File
@@ -335,6 +335,10 @@ impl Plugin for TargetPlugin {
// touching `Selection` at all.
(
by_name::target_by_name_requests,
// The Lua binding's own by-name asks (11 corpus addons call
// `TargetByName`), beside the chat layer's — same resolver, same commit,
// one extra argument the slash command cannot supply.
by_name::script_target_by_name_requests,
by_name::assist_requests,
by_name::follow_requests,
)
+2 -2
View File
@@ -32,8 +32,8 @@ use crate::target::{can_attack, ring_reaction, Factions};
use super::Spells;
/// `UNIT_FLAG_IN_COMBAT` (vmangos `UnitDefines.h`, bit 19) — leg 8's caster unit-flag test.
const UNIT_FLAG_IN_COMBAT: u32 = 0x0008_0000;
/// Leg 8's caster unit-flag test — the shared bit, declared once ([`crate::player`]).
use crate::player::UNIT_FLAG_IN_COMBAT;
/// The implicit-target enums leg 10b forks on (`0x6e3f8a`/`0x6e3fa2`): 6 = single enemy →
/// `CanAttack 0x606980`, 21 = single friend → `CanAssist 0x6066f0`.
+144 -2
View File
@@ -333,6 +333,13 @@ fn other_unit_auras(
expiration_time: 0.0,
helpful: a.slot < UNIT_AURA_POSITIVE_SLOTS,
cancelable: a.flags & AURA_FLAG_CANCELABLE != 0,
// `untilCancelled` is a field of the PLAYER cache record (`0xbc6040`+0xc) and
// `GetPlayerBuff` is the only reader; no other unit has a cache, and no binding can
// reach this flag through a non-player token. A self-target does not come through
// here at all — it mirrors the player list (decision 0257 §2).
until_cancelled: false,
channeled: display
.is_some_and(|d| d.attributes_ex & SPELL_ATTR_EX_IS_CHANNELED != 0),
}
})
.collect()
@@ -382,6 +389,59 @@ fn buffs_visible_on(
/// `UNIT_FLAG_AURAS_VISIBLE` — see [`buffs_visible_on`].
const UNIT_FLAG_AURAS_VISIBLE: u32 = 0x0800_0000;
/// `Spell.dbc` `AttributesEx & 0x4` — `SPELL_ATTR_EX_IS_CHANNELED` (vmangos `SpellDefines.h`), the
/// DBC arm of the cancel gate (`benilla_ui::script::aura`'s `cancel_authorized`, `0x4e4a10`).
const SPELL_ATTR_EX_IS_CHANNELED: u32 = 0x4;
/// The cache record's **`untilCancelled`** flag — `GetPlayerBuff`'s second return, `+0xc` of a
/// `0xbc6040` record. "This aura has no finite duration to display."
///
/// Byte-verified against `BuildBuffRecord 0x4e44b0`'s tail (`0x4e452e`-`0x4e45c5`), which is two
/// clauses ORed in sequence:
///
/// 1. **The DBC duration row.** `SpellRec+0x78` (`DurationIndex`) resolves a `SpellDuration.dbc` row;
/// the flag is set when the row says *infinite* — `Duration < 0 && DurationPerLevel <= 0`
/// (`0x4e456e`-`0x4e457a`) — or when the index resolves to no row at all (`jl`/`jg`/NULL all fall
/// into the same `mov ecx,1` at `0x4e4580`).
/// 2. **The area-aura override.** Only for a *non*-cancelable aura (`test [edi+0xa],0x1; jne` at
/// `0x4e4588` returns early otherwise): if any of the spell's three effects is one of
/// `{0x23, 0x77, 0x80, 0x81}` — the persistent/area-aura effects, which have no per-application
/// timer because they last while you stand in them — the flag is **forced** to 1
/// (`0x4e458e`-`0x4e45be`).
///
/// This closes the gap this module's header named ("the reference avoids this via a DBC 'until
/// cancelled' flag we don't parse"). It matters because it is answerable from the *spell*, on the
/// frame the aura appears: `ref-BuffFrame.lua:124` returns before ever calling
/// `GetPlayerBuffTimeLeft` when it reads 1, so a permanent aura never flashes a `0 s` countdown
/// while waiting for a duration packet that is never coming.
///
/// **The one inference.** When the catalog cannot resolve the spell at all, the reference leaves
/// `+0xc` holding whatever the record's previous occupant left there (its early-outs at `0x4e4533`/
/// `0x4e4550` skip the write) — stale memory, not a mechanism. We answer from the only evidence we
/// still have: no expiry stamp joined ⇒ nothing finite to display. A catalog miss already costs the
/// aura its icon and name, so this only decides whether a nameless icon also shows `0 s`.
fn until_cancelled(
display: Option<&benilla_formats::SpellDisplay>,
duration_row: Option<&benilla_formats::SpellDuration>,
cancelable: bool,
expiration_time: f64,
) -> bool {
let Some(d) = display else {
return expiration_time == 0.0;
};
let permanent = match duration_row {
Some(row) => row.base_ms < 0 && row.per_level_ms <= 0,
// No row — a `DurationIndex` that is negative, out of range, or resolves to nothing. The
// reference funnels all three into the same `mov ecx,1` at `0x4e4580`.
None => true,
};
permanent
|| (!cancelable
&& d.effects
.iter()
.any(|e| matches!(e, 0x23 | 0x77 | 0x80 | 0x81)))
}
/// The reference's aura display filter (decisions 0268 + 0417): an aura is shown iff its spell is
/// *not* flagged never-display (`SPELL_ATTR_DO_NOT_DISPLAY` / `SPELL_ATTR_EX_NO_AURA_ICON`, via
/// `SpellDisplay::hidden_from_aura_bar`) **and** is not a tracking spell
@@ -480,6 +540,9 @@ fn feed_auras(
let bevy_now = time.elapsed_secs_f64();
let catalog = spells.as_ref().map(|s| &s.catalog);
// `SpellDuration.dbc`, for the cache record's `untilCancelled` flag ([`until_cancelled`]) — the
// same catalog the `$d` tooltip token resolves against, so nothing new is loaded for this.
let spell_durations = spells.as_ref().map(|s| &s.durations);
let occupied: Vec<UnitAuraSlot> = store.0.unit_auras().collect();
// The reference's DISPLAY FILTER (byte-verified sites `0x4e42b6`–`0x4e42c8`; decisions
// 0268 + 0385, and 0417 for the target frame): a slot whose spell carries
@@ -531,6 +594,14 @@ fn feed_auras(
expiration_time,
helpful: c.slot < UNIT_AURA_POSITIVE_SLOTS,
cancelable: c.flags & AURA_FLAG_CANCELABLE != 0,
until_cancelled: until_cancelled(
display,
display.and_then(|d| spell_durations.and_then(|c| c.get(d.duration_index))),
c.flags & AURA_FLAG_CANCELABLE != 0,
expiration_time,
),
channeled: display
.is_some_and(|d| d.attributes_ex & SPELL_ATTR_EX_IS_CHANNELED != 0),
}
})
.collect();
@@ -678,8 +749,9 @@ fn feed_auras(
if changed {
script.fire_event("UNIT_AURA", vec![ScriptValue::Str("player".into())]);
// The reference's own event for the same rebuild — PLAYER_AURAS_CHANGED, no args — which
// the verbatim-transcribed 1.12 frames register (MiniMapTrackingFrame). Fired beside the
// Era-shaped UNIT_AURA the adapted BuffFrame listens on: one rebuild, both dialects.
// the verbatim-transcribed 1.12 frames register: MiniMapTrackingFrame, the buff bar since
// it came onto the `GetPlayerBuff` family, and every corpus aura addon. Fired beside the
// Era-shaped UNIT_AURA the unit frames listen on: one rebuild, both dialects.
script.fire_event("PLAYER_AURAS_CHANGED", vec![]);
}
if pet_changed {
@@ -1166,4 +1238,74 @@ mod tests {
"activePlayer.CHARMEDBY != 0 → no buffs on anyone"
);
}
/// [`until_cancelled`] — `GetPlayerBuff`'s second return, against the two clauses the reference
/// derives it from (`0x4e452e`-`0x4e45c5`). Every case here is one the corpus reads as a
/// **number** compared to `1` (`CT_BuffMod/CT_BuffFrame.lua:175,232` and three more), so a wrong
/// answer shows or hides a countdown rather than erroring.
#[test]
fn until_cancelled_follows_the_duration_row_and_the_area_aura_override() {
use benilla_formats::{SpellDisplay, SpellDuration};
let row = |base_ms: i32, per_level_ms: i32| SpellDuration {
base_ms,
per_level_ms,
max_ms: base_ms,
};
// The shapes the shipped file actually holds (asserted on the real DBC in
// `benilla_formats::spells::duration`'s tests): row 21 `{-1, 0}` = permanent, row 30
// `{1_800_000, 0}` = Frost Armor's 30 minutes, row 427 `{-600_000, 60_000}` = the one that
// makes this a TWO-field test, since a negative base alone is not permanence.
let permanent_row = row(-1, 0);
let timed_row = row(1_800_000, 0);
let scaling_row = row(-600_000, 60_000);
let spell = |effects: [u32; 3]| SpellDisplay {
effects,
..SpellDisplay::default()
};
let ordinary = spell([6, 0, 0]);
// 0x23 = the persistent-area-aura effect, one of the four in the override set.
let area = spell([0x23, 0, 0]);
// Clause 1 — the DBC duration row decides, cancelable or not.
assert!(!until_cancelled(
Some(&ordinary),
Some(&timed_row),
true,
0.0
));
assert!(!until_cancelled(
Some(&ordinary),
Some(&timed_row),
false,
0.0
));
assert!(until_cancelled(
Some(&ordinary),
Some(&permanent_row),
true,
0.0
));
assert!(
!until_cancelled(Some(&ordinary), Some(&scaling_row), true, 0.0),
"a negative base with a positive per-level term is a SCALING duration, not permanence"
);
// No row at all is the reference's `mov ecx,1` branch — permanent.
assert!(until_cancelled(Some(&ordinary), None, true, 0.0));
// Clause 2 — the area-aura override applies ONLY to a non-cancelable aura (`test
// [edi+0xa],0x1; jne` returns early at 0x4e4588). This asymmetry is the whole clause: the
// same spell answers differently depending on the wire's cancelable nibble.
assert!(until_cancelled(Some(&area), Some(&timed_row), false, 0.0));
assert!(
!until_cancelled(Some(&area), Some(&timed_row), true, 0.0),
"a CANCELABLE aura skips the effect scan entirely"
);
// The stated inference: the catalog cannot resolve the spell, so answer from the joined
// expiry — the only evidence left.
assert!(until_cancelled(None, None, false, 0.0));
assert!(!until_cancelled(None, None, false, 1234.0));
}
}
+45
View File
@@ -838,4 +838,49 @@ fn feed_char(
);
feed.last_inv = Some(inv);
}
// `GetWeaponEnchantInfo`'s whole data path (the buff bar's TemporaryEnchantFrame row, plus 8
// corpus addons). Pushed EVERY frame and change-gated by nothing: the remaining time is a live
// countdown, so riding the snapshot above would fire `UNIT_INVENTORY_CHANGED` on every tick.
// The reference recomputes it per call for exactly the same reason (`0x5d9d00`).
script.set_weapon_enchants(
weapon_enchant(store, &items, EQUIPMENT_SLOT_MAINHAND),
weapon_enchant(store, &items, EQUIPMENT_SLOT_OFFHAND),
);
}
/// The client's own 0-based `EQUIPMENT_SLOT_*` ids for the two weapon slots — the numbers
/// `GetWeaponEnchantInfo 0x4c9790` hands its container lookup (`push 0xf` at `0x4c97c3`,
/// `push 0x10` at `0x4c988b`). The live-API `GetInventorySlotInfo` ids the *frames* use are one
/// higher (16 `MainHandSlot`, 17 `SecondaryHandSlot`).
const EQUIPMENT_SLOT_MAINHAND: u8 = 15;
const EQUIPMENT_SLOT_OFFHAND: u8 = 16;
/// `ITEM_FIELD_ENCHANTMENT`'s temporary slot (0 = permanent, 1 = temporary, 2..6 random property)
/// — the slot index the reference passes to its remaining-time reader (`push 0x1`, `0x4c9828`).
const TEMP_ENCHANTMENT_SLOT: u8 = 1;
/// One weapon's temporary enchantment for [`benilla_ui::script::WeaponEnchant`], read off the
/// equipped item object.
///
/// **The RAW enchantment triple, not [`InvSlotView::enchants`]**: that view is tooltip-shaped and
/// drops both an id the `SpellItemEnchantment` catalog cannot name and the whole `Flags & 0x2`
/// print-no-line family (decision 0928 — the totem weapon imbues), which is exactly the set the
/// enchant row exists to show. The binary's gate is `[descriptor+0x4c] != 0`, the id and nothing
/// else, and `item_enchant` already answers `None` for a zero id.
fn weapon_enchant(
store: &ObjectStore,
items: &Items,
slot0: u8,
) -> Option<benilla_ui::script::WeaponEnchant> {
let guid = store.0.player_inv_slot(slot0)?;
// Before the object borrow — the deadlines and the objects both live on `Items`. The
// *deadline* read, not the tooltip's: an elapsed timer must answer 0, not "no timer".
let remaining_ms = items.enchant_deadline_ms(guid, u32::from(TEMP_ENCHANTMENT_SLOT));
let obj = items.object(guid)?;
obj.item_enchant(TEMP_ENCHANTMENT_SLOT)?;
Some(benilla_ui::script::WeaponEnchant {
remaining_ms,
charges: obj.item_enchant_charges(TEMP_ENCHANTMENT_SLOT),
})
}
+30
View File
@@ -12,6 +12,9 @@ use bevy::prelude::*;
use crate::ui_script::UiInput;
#[cfg(test)]
mod ace_gate_tests;
mod channels;
pub(crate) mod commands;
mod edit;
mod event;
@@ -21,6 +24,11 @@ mod input;
#[cfg(test)]
mod tests;
/// Test-only: `ui_script::chat_tests` checks every name we fire against the live `ChatTypeInfo`
/// table, which lives on that side of the tree. The app itself calls it through `event::` — the
/// router is the only production caller and it is inside this module.
#[cfg(test)]
pub(crate) use event::event_name;
pub(crate) use event::{default_color, ChatEvent, ChatEventKind};
pub(crate) use feed::ChatLog;
@@ -32,6 +40,17 @@ impl Plugin for UiChatPlugin {
.init_resource::<frames::ChatWindows>()
.init_resource::<edit::ChatEditState>()
.init_resource::<edit::ChannelState>()
.init_resource::<channels::ZoneChannelWalk>()
// `ChatChannels.dbc` — six rows, read once; the auto-join walk and every chat event's
// arg7 both come out of it. **`.after(AssetSet::Open)` is load-bearing**: without it
// this runs before the patch chain exists, takes its `assets: Option<Res<_>>` `None`
// arm, and silently loads nothing — no zone channels, ever, with no error. A live
// probe is what caught that; no unit test could, because the tests hand the catalog in.
// `crate::area`'s `AreaTable.dbc` load carries the same ordering for the same reason.
.add_systems(
Startup,
channels::load_chat_channels.after(benilla_assets::AssetSet::Open),
)
// The slash-command table (decision 0881), built from the reference's own alias strings
// once the VM's globals and the emote catalog exist — both are `Startup`, so this runs
// at the next schedule rather than chasing an ordering constraint into two modules.
@@ -51,10 +70,21 @@ impl Plugin for UiChatPlugin {
edit::chat_edit_live,
edit::chat_tab_cycle,
input::drain_chat_input,
// An addon's own line into the wire (decision 1199). AFTER the box's drain
// and in the same chain, so a `SendChatMessage` fired from a slash handler
// that the box's drain just ran goes out on the same frame.
input::drain_addon_chat_sends,
)
.chain()
.after(UiInput)
.run_if(in_state(crate::char_select::ClientState::InWorld)),
)
// The zone-channel auto-join (0288 P6): the client half of a handshake vmangos
// deliberately leaves to us. In-world only, and it early-outs on an unchanged zone.
.add_systems(
Update,
channels::auto_join_zone_channels
.run_if(in_state(crate::char_select::ClientState::InWorld)),
);
}
}
@@ -0,0 +1,245 @@
//! **Ace2's initialisation gate, opened end to end** — the reason 0288 §1's chat-event fire and
//! 0288 phase 6's zone auto-join were worth doing together.
//!
//! ## The failure this closes
//!
//! `AceEvent:IsFullyInitialized()` is called by 85 of the 218 addons in the vanilla corpus, and
//! under benilla it answered **false forever** — with no Lua error anywhere, which is why it went
//! unnoticed. The consequences are all silent: RosterLib never scans, BigWigs and oRA2 are
//! complete no-ops, Jostle never repositions, AceComm refuses to join its channels, and FuBar's
//! bar stays permanently blank across ~75 `FuBar_*` plugins.
//!
//! The gate is `AceEvent-2.0.lua:913-947`. On a fresh load `self.postInit` is false, and exactly
//! three things can set it:
//!
//! | armed by | what it schedules |
//! |---|---|
//! | `CHAT_MSG_CHANNEL_NOTICE` | `func` after 0.05 s |
//! | `MEETINGSTONE_CHANGED` | `func` after 1 s |
//! | `LANGUAGE_LIST_CHANGED` → `MINIMAP_ZONE_CHANGED` | `func` after 1 s |
//!
//! `func` sets `postInit = true` and fires `AceEvent_FullyInitialized`. Of the three arming
//! events, `CHAT_MSG_CHANNEL_NOTICE` is the one that can honestly fire at login on a 1.12 server —
//! it is the server's answer to joining a channel. benilla fired **no** `CHAT_MSG_*` at all, and
//! joined **no** channels, so none of the three ever arrived.
//!
//! ## What this file proves, and how
//!
//! Against the corpus's own Ace2 chain, loaded in FuBar's `.toc` order into a VM with our whole
//! FrameXML under it — not a stub, because the thing under test is third-party library code and a
//! stub would only ever prove that our stub works.
//!
//! **Nothing from the corpus is committed**, and the tests skip cleanly on a machine without it
//! (see [`corpus`]) — a checkout with no addon folder must never go red here.
use std::path::{Path, PathBuf};
use benilla_ui::script::UiScript;
use super::event::{ChatEvent, ChatEventKind as K};
use super::frames::{route, ChatWindows};
/// Where the vanilla addon corpus might be, in order. `$BENILLA_ADDON_CORPUS` first so a machine
/// that keeps it elsewhere needs no patch; then the sibling checkout, resolved from this crate's
/// manifest rather than the cwd (a pool worktree's cwd is not stable across tool calls, and
/// `CARGO_MANIFEST_DIR` is).
fn corpus_candidates() -> Vec<PathBuf> {
let mut out = Vec::new();
if let Some(over) = std::env::var_os("BENILLA_ADDON_CORPUS") {
out.push(PathBuf::from(over));
}
// …/<checkout>/crates/benilla-app → up to the checkout, then to its parent(s). A pool slot
// lives one level deeper than the primary checkout, so both hops are tried.
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
for up in [2usize, 3, 4] {
if let Some(root) = manifest.ancestors().nth(up) {
out.push(root.join("wow-addons-vanilla"));
}
}
out
}
/// The corpus root, or `None`. **`None` is a skip, never a failure**: the corpus is third-party
/// content that is deliberately not in this repo, so a machine without it must still be green.
fn corpus() -> Option<PathBuf> {
corpus_candidates().into_iter().find(|c| c.is_dir())
}
macro_rules! corpus_or_skip {
() => {
match corpus() {
Some(root) => root,
None => {
eprintln!(
"skipping: no vanilla addon corpus — looked in {:?} (set $BENILLA_ADDON_CORPUS)",
corpus_candidates()
);
return;
}
}
};
}
/// FuBar's `.toc` load order for the four files the gate lives in (`FuBar/FuBar.toc:16-19`).
/// AceEvent is last because it needs all three under it — AceLibrary to register into, Compost for
/// its table recycling, AceOO for the class it derives from.
const ACE_CHAIN: &[&str] = &[
"FuBar/libs/AceLibrary/AceLibrary.lua",
"FuBar/libs/Compost-2.0/Compost-2.0.lua",
"FuBar/libs/AceOO-2.0/AceOO-2.0.lua",
"FuBar/libs/AceEvent-2.0/AceEvent-2.0.lua",
];
/// A VM with our whole FrameXML and the corpus's Ace2 chain in it, at the point a real session
/// reaches `PLAYER_LOGIN` — which is where `AceEvent` starts waiting.
fn ace_vm(root: &Path) -> UiScript {
let mut script = UiScript::new().expect("VM");
script.set_screen_size(1024.0, 768.0);
// Our interface, exactly as the app loads it — Ace calls `CreateFrame`, `GetTime` and
// `ChatTypeInfo` as readily as it calls anything of its own.
let failures = crate::ui_script::load_default_ui(&script);
assert!(
failures.is_empty(),
"our own FrameXML failed to load: {failures:#?}"
);
for rel in ACE_CHAIN {
let path = root.join(rel);
let src = benilla_ui::source::decode(&std::fs::read(&path).unwrap_or_else(|e| {
panic!("{}: {e}", path.display());
}))
.into_owned();
script.run(&src).unwrap_or_else(|e| panic!("{rel}: {e}"))
}
// The UI-init tail the app runs once every file has loaded (`ui_script::finish_ui_load`'s
// second half): AceEvent registers PLAYER_LOGIN in `activate` and flips `playerLogin` on it.
script.fire_event("PLAYER_LOGIN", vec![]);
script
}
/// Whether `AceEvent:IsFullyInitialized()` answers true.
fn gate_open(s: &UiScript) -> bool {
s.eval::<bool>("return AceLibrary('AceEvent-2.0'):IsFullyInitialized() and true or false")
.expect("AceEvent-2.0 is registered")
}
/// **The headline.** A zone-channel join's `YOU_JOINED` notice opens Ace2's gate.
///
/// The event goes in through the real router, so what is being tested is benilla's own chat path
/// end to end — not a hand-rolled `fire_event`.
#[test]
fn a_you_joined_notice_opens_ace2s_initialisation_gate() {
let root = corpus_or_skip!();
let mut s = ace_vm(&root);
let mut windows = ChatWindows::default();
// An addon's-eye view of the gate opening: AceEvent fires `AceEvent_FullyInitialized` at its
// own registry, which is how every FuBar plugin learns it may start.
// The library-level registration AceEvent documents for a bare function (`:87-91` — called on
// AceEvent itself, `self` becomes the handler). `AceEvent_FullyInitialized` is in its
// `eventsWhichHappenOnce` table (`:79-84`), so it is a once-only registration by construction:
// the count below can only ever be 0 or 1, which is what makes 1 mean something.
s.run(
r#"
AceGateFired = 0
AceLibrary("AceEvent-2.0"):RegisterEvent("AceEvent_FullyInitialized", function()
AceGateFired = AceGateFired + 1
end)
"#,
)
.unwrap();
assert!(
!gate_open(&s),
"before any notice the gate is shut — that IS the bug this closes"
);
// The notice our zone auto-join provokes: the server's YOU_JOINED for "General - <zone>".
let mut joined = ChatEvent::text_only(K::ChannelNotice, String::new());
joined.notice = "2".into(); // channel_notice::YOU_JOINED
joined.channel = "General - Elwynn Forest".into();
joined.channel_base = "General - Elwynn Forest".into();
joined.channel_number = 1;
joined.zone_channel_id = 1; // ChatChannels.dbc General
route(&mut s, &mut windows, &joined);
// AceEvent does not open on the event itself — it schedules `func` 0.05 s out and lets its own
// OnUpdate drive it (`AceEvent-2.0.lua:938`, `:471`). So the clock has to move.
assert!(!gate_open(&s), "still shut inside the 0.05 s delay");
s.tick(0.02);
assert!(!gate_open(&s), "0.02 s is not 0.05 s");
s.tick(0.05);
assert!(
gate_open(&s),
"AceEvent:IsFullyInitialized() must be true once the notice's delay elapses"
);
assert_eq!(
s.eval::<i64>("return AceGateFired").unwrap(),
1,
"AceEvent_FullyInitialized fired exactly once"
);
assert!(s.errors().is_empty(), "Lua errors: {:?}", s.errors());
}
/// The control, and the reason the headline is not circular: **without the notice the gate stays
/// shut**, no matter how much time passes. If this ever goes green, something else is arming the
/// gate and the headline is measuring the wrong thing.
#[test]
fn without_the_notice_the_gate_stays_shut() {
let root = corpus_or_skip!();
let mut s = ace_vm(&root);
for _ in 0..40 {
s.tick(0.1); // four seconds — well past both the 0.05 s and the 1 s schedules
}
assert!(
!gate_open(&s),
"nothing but CHAT_MSG_CHANNEL_NOTICE can honestly arm this gate on a 1.12 server"
);
}
/// The other half: the walk that makes the notice happen at all. Composed against the corpus's
/// own expectations is not possible here (the corpus does not know our zone), so this asserts the
/// names the auto-join sends for a real zone and a real capital — the strings the server matches
/// to `ChatChannels.dbc` rows, and therefore the strings that decide whether we are in the same
/// channel as everyone else.
#[test]
fn the_auto_join_walk_names_the_channels_the_server_resolves() {
let data = benilla_formats::wow_data_or_skip!();
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
let cat = benilla_formats::load_chat_channels_catalog(&mut chain).expect("ChatChannels.dbc");
// The city word is DBC data, so read it the way the walk does rather than spelling it here —
// that this resolves to "City" at all is the finding (`AreaTable.dbc` row 3459,
// `Flags & 0x200`; wow-re `zone-chat-channel-autojoin.md` §3).
let areas = benilla_formats::load_area_table_catalog(&mut chain).expect("AreaTable.dbc");
let city = super::channels::city_word(&areas);
assert_eq!(
city,
Some("City"),
"the 0x200 sentinel row names the channel"
);
let out_in_the_world = super::channels::wanted_channels(&cat, "Elwynn Forest", false, city);
assert_eq!(
out_in_the_world,
vec!["General - Elwynn Forest", "LocalDefense - Elwynn Forest"]
);
let in_a_capital = super::channels::wanted_channels(&cat, "Stormwind City", true, city);
assert_eq!(
in_a_capital,
vec![
"General - Stormwind City",
"Trade - City",
"LocalDefense - Stormwind City",
]
);
// Every one of them resolves back to a built-in row — i.e. vmangos will treat them as the
// constant channels, not as custom ones (`GetChannelEntryFor`, DBCStores.cpp:531).
for name in out_in_the_world.iter().chain(in_a_capital.iter()) {
assert_ne!(
cat.zone_channel_id(name),
0,
"{name} must resolve to a ChatChannels.dbc row"
);
}
}
+340
View File
@@ -0,0 +1,340 @@
//! The zone-channel auto-join (decision 0288 phase 6's remainder) — the client half of a
//! handshake the server deliberately does not perform.
//!
//! **Nobody joins these for you.** vmangos's `Player::UpdateLocalChannels`
//! (`src/game/Objects/Player.cpp:5121`) is an empty function whose entire body is the comment
//! `// Updated client-side`, so a client that never sends `CMSG_JOIN_CHANNEL` sits in no channel
//! at all — silently, because nothing on the wire complains. That is what benilla did until now:
//! `/join` worked, and General/Trade/LocalDefense simply never existed.
//!
//! The walk is [`ChatChannels.dbc`](benilla_formats::ChatChannelsCatalog)'s `INITIAL` rows
//! composed against the player's current **zone** and sent as ordinary joins. It re-runs whenever
//! the zone (or the in-a-capital answer) changes: the zone-dependent rows carry the zone name
//! *inside* the channel name, so crossing a border is genuinely leaving one channel and joining
//! another — LEAVE(old) then JOIN(new), interleaved per DBC row.
//!
//! The mechanism is byte-verified in wow-re's `system/ui/scratch/zone-chat-channel-autojoin.md`
//! (the whole thing is one function, `ZoneChannelRefresh 0x49a210`); each part below cites the
//! section it comes from.
//!
//! ## Why this is worth more than three channel names
//!
//! The server answers each join with `SMSG_CHANNEL_NOTIFY`/`YOU_JOINED`, which becomes a
//! `CHAT_MSG_CHANNEL_NOTICE` at the Lua VM (0288 §1's addon-API phase) — and that one event is the
//! condition for **Ace2's initialisation gate**. `AceEvent`'s `activate` registers
//! `CHAT_MSG_CHANNEL_NOTICE` and, on the first one, schedules the function that sets
//! `self.postInit = true` and fires `AceEvent_FullyInitialized`
//! (`AceEvent-2.0.lua:913-947`). Until that happens `AceEvent:IsFullyInitialized()` answers false
//! forever, with no error anywhere — and ~75 FuBar plugins, BigWigs, oRA2, RosterLib, Jostle and
//! AceComm all wait on it. `ui_chat::ace_gate_tests` is that claim, proved against the corpus's
//! own Ace2 chain.
use bevy::prelude::*;
use benilla_assets::LockRecover;
use benilla_formats::ChatChannelsCatalog;
use crate::area::AreaTableRes;
use crate::net::{ClientCommand, NetCommands};
use super::edit::ChannelState;
/// `AreaTable.dbc` flag `0x08` — vmangos calls it `AREA_FLAG_SLAVE_CAPITAL`, with the comment
/// *"Allow trade channel"* (`src/game/Database/DBCEnums.h:58`). In the shipped 5875 table exactly
/// six rows carry it, all top-level zones and all with flags `0x138`: Undercity, Stormwind City,
/// Ironforge, Orgrimmar, Thunder Bluff and Darnassus. (`0x100` = `AREA_FLAG_CAPITAL` selects the
/// identical six in this build; `0x08` is the bit named for this job, so it is the one read.)
const AREA_FLAG_TRADE_CHANNEL: u32 = 0x08;
/// `AreaTable.dbc` flag `0x200` — the sentinel marking the row whose name is the **shared city
/// word** the Trade channel is named after.
///
/// Not a geographic area at all: the client scans the table once at load for this bit and keeps
/// the row's name at `0xb4e4f0` (single writer `0x4985fd`), then splices it into `Trade - %s`
/// (wow-re `system/ui/scratch/zone-chat-channel-autojoin.md` §3, VERIFIED). Exactly one row in the
/// shipped 5875 table carries it — **id 3459, `AreaName[enUS] = "City"`** — which is the whole
/// reason the word appears nowhere in `WoW.exe`. Reading it here rather than hardcoding `"City"`
/// is what keeps a localized install localized.
const AREA_FLAG_CITY_NAME_ROW: u32 = 0x200;
/// The shared city word, scanned out of `AreaTable.dbc` — the client's own load-time scan
/// ([`AREA_FLAG_CITY_NAME_ROW`]). `None` when no row carries the sentinel, in which case the
/// city-named rows are skipped rather than joined under a half-formed name.
pub(crate) fn city_word(areas: &benilla_formats::AreaTableCatalog) -> Option<&str> {
areas
.rows()
.find(|r| r.flags & AREA_FLAG_CITY_NAME_ROW != 0)
.map(|r| r.name.as_str())
.filter(|n| !n.is_empty())
}
/// What the walk currently believes it holds, and the state it composed that from.
///
/// Deliberately **not** [`ChannelState::joined`], which is server truth fed by YOU_JOINED /
/// YOU_LEFT notices. This is the *request* side: keying the diff off what we asked for is what
/// stops a join the server refuses (wrong faction, banned) from being re-sent every frame.
#[derive(Resource, Default)]
pub(crate) struct ZoneChannelWalk {
/// The composed names last requested, in `ChatChannels.dbc` row order.
held: Vec<String>,
/// The `(zone name, in a capital)` the held set was composed for. `None` = never walked, or
/// the session ended — either way the next in-world frame re-walks from scratch.
at: Option<(String, bool)>,
}
impl ZoneChannelWalk {
/// Forget everything: a dead socket took our channel membership with it, so the next login
/// must re-join rather than assume.
fn clear_session(&mut self) {
self.held.clear();
self.at = None;
}
}
/// Startup: read `ChatChannels.dbc` into [`ChannelState`], which owns it because the two things
/// that need it — composing the auto-join names and answering a chat event's arg7 — are both its
/// business. Absent install ⇒ an empty catalog: no auto-join, arg7 stays 0, nothing errors.
pub(super) fn load_chat_channels(
mut channels: ResMut<ChannelState>,
assets: Option<Res<benilla_assets::WorldAssets>>,
) {
let Some(assets) = assets else { return };
let loaded = {
let mut chain = assets.chain.lock_recover();
benilla_formats::load_chat_channels_catalog(&mut chain)
};
match loaded {
Ok(cat) => {
info!("chat: {} ChatChannels rows", cat.rows().len());
channels.channels = cat;
}
Err(e) => warn!("chat: ChatChannels.dbc failed to load — no zone channels: {e:#}"),
}
}
/// The names the auto-join rows compose to for a player standing in `zone_name`, in table order.
///
/// `in_city` gates the city-**only** rows (the `0x10` bit); `city_word` is what a city-**named**
/// row (`0x20`) puts in its `%s`, and an absent one skips those rows rather than composing
/// `"Trade - "`. The client has no such guard — its capital arm null-checks the DBC row pointer
/// but not the string, so a locale with that row's name blank would compose exactly that
/// (wow-re `zone-chat-channel-autojoin.md`, branch caveat). We decline instead: joining the wrong
/// channel name is invisible and permanent, and joining none is neither.
pub(crate) fn wanted_channels(
catalog: &ChatChannelsCatalog,
zone_name: &str,
in_city: bool,
city_word: Option<&str>,
) -> Vec<String> {
catalog
.auto_join_rows()
.filter(|r| in_city || !r.is_city_only())
.filter(|r| !r.takes_city_name() || city_word.is_some())
// A zone-dependent name with no zone to put in it would join a channel called
// "General - " — join nothing rather than something wrong.
.filter(|r| !r.is_zone_dependent() || !zone_name.is_empty())
.map(|r| r.joinable_name(zone_name, city_word.unwrap_or_default()))
.collect()
}
/// The walk: re-join the zone channels whenever the zone changes, and drop the ones that no
/// longer apply.
///
/// Reads the zone the same way [`crate::area`]'s splash does — the leaf `AreaTable` id under the
/// player, walked to its top-level parent — but takes the **real** zone name rather than the
/// display text. That is not a preference: the client composes from `GetRealZoneText`'s own cache
/// (`0xb4b404`, written from the parent zone's `AreaName_lang`), and the indoor/WMO name override
/// `0x67e670` rewrites only the slot feeding `GetZoneText`, never the one that becomes a channel
/// name (wow-re `zone-chat-channel-autojoin.md` §3). So a building never renames your channel.
///
/// **Timing:** the client re-walks inside `UpdateZoneText 0x494780` on every zone change (`§1`,
/// callsite `0x494931`), immediately *before* it fires `ZONE_CHANGED_NEW_AREA`, plus once at world
/// entry. This polls the same two inputs each frame and early-outs when they have not moved, which
/// reaches the same states; it is a poll rather than a hook because the zone is already derived
/// here, not published as an event payload.
///
/// **Selection:** the client's live predicate is the *saved* `ZONECHANNELS` mask, not `flags & 1`
/// read fresh — but that mask is **seeded from `flags & 1` when there is no usable chat-cache
/// file** (`0x4997fc`, gated `0x4997e8`). We persist no chat cache, so every session is that
/// fresh-character path and reading the DBC bit directly is exactly right. It stops being right
/// the day per-window channel settings are persisted.
pub(super) fn auto_join_zone_channels(
commands: Res<NetCommands>,
channels: Res<ChannelState>,
areas: Option<Res<AreaTableRes>>,
world: benilla_world::world_point::WorldPoint,
mut walk: ResMut<ZoneChannelWalk>,
mut disconnects: MessageReader<crate::net::DisconnectedMessage>,
) {
if disconnects.read().next().is_some() {
walk.clear_session();
}
let (Some(areas), false) = (areas, channels.channels.is_empty()) else {
return;
};
// The zone under the player, or nothing to do yet (tiles not streamed, no body).
let Some(zone_row) = world
.area()
.and_then(|leaf| areas.0.top_zone(leaf))
.and_then(|zone| areas.0.get(zone))
else {
return;
};
let at = (
zone_row.name.clone(),
zone_row.flags & AREA_FLAG_TRADE_CHANNEL != 0,
);
if walk.at.as_ref() == Some(&at) {
return; // same zone as last frame — the common case, and free
}
let wanted = wanted_channels(&channels.channels, &at.0, at.1, city_word(&areas.0));
// The diff is **per DBC row**, not per name — because a zone change does not add and remove
// channels, it *renames* one: `General - Felwood` and `General - Winterspring` are the same
// row (ChannelID 1) with a different `%s`. Keying on the row is what makes that a leave/join
// pair instead of an unrelated drop and add.
let row_of = |name: &str| channels.channels.zone_channel_id(name);
let leave = |name: &str| {
debug!("chat: leaving zone channel {name:?}");
let _ = commands.0.send(ClientCommand::LeaveChannel {
name: name.to_string(),
});
};
let join = |name: &str| {
debug!("chat: joining zone channel {name:?}");
let _ = commands.0.send(ClientCommand::JoinChannel {
name: name.to_string(),
password: String::new(),
});
};
// LEAVE(old) immediately followed by JOIN(new), per row, interleaved — the client does both in
// one iteration of its own walk (`0x49a367` sits earlier in the loop body than the join), and a
// retail sniff shows the pairs adjacent on the wire in exactly that order: LEAVE
// `General - Felwood` → JOIN `General - Winterspring` → LEAVE `LocalDefense - Felwood` → …
// (wow-re `zone-chat-channel-autojoin.md` §5).
for name in &wanted {
match walk.held.iter().find(|h| row_of(h) == row_of(name)) {
Some(old) if old == name => continue, // this row's name did not move
Some(old) => leave(old),
None => {}
}
join(name);
}
// Rows that stopped applying entirely — walking out of a capital drops Trade with nothing to
// replace it.
for old in walk
.held
.iter()
.filter(|h| !wanted.iter().any(|w| row_of(w) == row_of(h)))
{
leave(old);
}
walk.held = wanted;
walk.at = Some(at);
}
#[cfg(test)]
mod tests {
use super::*;
use benilla_formats::ChatChannelRow;
/// The shipped table, hand-built so this runs without an install (the real rows are asserted
/// against the DBC in `benilla_formats::chat_channels`).
fn catalog() -> ChatChannelsCatalog {
ChatChannelsCatalog::from_rows(
[
(1, 0x00003, "General - %s", "General"),
(2, 0x0003B, "Trade - %s", "Trade"),
(22, 0x10003, "LocalDefense - %s", "LocalDefense"),
(23, 0x10004, "WorldDefense", "WorldDefense"),
(24, 0x00000, "LookingForGroup", "LookingForGroup"),
(25, 0x20032, "GuildRecruitment - %s", "GuildRecruitment"),
]
.into_iter()
.map(|(id, flags, pattern, shortcut)| ChatChannelRow {
id,
flags,
pattern: pattern.into(),
shortcut: shortcut.into(),
})
.collect(),
)
}
/// The city word as the shipped `AreaTable.dbc` supplies it (row 3459, the `Flags & 0x200`
/// sentinel — see [`city_word`]).
const CITY: Option<&str> = Some("City");
/// Out in the world: General and LocalDefense, both zone-named. Trade is a city channel and
/// stays out; WorldDefense, LookingForGroup and GuildRecruitment carry no INITIAL bit.
#[test]
fn an_ordinary_zone_joins_general_and_local_defense() {
assert_eq!(
wanted_channels(&catalog(), "Elwynn Forest", false, CITY),
vec!["General - Elwynn Forest", "LocalDefense - Elwynn Forest"]
);
}
/// Inside a capital, Trade joins too — under the one shared name, not the city's own.
#[test]
fn a_capital_adds_the_shared_trade_channel() {
assert_eq!(
wanted_channels(&catalog(), "Stormwind City", true, CITY),
vec![
"General - Stormwind City",
"Trade - City",
"LocalDefense - Stormwind City",
]
);
}
/// No zone name ⇒ no zone-dependent join. "General - " is a real channel on the server, and
/// joining it would be a bug nothing else would ever report.
#[test]
fn an_unknown_zone_joins_nothing_zone_dependent() {
assert!(wanted_channels(&catalog(), "", false, CITY).is_empty());
assert!(wanted_channels(&catalog(), "", true, CITY).is_empty());
}
/// No city word (a locale whose sentinel row ships blank) ⇒ the city-named rows are skipped,
/// not composed as `"Trade - "`. The client has no such guard; declining is ours.
#[test]
fn a_missing_city_word_skips_the_trade_channel_rather_than_half_naming_it() {
assert_eq!(
wanted_channels(&catalog(), "Stormwind City", true, None),
vec!["General - Stormwind City", "LocalDefense - Stormwind City"]
);
}
/// A zone change **renames** a row rather than adding one: both zones' General lines carry
/// ChannelID 1, which is what makes the walk's diff a leave/join pair on one row instead of an
/// unrelated drop and add.
#[test]
fn crossing_a_zone_border_renames_the_same_rows() {
let cat = catalog();
let felwood = wanted_channels(&cat, "Felwood", false, CITY);
let winterspring = wanted_channels(&cat, "Winterspring", false, CITY);
assert_ne!(felwood, winterspring, "the names differ");
let ids = |v: &[String]| -> Vec<u32> { v.iter().map(|n| cat.zone_channel_id(n)).collect() };
assert_eq!(
ids(&felwood),
ids(&winterspring),
"…but they are the same DBC rows: [1, 22] either side of the border"
);
assert_eq!(ids(&felwood), vec![1, 22]);
}
/// arg7: the composed names resolve back to their `ChatChannels.dbc` id, custom ones to 0 —
/// matched by the server's own substring rule.
#[test]
fn composed_names_carry_their_channel_id_back() {
let cat = catalog();
assert_eq!(cat.zone_channel_id("General - Elwynn Forest"), 1);
assert_eq!(cat.zone_channel_id("Trade - City"), 2);
assert_eq!(cat.zone_channel_id("LocalDefense - Durotar"), 22);
assert_eq!(cat.zone_channel_id("World"), 0);
}
}
+70 -2
View File
@@ -56,6 +56,32 @@ impl SendType {
)
}
/// The chat-type TOKEN an addon passes to `SendChatMessage` (decision 1199) — the reference's
/// own `ChatTypeInfo` keys, uppercase.
///
/// `None` for a token we do not send. That is the honest answer for `"AFK"`/`"DND"` (which
/// set a flag rather than sending a line) and for anything an addon simply made up; the
/// caller reports it rather than guessing SAY, because a raid warning silently going to /say
/// is worse than one that does not go.
pub(crate) fn from_token(token: &str) -> Option<SendType> {
Some(match token {
"SAY" => SendType::Say,
"YELL" => SendType::Yell,
"EMOTE" => SendType::Emote,
"WHISPER" => SendType::Whisper,
"PARTY" => SendType::Party,
"RAID" => SendType::Raid,
"RAID_LEADER" => SendType::RaidLeader,
"RAID_WARNING" => SendType::RaidWarning,
"GUILD" => SendType::Guild,
"OFFICER" => SendType::Officer,
"BATTLEGROUND" => SendType::Battleground,
"BATTLEGROUND_LEADER" => SendType::BattlegroundLeader,
"CHANNEL" => SendType::Channel,
_ => return None,
})
}
/// The wire kind this type sends as.
pub(crate) fn wire(self) -> ChatKind {
match self {
@@ -127,11 +153,19 @@ impl SendType {
/// The channels this session has joined, in join order — the CLIENT-side number law
/// (`GetChannelName(n)`): `/1` is the first joined, `/2` the second; the numbered display form
/// ("1. General - Elwynn Forest") and the `[N. Name]` prefixes all derive from this order.
/// Fed by YOU_JOINED / YOU_LEFT notices ([`super::feed`]); the zone AUTO-join walk is the P6
/// remainder (flagged in 0288).
/// Fed by YOU_JOINED / YOU_LEFT notices ([`super::feed`]); the zone AUTO-join walk that fills it
/// at login is [`super::channels`].
#[derive(Resource, Default)]
pub(crate) struct ChannelState {
pub joined: Vec<String>,
/// `ChatChannels.dbc`, loaded once at Startup ([`super::channels::load_chat_channels`]).
///
/// It lives here because both of its consumers are this type's own business: composing the
/// auto-join names, and answering a chat event's **arg7** — the built-in ChannelID behind a
/// name, which is a pure function of the name (the server resolves it the same way) and so
/// needs no extra bookkeeping at join time. Empty without an install, which degrades to
/// "no zone channels, arg7 always 0" rather than to an error.
pub channels: benilla_formats::ChatChannelsCatalog,
}
impl ChannelState {
@@ -142,6 +176,40 @@ impl ChannelState {
.position(|c| c.eq_ignore_ascii_case(name))
.map(|i| i as u32 + 1)
}
/// Fill an event's four channel slots (arg4, arg7, arg8, arg9) in place.
///
/// **They are one record, not four fields.** In the reference all four are read off the
/// client's local channel record — `slot+0x00`, `+0x04`, `+0x94`, `+0x98` — so a name that is
/// *not* in the local list has no record to read and every one of them is empty: arg4 falls
/// back to the bare incoming name and arg7/arg8/arg9/arg10 are `0/0/""/0` together. They are
/// never independently populated. (wow-re `system/ui/scratch/chat-msg-event-args.md` §§4, 7-10,
/// VERIFIED; the `"%d. %s"` prefix at `0x8445c8` is applied on the hit leg `0x49aa48`, and
/// `0x49aa86` is the bare-name miss leg.)
///
/// So: on entry `event.channel` holds the name as the wire gave it ("General - Elwynn Forest").
/// If we are in that channel, on exit arg4 is the numbered display form, arg9 the stored name
/// **with its " - Zone" tail intact** (§9: the DBC name column *is* the format string the
/// client built the stored name with), arg8 the 1-based local slot and arg7 the
/// `ChatChannels.dbc` ChannelID — 0 for a custom channel. If we are not, nothing is stamped.
///
/// arg7 is resolved from the name against `ChatChannels.dbc` rather than remembered per join.
/// That is safe *because* it only ever runs on the hit leg: the id the client stores in
/// `slot+0x94` came from the same DBC row at join time, and vmangos resolves the name the same
/// way (`GetChannelEntryFor`), so no two of the three can disagree.
pub(crate) fn stamp_channel(&self, event: &mut super::event::ChatEvent) {
// A miss leaves all four alone — see the "one record" note above.
let Some(n) = self
.number_of(&event.channel)
.filter(|_| !event.channel.is_empty())
else {
return;
};
event.channel_base = event.channel.clone();
event.zone_channel_id = self.channels.zone_channel_id(&event.channel_base);
event.channel_number = n;
event.channel = format!("{n}. {}", event.channel_base);
}
}
/// The chat edit box's cross-open state (the fields `ChatEdit_OnLoad` seeds on the box).
+272 -7
View File
@@ -11,6 +11,16 @@
//! the group tables are `ChatTypeGroup` transcribed; the colors are the complete shipped default
//! table (the ref client's own `chat-cache.txt` COLORS block ≡ wow-re's byte-verified
//! `.rdata 0x804710` table, double-sourced in 0288's pin).
//!
//! This module also carries the **Lua face** of that currency: [`event_name`]
//! (kind → the reference's `CHAT_MSG_*` event name) and [`ChatEvent::script_args`] (the ten
//! positional args the client's own fire helper passes). 0288 §1 left that door open in its own
//! words — *"a future 0068 addon-API phase can move it into the VM (fire CHAT_MSG_* events at Lua)
//! without touching sources or sinks"* — and this is that phase: the router
//! ([`super::frames::route`]) now fires the real event beside the Rust render, so an addon sees the
//! same chat the window does.
use benilla_ui::script::ScriptValue;
/// The renderable chat-event kinds — `ChatTypeInfo`'s keys, minus the combat-log block.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -55,13 +65,120 @@ pub(crate) enum ChatEventKind {
BgSystemHorde,
}
/// One chat event — the reference's `CHAT_MSG_*` fire, typed. Field ↔ arg mapping (the 1.12
/// handler's positional args, constrained by `ChatFrame_OnEvent`'s reads):
/// `text`=arg1 · `sender`=arg2 · `language`=arg3 (already a *name*, empty = no header) ·
/// `channel`=arg4 (the display form, "N. Name - Zone" when numbered) · `target`=arg5 (the second
/// name of a two-name channel notice) · `flag`=arg6 ("AFK"/"DND"/"GM", empty none) ·
/// `channel_number`=arg8 · `channel_base`=arg9 (the number-less name). `notice`=arg1 of the
/// CHANNEL_NOTICE family (the token selecting the `CHAT_<X>_NOTICE` string).
impl ChatEventKind {
/// Every kind, for the sweeps that must be exhaustive to be worth anything — chiefly
/// `ui_script::chat_tests::fired_event_names_are_all_chat_type_info_keys`, which checks each
/// name we fire against the live `ChatTypeInfo` table rather than against a second copy of the
/// same list. Adding a variant makes [`event_name`]'s match fail to compile; the length
/// assertion in `tests::every_kind_is_in_all` is what makes you add it here too.
///
/// Test-only: the app itself never sweeps the kinds — it always has one in hand.
#[cfg(test)]
pub(crate) const ALL: &'static [ChatEventKind] = {
use ChatEventKind as K;
&[
K::Say,
K::Party,
K::Raid,
K::Guild,
K::Officer,
K::Yell,
K::Whisper,
K::WhisperInform,
K::Emote,
K::TextEmote,
K::System,
K::MonsterSay,
K::MonsterYell,
K::MonsterEmote,
K::MonsterWhisper,
K::Channel,
K::ChannelJoin,
K::ChannelLeave,
K::ChannelNotice,
K::ChannelNoticeUser,
K::ChannelList,
K::Afk,
K::Dnd,
K::Ignored,
K::Skill,
K::Loot,
K::Money,
K::CombatXpGain,
K::RaidLeader,
K::RaidWarning,
K::RaidBossEmote,
K::Battleground,
K::BattlegroundLeader,
K::BgSystemNeutral,
K::BgSystemAlliance,
K::BgSystemHorde,
]
};
}
/// One chat event — the reference's `CHAT_MSG_*` fire, typed.
///
/// **The arg list is TEN wide, and its shape is byte-pinned.** The client's per-type fire helper
/// `0x49b0b0` (reached from the chat chokepoint `0x49a870` at `0x49ac9a`) calls
/// `FrameScript_SignalEvent 0x703f50` with the format string `"%s%s%s%s%s%s%d%d%s%d"`
/// (`.rdata 0x844608`) — wow-re `system/ui/scratch/rested-xp-bindings.md` §9, VERIFIED there in
/// the course of the rest-state RE. So **arg1..arg6 are strings, arg7 and arg8 are numbers, arg9
/// is a string, arg10 is a number**, and not one of them is ever `nil`: `ChatFrame_OnEvent`
/// compares `arg7 > 0` and `arg10 > 0` bare, which under Lua 5.0 errors on a nil. Every slot is
/// always passed — zero or empty when unused.
///
/// Field ↔ arg mapping, each slot named by the consumer that reads it in the shipped
/// `ChatFrame.lua` (0288's pin; line numbers are that file's):
///
/// | arg | field | what it is |
/// |---|---|---|
/// | 1 | `text` / `notice` | the message body; for the CHANNEL_NOTICE family instead the **notice token** selecting `CHAT_<token>_NOTICE` (l.1416/1424) |
/// | 2 | `sender` | the speaker, or the notice's affected player (l.1404, l.1416) |
/// | 3 | `language` | already a *name* ("Orcish"); empty = no header (l.1442) |
//
// (arg3's empty case, checked rather than assumed: l.1442 also guards `arg3 ~= "Universal"`, which
// reads like the client passes that word for language 0. It does not — "Universal" is in neither
// `Languages.dbc` (13 rows, ids 1-33, no 0) nor `WoW.exe` nor `GlobalStrings.lua`, so that arm is
// vestigial in 1.12 and `strlen(arg3) > 0` is what actually suppresses the header. Our
// [`language_name`] answering "" for 0 is therefore the right shape, not a shortcut.)
/// | 4 | `channel` | the display form, "N. Name - Zone" when numbered (l.1373, l.1463) |
/// | 5 | `target` | the second name of a two-name notice — "X kicked by Y" (l.1414-1416) |
/// | 6 | `flag` | "AFK"/"DND"/"GM", empty none; read as `CHAT_FLAG_<flag>` (l.1431) |
/// | 7 | `zone_channel_id` | the **`ChatChannels.dbc` ChannelID** behind a zone channel, 0 for a custom one — matched against `ChatFrame.zoneChannelList` (l.1379) |
/// | 8 | `channel_number` | the client-local joined-channel slot; `ChatTypeInfo["CHANNEL"..arg8]` (l.1381) |
/// | 9 | `channel_base` | the channel name **without** the leading number (l.1378's own comment) |
/// | 10 | — (always 0) | the channel's **split/instance index**, appended as `arg4.." "..arg10` when `> 0` (l.1421-1423) |
///
/// **arg10 is deliberately not a field.** It is `slot+0x98`, the same value `GetChannelName`
/// returns third, and its wire source is the **second** `u32` of `SMSG_CHANNEL_NOTIFY`'s YOU_JOINED
/// tail — the one `Channel::MakeYouJoined` (`Chat/Channel.cpp:823-827`) hardcodes to 0 with the
/// comment *"the non-zero number will be appended to the channel name"*, which is `ChatFrame.lua`
/// l.1421-1423 exactly. (The *first* u32 of that tail is the channel flags, which the client reads
/// and discards.) Our decode already drops the second for that reason
/// (`ChannelNoticeTail::YouJoined`), so [`ChatEvent::script_args`] passes the literal 0 rather than
/// carrying a field that can only ever hold it.
///
/// **arg4/arg7/arg8/arg9 are one record**, all `""`/`0`/`0`/`""` together when the channel is not
/// in the local list — see [`super::edit::ChannelState::stamp_channel`], which is the only place
/// they are written.
///
/// **KNOWN GAP — arg1 should be GARBLED, and ours is not.** The reference garbles the body
/// client-side (`0x49b560` from `0x49aa7c`) and passes the wire text through only when the language
/// is Universal, there is no local player, or the player's skill in that language is ≥ 300
/// (`0x49b599 cmp esi,0x12c`). benilla speaks Common and does not model language skill at all
/// (0288 §3 keeps garbling out with the language picker), so arg1 is always the ungarbled wire
/// text. An addon reading a foreign-language line therefore sees more than the reference would give
/// it. Also unmodeled: the profanity filter `0x4a1ca0` (`0x49ab23`), which in the reference can
/// suppress the whole event for non-whisper types.
///
/// **Corrections on record.** This comment previously listed args 1-6, 8 and 9 only — arg7 and
/// arg10 were absent, and arg7 is a real slot the reference's own channel routing turns on. The
/// omission was ours. The whole map is now byte-verified end to end in wow-re
/// `system/ui/scratch/chat-msg-event-args.md` (T2), which also **refutes** 0288's standing lead
/// that rf77 had traced this marshaller: rf77's trace was opcode `0x92` = SMSG_GUILD_EVENT,
/// misfiled. Real `SMSG_MESSAGECHAT` is `0x96` → `0x49d560` and matches vmangos branch for branch,
/// which is what benilla's decode already did.
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct ChatEvent {
pub kind: Option<ChatEventKind>,
@@ -71,6 +188,9 @@ pub(crate) struct ChatEvent {
pub channel: String,
pub target: String,
pub flag: String,
/// arg7 — the `ChatChannels.dbc` ChannelID behind a zone channel (1 General, 2 Trade,
/// 22 LocalDefense); 0 for a custom channel. Filled from [`super::edit::ChannelState`].
pub zone_channel_id: u32,
pub channel_number: u32,
pub channel_base: String,
pub notice: String,
@@ -85,6 +205,151 @@ impl ChatEvent {
..Default::default()
}
}
/// The notice byte behind a CHANNEL_NOTICE(_USER) event, if this is one.
///
/// `notice` carries the `SMSG_CHANNEL_NOTIFY` byte in decimal because the composer and the
/// event bridge both need it and the field predates both; this is the single parse.
pub(crate) fn notice_byte(&self) -> Option<u8> {
self.notice.parse().ok()
}
/// This event's `arg1..arg10`, in the reference's own order and types (see the struct doc's
/// table). Ten values, always — `nil` is not a legal value in any slot.
pub(crate) fn script_args(&self) -> Vec<ScriptValue> {
// arg1 is the notice TOKEN for the notice family and the body for everything else — the
// one slot whose meaning is type-dependent (`ChatFrame_OnEvent` l.1416/1424 vs l.1396).
let arg1 = match (self.kind, self.notice_byte()) {
(Some(ChatEventKind::ChannelNotice | ChatEventKind::ChannelNoticeUser), Some(byte)) => {
notice_token(byte).unwrap_or_default().to_string()
}
_ => self.text.clone(),
};
vec![
ScriptValue::Str(arg1),
ScriptValue::Str(self.sender.clone()),
ScriptValue::Str(self.language.clone()),
ScriptValue::Str(self.channel.clone()),
ScriptValue::Str(self.target.clone()),
ScriptValue::Str(self.flag.clone()),
ScriptValue::Int(i64::from(self.zone_channel_id)),
ScriptValue::Int(i64::from(self.channel_number)),
ScriptValue::Str(self.channel_base.clone()),
// arg10 — see the struct doc: always 0 off this server.
ScriptValue::Int(0),
]
}
}
/// The reference's event NAME for a kind — `"CHAT_MSG_" ++ <the `ChatTypeInfo` key>`.
///
/// The key spellings are the reference's own (they are what `ChatFrame_OnEvent` recovers with
/// `strsub(event, 10)` and looks up in `ChatTypeInfo`), so this table and the `ChatTypeInfo` the
/// engine seeds are the same set by construction — asserted in
/// `ui_script::chat_tests::every_fired_event_name_is_a_chat_type_info_key`.
pub(crate) fn event_name(kind: ChatEventKind) -> &'static str {
use ChatEventKind as K;
match kind {
K::Say => "CHAT_MSG_SAY",
K::Party => "CHAT_MSG_PARTY",
K::Raid => "CHAT_MSG_RAID",
K::Guild => "CHAT_MSG_GUILD",
K::Officer => "CHAT_MSG_OFFICER",
K::Yell => "CHAT_MSG_YELL",
K::Whisper => "CHAT_MSG_WHISPER",
K::WhisperInform => "CHAT_MSG_WHISPER_INFORM",
K::Emote => "CHAT_MSG_EMOTE",
K::TextEmote => "CHAT_MSG_TEXT_EMOTE",
K::System => "CHAT_MSG_SYSTEM",
K::MonsterSay => "CHAT_MSG_MONSTER_SAY",
K::MonsterYell => "CHAT_MSG_MONSTER_YELL",
K::MonsterEmote => "CHAT_MSG_MONSTER_EMOTE",
K::MonsterWhisper => "CHAT_MSG_MONSTER_WHISPER",
K::Channel => "CHAT_MSG_CHANNEL",
K::ChannelJoin => "CHAT_MSG_CHANNEL_JOIN",
K::ChannelLeave => "CHAT_MSG_CHANNEL_LEAVE",
K::ChannelNotice => "CHAT_MSG_CHANNEL_NOTICE",
K::ChannelNoticeUser => "CHAT_MSG_CHANNEL_NOTICE_USER",
K::ChannelList => "CHAT_MSG_CHANNEL_LIST",
K::Afk => "CHAT_MSG_AFK",
K::Dnd => "CHAT_MSG_DND",
K::Ignored => "CHAT_MSG_IGNORED",
K::Skill => "CHAT_MSG_SKILL",
K::Loot => "CHAT_MSG_LOOT",
K::Money => "CHAT_MSG_MONEY",
K::CombatXpGain => "CHAT_MSG_COMBAT_XP_GAIN",
K::RaidLeader => "CHAT_MSG_RAID_LEADER",
K::RaidWarning => "CHAT_MSG_RAID_WARNING",
K::RaidBossEmote => "CHAT_MSG_RAID_BOSS_EMOTE",
K::Battleground => "CHAT_MSG_BATTLEGROUND",
K::BattlegroundLeader => "CHAT_MSG_BATTLEGROUND_LEADER",
K::BgSystemNeutral => "CHAT_MSG_BG_SYSTEM_NEUTRAL",
K::BgSystemAlliance => "CHAT_MSG_BG_SYSTEM_ALLIANCE",
K::BgSystemHorde => "CHAT_MSG_BG_SYSTEM_HORDE",
}
}
/// The notice TOKEN a `SMSG_CHANNEL_NOTIFY` byte becomes in `arg1` — the token
/// `ChatFrame_OnEvent` splices into `getglobal("CHAT_"..arg1.."_NOTICE")` (l.1416/1424).
///
/// The token set is read off the shipped `GlobalStrings.lua`'s own `CHAT_<X>_NOTICE` keys
/// (l.494-745 of the extracted file), paired to the vmangos notice byte that produces each line —
/// the same pairing [`super::frames::compose_notice`] already renders, which is why the two tables
/// are asserted against each other rather than left to drift
/// (`ui_chat::tests::every_rendered_notice_has_a_token`).
///
/// Byte-for-byte identical to the client's own jump table (`0x49c60c`, 32 direct arms), verified
/// arm by arm in wow-re `chat-msg-event-args.md` §9 — checked against that table after the fact,
/// not derived from it.
///
/// `None` = a byte the reference passes no token for: `0x00`/`0x01` are the CHANNEL_JOIN /
/// CHANNEL_LEAVE member lines (their arg1 is the empty string, not a token), `0x0C` MODE_CHANGE
/// fires **no chat event at all** (`0x49c24d` calls `0x49e910` and returns — which is why
/// [`super::feed::ChatLog::push_channel_notice`] drops it before it becomes an event), and anything
/// past `0x1F` is outside vmangos's range.
///
/// **Two state-dependent tokens we do not model:** the client answers `"YOU_CHANGED"` for `0x02`
/// and `"SUSPENDED"` for `0x03` when its own channel record is in the matching state
/// (`rec+0x9c == 2` / `== 3`) — a per-channel state benilla keeps nothing equivalent to, so we
/// always send the plain `YOU_JOINED` / `YOU_LEFT`. Both alternates are `CHAT_<X>_NOTICE` strings
/// that exist in GlobalStrings (`CHAT_YOU_CHANGED_NOTICE`, `CHAT_SUSPENDED_NOTICE`).
pub(crate) fn notice_token(byte: u8) -> Option<&'static str> {
use benilla_protocol::messages::channel_notice as n;
Some(match byte {
n::YOU_JOINED => "YOU_JOINED",
n::YOU_LEFT => "YOU_LEFT",
n::WRONG_PASSWORD => "WRONG_PASSWORD",
n::NOT_MEMBER => "NOT_MEMBER",
n::NOT_MODERATOR => "NOT_MODERATOR",
n::PASSWORD_CHANGED => "PASSWORD_CHANGED",
n::OWNER_CHANGED => "OWNER_CHANGED",
n::PLAYER_NOT_FOUND => "PLAYER_NOT_FOUND",
n::NOT_OWNER => "NOT_OWNER",
n::CHANNEL_OWNER => "CHANNEL_OWNER",
n::ANNOUNCEMENTS_ON => "ANNOUNCEMENTS_ON",
n::ANNOUNCEMENTS_OFF => "ANNOUNCEMENTS_OFF",
n::MODERATION_ON => "MODERATION_ON",
n::MODERATION_OFF => "MODERATION_OFF",
n::MUTED => "MUTED",
n::PLAYER_KICKED => "PLAYER_KICKED",
n::BANNED => "BANNED",
n::PLAYER_BANNED => "PLAYER_BANNED",
n::PLAYER_UNBANNED => "PLAYER_UNBANNED",
n::PLAYER_NOT_BANNED => "PLAYER_NOT_BANNED",
n::PLAYER_ALREADY_MEMBER => "PLAYER_ALREADY_MEMBER",
n::INVITE => "INVITE",
n::INVITE_WRONG_FACTION => "INVITE_WRONG_FACTION",
n::WRONG_FACTION => "WRONG_FACTION",
n::INVALID_NAME => "INVALID_NAME",
n::NOT_MODERATED => "NOT_MODERATED",
n::PLAYER_INVITED => "PLAYER_INVITED",
n::PLAYER_INVITE_BANNED => "PLAYER_INVITE_BANNED",
n::THROTTLED => "THROTTLED",
// MODE_CHANGE (0x0C) has no CHAT_*_NOTICE string in 1.12 — the reference renders nothing,
// so there is no token. JOINED/LEFT (0x00/0x01) are not notices at all: they are the
// CHANNEL_JOIN/CHANNEL_LEAVE member-line events.
_ => return None,
})
}
/// The message groups a window registers — `ChatTypeGroup`'s keys (transcribed; the chat-cache
+30 -15
View File
@@ -301,12 +301,24 @@ pub(super) fn feed_chat(
Pending::Event(mut event) => {
// The joined-list upkeep (the client-side number law): YOU_JOINED appends,
// YOU_LEFT (and the suspend form) removes.
//
// Logged, because this edge is where "we asked to join" becomes "the server says
// we are in": [`super::channels`]'s walk only ever proves the request went out, and
// the round trip is what actually arms an addon (it is the `CHAT_MSG_CHANNEL_NOTICE`
// Ace2's whole init gate waits on). A join the server refuses is otherwise
// completely silent on this side.
if event.kind == Some(ChatEventKind::ChannelNotice) {
match event.notice.as_str() {
"2" if channels.number_of(&event.channel).is_none() => {
channels.joined.push(event.channel.clone());
debug!(
"chat: server confirms channel {:?} joined (slot {})",
event.channel,
channels.joined.len()
);
}
"3" => {
debug!("chat: server confirms channel {:?} left", event.channel);
channels
.joined
.retain(|c| !c.eq_ignore_ascii_case(&event.channel));
@@ -315,9 +327,7 @@ pub(super) fn feed_chat(
}
}
// A member-line / notice channel renders numbered when we know its slot.
if let Some(n) = channels.number_of(&event.channel) {
event.channel = format!("{n}. {}", event.channel);
}
channels.stamp_channel(&mut event);
route(&mut script, &mut windows, &event);
}
Pending::Wire { msg, tries } => {
@@ -348,16 +358,9 @@ pub(super) fn feed_chat(
});
continue;
}
// The numbered display form ("1. General - Elwynn Forest") when the channel is
// one of ours — the client-built arg4 the composer strips/brackets.
let channel_disp = msg
.channel
.clone()
.map(|c| match channels.number_of(&c) {
Some(n) => format!("{n}. {c}"),
None => c,
})
.unwrap_or_default();
// The channel's base name; the numbered display form, its slot number and its
// zone id are stamped on below ([`ChannelState::stamp_channel`]) — arg4/arg7-arg9.
let channel_base = msg.channel.clone().unwrap_or_default();
// `$`-macro expansion (decision 0754, corrected by 0759): the reference runs its one
// server-text expander over the monster/boss + BG-system types and nothing else,
// against the guid the line is ADDRESSED to. Every other type reaches the frame
@@ -416,7 +419,7 @@ pub(super) fn feed_chat(
None
};
let text = expanded.unwrap_or_else(|| msg.text.clone());
let event = ChatEvent {
let mut event = ChatEvent {
kind,
text: text.clone(),
sender: name.unwrap_or_else(|| {
@@ -427,10 +430,11 @@ pub(super) fn feed_chat(
}
}),
language: language_name(msg.language).to_string(),
channel: channel_disp,
channel: channel_base,
flag: flag_of_tag(msg.chat_tag).to_string(),
..Default::default()
};
channels.stamp_channel(&mut event);
// A received whisper remembers its sender (`ChatEdit_SetLastTellTarget`,
// ChatFrame_OnEvent l.1471) — the `/r` + Tab-cycle ring.
if event.kind == Some(ChatEventKind::Whisper) && !event.sender.is_empty() {
@@ -469,6 +473,17 @@ pub(super) fn feed_chat(
});
continue;
}
// NOT stamped, deliberately — and this arm is inconsistent with the other two
// because of it. A guid-tail notice (a join/leave member line, a kick, a
// moderation change) reaches the composer with its channel name UNNUMBERED, so it
// renders "[World] Ann joined channel." where the same channel's speech renders
// "[1. World]". The reference numbers both: `ChatFrame_OnEvent` l.1463 strips only
// the " - Zone" tail from arg4, never the number.
//
// Adding `channels.stamp_channel(&mut event)` here fixes it in one line — and
// changes what the player sees, which this pass is not allowed to do. Left for the
// director's call; the cost of leaving it is that arg7/arg8/arg9 are 0/0/empty on
// these events alone.
let event = notice_event(
notice,
channel,
+52 -25
View File
@@ -11,7 +11,7 @@ use bevy::prelude::*;
use benilla_protocol::messages::channel_notice as notice;
use super::event::{default_color, group_kinds, ChatEvent, ChatEventKind, ChatGroup};
use super::event::{default_color, event_name, group_kinds, ChatEvent, ChatEventKind, ChatGroup};
/// How long after a received whisper the `TellMessage` alert stays silent
/// (`CHAT_TELL_ALERT_TIME = 300` — ref ChatFrame.lua l.4: only a tell arriving ≥5 min after the
@@ -72,10 +72,34 @@ impl ChatWindows {
}
}
/// Route one event: compose it once, then AddMessage into every subscribed window. Whisper
/// receipt side-effects (the throttled `TellMessage` chime + the unselected-tab flash — ref
/// ChatFrame_OnEvent l.1470-1477) fire through the script seam. A kind-less event (an
/// unmodeled wire type) drops with a warn — never silently.
/// Route one event: compose it once and AddMessage it into every subscribed window, then fire the
/// real `CHAT_MSG_*` at the VM. Whisper receipt side-effects (the throttled `TellMessage` chime +
/// the unselected-tab flash — ref ChatFrame_OnEvent l.1470-1477) ride the render half. A kind-less
/// event (an unmodeled wire type) drops with a warn — never silently.
///
/// **Two consumers, one event, no double-print.** In the reference, C fires `CHAT_MSG_<TYPE>` and
/// *Lua* — `ChatFrame_OnEvent` — is what turns it into a line; here the composer below IS that
/// handler, transcribed into Rust (0288 §1). So the fire is additive: it exists for **addons**,
/// and our windows keep rendering exactly as they did. Nothing prints twice because our shipped
/// `ChatFrame.xml` handles exactly one event, `EXECUTE_CHAT_LINE` (assets/ui/ChatFrame.xml, its
/// `<OnEvent>`) — an addon may `ChatFrame1:RegisterEvent("CHAT_MSG_SAY")` for its own reasons and
/// our frame's handler will simply not match it.
/// `ui_chat::tests::an_addon_registering_our_own_chat_frame_does_not_double_print` is the guard.
///
/// **Render first, then fire — that ordering is the reference's, not a convenience.** The client
/// dispatches an event to its listeners in registration order (FIFO, wow-re
/// `event-dispatch-order.md`, and [`benilla_ui::script::UiScript::fire_event`] transcribes it), and
/// ChatFrame1 registers at FrameXML load — before any addon exists. So in the real client the line
/// is already in the window by the time an addon's handler runs, and an addon that reads
/// `GetNumMessages()` or re-reads the last line from its own `CHAT_MSG_*` handler depends on that.
/// Our Rust composer stands in for ChatFrame1's handler, so it has to go first for the same reason.
///
/// **The fire is unconditional; the render is not.** The client's `SignalEvent` does not consult
/// any window's message-group registration — that is `ChatFrame_OnEvent`'s job, per frame — so an
/// event reaches Lua even when neither of our windows wants it. What does *not* reach here is a
/// notice the reference declines to make an event out of at all: MODE_CHANGE is dropped upstream,
/// at the feed, exactly as the client's `0x0C` arm returns without firing
/// (`ui_chat::tests::a_mode_change_notice_never_becomes_an_event`).
pub(crate) fn route(
script: &mut benilla_ui::script::UiScript,
windows: &mut ChatWindows,
@@ -85,27 +109,30 @@ pub(crate) fn route(
warn!("chat: unroutable event (no kind): {:?}", event.text);
return;
};
let Some(line) = compose(event, kind) else {
return; // composed to nothing (a silent notice)
};
let color = default_color(kind);
for idx in 0..2 {
if windows.wants(idx, kind) {
add(script, &format!("ChatFrame{}", idx + 1), &line, color);
// ── our own window: the transcribed ChatFrame_OnEvent, i.e. the first-registered listener ──
if let Some(line) = compose(event, kind) {
let color = default_color(kind);
for idx in 0..2 {
if windows.wants(idx, kind) {
add(script, &format!("ChatFrame{}", idx + 1), &line, color);
}
}
if kind == ChatEventKind::Whisper {
// The tell chime, throttled to one per 5 minutes (CHAT_TELL_ALERT_TIME), and the tab
// flash when the receiving window (1) isn't the selected dock tab. Inside this half
// because the reference does them inside ChatFrame_OnEvent too (l.1470-1477).
if windows.tell_alert_left <= 0.0 {
crate::ui_script::run_or_warn(script, "PlaySound(\"TellMessage\")");
}
windows.tell_alert_left = TELL_ALERT_SECS;
crate::ui_script::run_or_warn(
script,
"if BenillaFCF.selected ~= 1 then BenillaFCF_FlashTab(1) end",
);
}
}
if kind == ChatEventKind::Whisper {
// The tell chime, throttled to one per 5 minutes (CHAT_TELL_ALERT_TIME), and the tab
// flash when the receiving window (1) isn't the selected dock tab.
if windows.tell_alert_left <= 0.0 {
crate::ui_script::run_or_warn(script, "PlaySound(\"TellMessage\")");
}
windows.tell_alert_left = TELL_ALERT_SECS;
crate::ui_script::run_or_warn(
script,
"if BenillaFCF.selected ~= 1 then BenillaFCF_FlashTab(1) end",
);
}
// ── everyone else: the addons, after the default UI, exactly as registration order says ──
script.fire_event(event_name(kind), event.script_args());
}
/// Add one composed line, converting the `0..255` table color to the seam's `0..1` floats.
@@ -241,7 +268,7 @@ pub(crate) fn compose_notice(event: &ChatEvent) -> Option<String> {
let chan = strip_zone(&event.channel);
let a = &event.sender; // the notice's first name (actor / affected)
let b = &event.target; // the second name (kicked-by style)
let n: u8 = event.notice.parse().unwrap_or(0xFF);
let n: u8 = event.notice_byte().unwrap_or(0xFF);
Some(match n {
notice::YOU_JOINED => format!("Joined Channel: [{chan}]"),
notice::YOU_LEFT => format!("Left Channel: [{chan}]"),
@@ -739,6 +739,17 @@ pub(super) fn drain_chat_input(
}
}
ParsedChat::Unknown => {
// Before the help line: an ADDON may claim it (decision 1195). The reference
// resolves `SlashCmdList` in the same pass as its own commands; ours runs after
// the boot table misses, which gives the same precedence — a shipped command can
// never be shadowed — without moving our handlers into Lua.
if let Some(rest) = msg.strip_prefix('/') {
let rest = rest.trim();
let (cmd, args) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
if script.run_slash_command(cmd, args.trim()) {
continue;
}
}
// HELP_TEXT_SIMPLE (the ref's unknown-command reply, ChatEdit_ParseText l.2203).
chat_log.push_event(super::event::ChatEvent::text_only(
super::event::ChatEventKind::System,
@@ -840,3 +851,42 @@ pub(super) fn emote_send_eligible(emote_flags: u32, stand_state: u8, swimming: b
}
true
}
/// Turn an addon's `SendChatMessage` calls into sends (decision 1199).
///
/// Its own system rather than a branch inside [`drain_chat_input`], for the reason
/// `benilla_ui::script::chat_send`'s module doc gives: the box's drain runs the **slash grammar**
/// and this path must not. An addon announcing `"/dance"` is saying six characters.
///
/// An unknown chat-type token is **reported, not guessed**. `SendChatMessage(msg, "RAID_WARNING")`
/// silently going to /say is worse than not going: the addon believes it warned the raid.
pub(super) fn drain_addon_chat_sends(
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
commands: Res<NetCommands>,
mut chat_log: ResMut<super::feed::ChatLog>,
) {
let Some(mut script) = script else {
return;
};
for send in script.take_chat_sends() {
let Some(kind) = super::edit::SendType::from_token(&send.chat_type) else {
warn!(
"chat: SendChatMessage with unknown type {:?}",
send.chat_type
);
chat_log.push_event(super::event::ChatEvent::text_only(
super::event::ChatEventKind::System,
format!("Unknown chat type \"{}\".", send.chat_type),
));
continue;
};
let cmd = ClientCommand::Chat {
kind: kind.wire(),
target: send.target,
text: send.text,
};
if commands.0.send(cmd).is_err() {
warn!("chat: not connected; addon line dropped");
}
}
}
+315
View File
@@ -240,6 +240,321 @@ fn channel_notices_compose_by_the_notice_law() {
);
}
// ── the Lua face: the real CHAT_MSG_* fire (0288 §1's addon-API phase) ────────────────────────
//
// These drive the REAL router into a REAL VM with our shipped ChatFrame.xml under it, because the
// question they exist to answer — "does anything print twice now?" — cannot be answered by
// reasoning about the composer in isolation. `route` both renders and fires; only a VM holding
// our actual window can show that the two do not add up to two lines.
/// A fresh VM with the shipped chat stack under it — the same files the app loads, so `ChatFrame1`
/// here is the real window carrying its real `<OnEvent>`.
fn chat_vm() -> benilla_ui::script::UiScript {
let mut s = benilla_ui::script::UiScript::new().unwrap();
for file in ["Fonts.xml", "ChatFrame.xml"] {
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets/ui")
.join(file),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
}
s.set_screen_size(1600.0, 900.0);
s.resolve();
s
}
/// An "addon" that records what a `CHAT_MSG_*` fire actually delivered — the count, the event
/// name, and `arg1..arg10` joined with `|`. The concatenation is the point: a `nil` in any slot
/// raises in Lua, so a passing read is itself the proof that all ten args arrived.
const SPY: &str = r#"
SpyN, SpyEvent, SpyLine = 0, "", ""
Spy = CreateFrame("Frame", "BenillaChatSpy")
Spy:SetScript("OnEvent", function()
SpyN = SpyN + 1
SpyEvent = event
SpyLine = arg1.."|"..arg2.."|"..arg3.."|"..arg4.."|"..arg5.."|"..arg6..
"|"..arg7.."|"..arg8.."|"..arg9.."|"..arg10
end)
"#;
/// How many lines `ChatFrame1` is holding (`GetNumMessages`).
fn lines_in_window(s: &benilla_ui::script::UiScript) -> i64 {
s.eval::<i64>("return ChatFrame1:GetNumMessages()").unwrap()
}
/// **The double-print answer, proved rather than argued.**
///
/// In the reference, `CHAT_MSG_SAY` is what MAKES the line: C fires it, `ChatFrame_OnEvent` calls
/// `AddMessage`. benilla composes and adds in Rust instead (0288 §1) and now fires the event as
/// well — so the honest worry is that an addon registering the event on *our own* ChatFrame1 makes
/// the line land twice. It does not, and the mechanism is that our shipped `ChatFrame.xml`
/// `<OnEvent>` handles exactly one event (`EXECUTE_CHAT_LINE`) and ignores everything else.
///
/// The spy is a control, not decoration: without it a broken fire would pass this test.
#[test]
fn an_addon_registering_our_own_chat_frame_does_not_double_print() {
let mut s = chat_vm();
let mut windows = super::frames::ChatWindows::default();
s.run(SPY).unwrap();
s.run(r#"BenillaChatSpy:RegisterEvent("CHAT_MSG_SAY")"#)
.unwrap();
assert_eq!(lines_in_window(&s), 0, "the window starts empty");
super::frames::route(&mut s, &mut windows, &ev(K::Say, "hi there", "Bob"));
assert_eq!(lines_in_window(&s), 1, "our window prints exactly once");
assert_eq!(
s.eval::<i64>("return SpyN").unwrap(),
1,
"the addon saw the fire — otherwise the count above proves nothing"
);
// Now the addon registers OUR window for the event, exactly as the reference's own
// FloatingChatFrame does. This is the double-print case if there is one.
s.run(r#"ChatFrame1:RegisterEvent("CHAT_MSG_SAY")"#)
.unwrap();
super::frames::route(&mut s, &mut windows, &ev(K::Say, "hi again", "Bob"));
assert_eq!(
lines_in_window(&s),
2,
"one more line, not two — ChatFrame1's own OnEvent does not render CHAT_MSG_*"
);
assert_eq!(s.eval::<i64>("return SpyN").unwrap(), 2);
assert!(s.errors().is_empty(), "handler errors: {:?}", s.errors());
}
/// **The line is already in the window when an addon's handler runs.** The reference dispatches to
/// listeners in registration order and ChatFrame1 registers at FrameXML load, before any addon
/// exists — so an addon that reads `GetNumMessages()` (or re-reads the last line to recolour it)
/// from its own `CHAT_MSG_*` handler sees the line, not the gap before it. Our Rust composer stands
/// in for ChatFrame1's handler, so it has to run first for the same reason.
#[test]
fn an_addons_handler_sees_the_line_already_in_the_window() {
let mut s = chat_vm();
let mut windows = super::frames::ChatWindows::default();
s.run(
r#"
SeenAtFireTime = -1
Spy = CreateFrame("Frame", "BenillaChatSpy")
Spy:SetScript("OnEvent", function()
SeenAtFireTime = ChatFrame1:GetNumMessages()
end)
BenillaChatSpy:RegisterEvent("CHAT_MSG_SAY")
"#,
)
.unwrap();
super::frames::route(&mut s, &mut windows, &ev(K::Say, "hi there", "Bob"));
assert_eq!(
s.eval::<i64>("return SeenAtFireTime").unwrap(),
1,
"the handler ran AFTER our window took the line, as registration order requires"
);
}
/// A player line fires `CHAT_MSG_SAY` with the reference's own arg positions — including the two
/// slots our doc comment used to omit (arg7, arg10), both numbers, both zero for a non-channel
/// line.
#[test]
fn a_say_line_fires_chat_msg_say_in_the_references_arg_positions() {
let mut s = chat_vm();
let mut windows = super::frames::ChatWindows::default();
s.run(SPY).unwrap();
s.run(r#"BenillaChatSpy:RegisterEvent("CHAT_MSG_SAY")"#)
.unwrap();
let mut e = ev(K::Say, "throm-ka", "Grunk");
e.language = "Orcish".into();
e.flag = "GM".into();
super::frames::route(&mut s, &mut windows, &e);
assert_eq!(s.eval::<String>("return SpyEvent").unwrap(), "CHAT_MSG_SAY");
// arg1 is the RAW body, not the composed line — the reference's Lua is what adds
// "%s says: " and the |Hplayer link, so an addon must see what the wire sent.
assert_eq!(
s.eval::<String>("return SpyLine").unwrap(),
"throm-ka|Grunk|Orcish|||GM|0|0||0"
);
}
/// A channel notice fires the **token** in arg1 (not the rendered line), the numbered display form
/// in arg4, and the three numeric slots the reference reads bare.
///
/// The last assertion runs `ChatFrame_OnEvent`'s own two comparisons — `arg7 > 0` and
/// `arg10 > 0` — inside the handler. Under Lua 5.0 a `nil` there raises, so this is the test that
/// would have caught passing nine args instead of ten.
#[test]
fn a_channel_notice_fires_its_token_and_the_reference_reads_arg7_and_arg10_bare() {
let mut s = chat_vm();
let mut windows = super::frames::ChatWindows::default();
s.run(SPY).unwrap();
s.run(
r#"
SpyZone, SpySuffix = nil, nil
BenillaChatSpy:SetScript("OnEvent", function()
SpyN = SpyN + 1
SpyEvent = event
SpyLine = arg1.."|"..arg2.."|"..arg3.."|"..arg4.."|"..arg5.."|"..arg6..
"|"..arg7.."|"..arg8.."|"..arg9.."|"..arg10
-- ChatFrame_OnEvent l.1379 and l.1421, verbatim shape.
if arg7 > 0 then SpyZone = arg7 end
if arg10 > 0 then SpySuffix = arg10 end
end)
BenillaChatSpy:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE")
"#,
)
.unwrap();
let mut e = ChatEvent::text_only(K::ChannelNotice, String::new());
e.notice = "2".into(); // YOU_JOINED
e.channel = "1. General - Elwynn Forest".into();
e.channel_base = "General - Elwynn Forest".into();
e.channel_number = 1;
e.zone_channel_id = 1; // ChatChannels.dbc General
super::frames::route(&mut s, &mut windows, &e);
assert_eq!(s.eval::<i64>("return SpyN").unwrap(), 1);
assert_eq!(
s.eval::<String>("return SpyEvent").unwrap(),
"CHAT_MSG_CHANNEL_NOTICE"
);
assert_eq!(
s.eval::<String>("return SpyLine").unwrap(),
"YOU_JOINED|||1. General - Elwynn Forest|||1|1|General - Elwynn Forest|0"
);
assert_eq!(s.eval::<i64>("return SpyZone").unwrap(), 1);
assert!(s.errors().is_empty(), "handler errors: {:?}", s.errors());
// ...and the window still shows the one composed line it always did.
assert_eq!(lines_in_window(&s), 1);
}
/// **MODE_CHANGE produces no chat event at all** — not a silent one.
///
/// This test replaced an earlier one that asserted the opposite (that a notice the UI renders
/// silently still reaches Lua, using MODE_CHANGE as the example). The byte-level carve settled it
/// the other way: `0x49c24d`, the `0x0C` arm of the notice jump table, calls `0x49e910` and
/// **returns** — it never reaches the fire (wow-re `chat-msg-event-args.md` §9). So the right
/// behaviour is what our feed already does: drop it before it becomes an event, which is what this
/// now asserts.
#[test]
fn a_mode_change_notice_never_becomes_an_event() {
use benilla_protocol::messages::{channel_notice, ChannelNoticeTail};
let mut log = super::feed::ChatLog::default();
log.push_channel_notice(
channel_notice::MODE_CHANGE,
"World".into(),
&ChannelNoticeTail::ModeChange {
guid: 42,
old_flags: 0,
new_flags: 1,
},
);
assert_eq!(
log.pending_len(),
0,
"MODE_CHANGE is dropped at the feed — the reference's 0x0C arm fires nothing"
);
// The control: a notice that DOES fire still gets queued, so the assertion above is about
// MODE_CHANGE and not about `push_channel_notice` being broken.
log.push_channel_notice(
channel_notice::YOU_JOINED,
"World".into(),
&ChannelNoticeTail::YouJoined { flags: 0 },
);
assert_eq!(log.pending_len(), 1);
}
/// A channel line whose channel we are **not** in leaves all four channel slots empty — arg4 falls
/// back to the bare name and arg7/arg8/arg9 stay `0/0/""`. They are one record in the reference
/// (`slot+0x00/+0x04/+0x94/+0x98`), so they are one record here (`chat-msg-event-args.md` §§4, 7-10).
#[test]
fn a_channel_we_are_not_in_fires_the_bare_name_and_zeroes() {
let mut s = chat_vm();
let mut windows = super::frames::ChatWindows::default();
s.run(SPY).unwrap();
s.run(r#"BenillaChatSpy:RegisterEvent("CHAT_MSG_CHANNEL")"#)
.unwrap();
// An empty joined list: nothing is in the local channel record array.
let channels = super::edit::ChannelState::default();
let mut e = ev(K::Channel, "wts boar livers", "Bob");
e.channel = "SomeoneElsesChannel".into();
channels.stamp_channel(&mut e);
super::frames::route(&mut s, &mut windows, &e);
assert_eq!(
s.eval::<String>("return SpyLine").unwrap(),
"wts boar livers|Bob||SomeoneElsesChannel|||0|0||0",
"arg4 keeps the bare INCOMING name (the miss leg still has one); arg7/8/9/10 are the \
record we do not have, so 0/0/\"\"/0"
);
}
/// `stamp_channel` splits the wire's bare name into the reference's arg4/arg8/arg9 trio: the
/// display form gets the number prefix, arg9 never does.
#[test]
fn stamping_a_channel_splits_the_display_form_from_the_base_name() {
let mut channels = super::edit::ChannelState::default();
channels.joined.push("World".into());
channels.joined.push("General - Elwynn Forest".into());
let mut e = ev(K::Channel, "wts boar livers", "Bob");
e.channel = "General - Elwynn Forest".into();
channels.stamp_channel(&mut e);
assert_eq!(e.channel, "2. General - Elwynn Forest"); // arg4
assert_eq!(e.channel_number, 2); // arg8
assert_eq!(e.channel_base, "General - Elwynn Forest"); // arg9, " - Zone" tail intact
// A channel we are not in keeps its bare name in arg4 and leaves the whole rest of the record
// empty — the reference's miss leg `0x49aa86`, where there is no local record to read
// `slot+0x04/+0x94/+0x98` out of at all.
let mut other = ev(K::Channel, "hi", "Bob");
other.channel = "SomeoneElsesChannel".into();
channels.stamp_channel(&mut other);
assert_eq!(other.channel, "SomeoneElsesChannel"); // arg4: the bare incoming name
assert_eq!(other.channel_number, 0); // arg8
assert_eq!(other.channel_base, ""); // arg9 — NOT the name
assert_eq!(other.zone_channel_id, 0); // arg7
}
/// Every notice byte the composer renders has a token to fire, and vice versa — the two tables are
/// the same set by assertion rather than by good intentions (they are read off the same
/// `CHAT_<X>_NOTICE` GlobalStrings keys).
#[test]
fn every_rendered_notice_has_a_token() {
for byte in 0x00u8..=0x1F {
let mut e = ChatEvent::text_only(K::ChannelNotice, String::new());
e.channel = "World".into();
e.notice = byte.to_string();
let rendered = super::frames::compose_notice(&e).is_some();
let token = super::event::notice_token(byte).is_some();
assert_eq!(
rendered, token,
"notice {byte:#04x}: renders={rendered} but token={token}"
);
}
}
/// The `ALL` sweep list really is every variant. A new kind fails [`super::event::event_name`]'s
/// exhaustive match at compile time; this is what makes you add it to `ALL` as well.
#[test]
fn every_kind_is_in_all() {
let mut seen: Vec<&str> = K::ALL
.iter()
.map(|&k| super::event::event_name(k))
.collect();
let before = seen.len();
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), before, "a kind is listed twice in ALL");
assert_eq!(before, 36, "36 kinds — update this when the kind set grows");
}
#[test]
fn colors_match_the_shipped_table() {
assert_eq!(default_color(K::Say), [255, 255, 255]);
+25
View File
@@ -106,6 +106,7 @@ fn standing_label(rank: u32) -> &'static str {
/// `0x506f70`; its bare name otherwise), the skill requirement's name off `SkillLine.dbc`, the
/// reputation requirement off `Faction.dbc` names (the red check is the engine's, against the
/// player's rank map).
#[allow(clippy::too_many_arguments)] // one app-resolved catalog per argument, by design
fn template_view(
t: &ItemInfo,
spells: Option<&crate::ui_action::Spells>,
@@ -113,6 +114,8 @@ fn template_view(
home_area: Option<&str>,
factions: Option<&benilla_formats::FactionCatalog>,
sub_classes: Option<&benilla_formats::ItemSubClassCatalog>,
classes: Option<&benilla_formats::ItemClassCatalog>,
icons: Option<&ItemDisplays>,
) -> benilla_ui::script::ItemTemplateView {
let spell_name = |id: u32| -> Option<String> {
spells
@@ -128,9 +131,25 @@ fn template_view(
inventory_type: t.inventory_type,
proficiency_alt: sub_classes.and_then(|c| c.proficiency_alt(t.class, t.subclass)),
hide_subclass: sub_classes.is_some_and(|c| c.hides_name(t.class, t.subclass)),
// `GetItemInfo`'s type pair. The subclass spelling is `name()` — VerboseName first —
// because that is the binding's own two-step (`0x48e311`), and it is deliberately NOT the
// `display_name()` the tooltip's type cell prints: a one-handed sword is "One-Handed
// Swords" to an addon and "Sword" on the tooltip. Both spellings are in the same row.
item_type: classes.and_then(|c| c.name(t.class)).map(str::to_string),
item_sub_type: sub_classes
.and_then(|c| c.name(t.class, t.subclass))
.map(str::to_string),
flags: t.flags,
bonding: t.bonding,
max_count: t.max_count,
// The stack size — a DIFFERENT wire field from `max_count` (the account-wide unique cap).
// Linen Cloth is `max_count 0, stackable 20`; `GetItemInfo`'s `itemStackCount` is this one.
stackable: t.stackable,
// The same `ItemDisplayInfo.dbc` icon the bag slots resolve, as a ready
// `Interface\Icons\…` path — `GetItemInfo`'s `itemTexture`.
icon: icons
.and_then(|i| i.catalog.get(t.display_info_id))
.and_then(|d| d.icon.clone()),
start_quest: t.start_quest,
container_slots: t.container_slots,
stats: t.stats.clone(),
@@ -281,6 +300,10 @@ pub(super) fn feed_item_stats(
area_names: Option<Res<crate::ui_quest_log::QuestHeaderNamesRes>>,
factions: Option<Res<crate::target::Factions>>,
sub_classes: Option<Res<super::ItemSubClasses>>,
// `GetItemInfo`'s `itemType` and `itemTexture`: the class-name table, and the same
// ItemDisplayInfo icons the bag slots already resolve through.
classes: Option<Res<super::ItemClasses>>,
icons: Option<Res<ItemDisplays>>,
mut pending: Local<std::collections::HashSet<u32>>,
mut last_home: Local<Option<String>>,
) {
@@ -322,6 +345,8 @@ pub(super) fn feed_item_stats(
home_area.as_deref(),
factions.as_deref().map(|f| f.catalog()),
sub_classes.as_deref().map(|s| &s.0),
classes.as_deref().map(|c| &c.0),
icons.as_deref(),
);
script.set_item_template(id, view);
}
+12
View File
@@ -926,6 +926,11 @@ pub(crate) struct ItemSubClasses(pub(crate) benilla_formats::ItemSubClassCatalog
#[derive(Resource)]
pub(crate) struct ItemBagFamilies(pub(crate) benilla_formats::ItemBagFamilyCatalog);
/// The ItemClass.dbc catalog — what an item's class is *called* ("Weapon", "Container"),
/// i.e. `GetItemInfo`'s `itemType`. See [`feed`]'s template resolve.
#[derive(Resource)]
pub(crate) struct ItemClasses(pub(crate) benilla_formats::ItemClassCatalog);
/// Startup (after the MPQ chain opens): the item-tooltip DBCs. On failure a resource is simply
/// absent — set items render without their SET block, subclass gates read as absent.
fn load_item_dbcs(mut commands: Commands, world_assets: Option<Res<benilla_assets::WorldAssets>>) {
@@ -948,6 +953,13 @@ fn load_item_dbcs(mut commands: Commands, world_assets: Option<Res<benilla_asset
}
Err(e) => warn!("ui_items: ItemSubClass.dbc failed to load: {e:#}"),
}
match benilla_formats::load_item_classes(&mut chain) {
Ok(cat) => {
info!("ui_items: ItemClass.dbc loaded ({} classes)", cat.len());
commands.insert_resource(ItemClasses(cat));
}
Err(e) => warn!("ui_items: ItemClass.dbc failed to load: {e:#}"),
}
match benilla_formats::load_item_bag_families(&mut chain) {
Ok(cat) => {
info!(
+1 -1
View File
@@ -1,7 +1,7 @@
//! The connection-telemetry feed — the app side of `benilla_ui::script::net_stats`' seam.
//!
//! One number: the latency `GetNetStats()` reports, which is the main bar's performance ("ping")
//! meter's whole input (`ActionBar.xml`'s `BenillaPerformanceBarFrame` polls it every 10 s and
//! meter's whole input (`ActionBar.xml`'s `MainMenuBarPerformanceBarFrame` polls it every 10 s and
//! colors the bar green/yellow/red). The measurement itself belongs to the net drain — every
//! `SMSG_PONG` lands a round trip in [`NetStatus::rtt_ring`], the reference's 16-deep RTT history —
//! and this feed only carries its average across the engine boundary (decision 0068 §3: the engine
+238 -10
View File
@@ -4,10 +4,10 @@
//! composer live in the parent module; this file owns only the per-frame bridge.
use benilla_protocol::messages::{
member_status, GroupLootInfo, GroupMemberEntry, PartyMemberStatsInfo,
member_status, GroupLootInfo, GroupMemberEntry, PartyMemberStatsInfo, GROUP_MEMBER_ASSISTANT,
};
use benilla_ui::script::{
PartyMemberInfo, PartyRequest, PartyState, ScriptValue, UiScript, UnitState,
PartyMemberInfo, PartyRequest, PartyState, RaidMemberInfo, ScriptValue, UiScript, UnitState,
};
use bevy::prelude::*;
@@ -31,10 +31,19 @@ pub(super) struct FedParty {
const PARTY_TOKENS: [&str; 4] = ["party1", "party2", "party3", "party4"];
/// `GROUPTYPE_RAID` — `SMSG_GROUP_LIST`'s first byte (`0` party, `1` raid; vmangos `Group.h:116`).
const GROUPTYPE_RAID: u8 = 1;
/// The subgroup index in a member's flags byte — bits 0-2 (`GroupMemberEntry::flags`'s own doc;
/// the assistant bit `0x80` is its neighbour, and the `0x7f` mask `party_slots` uses is a
/// different question, "same subgroup AND same assistant state").
const GROUP_MEMBER_SUBGROUP: u8 = 0x07;
/// Push the roster snapshot + the `party1..party4` unit snapshots into the VM and fire the party
/// events on their edges. The per-member unit state is the 0434 §2 **merged view**: a streamed
/// member's live descriptor wins; the `PARTY_MEMBER_STATS` snapshot covers the rest — and the
/// roster status byte overlays both (the descriptor never carries connected/AFK/DND).
#[allow(clippy::too_many_arguments)] // a Bevy system's param list IS its dependency set
pub(super) fn feed_party(
script: Option<NonSendMut<UiScript>>,
group: Res<GroupState>,
@@ -42,6 +51,13 @@ pub(super) fn feed_party(
stores: Query<&ObjectStore>,
self_q: Query<(&Guid, &ObjectStore), With<SelfPlayer>>,
factions: Option<Res<crate::target::Factions>>,
names: Res<NameCache>,
areas: Option<Res<crate::area::AreaTableRes>>,
// The leaf area under us, through the SAME accessor `crate::area`'s zone-text resolver uses.
// Deliberately not `terrain_stream::CurrentArea` directly: that item is named today only by
// the instruments, and naming it from a game module would push it across the world-API wall
// (`tests/world_api_wall.rs`, decision 1164) for a value this already answers.
here: benilla_world::world_point::WorldPoint,
mut fed: Local<FedParty>,
) {
let Some(mut script) = script else {
@@ -59,9 +75,10 @@ pub(super) fn feed_party(
// the 0440 byte law) — in a plain party this is simply the roster.
let slots: Vec<&GroupMemberEntry> = group.party_slots().collect();
// The roster-level snapshot (leader/master indices on the Lua scale: 0 = the player). A
// raid's other subgroups feed only the count (the engine contract's v1 gap; a leader in
// another subgroup reads as index 0 until the raid UI slice).
// The roster-level snapshot (leader/master indices on the Lua scale: 0 = the player). These
// indices are the PARTY view — our own subgroup's four slots — so in a raid a leader in
// another subgroup still reads as index 0 here; the whole-raid answer is `raid` below, which
// `GetRaidRosterInfo` reads and which does carry every subgroup.
let members: Vec<PartyMemberInfo> = slots
.iter()
.map(|m| PartyMemberInfo {
@@ -100,14 +117,31 @@ pub(super) fn feed_party(
}
None => ("group".to_string(), None, 0),
};
// The raid roster — `GetNumRaidMembers`/`GetRaidRosterInfo`/`UnitInRaid` all read this one
// list, so the count can never disagree with the array the way it would if we kept both.
let me = self_pair.map(|(g, store)| RaidSelf {
guid: g.0,
flags: group.own_flags,
level: store.0.unit_level().unwrap_or(0),
area: here.area(),
dead: store.0.unit_is_dead(),
});
let zone_name = |area: u32| {
let areas = areas.as_ref()?;
// The wire's per-member `zone` is already a top-level zone id; our own `CurrentArea` is
// the finest MCNK leaf, so it needs the parent walk first. `top_zone` is idempotent on a
// row that is already a zone, which is what lets one resolver serve both.
areas
.0
.name(areas.0.top_zone(area).unwrap_or(area))
.map(str::to_string)
};
let raid = raid_roster(&group, me.as_ref(), &names, &zone_name);
script.set_party(PartyState {
members,
leader_index,
raid_members: if group.group_type == 1 {
group.members.len() as u32 + 1
} else {
0
},
raid,
loot_method,
master_looter,
loot_threshold,
@@ -160,6 +194,116 @@ pub(super) fn feed_party(
}
}
/// The player's own raid row — the one entry `GroupState` cannot supply, because
/// `SMSG_GROUP_LIST` never lists the recipient.
pub(super) struct RaidSelf {
pub(super) guid: u64,
/// Our own subgroup bits + the assistant flag, `SMSG_GROUP_LIST`'s second byte.
pub(super) flags: u8,
pub(super) level: u32,
/// The finest area under us (`WorldPoint::area()`), walked to a zone by the caller's resolver
/// — the reference reads a zone-id global here (`0xb4e314`) on the live-object arm.
pub(super) area: Option<u32>,
/// `UNIT_FIELD_HEALTH <= 0` — the reference's live-object arm for return 9, and *only* that:
/// deliberately `unit_is_dead` rather than `unit_reads_dead`, so a feigning hunter does not
/// take the arm a health test would not.
pub(super) dead: bool,
}
/// Build `GetRaidRosterInfo`'s array (decision 0434 §6's roster, wow-re
/// `ui/scratch/raid-roster-bindings.md` §2). Empty outside a raid — `GroupState::group_type` is
/// `1` only for one — which is what makes `GetNumRaidMembers()` answer `0` in a party.
///
/// **The player is row 1.** The reference's array contains the local player (it is why
/// `UnitInRaid("player")` answers `1`), and the wire's list does not, so the recipient is spliced
/// back in here. *Where* the real client puts itself is **not derived** — the note carves the
/// binding, not the array's fill order — and nothing observed depends on it: every corpus consumer
/// sweeps `1..GetNumRaidMembers()` (or `1..MAX_RAID_MEMBERS`) and keys the result by name.
///
/// A pure function over plain data so the shape is testable without a second account in a raid:
/// the tuple this fills is the part a live raid could confirm and a unit test cannot, but the
/// *mapping* — rank, subgroup, the online/dead bit pair — is the part that can be, and is below.
fn raid_roster(
group: &GroupState,
me: Option<&RaidSelf>,
names: &NameCache,
zone_name: &dyn Fn(u32) -> Option<String>,
) -> Vec<RaidMemberInfo> {
if group.group_type != GROUPTYPE_RAID {
return Vec::new();
}
// rank: 2 leader · 1 assistant · 0 member. The binding exposes `[edi+0xc]` unadjusted and
// wow-re could not derive the convention from its bytes; the corpus can and does —
// `ChatLog.lua:351-353` prints `@` for 2 and `*` for 1.
let rank_of = |guid: u64, flags: u8| {
if guid == group.leader {
2
} else if flags & GROUP_MEMBER_ASSISTANT != 0 {
1
} else {
0
}
};
let row = |guid: u64,
name: String,
flags: u8,
level: u32,
zone: Option<String>,
online: bool,
ninth: bool| {
let class = names
.player_traits(guid)
.and_then(|(_, class, _)| crate::ui_unit::class_names(class));
RaidMemberInfo {
name,
guid,
rank: rank_of(guid, flags),
// Stored 0-based; the binding adds the `0x4bb61a inc`.
subgroup: u32::from(flags & GROUP_MEMBER_SUBGROUP),
level,
class: class.map(|(n, _)| n.to_string()),
class_file: class.map(|(_, f)| f.to_string()),
zone,
online,
ninth,
}
};
let mut roster = Vec::with_capacity(group.members.len() + 1);
if let Some(me) = me {
roster.push(row(
me.guid,
// An unresolved name takes the reference's name-cache-miss arm at the binding: the
// whole 9-tuple becomes the fixed miss tuple, not a half-filled row.
names.peek(me.guid).unwrap_or_default().to_string(),
me.flags,
me.level,
me.area.and_then(zone_name),
true,
me.dead,
));
}
for m in &group.members {
let stats = group.stats.get(&m.guid);
let online = m.status & member_status::ONLINE != 0;
roster.push(row(
m.guid,
m.name.clone(),
m.flags,
stats.and_then(|s| s.level).map_or(0, u32::from),
stats
.and_then(|s| s.zone)
.and_then(|z| zone_name(u32::from(z))),
online,
// The reference's cached arm: `[edi+0x18]` must carry BOTH `0x4` and `0x1`. Our wire
// spells those `member_status::DEAD` and `ONLINE` (vmangos `Group.cpp:45-63`), which
// is the corroboration recorded on `RaidMemberInfo::ninth` — an offline dead member
// answers nil there, and reproducing the conjunction is the point.
online && m.status & member_status::DEAD != 0,
));
}
roster
}
/// One member's merged-view unit snapshot (see [`feed_party`]).
fn member_unit_state(
m: &GroupMemberEntry,
@@ -617,4 +761,88 @@ mod tests {
group.apply_list(0, 0, vec![], 0x123, None);
assert!(!group.test, "the real wire always wins");
}
/// The raid roster mapping — the half a unit test can actually settle. The nine-value *shape*
/// is pinned in `benilla-ui`'s own tests; what is pinned here is which wire fact lands in
/// which slot, because that is what a live raid would otherwise be the only witness to.
#[test]
fn the_raid_roster_maps_the_wire_to_get_raid_roster_info() {
let mut names = NameCache::default();
names.insert_player(0x5E1F, "Me".into(), Some((4, 11, 0))); // Night Elf DRUID
names.insert_player(0xA11CE, "Alice".into(), Some((1, 1, 1))); // Human WARRIOR
names.insert_player(0xB0B, "Bob".into(), None); // no traits yet
let member = |guid: u64, name: &str, status: u8, flags: u8| GroupMemberEntry {
name: name.into(),
guid,
status,
flags,
};
let mut group = GroupState {
group_type: GROUPTYPE_RAID,
own_flags: 0,
leader: 0x5E1F,
members: vec![
// Alice: subgroup 2 (stored), an assistant, online and DEAD.
member(
0xA11CE,
"Alice",
member_status::ONLINE | member_status::DEAD,
2 | GROUP_MEMBER_ASSISTANT,
),
// Bob: subgroup 0, offline — and offline-AND-dead, which must NOT set return 9.
member(0xB0B, "Bob", member_status::DEAD, 0),
],
..Default::default()
};
group.stats.insert(
0xA11CE,
PartyMemberStatsInfo {
level: Some(58),
zone: Some(1537),
..Default::default()
},
);
let zone = |id: u32| (id == 1537).then(|| "Ironforge".to_string());
let me = RaidSelf {
guid: 0x5E1F,
flags: 0,
level: 60,
area: Some(1537),
dead: false,
};
let roster = raid_roster(&group, Some(&me), &names, &zone);
assert_eq!(roster.len(), 3, "the player is spliced back in");
// Row 1 — us. Leader (rank 2), subgroup stored 0, class from our own name-query traits.
assert_eq!(roster[0].name, "Me");
assert_eq!(roster[0].rank, 2);
assert_eq!(
roster[0].subgroup, 0,
"stored 0-based; the BINDING adds one"
);
assert_eq!(roster[0].level, 60);
assert_eq!(roster[0].class_file.as_deref(), Some("DRUID"));
assert_eq!(roster[0].zone.as_deref(), Some("Ironforge"));
assert!(roster[0].online && !roster[0].ninth);
// Row 2 — an assistant, and the online+dead pair the reference's cached arm tests.
assert_eq!((roster[1].rank, roster[1].subgroup), (1, 2));
assert_eq!(roster[1].level, 58);
assert_eq!(roster[1].class_file.as_deref(), Some("WARRIOR"));
assert!(roster[1].online && roster[1].ninth, "online AND dead");
// Row 3 — offline. Return 9 needs BOTH bits, so a dead-but-offline member is nil there,
// and an unresolved class is nil rather than a guess.
assert!(!roster[2].online, "offline");
assert!(!roster[2].ninth, "0x4 without 0x1 is not the arm");
assert_eq!(roster[2].class, None);
assert_eq!(roster[2].zone, None, "no stats packet, no zone");
// A PARTY is not a raid: the whole list is empty, so `GetNumRaidMembers()` answers 0
// while `IsRaidLeader()` still answers 1 — the pair that surprises people.
group.group_type = 0;
assert!(raid_roster(&group, Some(&me), &names, &zone).is_empty());
}
}
+3 -3
View File
@@ -73,13 +73,13 @@ pub(crate) struct PetFamilyTables {
/// the codebase's own "push before firing" rule (`crate::ui_unit`), which held *inside* every
/// feed but not *between* them.
///
/// It cost the Pet tab. `BenillaPetTab_Update` is edge-driven off `UNIT_PET` / `PET_BAR_UPDATE`
/// It cost the Pet tab. `PetTab_Update` is edge-driven off `UNIT_PET` / `PET_BAR_UPDATE`
/// and its whole predicate is `HasPetUI()`, which is this snapshot. With the three pet feeds
/// merely unordered inside [`UnitFeed`], a cold first summon fired both edges while `HasPetUI()`
/// still answered "no pet"; the push landed after, and since neither edge repeats, the tab stayed
/// down for the rest of the session. Measured live, frame by frame: both events on frame 440 with
/// `HasPetUI()=(nil,nil)`, the flip to `(1,nil)` on 441, and the tab still hidden 48 s later —
/// while calling `BenillaPetTab_Update()` by hand raised it instantly.
/// while calling `PetTab_Update()` by hand raised it instantly.
///
/// A set rather than a pair of `.before()` calls because the constraint belongs to the *snapshot*,
/// not to today's two consumers: anything that later fires a pet event inherits it by ordering
@@ -679,7 +679,7 @@ mod tests {
/// ordering does. Driven through **one** `app.update()` on a cold pet — the exact live shape,
/// where `feed_pet_unit` sees the guid appear and fires while `feed_pet_stats` has or has not
/// yet pushed. A handler that reads `HasPetUI()` at that instant is what the shipped
/// `BenillaPetTab_Update` is, minus the frames.
/// `PetTab_Update` is, minus the frames.
#[test]
fn unit_pet_reaches_lua_with_has_pet_ui_already_true() {
use crate::char_select::ClientState;
+10 -26
View File
@@ -28,14 +28,16 @@ use bevy::prelude::*;
use benilla_ui::script::UiScript;
use crate::char_select::ClientState;
pub(crate) struct UiSavedPlugin;
impl Plugin for UiSavedPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnExit(ClientState::InWorld), save_on_session_end)
.add_systems(Update, save_on_exit);
fn build(&self, _app: &mut App) {
// **The write is no longer this plugin's own edge.** It is one step of the ordered
// shutdown tail in [`crate::ui_script::shutdown_ui_state`], because the reference has
// exactly one shutdown (`0x490bd0`) reached from five roots and its steps are ordered
// against each other: `PLAYER_LOGOUT` fires before any write, the flat file is written
// before the per-addon files, and `AddOns.txt` is last. Three independent systems on the
// same edge cannot express that.
}
}
@@ -70,11 +72,12 @@ pub(crate) fn load_saved_variables(script: &mut UiScript) {
script.fire_event("VARIABLES_LOADED", vec![]);
}
/// Write the file from the live globals. No-op when nothing is registered — the UI never loaded
/// Write the flat file from the live globals — step 3 of the shutdown tail
/// ([`crate::ui_script::shutdown_ui_state`]). No-op when nothing is registered — the UI never loaded
/// (a glue-only run, a capture), and an empty write would be a wipe rather than a save. The
/// reference *does* delete the file when an addon declares nothing, which is a different case:
/// there, the addon loaded and its declaration list is genuinely empty.
fn save(script: &mut UiScript) {
pub(crate) fn save(script: &mut UiScript) {
let names = script.saved_variable_names();
if names.is_empty() {
return;
@@ -103,25 +106,6 @@ const HEADER: &str = "\
-- Written at logout/exit from the live values; executed as a Lua chunk at UI load.
";
/// `OnExit(InWorld)`: a `/logout` back to the glue, or a disconnect — the reference's
/// logout-to-character-select and disconnect roots.
fn save_on_session_end(script: Option<NonSendMut<UiScript>>) {
if let Some(mut script) = script {
save(&mut script);
}
}
/// `AppExit`: quitting the client — the reference's quit / application-exit roots. Reads the
/// message rather than a state edge because a quit from in-world never leaves `InWorld`.
fn save_on_exit(script: Option<NonSendMut<UiScript>>, mut exits: MessageReader<AppExit>) {
if exits.read().next().is_none() {
return;
}
if let Some(mut script) = script {
save(&mut script);
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -94,11 +94,11 @@ fn shipped_action_bar_drives_end_to_end() {
s.mouse_button(26.0, 22.0, "LeftButton", false);
assert_eq!(s.take_action_uses(), vec![73]);
// The keybinding entry (the app's key feed runs `BenillaActionButtonDown/Up(i)` on the two
// The keybinding entry (the app's key feed runs `ActionButtonDown/Up(i)` on the two
// key edges — the ref's ACTIONBUTTONn binding, ActionButton.lua:15-45): UP fires UseAction
// directly with no checkCursor (a keybind never places, decision 0216 §7) — but ONLY from
// the PUSHED state a DOWN set, so a stray release with no press is the ref's own no-op.
s.run("BenillaActionButtonUp(2)").unwrap();
s.run("ActionButtonUp(2)").unwrap();
assert!(
s.take_action_uses().is_empty(),
"an Up without a Down is a no-op (the PUSHED gate)"
@@ -112,14 +112,14 @@ fn shipped_action_bar_drives_end_to_end() {
})
.count()
};
s.run("BenillaActionButtonDown(2)").unwrap();
s.run("ActionButtonDown(2)").unwrap();
assert_eq!(depressed(&s), 1, "key DOWN shows the pushed texture");
assert_eq!(
s.eval::<String>("return BenillaActionButton2:GetButtonState()")
s.eval::<String>("return ActionButton2:GetButtonState()")
.unwrap(),
"PUSHED"
);
s.run("BenillaActionButtonUp(2)").unwrap();
s.run("ActionButtonUp(2)").unwrap();
assert_eq!(s.take_action_uses(), vec![74], "key '2' fires action 74");
assert_eq!(depressed(&s), 0, "key UP restores the normal state");
@@ -212,9 +212,7 @@ fn state_feedback_drives_cooldown_checked_and_usable_through_the_xml() {
}),
);
s.fire_event("ACTIONBAR_UPDATE_STATE", vec![]);
assert!(s
.eval::<bool>("return BenillaActionButton1:GetChecked()")
.unwrap());
assert!(s.eval::<bool>("return ActionButton1:GetChecked()").unwrap());
// The OOM blue tint (the transcribed UpdateUsable): usable=false + notEnoughMana=true.
s.set_action_state(
@@ -442,7 +440,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
s.resolve();
s.run(r#"LOCK_ACTIONBAR = "1""#).unwrap();
s.run("BenillaActionButton_OnDragStart(BenillaActionButton1)")
s.run("BenillaActionButton_OnDragStart(ActionButton1)")
.unwrap();
assert!(
s.cursor_payload().is_none(),
@@ -468,7 +466,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
);
// The receiving end is guarded too: the held action cannot be dropped back by a drag…
s.run("BenillaActionButton_OnReceiveDrag(BenillaActionButton1)")
s.run("BenillaActionButton_OnReceiveDrag(ActionButton1)")
.unwrap();
assert!(
s.cursor_payload().is_some(),
@@ -476,7 +474,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
);
// …and unlocking makes both ends live again.
s.run(r#"LOCK_ACTIONBAR = "0""#).unwrap();
s.run("BenillaActionButton_OnReceiveDrag(BenillaActionButton1)")
s.run("BenillaActionButton_OnReceiveDrag(ActionButton1)")
.unwrap();
assert!(s.cursor_payload().is_none(), "unlocked, the drop lands");
assert_eq!(
@@ -614,10 +612,8 @@ fn count_fontstring_follows_is_consumable_action_not_the_bag_count() {
// Read the fontstrings by name, not by scanning painted text: a single-character count
// ("0") is indistinguishable from a neighbouring button's static HotKey label by content.
let count_of = |s: &UiScript, n: u32| {
s.eval::<String>(&format!(
"return BenillaActionButton{n}Count:GetText() or \"\""
))
.unwrap()
s.eval::<String>(&format!("return ActionButton{n}Count:GetText() or \"\""))
.unwrap()
};
assert_eq!(
count_of(&s, 1),
@@ -670,7 +666,7 @@ fn shipped_bag_frame_drives_end_to_end() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The bag toggle now seats on the main action bar (BenillaActionBarArtFrame, ActionBar.xml);
// The bag toggle now seats on the main action bar (MainMenuBarArtFrame, ActionBar.xml);
// load the bar FIRST so the toggle's cross-file relativeTo resolves to the real art frame at
// SetPoint time. The app loads ActionBar before BagFrame (load_default_ui order); without it the
// anchor would silently fall back to the screen root (it only lands right here by the 1024-wide
@@ -0,0 +1,360 @@
//! The shipped `assets/ui/AddonList.xml` — the in-game AddOns list (decision 1197).
//!
//! **The window is the test of the API as much as the API is the test of the window.** This panel
//! is written entirely against the eleven AddOn verbs 1191 landed — `GetNumAddOns`,
//! `GetAddOnInfo`, `GetAddOnDependencies`, `EnableAddOn`, `DisableAddOn`, `EnableAllAddOns`,
//! `DisableAllAddOns` — with no host support of its own. So driving it drives them, through the
//! same Lua an addon author would write, which is worth more than a Rust test of each verb.
//!
//! What these guard: the row painter's three-colour rule and its status column against a registry
//! with a real dependency graph; the checkbox round-trip through `EnableAddOn`/`DisableAddOn`;
//! Cancel restoring what the panel found (`ResetAddOns` written out, since that verb is
//! glue-namespace and absent in-world); the scroll clamp; and the Enable All / Disable All pair.
use benilla_ui::script::{AddOnInfo, UiScript};
/// Fonts + the panel, and a registry with a shape worth painting: one plain addon, a library, a
/// dependent on that library, and one whose dependency is not installed.
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"Fonts.xml",
"UiPanels.xml",
"GameTooltip.xml",
"AddonList.xml",
] {
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets/ui")
.join(file),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(
report.errors.is_empty(),
"{file}: loader errors: {:?}",
report.errors
);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
let info = |name: &str, title: &str, deps: &[&str]| AddOnInfo {
name: name.into(),
title: Some(title.into()),
notes: Some(format!("{name}'s notes")),
url: None,
secure: false,
load_on_demand: false,
dependencies: deps.iter().map(|d| (*d).to_string()).collect(),
directives: Default::default(),
files: Vec::new(),
saved_variables: Vec::new(),
saved_variables_per_character: Vec::new(),
enabled: true,
loaded: false,
};
s.register_addons(
vec![
info("Plain", "Plain Addon", &[]),
info("Lib", "The Library", &[]),
info("Needs", "Needs The Library", &["Lib"]),
info("Orphan", "Orphaned", &["Nowhere"]),
],
None,
None,
None,
);
s
}
/// The text of row `i`'s title and status, as the panel painted them.
fn row(s: &UiScript, i: usize) -> (String, String) {
let title = s
.eval::<String>(&format!("return AddonListEntry{i}Title:GetText() or ''"))
.unwrap();
let status = s
.eval::<String>(&format!("return AddonListEntry{i}Status:GetText() or ''"))
.unwrap();
(title, status)
}
/// The panel paints the registry: title (`## Title` else the folder name), the reference's status
/// token, and a checkbox per row — all through `GetAddOnInfo`.
#[test]
fn the_panel_paints_the_registry_through_the_addon_api() {
let s = harness();
s.run("AddonList_Show()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s.eval::<bool>("return AddonListFrame:IsVisible()").unwrap());
assert_eq!(
row(&s, 1).0,
"Plain Addon",
"`## Title` wins over the folder"
);
assert_eq!(row(&s, 3).0, "Needs The Library");
// Rows past the registry are hidden, not left showing a stale addon.
assert!(
!s.eval::<bool>("return AddonListEntry5:IsVisible()")
.unwrap(),
"row 5 has no addon behind it"
);
// Four addons is well under MAX_ADDONS_DISPLAYED, so the scroll pair stays hidden — the
// reference's own GlueScrollFrame_Update condition.
assert!(!s
.eval::<bool>("return AddonListScrollUp:IsVisible()")
.unwrap());
// A dependency that is not installed is a status, not a checkbox state.
assert_eq!(
row(&s, 4).1,
"DEP_MISSING",
"the reference's own reason token, shown when GlobalStrings has no ADDON_ string for it"
);
}
/// The checkbox round-trips through `EnableAddOn`/`DisableAddOn`, and the effect on a *dependent*
/// shows up in its status column — which is the whole reason the panel re-reads rather than
/// caching.
#[test]
fn a_checkbox_click_disables_through_the_api_and_the_dependent_says_so() {
let s = harness();
s.run("AddonList_Show()").unwrap();
// Row 2 is the library. Click its checkbox the way the XML does.
s.run("AddonList_ToggleEntry(2)").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(row(&s, 2).1, "DISABLED", "the player turned it off");
assert_eq!(
row(&s, 3).1,
"DEP_DISABLED",
"its dependent reports the reference's dependency token, not a stale clean row"
);
assert!(
!s.eval::<bool>("return AddonListEntry2Check:GetChecked()")
.unwrap(),
"and the checkbox follows the registry rather than its own memory"
);
// Clicking again re-enables, and the dependent recovers.
s.run("AddonList_ToggleEntry(2)").unwrap();
assert_eq!(row(&s, 2).1, "");
assert_eq!(row(&s, 3).1, "");
}
/// **Cancel restores what the panel found** — the reference's `ResetAddOns`, written out in Lua
/// because that verb is glue-namespace and deliberately absent in-world (1191 §6).
#[test]
fn cancel_restores_the_state_the_panel_opened_with() {
let s = harness();
s.run("AddonList_Show()").unwrap();
s.run("AddonList_ToggleEntry(1) AddonList_ToggleEntry(2)")
.unwrap();
assert_eq!(row(&s, 1).1, "DISABLED");
s.run("AddonList_OnCancel()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(!s.eval::<bool>("return AddonListFrame:IsVisible()").unwrap());
// Re-open: nothing the cancelled session did survived.
s.run("AddonList_Show()").unwrap();
assert_eq!(row(&s, 1).1, "");
assert_eq!(row(&s, 2).1, "");
}
/// Okay keeps the edit — the other half of the pair, and the one that would silently pass if
/// `AddonList_OnOk` accidentally called the restore loop.
#[test]
fn okay_keeps_the_edit() {
let s = harness();
s.run("AddonList_Show()").unwrap();
s.run("AddonList_ToggleEntry(1)").unwrap();
s.run("AddonList_OnOk()").unwrap();
assert!(!s.eval::<bool>("return AddonListFrame:IsVisible()").unwrap());
s.run("AddonList_Show()").unwrap();
assert_eq!(row(&s, 1).1, "DISABLED", "Okay is not a disguised Cancel");
}
/// Enable All / Disable All go through the API's own pair, and the panel repaints.
#[test]
fn enable_all_and_disable_all_move_every_row() {
let s = harness();
s.run("AddonList_Show()").unwrap();
s.run("AddonList_SetAll(nil)").unwrap();
for i in 1..=4 {
assert_eq!(row(&s, i).1, "DISABLED", "row {i} after Disable All");
}
s.run("AddonList_SetAll(1)").unwrap();
assert_eq!(row(&s, 1).1, "");
assert_eq!(
row(&s, 4).1,
"DEP_MISSING",
"Enable All cannot fix a dependency that is not installed — the status is not a checkbox"
);
}
/// The scroll offset clamps at both ends, and a list that fits never scrolls.
#[test]
fn the_scroll_offset_clamps() {
let s = harness();
s.run("AddonList_Show()").unwrap();
s.run("AddonList_Scroll(5)").unwrap();
assert_eq!(
s.eval::<i64>("return AddonList.offset").unwrap(),
0,
"four addons fit in nineteen rows — there is nothing to scroll"
);
s.run("AddonList_Scroll(-5)").unwrap();
assert_eq!(s.eval::<i64>("return AddonList.offset").unwrap(), 0);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **The ESC-menu path, end to end** — the director's ask stated as a test: open the game menu,
/// click AddOns, and the list is up with the menu gone.
///
/// Loaded in the shipped manifest's own order (`AddonList.xml` before `GameMenuFrame.xml`), which
/// is what makes the button's `AddonList_Toggle()` resolve at click time. A test that loaded them
/// the other way round would pass on the button's `IsEnabled()` and fail on the click, which is
/// exactly the seam worth covering.
#[test]
fn the_esc_menu_addons_entry_opens_the_list() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"Fonts.xml",
"UiPanels.xml",
"GameTooltip.xml",
"AddonList.xml",
"GameMenuFrame.xml",
] {
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets/ui")
.join(file),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
}
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
assert!(
s.eval::<bool>("return GameMenuButtonAddOns:IsEnabled()")
.unwrap(),
"the entry is live — it stopped being GameMenuButton_Pending in 1197"
);
s.run("GameMenuButtonAddOns:Click()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
s.eval::<bool>("return AddonListFrame:IsVisible()").unwrap(),
"the list came up"
);
assert!(
!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
"and the menu went away — the Macros entry's own body shape"
);
}
/// **ESC cancels the list**, the reference's own `AddonList_OnKeyDown` arm — and it discards.
///
/// This engine gives plain frames no raw key events, so the window joins `ToggleGameMenu`'s
/// shared ESC chain like the stack-split spinner and the world map. The rung sits above the
/// options window and below the popup engine, so a confirm dialog still wins the key.
#[test]
fn escape_cancels_the_list_and_discards_the_edit() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"Fonts.xml",
"UiPanels.xml",
"GameTooltip.xml",
"AddonList.xml",
] {
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets/ui")
.join(file),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
}
s.register_addons(
vec![benilla_ui::script::AddOnInfo {
name: "Solo".into(),
title: Some("Solo".into()),
notes: None,
url: None,
secure: false,
load_on_demand: false,
dependencies: Vec::new(),
directives: Default::default(),
files: Vec::new(),
saved_variables: Vec::new(),
saved_variables_per_character: Vec::new(),
enabled: true,
loaded: false,
}],
None,
None,
None,
);
s.run("AddonList_Show()").unwrap();
s.run("AddonList_ToggleEntry(1)").unwrap();
assert_eq!(
s.eval::<String>("return AddonListEntry1Status:GetText() or ''")
.unwrap(),
"DISABLED"
);
s.run("ToggleGameMenu()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
!s.eval::<bool>("return AddonListFrame:IsVisible()").unwrap(),
"ESC put the list away"
);
s.run("AddonList_Show()").unwrap();
assert_eq!(
s.eval::<String>("return AddonListEntry1Status:GetText() or ''")
.unwrap(),
"",
"and discarded the edit — ESC is Cancel, not Okay"
);
}
/// The hover tooltip runs — `AddonTooltip_Update`'s three lines (title, notes, dependencies) on
/// the shared `GameTooltip` rather than a private frame.
///
/// Worth its own test because it is the one path the other seven never touch: a load error would
/// have shown up in the harness, but a *runtime* error in a hover handler goes to
/// `UiScript::errors` and nothing else, which is exactly the silent kind.
#[test]
fn hovering_a_row_raises_the_tooltip_with_notes_and_dependencies() {
let s = harness();
s.run("AddonList_Show()").unwrap();
// Row 3 is `Needs`, which depends on `Lib` — so it exercises both optional lines.
s.run("AddonList_RowEnter(AddonListEntry3)").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return GameTooltip:IsOwned(AddonListEntry3)")
.unwrap());
s.run("AddonList_RowLeave()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// And a row with no dependencies takes the other branch without erroring.
s.run("AddonList_RowEnter(AddonListEntry1)").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}

Some files were not shown because too many files have changed in this diff Show More