the colour picker, guild charter, honour page and four more go stock

- the colour dialog is the reference's own `ColorPickerFrame.xml` — the window with the largest reach in the addon corpus (86 of 218 addons name it, 64 of those Dewdrop-2.0, so every Ace2 config menu's colour row opens it), and the guild charter, the guild registrar, the error toasts with the raid warning (which our copy had declared with NO scripts at all, so the big centre text and its sound had been inert scenery), the honour page, the quest timer and the stack-split spinner follow — each a toc line and a deletion
- the swaps caught our own bugs on the way: the registrar's two service rows were hidden by a template modelled on the quest list's pooled rows, leaving a bare "Available Services" heading over blank parchment; the stack spinner CLAMPED a too-big digit where the reference ignores it (37 against a 5-stack leaves the 3 standing, `StackSplitFrame.lua:72-85`) — a divergence with a test defending it; and 1.12's Lua-5.0 `arg`/`arg.n` varargs turn out to work in this VM (mlua's vendored 5.1 has `LUA_COMPAT_VARARG`), which unblocks every stock file written in that idiom
- 42 test modules carried five near-identical private copies of a disk-only XML loader that structurally cannot name a chain file; they are one shared reader now (733 lines out), with a strict variant that fails on an unknown-template WARNING — the case where a frame builds with none of its art — which is how the registrar bug was caught on the first window after

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 16:58:29 +07:00
co-authored by Claude Fable 5.1
parent 3cce6f5ac3
commit 149a1beb47
64 changed files with 485 additions and 3304 deletions
@@ -1,390 +0,0 @@
<Ui>
<!--
The shared colour dialog — our own implementation of the window that
Interface\FrameXML\ColorPickerFrame.xml specifies. 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). It is
cited throughout as the SPECIFICATION — for the names, the geometry, the sounds and the
call order, all of which are the API addons are written against and none of which we are
free to invent. The Lua in this file is ours.
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 WHEEL IS ENGINE ART, AND THAT IS WHY IT TOOK A §5 ───────────────────────────────────
Look at what `<ColorWheelTexture>` and `<ColorValueTexture>` below do NOT have: a `file=`.
The hue disc and the brightness strip are **generated by the client**, not loaded — there is
no BLP anywhere in the MPQ chain that is a colour wheel, only the two little thumb markers
that ride on top of them. So the four elements are declared here for their names and their
geometry, and the *pixels* are the app renderer's
(`benilla-app/src/ui_script/extract/colorselect.rs`), the same division of labour the
`<Minimap>` widget's content hole uses.
For two years this file shipped without them and said so. What unblocked it was wow-re's
`system/ui/scratch/colorselect-drawn-appearance.md` (decision 1592), which carved what the
client actually draws — a 128×128 generated ARGB texture on an integer lattice with a hard
alpha cut, and a solid `HSV(H,S,1)` tile under a black→white vertex gradient — and, in
passing, corrected the obvious guess: **the wheel is drawn at a literal `V = 1.0` and never
reads the widget's brightness.** Lowering the opacity/value marker moves the marker; the
disc behind it does not dim. Read that module's header before touching any of this.
The two thumbs carry `file=` and **no `<Anchors>`**, which is not an omission either: the
client `ClearAllPoints`es them and re-`SetPoint`s from C++ on every colour change
(`0x78b850`/`0x78ba90`), so an authored anchor would be discarded. benilla derives their
rects the same way, in `script::colorselect::wheel_thumb_rect`/`value_thumb_rect` — including
the client's two asymmetries (the wheel marker scales its *vertical* offset by the wheel's
**width**; the value marker scales by the **wheel's** height while anchoring to the strip).
OTHER DEVIATIONS, each forced:
· **`<OnKeyDown>` (ref l.177-184) is not transcribed.** The engine reason is GONE — decision
1319 built the frame key-down walk and `OnKeyDown` is a live script kind (StackSplit.xml
declares one), so this could now be transcribed as written. It is not, because transcribing
it changes more than ESC: the walk's gate is EXISTENCE, so the frame would begin swallowing
EVERY key while the picker is open, and the UiPanels ESC rung below would have to come out
in the same move. That is a deliberate deferral now, not a wall. 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 transcribed now** — the second frame in the reference's
file: a standalone 80x180 opacity popup with its own vertical slider and a fullscreen
click-off-to-close button, used by FloatingChatFrame's chat-window alpha. This note used
to read "**zero corpus callers** and no benilla consumer, so it waits for its first
customer (decision 1195)". Its first customer arrived: pfUI skins it in two files —
`skins/blizzard/miscellaneous.lua` l.36-41 (`CreateBackdrop(OpacityFrame)`,
`SkinSlider(OpacityFrameSlider)`, then a re-anchor) and
`skins/blizzard/battlefield_minimap.lua` l.23-24 (re-anchors `OpacityFrame` off the
battlefield minimap). 1195's own bar is demand, so demand is what moves it (1719).
It is transcribed verbatim, `opacityFunc`/`saveOpacityFunc` hooks included — those are
the whole contract with a caller, and a popup that drops them would move a slider that
changes nothing.
· **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 for the name, the geometry and the sound. Cancel is declared FIRST
because Okay anchors to it. -->
<Button name="ColorPickerCancelButton" inherits="GameMenuButtonTemplate" text="CANCEL">
<Anchors>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-10" y="10"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
-- Dismiss first, undo second — the undo is the CALLER's code, and a
-- cancelFunc that opens a window of its own should not have to fight this
-- one for the panel slot.
HideUIPanel(this:GetParent());
-- This window remembers nothing itself: `previousValues` is the snapshot
-- whoever opened it left behind (`UIDropDownMenuButton_OpenColorPicker`
-- fills it in; Dewdrop closes over its own copy and ignores it) and
-- `cancelFunc` is the caller's restore. Both are optional fields on the
-- frame, so a caller that set neither simply gets the window closed.
local cancelFunc = ColorPickerFrame.cancelFunc;
if ( cancelFunc ) then
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());
-- Commit. Neither callback is handed the values: both read the widget and
-- the slider for themselves, which is the contract every corpus caller is
-- written against. `func` is deliberately NOT guarded (header) — a caller
-- that opened the window without one should hear about it. The alpha half
-- genuinely is optional: a caller that never asked for opacity has no
-- reason to supply one.
ColorPickerFrame.func();
local opacityFunc = ColorPickerFrame.opacityFunc;
if ( opacityFunc ) then
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>
-- Live preview: every step of the drag reaches the caller, which is what
-- makes the alpha follow the thumb instead of jumping at Okay.
local opacityFunc = ColorPickerFrame.opacityFunc;
if ( opacityFunc ) then
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 for the two widths. The opacity slider is opt-in per caller, and the
window narrows by the slider's 60 units when nobody asked for it. -->
<OnShow>
-- Seeding the slider AFTER showing it is deliberate: SetValue fires
-- OnValueChanged, so the caller's opacityFunc runs against a slider that is
-- already on screen and against a window still at its old width — the reference's
-- own order, and observable to any addon whose preview writes through. The width
-- is settled once, at the end, instead of in both arms.
local width = 305;
if ( this.hasOpacity ) then
OpacitySliderFrame:Show();
OpacitySliderFrame:SetValue(this.opacity);
width = 365;
else
OpacitySliderFrame:Hide();
end
this:SetWidth(width);
</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>
<!-- ref l.186-221. The reference declares all four AFTER `<Scripts>`, and the order is not
cosmetic: `<ColorValueTexture>` anchors to `ColorPickerWheel` by name, so the wheel has
to be published first. The wheel's name is the reference's own and unprefixed — an
addon re-skinning the picker addresses it. -->
<ColorWheelTexture name="ColorPickerWheel">
<Size><AbsDimension x="128" y="128"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="16" y="-32"/></Offset></Anchor>
</Anchors>
</ColorWheelTexture>
<!-- The one BLP in this window: a 4x2 sheet of markers, of which these two crops are the
wheel dot and the strip's arrow pair. Unnamed in the reference, and unanchored — see
the header. -->
<ColorWheelThumbTexture file="Interface\Buttons\UI-ColorPicker-Buttons">
<Size><AbsDimension x="10" y="10"/></Size>
<TexCoords left="0" right="0.15625" top="0" bottom="0.625"/>
</ColorWheelThumbTexture>
<ColorValueTexture>
<Size><AbsDimension x="32" y="128"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="ColorPickerWheel" relativePoint="RIGHT"><Offset><AbsDimension x="24" y="0"/></Offset></Anchor>
</Anchors>
</ColorValueTexture>
<ColorValueThumbTexture file="Interface\Buttons\UI-ColorPicker-Buttons">
<Size><AbsDimension x="48" y="14"/></Size>
<TexCoords left="0.25" right="1.0" top="0" bottom="0.875"/>
</ColorValueThumbTexture>
</ColorSelect>
<!-- ═══ ref l.223-335, verbatim ═══ The opacity popup: a standalone toplevel the colour
picker's own file ships beside it, opened by whatever wants an alpha (FloatingChatFrame's
chat-window alpha in the reference; pfUI re-anchors it off the battlefield minimap). It is
NOT a child of ColorPickerFrame — `parent="UIParent"`, its own strata, its own show/hide.
The two hook fields are the contract: `opacityFunc` fires on every slider tick,
`saveOpacityFunc` once on the click-off close. Both are read UNGUARDED-if-set exactly as
the reference writes them (an `if ( ... ) then` around each), so a caller that sets neither
gets a slider that moves and does nothing — which is the reference's own behaviour, not a
gap.
`OpacityFrameCloseButton` is the reference's click-off catcher: declared inside this
frame's `<Frames>` but RE-PARENTED to UIParent by its own `parent=` attr, `setAllPoints`
over the whole screen, and dropped one frame level in OnLoad so it sits under the popup it
closes. `RegisterForClicks("LeftButtonDown","RightButtonDown")` — it fires on the press,
not the release, so a click anywhere outside dismisses immediately. -->
<Frame name="OpacityFrame" toplevel="true" parent="UIParent" movable="true" enableMouse="true" hidden="true">
<Size><AbsDimension x="80" y="180"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></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>
<Frames>
<Slider name="OpacityFrameSlider" orientation="VERTICAL" minValue="0" maxValue="1" valueStep="0.01" defaultValue="1">
<Size><AbsDimension x="16" y="128"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="-10" y="-35"/></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">
<!-- The title hangs off the FRAME, not the slider — the ref anchors it
`relativeTo="OpacityFrame"` from inside the slider's own layers. -->
<FontString name="$parentText" inherits="GameFontNormalSmall" text="OPACITY">
<Anchors><Anchor point="TOP" relativeTo="OpacityFrame"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
</FontString>
<!-- The two end labels are UNNAMED in the ref, and the literals are the
ref's own: "0%"/"100%" are written as text, not as global strings. -->
<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 ( OpacityFrame.opacityFunc ) then
OpacityFrame.opacityFunc();
end
</OnValueChanged>
</Scripts>
<ThumbTexture file="Interface\Buttons\UI-SliderBar-Button-Vertical">
<Size><AbsDimension x="32" y="32"/></Size>
</ThumbTexture>
</Slider>
<Button name="OpacityFrameCloseButton" parent="UIParent" setAllPoints="true" hidden="true">
<Scripts>
<OnLoad>
this:SetFrameLevel(this:GetFrameLevel()-1);
this:RegisterForClicks("LeftButtonDown", "RightButtonDown");
</OnLoad>
<OnClick>
OpacityFrame:Hide();
if ( OpacityFrame.saveOpacityFunc ) then
OpacityFrame.saveOpacityFunc();
end
</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnShow>
OpacityFrameCloseButton:Show();
</OnShow>
<OnHide>
OpacityFrameCloseButton:Hide();
</OnHide>
</Scripts>
</Frame>
</Ui>
@@ -1,103 +0,0 @@
<Ui>
<!--
The real client's UIErrorsFrame, now declared as what it actually IS: a
`<MessageFrame>` (ref UIErrorsFrame.xml:4 — 512×60 at TOP (0,-122), displayDuration="5",
insertMode="TOP", with `<FontString inherits="ErrorFont" justifyH="CENTER"/>` as its line
font). UI_INFO_MESSAGE shows YELLOW (1,1,0) — the quest objective-progress toasts land
here — and UI_ERROR_MESSAGE red (1,0.1,0.1), the ref handler verbatim (UIErrorsFrame.lua:8-16;
its SYSMSG arm is unwired, no engine producer yet).
It used to be a hand-rolled `<Frame>` painting three stacked FontStrings from Lua, because
this engine had no MessageFrame widget. It has one now (`script::messageframe::plain`,
`widget::MessageFrameState`), so the file that stood in for it is gone and the display —
insertMode, the vertical cap, the two-phase fade — is the widget's own, exactly as in the
reference. Two consequences worth naming:
* `UIErrorsFrame:AddMessage(text, r, g, b, a)` is REAL now, so FrameXML producers call it
the way the reference does (the old `BenillaErrorsFrame_AddMessage` seam is retired),
and so does an addon: `EasyCopy`, `QuestHistory` and `QuestItem` each died on that exact
line at load. A sixth argument (all three pass one, believing it is a hold time) is
silently IGNORED — the real binding reads five, and honouring it would be unfaithful.
* The post-hold fade is the class ctor's 3.0 s (msgframe-runtime.md, byte-verified), not
the 1.0 s this file used to carry as an explicitly INTERIM guess while that curve was
unpinned. The 5 s hold is unchanged.
frameStrata HIGH is the ref's own (`UIErrorsFrame.xml:4`) and decision 0597's: this frame
overlaps the right edge of every left-slot panel window (character, quest log, loot,
merchant, mail, trainer, taxi — all at TOPLEFT (0,-104)), so in the default MEDIUM its
toasts draw UNDER whichever panel is open, invisible in exactly the state you most want to
read them. The ref also carries toplevel="true"; that is deliberately not transcribed —
HIGH already wins the strata and 0597's fix is the one that was looked at.
-->
<Script><![CDATA[
function UIErrorsFrame_OnLoad()
this:RegisterEvent("UI_INFO_MESSAGE")
this:RegisterEvent("UI_ERROR_MESSAGE")
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)
if not message then return end
if event == "UI_INFO_MESSAGE" then
this:AddMessage(message, 1.0, 1.0, 0.0, 1.0) -- ref UIErrorsFrame.lua:12
elseif event == "UI_ERROR_MESSAGE" then
this:AddMessage(message, 1.0, 0.1, 0.1, 1.0) -- ref UIErrorsFrame.lua:14
end
end
]]></Script>
<MessageFrame name="UIErrorsFrame" toplevel="true" parent="UIParent" displayDuration="5" insertMode="TOP" frameStrata="HIGH">
<Size><AbsDimension x="512" y="60"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="TOP">
<Offset><AbsDimension x="0" y="-122"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>UIErrorsFrame_OnLoad()</OnLoad>
<OnEvent>UIErrorsFrame_OnEvent(event, arg1)</OnEvent>
</Scripts>
<!-- The line font AND the centring, both: the class draws its messages through this
declared child, so `justifyH="CENTER"` is what keeps the toasts centred in the 512-wide
box (a chat frame's own child says LEFT). ErrorFont = GameFontNormalLarge (ref
Fonts.xml:258), which is the face the hand-rolled version used too. -->
<FontString inherits="ErrorFont" justifyH="CENTER"/>
</MessageFrame>
<!-- RaidWarningFrame (ref RaidWarning.xml:4-24), declared here beside the frame it anchors to.
Two corpus addons push into it directly — `Participant/RaidWarn.lua:133` and
`oRA2/RaidWarn.lua:133` are the same code in two distributions, plus their MainTank.lua's
"Tank %s has died!" — and both call
`RaidWarningFrame:AddMessage(text, r, g, b, 1.0, UIERRORS_HOLD_TIME)`. That is the plain
MessageFrame arity we already ship: the 6th argument is read and DROPPED, because the
reference's own `0x795590` reads five and stops (messageframe/plain.rs's first law).
Absent, that line is `attempt to index global` and takes the handler with it.
(`BigWigs` also names it — in a LOCALISATION STRING, "Suppressing RaidWarningFrame". Not a
use; 1207.)
Geometry quoted: 512x40, insertMode TOP, HIGH + toplevel, TOP anchored to UIErrorsFrame's
BOTTOM at (0,-10), and the direct-child FontString the class draws its lines through —
GameFontNormalHuge, CENTER, exactly as the sibling above takes ErrorFont.
**No <Scripts>, deliberately.** The reference's OnLoad/OnEvent call into RaidWarning.lua to
register CHAT_MSG_RAID_WARNING and route it here. We already deliver that chat type through
`ui_chat::feed`, so wiring a second consumer would print every raid warning TWICE; and
declaring handlers whose functions we do not ship would raise at load. The frame exists and
accepts messages, which is what the corpus asks of it — the automatic routing is the
ChatFrame_OnEvent architecture question, not this frame's.
RaidBossEmoteFrame, its sibling in the same file, is NOT declared: zero corpus addons name
it, and a shadowed name with no caller is building against a guess (1222's rejected clause). -->
<MessageFrame name="RaidWarningFrame" parent="UIParent" insertMode="TOP" frameStrata="HIGH" toplevel="true">
<Size><AbsDimension x="512" y="40"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="UIErrorsFrame" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-10"/></Offset>
</Anchor>
</Anchors>
<FontString inherits="GameFontNormalHuge" justifyH="CENTER"/>
</MessageFrame>
</Ui>
@@ -1,355 +0,0 @@
<Ui>
<!--
benilla's guild registrar (decision 1672) — the window the "How do I form a guild?" gossip
row opens, and the only place a guild charter can be bought or turned in.
The geometry and art are the real `GuildRegistrarFrame.xml`'s (extracted at
`WoW/_extracted_framexml/GuildRegistrarFrame.{xml,lua}`; `ref l.N` cites are into those).
Every global, frame name, event, texture, string and number below is the reference's and is
the contract addons bind to (1260's rule, still the rule after 1602 retired the
transcription gate): what is ours is the expression — named functions instead of inline
handler bodies, the service rows over one small local template, and this comment.
TWO PANELS ON ONE CHROME, exactly as the reference has it (ref l.116, l.196):
GUILD_REGISTRAR_SHOW → the window opens on GuildRegistrarGreetingFrame — the two
service rows, "Purchase a Guild Charter" and "Register a Guild
Charter".
Purchase clicked → GuildRegistrarPurchaseFrame swaps in: the explanatory text, the
cost, and the guild-name box. NOT a second window and NOT a
server round trip — `GuildRegistrar_ShowPurchaseFrame` is a pure
local swap (ref l.8-12).
GUILD_REGISTRAR_CLOSED → the window hides.
WHY THE COST IS READ AT PANEL-SWAP TIME AND NOT AT SHOW TIME: the reference calls
`MoneyFrame_Update("GuildRegistrarMoneyFrame", GetGuildCharterCost())` inside
`GuildRegistrar_ShowPurchaseFrame` (ref l.10) — the greeting panel never displays a price.
Ours does the same, and it matters here: `GetGuildCharterCost()` answers from the *open
registrar's* showlist row, so reading it before a registrar is open would paint a 0.
THE WINDOW IS NOT SELF-CLOSING ON PURCHASE BY ACCIDENT. Both the Purchase button and the
edit box's Enter key hide the whole panel after sending (ref l.253-257, l.312-315) — a buy
is one-shot, and the charter arrives as an item rather than as any answer to this window.
That is the reference's own behaviour and it is quoted, not invented.
UIPanel slot system (decision 0084 §2): carries NO hardcoded <Anchors> — ShowUIPanel seats
it in the left slot from `UIPanelWindows["GuildRegistrarFrame"]` (UiPanels.xml), which is
the reference's own registration (`UIParent.lua:33`, area="left", pushable=0). Hiding it
goes through HideUIPanel so the slot is released and ESC reaches it.
-->
<Script><![CDATA[
-- ── The registrar's strings (GlobalStrings.lua, verbatim) ──────────────────────────────
-- The `text=` attributes below name these KEYS rather than spelling the words, which is
-- what the reference's own XML does and what the loader resolves (0991). In a real session
-- the app has already run the shipped GlobalStrings.lua, so these lines are the
-- standalone-load copy — same values, same file, line numbers named. `X = X or "…"`
-- throughout, never a bare `=`, for FriendsFrame.xml's reason: CANCEL and PURCHASE are
-- generic names the real string table already owns, and a bare assignment would overwrite
-- the shipped value with this copy on every load.
AVAILABLE_SERVICES = AVAILABLE_SERVICES or "Available Services" -- l.128
COSTS_LABEL = COSTS_LABEL or "Cost:" -- l.870
PURCHASE = PURCHASE or "Purchase" -- l.3128
CANCEL = CANCEL or "Cancel" -- l.465
GUILD_CHARTER_PURCHASE = GUILD_CHARTER_PURCHASE or "Purchase a Guild Charter" -- l.2058
GUILD_CHARTER_REGISTER = GUILD_CHARTER_REGISTER or "Register a Guild Charter" -- l.2059
GUILD_REGISTRAR_PURCHASE_TEXT = GUILD_REGISTRAR_PURCHASE_TEXT or "To create a guild you must purchase this charter, get 9 unique player signatures, and return the charter to me. Please enter the desired name for your guild." -- l.2107
-- ref GuildRegistrarFrame.lua:1-6. Every show starts on the services list: the purchase
-- panel is a transient sub-view, so re-opening the window must not land on a half-filled
-- name box from last time.
function GuildRegistrar_OnShow()
GuildRegistrarGreetingFrame:Show()
GuildRegistrarPurchaseFrame:Hide()
SetPortraitTexture(GuildRegistrarFramePortrait, "npc")
GuildRegistrarFrameNpcNameText:SetText(UnitName("npc"))
end
-- ref GuildRegistrarFrame.lua:8-12. A pure local panel swap — no packet, no round trip.
-- The cost is read HERE rather than on show because this is the only panel that displays
-- one (see the file header).
function GuildRegistrar_ShowPurchaseFrame()
GuildRegistrarPurchaseFrame:Show()
GuildRegistrarGreetingFrame:Hide()
MoneyFrame_Update("GuildRegistrarMoneyFrame", GetGuildCharterCost())
end
-- The buy, shared by the Purchase button and the box's Enter key (ref l.253-257 and
-- l.312-315, which carry the same three lines twice). Hiding the panel after sending is
-- the reference's own behaviour: a buy is one-shot and its answer is an item arriving, not
-- anything this window will be told.
function GuildRegistrar_Purchase(name)
BuyGuildCharter(name)
HideUIPanel(GuildRegistrarFrame)
GuildRegistrar_RefocusChat()
end
-- The reference indexes `ChatFrameEditBox` bare (ref l.256, l.314, l.317). Ours nil-guards
-- it, this house's convention everywhere it is touched from outside ChatFrame.xml
-- (FriendsFrame.xml's four dialogs, BagFrame.xml) — an unguarded index is a real error line
-- for anyone who loads this window without the chat frame, which the reference never has to
-- consider because there FrameXML is all-or-nothing. The refocus itself stays
-- UNCONDITIONAL when the box exists: the popups' extra `IsVisible()` test is a rule about
-- dialogs, not about this window, and borrowing it would change behaviour rather than
-- harden it.
function GuildRegistrar_RefocusChat()
if ChatFrameEditBox then
ChatFrameEditBox:SetFocus()
end
end
-- ref l.318-327. The show arm's `IsVisible` re-test is the reference's own: ShowUIPanel can
-- refuse (another window owns the slot, or the player is dead), and a registrar session the
-- server thinks is open with no window on screen is a stuck NPC — so the client closes it
-- back out immediately.
function GuildRegistrar_OnEvent(event)
if event == "GUILD_REGISTRAR_SHOW" then
ShowUIPanel(GuildRegistrarFrame)
if not GuildRegistrarFrame:IsVisible() then
CloseGuildRegistrar()
end
elseif event == "GUILD_REGISTRAR_CLOSED" then
HideUIPanel(GuildRegistrarFrame)
end
end
]]></Script>
<!-- One service row (ref inherits="QuestTitleButtonTemplate", QuestFrameTemplates.xml l.174-213):
a bullet plus the service name. Local rather than QuestFrame.xml's
`BenillaQuestTitleButtonTemplate` because that one hardwires `QuestTitleButton_OnClick()`
and would drag this window's load order behind the questgiver's for a fifteen-line shape.
Same LEFT-anchored title as that template, and for the same reason: the round bullet is
cell-centred, so a TOPLEFT-anchored label rides above it (QuestFrame.xml's own note).
**NOT `hidden="true"`, and that is the whole difference from the template it is modelled
on.** `BenillaQuestTitleButtonTemplate` carries `hidden` because the quest list is a POOL:
its rows are hidden by default and `:Show()`n one at a time as the greeting fills. These
two rows are STATIC — the registrar's services never change, nothing ever calls `:Show()`
on them — so inheriting `hidden` left both permanently invisible and the window came up as
a bare "Available Services" heading over blank parchment. The reference's own
`QuestTitleButtonTemplate` (QuestFrameTemplates.xml l.174) has no `hidden` either; the
attribute was copied across with the shape. -->
<Button name="BenillaGuildRegistrarServiceTemplate" virtual="true">
<Size><AbsDimension x="285" y="16"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\QuestFrame\UI-Quest-BulletPoint">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
</Texture>
</Layer>
</Layers>
<HighlightTexture file="Interface\QuestFrame\UI-QuestTitleHighlight" alphaMode="ADD"/>
<ButtonText>
<Size><AbsDimension x="275" y="0"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="20" y="0"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="QuestFont" justifyH="LEFT"/>
</Button>
<Frame name="GuildRegistrarFrame" toplevel="true" parent="UIParent" movable="true" enableMouse="true" hidden="true">
<Size><AbsDimension x="384" y="512"/></Size>
<!-- ref l.16-19: the window's own dead zone at the right edge and along the bottom, so a
drag on the parchment's painted border does not grab the window. -->
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="70"/></HitRectInsets>
<Layers>
<Layer level="BACKGROUND">
<!-- The NPC's face (ref l.21-31), filled by SetPortraitTexture on show. -->
<Texture name="GuildRegistrarFramePortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The quest-greeting parchment, four quadrant slabs (ref l.34-65) — the same art
GossipFrame and QuestFrame wear, at the same 384x512. -->
<Texture file="Interface\QuestFrame\UI-QuestGreeting-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
<Texture file="Interface\QuestFrame\UI-QuestGreeting-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPRIGHT"/></Anchors>
</Texture>
<Texture file="Interface\QuestFrame\UI-QuestGreeting-BotLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture file="Interface\QuestFrame\UI-QuestGreeting-BotRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<!-- ref l.66-79. Defined but never written by the shipped UI; kept because it is a
name an addon can reach. -->
<FontString name="GuildRegistrarText" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-10"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The title banner (ref l.83-107): the NPC's name, two frame levels above the
parchment so it is not painted over. -->
<Frame name="GuildRegistrarNpcNameFrame">
<Size><AbsDimension x="300" y="14"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="GuildRegistrarFrame" relativePoint="TOP"><Offset><AbsDimension x="0" y="-23"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString name="GuildRegistrarFrameNpcNameText" inherits="GameFontHighlight"/>
</Layer>
</Layers>
<Scripts>
<OnLoad>this:SetFrameLevel(this:GetFrameLevel() + 2)</OnLoad>
</Scripts>
</Frame>
<Button name="GuildRegistrarFrameCloseButton" inherits="UIPanelCloseButton">
<Anchors>
<Anchor point="CENTER" relativeTo="GuildRegistrarFrame" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-42" y="-30"/></Offset></Anchor>
</Anchors>
</Button>
<!-- ── Panel 1: the services list ─────────────────────────────────────────────── -->
<Frame name="GuildRegistrarGreetingFrame" setAllPoints="true" hidden="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="AvailableServicesText" inherits="QuestTitleFont" text="AVAILABLE_SERVICES" justifyH="LEFT">
<Size><AbsDimension x="300" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="35" y="-100"/></Offset></Anchor></Anchors>
</FontString>
<!-- The torn-parchment patch along the bottom edge (ref l.127-138). -->
<Texture file="Interface\QuestFrame\UI-Quest-BotLeftPatch">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="22" y="68"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="GuildRegistrarFrameGoodbyeButton" inherits="UIPanelButtonTemplate" text="CANCEL">
<Size><AbsDimension x="75" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="GuildRegistrarFrame" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-40" y="72"/></Offset></Anchor>
</Anchors>
<Scripts><OnClick>HideUIPanel(GuildRegistrarFrame)</OnClick></Scripts>
</Button>
<Button name="GuildRegistrarButton1" inherits="BenillaGuildRegistrarServiceTemplate" text="GUILD_CHARTER_PURCHASE">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="AvailableServicesText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-10" y="-10"/></Offset></Anchor>
</Anchors>
<Scripts><OnClick>GuildRegistrar_ShowPurchaseFrame()</OnClick></Scripts>
</Button>
<Button name="GuildRegistrarButton2" inherits="BenillaGuildRegistrarServiceTemplate" text="GUILD_CHARTER_REGISTER">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GuildRegistrarButton1" relativePoint="BOTTOMLEFT"/>
</Anchors>
<!-- Turn the charter in. Takes no argument: the engine finds the charter in
the bags, because the wire wants an item guid and Lua has no way to
name one. A player with no charter gets ERR_NO_GUILD_CHARTER and no
packet leaves (`ui_petition::feed`'s drain). -->
<Scripts><OnClick>TurnInGuildCharter()</OnClick></Scripts>
</Button>
<!-- The reference's third row, "Design a Guild Crest", is COMMENTED OUT in the
shipped file (ref l.183-194) and its `GUILD_CREST_DESIGN` string does not
exist in 1.12's GlobalStrings — tabard design runs off its own NPC gossip
option, not this window. Not shipped here either, deliberately: the
reference's own dead code is not a gap to fill. -->
</Frames>
</Frame>
<!-- ── Panel 2: buy a charter ─────────────────────────────────────────────────── -->
<Frame name="GuildRegistrarPurchaseFrame" setAllPoints="true" hidden="true">
<Layers>
<Layer level="ARTWORK">
<FontString name="GuildRegistrarPurchaseText" inherits="QuestFont" text="GUILD_REGISTRAR_PURCHASE_TEXT" justifyH="LEFT">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="35" y="-95"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="GuildRegistrarCostLabel" inherits="GameFontNormal" text="COSTS_LABEL">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GuildRegistrarPurchaseText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The shared money kit (MoneyFrame.xml), STATIC type — a read-only price, no
click-to-edit. GetGuildCharterCost() answers in copper, which is this
kit's whole vocabulary. -->
<Frame name="GuildRegistrarMoneyFrame" inherits="MoneyFrameTemplate">
<Anchors>
<Anchor point="LEFT" relativeTo="GuildRegistrarCostLabel" relativePoint="RIGHT"><Offset><AbsDimension x="15" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>
MoneyFrame_OnLoad()
MoneyFrame_SetType("STATIC")
</OnLoad>
</Scripts>
</Frame>
<Button name="GuildRegistrarFrameCancelButton" inherits="UIPanelButtonTemplate" text="CANCEL">
<Size><AbsDimension x="75" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="GuildRegistrarFrame" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-40" y="72"/></Offset></Anchor>
</Anchors>
<Scripts><OnClick>HideUIPanel(GuildRegistrarFrame)</OnClick></Scripts>
</Button>
<Button name="GuildRegistrarFramePurchaseButton" inherits="UIPanelButtonTemplate" text="PURCHASE">
<Size><AbsDimension x="85" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="GuildRegistrarFrame"><Offset><AbsDimension x="22" y="72"/></Offset></Anchor>
</Anchors>
<Scripts><OnClick>GuildRegistrar_Purchase(GuildRegistrarFrameEditBox:GetText())</OnClick></Scripts>
</Button>
<!-- 24 letters is the server's own MAX_CHARTER_NAME, so the box cannot compose
a name the server will reject for length (ref l.271). -->
<EditBox name="GuildRegistrarFrameEditBox" letters="24" historyLines="1">
<Size><AbsDimension x="130" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GuildRegistrarCostLabel" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\ChatFrame\UI-ChatInputBorder-Left">
<Size><AbsDimension x="75" y="32"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.29296875" top="0" bottom="1.0"/>
</Texture>
<Texture file="Interface\ChatFrame\UI-ChatInputBorder-Right">
<Size><AbsDimension x="75" y="32"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.70703125" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEnterPressed>GuildRegistrar_Purchase(this:GetText())</OnEnterPressed>
<OnEscapePressed>GuildRegistrar_RefocusChat()</OnEscapePressed>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
</Frames>
</Frame>
</Frames>
<Scripts>
<OnLoad>
this:RegisterEvent("GUILD_REGISTRAR_SHOW")
this:RegisterEvent("GUILD_REGISTRAR_CLOSED")
</OnLoad>
<OnEvent>GuildRegistrar_OnEvent(event)</OnEvent>
<OnShow>
GuildRegistrar_OnShow()
PlaySound("igQuestListOpen")
</OnShow>
<!-- CloseGuildRegistrar() on hide is load-bearing, not tidiness: it is what clears the
engine's registrar session, so the next GUILD_REGISTRAR_SHOW is a real open (and
GetGuildCharterCost() stops quoting a price for a registrar nobody is talking to).
Same shape as GossipFrame's CloseGossip(). -->
<OnHide>
PlaySound("igQuestListClose")
CloseGuildRegistrar()
</OnHide>
</Scripts>
</Frame>
</Ui>
-652
View File
@@ -1,652 +0,0 @@
<Ui>
<!--
benilla's Honor tab: our own build of the 1.12 `HonorFrame`, the fifth and last page of the
character window. The reference FrameXML `HonorFrame.{xml,lua}` + `HonorFrameTemplates.xml`
are the SPECIFICATION throughout (cites read `ref l.N` into `HonorFrame.xml`, `ref-lua l.N`
into `HonorFrame.lua`, `ref-tpl l.N` into `HonorFrameTemplates.xml`); every geometry number
and art path below is QUOTED from them, because those are the client's facts and there is no
other value they could take. The Lua is OURS, written against that specification and onto
the PvP/honor Era API (`benilla-ui/src/script/pvp.rs` and its honor siblings). Frame names
are the reference's, because a frame name is the addon-visible contract and not ours to
vary.
WHAT THE PAGE IS. A rank block at the top (badge, rank title, "(Rank N)", and the progress
bar that says how far through the rank you are), then five labelled sections of counters:
Today, Yesterday, This Week, Last Week, Lifetime.
A CHILD PAGE of `CharacterFrame` (ref l.5: `parent="CharacterFrame"`, `id="5"`), shown and
hidden by `CharacterFrame_ShowSubFrame` — a SEPARATE top-level file, the SkillFrame.xml /
PetPaperDollFrame.xml / ReputationFrame.xml shape, and it carries the reference's
`parent="CharacterFrame"` for real: the loader does read a cross-file `parent=`, resolving
the name by global lookup at load (decision 1734). That name has to exist by the time this
file loads — an unresolvable one is not an error, it warns and falls back to the enclosing
element — which the manifest order gives it. The page is still sized and pinned to
`CharacterFrame`'s own TOPLEFT by NAME and still loaded right after its siblings so it paints
on top (later file = higher z): that arrangement produces the identical rect and is what the
page was built and approved against, not a workaround for something the engine cannot do.
Joining `BENILLA_CHARACTERFRAME_SUBFRAMES` as entry 5
is all it has to do to be closed by `CharacterFrame_OnHide`'s hide-loop.
THE TAB ROW IS NOW WHOLE. Four of the reference's five tabs shipped before this page and
each sat at the reference's own number; this is the fifth, so the row and
`BENILLA_CHARACTERFRAME_SUBFRAMES` are the real client's, entire, with nothing left past
the end. The invariant CharacterFrame.xml states — a page's `id=` IS its slot in the tab
row, and the roster is the same 1:1 mapping written the other way round — is pinned by
`character_tests.rs`, whose loop now runs the full five.
NO OWN PORTRAIT: the "General" quadrant art below carries the portrait-ring cutout every
PaperDollInfoFrame page shares, so `CharacterFrame`'s portrait/name/level (drawn underneath)
shows through. ReputationFrame.xml's and SkillFrame.xml's note, unchanged.
THE TWO SHARED PAINTERS. `BenillaHonorPane_SetRows` and `BenillaHonorPane_SetRank` are
written unit-first and pane-first because the INSPECT window's honor page (InspectFrame.xml)
paints the identical twelve rows and the identical rank block from a foreign player's
numbers. The reference keeps two full copies of both, one per pane, differing only in the
frame prefix and the unit token; we keep one. The row templates below are shared the same
way — the reference shares those too (its `HonorFrameTemplates.xml` is `<Include>`d by
`HonorFrame.xml` and inherited by `Blizzard_InspectUI`), which is why this file must load
BEFORE `InspectFrame.xml` in `benilla.toc`.
DEVIATIONS (each re-cited at its own site below):
1. THE GLOBALSTRINGS ARE NAMED, NEVER COPIED. Older windows here carry an
`X = X or "<the enUS text>"` fallback so a chainless harness still paints words; this
page ships no such copy for its own labels, because three of them are the
`NEWBIE_TOOLTIP_*` paragraphs and shipping Blizzard's prose was what decision 1234
forbade (retired by 1602; the lookup stays because it is the better design). The section labels reach the player's own strings through the loader's
`text=` resolution (a `text=` attribute is a GlobalStrings LOOKUP, `loader::resolve_text`);
the row labels and tooltip bodies reach them as plain globals. A harness with no
install behind it therefore paints a key where a label goes and raises if you hover a
row — the honest face of "these words live in the install, not in our tree". The ONE
exception is `PLAYER_LEVEL`, a format template rather than prose, declared here with
the same guarded fallback CharacterFrame.xml and InspectFrame.xml already carry and
for their stated reason: load order must not be a hidden dependency between windows.
2. THE NEWBIE TIPS GO THROUGH THE HOUSE HELPER. All five hover sites call
`BenillaTooltip_AddNewbieTipFor` (GameTooltip.xml), so they obey `SHOW_NEWBIE_TIPS` —
the "Show Detailed Tooltips" box our own OptionsFrame ships — like every other newbie
tip in this UI. The reference's row template does the same thing by hand (ref-tpl
l.8-16) but ignores the CVar, and its rank/bar pair (ref l.324-333, l.352-361) ignores
it AND anchors beside the widget instead of at the screen corner. Detailed tips are ON
by default (1.12's own default, GameTooltip.xml), so the default look is the
reference's; the difference is that a player who turned them off is obeyed here.
3. THE ROW TEMPLATES ARE THE REFERENCE'S FIVE, under the reference's five names, but the
base carries everything that can be shared — the strip's size and the whole hover —
and the leaves carry only the two FontStrings. They cannot carry less: this engine's
`inherits=` concatenates children instead of merging them by name
(`framexml::merge`), so a leaf that re-declared an inherited region would end up with
two of it. That constraint is stated at the template rather than discovered later.
4. THE LEVEL LINE PAINTS ITSELF. `HonorLevelText` is the reference's own FontString, and
the reference fills it from the PAPER DOLL's setter — `PaperDollFrame_SetLevel` writes
`CharacterLevelText` and then, in its own words, sets it "for the honor frame while we
at it" (ref PaperDollFrame.lua:100-104). That is a hidden page reaching across a file
boundary into another hidden page. The string belongs to THIS page, so the write does
too: this file registers `UNIT_LEVEL` itself and paints its own line.
5. `HonorGuildText` (ref l.68-76) IS DROPPED, with its `CharacterGuildText` twin — decision
0208's own named deferral, restated in CharacterFrame.xml: the guild line needs a guild
query this window has never made, and the honor page's copy is the same line.
6. THE RANK TITLE'S XML `y` IS THE ONE THE PAGE ACTUALLY WEARS. The reference anchors
`HonorFrameCurrentPVPTitle` at TOP (0,-87) and then re-points it every repaint at
(…,-83) (ref l.127-134 vs ref-lua l.76); its inspect twin anchors at -81 and re-points
at -83 the same way. The XML value is unobservable — nothing paints before the first
repaint — so the two contradictory numbers are collapsed to the one that survives.
7. `enableMouse="true"` on the StatusBar and on the two hover frames. In this engine only
Button/CheckButton/EditBox/ScrollingMessageFrame are mouse-enabled by construction
(`widget/mod.rs`; UnitFrames.xml, CharacterFrame.xml and ReputationFrame.xml all carry
the same note), so a Frame with an `<OnEnter>` needs the attribute or the handler
silently never fires. This REPRODUCES the reference's behaviour rather than departing
from it.
-->
<Script><![CDATA[
-- ── The one string this file spells out ─────────────────────────────────────────────────
-- A format TEMPLATE, not prose, and declared here rather than borrowed from
-- CharacterFrame.xml's identical line for that file's own stated reason: load order must
-- not be a hidden dependency between two windows. Everything else this page says comes out
-- of the player's own GlobalStrings.lua (this file's header, deviation 1).
PLAYER_LEVEL = PLAYER_LEVEL or "Level %d %s %s"
-- ── The row: two strings and a hover ────────────────────────────────────────────────────
-- A row remembers the pair its hover shows: its own label as the tip's title, and the
-- matching detailed explanation as the body. Both are the player's strings, passed in by
-- the instance below rather than baked into a template, so the five leaf templates differ
-- only in what they LOOK like.
function BenillaHonorRow_OnLoad(row, title, detail)
row.title = title
row.detail = detail
end
-- The house newbie tip (this file's header, deviation 2): label in highlight white, the
-- explanation wrapped under it in NORMAL_FONT_COLOR, both only when the player has detailed
-- tips on.
function BenillaHonorRow_OnEnter(row)
BenillaTooltip_AddNewbieTipFor(row, row.title,
HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b, row.detail)
end
-- ── The two painters both honor panes share (ref-lua l.19-77 and its inspect twin) ───────
-- Paint a set of counters. `pane` is the owning frame's name, `values` a
-- `{ [rowSuffix] = number }` table: each row frame is `<pane><suffix>` and its figure lives
-- in that frame's own `$parentValue` string, so a repaint is a table walk rather than
-- twelve hand-written assignments. Rows are looked up by name and skipped when absent,
-- which is what lets a caller paint a SUBSET (the character pane repaints its weekly
-- sections on a different trigger from its session ones — see HonorFrame_Update).
--
-- A nil figure is likewise not painted, and that falls out of Lua rather than being coded:
-- a nil value never enters the table at all, so `pairs` cannot reach it. That is the
-- behaviour we want on the inspect side, where the whole twelve-wide read answers with
-- NOTHING until the server's reply lands — better a row that has not been filled in yet
-- than a row confidently showing a zero nobody sent.
function BenillaHonorPane_SetRows(pane, values)
for suffix, value in pairs(values) do
local figure = getglobal(pane .. suffix .. "Value")
if figure then
figure:SetText(value)
end
end
end
-- Paint the rank block: the badge, the rank's title, the "(Rank N)" that trails it, and the
-- progress bar under both. `rank` is the unit's rank as the engine reports it, `progress`
-- the 0..1 position within that rank, `unit` the token whose faction colours the bar.
--
-- Called with ("HonorFrame", …, "player") by the page below and with
-- ("BenillaInspectHonorFrame", …, the inspected token) by InspectFrame.xml.
function BenillaHonorPane_SetRank(pane, rank, progress, unit)
local rankName, rankNumber = GetPVPRankInfo(rank)
local title = getglobal(pane .. "CurrentPVPTitle")
local ordinal = getglobal(pane .. "CurrentPVPRank")
-- THE NIL IS THE MECHANISM, not an edge case. The rank names are GlobalStrings keyed
-- `PVP_RANK_<rank>_<team>` and there is no `PVP_RANK_0_*`: rank 0 means "no rank at
-- all", which is every character who has never taken an honorable kill. So the name
-- comes back nil there and the pane says NONE. The NUMBER is not guarded, and that is
-- deliberate too: it is a rank arithmetic result, never absent, and swallowing a nil
-- would hide a broken binding instead of reporting it.
title:SetText(rankName or NONE)
-- `RANK or ""` rather than a bare concat: a harness with no install behind it has no
-- GlobalStrings, and taking the pane down over a missing label helps nobody.
ordinal:SetText("(" .. (RANK or "") .. " " .. rankNumber .. ")")
-- The badge is built HERE, not by the engine: the file name is the rank number padded
-- to two digits, and it is shown only above zero. Verified against the patch chain this
-- session: `PvPRank01`..`PvPRank15` exist and `PvPRank00`/`16`/`17`/`18`/`19` do not,
-- which is exactly the range the arithmetic can produce (the fourteen player ranks plus
-- the PvP-leader one); zero and the four negative ranks have no badge to show.
local badge = getglobal(pane .. "PvPIcon")
if rankNumber > 0 then
badge:SetTexture(format("Interface\\PvPRankBadges\\PvPRank%02d", rankNumber))
badge:Show()
else
badge:Hide()
end
-- The bar wears the unit's own faction: Alliance navy, everything else Horde red. The
-- fork is a positive test for Alliance with an `else`, not a two-sided test, so a unit
-- whose faction group reads nil (a race the app has no snapshot for yet — ui_unit.rs's
-- own note) still gets a painted bar rather than a bare trough.
local bar = getglobal(pane .. "ProgressBar")
if UnitFactionGroup(unit) == "Alliance" then
bar:SetStatusBarColor(0.05, 0.15, 0.36)
else
bar:SetStatusBarColor(0.63, 0.09, 0.09)
end
bar:SetValue(progress)
-- Re-centre the PAIR, not the title. The title is centred on the pane and then shifted
-- left by half the width of the "(Rank N)" that trails it, so title and ordinal read as
-- one centred phrase. It has to be redone on every repaint because that width moves
-- with the number and with the locale.
title:SetPoint("TOP", pane, "TOP", -ordinal:GetWidth() / 2, -83)
end
-- ── The character window's honor page (ref-lua l.1-13) ───────────────────────────────────
-- The two PvP events plus world entry, and one of our own. The SPLIT between them is the
-- reference's and it is deliberate: the weekly sections (yesterday, this week, last week)
-- only move at the server's weekly maintenance, so nothing carries them mid-session and
-- re-reading them per kill would be waste. World entry is the flag that says "repaint those
-- too". `UNIT_LEVEL` is ours (this file's header, deviation 4).
function HonorFrame_OnLoad(frame)
frame:RegisterEvent("PLAYER_PVP_KILLS_CHANGED")
frame:RegisterEvent("PLAYER_PVP_RANK_CHANGED")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("UNIT_LEVEL")
end
function HonorFrame_OnEvent(event)
if event == "UNIT_LEVEL" then
if arg1 == "player" then
HonorFrame_SetLevel()
end
return
end
HonorFrame_Update(event == "PLAYER_ENTERING_WORLD")
end
-- The level/race/class line under the window's name (this file's header, deviation 4). The
-- "?" placeholders match CharacterFrame.xml's and exist for its reason: UnitRace/UnitClass
-- read nil for the one tick before the app's first player snapshot lands, and an unguarded
-- format() errors on that nil.
function HonorFrame_SetLevel()
getglobal("HonorLevelText"):SetText(
format(PLAYER_LEVEL, UnitLevel("player"), UnitRace("player") or "?", UnitClass("player") or "?"))
end
-- `weekly` repaints the three sections that only move at the weekly reset; everything else
-- is repainted on every call. Note which return each figure comes from — the four stat
-- readers do NOT have the same shape, and that is the server's doing, not a slip:
-- yesterday and last week report dishonorable kills the pane does not show, and only last
-- week carries a standing.
function HonorFrame_Update(weekly)
if weekly then
local yesterdayHK, _, yesterdayHonor = GetPVPYesterdayStats()
local thisWeekHK, thisWeekHonor = GetPVPThisWeekStats()
local lastWeekHK, _, lastWeekHonor, standing = GetPVPLastWeekStats()
BenillaHonorPane_SetRows("HonorFrame", {
YesterdayHK = yesterdayHK,
YesterdayContribution = yesterdayHonor,
ThisWeekHK = thisWeekHK,
ThisWeekContribution = thisWeekHonor,
LastWeekHK = lastWeekHK,
LastWeekContribution = lastWeekHonor,
LastWeekStanding = standing,
})
end
local sessionHK, sessionDK = GetPVPSessionStats()
local lifetimeHK, lifetimeDK, highestRank = GetPVPLifetimeStats()
-- The highest rank EVER held, which is a different field from the current one and moves
-- only upward; its name falls back to NONE on the same nil as the live rank's.
local highestName = GetPVPRankInfo(highestRank)
BenillaHonorPane_SetRows("HonorFrame", {
CurrentHK = sessionHK,
CurrentDK = sessionDK,
LifeTimeHK = lifetimeHK,
LifeTimeDK = lifetimeDK,
LifeTimeRank = highestName or NONE,
})
BenillaHonorPane_SetRank("HonorFrame", UnitPVPRank("player"), GetPVPRankProgress(), "player")
HonorFrame_SetLevel()
end
]]></Script>
<!--
The row's SHARED half (ref-tpl l.3-18): a 278x12 strip and the hover that explains it. The
five leaves below add the two FontStrings, because this engine's `inherits=` CONCATENATES a
template's children onto the inheriting node's rather than merging them by name
(`framexml::merge`) — so a leaf that re-declared `$parentText` would get TWO of them, both
drawn, sharing one name. Everything that can be hoisted is hoisted; the regions cannot be.
`enableMouse` is required for the hover to fire at all (this file's header, deviation 7).
-->
<Frame name="HonorFrameTextButtonTemplate" virtual="true" enableMouse="true">
<Size><AbsDimension x="278" y="12"/></Size>
<Scripts>
<OnEnter>BenillaHonorRow_OnEnter(self)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<!--
The five leaves (ref-tpl l.19-148). Each is the same pair of strings in the same two places
and differs in exactly two things: WHICH label it carries, and what colour its figure is.
The label is the player's own GlobalStrings key, resolved by the loader (a `text=` attribute
is a lookup, not a literal); the figure's colour rides a font object rather than a colour
call, which is how every other coloured string in this UI is painted.
The value's `RIGHT` anchored to the row's own `LEFT` plus 278 is the reference's spelling of
"right-aligned at the row's right edge", quoted rather than restated as an anchor to RIGHT.
Honorable kills: GREEN.
-->
<Frame name="HonorFrameHKButtonTemplate" inherits="HonorFrameTextButtonTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontHighlightSmall" text="HONORABLE_KILLS" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</FontString>
<FontString name="$parentValue" inherits="GameFontGreenSmall">
<Anchors>
<Anchor point="RIGHT" relativePoint="LEFT"><Offset><AbsDimension x="278" y="1"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>BenillaHonorRow_OnLoad(self, HONORABLE_KILLS, NEWBIE_TOOLTIP_HONORABLE_KILLS)</OnLoad>
</Scripts>
</Frame>
<!-- Dishonorable kills: RED (ref-tpl l.45-70), because a dishonorable kill costs you rank —
the colour is the pane telling you which way the number moves you. -->
<Frame name="HonorFrameDKButtonTemplate" inherits="HonorFrameTextButtonTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontHighlightSmall" text="DISHONORABLE_KILLS" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</FontString>
<FontString name="$parentValue" inherits="GameFontRedSmall">
<Anchors>
<Anchor point="RIGHT" relativePoint="LEFT"><Offset><AbsDimension x="278" y="1"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>BenillaHonorRow_OnLoad(self, DISHONORABLE_KILLS, NEWBIE_TOOLTIP_DISHONORABLE_KILLS)</OnLoad>
</Scripts>
</Frame>
<!-- Honor earned in the period (ref-tpl l.71-96). The key is HONOR_CONTRIBUTION_POINTS and the
string it holds in 1.12 is the single word "Honor" — the key is the older name for the same
figure, kept because it is the key the player's own string table actually has. -->
<Frame name="HonorFrameContributionButtonTemplate" inherits="HonorFrameTextButtonTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontHighlightSmall" text="HONOR_CONTRIBUTION_POINTS" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</FontString>
<FontString name="$parentValue" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="RIGHT" relativePoint="LEFT"><Offset><AbsDimension x="278" y="1"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>BenillaHonorRow_OnLoad(self, HONOR_CONTRIBUTION_POINTS, NEWBIE_TOOLTIP_HONOR_CONTRIBUTION_POINTS)</OnLoad>
</Scripts>
</Frame>
<!-- Last week's standing (ref-tpl l.97-122): where you placed against everyone on your own
side. Only the Last Week section has one — it is a ranking of a finished week. -->
<Frame name="HonorFrameStandingButtonTemplate" inherits="HonorFrameTextButtonTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontHighlightSmall" text="HONOR_STANDING" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</FontString>
<FontString name="$parentValue" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="RIGHT" relativePoint="LEFT"><Offset><AbsDimension x="278" y="1"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>BenillaHonorRow_OnLoad(self, HONOR_STANDING, NEWBIE_TOOLTIP_HONOR_STANDING)</OnLoad>
</Scripts>
</Frame>
<!-- The highest rank ever held (ref-tpl l.123-148). Its label and its TIP TITLE deliberately
differ, and that is the reference's own choice, not a slip: the row is labelled "Highest
Rank" because that is what the figure is, while the hover is titled "Rank" because the
paragraph under it explains ranking in general. -->
<Frame name="HonorFrameRankButtonTemplate" inherits="HonorFrameTextButtonTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontHighlightSmall" text="HONOR_HIGHEST_RANK" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</FontString>
<FontString name="$parentValue" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="RIGHT" relativePoint="LEFT"><Offset><AbsDimension x="278" y="1"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>BenillaHonorRow_OnLoad(self, RANK, NEWBIE_TOOLTIP_RANK)</OnLoad>
</Scripts>
</Frame>
<!-- id=5, the reference's own (ref l.5): a page's id is the slot it occupies in the WINDOW'S
OWN tab row, and ToggleCharacter selects the tab from it. `character_tests.rs` pins every
page's id against its slot in BENILLA_CHARACTERFRAME_SUBFRAMES, and with this page the two
cover the real client's whole row. -->
<Frame name="HonorFrame" enableMouse="true" parent="CharacterFrame" hidden="true" id="5">
<!-- The real 384x512 page. The explicit `<Size>` + TOPLEFT `<Anchor>` below is an exact
equivalent of the reference's `setAllPoints="true"` here — CharacterFrame, the parent
this frame declares, is itself 384x512 (ref CharacterFrame.xml l.4-7) — and it is kept
rather than swapped because it is the geometry the page ships with, not because the
attribute would not work. ReputationFrame.xml, SkillFrame.xml and
PetPaperDollFrame.xml all carry the identical pair. -->
<Size><AbsDimension x="384" y="512"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="CharacterFrame" relativePoint="TOPLEFT"/></Anchors>
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="75"/></HitRectInsets>
<Layers>
<Layer level="BACKGROUND">
<!-- The four quadrant slabs (ref l.10-58), at the reference's own four TOPLEFT
offsets. The "General" set is the variant every PaperDollInfoFrame page shares
and carries the portrait-ring cutout (this file's header comment). -->
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="258" y="-1"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-BottomLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-257"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-BottomRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="258" y="-257"/></Offset></Anchor></Anchors>
</Texture>
<!-- The level/race/class line, under the window's own name (ref l.59-67). This page
is the only sibling of the paper doll that carries one; the guild line beside
it in the reference is dropped with its twin (header deviation 5). The
reference ships a "Level level race class" placeholder in the attribute, which
only shows on a page that has not been painted — and this one is painted before
it can be seen, so the placeholder is dropped (the ReputationFrame.xml
precedent for the reference's own dead placeholders). -->
<FontString name="HonorLevelText" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="TOP" relativeTo="CharacterNameText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<Layer level="ARTWORK">
<!-- The pane's OWN art, over the shared background: the rank plaque and the five
section plates (ref l.79-126). Four slabs again, but not the same four sizes —
the bottom pair is 128 tall, not 256. -->
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Honor-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-69"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Honor-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="275" y="-69"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Honor-BottomLeft">
<Size><AbsDimension x="256" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-325"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Honor-BottomRight">
<Size><AbsDimension x="128" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="275" y="-325"/></Offset></Anchor></Anchors>
</Texture>
<!-- The rank title and the "(Rank N)" beside it (ref l.127-144). The title's y is
the repaint's, not the XML's (header deviation 6); the pair is re-centred
together on every repaint by BenillaHonorPane_SetRank. -->
<FontString name="HonorFrameCurrentPVPTitle" inherits="GameFontNormal">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-83"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="HonorFrameCurrentPVPRank" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="LEFT" relativeTo="HonorFrameCurrentPVPTitle" relativePoint="RIGHT"><Offset><AbsDimension x="5" y="-1"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- The five section headings (ref l.145-189), each chained off the last. The gaps
are NOT uniform (41/43/42/64) because each section holds a different number of
rows; the 64 before Lifetime is the three-row Last Week section above it. Each
`text=` is the player's own GlobalStrings key, resolved by the loader — note
the first one reads "Today" in 1.12, not "This Session". -->
<FontString name="HonorFrameCurrentSessionTitle" inherits="GameFontNormal" text="HONOR_THIS_SESSION">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="36" y="-112"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="HonorFrameYesterdayTitle" inherits="GameFontNormal" text="HONOR_YESTERDAY">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameCurrentSessionTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-41"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="HonorFrameThisWeekTitle" inherits="GameFontNormal" text="HONOR_THISWEEK">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameYesterdayTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-43"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="HonorFrameLastWeekTitle" inherits="GameFontNormal" text="HONOR_LASTWEEK">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameThisWeekTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-42"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="HonorFrameLifeTimeTitle" inherits="GameFontNormal" text="HONOR_LIFETIME">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameLastWeekTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-64"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<Layer level="OVERLAY">
<!-- The rank badge, left of the title (ref l.191-204). No `file=` here: the path is
built per rank at repaint time (BenillaHonorPane_SetRank), and there is no
badge at all below rank 1 — an unpainted texture is an empty one. -->
<Texture name="HonorFramePvPIcon">
<Size><AbsDimension x="24" y="24"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="HonorFrameCurrentPVPTitle" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- THE TWELVE ROWS (ref l.207-314), section by section. Each section's first row
hangs off its heading's BOTTOMLEFT indented 10, and each following row chains off
the one above it. The row SET per section is the shape of the data, not a layout
choice: Today is the only section that shows dishonorable kills beside honorable
ones without an honor figure (honor for today is not banked yet), the two middle
sections trade the dishonorable count for the honor earned, Last Week adds the
standing that closed it, and Lifetime trades honor for the highest rank ever held.
Today. -->
<Frame name="HonorFrameCurrentHK" inherits="HonorFrameHKButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameCurrentSessionTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="10" y="-3"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="HonorFrameCurrentDK" inherits="HonorFrameDKButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameCurrentHK" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor>
</Anchors>
</Frame>
<!-- Yesterday. -->
<Frame name="HonorFrameYesterdayHK" inherits="HonorFrameHKButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameYesterdayTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="10" y="-3"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="HonorFrameYesterdayContribution" inherits="HonorFrameContributionButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameYesterdayHK" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor>
</Anchors>
</Frame>
<!-- This Week. -->
<Frame name="HonorFrameThisWeekHK" inherits="HonorFrameHKButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameThisWeekTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="10" y="-3"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="HonorFrameThisWeekContribution" inherits="HonorFrameContributionButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameThisWeekHK" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor>
</Anchors>
</Frame>
<!-- Last Week: the only section with three rows, and the only one with a standing. -->
<Frame name="HonorFrameLastWeekHK" inherits="HonorFrameHKButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameLastWeekTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="10" y="-3"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="HonorFrameLastWeekContribution" inherits="HonorFrameContributionButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameLastWeekHK" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="HonorFrameLastWeekStanding" inherits="HonorFrameStandingButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameLastWeekContribution" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor>
</Anchors>
</Frame>
<!-- Lifetime. -->
<Frame name="HonorFrameLifeTimeHK" inherits="HonorFrameHKButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameLifeTimeTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="10" y="-4"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="HonorFrameLifeTimeDK" inherits="HonorFrameDKButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameLifeTimeHK" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="HonorFrameLifeTimeRank" inherits="HonorFrameRankButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="HonorFrameLifeTimeDK" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
</Anchors>
</Frame>
<!-- The rank hover (ref l.315-334): an invisible strip spanning badge to ordinal, so
the whole "badge Marshal (Rank 10)" phrase explains itself. Two anchors and no
width of its own — the LEFT edge is the badge's, the RIGHT edge the ordinal's, so
the strip re-fits itself when either moves. -->
<Frame name="HonorFrameRankButton" enableMouse="true">
<Size><AbsDimension x="10" y="24"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="HonorFramePvPIcon"/>
<Anchor point="RIGHT" relativeTo="HonorFrameCurrentPVPRank"/>
</Anchors>
<Scripts>
<OnEnter>BenillaTooltip_AddNewbieTipFor(self, RANK, HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b, NEWBIE_TOOLTIP_RANK)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<!-- The progress bar (ref l.335-370): how far through the current rank you are, 0..1.
It reuses the skills bar's own fill art, which is the reference's choice.
The two frame-level drops are the reference's and they are the mechanism, not
decoration. The bar's fill is a BORDER-layer texture on a child frame, and the
pane's plaque art is ARTWORK on the pane itself: at the child's natural level the
fill would sort above the plaque and paint over the socket cut for it. Dropping the
bar to the pane's own level puts the two in one bucket, where the draw layer
decides and ARTWORK wins. The hover frame inside it drops again for the same
reason, and because it carries no art its level only decides where a mouse lands. -->
<StatusBar name="HonorFrameProgressBar" enableMouse="true" drawLayer="BORDER" minValue="0" maxValue="1" defaultValue="0">
<Size><AbsDimension x="315" y="29"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-77"/></Offset></Anchor></Anchors>
<Frames>
<Frame name="HonorFrameProgressButton" enableMouse="true" setAllPoints="true">
<Scripts>
<OnLoad>self:SetFrameLevel(self:GetFrameLevel() - 1)</OnLoad>
<OnEnter>BenillaTooltip_AddNewbieTipFor(self, RANK_POSITION, HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b, NEWBIE_TOOLTIP_RANK_POSITION)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
</Frames>
<Scripts>
<OnLoad>self:SetFrameLevel(self:GetFrameLevel() - 1)</OnLoad>
</Scripts>
<BarTexture name="$parentBar" file="Interface\PaperDollInfoFrame\UI-Character-Skills-Bar"/>
</StatusBar>
</Frames>
<Scripts>
<OnLoad>HonorFrame_OnLoad(self)</OnLoad>
<OnEvent>HonorFrame_OnEvent(event)</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -54,6 +54,113 @@
(CharacterFrame.xml hit the identical wall) — click-only, so no OnUpdate is wired.
-->
<Script><![CDATA[
-- ══ THE HONOUR-PANE PAINTERS, rehomed (1751 window 15) ═══════════════════════════════
-- These four lived in our HonorFrame.xml, shared by the character sheet's honour page and
-- this window's inspect twin. That page is the reference's own `Interface\FrameXML\
-- HonorFrame.xml` now, which paints itself with its own `HonorFrame_Update`, so the only
-- remaining caller is this file — and a helper belongs with its caller.
--
-- `BenillaHonorRow_OnLoad`/`_OnEnter` come with them: they drive the row hover that the
-- reference's page does not have (a benilla addition, and a `frame_flag_gate` KNOWN entry
-- until the swap made it true only of this window). The names keep their `Benilla` prefix
-- because that is what they are — ours, on our window, colliding with nothing.
-- ── The row: two strings and a hover ────────────────────────────────────────────────────
-- A row remembers the pair its hover shows: its own label as the tip's title, and the
-- matching detailed explanation as the body. Both are the player's strings, passed in by
-- the instance below rather than baked into a template, so the five leaf templates differ
-- only in what they LOOK like.
function BenillaHonorRow_OnLoad(row, title, detail)
row.title = title
row.detail = detail
end
-- The house newbie tip (this file's header, deviation 2): label in highlight white, the
-- explanation wrapped under it in NORMAL_FONT_COLOR, both only when the player has detailed
-- tips on.
function BenillaHonorRow_OnEnter(row)
BenillaTooltip_AddNewbieTipFor(row, row.title,
HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b, row.detail)
end
-- ── The two painters both honor panes share (ref-lua l.19-77 and its inspect twin) ───────
-- Paint a set of counters. `pane` is the owning frame's name, `values` a
-- `{ [rowSuffix] = number }` table: each row frame is `<pane><suffix>` and its figure lives
-- in that frame's own `$parentValue` string, so a repaint is a table walk rather than
-- twelve hand-written assignments. Rows are looked up by name and skipped when absent,
-- which is what lets a caller paint a SUBSET (the character pane repaints its weekly
-- sections on a different trigger from its session ones — see HonorFrame_Update).
--
-- A nil figure is likewise not painted, and that falls out of Lua rather than being coded:
-- a nil value never enters the table at all, so `pairs` cannot reach it. That is the
-- behaviour we want on the inspect side, where the whole twelve-wide read answers with
-- NOTHING until the server's reply lands — better a row that has not been filled in yet
-- than a row confidently showing a zero nobody sent.
function BenillaHonorPane_SetRows(pane, values)
for suffix, value in pairs(values) do
local figure = getglobal(pane .. suffix .. "Value")
if figure then
figure:SetText(value)
end
end
end
-- Paint the rank block: the badge, the rank's title, the "(Rank N)" that trails it, and the
-- progress bar under both. `rank` is the unit's rank as the engine reports it, `progress`
-- the 0..1 position within that rank, `unit` the token whose faction colours the bar.
--
-- Called with ("HonorFrame", …, "player") by the page below and with
-- ("BenillaInspectHonorFrame", …, the inspected token) by InspectFrame.xml.
function BenillaHonorPane_SetRank(pane, rank, progress, unit)
local rankName, rankNumber = GetPVPRankInfo(rank)
local title = getglobal(pane .. "CurrentPVPTitle")
local ordinal = getglobal(pane .. "CurrentPVPRank")
-- THE NIL IS THE MECHANISM, not an edge case. The rank names are GlobalStrings keyed
-- `PVP_RANK_<rank>_<team>` and there is no `PVP_RANK_0_*`: rank 0 means "no rank at
-- all", which is every character who has never taken an honorable kill. So the name
-- comes back nil there and the pane says NONE. The NUMBER is not guarded, and that is
-- deliberate too: it is a rank arithmetic result, never absent, and swallowing a nil
-- would hide a broken binding instead of reporting it.
title:SetText(rankName or NONE)
-- `RANK or ""` rather than a bare concat: a harness with no install behind it has no
-- GlobalStrings, and taking the pane down over a missing label helps nobody.
ordinal:SetText("(" .. (RANK or "") .. " " .. rankNumber .. ")")
-- The badge is built HERE, not by the engine: the file name is the rank number padded
-- to two digits, and it is shown only above zero. Verified against the patch chain this
-- session: `PvPRank01`..`PvPRank15` exist and `PvPRank00`/`16`/`17`/`18`/`19` do not,
-- which is exactly the range the arithmetic can produce (the fourteen player ranks plus
-- the PvP-leader one); zero and the four negative ranks have no badge to show.
local badge = getglobal(pane .. "PvPIcon")
if rankNumber > 0 then
badge:SetTexture(format("Interface\\PvPRankBadges\\PvPRank%02d", rankNumber))
badge:Show()
else
badge:Hide()
end
-- The bar wears the unit's own faction: Alliance navy, everything else Horde red. The
-- fork is a positive test for Alliance with an `else`, not a two-sided test, so a unit
-- whose faction group reads nil (a race the app has no snapshot for yet — ui_unit.rs's
-- own note) still gets a painted bar rather than a bare trough.
local bar = getglobal(pane .. "ProgressBar")
if UnitFactionGroup(unit) == "Alliance" then
bar:SetStatusBarColor(0.05, 0.15, 0.36)
else
bar:SetStatusBarColor(0.63, 0.09, 0.09)
end
bar:SetValue(progress)
-- Re-centre the PAIR, not the title. The title is centred on the pane and then shifted
-- left by half the width of the "(Rank N)" that trails it, so title and ordinal read as
-- one centred phrase. It has to be redone on every repaint because that width moves
-- with the number and with the locale.
title:SetPoint("TOP", pane, "TOP", -ordinal:GetWidth() / 2, -83)
end
-- ── GlobalStrings values this window uses (ref-GlobalStrings.lua, cited per line — the
-- `X = X or "..."` idiom is this codebase's convention, CharacterFrame.xml/CastingBar.xml:
-- a real GlobalStrings.lua, if one ever loads, wins over the fallback). Declared HERE and
@@ -1,389 +0,0 @@
<Ui>
<!--
benilla's guild-charter window (decision 1672) — the sheet of parchment nine people have to
sign before a guild exists. It opens on a charter: yours, clicked in your bags, or somebody
else's, offered to you.
The geometry and art are the real `PetitionFrame.xml`'s (extracted at
`WoW/_extracted_framexml/PetitionFrame.{xml,lua}`; `ref l.N` cites are into those). Every
global, frame name, event, texture, string and number is the reference's — the contract
addons bind to. What is ours is the expression: `PetitionFrame_Update` is four small painters
over a loop instead of one function, the event ladder is a named function, and the nine name
rows are built by a loop rather than nine near-identical FontString blocks.
ONE WINDOW, TWO FACES, decided by a single value (ref PetitionFrame.lua:10-21):
isOriginator → the LEADER's view: "Request Signature" + "Rename Guild", and the
leader instructions.
not → the SIGNER's view: "Sign Charter", and the member instructions.
The two buttons occupy the SAME anchor (ref l.304-336, both BOTTOMLEFT +22,+72) — they are
never both visible, which is why Show/Hide rather than Enable/Disable does the switching.
WHAT THE WINDOW OPENS WITHOUT, AND WHY IT REPAINTS. `SMSG_PETITION_SHOW_SIGNATURES` carries
an item guid, an owner guid, a petition id and a list of signer guids — no text at all. The
guild's proposed name comes from a separate petition-record query and the names from the
name cache, so the first paint is deliberately partial: the title reads empty and unresolved
signers read blank until their lookups land, at which point the engine re-fires
PETITION_SHOW and this file repaints. `ShowUIPanel` early-returns on an already-visible
frame and the open sound rides OnShow, so a repaint costs one `PetitionFrame_Update`.
(`ui_petition`'s module doc has the mechanism and flags what is INFERRED in it.)
THE TWO SIGNATURE NUMBERS ARE DIFFERENT NUMBERS, and this file uses both — that is the
reference's own doing, not a wrinkle we added. `MAX_PETITION_SIGNATURES` (9) sizes the name
ROWS; `maxSignatures`, from GetPetitionInfo, is what the wire says this charter REQUIRES and
is what disables Request Signature (ref l.30-40). They agree at 9 on a stock server and need
not on one that lowers its signature requirement.
UIPanel slot system (decision 0084 §2): no hardcoded <Anchors> — ShowUIPanel seats it in the
left slot from `UIPanelWindows["PetitionFrame"]` (UiPanels.xml), the reference's own
registration (`UIParent.lua:34`, area="left", pushable=0).
-->
<Script><![CDATA[
-- ── The charter's strings (GlobalStrings.lua, verbatim) ────────────────────────────────
-- The standalone-load copy of every key this file's `text=` attributes and Lua name; see
-- GuildRegistrarFrame.xml's twin block for why it is `X = X or "…"` and never a bare `=`.
GUILD_NAME = GUILD_NAME or "Guild Name" -- l.2090
GUILD_RANK0_DESC = GUILD_RANK0_DESC or "Guild Master" -- l.2102
MEMBERS = MEMBERS or "Members" -- l.2683
NOT_YET_SIGNED = NOT_YET_SIGNED or "<not yet signed>" -- l.2807
GUILD_CHARTER_TEMPLATE = GUILD_CHARTER_TEMPLATE or "%s Guild Charter" -- l.2060
SIGN_CHARTER = SIGN_CHARTER or "Sign Charter" -- l.3464
REQUEST_SIGNATURE = REQUEST_SIGNATURE or "Request Signature" -- l.3337
RENAME_GUILD = RENAME_GUILD or "Rename Guild" -- l.3318
RENAME_GUILD_LABEL = RENAME_GUILD_LABEL or "Enter new guild name:" -- l.3319
GUILD_PETITION_LEADER_INSTRUCTIONS = GUILD_PETITION_LEADER_INSTRUCTIONS or "Select a player you wish to invite and click <request signature>. To create this guild, turn it in to the guild registrar when you have filled the charter." -- l.2099
GUILD_PETITION_MEMBER_INSTRUCTIONS = GUILD_PETITION_MEMBER_INSTRUCTIONS or "Click the <Sign Charter> button to become a charter member of this guild." -- l.2100
ACCEPT = ACCEPT or "Accept" -- l.11
CANCEL = CANCEL or "Cancel" -- l.465
CLOSE = CLOSE or "Close" -- l.760
-- The nine name ROWS (ref PetitionFrame.lua:1). Not the signature requirement — that comes
-- off the wire per charter; see the file header.
MAX_PETITION_SIGNATURES = 9
-- The rename dialog (ref StaticPopup.lua:133-160). Registered here rather than in
-- UiPanels.xml for the same reason the guild dialogs live in FriendsFrame.xml: a popup
-- belongs to the window that raises it. 24 letters is the server's MAX_CHARTER_NAME, so
-- the box cannot compose a name that will be refused for length.
--
-- Accept and Enter run the same two lines, which is why they are one local function; the
-- reference spells them out twice (l.139-147). Note the ref's own Enter arm hides the
-- dialog and its Accept arm does not — the popup engine hides on a plain Accept anyway.
StaticPopupDialogs["RENAME_GUILD"] = {
text = RENAME_GUILD_LABEL,
button1 = ACCEPT,
button2 = CANCEL,
hasEditBox = 1,
maxLetters = 24,
OnAccept = function()
RenamePetition(getglobal(this:GetParent():GetName() .. "EditBox"):GetText())
end,
EditBoxOnEnterPressed = function()
RenamePetition(getglobal(this:GetParent():GetName() .. "EditBox"):GetText())
this:GetParent():Hide()
end,
OnShow = function()
getglobal(this:GetName() .. "EditBox"):SetFocus()
end,
OnHide = function()
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:SetFocus()
end
getglobal(this:GetName() .. "EditBox"):SetText("")
end,
timeout = 0,
exclusive = 1,
whileDead = 1,
}
-- ref PetitionFrame.lua:2-42, as four painters. The reference has one function; the split
-- is ours, because the four jobs share nothing but the tuple they read.
function PetitionFrame_Update()
local petitionType, title, bodyText, maxSignatures, originatorName, isOriginator = GetPetitionInfo()
PetitionFrame_PaintRole(isOriginator)
PetitionFrame_PaintHeader(petitionType, title, originatorName)
local numNames = PetitionFrame_PaintSignatures()
PetitionFrame_PaintRequestButton(numNames, maxSignatures)
end
-- Which face the window wears. Show/Hide rather than Enable/Disable because the two
-- buttons sit on the same anchor (see the header).
function PetitionFrame_PaintRole(isOriginator)
if isOriginator then
PetitionFrameRequestButton:Show()
PetitionFrameRenameButton:Show()
PetitionFrameSignButton:Hide()
PetitionFrameInstructions:SetText(GUILD_PETITION_LEADER_INSTRUCTIONS)
else
PetitionFrameRequestButton:Hide()
PetitionFrameRenameButton:Hide()
PetitionFrameSignButton:Show()
PetitionFrameInstructions:SetText(GUILD_PETITION_MEMBER_INSTRUCTIONS)
end
-- The Sign button's own gate is a separate question from which face is showing: a
-- charter can be a signer's view and still not be signable (already signed, already
-- guilded, full).
if CanSignPetition() then
PetitionFrameSignButton:Enable()
else
PetitionFrameSignButton:Disable()
end
end
-- The banner and the two named lines. `petitionType` has exactly one value in 1.12,
-- "charter"; the else arm is the reference's own dead branch and is kept as written.
function PetitionFrame_PaintHeader(petitionType, title, originatorName)
if petitionType == "charter" then
PetitionFrameNpcNameText:SetText(format(GUILD_CHARTER_TEMPLATE, title))
PetitionFrameCharterName:SetText(title)
PetitionFrameMasterName:SetText(originatorName)
else
PetitionFrameNpcNameText:SetText("Petition")
end
end
-- Nine rows, always nine: a signed row shows its name, an unsigned one shows
-- "<not yet signed>". Returns how many have signed, which the Request button needs.
function PetitionFrame_PaintSignatures()
local numNames = GetNumPetitionNames()
for i = 1, MAX_PETITION_SIGNATURES do
local row = getglobal("PetitionFrameMemberName" .. i)
if i <= numNames then
row:SetText(GetPetitionNameInfo(i))
else
row:SetText(NOT_YET_SIGNED)
end
end
return numNames
end
-- Disabled once the charter is full. `maxSignatures` is the WIRE's requirement, not
-- MAX_PETITION_SIGNATURES (ref PetitionFrame.lua:38).
function PetitionFrame_PaintRequestButton(numNames, maxSignatures)
if numNames >= maxSignatures then
PetitionFrameRequestButton:Disable()
else
PetitionFrameRequestButton:Enable()
end
end
-- ref l.375-387. The `IsVisible` re-test after ShowUIPanel is the reference's own: the
-- panel manager can refuse, and a charter session the engine thinks is open with no window
-- on screen would strand it.
--
-- PETITION_SHOW also arrives for a REPAINT of a charter already on screen (a name or the
-- title resolving), which costs one Update and no sound — see the file header.
function PetitionFrame_OnEvent(event)
if event == "PETITION_SHOW" then
ShowUIPanel(PetitionFrame)
if not PetitionFrame:IsVisible() then
ClosePetition()
return
end
PetitionFrame_Update()
elseif event == "PETITION_CLOSED" then
HideUIPanel(PetitionFrame)
end
end
]]></Script>
<Frame name="PetitionFrame" toplevel="true" parent="UIParent" movable="true" enableMouse="true" hidden="true">
<Size><AbsDimension x="384" y="512"/></Size>
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="70"/></HitRectInsets>
<Layers>
<Layer level="BACKGROUND">
<!-- The charter's own icon, not an NPC portrait (ref l.20-30): this window has no
NPC. It is a piece of paper. -->
<Texture name="PetitionFramePortrait" file="Interface\PetitionFrame\GuildCharter-Icon">
<Size><AbsDimension x="58" y="58"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-8"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The quest-greeting parchment, four quadrant slabs (ref l.32-63). -->
<Texture file="Interface\QuestFrame\UI-QuestGreeting-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
<Texture file="Interface\QuestFrame\UI-QuestGreeting-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPRIGHT"/></Anchors>
</Texture>
<Texture file="Interface\QuestFrame\UI-QuestGreeting-BotLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture file="Interface\QuestFrame\UI-QuestGreeting-BotRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<!-- ref l.66-77. Defined but never written by the shipped UI; kept because it is
a name an addon can reach (GuildRegistrarText's twin). -->
<FontString name="PetitionText" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-10"/></Offset></Anchor></Anchors>
</FontString>
<!-- The header block: a static label over its value, three times — "Guild Name",
"Guild Master", "Members" — then the nine signature rows, then the
instructions (ref l.78-257). The labels come from GlobalStrings via `text=`
and are never rewritten; only the two *Name strings and the rows are painted.
The whole column is one anchor chain, so a row's height change pushes
everything below it — which is what lets the instructions sit under a list
that is always nine rows tall. -->
<FontString name="PetitionFrameCharterTitle" inherits="QuestTitleFont" text="GUILD_NAME" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="30" y="-95"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="PetitionFrameCharterName" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PetitionFrameCharterTitle" relativePoint="BOTTOMLEFT"/>
</Anchors>
</FontString>
<FontString name="PetitionFrameMasterTitle" inherits="QuestTitleFont" text="GUILD_RANK0_DESC" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PetitionFrameCharterName" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PetitionFrameMasterName" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PetitionFrameMasterTitle" relativePoint="BOTTOMLEFT"/>
</Anchors>
</FontString>
<FontString name="PetitionFrameMemberTitle" inherits="QuestTitleFont" text="MEMBERS" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PetitionFrameMasterName" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Row 1 anchors to the "Members" label; rows 2..9 chain off their predecessor
(ref l.138-245, nine identical blocks). Written out because a FontString
cannot be created from Lua in 1.12 and the chain is what makes the column. -->
<FontString name="PetitionFrameMemberName1" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberTitle" relativePoint="BOTTOMLEFT"/>
</Anchors>
</FontString>
<FontString name="PetitionFrameMemberName2" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName1" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameMemberName3" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName2" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameMemberName4" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName3" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameMemberName5" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName4" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameMemberName6" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName5" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameMemberName7" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName6" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameMemberName8" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName7" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameMemberName9" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName8" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<FontString name="PetitionFrameInstructions" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PetitionFrameMemberName9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The title banner (ref l.261-286): "<Guild> Guild Charter". -->
<Frame name="PetitionNpcNameFrame">
<Size><AbsDimension x="300" y="14"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="PetitionFrame" relativePoint="TOP"><Offset><AbsDimension x="0" y="-23"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString name="PetitionFrameNpcNameText" inherits="GameFontHighlight"/>
</Layer>
</Layers>
<Scripts>
<OnLoad>this:SetFrameLevel(this:GetFrameLevel() + 2)</OnLoad>
</Scripts>
</Frame>
<Button name="PetitionFrameCancelButton" inherits="UIPanelButtonTemplate" text="CLOSE">
<Size><AbsDimension x="75" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="PetitionFrame" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-40" y="72"/></Offset></Anchor>
</Anchors>
<Scripts><OnClick>HideUIPanel(PetitionFrame)</OnClick></Scripts>
</Button>
<!-- Sign and Request share this anchor: exactly one of them is ever shown. -->
<Button name="PetitionFrameSignButton" inherits="UIPanelButtonTemplate" text="SIGN_CHARTER">
<Size><AbsDimension x="110" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="PetitionFrame"><Offset><AbsDimension x="22" y="72"/></Offset></Anchor>
</Anchors>
<Scripts><OnClick>SignPetition()</OnClick></Scripts>
</Button>
<!-- Offers the charter to the current TARGET — the verb carries no argument, so with
nobody targeted nothing is sent (`ui_petition`'s drain). -->
<Button name="PetitionFrameRequestButton" inherits="UIPanelButtonTemplate" text="REQUEST_SIGNATURE">
<Size><AbsDimension x="140" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="PetitionFrame"><Offset><AbsDimension x="22" y="72"/></Offset></Anchor>
</Anchors>
<Scripts><OnClick>OfferPetition()</OnClick></Scripts>
</Button>
<Button name="PetitionFrameCloseButton" inherits="UIPanelCloseButton">
<Anchors>
<Anchor point="CENTER" relativeTo="PetitionFrame" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-42" y="-30"/></Offset></Anchor>
</Anchors>
</Button>
<!-- Two-point width (ref l.347-362): the button stretches to fill whatever is left
between Request and Close, so its label fits at any panel width. -->
<Button name="PetitionFrameRenameButton" inherits="UIPanelButtonTemplate" text="RENAME_GUILD">
<Size><AbsDimension x="10" y="22"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="PetitionFrameRequestButton" relativePoint="RIGHT"><Offset><AbsDimension x="-3" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="PetitionFrameCancelButton" relativePoint="LEFT"/>
</Anchors>
<Scripts><OnClick>StaticPopup_Show("RENAME_GUILD")</OnClick></Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
this:RegisterEvent("PETITION_SHOW")
this:RegisterEvent("PETITION_CLOSED")
</OnLoad>
<OnEvent>PetitionFrame_OnEvent(event)</OnEvent>
<OnShow>PlaySound("igQuestListOpen")</OnShow>
<!-- ClosePetition() on hide clears the engine's charter session — the same load-bearing
role CloseGossip() has on GossipFrame. -->
<OnHide>
PlaySound("igQuestListClose")
ClosePetition()
</OnHide>
<!-- ref l.395-397: the window swallows the wheel rather than letting it reach whatever
is behind it. -->
<OnMouseWheel>return</OnMouseWheel>
</Scripts>
</Frame>
</Ui>
+18 -13
View File
@@ -845,10 +845,15 @@
<Scripts><OnClick>QuestRewardItem_OnClick()</OnClick></Scripts>
</Button>
<!-- A greeting quest-title button (ref QuestTitleButtonTemplate, QuestFrameTemplates.xml l.174-213):
<!-- **The reference's own name**, since 1751's thirteenth window. It was
`BenillaQuestTitleButtonTemplate` while both this file and its only consumers were ours;
the sourced `Interface\FrameXML\GuildRegistrarFrame.xml` inherits the reference's name
for its two service rows, and an unknown template is a loader WARNING — the frame builds
and silently has no art — so the rename is what keeps that window skinned.
A greeting quest-title button (ref QuestTitleButtonTemplate, QuestFrameTemplates.xml l.174-213):
a bullet + the quest title. -->
<Button name="BenillaQuestTitleButtonTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="285" y="16"/></Size>
<Button name="QuestTitleButtonTemplate" virtual="true">
<Size><AbsDimension x="300" y="16"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\QuestFrame\UI-Quest-BulletPoint">
@@ -1035,18 +1040,18 @@
</Layer>
</Layers>
<Frames>
<Button name="QuestTitleButton1" inherits="BenillaQuestTitleButtonTemplate">
<Button name="QuestTitleButton1" inherits="QuestTitleButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="CurrentQuestsText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-10" y="-5"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTitleButton2" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton3" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton4" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton5" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton6" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton7" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton8" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton9" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton10" inherits="BenillaQuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton2" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton3" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton4" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton5" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton6" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton7" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton8" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton9" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestTitleButton10" inherits="QuestTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestTitleButton9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
</Frames>
</Frame>
</ScrollChild>
@@ -69,6 +69,16 @@
to be the real widget method rather than a benilla global.
-->
<Script><![CDATA[
-- **`MAX_QUESTS` belongs to the quest LOG in 1.12** — `QuestLogFrame.lua:2`, the file this
-- one is our counterpart to — and it is here since 1751's sixteenth window. It was
-- declared in our QuestTimerFrame.xml as `MAX_QUESTS = MAX_QUESTS or 20`, which was the
-- honest shape while both files were ours; the timer window is the reference's own now and
-- its `QuestTimerFrame_Update` walks `for i = arg.n + 1, MAX_QUESTS` with no fallback, so
-- a nil there is `'for' limit must be a number` on the first repaint.
--
-- Distinct from `MAX_QUESTLOG_QUESTS` below, which the reference also has: this is the
-- quest-log CAPACITY (how many a character may carry), that is the display cap.
MAX_QUESTS = 20 -- MAX_QUESTS (QuestLogFrame.lua:2)
QUESTS_DISPLAYED = 6 -- QUESTS_DISPLAYED (pin §2, QuestLogFrame.lua:1)
QUESTLOG_QUEST_HEIGHT = 16 -- the row pitch the faux kit scrolls by (ref l.4)
MAX_QUESTLOG_QUESTS = 20 -- MAX_QUESTLOG_QUESTS (pin §2, :6) — display cap only
@@ -1,259 +0,0 @@
<Ui>
<!--
The quest-timer window — the small dialog under the minimap that counts a timed quest down
(decision 1150, B234). Geometry, art, layout and behaviour are QUOTED from the real 1.12
QuestTimerFrame (extracted Interface\FrameXML\QuestTimerFrame.xml + .lua): a 160x72
UI-DialogBox backdrop with the 256x64 header art and the QUEST_TIMERS title on it, twenty
140x16 rows stacked flush from TOP -30, and a height of 45 + 16*n for n live timers.
It is a LIST OF TIMES, not a bar. The report this closes guessed a mirror-timer-style bar,
and the reference file settles it: one `SecondsToTime` line per timed quest ("14 Mins 20
Secs"), clickable to open the quest log at that quest, hovering to name it. The mirror
timers (MirrorTimer.xml, 0874) are a different mechanism entirely — those are server-pushed
values with a rate to integrate, this reads the quest log's own deadlines.
The feed is the engine's GetQuestTimers()/GetQuestIndexForTimer(), which subtract the
server's wall clock (SMSG_QUERY_TIME_RESPONSE) from each quest-log slot's absolute deadline
per call — the reference's own division of labour, and why the OnUpdate below can repaint a
ticking number without the quest-log snapshot changing under it. No timed quests → the
bindings return no values at all → the frame hides, which is the ref's own signal.
Departures, each named:
- BARE frame names (no `Benilla` prefix), like DurabilityFrame and GroupLootFrame1 before
it (decision 0591 §3): UIParent_ManageFramePositions resolves its stack through a literal
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 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, 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
positioning) is left out rather than transcribed as dead code.
NOT a departure, though it looks like one: the row buttons are laid out flush (0,0 between
them) while each is 16 tall and the frame grows by exactly 16 per timer. That is the ref's
own arithmetic (45 + 16*n against a 72-tall empty frame), not a rounding of ours.
-->
<Script><![CDATA[
-- ref GlobalStrings.lua:3241 — the window's title, under its real key so the ref's own
-- `text="QUEST_TIMERS"` below resolves through the loader's global-string lookup verbatim
-- (loader/mod.rs `resolve_text`). Declared here because we load no GlobalStrings.lua;
-- guarded, in case a later file ships one.
QUEST_TIMERS = QUEST_TIMERS or "Quest Timers"
-- ref QuestLogFrame.lua:2. Declared here too because the ref's hide loop below runs to it
-- and this file may load before the quest log's own copy.
MAX_QUESTS = MAX_QUESTS or 20
-- ref QuestTimerFrame_OnLoad (QuestTimerFrame.lua:1-6).
function QuestTimerFrame_OnLoad(frame)
frame:RegisterEvent("QUEST_LOG_UPDATE")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
QuestTimerFrameNumTimers = 0
QuestTimerFrameUpdating = nil
end
-- ref QuestTimerFrame_OnEvent (:8-14). The `updating` latch is the ref's own re-entrancy
-- guard: QuestTimerFrame_Update's Show() fires OnShow → the manage pass, and a
-- QUEST_LOG_UPDATE arriving inside that would re-enter this.
function QuestTimerFrame_OnEvent()
if not QuestTimerFrameUpdating then
QuestTimerFrame_Update(GetQuestTimers())
end
end
-- ref QuestTimerFrame_Update (:16-32), with select() for the 5.0 vararg table.
function QuestTimerFrame_Update(...)
QuestTimerFrameUpdating = 1
local n = select("#", ...)
for i = 1, n do
-- `select(i, ...)` expands to EVERY value from i on, and in a call's last argument
-- position they all get passed — so an inlined `SecondsToTime(select(i, ...))`
-- hands timer i+1 to SecondsToTime's `noSeconds` and silently drops the seconds
-- off every row but the last ("14 Mins" instead of "14 Mins 20 Secs"). The local
-- takes the first value only. The ref's `arg[i]` had no such hazard; this is the
-- 5.0→5.1 vararg swap's own trap, caught by the test that expected the seconds.
local seconds = select(i, ...)
getglobal("QuestTimer" .. i .. "Text"):SetText(SecondsToTime(seconds))
getglobal("QuestTimer" .. i):Show()
end
for i = n + 1, MAX_QUESTS do
getglobal("QuestTimer" .. i):Hide()
end
QuestTimerFrameNumTimers = n
local frame = getglobal("QuestTimerFrame")
if n > 0 then
frame:SetHeight(45 + (16 * n))
frame:Show()
else
frame:Hide()
end
QuestTimerFrameUpdating = nil
end
-- ref QuestTimerFrame_OnUpdate (:34-38): re-read every frame while any timer is live —
-- that, not an event, is what makes the number tick. Costs nothing while hidden (the
-- engine's OnUpdate gate is effective-visibility) and nothing while empty (numTimers 0).
function QuestTimerFrame_OnUpdate()
if QuestTimerFrameNumTimers > 0 then
QuestTimerFrame_Update(GetQuestTimers())
end
end
-- ref QuestTimerButton_OnClick (:40-44): open the log at that quest.
function QuestTimerButton_OnClick(id)
local index = GetQuestIndexForTimer(id)
if not index then
return
end
ShowUIPanel(getglobal("QuestLogFrame"))
SelectQuestLogEntry(index)
BenillaQuestLogFrame_Update()
end
]]></Script>
<Button name="QuestTimerTemplate" virtual="true">
<Size>
<AbsDimension x="140" y="16"/>
</Size>
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontHighlightSmall">
<Anchors>
<Anchor point="TOP"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<!-- ref QuestTimerFrame.xml:18-27. The tooltip names the quest the row belongs to;
GetQuestLogTitle's FIRST return only (the rest of its 6-tuple is level/tag/state). -->
<OnEnter>
local index = GetQuestIndexForTimer(self:GetID())
if index then
GameTooltip:SetOwner(self)
GameTooltip:SetText(GetQuestLogTitle(index), 1.0, 1.0, 1.0)
GameTooltip:Show()
end
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
<OnClick>QuestTimerButton_OnClick(self:GetID())</OnClick>
</Scripts>
</Button>
<Frame name="QuestTimerFrame" toplevel="true" parent="UIParent" enableMouse="true" frameStrata="BACKGROUND" hidden="true">
<Size>
<AbsDimension x="160" y="72"/>
</Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="MinimapCluster" relativePoint="BOTTOMRIGHT">
<Offset>
<AbsDimension x="10" y="0"/>
</Offset>
</Anchor>
</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">
<Texture name="QuestTimerHeader" file="Interface\DialogFrame\UI-DialogBox-Header">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="1" y="12"/></Offset></Anchor>
</Anchors>
</Texture>
<FontString inherits="GameFontNormal" text="QUEST_TIMERS">
<Anchors>
<Anchor point="TOP" relativeTo="QuestTimerHeader"><Offset><AbsDimension x="0" y="-13"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The ref's twenty rows (MAX_QUESTS), first at TOP -30 and the rest flush beneath it.
Named/id'd 1..20 because both the ref's row lookup and its timer→quest mapping are
by ordinal. -->
<Button name="QuestTimer1" inherits="QuestTimerTemplate" hidden="true" id="1">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-30"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer2" inherits="QuestTimerTemplate" hidden="true" id="2">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer3" inherits="QuestTimerTemplate" hidden="true" id="3">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer4" inherits="QuestTimerTemplate" hidden="true" id="4">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer3" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer5" inherits="QuestTimerTemplate" hidden="true" id="5">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer4" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer6" inherits="QuestTimerTemplate" hidden="true" id="6">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer5" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer7" inherits="QuestTimerTemplate" hidden="true" id="7">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer6" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer8" inherits="QuestTimerTemplate" hidden="true" id="8">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer7" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer9" inherits="QuestTimerTemplate" hidden="true" id="9">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer8" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer10" inherits="QuestTimerTemplate" hidden="true" id="10">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer9" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer11" inherits="QuestTimerTemplate" hidden="true" id="11">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer10" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer12" inherits="QuestTimerTemplate" hidden="true" id="12">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer11" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer13" inherits="QuestTimerTemplate" hidden="true" id="13">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer12" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer14" inherits="QuestTimerTemplate" hidden="true" id="14">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer13" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer15" inherits="QuestTimerTemplate" hidden="true" id="15">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer14" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer16" inherits="QuestTimerTemplate" hidden="true" id="16">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer15" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer17" inherits="QuestTimerTemplate" hidden="true" id="17">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer16" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer18" inherits="QuestTimerTemplate" hidden="true" id="18">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer17" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer19" inherits="QuestTimerTemplate" hidden="true" id="19">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer18" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestTimer20" inherits="QuestTimerTemplate" hidden="true" id="20">
<Anchors><Anchor point="TOP" relativeTo="QuestTimer19" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>QuestTimerFrame_OnLoad(self)</OnLoad>
<OnUpdate>QuestTimerFrame_OnUpdate()</OnUpdate>
<OnEvent>QuestTimerFrame_OnEvent()</OnEvent>
<!-- ref :46-52 — the stack under the minimap re-flows when this window appears or goes. -->
<OnShow>UIParent_ManageFramePositions()</OnShow>
<OnHide>UIParent_ManageFramePositions()</OnHide>
</Scripts>
</Frame>
</Ui>
-314
View File
@@ -1,314 +0,0 @@
<Ui>
<!--
The stack-split spinner (decision 0216 §6/§D, scratchpad `ref-framexml/StackSplitFrame.xml`/
`.lua`, re-expressed onto our own cursor seam — `SplitContainerItem`/
`OpenStackSplitFrame` already exist in benilla-ui's `cursor.rs`; this file is purely the
window + the Left/Right/Okay/Cancel plumbing around it). One shared instance, like the real
client's `StackSplitFrame` — any owner (a bag slot today; the paper doll / bank / mail slices
reuse it unmodified later) sets `owner.SplitStack = function(owner, n) ... end` before calling
`OpenStackSplitFrame`, and Okay calls it generically (`StackSplitFrameOkay_Click`).
Textures: `Interface\MoneyFrame\UI-MoneyFrame` (the little parchment plate) and
`Interface\MoneyFrame\Arrow-Left/Right-Up/Down/Disabled` are the ref's own — both already
resolve through the shared sprite loader the money-purse coin icons use
(`Interface\MoneyFrame\UI-MoneyIcons`, BagFrame.xml/MerchantFrame.xml), so they're quoted
verbatim rather than substituted. The Okay/Cancel button face (`UI-Panel-Button-*`) is our
own local virtual template, NOT a cross-file `inherits` of `BenillaQuestPanelButtonTemplate`
(QuestFrame.xml) — this file loads BEFORE QuestFrame.xml (see ui_script/mod.rs's load order),
so that template isn't registered yet; the house convention already answered this exact case
for GossipFrame/QuestFrame/QuestLogFrame ("our engine ships no UIPanelButtonTemplate, so we
draw it") — this is one more independent draw of the same real art.
TYPED-DIGIT ENTRY IS LIVE (decision 1319). It used to be this file's headline deferral
("this engine has no plain-frame OnChar/OnKeyDown capture, only EditBox focus"), and the
engine now has the delivery walk that unblocks it (`script::keyboard`, off wow-re's
byte-verified `frame-key-script-delivery.md`). The handlers below are OUR OWN spinner
logic, written from the widget's behaviour, not a transcription of the reference's Lua
(their text is never copied into this repo). `enableKeyboard` on the frame is
what puts it in the walk's bucket at all.
NAMED DEFERRAL:
- The ref's shift-click "insert the item link into the chat editbox" (StackSplitFrame isn't
even opened in that branch — ContainerFrame.lua:568-569 diverts to
`ChatFrameEditBox:Insert` instead of `OpenStackSplitFrame`) never triggers here: the
shift-click gesture itself has no plumbing in this engine (no modifier-key global exists
at all), so there is no path that
reaches this branch to defer. `EditBox:Insert` DOES exist on our engine (editbox.rs) for
when that gap closes.
-->
<Script><![CDATA[
-- OpenStackSplitFrame(maxStack, owner, anchor, anchorTo) — ref StackSplitFrame.lua:2-24,
-- verbatim shape. `owner` must already carry `owner.SplitStack = function(owner, n) ... end`
-- (the reference's own `ContainerFrameItemButton_OnClick` and `BankFrameItemButton_OnLoad`
-- both set it, and both are sourced since 1751). 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 = StackSplitFrame
if frame.owner then
frame.owner.hasStackSplit = nil
end
frame.maxStack = maxStack
if frame.maxStack < 2 then
frame:Hide()
return
end
frame.owner = owner
owner.hasStackSplit = true
frame.split = 1
-- `typed` distinguishes "1 because nothing has been typed yet" from "1 because the
-- player typed it": the first digit REPLACES the seed, later digits append. Cleared on
-- every open, so a dialog reopened after typing starts fresh.
frame.typed = nil
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.
frame:ClearAllPoints()
frame:SetPoint(anchor, owner, anchorTo, 0, 0)
frame:Show()
end
-- UpdateStackSplitFrame(maxStack) — ref l.26-52: the owner's stack shrank/grew out from
-- under an OPEN spinner (e.g. a server correction). Not called by anything in benilla yet
-- (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 = StackSplitFrame
frame.maxStack = maxStack
if frame.maxStack < 2 then
if frame.owner then
frame.owner.hasStackSplit = nil
end
frame:Hide()
return
end
if frame.split > frame.maxStack then
frame.split = frame.maxStack
StackSplitText:SetText(frame.split)
end
if frame.split == frame.maxStack then
StackSplitRightButton:Disable()
else
StackSplitRightButton:Enable()
end
if frame.split == 1 then
StackSplitLeftButton:Disable()
else
StackSplitLeftButton:Enable()
end
end
-- The arrows go through the shared setter (below) rather than nudging the count and the
-- two buttons themselves: with typed entry live there are now three writers of
-- `frame.split`, and three copies of the enable/disable rule is how they drift apart.
function BenillaStackSplitLeft_Click()
BenillaStackSplitSet(StackSplitFrame.split - 1)
end
function BenillaStackSplitRight_Click()
BenillaStackSplitSet(StackSplitFrame.split + 1)
end
function BenillaStackSplitOkay_Click()
local frame = StackSplitFrame
if frame.owner then
frame.owner.SplitStack(frame.owner, frame.split)
end
frame:Hide()
end
function BenillaStackSplitCancel_Click()
StackSplitFrame:Hide()
end
-- The one place the count is written, so the buttons' enable state and the label can
-- never disagree with `frame.split` (the arrow handlers below call it too). Clamped to
-- [1, maxStack] because every caller can overshoot: typing 9 into a 5-stack, or the
-- arrows at their ends.
function BenillaStackSplitSet(n)
local frame = StackSplitFrame
if n < 1 then n = 1 end
if n > frame.maxStack then n = frame.maxStack end
frame.split = n
StackSplitText:SetText(n)
if n <= 1 then
StackSplitLeftButton:Disable()
else
StackSplitLeftButton:Enable()
end
if n >= frame.maxStack then
StackSplitRightButton:Disable()
else
StackSplitRightButton:Enable()
end
end
-- Typed digits (decision 1319). The engine hands OnChar the literal character, so this is
-- a digit test and an append: the first keystroke replaces the seeded 1, each later one
-- shifts a decimal place in. A digit that would overshoot the stack is CLAMPED rather than
-- rejected -- typing 9 against a 5-stack leaves 5, which is what the player meant -- and
-- clamping keeps `typed` set, so a following digit still appends against the clamp.
-- Anything that is not a digit is ignored here; the frame consumes the key regardless
-- (the engine's existence gate decides that, not this handler's return).
function StackSplitFrame_OnChar()
local c = arg1
if c < "0" or c > "9" then return end
local frame = StackSplitFrame
local digit = tonumber(c)
local n
if frame.typed then
n = frame.split * 10 + digit
else
n = digit
end
-- A leading 0 types nothing: the count can never be 0, so it stays 1 and unlatched,
-- and the next digit still reads as the first one.
if n < 1 then return end
frame.typed = true
BenillaStackSplitSet(n)
end
-- The editing/commit keys for typed entry. BACKSPACE drops the last digit (and unlatches
-- at the end, so the next digit starts over rather than appending to a stale 1); ENTER
-- commits exactly as Okay does and ESCAPE cancels, which is what a player expects of a
-- dialog they are typing into. The arrow keys drive the spinner. Every other key is
-- swallowed by the frame -- the reference's own consequence of having a key script bound
-- at all, and the reason this dialog must not stay open invisibly.
function StackSplitFrame_OnKeyDown()
local key = arg1
local frame = StackSplitFrame
if key == "BACKSPACE" then
local n = floor(frame.split / 10)
if n < 1 then
frame.typed = nil
BenillaStackSplitSet(1)
else
BenillaStackSplitSet(n)
end
elseif key == "ENTER" then
BenillaStackSplitOkay_Click()
elseif key == "ESCAPE" then
BenillaStackSplitCancel_Click()
elseif key == "LEFT" then
BenillaStackSplitLeft_Click()
elseif key == "RIGHT" then
BenillaStackSplitRight_Click()
end
end
-- OnHide (ref l.158-162): ANY hide — Okay, Cancel, or ToggleGameMenu's chain hook below —
-- clears the owner's `hasStackSplit` flag (ref ItemButtonTemplate's OnHide, ContainerFrame.xml
-- 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 StackSplitFrame_OnHide()
local frame = StackSplitFrame
if frame.owner then
frame.owner.hasStackSplit = nil
end
end
]]></Script>
<!-- The Okay/Cancel face — our own local draw (see the file header) of the real
`UIPanelButtonTemplate` art (`Interface\Buttons\UI-Panel-Button-*`). -->
<Button name="BenillaStackSplitButtonTemplate" virtual="true">
<Size><AbsDimension x="64" y="24"/></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 frame attrs are the ref's own (l.4), and `toplevel` is load-bearing: Bagnon's bag
windows sit in the SAME HIGH stratum, and without the bit the dialog's Show raises
nothing — it loses the level tie to the later-shown bag window and the plate draws
UNDERNEATH the slots while the child buttons (level+1) poke through (the director's
08-14 Bagnon report; decision 1318). With the bit, Show runs the verified raise (compact,
then top-occupied+1). `enableKeyboard` alone is left off: its only consumer is the
typed-digit deferral in the file header. -->
<Frame name="StackSplitFrame" frameStrata="HIGH" parent="UIParent" toplevel="true" enableMouse="true" enableKeyboard="true" 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>
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- ref l.9-15 verbatim: the plate, authored 256×32 with NO anchors. The size is
VESTIGIAL and stays that way by the byte-verified law (decision 1310): an
anchor-less region gets the creation-path implicit SetAllPoints(parent), whose
two corners pin all four edges and leave the authored size unread — the
TexCoords crop exactly 172×96 out of the 256×128 art, the frame's own size,
drawn 1:1 filling the dialog. 1308 dropped this size to dodge the old centered
fallback (B180's squashed bar); with the fallback corrected the ref's own text
is back, and the bag test pins the render either way. -->
<Texture file="Interface\MoneyFrame\UI-MoneyFrame">
<Size><AbsDimension x="256" y="32"/></Size>
<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="StackSplitText" inherits="GameFontHighlight" justifyH="RIGHT">
<Anchors>
<Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-50" y="18"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- ref l.28-47: the left/right arrows, 16×16, RIGHT/LEFT-of-CENTER ±59/64, +18. -->
<Button name="StackSplitLeftButton">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="RIGHT" relativePoint="CENTER"><Offset><AbsDimension x="-59" y="18"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\MoneyFrame\Arrow-Left-Up"/>
<PushedTexture file="Interface\MoneyFrame\Arrow-Left-Down"/>
<DisabledTexture file="Interface\MoneyFrame\Arrow-Left-Disabled"/>
<Scripts>
<OnClick>BenillaStackSplitLeft_Click()</OnClick>
</Scripts>
</Button>
<Button name="StackSplitRightButton">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativePoint="CENTER"><Offset><AbsDimension x="64" y="18"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\MoneyFrame\Arrow-Right-Up"/>
<PushedTexture file="Interface\MoneyFrame\Arrow-Right-Down"/>
<DisabledTexture file="Interface\MoneyFrame\Arrow-Right-Disabled"/>
<Scripts>
<OnClick>BenillaStackSplitRight_Click()</OnClick>
</Scripts>
</Button>
<!-- ref l.68-84: Okay, RIGHT-of-BOTTOM -3/+32. -->
<Button name="StackSplitButton1" inherits="BenillaStackSplitButtonTemplate" text="OKAY">
<Anchors>
<Anchor point="RIGHT" relativePoint="BOTTOM"><Offset><AbsDimension x="-3" y="32"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaStackSplitOkay_Click()</OnClick>
</Scripts>
</Button>
<!-- ref l.85-101: Cancel, LEFT-of-BOTTOM +5/+32. -->
<Button name="StackSplitButton2" inherits="BenillaStackSplitButtonTemplate" text="CANCEL">
<Anchors>
<Anchor point="LEFT" relativePoint="BOTTOM"><Offset><AbsDimension x="5" y="32"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaStackSplitCancel_Click()</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnHide>StackSplitFrame_OnHide()</OnHide>
<OnChar>StackSplitFrame_OnChar()</OnChar>
<OnKeyDown>StackSplitFrame_OnKeyDown()</OnKeyDown>
</Scripts>
</Frame>
</Ui>
+39
View File
@@ -1660,6 +1660,45 @@
hideOnEscape = 1,
}
-- The guild-charter rename box, ref StaticPopup.lua l.133-160 — **in the reference's own
-- home, which is this file**. It lived in our PetitionFrame.xml on the reasoning that "a
-- popup belongs to the window that raises it"; that is a fine rule and it is not 1.12's,
-- which keeps every dialog in one registry. 1751's twelfth window made the charter the
-- reference's own file, whose Rename button calls `StaticPopup_Show("RENAME_GUILD")` and
-- registers nothing, so the entry had to come here.
--
-- 24 letters is the server's MAX_CHARTER_NAME, so the box cannot compose a name that will
-- be refused for length. Accept and Enter run the same two lines; the reference spells
-- them out twice (l.139-147), and its Enter arm hides the dialog while its Accept arm does
-- not — the popup engine hides on a plain Accept anyway.
StaticPopupDialogs["RENAME_GUILD"] = {
text = RENAME_GUILD_LABEL,
button1 = ACCEPT,
button2 = CANCEL,
hasEditBox = 1,
maxLetters = 24,
OnAccept = function()
RenamePetition(getglobal(this:GetParent():GetName() .. "EditBox"):GetText())
end,
EditBoxOnEnterPressed = function()
RenamePetition(getglobal(this:GetParent():GetName() .. "EditBox"):GetText())
this:GetParent():Hide()
end,
OnShow = function()
getglobal(this:GetName() .. "EditBox"):SetFocus()
end,
OnHide = function()
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:SetFocus()
end
getglobal(this:GetName() .. "EditBox"):SetText("")
end,
timeout = 0,
exclusive = 1,
whileDead = 1,
hideOnEscape = 1,
}
-- ref UIParent.lua l.324-343 + l.356-360, the four arms on one driver. Each equip arm
-- hides the OTHER dialog first — the two questions are mutually exclusive by construction
-- (one item, one destination), and the hide is what cancels the record the new question
+65 -26
View File
@@ -317,21 +317,27 @@ UIPanelTemplates.xml
# 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 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
# ══ THE COLOUR DIALOG — the reference's own, off the player's chain (1751, window 11) ══
# `ColorPickerFrame` (a `<ColorSelect>`), `OpacitySliderFrame`, `ColorPickerOkayButton`,
# `ColorPickerCancelButton`, and the separate `OpacityFrame` beside them. The file ships no `.lua`
# — its whole script is inline, and the one function that drives it
# (`UIDropDownMenuButton_OpenColorPicker`) lives in UIDropDownMenu.lua.
#
# Last of the "shared FrameXML surface with no benilla consumer yet" cluster that runs from
# UIPanelTemplates.xml down, and the swap is exactly why that posture was worth holding: **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 — so every one of them is now
# talking to the reference's own frames rather than to our re-expression of them.
#
# THE SEAT IS A REQUIREMENT, not tidiness: the Okay/Cancel buttons
# `inherits="GameMenuButtonTemplate"`, 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
# file for it — precisely because GameMenuFrame.xml is deliberately LAST, which is also why this
# entry does not follow the reference's own position below it). Everything else it needs is far
# above: Fonts' GameFontNormal trio, and UiPanels' ShowUIPanel/HideUIPanel plus its UISpecialFrames
# entry and the ESC rung that cancel-clicks it. UIDropDownMenu.xml sits above too, but that
# direction is call-time only, so the two could sit either way round.
Interface\FrameXML\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().
@@ -397,7 +403,28 @@ BuffFrame.xml
# bind. It is reachable only from the dead `enchantTimer` branch above, so it is left absent rather
# than stubbed (1203) — the honest state is "nothing calls it", not "we answer it with a lie".
Interface\FrameXML\DurabilityFrame.xml
ErrorsFrame.xml
# ══ THE ERROR TOASTS AND THE RAID WARNING — the reference's own (1751, window 14) ══
# `UIErrorsFrame` (the red/yellow toast strip) and `UIErrorsFrame_OnLoad`/`_OnEvent`, then
# `RaidWarningFrame` and `RaidBossEmoteFrame` with `RaidWarningFrame_OnLoad`/`_OnEvent`. Two files,
# in the reference's own split and its own order: the warning frame anchors `relativeTo=
# "UIErrorsFrame"`, resolved at LOAD, so it cannot precede it.
#
# Our single `ErrorsFrame.xml` held both and is deleted. It declared `RaidWarningFrame` with **no
# `<Scripts>` at all**, and the reason it gave was wrong: *"we already deliver that chat type
# through `ui_chat::feed`, so wiring a second consumer would print every raid warning TWICE"*.
# Checked rather than inherited — `ui_chat` fires `CHAT_MSG_RAID_WARNING` as an ordinary chat event
# and nothing of ours consumes it for a display. So the reference's handler adds the big centre
# text and its `PlaySound("RaidWarning")` **beside** the chat line, which is exactly what 1.12
# does: the chat window and the warning frame are two consumers of one event there too.
#
# It also brings `RaidBossEmoteFrame`, which our copy withheld on the "no corpus caller, and a
# shadowed name with no caller is building against a guess" rule (1222). That rule is about names
# WE would invent; this one is the reference's own declaration, so there is nothing left to guess.
#
# `ChatTypeInfo["RAID_WARNING"]` comes from ChatFrame.xml far below — read at call time, so the
# order does not bind.
Interface\FrameXML\UIErrorsFrame.xml
Interface\FrameXML\RaidWarning.xml
# ══ THE BAG WINDOWS — the reference's own, off the player's chain (decision 1751, window 1) ══
# `ContainerFrame1..12`, `ContainerFrameTemplate`, `ContainerFrameItemButtonTemplate` and the whole
# `ToggleBag`/`ToggleBackpack`/`OpenAllBags`/`IsBagOpen`/`ToggleKeyRing` verb family — the real
@@ -453,7 +480,7 @@ Interface\FrameXML\MainMenuBarBagButtons.xml
# click handler (the reference's `ContainerFrameItemButton_OnClick`) calls `OpenStackSplitFrame`.
# Order between the two doesn't matter for correctness (Lua globals resolve at call time, and
# neither file's XML `inherits=` reaches into the other's templates) — adjacent for legibility.
StackSplit.xml
Interface\FrameXML\StackSplitFrame.xml
# Our ADAPTERS over the reference's own container files — one wrapper, for the keyring tooltip
# detour benilla answers differently (0765). Must load AFTER them, since it captures at load the
# body it wraps; its own header says what it stopped needing to do when the XML became sourceable.
@@ -505,7 +532,7 @@ SkillFrame.xml
# declared here, and `inherits=` resolves at LOAD. The reference has the identical edge — its
# `HonorFrameTemplates.xml` is `<Include>`d by `HonorFrame.xml` and inherited by the LoadOnDemand
# `Blizzard_InspectUI`, which loads later still.
HonorFrame.xml
Interface\FrameXML\HonorFrame.xml
# The inspect window (decision 0631): another player's paper doll. Beside the character
# window it mirrors — it needs the same three (Fonts/UiPanels/GameTooltip) plus
# `CharacterFrameTabButtonTemplate` from UiPanels.xml, and nothing from CharacterFrame.xml
@@ -651,15 +678,27 @@ GroupLootFrame.xml
# art conventions); its title/panel-button labels use the auto-centred ButtonText slot.
QuestFrame.xml
# The two guild-charter windows (decision 1672): the guild registrar — the window the "How do I
# form a guild?" gossip row opens — and the charter itself, the sheet nine people sign. Loaded
# here, right after QuestFrame, because both wear the QuestGreeting parchment and its QuestFont /
# form a guild?" gossip row opens — and the charter itself, the sheet nine people sign. **Both are
# the reference's own files off the player's chain now** (1751, windows 12 and 13). Loaded here,
# right after QuestFrame, because both wear the QuestGreeting parchment and its QuestFont /
# QuestTitleFont objects and read as that family; they need UiPanels (the panel manager seats both,
# and PetitionFrame registers its RENAME_GUILD dialog on the shared popup engine), MoneyFrame (the
# registrar's price rides MoneyFrameTemplate, resolved at LOAD — decision 1580's ordering rule) and
# UIPanelTemplates (the close/action buttons), all far above. The registrar must come first: no
# load-order dependency links them, but the flow does, and the pair reads in the order it is used.
GuildRegistrarFrame.xml
PetitionFrame.xml
# and the RENAME_GUILD dialog the charter raises is registered there, where 1.12 keeps every
# dialog), MoneyFrame (the registrar's price rides MoneyFrameTemplate, resolved at LOAD — decision
# 1580's ordering rule) and UIPanelTemplates (the close/action buttons), all far above. The
# registrar must come first: no load-order dependency links them, but the flow does, and the pair
# reads in the order it is used.
Interface\FrameXML\GuildRegistrarFrame.xml
# ══ THE GUILD CHARTER — the reference's own, off the player's chain (1751, window 12) ══
# `PetitionFrame` and its nine signature rows, `PetitionFrame_Update`, `MAX_PETITION_SIGNATURES`,
# and the leader/signer button pair that share one anchor. Ours was the same window under the same
# names (decision 1672) with a different expression of the same painter; the reference's file is
# what that expression was written from.
#
# The seat is unchanged and every dependency it names is above: UiPanels (the panel manager seats
# it, and its RENAME_GUILD dialog rides the shared popup engine), MoneyFrame, UIPanelTemplates (the
# close/action buttons `inherits=` at LOAD), and the QuestGreeting parchment family it shares with
# GuildRegistrarFrame directly above.
Interface\FrameXML\PetitionFrame.xml
# The quest log window (decision 0088 arc, the quest-log slice): durable player state, not an
# NPC session — loaded after MerchantFrame (the BenillaMoney coin helpers it reuses) and
# alongside QuestFrame (same arc); the 'L' binding (ui_script/input.rs) calls its
@@ -669,7 +708,7 @@ QuestLogFrame.xml
# file split. After MinimapCluster (its anchor), UIParent (the manage pass that re-seats it,
# and SecondsToTime), GameTooltip (its row hover), and QuestLogFrame — whose window a row
# click opens, and whose MAX_QUESTS it shares.
QuestTimerFrame.xml
Interface\FrameXML\QuestTimerFrame.xml
ChatFrame.xml
# The macro window (decision 0983): the editor + its name/icon popup, driven over the
# engine's OWN macro table (`benilla_ui::script::macros` — 1.12 macros have no server
+4 -10
View File
@@ -415,16 +415,10 @@ fn pre_resolved_lines_land_on_the_errors_frame_in_the_arms_colour() {
{
let mut script = app.world_mut().non_send_resource_mut::<UiScript>();
script.set_screen_size(1024.0, 768.0);
for file in ["Fonts.xml", "ErrorsFrame.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(&script, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
// The errors frame is the reference's own file since 1751 window 14, so this reads both
// stores through the one loader that speaks them.
for file in ["Fonts.xml", "Interface\\FrameXML\\UIErrorsFrame.xml"] {
crate::ui_script::load_ui_for_test(&script, file);
}
}
@@ -784,7 +784,7 @@ fn shipped_bag_frame_drives_end_to_end() {
bar_frames = frames;
}
}
load_ui(&s, "StackSplit.xml");
load_ui(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
load_ui(&s, "MerchantFrame.xml");
assert_eq!(
+13 -9
View File
@@ -1006,7 +1006,7 @@ fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
load_xml(s, file);
}
load_xml(s, "MerchantFrame.xml");
load_xml(s, "StackSplit.xml");
load_xml(s, "Interface\\FrameXML\\StackSplitFrame.xml");
s.set_money(0);
let mut slots = std::collections::HashMap::new();
@@ -1233,14 +1233,18 @@ fn typing_a_number_into_the_split_spinner_sets_the_count() {
"and the label follows"
);
// A second digit appends, then clamps to the 5-stack rather than being thrown away.
// A second digit that would overflow the stack is **IGNORED**, not clamped — the reference's
// `StackSplitFrame_OnChar` writes `this.split` only inside `if split <= this.maxStack`
// (StackSplitFrame.lua l.72-85), so 37 against a 5-stack leaves 3 standing. Our own copy
// clamped to 5 instead, and this assertion was written against that; the window is the
// reference's since 1751 window 17, so the ignore is the behaviour.
assert!(s.char_input("7"));
assert_eq!(
s.eval::<i64>("return StackSplitFrame.split").unwrap(),
5,
"37 against a 5-stack clamps to 5"
3,
"37 against a 5-stack keeps the 3 — the ref ignores the digit, it does not clamp"
);
// BACKSPACE drops a digit off the clamped value.
// BACKSPACE drops a digit off it.
assert!(s.frame_key_input("BACKSPACE"), "the dialog took BACKSPACE");
assert_eq!(s.eval::<i64>("return StackSplitFrame.split").unwrap(), 1);
@@ -1273,10 +1277,10 @@ fn split_okay_then_a_placement_queues_the_split_move() {
assert!(s.eval::<bool>("return StackSplitFrame:IsShown()").unwrap());
// Bump the spinner from 1 to 3, then Okay — the carry lands on the cursor.
s.run("BenillaStackSplitRight_Click()").unwrap();
s.run("BenillaStackSplitRight_Click()").unwrap();
click(&mut s, "StackSplitRightButton", "LeftButton");
click(&mut s, "StackSplitRightButton", "LeftButton");
assert_eq!(s.eval::<i64>("return StackSplitFrame.split").unwrap(), 3);
s.run("BenillaStackSplitOkay_Click()").unwrap();
click(&mut s, "StackSplitOkayButton", "LeftButton");
assert!(
!s.eval::<bool>("return StackSplitFrame:IsShown()").unwrap(),
"Okay hides the spinner"
@@ -1548,7 +1552,7 @@ fn keyring_surface(s: &UiScript) {
load_xml(s, file);
}
load_xml(s, "MerchantFrame.xml");
load_xml(s, "ErrorsFrame.xml");
load_xml(s, "Interface\\FrameXML\\UIErrorsFrame.xml");
}
/// **The gate** (decision 0765): no key ⇒ no keyring anywhere on the bar — the button is hidden and
@@ -7,22 +7,7 @@
use benilla_ui::script::UiScript;
/// One shipped file into `s`, asserting it loaded clean.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// Fonts then BasicControls — the manifest's own order, and the whole dependency this file has.
fn basic_controls() -> UiScript {
+1 -16
View File
@@ -17,22 +17,7 @@
use benilla_ui::script::{AuraState, ExtractedQuad, QuadContent, UiScript};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
+1 -16
View File
@@ -10,22 +10,7 @@
use benilla_ui::script::{ExtractedQuad, QuadContent, ScriptValue, UiScript};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error (the bag/panel tests' loader).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
@@ -18,23 +18,7 @@ use benilla_ui::script::{
UnitCombatStats, UnitState,
};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the questlog/panel
/// tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// A Night Elf Warrior, level 12 — the fixture every test below shares for the level/name line.
fn player_unit() -> UnitState {
@@ -138,6 +122,7 @@ fn backpack_with_fitting_helm() -> benilla_ui::script::ContainerState {
/// attack/damage/ranged rows + 5 resistance frames + the model pane + its 2 rotate buttons + chrome).
#[test]
fn shipped_character_frame_loads_clean() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_xml(&s, "Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
@@ -154,6 +139,7 @@ fn shipped_character_frame_loads_clean() {
/// and a second toggle closes it again — each transition its own sound.
#[test]
fn shipped_character_frame_drives_end_to_end() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -369,6 +355,7 @@ fn shipped_character_frame_drives_end_to_end() {
/// page's own quads. Checked on the real extracted draw order, not merely the level integer.
#[test]
fn close_button_draws_above_the_paper_doll_page() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -414,6 +401,7 @@ fn close_button_draws_above_the_paper_doll_page() {
/// comment) keep `format()` from erroring outright, rather than the window failing to open at all.
#[test]
fn level_line_survives_no_player_snapshot_yet() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -447,6 +435,7 @@ fn level_line_survives_no_player_snapshot_yet() {
/// the SAME slot cancels: the cursor empties and the dim clears.
#[test]
fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -501,6 +490,7 @@ fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() {
/// this file's own header comment on the emulation).
#[test]
fn cursor_update_highlights_fitting_doll_slots_while_holding_a_bag_item() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -544,6 +534,7 @@ fn cursor_update_highlights_fitting_doll_slots_while_holding_a_bag_item() {
/// `(bag, slot)` source, cursor cleared.
#[test]
fn model_pane_click_auto_equips_a_held_bag_item() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -581,6 +572,7 @@ fn model_pane_click_auto_equips_a_held_bag_item() {
/// armor guy). Repairing it restores both to white.
#[test]
fn broken_equipped_item_tints_its_doll_slot_red() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -657,6 +649,7 @@ fn broken_equipped_item_tints_its_doll_slot_red() {
/// HERE, not on the director's screen. Zero collected handler errors allowed anywhere.
#[test]
fn tab_round_trip_with_a_selected_skill_by_point() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::{SkillEntry, SkillsState};
let mut s = UiScript::new().unwrap();
@@ -857,6 +850,7 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
/// click-LEFT subtracts); that quirk is quoted, so it is asserted here rather than "corrected".
#[test]
fn rotate_arrows_tap_twice_and_spin_while_held() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -950,6 +944,7 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
/// to miss.
#[test]
fn a_keybind_page_switch_moves_the_tab_row_with_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -964,7 +959,7 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
load_xml(&s, "PetPaperDollFrame.xml");
load_xml(&s, "ReputationFrame.xml");
load_xml(&s, "SkillFrame.xml");
load_xml(&s, "HonorFrame.xml");
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
s.set_unit("player", Some(player_unit()));
// THE INVARIANT the fix rests on: a page's `id=` is its slot in this window's own tab row, and
@@ -1060,6 +1055,7 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
/// when it passes `this:GetParent()`.
#[test]
fn an_addons_tab_click_selects_through_the_generic_entry_point() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -13,22 +13,7 @@
use benilla_ui::script::UiScript;
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The GlobalStrings the menu labels itself with. The app runs the real
/// `Interface\FrameXML\GlobalStrings.lua` off the player's own patch chain at boot
@@ -62,7 +47,7 @@ fn chat_with_menu() -> UiScript {
"UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
"ColorPickerFrame.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
"ChatFrame.xml",
] {
load_xml(&s, file);
@@ -125,6 +110,7 @@ fn left_click(s: &mut UiScript, frame: &str) {
/// options at all" rather than "the menu is missing a row".
#[test]
fn right_clicking_a_chat_tab_opens_its_options_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
assert!(
s.eval::<bool>("return not DropDownList1:IsVisible()")
@@ -173,6 +159,7 @@ fn right_clicking_a_chat_tab_opens_its_options_menu() {
/// change. (The reference's own fork: the right-button arm returns before the select.)
#[test]
fn a_left_click_still_selects_the_tab_and_opens_no_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
left_click(&mut s, "ChatFrame2Tab");
assert_eq!(
@@ -206,6 +193,7 @@ fn a_left_click_still_selects_the_tab_and_opens_no_menu() {
/// `204/255`, not as 0.8.
#[test]
fn the_background_row_opens_the_picker_and_its_opacity_slider_drives_the_window() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
right_click(&mut s, "ChatFrame1Tab");
@@ -274,6 +262,7 @@ fn the_background_row_opens_the_picker_and_its_opacity_slider_drives_the_window(
/// hover ramp, which is the look the director signed off in 0288.
#[test]
fn the_shipped_window_still_fades_zero_to_a_quarter() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
s.mouse_move(1500.0, 850.0);
for _ in 0..4 {
@@ -301,6 +290,7 @@ fn the_shipped_window_still_fades_zero_to_a_quarter() {
/// pick moves both the live font and the stored `SIZE`.
#[test]
fn the_font_size_submenu_resizes_the_window_and_stores_the_pick() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
right_click(&mut s, "ChatFrame1Tab");
@@ -343,6 +333,7 @@ fn the_font_size_submenu_resizes_the_window_and_stores_the_pick() {
/// parent, so a single shared capsule would have written window 1 whichever tab was clicked.
#[test]
fn each_tabs_menu_writes_its_own_window() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
right_click(&mut s, "ChatFrame2Tab");
assert_eq!(
@@ -370,6 +361,7 @@ fn each_tabs_menu_writes_its_own_window() {
/// `previousValues` contract, which the reference reaches through the same `cancelFunc` field.
#[test]
fn cancelling_the_picker_puts_the_window_back() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
// Start from a non-default look so "restored" is distinguishable from "reset".
s.run("FCF_SetWindowAlpha(ChatFrame1, 0.4) FCF_SetWindowColor(ChatFrame1, 0.2, 0.4, 0.6)")
@@ -405,6 +397,7 @@ fn cancelling_the_picker_puts_the_window_back() {
/// the fade latch.
#[test]
fn the_restore_event_repaints_the_window_from_the_store() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_with_menu();
s.set_chat_window_looks([(
0,
@@ -11,22 +11,7 @@
use benilla_ui::script::UiScript;
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The chat dock with everything its tab menu and its grips reach under it.
fn chat_ui() -> UiScript {
@@ -40,7 +25,7 @@ fn chat_ui() -> UiScript {
"UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
"ColorPickerFrame.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
"ChatFrame.xml",
] {
load_xml(&s, file);
@@ -102,6 +87,7 @@ fn grab_grip(s: &mut UiScript, grip: &str) -> (f32, f32) {
/// `GetChatWindowInfo` answers, because it is the chat-cache's own `LOCKED 1`.
#[test]
fn both_dock_windows_ship_locked() {
let _data = benilla_formats::wow_data_or_skip!();
let s = chat_ui();
for i in 1..=2 {
assert_eq!(
@@ -133,6 +119,7 @@ fn both_dock_windows_ship_locked() {
/// locked, `LOCK_WINDOW` when it is not, `notCheckable`.
#[test]
fn the_tab_menus_lock_row_toggles_the_window_and_flips_its_label() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
right_click(&mut s, "ChatFrame1Tab");
// Shipped locked, so the row offers the way OUT of it.
@@ -183,6 +170,7 @@ fn the_tab_menus_lock_row_toggles_the_window_and_flips_its_label() {
/// it — see the template's note), and `FCF_Resize` must refuse even if something did reach it.
#[test]
fn a_locked_window_refuses_a_grip_drag() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
let before = width(&mut s);
assert_eq!(before, 430.0, "the authored width");
@@ -229,6 +217,7 @@ fn a_locked_window_refuses_a_grip_drag() {
/// so a window held past its minimum stops shrinking and starts *walking*.
#[test]
fn an_unlocked_bottom_right_grip_resizes_and_clamps_at_the_bounds() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
assert!(
@@ -277,6 +266,7 @@ fn an_unlocked_bottom_right_grip_resizes_and_clamps_at_the_bounds() {
/// the session.
#[test]
fn the_release_ends_the_resize_from_anywhere_on_screen() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
let (x, y) = grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
@@ -308,6 +298,7 @@ fn the_release_ends_the_resize_from_anywhere_on_screen() {
/// held-spell dismissal working on the same button.
#[test]
fn the_body_drag_moves_an_unlocked_window_and_a_locked_one_stays_put() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
let (cx, cy) = centre(&mut s, "ChatFrame1");
let start = left(&mut s);
@@ -348,6 +339,7 @@ fn the_body_drag_moves_an_unlocked_window_and_a_locked_one_stays_put() {
/// back on the bottom-stack seat.
#[test]
fn a_moved_window_is_user_placed_and_the_managed_pass_skips_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
// The managed pass owns the seat to begin with — the control for the assertion below.
s.run("UIParent_ManageFramePositions()").unwrap();
@@ -388,6 +380,7 @@ fn a_moved_window_is_user_placed_and_the_managed_pass_skips_it() {
/// seat it into a *fresh* VM whose windows are all on their authored anchors.
#[test]
fn the_geometry_round_trips_through_the_save_file() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
@@ -455,6 +448,7 @@ fn the_geometry_round_trips_through_the_save_file() {
/// so the handles appear with the box and are invisible without it.
#[test]
fn the_grip_art_follows_the_windows_reveal_and_tint() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
// Off-hover, at the shipped alpha-0 base: everything in the set is invisible.
s.run("BenillaFCF.hover = false FCF_OnUpdate(1.0)").unwrap();
@@ -497,6 +491,7 @@ fn the_grip_art_follows_the_windows_reveal_and_tint() {
/// clause the box would fade out from under the hand holding it.
#[test]
fn a_drag_in_flight_holds_the_chrome_visible() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
@@ -524,6 +519,7 @@ fn a_drag_in_flight_holds_the_chrome_visible() {
/// `FCF_Get_ChatLocked()` gate, which is the first line of both verbs.
#[test]
fn the_chat_locked_global_overrides_the_per_window_lock() {
let _data = benilla_formats::wow_data_or_skip!();
let s = chat_ui();
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
assert!(s
@@ -556,6 +552,7 @@ fn the_chat_locked_global_overrides_the_per_window_lock() {
/// reference's third gate — `isDocked and chatFrame ~= DEFAULT_CHAT_FRAME`.
#[test]
fn the_docked_combat_log_cannot_be_moved_or_resized_on_its_own() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = chat_ui();
s.run("FCF_SetLocked(ChatFrame2, nil)").unwrap();
assert!(
+1 -16
View File
@@ -7,22 +7,7 @@
use benilla_ui::script::{ExtractedQuad, QuadContent, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The full ChatFrame stack (fonts first, so the FontString's `inherits="ChatFontNormal"` resolves).
fn chat_frame() -> UiScript {
@@ -15,17 +15,7 @@
use benilla_ui::script::UiScript;
fn load_xml(s: &UiScript, file: &str) {
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);
}
use super::test_ui::load_ui as load_xml;
/// The frame tree a cinematic actually runs against: `UIParent` (whose `ShowUIPanel` the frame's
/// `CINEMATIC_START` arm calls) and the cinematic frame itself.
@@ -9,22 +9,7 @@
use benilla_ui::script::{QuadContent, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The manifest prefix the picker needs: fonts, the panel manager (`ShowUIPanel`/`HideUIPanel`,
/// `UISpecialFrames`, the ESC chain), the shared widget kit (`GameMenuButtonTemplate`), then the
@@ -40,7 +25,7 @@ fn picker() -> UiScript {
"UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
"ColorPickerFrame.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
] {
load_xml(&s, file);
}
@@ -73,6 +58,7 @@ fn after_round_trip(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
/// `ColorPickerOkayButton` is nil.
#[test]
fn the_named_pieces_exist_and_their_scripts_are_fetchable() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
assert!(s
.eval::<bool>("return ColorPickerFrame ~= nil and ColorPickerFrame.SetColorRGB ~= nil")
@@ -110,6 +96,7 @@ fn the_named_pieces_exist_and_their_scripts_are_fetchable() {
/// this test while the swatch drew nothing. What the renderer receives is the only honest witness.
#[test]
fn set_color_rgb_paints_the_swatch_and_reads_back_the_widgets_colour() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = picker();
s.run("ShowUIPanel(ColorPickerFrame)").unwrap();
s.run("ColorPickerFrame:SetColorRGB(0.2, 0.4, 0.8)")
@@ -139,6 +126,7 @@ fn set_color_rgb_paints_the_swatch_and_reads_back_the_widgets_colour() {
/// WIDTH with it. Both arms of the reference's `OnShow` (l.161-170).
#[test]
fn has_opacity_shows_the_slider_and_widens_the_window() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = picker();
// Without it: no slider, the narrow window. (`GetWidth` reports the *resolved* rect, so the
@@ -179,6 +167,7 @@ fn has_opacity_shows_the_slider_and_widens_the_window() {
/// and the reference's own `<OnValueChanged>` (l.146-152).
#[test]
fn the_opacity_slider_drives_opacity_func_on_every_change() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
s.run(
r#"
@@ -247,6 +236,7 @@ fn dewdrop_open(s: &UiScript, r: f64, g: f64, b: f64, opacity: f64) {
/// quantized colour and the slider-derived alpha, and the window closes.
#[test]
fn the_dewdrop_sequence_commits_on_okay() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
dewdrop_open(&s, 0.1, 0.5, 0.9, 0.25);
assert!(s.errors().is_empty(), "{:?}", s.errors());
@@ -296,6 +286,7 @@ fn the_dewdrop_sequence_commits_on_okay() {
/// in with, and it does it by *calling `cancelFunc`* — the window has no memory of its own.
#[test]
fn cancel_restores_the_previous_colour_through_cancel_func() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
dewdrop_open(&s, 0.1, 0.5, 0.9, 0.25);
@@ -331,6 +322,7 @@ fn cancel_restores_the_previous_colour_through_cancel_func() {
/// open behind it.
#[test]
fn escape_cancels_rather_than_merely_hiding() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
dewdrop_open(&s, 0.1, 0.5, 0.9, 0.25);
s.run("ColorPickerFrame:SetColorRGB(1, 0, 0)").unwrap();
@@ -356,6 +348,7 @@ fn escape_cancels_rather_than_merely_hiding() {
/// this library.
#[test]
fn ace_console_can_chain_the_okay_buttons_onclick() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
s.run(
r#"
@@ -392,6 +385,7 @@ fn ace_console_can_chain_the_okay_buttons_onclick() {
/// world's windows.
#[test]
fn the_picker_is_a_uispecialframe_and_close_windows_puts_it_away() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
let listed: bool = s
.eval(
@@ -426,6 +420,7 @@ fn the_picker_is_a_uispecialframe_and_close_windows_puts_it_away() {
/// exact one FloatingChatFrame's colour rows use.
#[test]
fn a_dropdown_row_with_has_color_swatch_opens_the_picker_and_cancel_restores() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
s.run(
r#"
@@ -506,6 +501,7 @@ fn a_dropdown_row_with_has_color_swatch_opens_the_picker_and_cancel_restores() {
/// menu row from sprouting a colour button.
#[test]
fn a_row_without_the_flag_has_no_swatch() {
let _data = benilla_formats::wow_data_or_skip!();
let s = picker();
s.run(
r#"
@@ -536,6 +532,7 @@ fn a_row_without_the_flag_has_no_swatch() {
/// the picker addresses it by name.
#[test]
fn the_shipped_window_carries_the_wheel_at_the_references_geometry() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = picker();
s.run("ShowUIPanel(ColorPickerFrame)").unwrap();
s.resolve();
@@ -593,6 +590,7 @@ fn the_shipped_window_carries_the_wheel_at_the_references_geometry() {
/// The click is at the wheel's right rim, which the pick law makes 180° — cyan.
#[test]
fn a_click_on_the_wheel_reaches_the_callers_func() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = picker();
// Dewdrop-2.0.lua l.425-465's block, as transcribed in this file's other tests.
s.run(
@@ -641,6 +639,7 @@ fn a_click_on_the_wheel_reaches_the_callers_func() {
/// if the content is (decision 1592 §4).
#[test]
fn the_generated_art_reaches_the_renderer_carrying_only_what_moves_a_pixel() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = picker();
s.run("ColorPickerFrame:SetColorRGB(0.15, 0.55, 0.75); ShowUIPanel(ColorPickerFrame)")
.unwrap();
@@ -4,23 +4,7 @@
use benilla_ui::script::{ScriptValue, UiScript, UnitState};
/// Load one shipped `assets/ui/<file>` (the panel tests' loader, duplicated to stay
/// self-contained), panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
fn load_combat_text() -> UiScript {
let mut s = UiScript::new().unwrap();
@@ -32,7 +32,7 @@ const ROOM_UI: &[&str] = &[
"UnitPopup.xml",
"Interface\\FrameXML\\ItemRef.xml",
"MerchantFrame.xml",
"StackSplit.xml",
"Interface\\FrameXML\\StackSplitFrame.xml",
"DressUpFrame.xml",
"ChatFrame.xml",
];
@@ -12,21 +12,7 @@
use benilla_ui::script::UiScript;
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The production prefix the lists need (ui_script/mod.rs order): the fonts (this OnLoad reads a
/// real font height off Button1's NormalText), UIParent (the lists declare `parent="UIParent"`),
@@ -10,22 +10,7 @@
use benilla_ui::script::{EnchantConfirm, ScriptValue, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
@@ -10,23 +10,7 @@
use benilla_ui::script::{ExtractedQuad, QuadContent, ScriptValue, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the panel/quest
/// tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The toast lines as drawn, **top row first**, each with its colour+alpha and its band bottom.
fn toast_lines(s: &mut UiScript) -> Vec<(String, [f32; 4], f32)> {
@@ -55,7 +39,7 @@ fn info_and_error_messages_stack_hold_and_expire() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
load_xml(&s, "ErrorsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
// Empty at load. The frame itself is shown — a MessageFrame with nothing to say simply draws
// nothing, where the hand-rolled version had to `Hide()` itself.
@@ -126,7 +110,7 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, "Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ErrorsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
@@ -231,7 +231,7 @@ fn escape_closes_an_open_stack_split_frame() {
}
load_xml(&s, "MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
load_xml(&s, "ChatFrame.xml"); // …and ChatFrameEditBox, the shift fork's first test
load_xml(&s, "StackSplit.xml");
load_xml(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
s.set_money(0);
let mut slots = std::collections::HashMap::new();
+1 -18
View File
@@ -5,24 +5,7 @@
use benilla_ui::script::UiScript;
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error — the same
/// per-file loader every `ui_script` test file carries (these tests only ever need
/// `UiPanels.xml`, where the fade kit lives).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// **The idle fade driver parks itself** (decision 1396's class, the audit's driver-hide item):
/// with `FADEFRAMES` empty there is nothing to walk, so the driver's OnUpdate hides its own frame
@@ -17,22 +17,7 @@
use benilla_ui::script::UiScript;
/// One shipped file into `s`, asserting it loaded clean.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// A document written here in the test, loaded against the templates already registered — the
/// cross-file template registry is on the `Model`, so an inline doc reaches `ScrollTemplates.xml`'s
@@ -287,11 +287,6 @@ const KNOWN: &[Known] = &[
flag: Flag::Mouse,
why: "inherits BenillaMerchantItemTemplate",
},
Known {
frame: "HonorFrameProgressBar",
flag: Flag::Mouse,
why: "our honour bar carries its own hover tooltip; the reference's page has none",
},
Known {
frame: "WorldMapFrame",
flag: Flag::Mouse,
@@ -9,39 +9,7 @@
use benilla_ui::script::{FriendInfo, SocialRequest, SocialState, UiScript, WhoInfo};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error — **and on any
/// unknown-template warning**.
///
/// That second check is not decoration. `inherits=` a template this house doesn't ship is a
/// *warning*, not an error: the frame is still created, just with none of the template's art or
/// size. Every test below stayed green while all eight action buttons and the close button were
/// invisible, because a bare Button still clicks. Only a live run showed it. The guard is here so
/// the next window cannot repeat it.
fn load_xml(s: &UiScript, file: &str) {
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
);
let missing: Vec<&String> = report
.warnings
.iter()
.filter(|w| w.contains("unknown template"))
.collect();
assert!(
missing.is_empty(),
"{file}: inherits a template this house does not ship (the frame loads, its ART does \
not): {missing:?}"
);
}
use super::test_ui::load_ui_strict as load_xml;
/// The window's own manifest slice, in `load_default_ui` order.
fn setup() -> UiScript {
@@ -22,40 +22,7 @@
use benilla_ui::script::{GuildState, ScriptValue, UiScript, UnitState};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error — **and on any
/// unknown-template warning**.
///
/// Copied from `friends_tests` on purpose, and the reason is its own: `inherits=` a template this
/// house doesn't ship is a *warning*, not an error — the frame is still created, just with none of
/// the template's art or size. Every social-window test stayed green while all eight action buttons
/// and the close button were invisible, because a bare Button still clicks (decision 0672). The
/// guild pane inherits four shared templates from `UIPanelTemplates.xml` and four local ones; this
/// is what makes a typo in any of the eight loud.
fn load_xml(s: &UiScript, file: &str) {
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
);
let missing: Vec<&String> = report
.warnings
.iter()
.filter(|w| w.contains("unknown template"))
.collect();
assert!(
missing.is_empty(),
"{file}: inherits a template this house does not ship (the frame loads, its ART does \
not): {missing:?}"
);
}
use super::test_ui::load_ui_strict as load_xml;
/// The guild engine API, stood in for in Lua (see the module header).
///
@@ -19,23 +19,7 @@
use benilla_ui::script::{HonorState, UiScript, UnitState};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the
/// character/reputation tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The fixture snapshot — a rank-12 Alliance character with a figure in every bucket, all thirteen
/// distinct so a swapped pair of rows cannot pass. Deliberately the same numbers as the protocol
@@ -92,7 +76,7 @@ fn load_page(s: &UiScript) {
"UIPanelTemplates.xml",
"OptionsFrameTemplates.xml",
"CharacterFrame.xml",
"HonorFrame.xml",
"Interface\\FrameXML\\HonorFrame.xml",
] {
load_xml(s, file);
}
@@ -138,6 +122,7 @@ fn text(s: &mut UiScript, frame: &str) -> String {
/// standing against a kill count — fails here and only here.
#[test]
fn every_figure_lands_in_its_own_row() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_honor_page();
for (frame, want) in [
("HonorFrameCurrentHKValue", "17"),
@@ -164,6 +149,7 @@ fn every_figure_lands_in_its_own_row() {
/// makes it impossible to ship.
#[test]
fn the_title_is_the_internal_rank_and_the_badge_is_the_visual_one() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_honor_page();
assert_eq!(text(&mut s, "HonorFrameCurrentPVPTitle"), "Knight-Captain");
// Internal 12 → visual 8. The line reads "(Rank 8)", never "(Rank 12)".
@@ -186,6 +172,7 @@ fn the_title_is_the_internal_rank_and_the_badge_is_the_visual_one() {
/// badge. The nil is the mechanism, so this is the case that proves the pane never invents a title.
#[test]
fn an_unranked_character_reads_none_and_shows_no_badge() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_honor_page();
s.set_honor(Some(HonorState {
rank: 0,
@@ -217,6 +204,7 @@ fn an_unranked_character_reads_none_and_shows_no_badge() {
/// resolved — a bar with no colour at all would read as a broken pane.
#[test]
fn the_bar_takes_the_fraction_and_the_faction_colour() {
let _data = benilla_formats::wow_data_or_skip!();
let s = shown_honor_page();
// Against the binding's own arithmetic, not against `191/255`. The client MULTIPLIES by the
// f32 nearest 1/255 (`0x3B808081`, wow-re `honor-panel-law.md` `0x51aace`) rather than
@@ -255,6 +243,7 @@ fn the_bar_takes_the_fraction_and_the_faction_colour() {
/// the flag's *derivation from the event name* is the half that can be got wrong.
#[test]
fn a_kill_repaints_the_session_but_not_the_week() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_honor_page();
// Everything moves in the snapshot — including a weekly figure, which is the bait.
s.set_honor(Some(HonorState {
@@ -262,8 +251,7 @@ fn a_kill_repaints_the_session_but_not_the_week() {
this_week_hk: 124,
..state()
}));
s.run(r#"HonorFrame_OnEvent("PLAYER_PVP_KILLS_CHANGED")"#)
.unwrap();
s.fire_event("PLAYER_PVP_KILLS_CHANGED", vec![]);
s.resolve();
assert_eq!(
text(&mut s, "HonorFrameCurrentHKValue"),
@@ -277,8 +265,7 @@ fn a_kill_repaints_the_session_but_not_the_week() {
);
// World entry is the flag that says "repaint those too".
s.run(r#"HonorFrame_OnEvent("PLAYER_ENTERING_WORLD")"#)
.unwrap();
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.resolve();
assert_eq!(text(&mut s, "HonorFrameThisWeekHKValue"), "124");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -378,7 +365,7 @@ fn shown_inspect_honor_page() -> UiScript {
"GameTooltip.xml",
// Before InspectFrame.xml — the honor page inherits this file's row templates and calls
// its two shared painters, and `inherits=` resolves at load (the manifest's own order).
"HonorFrame.xml",
"Interface\\FrameXML\\HonorFrame.xml",
"InspectFrame.xml",
] {
load_xml(&s, file);
@@ -406,6 +393,7 @@ fn shown_inspect_honor_page() -> UiScript {
/// not the player. Everything the character page's row test pins, on the other feed.
#[test]
fn the_inspect_page_paints_the_reply_it_holds() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_inspect_honor_page();
for (frame, want) in [
("BenillaInspectHonorFrameCurrentHKValue", "17"),
@@ -454,6 +442,7 @@ fn the_inspect_page_paints_the_reply_it_holds() {
/// happens to be held looks identical either way.
#[test]
fn the_page_asks_only_when_it_holds_nothing() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_inspect_honor_page();
// Held: the open above consumed no request.
assert_eq!(
@@ -35,22 +35,7 @@ fn reach(dist_sq: f64) -> UnitReach {
}
}
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The inspected player: a Dwarf Paladin, level 34.
fn target_unit() -> UnitState {
@@ -132,7 +117,7 @@ fn armed() -> UiScript {
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
load_xml(&s, "HonorFrame.xml");
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
load_xml(&s, "InspectFrame.xml");
s.set_unit("target", Some(target_unit()));
s.set_inspect(Some(inspect_view("target")));
@@ -144,6 +129,7 @@ fn armed() -> UiScript {
/// The loader itself: the window and its 19 slots + model pane materialize with no errors.
#[test]
fn shipped_inspect_frame_loads_clean() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_xml(&s, "Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
@@ -152,7 +138,7 @@ fn shipped_inspect_frame_loads_clean() {
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
load_xml(&s, "HonorFrame.xml");
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
load_xml(&s, "InspectFrame.xml");
// All 19 slots exist and carry their GetInventorySlotInfo id (1..=19, no ammo slot).
for (name, id) in [
@@ -174,6 +160,7 @@ fn shipped_inspect_frame_loads_clean() {
/// call in range does both. A `CanInspect` stuck truthy passes every other test in this file.
#[test]
fn inspect_unit_refuses_out_of_range() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = armed();
// 11 yards (d² = 121) — past the verified 100.0 threshold.
s.set_unit_reach(HashMap::from([("target".to_string(), reach(121.0))]));
@@ -212,6 +199,7 @@ fn inspect_unit_refuses_out_of_range() {
/// silently normalizes — hence a test on the boundary itself.
#[test]
fn range_predicates_transcribe_the_verified_thresholds() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = armed();
for (d2, can_inspect, interact1) in [
(99.9_f64, true, true),
@@ -301,6 +289,7 @@ fn range_predicates_transcribe_the_verified_thresholds() {
/// slot; the inspected token must read the foreign one and `"player"` must still read ours.
#[test]
fn inventory_bindings_route_by_unit_token() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = armed();
s.set_inventory_slots(own_slots());
@@ -336,6 +325,7 @@ fn inventory_bindings_route_by_unit_token() {
/// moving the booth yaw, and closing firing `ClearInspectPlayer`.
#[test]
fn shipped_inspect_frame_drives_end_to_end() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = armed();
// Hidden at load, and no sound queued (never transitions on startup).
@@ -473,6 +463,7 @@ fn shipped_inspect_frame_drives_end_to_end() {
/// second one non-obvious.
#[test]
fn the_active_tab_is_inert_and_toggle_inspect_closes() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = armed();
s.run(r#"InspectUnit("target")"#).unwrap();
let _ = s.take_sounds();
@@ -8,22 +8,7 @@
use benilla_ui::script::UiScript;
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The three bindings answer with the reference's own shapes — `IsInInstance` a PAIR, both
/// readers `1`/`nil` rather than `true`/`false` (the reference pushes the double 1.0), and
+7
View File
@@ -767,6 +767,13 @@ impl benilla_ui::script::TextMeasure for FixedWidthFont {
#[cfg(test)]
mod test_ui;
/// [`test_ui::load_ui`] for a test module OUTSIDE `ui_script` — `ui_action::feed_tests` drives the
/// real `UIErrorsFrame` end to end and needs the same both-stores reader everything else uses.
#[cfg(test)]
pub(crate) fn load_ui_for_test(script: &benilla_ui::script::UiScript, entry: &str) -> usize {
test_ui::load_ui(script, entry)
}
#[cfg(test)]
mod cinematic_tests;
@@ -15,22 +15,7 @@
use benilla_ui::script::{QuadContent, UiScript};
/// One shipped file into `s`, asserting it loaded clean.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The kit plus the two frames an addon would declare: one on each template.
fn harness(money: u64) -> UiScript {
@@ -7,22 +7,7 @@
use benilla_ui::script::{ActionSlot, QuadContent, ScriptValue, SpellTooltipView, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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(),
"loader errors in {file}: {:?}",
report.errors
);
}
use super::test_ui::load_ui as load_xml;
/// Load the shipped `UIParent.xml` (UIParent_ManageFramePositions — the stance bar's
/// OnShow/OnHide calls it, decision 0272; the runtime loads it before every bar) +
@@ -30,17 +30,7 @@
use benilla_ui::script::UiScript;
/// One shipped file into `s`, asserting it loaded clean.
fn load_xml(s: &UiScript, file: &str) {
let text = std::fs::read_to_string(ui_dir().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
);
}
use super::test_ui::load_ui as load_xml;
fn ui_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui")
@@ -7,22 +7,7 @@
use benilla_ui::script::{QuadContent, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The bar on a 1024×768 screen. 1024 wide is deliberate: the 1024-wide bar then spans x 0..1024,
/// so every ref offset below is also an absolute screen coordinate.
@@ -4,22 +4,7 @@
use benilla_ui::script::{PetActionView, QuadContent, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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(),
"loader errors in {file}: {:?}",
report.errors
);
}
use super::test_ui::load_ui as load_xml;
/// The pet bar's own load prerequisites, in manifest order: UiPanels (`SetDesaturation`, the
/// disabled-bar grey), UIParent (the managed bottom stack its OnShow/OnHide re-fires), Cooldown
@@ -9,23 +9,7 @@ use benilla_ui::script::{
AuraState, QuadContent, ScriptValue, SelectionRequest, UiScript, UnitState,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error (the unit-frame tests'
/// loader).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The pet frame's production load prefix — it is parented to `PlayerFrame`, so the whole
/// unit-frame file comes with it, and the tooltip/dropdown kit its hover and its neighbours need.
@@ -11,22 +11,7 @@ use benilla_ui::script::{
PetStats, QuadContent, ScriptValue, UiScript, UnitCombatStats, UnitState,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error (the character tests' loader).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The page's production load prefix — it leans on `CharacterFrame.xml` for the shared setters,
/// the row templates and the tab row itself, exactly as the manifest orders them.
@@ -19,36 +19,7 @@
use benilla_ui::script::UiScript;
/// Load one shipped `assets/ui/<file>`, panicking on any loader error **and on any
/// unknown-template warning** — `guild_tests`' loader, and the decision-0672 reason is the same:
/// `inherits=` a template this house doesn't ship is a *warning*, the frame still loads, and a
/// bare Button still clicks. Both windows here inherit from `UIPanelTemplates.xml` and
/// `MoneyFrame.xml`, and the registrar defines one local template of its own.
fn load_xml(s: &UiScript, file: &str) {
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
);
let missing: Vec<&String> = report
.warnings
.iter()
.filter(|w| w.contains("unknown template"))
.collect();
assert!(
missing.is_empty(),
"{file}: inherits a template this house does not ship (the frame loads, its ART does \
not): {missing:?}"
);
}
use super::test_ui::load_ui_strict as load_xml;
/// The charter engine API, stood in for in Lua.
///
@@ -114,13 +85,31 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run(PETITION_FIXTURE).unwrap();
// The player's own strings: the reference's `PetitionFrame_Update` formats
// GUILD_CHARTER_TEMPLATE and reads GUILD_PETITION_*_INSTRUCTIONS and NOT_YET_SIGNED straight
// out of GlobalStrings, with no fallback of its own — `format(nil, …)` raises, and the window
// never paints.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
load_xml(&s, "BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "GuildRegistrarFrame.xml");
load_xml(&s, "PetitionFrame.xml");
// `QuestTitleButtonTemplate`, which the reference's registrar inherits for its two service
// rows — 1.12 declares it in QuestFrameTemplates.xml, an `<Include>` of QuestFrame.xml, and
// ours declares it in QuestFrame.xml directly. An unknown template is a loader WARNING, so
// without this the rows build with no art at all and nothing goes red — which is exactly the
// failure `load_ui_strict` exists to turn into a red test.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "QuestFrame.xml");
// `ChatFrameEditBox`, which the reference's own purchase button indexes on every click to
// decide where focus goes after the name box closes — a nil there raises before the charter is
// bought. Ours guarded it; the reference does not.
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrops
load_xml(&s, "UIDropDownMenu.xml"); // ChatFrame's seven dropdowns inherit its template
load_xml(&s, "ChatFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GuildRegistrarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\PetitionFrame.xml");
s
}
@@ -161,6 +150,7 @@ fn show_registrar(s: &mut UiScript) {
/// blank row 2 while every other assertion here still passed.
#[test]
fn a_signers_charter_shows_the_sign_face_and_nine_rows() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
show_petition(&mut s);
@@ -223,6 +213,7 @@ fn a_signers_charter_shows_the_sign_face_and_nine_rows() {
/// either view alone.
#[test]
fn the_owners_charter_swaps_sign_for_request_and_rename() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
show_petition(&mut s);
@@ -245,6 +236,7 @@ fn the_owners_charter_swaps_sign_for_request_and_rename() {
/// still pass.
#[test]
fn request_signature_disables_at_the_wires_requirement_not_at_nine() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run(
r#"
@@ -279,6 +271,7 @@ fn request_signature_disables_at_the_wires_requirement_not_at_nine() {
/// a signer's view and still be unsignable (already signed, already guilded, full).
#[test]
fn the_sign_button_follows_can_sign_petition() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
show_petition(&mut s);
assert!(s
@@ -304,6 +297,7 @@ fn the_sign_button_follows_can_sign_petition() {
/// window closes, and without it the engine keeps a charter session for a window nobody can see.
#[test]
fn the_charter_buttons_reach_their_verbs_and_the_close_clears_the_session() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
show_petition(&mut s);
let _ = calls(&s);
@@ -326,6 +320,7 @@ fn the_charter_buttons_reach_their_verbs_and_the_close_clears_the_session() {
/// 24-character charter-name limit.
#[test]
fn rename_guild_sends_the_box_text_and_caps_at_twenty_four() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
show_petition(&mut s);
@@ -358,6 +353,7 @@ fn rename_guild_sends_the_box_text_and_caps_at_twenty_four() {
/// before the player has typed a name.
#[test]
fn the_registrar_opens_on_services_and_purchase_is_a_local_panel_swap() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
show_registrar(&mut s);
@@ -409,6 +405,7 @@ fn the_registrar_opens_on_services_and_purchase_is_a_local_panel_swap() {
/// window will be told.
#[test]
fn purchase_sends_the_typed_name_and_register_turns_the_charter_in() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
show_registrar(&mut s);
s.run("GuildRegistrarButton1:Click()").unwrap();
@@ -444,6 +441,7 @@ fn purchase_sends_the_typed_name_and_register_turns_the_charter_in() {
/// charter list, and the greeting panel never displays one.
#[test]
fn the_charter_price_is_read_when_the_purchase_panel_opens() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
show_registrar(&mut s);
s.run("GuildRegistrarButton1:Click()").unwrap();
@@ -456,6 +454,7 @@ fn the_charter_price_is_read_when_the_purchase_panel_opens() {
/// panel left over from last time.
#[test]
fn reopening_the_registrar_returns_to_the_services_list() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
show_registrar(&mut s);
s.run("GuildRegistrarButton1:Click()").unwrap();
@@ -472,6 +471,7 @@ fn reopening_the_registrar_returns_to_the_services_list() {
/// in no slot and two left-slot windows paint over each other (B288's shape, decision 1507).
#[test]
fn both_charter_windows_are_registered_left_slot_panels() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
for frame in ["GuildRegistrarFrame", "PetitionFrame"] {
assert_eq!(
@@ -5,23 +5,7 @@
use benilla_ui::script::{QuestPanel, QuestState, ScriptValue, SoundRequest, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the panel tests'
/// loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The questgiver window's open/close kits — the window-sound convention (decision 0090). The real
/// QuestFrame.lua plays igQuestListOpen in QuestFrame_OnShow (l.285) and igQuestListClose in
@@ -13,22 +13,7 @@ use benilla_ui::script::{
QuestLogEntryView, QuestLogObjectiveView, QuestLogState, ScriptValue, UiScript,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// Fonts + UIParent (SecondsToTime lives there) + the window itself. Deliberately NOT the whole
/// manifest: the window must stand up on its own dependencies, and a harness that loads everything
@@ -36,9 +21,22 @@ fn load_xml(s: &UiScript, file: &str) {
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The window's title is `text="QUEST_TIMERS"`, resolved against the global at load — without
// the player's own strings it draws the KEY, which is exactly what a `text=` attribute does
// with an unknown name.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
// `SecondsToTime`, which the reference's `QuestTimerFrame_Update` formats every row with, and
// `UIParent_ManageFramePositions`, which its OnShow/OnHide call.
load_xml(&s, "UIParent.xml");
load_xml(&s, "QuestTimerFrame.xml");
// `MAX_QUESTS`, the loop bound the reference's repaint hides its spare rows with. 1.12
// declares it on QuestLogFrame.lua:2 and so do we (1751 window 16) — a nil there is
// `'for' limit must be a number` on the first repaint, not a missing row.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
load_xml(&s, "Interface\\FrameXML\\QuestTimerFrame.xml");
s
}
@@ -83,6 +81,7 @@ fn row_text(s: &UiScript, i: u32) -> String {
/// `45 + 16·n`; and back to hidden when the last timer goes.
#[test]
fn the_window_follows_the_timed_quests() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
// An untimed log: QUEST_LOG_UPDATE fires, the frame stays down.
@@ -132,6 +131,7 @@ fn the_window_follows_the_timed_quests() {
/// and `mirror_timer_tests` were both written for.
#[test]
fn the_countdown_reaches_the_draw_list() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_quest_log(log(vec![quest(1, "Deliver the Message", 1_000_860)]));
s.set_server_unix_time(1_000_000.0);
@@ -162,6 +162,7 @@ fn the_countdown_reaches_the_draw_list() {
/// change again.
#[test]
fn a_world_entry_paints_a_quest_already_running() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_quest_log(log(vec![quest(1, "Deliver the Message", 1_000_090)]));
s.set_server_unix_time(1_000_000.0);
@@ -181,12 +182,20 @@ fn a_world_entry_paints_a_quest_already_running() {
/// `QuestTimer1: ours [(140.0, 16.0), (0.0, -29.0)] != ref [(140.0, 16.0), (0.0, -30.0)]`.
#[test]
fn the_window_geometry_matches_the_reference_framexml() {
let Some(reference) = super::framexml_diff::reference("QuestTimerFrame.xml") else {
let _data = benilla_formats::wow_data_or_skip!();
let Some(reference) =
super::framexml_diff::reference("Interface\\FrameXML\\QuestTimerFrame.xml")
else {
eprintln!("skipping: no extracted FrameXML");
return;
};
// No exemptions: this window's frames keep the reference's bare names (decision 0591 §3 — the
// manage pass and the ref's own row lookup both resolve by literal name), and every number in
// it is the reference's. If an entry ever needs to go here, it names its reason.
super::framexml_diff::assert_geometry_matches("QuestTimerFrame.xml", &reference, &[], 22);
super::framexml_diff::assert_geometry_matches(
"Interface\\FrameXML\\QuestTimerFrame.xml",
&reference,
&[],
22,
);
}
@@ -8,23 +8,7 @@ use benilla_ui::script::{
QuestLogEntryView, QuestLogObjectiveView, QuestLogState, SoundRequest, UiScript,
};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the panel/quest
/// tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// Find a bare frame's own rect via its `QuadContent::Frame` entry (`panel_tests.rs`'s helper,
/// duplicated) — used to locate the window so the wheel test's coordinates land inside it.
@@ -427,11 +411,11 @@ fn watch_guards_no_op_without_erroring() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
load_xml(&s, "ErrorsFrame.xml"); // the guards' red-line surface
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // the guards' red-line surface
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
+1 -29
View File
@@ -16,35 +16,7 @@ use benilla_ui::script::{
UiScript,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error **or unknown-template
/// warning** — `friends_tests::load_xml`'s reason verbatim: `inherits=` a template this house does
/// not ship is a warning, the frame still loads, and every behavioural test stays green while the
/// art is missing.
fn load_xml(s: &UiScript, file: &str) {
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
);
let missing: Vec<&String> = report
.warnings
.iter()
.filter(|w| w.contains("unknown template"))
.collect();
assert!(
missing.is_empty(),
"{file}: inherits a template this house does not ship (the frame loads, its ART does \
not): {missing:?}"
);
}
use super::test_ui::load_ui_strict as load_xml;
/// The window's slice of the manifest, in `load_default_ui` order. `UIParent.xml` is in it for
/// two functions the pane really calls — `MouseIsOver` (the drag's hover sweep) and
@@ -15,23 +15,7 @@
use benilla_ui::script::{FactionEntry, ReputationState, UiScript, UnitState};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the
/// skills/character tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// An ordinary bar row: visible, not a header, `standing_id` 5 ("Friendly") sitting 1000 into a
/// 6000-wide rank window. The same numbers `benilla-ui`'s own fixture uses.
@@ -8,21 +8,7 @@
use benilla_ui::script::UiScript;
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
@@ -14,23 +14,7 @@
use benilla_ui::script::{QuadContent, SkillEntry, SkillsState, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the
/// character/spellbook tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
fn skill(
skill_id: u32,
@@ -5,23 +5,7 @@
use benilla_ui::script::{SpellBookState, SpellSlotView, SpellTabView, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the
/// character/questlog tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// Two tabs' worth of a small book: "Fire" (Fireball, Fire Blast — slots 0-1) and "Frost" (Frost
/// Armor — slot 2), the flat `slots` in tab order. `offset` is each tab's 0-based start index into
@@ -11,21 +11,7 @@
use benilla_ui::script::{AuraState, QuadContent, ScriptValue, UiScript, UnitState};
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
+2 -18
View File
@@ -8,23 +8,7 @@
use benilla_ui::script::{ScriptValue, TaxiNodeType, TaxiUiNode, TaxiUiState, UiScript};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) -> usize {
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
);
report.frames
}
use super::test_ui::load_ui as load_xml;
/// Load the taxi window + its deps into a fresh script, screen sized.
fn taxi_script() -> UiScript {
@@ -34,7 +18,7 @@ fn taxi_script() -> UiScript {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml"); // TaxiNodeOnButtonEnter's tooltip + SetTooltipMoney
load_xml(&s, "ErrorsFrame.xml"); // BenillaErrorsFrame_AddMessage — DrawOneHopLines' refusal
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // BenillaErrorsFrame_AddMessage — DrawOneHopLines' refusal
load_xml(&s, "TaxiFrame.xml");
s
}
+28 -1
View File
@@ -28,6 +28,21 @@ use benilla_ui::script::UiScript;
/// **A chain entry needs client data**, so a test that names one has to open with
/// `benilla_formats::wow_data_or_skip!()`; [`BAG_UI`] is a list that always does.
pub(super) fn load_ui(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, false)
}
/// [`load_ui`], and **a missing template is a failure too**.
///
/// A frame that inherits a template nothing declares is a loader *warning*, not an error: the frame
/// is built and simply has none of the template's art. So an under-loaded dependency list passes
/// [`load_ui`] and then loses a window's whole skin silently — which is why four of the social
/// windows' test modules grew this check by hand. It is one function now rather than four copies,
/// and any test may ask for it.
pub(super) fn load_ui_strict(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, true)
}
fn load_entry(s: &UiScript, entry: &str, strict_templates: bool) -> usize {
let path = entry.replace('\\', "/");
let bytes = read(&path).unwrap_or_else(|| panic!("{entry}: not found"));
if path.to_ascii_lowercase().ends_with(".lua") {
@@ -44,6 +59,18 @@ pub(super) fn load_ui(s: &UiScript, entry: &str) -> usize {
"{entry}: loader errors: {:?}",
report.errors
);
if strict_templates {
let missing: Vec<&String> = report
.warnings
.iter()
.filter(|w| w.contains("unknown template"))
.collect();
assert!(
missing.is_empty(),
"{entry}: inherits a template this house does not ship (the frame loads, its ART does \
not): {missing:?}"
);
}
report.frames
}
@@ -112,7 +139,7 @@ pub(super) const BAG_UI: &[&str] = &[
// `StackSplitFrame` is not optional either: the reference's own
// `ContainerFrameItemButton_OnClick` calls `StackSplitFrame:Hide()` on EVERY plain click
// (ContainerFrame.lua l.581) before the pickup, and opens it on the shift fork.
"StackSplit.xml",
"Interface\\FrameXML\\StackSplitFrame.xml",
// …nor is the chat edit box. The reference's SHIFT arm opens with
// `if ( ChatFrameEditBox:IsShown() )` (ContainerFrame.lua l.569) to decide between posting the
// item's link and splitting the stack, so a VM without it raises before either.
@@ -11,23 +11,7 @@
use benilla_ui::script::{AuraState, UiScript, UnitState};
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the bag/panel
/// tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// A 1024×768 screen with the anchor law's three fixed files (fonts, the real UIParent, the
/// real GameTooltip) plus `extra`. `CONTAINER_OFFSET_X/Y` hold their UIParent.xml load values
@@ -27,21 +27,7 @@
use benilla_ui::script::UiScript;
fn ui_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui")
}
/// One shipped `assets/ui/<file>` into `s`, asserting it loaded clean.
fn load_xml(s: &UiScript, file: &str) {
let text = std::fs::read_to_string(ui_dir().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
);
}
use super::test_ui::load_ui as load_xml;
/// An **addon's** document text, loaded after the shipped files exactly as `LoadAddOn` would —
/// the template registry is persistent, so `inherits=` reaches what `GameTooltip.xml` registered.
@@ -18,22 +18,7 @@ use benilla_ui::script::{
SelectionRequest, UiScript, UnitState,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error (the unit-frame tests').
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// A unit snapshot with a guid — the identity `UnitIsUnit` compares, which the "your target is
/// you" gate reads.
@@ -2,23 +2,7 @@ use benilla_ui::script::{
QuadContent, ScriptValue, SelectionRequest, SoundRequest, UiScript, UnitState,
};
/// Load one shipped `assets/ui/<file>` (the panel tests' loader, duplicated to stay
/// self-contained), panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
/// The unit frames' production load prefix (ui_script/mod.rs order): fonts + UIParent +
/// tooltip, then the dropdown kit + unit popups the frames' DropDown children initialize into.
@@ -9,21 +9,7 @@
use benilla_ui::script::{QuadContent, ScriptValue, UiScript, WorldStateUiView};
fn load_xml(s: &UiScript, file: &str) {
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
);
}
use super::test_ui::load_ui as load_xml;
pub(crate) fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
+13
View File
@@ -45,6 +45,19 @@
//! `foreachi`. `reference/1.12-globals.tsv` lists all three; we had the other seventeen of that
//! family and not these.
//!
//! ## What this module does NOT have to do: the vararg `arg` table
//!
//! Lua 5.0 gives every vararg function an implicit `arg` table with an `n` field, and 1.12's
//! FrameXML uses it directly — `QuestTimerFrame_Update(...)` walks `for i=1, arg.n`, and so do
//! `FCFDropDown_LoadChannels`, `TradeSkill_OnEvent` and the GM-ticket and CVar families. Three of
//! our own transcriptions carry a note saying they rewrote it "the 5.1-native way" with
//! `select("#", ...)`, which reads as though the dialect demanded it.
//!
//! **It does not.** mlua's vendored 5.1 is built with `LUA_COMPAT_VARARG`, so `arg` and `arg.n`
//! are already there — measured, not assumed: `local function f(...) return arg.n end; f('a','b')`
//! answers 2 in this VM. That matters for 1751: a stock file using 5.0 varargs needs nothing from
//! this module, and the rewrite was a precaution against a problem we did not have.
//!
//! ## The one known divergence, stated rather than hidden
//!
//! `table.insert`/`table.remove` stay on 5.1's `#t` border rather than consulting `getn`. **This