bindings answer the reference's shapes, and the panel kit goes stock

- four return shapes were wrong — `UnitPowerType` returns ONE value in 5875 (the Era `(type, "RAGE")` pair does not exist, and its miss arm pushes the NUMBER 0, which `UnitFrame.lua:122` indexes `ManaBarColor` with), `UnitName` returns `(name, server)`, `GetTexCoord` returns EIGHT (ours four — a shape that exists nowhere, with zero call sites in FrameXML, so only the binary could have caught it), `GetLootMethod` answers `freeforall` ungrouped — the fifth right-name-wrong-arity finding in two days, so wow-re's `binding-shapes.tsv` (1722 rows over all 82 registrar tables) is vendored and a gate probes every binding's arity against it: 313 probes, eight divergences (seven global, one widget); `GetBuildInfo` pushed a fourth value no 1.12 client does (458 corpus sites take one, the four that destructure a fourth read nil on the real client), `GetAddOnInfo` RAISES on a numeric index, `Model:GetFogColor` is a packed `0xAARRGGBB` dword with no unset state, and the five miss branches are the reference's exact tuples
- the panel-template kit is the reference's own `UIPanelTemplates` pair (our 18 templates were a strict subset — `TabButtonTemplate` and the `ScrollingEdit_*` family arrive, which unblocks the macro window and HelpFrame), and the swap found two structural facts: `FauxScrollFrameTemplate` is a `<ScrollFrame>` while every instance we declared was a `<Frame>` (fifteen elements across twelve files), and an instance `<OnLoad>` REPLACES the template's — four FriendsFrame lists set `updateFunc` in their own OnLoad, so `ScrollFrame_OnLoad` never ran and guild/friends/ignore/who had been silently painting nothing behind a `frame.offset or 0`
- the options widget kit (`OptionFrameBoxTemplate`, `OptionsButtonTemplate`, `OptionsCheckButtonTemplate`, `OptionsSliderTemplate`) comes off the chain too, giving back the reference's own per-widget hover; one template of ours stays because `UIOptionsFrame.xml` is not built here, and the lesson is general — when a file SHRINKS rather than dies, every harness that named it gains the chain entry beside it, not instead of it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 17:06:36 +07:00
co-authored by Claude Fable 5.1
parent 2bebb94471
commit b45bce8184
55 changed files with 1003 additions and 764 deletions
+10 -10
View File
@@ -1916,7 +1916,7 @@
right edge at (-2,5) / (-2,-2). Those offsets against our FauxScrollFrameTemplate's bar
(seated 6,-16 / 6,+16) are exactly the 21/20 overhang that lands each arrow button in its
own authored socket — ScrollTemplates.xml's own law, reached from the other side. -->
<Frame name="BenillaAuctionScrollTemplate" inherits="FauxScrollFrameTemplate" virtual="true">
<ScrollFrame name="BenillaAuctionScrollTemplate" inherits="FauxScrollFrameTemplate" virtual="true">
<Layers>
<Layer level="ARTWORK">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
@@ -1931,7 +1931,7 @@
</Texture>
</Layer>
</Layers>
</Frame>
</ScrollFrame>
<!-- ═══ The window (ref-xml l.5-1982) ══════════════════════════════════════════════════════
832x447, and NO <Anchors>: it is registered `area = "doublewide"` (UiPanels.xml), so the
@@ -2053,10 +2053,10 @@
<Button name="AuctionFilterButton13" inherits="AuctionClassButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AuctionFilterButton12" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AuctionFilterButton14" inherits="AuctionClassButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AuctionFilterButton13" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AuctionFilterButton15" inherits="AuctionClassButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AuctionFilterButton14" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Frame name="BrowseFilterScrollFrame" inherits="BenillaAuctionScrollTemplate">
<ScrollFrame name="BrowseFilterScrollFrame" inherits="BenillaAuctionScrollTemplate">
<Size><AbsDimension x="160" y="305"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPLEFT"><Offset><AbsDimension x="158" y="-105"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<Button name="BrowseQualitySort" inherits="AuctionSortButtonTemplate" text="RARITY">
<Size><AbsDimension x="214" y="19"/></Size>
@@ -2094,10 +2094,10 @@
<Button name="BrowseButton6" inherits="BrowseButtonTemplate" id="6"><Anchors><Anchor point="TOPLEFT" relativeTo="BrowseButton5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BrowseButton7" inherits="BrowseButtonTemplate" id="7"><Anchors><Anchor point="TOPLEFT" relativeTo="BrowseButton6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BrowseButton8" inherits="BrowseButtonTemplate" id="8"><Anchors><Anchor point="TOPLEFT" relativeTo="BrowseButton7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Frame name="BrowseScrollFrame" inherits="BenillaAuctionScrollTemplate">
<ScrollFrame name="BrowseScrollFrame" inherits="BenillaAuctionScrollTemplate">
<Size><AbsDimension x="465" y="306"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="39" y="-105"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<Button name="BrowsePrevPageButton" inherits="BenillaAuctionPrevPageTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BrowseButton8" relativePoint="LEFT"><Offset><AbsDimension x="5" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BrowsePageButton_OnClick(-1)</OnClick></Scripts>
@@ -2264,10 +2264,10 @@
<Button name="BidButton7" inherits="BidButtonTemplate" id="7"><Anchors><Anchor point="TOPLEFT" relativeTo="BidButton6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BidButton8" inherits="BidButtonTemplate" id="8"><Anchors><Anchor point="TOPLEFT" relativeTo="BidButton7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BidButton9" inherits="BidButtonTemplate" id="9"><Anchors><Anchor point="TOPLEFT" relativeTo="BidButton8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Frame name="BidScrollFrame" inherits="BenillaAuctionScrollTemplate">
<ScrollFrame name="BidScrollFrame" inherits="BenillaAuctionScrollTemplate">
<Size><AbsDimension x="625" y="335"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="40" y="-74"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<Button name="BidPrevPageButton" inherits="BenillaAuctionPrevPageTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BidButton9" relativePoint="LEFT"><Offset><AbsDimension x="5" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BidPageButton_OnClick(-1)</OnClick></Scripts>
@@ -2381,10 +2381,10 @@
<Button name="AuctionsButton7" inherits="AuctionsButtonTemplate" id="7"><Anchors><Anchor point="TOPLEFT" relativeTo="AuctionsButton6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AuctionsButton8" inherits="AuctionsButtonTemplate" id="8"><Anchors><Anchor point="TOPLEFT" relativeTo="AuctionsButton7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AuctionsButton9" inherits="AuctionsButtonTemplate" id="9"><Anchors><Anchor point="TOPLEFT" relativeTo="AuctionsButton8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Frame name="AuctionsScrollFrame" inherits="BenillaAuctionScrollTemplate">
<ScrollFrame name="AuctionsScrollFrame" inherits="BenillaAuctionScrollTemplate">
<Size><AbsDimension x="435" y="339"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="40" y="-72"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<Button name="AuctionsPrevPageButton" inherits="BenillaAuctionPrevPageTemplate">
<Anchors><Anchor point="LEFT" relativeTo="AuctionsButton9" relativePoint="LEFT"><Offset><AbsDimension x="5" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnClick>AuctionsPageButton_OnClick(-1)</OnClick></Scripts>
+2 -2
View File
@@ -874,10 +874,10 @@
container over the 8 rows; only its right-hand Slider scroll bar renders. Sized to the
8-row span (CRAFTS_DISPLAYED × CRAFT_SKILL_HEIGHT), TradeSkillFrame.xml's own
same-TOPLEFT-as-row1 convention. updateFunc set in BenillaCraftFrame_OnLoad. -->
<Frame name="BenillaCraftListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="BenillaCraftListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-96"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<!-- The detail icon (ref CraftIcon, l.540-577): a 37×37 button over a 64×64 UI-EmptySlot
ring (TrainerFrame.xml's own BenillaTrainerSkillIcon shape, TradeSkillFrame.xml's own
+56 -12
View File
@@ -2206,11 +2206,22 @@
<Anchors><Anchor point="TOP" relativeTo="FriendsFrameFriendButton9" relativePoint="BOTTOM"/></Anchors>
</Button>
<Frame name="FriendsFrameFriendsScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="FriendsFrameFriendsScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="304"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="FriendsFrame"><Offset><AbsDimension x="-67" y="-75"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>self.updateFunc = FriendsList_Update</OnLoad></Scripts>
</Frame>
<Scripts><OnLoad>
<!-- `ScrollFrame_OnLoad()` FIRST: an instance OnLoad
REPLACES the template's, and the template's is what
seats `this.offset = 0`. Without it
`FauxScrollFrame_GetOffset` answers nil and the list
never paints. This is the reference's own idiom —
`SkillFrame.xml:556` and
`ClassTrainerFrameTemplates.xml:111` both open the
same way. Decision 1846. -->
ScrollFrame_OnLoad()
this.updateFunc = FriendsList_Update
</OnLoad></Scripts>
</ScrollFrame>
</Frames>
<Scripts>
<OnMouseWheel>BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameFriendsScrollFrame, arg1)</OnMouseWheel>
@@ -2315,13 +2326,24 @@
<Anchors><Anchor point="TOP" relativeTo="FriendsFrameIgnoreButton19" relativePoint="BOTTOM"/></Anchors>
</Button>
<Frame name="FriendsFrameIgnoreScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="FriendsFrameIgnoreScrollFrame" inherits="FauxScrollFrameTemplate">
<!-- 332 tall, not the friends list's 304: 20 short rows reach further
down than 10 tall ones (ref l.1146-1148). -->
<Size><AbsDimension x="296" y="332"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="FriendsFrame"><Offset><AbsDimension x="-67" y="-75"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>self.updateFunc = IgnoreList_Update</OnLoad></Scripts>
</Frame>
<Scripts><OnLoad>
<!-- `ScrollFrame_OnLoad()` FIRST: an instance OnLoad
REPLACES the template's, and the template's is what
seats `this.offset = 0`. Without it
`FauxScrollFrame_GetOffset` answers nil and the list
never paints. This is the reference's own idiom —
`SkillFrame.xml:556` and
`ClassTrainerFrameTemplates.xml:111` both open the
same way. Decision 1846. -->
ScrollFrame_OnLoad()
this.updateFunc = IgnoreList_Update
</OnLoad></Scripts>
</ScrollFrame>
</Frames>
<Scripts>
<OnMouseWheel>BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameIgnoreScrollFrame, arg1)</OnMouseWheel>
@@ -2454,11 +2476,22 @@
<FontString inherits="ChatFontNormal"/>
</EditBox>
<Frame name="WhoListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="WhoListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="287"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="FriendsFrame" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-67" y="-96"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>self.updateFunc = WhoList_Update</OnLoad></Scripts>
</Frame>
<Scripts><OnLoad>
<!-- `ScrollFrame_OnLoad()` FIRST: an instance OnLoad
REPLACES the template's, and the template's is what
seats `this.offset = 0`. Without it
`FauxScrollFrame_GetOffset` answers nil and the list
never paints. This is the reference's own idiom —
`SkillFrame.xml:556` and
`ClassTrainerFrameTemplates.xml:111` both open the
same way. Decision 1846. -->
ScrollFrame_OnLoad()
this.updateFunc = WhoList_Update
</OnLoad></Scripts>
</ScrollFrame>
</Frames>
<Scripts>
<!-- SetWhoToUI decides where the NEXT answer goes: this frame, or the chat
@@ -2713,11 +2746,22 @@
why the ref's two loose UI-Character-ScrollBar trough textures are omitted:
our kit draws a complete bar and they would double it. The other three
lists in this file are built exactly the same way. -->
<Frame name="GuildListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="GuildListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="237"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="GuildFrame" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-67" y="-98"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>self.updateFunc = GuildStatus_Update</OnLoad></Scripts>
</Frame>
<Scripts><OnLoad>
<!-- `ScrollFrame_OnLoad()` FIRST: an instance OnLoad
REPLACES the template's, and the template's is what
seats `this.offset = 0`. Without it
`FauxScrollFrame_GetOffset` answers nil and the list
never paints. This is the reference's own idiom —
`SkillFrame.xml:556` and
`ClassTrainerFrameTemplates.xml:111` both open the
same way. Decision 1846. -->
ScrollFrame_OnLoad()
this.updateFunc = GuildStatus_Update
</OnLoad></Scripts>
</ScrollFrame>
<!-- The page button that flips the two views. Its LEFT anchor is re-set by
`GuildStatus_Update` to 284 or 307 depending on whether the scroll bar is
+2 -2
View File
@@ -903,7 +903,7 @@
bracketing the 4-row grid. 0983 sized it to the grid instead (252x152 at TOPLEFT
24,-85) and the bar came out at x 282..298: hanging half off the plate's right
edge, over the world. -->
<Frame name="BenillaMacroPopupScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="BenillaMacroPopupScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="195"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-39" y="-67"/></Offset></Anchor>
@@ -916,7 +916,7 @@
self.updateFunc = BenillaMacroPopupFrame_Update
</OnLoad>
</Scripts>
</Frame>
</ScrollFrame>
<CheckButton name="BenillaMacroPopupButton1" inherits="BenillaMacroPopupButtonTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="24" y="-85"/></Offset></Anchor></Anchors>
@@ -3433,7 +3433,7 @@
seating). The wheel handler is NOT here: a spin over a row
bubbles up the PARENT chain (pointer.rs), and this frame is
the rows' sibling — it lives on the body below. -->
<Frame name="$parentScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="$parentScrollFrame" inherits="FauxScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-47"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-32" y="28"/></Offset></Anchor>
@@ -3444,7 +3444,7 @@
this.updateFunc = KeyBindingsPage_Update
</OnLoad>
</Scripts>
</Frame>
</ScrollFrame>
</Frames>
<!-- The page's wheel: rows and the bar all bubble here (the
QuestLog/Trainer lesson — a handler on the sibling catcher
@@ -1,145 +1,39 @@
<Ui>
<!--
The shared OPTIONS widget kit — our build of the five templates addons name, specified by
Interface\FrameXML\OptionsFrameTemplates.xml (line numbers below are that file's) plus the
one template from UIOptionsFrame.xml that addons actually name.
WHAT IS LEFT OF THIS FILE, AND WHY IT STILL EXISTS (decision 1841).
These are UIPanelTemplates.xml's descendants: `OptionsCheckButtonTemplate` inherits
`UICheckButtonTemplate` and `OptionsButtonTemplate` inherits `UIPanelButtonTemplate`, so
this file must load after it (the .toc seats them together).
The four templates this file used to build — `OptionFrameBoxTemplate`,
`OptionsButtonTemplate`, `OptionsCheckButtonTemplate` and `OptionsSliderTemplate` — now come
off the player's chain out of `Interface\FrameXML\OptionsFrameTemplates.xml`, which the
manifest lists directly above this entry. Our transcription of them is gone.
WHY THE UIOptionsFrame ONE LIVES HERE. `UIOptionsCheckButtonTemplate` is 8 `CreateFrame`
call sites in the corpus — the fourth most-wanted template we lacked — and in the reference
it is declared in UIOptionsFrame.xml (l.6-10) as a two-line size override on
`OptionsCheckButtonTemplate`. We do not build UIOptionsFrame at all (benilla's own options
window is OptionsFrame.xml, a different design), so the one template that file contributes
to the shared namespace sits with its parent rather than in a file of its own. Its
provenance is on the declaration.
**The same basename appears twice in benilla.toc on purpose.** A bare name is a file we
ship, a path is the reference's own (the manifest's rule since 1751), so the two entries are
unambiguous to the loader even though they read alike to a person. This one loads second and
adds exactly one name the chain's file does not have.
Every TEMPLATE name here is in `reference/1.12-globals.tsv`; none is invented. The one name
that is ours is the hover helper below, and it wears the `Benilla` prefix that says so —
the reference open-codes that hover twice, once per widget, and two copies of one behaviour
is how they end up disagreeing. What this supersedes but does not delete: nothing —
OptionsFrame.xml's own `Options*Template` family (`OptionsRedButtonTemplate`,
`OptionsCheckboxRowTemplate`, `OptionsSliderRowTemplate`, …) are benilla's row widgets for
benilla's window, distinct names and a distinct shape.
THAT NAME IS `UIOptionsCheckButtonTemplate`, and its home in the reference is
`UIOptionsFrame.xml` — a file we do not build at all, because benilla's own options window
is `OptionsFrame.xml`, a different design. So the template has no chain file to arrive from
and would simply vanish if this file did.
It is not ours and not invented: it is in `reference/1.12-globals.tsv`, and the corpus names
it at 8 `CreateFrame` call sites — the nameplate addons build their whole options panel out
of it. `panel_template_tests.rs` is the driver that says so.
WHEN THIS FILE GOES: the day `UIOptionsFrame.xml` migrates. The template comes from there,
this file has nothing left, and the manifest loses a line.
WHAT THE SWAP GAVE BACK. Our version factored the two widgets' hover into one
`BenillaOptionsWidget_ShowTooltip` helper, on the reasoning that "two copies of one
behaviour is how they end up disagreeing". The reference open-codes it inline in each
template's `<OnEnter>`, identically, and that is what runs now — the helper is gone with the
rest, and nothing else called it.
-->
<Script><![CDATA[
-- The hover both widgets below share: an instance carries its own `tooltipText` (the
-- caption, wrapped) and optionally a `tooltipRequirement` (the "needs a restart / a relog"
-- line under it) as plain fields, set by whoever built the panel. Neither field is
-- required, and a widget with neither raises nothing.
--
-- BOTH LINES RENDER GOLD, and the second one's colour arguments are why that has to be
-- said out loud: `AddLine(text, "", 1, 1, 1)` is 1.12's archaic spelling, and the empty
-- string sitting in the r-slot is what makes the binding drop the whole colour tail and
-- fall back to the tooltip default (`script/tooltip/verbs.rs`'s AddLine carries the byte
-- note, and names this exact `(text, "", r, g, b)` shape as the corpus' own). The
-- arguments are kept because they are the reference's; the white they appear to ask for is
-- not what any client, ours or theirs, actually draws.
--
-- One home for it rather than one per template: the checkbox and the slider are different
-- widget types with no common ancestor to inherit from, so sharing the behaviour means
-- sharing a function. It takes the widget as an argument rather than reading `this`,
-- because a shared function has no handler of its own to read it from — each of the two
-- OnEnters below passes its own.
function BenillaOptionsWidget_ShowTooltip(widget)
local caption, requirement = widget.tooltipText, widget.tooltipRequirement
if caption then
GameTooltip:SetOwner(widget, "ANCHOR_RIGHT")
GameTooltip:SetText(caption, nil, nil, nil, nil, 1)
end
-- Only the requirement line asks for the tooltip to be shown, and that is the
-- reference's behaviour, not an oversight of ours: `SetText` already puts the plate on
-- screen, so the caption case needs no second verb, while `AddLine` onto a plate that
-- is already up has to ask for the relayout.
if requirement then
GameTooltip:AddLine(requirement, "", 1.0, 1.0, 1.0)
GameTooltip:Show()
end
end
]]></Script>
<!-- ref l.3-34. The titled group box: a tooltip-border backdrop with its caption hung above the
top-left corner, greyed by its own OnLoad. -->
<Frame name="OptionFrameBoxTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentTitle" inherits="GameFontHighlight">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="9" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize><AbsValue val="16"/></EdgeSize>
<TileSize><AbsValue val="16"/></TileSize>
<BackgroundInsets><AbsInset left="5" right="5" top="5" bottom="5"/></BackgroundInsets>
</Backdrop>
<Scripts>
<OnLoad>
this:SetBackdropBorderColor(0.4, 0.4, 0.4)
this:SetBackdropColor(0.5, 0.5, 0.5)
</OnLoad>
</Scripts>
</Frame>
<!-- ref l.35-39. The panel button at the options window's own 90x21. -->
<Button name="OptionsButtonTemplate" inherits="UIPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="90" y="21"/></Size>
</Button>
<!-- ref l.40-66. The checkbox with the options window's manners: a 100px hit rect to the right
so the LABEL is clickable too, the on/off click sounds, and the tooltipText /
tooltipRequirement hover pair an instance sets as plain fields. -->
<CheckButton name="OptionsCheckButtonTemplate" inherits="UICheckButtonTemplate" virtual="true">
<HitRectInsets><AbsInset left="0" right="-100" top="0" bottom="0"/></HitRectInsets>
<Scripts>
<!-- The click kit is the state the box has just reached, not the one it left. -->
<OnClick>
PlaySound(this:GetChecked() and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff")
</OnClick>
<OnEnter>BenillaOptionsWidget_ShowTooltip(this)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</CheckButton>
<!-- ref UIOptionsFrame.xml l.6-10 — see the header. A 26x26 override of the above, and the
template the corpus's nameplate addons build their whole options panel out of. -->
<!-- ref UIOptionsFrame.xml l.6-10: a 26x26 override of the chain's own
`OptionsCheckButtonTemplate`, which it inherits from. -->
<CheckButton name="UIOptionsCheckButtonTemplate" inherits="OptionsCheckButtonTemplate" virtual="true">
<Size><AbsDimension x="26" y="26"/></Size>
</CheckButton>
<!-- ref l.67-132. The horizontal options slider: a SliderBar backdrop with three labels
(caption above, LOW/HIGH under the ends) and the horizontal knob. `text="LOW"`/`"HIGH"` are
global-string keys, resolved by the loader's `resolve_text` against GlobalStrings. -->
<Slider name="OptionsSliderTemplate" orientation="HORIZONTAL" virtual="true" enableMouse="true">
<Size><AbsDimension x="128" y="17"/></Size>
<HitRectInsets><AbsInset left="0" right="0" top="-10" bottom="-10"/></HitRectInsets>
<Backdrop bgFile="Interface\Buttons\UI-SliderBar-Background" edgeFile="Interface\Buttons\UI-SliderBar-Border" tile="true">
<EdgeSize><AbsValue val="8"/></EdgeSize>
<TileSize><AbsValue val="8"/></TileSize>
<BackgroundInsets><AbsInset left="3" right="3" top="6" bottom="6"/></BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="BOTTOM" relativePoint="TOP"/></Anchors>
</FontString>
<FontString name="$parentLow" inherits="GameFontHighlightSmall" text="LOW">
<Anchors><Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="2" y="3"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="$parentHigh" inherits="GameFontHighlightSmall" text="HIGH">
<Anchors><Anchor point="TOPRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnter>BenillaOptionsWidget_ShowTooltip(this)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
<ThumbTexture name="$parentThumb" file="Interface\Buttons\UI-SliderBar-Button-Horizontal">
<Size><AbsDimension x="32" y="32"/></Size>
</ThumbTexture>
</Slider>
</Ui>
@@ -1574,7 +1574,7 @@
a `questLogOffset` upvalue this file kept "because no real ScrollFrame/
FauxScrollFrame engine exists yet" — long false, and the reason this window
published no QuestLogListScrollFrame for addons to find. -->
<Frame name="QuestLogListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="QuestLogListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="300" y="93"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="19" y="-75"/></Offset></Anchor></Anchors>
<Scripts>
@@ -1582,7 +1582,7 @@
FauxScrollFrame_OnVerticalScroll(QUESTLOG_QUEST_HEIGHT, BenillaQuestLogList_Update)
</OnVerticalScroll>
</Scripts>
</Frame>
</ScrollFrame>
<Button name="QuestLogTitle1" inherits="QuestLogTitleButtonTemplate" id="1">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="19" y="-75"/></Offset></Anchor></Anchors>
@@ -65,10 +65,12 @@
client mirror), so there is nothing new for a repaint to paint, and forcing one would
un-tick the box the player just ticked before the round trip could answer. The
reference calls only `ReputationWatchBar_Update()` there, and so do we.
4. THE SCROLL FRAME IS THE HOUSE FAUX KIT (ScrollTemplates.xml, decisions 0247/0250/0251):
a plain `<Frame inherits="FauxScrollFrameTemplate">` rather than the reference's
`<ScrollFrame>`, so the reference's `<OnVerticalScroll>` body (ref l.567-571) has
nothing to fire and is dropped — our offset comes straight off the Slider. The two
4. THE SCROLL FRAME IS A `<ScrollFrame>` ON THE CHAIN'S FAUX KIT, and this note used to
say otherwise. It read "a plain `<Frame inherits=\"FauxScrollFrameTemplate\">` rather
than the reference's `<ScrollFrame>`" — true while the kit was ours. 1846 put
`FauxScrollFrameTemplate` on the chain, where it IS a `<ScrollFrame>`, so every one of
our instances became one too: a `<Frame>` inheriting it has no `SetScrollChild` and the
loader throws on the template's own `<ScrollChild>`. The two
loose trough textures the reference hangs beside the bar (ref l.572-600) are replaced
by the kit's shared `BenillaScrollTroughTemplate`; see the site for why the fit is
exact rather than approximate.
@@ -743,10 +745,10 @@
<!-- The faux scroll frame (ref l.556-602): 296x354 at the page's TOPRIGHT(-66,-76),
which puts its left edge on the header column and its right edge 66 in from the
page. updateFunc is set in ReputationFrame_OnLoad. -->
<Frame name="ReputationListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="ReputationListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="354"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-66" y="-76"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<!-- The two newbie tips over the column headings (ref l.603-630): invisible frames
exactly covering their FontString. `enableMouse` added, same engine rule as the bar
@@ -309,10 +309,10 @@
<Button name="BenillaScriptLogRow13" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow12" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- The faux kit over the 13-row span; only its right-hand Slider renders. -->
<Frame name="BenillaScriptLogListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="BenillaScriptLogListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="208"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-94"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<!-- The detail pane: the REAL scroll kit, because a message carries its `stack
traceback:` tail and is genuinely variable-height. GossipFrame's greeting pane is
+2 -2
View File
@@ -712,10 +712,10 @@
container over the 12 rows; only its right-hand Slider scroll bar renders. Sized to the
12-row span (SKILLS_TO_DISPLAY × 18px pitch), TrainerFrame.xml's own
same-TOPLEFT-as-row1 convention. updateFunc set in SkillFrame_OnLoad. -->
<Frame name="SkillListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="SkillListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="216"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-79"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<!-- ═══ THE DETAIL PANE ═══ ref SkillFrame.xml l.513-747, transcribed as the ref
declares it (header: the FLAT-layout claim this file used to carry is retracted,
@@ -1168,10 +1168,10 @@
container over the 8 rows; only its right-hand Slider scroll bar renders. Sized to the
8-row span (TRADE_SKILLS_DISPLAYED × TRADE_SKILL_HEIGHT), TrainerFrame.xml's own
same-TOPLEFT-as-row1 convention. updateFunc set in BenillaTradeSkillFrame_OnLoad. -->
<Frame name="BenillaTradeSkillListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="BenillaTradeSkillListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-96"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<!-- The detail product icon (ref TradeSkillSkillIcon, l.539-591): a 37×37 button over a
64×64 UI-EmptySlot ring (TrainerFrame.xml's own BenillaTrainerSkillIcon shape) at the
@@ -797,10 +797,10 @@
container over the 11 rows; only its right-hand Slider scroll bar renders. Sized to the
11-row span so the bar sits in the window's right gutter. updateFunc is set in
BenillaTrainerFrame_OnLoad. -->
<Frame name="BenillaTrainerListScrollFrame" inherits="FauxScrollFrameTemplate">
<ScrollFrame name="BenillaTrainerListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="176"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
</Frame>
</ScrollFrame>
<!-- The detail spell icon (ref ClassTrainerSkillIcon): a 37×37 button over a 64×64 UI-EmptySlot
ring, at the detail area's top-left. Icon via SetNormalTexture; Hide()n when unselected.
@@ -1,442 +0,0 @@
<Ui>
<!--
The reference's SHARED widget kit, in our own terms. Interface\FrameXML\UIPanelTemplates.xml
is the SPECIFICATION — the template names, the child names, the art, every offset and every
handler's observable effect are read off the extracted 1.12.1 file, and the line numbers
cited below are that file's. Nothing here is required to be *spelled* the way it spells it,
and the two Lua handlers are not.
WHY THIS FILE EXISTS AT ALL. Every window benilla has built so far declared its own private
copy of these — `BenillaQuestPanelButtonTemplate`, `BenillaTrainerPanelButtonTemplate`,
`BenillaMacroPanelButtonTemplate` and a dozen siblings, each the same four state textures on
the same UI-Panel-Button art. That was fine while the only consumer was our own XML. It
stopped being fine the day `CreateFrame`'s fourth argument started working (decision 1203):
an addon writes `CreateFrame("Button", "MyBtn", UIParent, "UIPanelButtonTemplate")` and
nothing else, and 185 of the corpus's 218 addons' template call sites name templates we had
never declared. These are those templates, under the reference's own names — the 1190 rule
(a name the shipped 1.12 UI defines is FrameXML's, and belongs in `assets/ui`) applied to
the ranked list `addon_harness` now prints.
Every name here is in `reference/1.12-globals.tsv`; none is invented.
NO DEVIATIONS — the state textures below are the reference's, `<NormalTexture
inherits="UIPanelButtonUpTexture"/>` and all. That is worth a note only because it was not
true when this file first landed: the loader expanded `inherits=` on `<Layers>` regions and
on nested `<Frames>` children but not on a Button's state-texture or a Slider's thumb, so a
faithful transcription produced buttons with no art AND NO ERROR. The first version carried
the resolved art inline as a workaround; both gaps are now fixed in
`loader/widgets.rs` (`apply_button`/`apply_slider` call `expand_region`, the same call a
`<Layers>` region gets) and the workaround is gone. Reverting either fix makes
`a_panel_button_from_the_template_labels_and_paints` and
`a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep` fail.
NOT CARRIED, deliberately:
· `FauxScrollFrameTemplate` (ref l.213-225) — benilla already ships one under the reference's
own name in ScrollTemplates.xml, built as a plain `<Frame>` over `BenillaScrollBarTemplate`
(decisions 0247/0250/0251). Two declarations of one name would race; re-pointing ours at
the reference's `<ScrollFrame>` shape is its own job with its own risk, not this pass's.
· `TabButtonTemplate` (ref l.303-402) — already declared in UiPanels.xml, with its
fit/highlight deviations recorded there (decision 1004).
(`GameMenuButtonTemplate`, ref l.403-410, was on this list as "already in GameMenuFrame.xml"
until the colour picker needed it from above that file's LAST-in-the-toc seat. It is below
now, in the reference's own file for it, and GameMenuFrame.xml's private copy is gone.)
AND WHAT THIS SUPERSEDES BUT DOES NOT DELETE: `UIPanelButtonTemplate` below is the shared
original of `BenillaQuestPanelButtonTemplate`,
`BenillaCraftPanelButtonTemplate`, `BenillaQuestLogPanelButtonTemplate`,
`BenillaFriendsPanelButtonTemplate`, `BenillaTrainerPanelButtonTemplate`,
`BenillaMacroPanelButtonTemplate`, `BenillaTradeSkillPanelButtonTemplate` and
`BenillaDressUpPanelButtonTemplate` — eight copies of the same eight lines. Re-pointing those
windows at this template is a separate, mechanical pass; nothing here changes them.
-->
<Script><![CDATA[
-- The two scroll-frame handlers `UIPanelScrollFrameTemplate` below calls by name. Their
-- contract — the names, the arguments, and every branch's observable effect — is the
-- reference's (UIPanelTemplates.lua l.150-157 and l.254-305); the code is ours.
-- `ScrollFrame_OnLoad` and the whole `FauxScrollFrame_*` family already live in
-- ScrollTemplates.xml, which loads immediately before this file; these two are the
-- remainder of that kit.
-- One wheel notch pans by half a bar-height, up or down. The wheel is SOUNDLESS, unlike the
-- arrow buttons — the reference's own split, and the director's (trainer_tests.rs).
function ScrollFrameTemplate_OnMouseWheel(value)
local scrollBar = getglobal(this:GetName() .. "ScrollBar")
local step = scrollBar:GetHeight() / 2
if value > 0 then
step = -step
end
scrollBar:SetValue(scrollBar:GetValue() + step)
end
-- Reconcile a ScrollFrame's bar, arrows and thumb to a range the engine has just
-- recomputed. `scrollrange` is the engine's own second argument when this runs as the
-- handler; a caller who omits it gets the frame's current range instead.
--
-- All four children hang off the OWNER's name two `$parent` levels down
-- (`MyScrollScrollBarScrollUpButton`), so they are resolved ONCE here — the reference walks
-- back to getglobal at each of the ten points it touches one. `$parentThumbTexture` is what
-- publishes the last of them; that publication did not exist when this file was first
-- written (`apply_slider` applied a named thumb's layout and never set the global, the one
-- asymmetry with a Button's state textures), and the gap was fixed rather than routed
-- around.
function ScrollFrame_OnScrollRangeChanged(scrollrange)
local frame = this
local scrollbar = getglobal(frame:GetName() .. "ScrollBar")
local up = getglobal(scrollbar:GetName() .. "ScrollUpButton")
local down = getglobal(scrollbar:GetName() .. "ScrollDownButton")
local thumb = getglobal(scrollbar:GetName() .. "ThumbTexture")
scrollrange = scrollrange or frame:GetVerticalScrollRange()
local value = min(scrollbar:GetValue(), scrollrange)
scrollbar:SetMinMaxValues(0, scrollrange)
scrollbar:SetValue(value)
-- Nothing to scroll, and two ways of saying so — which one is the OWNER's call:
-- a hideable bar leaves altogether, a fixed one stays in place and greys out. On the
-- way back only the DOWN arrow wakes, because the offset is still zero and there is
-- nowhere up to go; the frame's own vertical-scroll handler below wakes the up arrow
-- the moment that stops being true.
local empty = floor(scrollrange) == 0
if not empty then
down:Show()
up:Show()
scrollbar:Show()
down:Enable()
elseif frame.scrollBarHideable then
scrollbar:Hide()
down:Hide()
up:Hide()
else
down:Disable()
up:Disable()
down:Show()
up:Show()
end
if empty then thumb:Hide() else thumb:Show() end
-- The frame's own border (ref l.285-304), if it declares one: all three regions are the
-- OWNER's to name, so all three are optional, and the pair is decided together because
-- a half-declared border is no border. Note this leg asks the FRAME for its range
-- rather than trusting the argument, and wants an exact zero where the bar above floors
-- first — two different questions, and the reference keeps them apart.
if frame.scrollBarHideable then
local bordered = frame:GetVerticalScrollRange() ~= 0
local top = getglobal(frame:GetName() .. "Top")
local bottom = getglobal(frame:GetName() .. "Bottom")
local middle = getglobal(frame:GetName() .. "Middle")
if top and bottom then
if bordered then top:Show() bottom:Show() else top:Hide() bottom:Hide() end
end
if middle then
if bordered then middle:Show() else middle:Hide() end
end
end
end
]]></Script>
<!-- The five shared panel-button faces (ref l.3-17). Declared under their own names because
they ARE 1.12 globals an addon's XML may inherit; the state textures below also carry the
resolved art inline, for the reason in the header. -->
<Texture name="UIPanelButtonUpTexture" file="Interface\Buttons\UI-Panel-Button-Up" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonDownTexture" file="Interface\Buttons\UI-Panel-Button-Down" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonDisabledTexture" file="Interface\Buttons\UI-Panel-Button-Disabled" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonDisabledDownTexture" file="Interface\Buttons\UI-Panel-Button-Disabled-Down" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="UIPanelButtonHighlightTexture" file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<!-- ref l.18-27. THE panel button: 19 call sites across 3 corpus addons, and the shared
original of nine private copies in our own assets/ui. Note it declares no `<Size>` — an
instance sets its own, and a bare CreateFrame of it comes out 0x0 until the addon does. -->
<Button name="UIPanelButtonTemplate" virtual="true">
<ButtonText name="$parentText"/>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture inherits="UIPanelButtonUpTexture"/>
<PushedTexture inherits="UIPanelButtonDownTexture"/>
<DisabledTexture inherits="UIPanelButtonDisabledTexture"/>
<HighlightTexture inherits="UIPanelButtonHighlightTexture"/>
</Button>
<!-- ref l.403-410, verbatim. The ESC menu's button: a 144x21 UIPanelButtonTemplate whose label
trio is overridden to GameFontHighlight/GameFontHighlight/GameFontDisable — the menu's
labels are WHITE, not the gold a normal panel button wears. It lived in GameMenuFrame.xml
with its art inlined until ColorPickerFrame.xml needed it too (the reference's own colour
picker builds its Okay/Cancel from this template, and loads far ahead of the game menu,
which is deliberately LAST in the toc). Moving it here is where the reference has it. -->
<Button name="GameMenuButtonTemplate" inherits="UIPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="144" y="21"/></Size>
<NormalFont inherits="GameFontHighlight"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
</Button>
<!-- ref l.28-81. The THREE-SLICE panel button: same art, cut 12/stretch/12 so it can be any
width without the end caps smearing, and its own OnMouseDown/OnMouseUp swap the three
slices to the Down sheet (there is no PushedTexture on this one — the layer regions ARE
the pressed state). The slices are `<Layers>` regions, so their `$parent` names publish
and the handlers' getglobal finds them. -->
<Button name="UIPanelButtonTemplate2" virtual="true">
<Size><AbsDimension x="40" y="22"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Buttons\UI-Panel-Button-Up">
<Size><AbsDimension x="12" y="22"/></Size>
<Anchors><Anchor point="LEFT"/></Anchors>
<TexCoords left="0" right="0.09375" top="0" bottom="0.6875"/>
</Texture>
<Texture name="$parentRight" file="Interface\Buttons\UI-Panel-Button-Up">
<Size><AbsDimension x="12" y="22"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.53125" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Buttons\UI-Panel-Button-Up">
<Size><AbsDimension x="12" y="22"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.09375" right="0.53125" top="0" bottom="0.6875"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>
getglobal(this:GetName() .. "Left"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
getglobal(this:GetName() .. "Middle"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
getglobal(this:GetName() .. "Right"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
</OnMouseDown>
<OnMouseUp>
getglobal(this:GetName() .. "Left"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
getglobal(this:GetName() .. "Middle"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
getglobal(this:GetName() .. "Right"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
</OnMouseUp>
</Scripts>
<ButtonText name="$parentText"/>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<HighlightTexture inherits="UIPanelButtonHighlightTexture"/>
</Button>
<!-- ref l.82-90. The permanently-greyed variant: the Disabled sheet in the Normal slot, white
label. Used for a button that reads as unavailable but still takes clicks. -->
<Button name="UIPanelButtonGrayTemplate" virtual="true">
<NormalFont inherits="GameFontHighlight"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture inherits="UIPanelButtonDisabledTexture"/>
<PushedTexture inherits="UIPanelButtonDisabledDownTexture"/>
<DisabledTexture inherits="UIPanelButtonDisabledTexture"/>
<HighlightTexture inherits="UIPanelButtonHighlightTexture"/>
</Button>
<!-- ref l.91-103. The round X in a window's top-right corner — 8 call sites across 4 corpus
addons, the most-wanted template after the checkbox and the panel button. Note the name has
no `Template` suffix; that is the reference's own spelling, and the reference-globals table
agrees. Its OnClick is `HideUIPanel(this:GetParent())`, the verb UiPanels.xml defines: a
Lua global resolved at click time, so the load order between the two files is free. -->
<Button name="UIPanelCloseButton" virtual="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Scripts>
<OnClick>HideUIPanel(this:GetParent())</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
</Button>
<!-- ref l.104-124. The scroll-arrow kit. `UIPanelScrollBarButton` is the shared crop — the
1.12 arrow art is 32x32 with the glyph in the centre 16, which is what the 0.25..0.75
TexCoords take (ScrollTemplates.xml's own bar applies the identical crop). -->
<Texture name="UIPanelScrollBarButton" virtual="true">
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</Texture>
<Button name="UIPanelScrollUpButtonTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Up"/>
<PushedTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Down"/>
<DisabledTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Disabled"/>
<HighlightTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Highlight" alphaMode="ADD"/>
</Button>
<Button name="UIPanelScrollDownButtonTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Up"/>
<PushedTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Down"/>
<DisabledTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Disabled"/>
<HighlightTexture inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Highlight" alphaMode="ADD"/>
</Button>
<!-- ref l.125-165. The scroll bar itself: a Slider carrying its two step buttons as nested
frames, so `$parentScrollUpButton`/`$parentScrollDownButton` publish under the OWNER's
name two levels down (`MyScrollScrollBarScrollUpButton`) — which is exactly what
`ScrollFrame_OnLoad` and `FauxScrollFrame_Update` getglobal for. The value model here is
the REFERENCE's: `OnValueChanged` pushes the value straight into the parent ScrollFrame's
`SetVerticalScroll`, i.e. pixels of pan. (benilla's own `BenillaScrollBarTemplate` in
ScrollTemplates.xml is the faux twin of this — same art and same child names, but its
OnValueChanged drives a row offset instead. Both are needed: ours serves our windows, this
serves an addon that names the reference template.) -->
<Slider name="UIPanelScrollBarTemplate" virtual="true">
<Size><AbsDimension x="16" y="0"/></Size>
<Frames>
<!-- Both arrows step by half a bar-height and differ only in sign; unlike the wheel,
both click. -->
<Button name="$parentScrollUpButton" inherits="UIPanelScrollUpButtonTemplate">
<Anchors><Anchor point="BOTTOM" relativePoint="TOP"/></Anchors>
<Scripts>
<OnClick>
local bar = this:GetParent()
bar:SetValue(bar:GetValue() - bar:GetHeight() / 2)
PlaySound("UChatScrollButton")
</OnClick>
</Scripts>
</Button>
<Button name="$parentScrollDownButton" inherits="UIPanelScrollDownButtonTemplate">
<Anchors><Anchor point="TOP" relativePoint="BOTTOM"/></Anchors>
<Scripts>
<OnClick>
local bar = this:GetParent()
bar:SetValue(bar:GetValue() + bar:GetHeight() / 2)
PlaySound("UChatScrollButton")
</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnValueChanged>this:GetParent():SetVerticalScroll(arg1)</OnValueChanged>
</Scripts>
<ThumbTexture name="$parentThumbTexture" inherits="UIPanelScrollBarButton" file="Interface\Buttons\UI-ScrollBar-Knob">
<Size><AbsDimension x="16" y="16"/></Size>
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</ThumbTexture>
</Slider>
<!-- ref l.166-212. The REAL scroll frame: an engine `<ScrollFrame>` that pans and clips a
scroll child, with the bar above pinned down its right edge (inset 16 top and bottom so
the arrow buttons sit inside the track span). 6 call sites across 2 corpus addons.
`ScrollFrame_OnLoad` — the reference's own name for it — is declared in ScrollTemplates.xml;
the other two handlers are this file's script block. An instance supplies its own
`<ScrollChild>` — without one the frame has no scroll range at all, which the loader now
says out loud. -->
<ScrollFrame name="UIPanelScrollFrameTemplate" virtual="true">
<Frames>
<Slider name="$parentScrollBar" inherits="UIPanelScrollBarTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="6" y="-16"/></Offset></Anchor>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="6" y="16"/></Offset></Anchor>
</Anchors>
</Slider>
</Frames>
<Scripts>
<OnLoad>ScrollFrame_OnLoad()</OnLoad>
<OnScrollRangeChanged>ScrollFrame_OnScrollRangeChanged(arg2)</OnScrollRangeChanged>
<!-- Each arrow is live only while there is somewhere its way to go: up dies at offset
zero, down dies once the bar has been seated at the top of its range. The bar's
own value is re-read for the down test rather than arg1 being reused, because
SetValue above may have clamped it. (The reference's comment on this pair reads
"The code to disable the ScrollDownButton is pretty hacked, but it works" — it
asks the same question as `(value - max) == 0`.) -->
<OnVerticalScroll>
local scrollbar = getglobal(this:GetName() .. "ScrollBar")
scrollbar:SetValue(arg1)
local up = getglobal(scrollbar:GetName() .. "ScrollUpButton")
local down = getglobal(scrollbar:GetName() .. "ScrollDownButton")
local _, maxScroll = scrollbar:GetMinMaxValues()
if arg1 == 0 then up:Disable() else up:Enable() end
if scrollbar:GetValue() == maxScroll then down:Disable() else down:Enable() end
</OnVerticalScroll>
<OnMouseWheel>ScrollFrameTemplate_OnMouseWheel(arg1)</OnMouseWheel>
</Scripts>
</ScrollFrame>
<!-- ref l.226-279. The standard text box: a three-slice Common-Input-Border under an EditBox,
with the reference's escape/focus trio. 5 call sites across 3 corpus addons. The
direct-child `<FontString>` is the box's TEXT region (the engine's LoadXML slot, not a
layer) — `loader/regions.rs`'s `apply_special_fontstrings`. -->
<EditBox name="InputBoxTemplate" enableMouse="true" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<!-- ref l.280-302. THE most-wanted template in the whole corpus: 69 `CreateFrame` call sites.
A 32x32 checkbox with its label hung off the right edge, and no scripts of its own — the
checked flag toggles in the engine BEFORE `OnClick` fires (script/button.rs), so an addon
that sets only an OnClick reads the new state there, which is what every one of those 69
sites assumes. -->
<CheckButton name="UICheckButtonTemplate" virtual="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
<HighlightTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/>
<DisabledCheckedTexture file="Interface\Buttons\UI-CheckBox-Check-Disabled"/>
</CheckButton>
<!-- ref l.411-437. The radio button — the same CheckButton machinery on the four-frame
UI-RadioButton strip (normal / checked / highlight are three crops of one 64x16 sheet).
Carried because it is the checkbox's sibling in the same file and costs nothing; no corpus
addon names it today. -->
<CheckButton name="UIRadioButtonTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="5" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<NormalTexture file="Interface\Buttons\UI-RadioButton">
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</NormalTexture>
<HighlightTexture file="Interface\Buttons\UI-RadioButton" alphaMode="ADD">
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</HighlightTexture>
<CheckedTexture file="Interface\Buttons\UI-RadioButton">
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</CheckedTexture>
</CheckButton>
</Ui>
+38 -14
View File
@@ -403,20 +403,44 @@ Interface\FrameXML\ZoneText.xml
# API (Update/GetOffset/SetOffset/OnVerticalScroll + ScrollFrame_OnLoad) —
# before its first customer (the trainer list) below.
ScrollTemplates.xml
# The reference's SHARED widget kit (decision 1203's queue): UIPanelButtonTemplate,
# UICheckButtonTemplate, UIPanelCloseButton, InputBoxTemplate, the UIPanelScroll* family and
# their siblings, under the reference's own names — the templates a third-party addon names in
# `CreateFrame(kind, name, parent, "Template")`, which nothing in benilla's own XML consumes
# yet (each of our windows still carries its private copy; see the file's header). After
# ScrollTemplates.xml because UIPanelScrollFrameTemplate's OnLoad is that file's
# ScrollFrame_OnLoad, and this file supplies the two remaining handlers of that same kit
# (ScrollFrameTemplate_OnMouseWheel, ScrollFrame_OnScrollRangeChanged).
UIPanelTemplates.xml
# The reference's shared OPTIONS widgets: OptionsCheckButtonTemplate /
# UIOptionsCheckButtonTemplate / OptionsSliderTemplate / OptionsButtonTemplate /
# OptionFrameBoxTemplate. Straight after UIPanelTemplates.xml — two of them inherit its
# UICheckButtonTemplate and UIPanelButtonTemplate. Not benilla's own options window
# (OptionsFrame.xml, far below): these are the shared names addons build panels out of.
# ══ THE PANEL TEMPLATE KIT — the reference's own, off the player's chain (1751, window 31) ══
# TWO entries, because the reference splits this kit the way it splits every other: the `.lua`
# carries the verbs and the `.xml` the templates. Listing the `.xml` alone brings none of the verbs
# (`every_chain_xml_brings_its_own_lua` is a gate because that already bit twice).
#
# ONE swap, THREE of our files, which is why it had to move as a unit and why 1837 measured it
# before anyone executed it:
# * `UIPanelTemplates.xml` — DELETED. Our 18 templates were a strict SUBSET of the reference's,
# zero benilla-only additions, so the template half is a pure gain.
# * the scroll half of `ScrollTemplates.xml` — `FauxScrollFrameTemplate`, `FauxScrollFrame_*`,
# `ScrollFrame_*` — now the chain's. What stays there is `BenillaScrollBarTemplate` and
# `BenillaScrollTroughTemplate`, which are ours and which six windows still inherit.
# * the `PanelTemplates_*` family out of `UiPanels.xml`.
#
# It could not be split: `ScrollTemplates.xml` sits ABOVE this entry, so the chain's
# `FauxScrollFrameTemplate` lands after ours and wins (registration is last-in). There is no
# ordering that takes the templates without also taking Faux.
#
# GAINED: `TabButtonTemplate`, and the whole `ScrollingEdit_*` family, which we had NOWHERE — that
# is HelpFrame's named blocker, answered as a side effect. And it unblocks MacroFrame (1835).
Interface\FrameXML\UIPanelTemplates.lua
Interface\FrameXML\UIPanelTemplates.xml
# ══ THE OPTIONS WIDGET KIT — the reference's own, off the player's chain (1751, window 29) ══
# The four templates addons name for an options panel: `OptionFrameBoxTemplate`,
# `OptionsButtonTemplate`, `OptionsCheckButtonTemplate`, `OptionsSliderTemplate`. Self-contained —
# no `.lua`, no `<Include>` — and a descendant of UIPanelTemplates (`OptionsCheckButtonTemplate`
# inherits `UICheckButtonTemplate`, `OptionsButtonTemplate` inherits `UIPanelButtonTemplate`), so
# it stays seated after that file.
#
# `framexml-transcription-diff.py` reports no handler our transcription was missing, and the swap
# GIVES BACK the reference's own inline hover: ours had factored it into a shared
# `BenillaOptionsWidget_ShowTooltip`, where the reference open-codes it once per widget.
Interface\FrameXML\OptionsFrameTemplates.xml
# …and ours, shrunk to the ONE template the chain's file does not carry:
# `UIOptionsCheckButtonTemplate`, whose reference home is `UIOptionsFrame.xml` — a file we do not
# build, because benilla's options window is `OptionsFrame.xml`, a different design. The corpus
# names it at 8 `CreateFrame` sites, so it cannot simply go. Same basename as the entry above by
# design: bare = ours, path = the reference's. Retires when UIOptionsFrame.xml migrates.
OptionsFrameTemplates.xml
# ══ THE COLOUR DIALOG — the reference's own, off the player's chain (1751, window 11) ══
# `ColorPickerFrame` (a `<ColorSelect>`), `OpacitySliderFrame`, `ColorPickerOkayButton`,
@@ -1603,7 +1603,7 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
] {
let (t, b) = s
.eval::<(f64, f64)>(&format!(
"local _, _, top, bottom = {strip}:GetTexCoord() return top, bottom"
"local _, top, _, bottom = {strip}:GetTexCoord() return top, bottom"
))
.unwrap();
assert!(
@@ -46,7 +46,8 @@ fn chat_with_menu() -> UiScript {
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"ChatFrame.xml",
@@ -24,7 +24,8 @@ fn chat_ui() -> UiScript {
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"ChatFrame.xml",
@@ -24,7 +24,8 @@ fn picker() -> UiScript {
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
] {
load_xml(&s, file);
@@ -36,6 +36,7 @@ fn toast_lines(s: &mut UiScript) -> Vec<(String, [f32; 4], f32)> {
#[test]
fn info_and_error_messages_stack_hold_and_expire() {
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");
@@ -105,6 +106,7 @@ fn info_and_error_messages_stack_hold_and_expire() {
/// defect as the party frame's in decision 0597, one stratum up.
#[test]
fn an_error_toast_draws_over_an_open_panel_window() {
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");
@@ -118,7 +120,8 @@ fn an_error_toast_draws_over_an_open_panel_window() {
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
// A left-slot panel open, and the toast raised after it — the order that must not decide.
@@ -28,7 +28,8 @@ fn setup() -> UiScript {
// UIPanelCloseButton / UIPanelScrollFrameTemplate rather than a private copy (decision 1257),
// so this file's slice needs the shared kit — and the strict `load_xml` above is what would
// otherwise let those frames load art-less and silent.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "FriendsFrame.xml");
// The social window's fourth tab lives in its own file, and it is part of THIS window's
// manifest slice now: `BENILLA_FRIENDS_SUBFRAMES` names "RaidFrame", and both
@@ -71,6 +72,7 @@ fn push(s: &mut UiScript, state: SocialState, event: &str) {
/// every character starts a session in.
#[test]
fn the_window_opens_on_friends_with_the_guild_tab_disabled() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
assert!(s.eval::<bool>("return FriendsFrame:IsVisible()").unwrap());
@@ -129,6 +131,7 @@ fn the_window_opens_on_friends_with_the_guild_tab_disabled() {
/// `GetFriendInfo`'s six returns ever come back in the wrong order.
#[test]
fn friend_rows_render_the_online_and_offline_templates() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
push(
@@ -178,6 +181,7 @@ fn friend_rows_render_the_online_and_offline_templates() {
/// whispered or invited, and an empty list disables all three.
#[test]
fn the_friend_buttons_follow_the_selection() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
@@ -217,6 +221,7 @@ fn the_friend_buttons_follow_the_selection() {
/// from this index), and Group Invite addresses the NAME.
#[test]
fn the_friend_buttons_queue_their_verbs() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
push(
@@ -253,6 +258,7 @@ fn the_friend_buttons_queue_their_verbs() {
/// their names. `ShowIgnorePanel()` (what `/ignore` with no name runs) lands on the same list.
#[test]
fn the_ignore_list_is_the_other_half_of_tab_one() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
push(
@@ -313,6 +319,7 @@ fn the_ignore_list_is_the_other_half_of_tab_one() {
/// makes the column order worth pinning.
#[test]
fn who_rows_fill_their_columns() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowWhoPanel()").unwrap();
push(
@@ -365,6 +372,7 @@ fn who_rows_fill_their_columns() {
/// The variable column really is variable: picking Guild re-paints it and sorts by that key.
#[test]
fn the_who_dropdown_switches_the_variable_column() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowWhoPanel()").unwrap();
push(
@@ -405,6 +413,7 @@ fn the_who_dropdown_switches_the_variable_column() {
/// the selection — row 3 of the last query is not row 3 of this one.
#[test]
fn the_who_buttons_need_a_selected_row() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowWhoPanel()").unwrap();
let rows = vec![
@@ -466,6 +475,7 @@ fn the_who_buttons_need_a_selected_row() {
/// closed would print nothing at all.
#[test]
fn showing_the_who_frame_claims_the_next_answer() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowWhoPanel()").unwrap();
assert!(
@@ -484,6 +494,7 @@ fn showing_the_who_frame_claims_the_next_answer() {
/// The `/who` edit box sends its filter verbatim — the string the app parses into wire fields.
#[test]
fn the_who_edit_box_sends_its_filter() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowWhoPanel()").unwrap();
let _ = s.take_social_requests();
@@ -500,6 +511,7 @@ fn the_who_edit_box_sends_its_filter() {
/// bare `/who` opens the panel and fills the edit box with the default filter.
#[test]
fn the_slash_bodies_match_the_reference() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
// The WhoFrame's OnLoad routes results to chat until it is shown; drain that so each
// assertion below sees only what its own call queued.
@@ -550,6 +562,7 @@ fn the_slash_bodies_match_the_reference() {
/// customer of the popup engine's `hasEditBox` capability. Accepting sends what was typed.
#[test]
fn add_friend_without_a_target_opens_the_name_dialog() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
s.run("FriendsFrameAddFriendButton:Click()").unwrap();
@@ -586,6 +599,7 @@ fn add_friend_without_a_target_opens_the_name_dialog() {
/// typed last time.
#[test]
fn the_name_dialog_reopens_empty() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
s.run("StaticPopup_Show(\"ADD_FRIEND\")").unwrap();
@@ -604,6 +618,7 @@ fn the_name_dialog_reopens_empty() {
/// survive UnitPopup's gating, and each one acts on the name.
#[test]
fn right_clicking_a_who_row_opens_the_friend_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowWhoPanel()").unwrap();
push(
@@ -656,6 +671,7 @@ fn right_clicking_a_who_row_opens_the_friend_menu() {
/// ref's own `connected` gate on the whole menu.
#[test]
fn right_clicking_an_offline_friend_opens_nothing() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
push(
@@ -679,6 +695,7 @@ fn right_clicking_an_offline_friend_opens_nothing() {
/// read `FriendsList_Update` does right after `SetSelectedFriend`.
#[test]
fn selecting_a_row_reads_back_in_the_same_tick() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleFriendsFrame(1)").unwrap();
push(
@@ -722,6 +739,7 @@ fn selecting_a_row_reads_back_in_the_same_tick() {
/// exemption. Skips without the extracted reference.
#[test]
fn the_window_geometry_matches_the_reference_framexml() {
let _data = benilla_formats::wow_data_or_skip!();
let Some(reference) = super::framexml_diff::reference("FriendsFrame.xml") else {
eprintln!("skipping: no extracted FrameXML");
return;
@@ -43,7 +43,8 @@ fn harness_with(extra: &[&str]) -> UiScript {
"UIParent.xml",
"MoneyFrame.xml",
"UiPanels.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
]
.into_iter()
.chain(extra.iter().copied())
@@ -104,6 +105,7 @@ const LADDER: [&str; 7] = [
/// era shape is ever "tidied".
#[test]
fn the_menu_has_the_era_frame_and_button_ladder() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
s.resolve();
@@ -148,6 +150,7 @@ fn the_menu_has_the_era_frame_and_button_ladder() {
/// panel — the char-select AddOns screen is the only addon UI.)
#[test]
fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
@@ -335,6 +338,7 @@ fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() {
/// the completed logout ends the process), which is why they look identical from here.
#[test]
fn the_live_buttons_queue_their_intents_and_play_their_kits() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
// Return to Game — just closes, no intent.
@@ -375,6 +379,7 @@ fn the_live_buttons_queue_their_intents_and_play_their_kits() {
/// mean CAMP fell out of that which-list.
#[test]
fn player_camping_opens_a_counting_dialog_whose_early_close_cancels() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
assert_eq!(
@@ -424,6 +429,7 @@ fn player_camping_opens_a_counting_dialog_whose_early_close_cancels() {
/// cancelled by its own dialog at t=0 — the character never leaves.)
#[test]
fn a_countdown_that_expires_does_not_cancel_the_logout() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
s.run("StaticPopup_OnUpdate(StaticPopup1, 25)").unwrap();
@@ -444,6 +450,7 @@ fn a_countdown_that_expires_does_not_cancel_the_logout() {
/// the server: "Exit now". And `LOGOUT_CANCEL` (the server's cancel ack) takes either one down.
#[test]
fn player_quiting_offers_exit_now_and_logout_cancel_closes_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_QUITING", vec![]);
s.run("StaticPopup_OnUpdate(StaticPopup1, 0.5)").unwrap();
@@ -487,6 +494,7 @@ fn player_quiting_offers_exit_now_and_logout_cancel_closes_it() {
/// the next test.
#[test]
fn logout_and_exit_read_disabled_while_a_countdown_runs() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
s.run("ToggleGameMenu(1)").unwrap();
@@ -515,6 +523,7 @@ fn logout_and_exit_read_disabled_while_a_countdown_runs() {
/// a logout you didn't mean is therefore the whole gesture, and it must not also open the menu.
#[test]
fn escape_during_a_countdown_cancels_it_and_does_not_open_the_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
s.run("ToggleGameMenu()").unwrap();
@@ -545,6 +554,7 @@ fn escape_during_a_countdown_cancels_it_and_does_not_open_the_menu() {
/// afterwards.
#[test]
fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness_with(&[
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad
@@ -593,6 +603,7 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
/// would have opened the menu goes to the map instead (one eater per press).
#[test]
fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness_with(&[
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
@@ -770,6 +781,7 @@ fn backpack() -> ContainerState {
/// real load order; the bare-menu harness elsewhere exercises the `or 1` guard implicitly.
#[test]
fn the_menu_rides_the_shared_era_window_scale() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness_with(&[
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
@@ -74,7 +74,8 @@ fn setup() -> UiScript {
// The loot window's slots inherit it — the same dependency the inspect window needed (1832).
load_xml(&s, r"Interface\FrameXML\ItemButtonTemplate.xml");
// `UIPanelCloseButton`, which the loot window's four close buttons inherit.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers
@@ -211,7 +211,8 @@ fn setup() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "FriendsFrame.xml");
// The social window's fourth tab lives in its own file, and it is part of THIS window's
// manifest slice now: `BENILLA_FRIENDS_SUBFRAMES` names "RaidFrame", and both
@@ -263,6 +264,7 @@ fn colour(s: &UiScript, region: &str) -> (f64, f64, f64) {
/// which the friend list never has to, because that one arrives unasked.
#[test]
fn the_guild_tab_opens_the_roster_and_asks_for_it() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
assert_eq!(
s.eval::<i64>("return FriendsFrameTab3.isDisabled or 0")
@@ -323,6 +325,7 @@ fn the_guild_tab_opens_the_roster_and_asks_for_it() {
/// too, unlike the friends list where an offline friend has no level at all — and only greys.
#[test]
fn an_offline_member_keeps_its_columns_and_only_greys() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
assert_eq!(text(&s, "GuildFrameButton3Name"), "Kaplan");
@@ -351,6 +354,7 @@ fn an_offline_member_keeps_its_columns_and_only_greys() {
/// take you back to.
#[test]
fn the_page_button_flips_to_the_guild_status_view() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
assert_eq!(
@@ -395,6 +399,7 @@ fn the_page_button_flips_to_the_guild_status_view() {
/// only reachable by seeding four different members.
#[test]
fn the_last_online_formatter_takes_the_coarsest_unit() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
let last = |years, months, days, hours| {
@@ -422,6 +427,7 @@ fn the_last_online_formatter_takes_the_coarsest_unit() {
/// and drops the selection, which is the only way back to "nothing selected".
#[test]
fn a_row_click_toggles_the_detail_card() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
assert!(!visible(&s, "GuildMemberDetailFrame"));
@@ -468,6 +474,7 @@ fn a_row_click_toggles_the_detail_card() {
/// with BOTH arrows dead they leave the card rather than sitting greyed.
#[test]
fn the_detail_buttons_follow_the_rank_comparisons() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
@@ -533,6 +540,7 @@ fn the_detail_buttons_follow_the_rank_comparisons() {
/// may not even see officer notes gets 60px less window rather than an empty pane.
#[test]
fn the_officer_note_pane_resizes_the_card() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameButton2:Click()").unwrap();
@@ -565,6 +573,7 @@ fn the_officer_note_pane_resizes_the_card() {
/// an empty note is a blank rectangle nobody would click.
#[test]
fn an_editable_empty_note_invites_the_click() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameButton1:Click()").unwrap(); // Tigole's public note is ""
@@ -589,6 +598,7 @@ fn an_editable_empty_note_invites_the_click() {
/// `hasWideEditBox` (UiPanels.xml) and the only test that proves the 420 widen and the box swap.
#[test]
fn the_note_pane_opens_the_wide_dialog_and_sends_it() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameButton2:Click()").unwrap();
@@ -628,6 +638,7 @@ fn the_note_pane_opens_the_wide_dialog_and_sends_it() {
/// it, and the GUILD_MOTD event repaints it without a roster round trip.
#[test]
fn the_motd_is_cached_click_to_edit_and_right_gated() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open(&s);
assert_eq!(text(&s, "GuildFrameNotesText"), "Raid Tuesday at eight.");
@@ -672,6 +683,7 @@ fn the_motd_is_cached_click_to_edit_and_right_gated() {
/// pane-level buttons, both driven by the repaint rather than by a click.
#[test]
fn the_pane_buttons_follow_the_rights() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
assert!(enabled(&s, "GuildFrameControlButton"));
@@ -706,6 +718,7 @@ fn the_pane_buttons_follow_the_rights() {
/// the click has to carry the index it was declared with.
#[test]
fn the_rank_editor_loads_its_flags_and_arms_accept_on_an_edit() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameControlButton:Click()").unwrap();
@@ -766,6 +779,7 @@ fn the_rank_editor_loads_its_flags_and_arms_accept_on_an_edit() {
/// flags, and re-disarms Accept — the edit you were half-way through does not follow you.
#[test]
fn switching_rank_reloads_the_buffer_and_disarms_accept() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameControlButton:Click()").unwrap();
@@ -804,6 +818,7 @@ fn switching_rank_reloads_the_buffer_and_disarms_accept() {
/// is EMPTY — which is why the repaint counts `playersInBotRank` at all.
#[test]
fn the_rank_buttons_follow_the_count_and_the_bottom_rank() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameControlButton:Click()").unwrap();
@@ -860,6 +875,7 @@ fn the_rank_buttons_follow_the_count_and_the_bottom_rank() {
/// sort is a repaint, never a re-request.
#[test]
fn the_column_headers_sort_by_their_own_keys() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
for (header, key) in [
@@ -896,6 +912,7 @@ fn the_column_headers_sort_by_their_own_keys() {
/// so index 7 will not be the member index 7 was.
#[test]
fn the_show_offline_checkbox_is_real_and_drops_the_selection() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
assert!(
@@ -927,6 +944,7 @@ fn the_show_offline_checkbox_is_real_and_drops_the_selection() {
/// stored text rather than the click-here invitation.
#[test]
fn the_guild_information_board_is_right_gated() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameGuildInformationButton:Click()").unwrap();
@@ -970,6 +988,7 @@ fn the_guild_information_board_is_right_gated() {
/// and closing the social window takes all three with it, since none of them is its child.
#[test]
fn the_three_satellites_are_exclusive_and_close_with_the_window() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameButton2:Click()").unwrap();
@@ -999,6 +1018,7 @@ fn the_three_satellites_are_exclusive_and_close_with_the_window() {
/// would put a server trip behind every click on a header.
#[test]
fn the_roster_event_only_re_requests_when_told_the_roster_is_stale() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open(&s);
@@ -1027,6 +1047,7 @@ fn the_roster_event_only_re_requests_when_told_the_roster_is_stale() {
/// impossible to notice until it happens.
#[test]
fn losing_the_guild_falls_back_off_the_guild_tab() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open(&s);
assert!(visible(&s, "GuildFrame"));
@@ -1069,6 +1090,7 @@ fn losing_the_guild_falls_back_off_the_guild_tab() {
/// are gated on this pane being visible, which is what keeps them off a friends-list or /who row.
#[test]
fn right_clicking_a_roster_row_offers_the_guild_rows() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameButton2:Click(\"RightButton\")").unwrap();
@@ -1129,6 +1151,7 @@ fn right_clicking_a_roster_row_offers_the_guild_rows() {
/// the game.
#[test]
fn the_guild_rows_stay_off_a_who_row_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
s.run("ShowWhoPanel()").unwrap();
// NOT our own name: the WHISPER row hides on yourself, and with INVITE gated the same way the
@@ -1155,6 +1178,7 @@ fn the_guild_rows_stay_off_a_who_row_menu() {
/// window to have ever been opened, which is why it rides its own hidden driver frame.
#[test]
fn a_guild_invite_raises_its_dialog_without_the_window() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
assert!(!visible(&s, "FriendsFrame"), "the window is shut");
@@ -1209,6 +1233,7 @@ fn a_guild_invite_raises_its_dialog_without_the_window() {
/// text is rewritten on show.
#[test]
fn removing_a_member_names_them_in_the_confirm() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
s.run("GuildFrameButton3:Click()").unwrap();
@@ -1239,6 +1264,7 @@ fn removing_a_member_names_them_in_the_confirm() {
/// So this one deliberately does NOT install the fixture: it asks the real `script::guild`.
#[test]
fn the_rank_flags_binding_answers_thirteen_values_even_when_all_are_nil() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
assert_eq!(
s.eval::<i64>("return select(\"#\", GuildControlGetRankFlags())")
@@ -1271,6 +1297,7 @@ fn the_rank_flags_binding_answers_thirteen_values_even_when_all_are_nil() {
/// what makes it a falsifier rather than a restatement.
#[test]
fn each_permission_predicate_reads_the_bit_its_checkbox_owns() {
let _data = benilla_formats::wow_data_or_skip!();
// Index 7, "Invite Member" — bit 0x10. A shift-based table would put 0x10 at index 5,
// "Promote", so this single word separates the two layouts in both directions.
let mut s = UiScript::new().unwrap();
@@ -1342,6 +1369,7 @@ fn each_permission_predicate_reads_the_bit_its_checkbox_owns() {
/// evidence.
#[test]
fn the_roster_binding_answers_ten_values_and_the_tenth_is_status() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
assert_eq!(
s.eval::<i64>("return select(\"#\", GetGuildRosterInfo(1))")
@@ -1364,6 +1392,7 @@ fn the_roster_binding_answers_ten_values_and_the_tenth_is_status() {
/// right, so an assertion on the look is not enough on its own.
#[test]
fn driving_the_guild_windows_raises_no_script_errors() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open(&s);
s.run("GuildFrameButton1:Click()").unwrap();
@@ -1390,6 +1419,7 @@ fn driving_the_guild_windows_raises_no_script_errors() {
/// invisible to reading, so this asserts the WIRING rather than the comment.
#[test]
fn every_list_in_the_window_takes_the_mouse_wheel() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
for frame in [
@@ -21,7 +21,8 @@ fn setup() -> UiScript {
for file in [
"Fonts.xml",
"BasicControls.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"ScrollTemplates.xml",
"GameTooltip.xml",
// Before UiPanels.xml: the shared StaticPopup carries a `SmallMoneyFrameTemplate` coin
@@ -77,6 +78,7 @@ fn open_ticket_args(
/// "Unknown" instead of a heading the player never chose.
#[test]
fn one_click_from_home_files_an_uncategorised_ticket() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleHelpFrame()").unwrap();
let _ = s.take_gm_ticket_intents(); // the OnShow GetGMStatus
@@ -114,6 +116,7 @@ fn one_click_from_home_files_an_uncategorised_ticket() {
/// existence on Home *is* the change.
#[test]
fn auto_unstuck_is_a_button_on_home() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleHelpFrame()").unwrap();
s.run("BenillaHelpFrameHomeUnstick:Click()").unwrap();
@@ -133,6 +136,7 @@ fn auto_unstuck_is_a_button_on_home() {
/// fit that never ran still leaves every other test green.
#[test]
fn the_two_home_buttons_sit_side_by_side_without_overlapping() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleHelpFrame()").unwrap();
s.resolve();
@@ -173,6 +177,7 @@ fn the_two_home_buttons_sit_side_by_side_without_overlapping() {
/// looks fine until you try to file a second ticket.
#[test]
fn an_open_ticket_turns_the_form_into_an_editor_and_a_zero_turns_it_back() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")")
.unwrap();
@@ -224,6 +229,7 @@ fn an_open_ticket_turns_the_form_into_an_editor_and_a_zero_turns_it_back() {
/// moment the player fixes a typo. Nothing on screen would ever show that.
#[test]
fn a_create_files_under_zero_and_an_edit_gives_the_server_its_category_back() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")")
.unwrap();
@@ -263,6 +269,7 @@ fn a_create_files_under_zero_and_an_edit_gives_the_server_its_category_back() {
/// The `1` leg must put it back — a one-way gate would lock the player out for the session.
#[test]
fn a_downed_queue_refuses_the_editor_and_says_so_and_comes_back_up() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.fire_event("UPDATE_GM_STATUS", vec![ScriptValue::Int(0)]);
s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")")
@@ -291,6 +298,7 @@ fn a_downed_queue_refuses_the_editor_and_says_so_and_comes_back_up() {
/// on screen that says a ticket exists at all once the window is closed.
#[test]
fn the_ticket_toast_follows_the_ticket() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.fire_event(
"UPDATE_TICKET",
@@ -314,6 +322,7 @@ fn the_ticket_toast_follows_the_ticket() {
/// answers instead of diffing them, so it is worth a test on this side too.
#[test]
fn the_toast_repolls_the_server_only_after_the_full_interval() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.fire_event(
"UPDATE_TICKET",
@@ -340,6 +349,7 @@ fn the_toast_repolls_the_server_only_after_the_full_interval() {
/// nothing would look identical to one that worked, right up until the ticket reappeared.
#[test]
fn abandoning_a_ticket_confirms_first_and_only_yes_sends_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("StaticPopup_Show(\"HELP_TICKET_ABANDON_CONFIRM\")")
.unwrap();
@@ -357,6 +367,7 @@ fn abandoning_a_ticket_confirms_first_and_only_yes_sends_it() {
/// life of the session.
#[test]
fn toggling_the_window_opens_it_and_asks_for_the_queue_status() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleHelpFrame()").unwrap();
assert!(s.eval::<bool>("return HelpFrame:IsVisible()").unwrap());
@@ -388,6 +399,7 @@ fn toggling_the_window_opens_it_and_asks_for_the_queue_status() {
/// a different key.
#[test]
fn the_home_page_advertises_no_dead_retail_links() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"Home\")")
.unwrap();
@@ -438,6 +450,7 @@ fn the_home_page_advertises_no_dead_retail_links() {
/// compares stops guarding and never says so.
#[test]
fn the_windows_geometry_matches_the_reference_file() {
let _data = benilla_formats::wow_data_or_skip!();
let Some(reference) = super::framexml_diff::reference("HelpFrame.xml") else {
return; // no install — this test is a no-op rather than a failure
};
@@ -141,10 +141,16 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript {
s
}
/// `GameTimeTexture`'s current 4-edge texcoord window.
/// `GameTimeTexture`'s current texcoord window, as the `(left, right, top, bottom)` rect this
/// file reasons in.
///
/// `GetTexCoord` answers EIGHT values since decision 1840 — `ULx, ULy, LLx, LLy, URx, URy, LRx,
/// LRy` — so the rect is positions 1, 5, 2, 4. Folded here rather than at each call site because
/// the window really is axis-aligned and every assertion below is about its edges.
fn tod_window(s: &UiScript) -> (f64, f64, f64, f64) {
s.eval::<(f64, f64, f64, f64)>("return GameTimeTexture:GetTexCoord()")
.unwrap()
let (ulx, uly, _, lly, urx, ..): (f64, f64, f64, f64, f64, f64, f64, f64) =
s.eval("return GameTimeTexture:GetTexCoord()").unwrap();
(ulx, urx, uly, lly)
}
/// `GameTimeFrame_Update`'s law, exactly: the 50-px window over the 128×64 UI-TOD-Indicator
+6
View File
@@ -847,6 +847,12 @@ mod faux_scroll_tests;
#[cfg(test)]
mod panel_template_tests;
/// The RETURN-SHAPE gate (decision 1842): `reference/1.12-shapes.tsv` against what this client
/// actually answers. `reference_surface` gates names; nothing gated shapes, and that gap produced
/// six decisions in two days.
#[cfg(test)]
mod shape_gate;
/// The reference's BasicControls.xml — TEXT/message/_ERRORMESSAGE and the ScriptErrors dialog,
/// none of which benilla itself calls: every test enters from Lua the way an addon does.
#[cfg(test)]
@@ -49,7 +49,9 @@ fn harness() -> UiScript {
"UiPanels.xml",
"GameTooltip.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
"OptionsFrameTemplates.xml",
] {
load_xml(&s, file);
@@ -78,6 +80,7 @@ fn drawn_textures(s: &mut UiScript) -> Vec<String> {
/// `SetText` dies — with the `CreateFrame` itself having succeeded and returned an object.
#[test]
fn a_check_button_from_the_template_names_its_label_after_the_caller() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run(r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")"#)
.unwrap();
@@ -119,6 +122,7 @@ fn a_check_button_from_the_template_names_its_label_after_the_caller() {
/// all — no error, no warning, 69 invisible checkboxes.
#[test]
fn the_templated_check_button_draws_the_reference_checkbox_art() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")
@@ -160,6 +164,7 @@ fn the_templated_check_button_draws_the_reference_checkbox_art() {
/// options panel in the corpus is written against (`if this:GetChecked() then …`).
#[test]
fn a_templated_check_button_toggles_before_its_on_click_runs() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run(
r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")
@@ -197,6 +202,7 @@ fn a_templated_check_button_toggles_before_its_on_click_runs() {
/// addon idiom is `btn:SetText(...)` and the published global is still `$parentText`.
#[test]
fn a_panel_button_from_the_template_labels_and_paints() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyBtn = CreateFrame("Button", "MyBtn", UIParent, "UIPanelButtonTemplate")
@@ -230,6 +236,7 @@ fn a_panel_button_from_the_template_labels_and_paints() {
/// `UiPanels.xml`.
#[test]
fn the_templated_close_button_hides_the_frame_it_sits_on() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run(
r#"MyPanel = CreateFrame("Frame", "MyPanel", UIParent)
@@ -260,6 +267,7 @@ fn the_templated_close_button_hides_the_frame_it_sits_on() {
/// region, not a layer.
#[test]
fn an_input_box_from_the_template_carries_its_border_and_takes_text() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyBox = CreateFrame("EditBox", "MyBox", UIParent, "InputBoxTemplate")
@@ -306,6 +314,7 @@ fn an_input_box_from_the_template_carries_its_border_and_takes_text() {
/// is exactly how someone later introduces a real one by "restoring" it.)
#[test]
fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyScroll = CreateFrame("ScrollFrame", "MyScroll", UIParent, "UIPanelScrollFrameTemplate")
@@ -383,6 +392,7 @@ fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() {
/// `CreateFrame`'s fourth argument the same way it resolves through XML `inherits=`.
#[test]
fn the_options_check_button_resolves_its_whole_inheritance_chain() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyOpt = CreateFrame("CheckButton", "MyOpt", UIParent, "UIOptionsCheckButtonTemplate")"#,
@@ -430,6 +440,7 @@ fn the_options_check_button_resolves_its_whole_inheritance_chain() {
/// plausible-but-absent here is a template no addon will ever name.
#[test]
fn every_template_these_files_declare_is_a_real_1_12_name() {
let _data = benilla_formats::wow_data_or_skip!();
let tsv =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../reference/1.12-globals.tsv");
let text = std::fs::read_to_string(&tsv).unwrap_or_else(|e| {
@@ -445,7 +456,12 @@ fn every_template_these_files_declare_is_a_real_1_12_name() {
.collect();
let mut checked = 0;
for file in ["UIPanelTemplates.xml", "OptionsFrameTemplates.xml"] {
// OUR files only. This reads the source tree directly, so it cannot name a chain entry — and
// it has no reason to: asserting that the reference's own file declares 1.12 names is
// tautological. What it guards is us inventing one (decision 1841).
// Our remaining file only. `UIPanelTemplates.xml` was ours until 1846 put the whole kit on the
// chain; what is left to guard is the one template we still declare.
for file in ["OptionsFrameTemplates.xml"] {
let src = std::fs::read_to_string(ui_dir().join(file)).unwrap();
let doc = benilla_ui::framexml::parse(&src).unwrap();
for item in &doc.items {
@@ -462,7 +478,7 @@ fn every_template_these_files_declare_is_a_real_1_12_name() {
}
}
assert!(
checked >= 18,
checked >= 1,
"only {checked} templates swept — the sweep, not the files, is what broke"
);
}
@@ -480,6 +496,7 @@ fn every_template_these_files_declare_is_a_real_1_12_name() {
/// under test: a plain function call would not set it.
#[test]
fn tab_resize_falls_back_to_this_when_no_tab_is_passed() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
let doc = benilla_ui::framexml::parse(
@@ -40,6 +40,7 @@ fn frame_rect(quads: &[ExtractedQuad], w: f32, h: f32) -> benilla_ui::layout::Re
/// gets its own dedicated test below (`shipped_gossip_frame_renders_quest_rows_above_options`).
#[test]
fn shipped_gossip_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);
// A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare
@@ -53,7 +54,8 @@ fn shipped_gossip_frame_drives_end_to_end() {
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
// The window + its scroll frame (bar + child) + the 32-row shared pool (quest rows and option
// rows both draw from it, decision 0088 §3 — the reference's own NUMGOSSIPBUTTONS) + the close
// button + the GOODBYE button. The greeting and the NPC-name banner are FontString layers (the
@@ -243,6 +245,7 @@ fn shipped_gossip_frame_drives_end_to_end() {
/// right 1-based position on `take_gossip_quest_selects` (`benilla-ui` `script/gossip.rs`).
#[test]
fn shipped_gossip_frame_renders_quest_rows_above_options() {
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, "MoneyFrame.xml");
@@ -251,7 +254,8 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
s.set_gossip(Some(GossipMenu {
@@ -388,6 +392,7 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
/// an edge — not any glyph metric.
#[test]
fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// **A measurer, because the reference measures inside the tick it sets the text.**
@@ -407,7 +412,8 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// Three long options — the shape of a real judgement/roleplay menu, every one of them wrapping
@@ -510,6 +516,7 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
/// OnHide. Nothing queues at load (the frame is authored hidden="true").
#[test]
fn gossip_show_hide_plays_open_and_close_kits() {
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, "MoneyFrame.xml");
@@ -518,7 +525,8 @@ fn gossip_show_hide_plays_open_and_close_kits() {
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// Hidden at load: no open sound (never transitions on startup).
@@ -551,6 +559,7 @@ fn gossip_show_hide_plays_open_and_close_kits() {
#[test]
fn shipped_panel_slot_replaces_gossip_with_merchant() {
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, "MoneyFrame.xml");
@@ -559,7 +568,8 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
@@ -643,6 +653,7 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
/// director's gossip↔vendor lockup. This drives the exact displacement and asserts the intents.
#[test]
fn displacing_an_npc_window_ends_the_displaced_session() {
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, "MoneyFrame.xml");
@@ -651,7 +662,8 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
@@ -699,6 +711,7 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
/// enough to prove the slot math without a real loot window.
#[test]
fn shipped_panel_slot_pushable_promotes_to_center() {
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, "MoneyFrame.xml");
@@ -792,6 +805,7 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
/// center-parked "left-area" occupant back — UIParent.lua l.777-782).
#[test]
fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::BankState;
// The bank window is the reference's own file off the player's chain (1751).
@@ -813,7 +827,8 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
// list passes load_xml and then loses the scrollbar silently.
// Before BankFrame, not after: its close and purchase buttons inherit UIPanelCloseButton
// and UIPanelButtonTemplate, and an `inherits=` is resolved at LOAD.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BankFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
@@ -877,6 +892,7 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
/// scrolling moves the content up under that clip. Same deterministic measure fake as the row test.
#[test]
fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare
@@ -887,7 +903,8 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// Eight wrapping options: ~4 lines each, far past the 334 px scroll frame.
@@ -1036,12 +1053,14 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
/// it is where a client window would be.
#[test]
fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
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, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// The addon's three lines, in the order an addon writes them.
@@ -1111,6 +1130,7 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
/// either now fails a named test instead of waiting for a player with a Verdant Note.
#[test]
fn the_1507_registry_rows_match_the_reference_bytes() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
@@ -1146,6 +1166,7 @@ fn the_1507_registry_rows_match_the_reference_bytes() {
/// not take a slot; a `whileDead = 1` row (the quest log, l.25) opens exactly as alive.
#[test]
fn a_dead_player_opens_only_whiledead_windows() {
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, "MoneyFrame.xml");
@@ -1211,6 +1232,7 @@ fn a_dead_player_opens_only_whiledead_windows() {
/// dependency chain.
#[test]
fn a_frame_arriving_at_center_puts_the_child_windows_away() {
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, "MoneyFrame.xml");
@@ -94,7 +94,8 @@ fn setup() -> UiScript {
load_xml(&s, "BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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
@@ -81,6 +81,7 @@ const HAMMER_LINK: &str = "|cffffffff|Hitem:2024:0:0:0|h[Militia Hammer]|h|r";
/// The loader itself: every file the window depends on parses and materializes with no errors.
#[test]
fn shipped_questlog_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");
@@ -91,7 +92,8 @@ fn shipped_questlog_frame_loads_clean() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
}
@@ -101,6 +103,7 @@ fn shipped_questlog_frame_loads_clean() {
/// close kit.
#[test]
fn shipped_questlog_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");
@@ -112,7 +115,8 @@ fn shipped_questlog_frame_drives_end_to_end() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -209,13 +213,21 @@ fn shipped_questlog_frame_drives_end_to_end() {
_ => None,
})
.expect("a 'Quest 3' row text quad");
let before = s
.eval::<String>("return QuestLogTitle1Text:GetText()")
.unwrap();
s.mouse_wheel(wx, wy, -1.0);
assert!(s.errors().is_empty(), "wheel errors: {:?}", s.errors());
assert!(
// **Not "by one row" any more.** That was our own `ScrollFrameTemplate_OnMouseWheel`, which
// stepped one `valueStep`. The reference's moves the bar by **half its own height** in value
// units (`UIPanelTemplates.lua`), which over a 16px row and a 237px bar is roughly seven rows.
// The migration reverts our step; how far a notch should travel is a look call, recorded in
// 1846 rather than re-authored back in.
assert_ne!(
s.eval::<String>("return QuestLogTitle1Text:GetText()")
.unwrap()
.contains("Quest 2"),
"wheel-down scrolled the list by one row"
.unwrap(),
before,
"wheel-down scrolled the list"
);
// Selection itself is untouched by scrolling.
assert_eq!(s.eval::<i64>("return GetQuestLogSelection()").unwrap(), 1);
@@ -285,6 +297,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
/// own or gate on visibility.
#[test]
fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
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");
@@ -296,7 +309,8 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -405,6 +419,7 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
/// selects the row exactly like any other click (pin §5).
#[test]
fn watch_guards_no_op_without_erroring() {
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");
@@ -417,7 +432,8 @@ fn watch_guards_no_op_without_erroring() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -499,6 +515,7 @@ fn watch_guards_no_op_without_erroring() {
/// re-arms it, and expiry (the watch frame's OnUpdate) unwatches and hides the empty HUD.
#[test]
fn progress_auto_watches_for_five_minutes() {
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");
@@ -510,7 +527,8 @@ fn progress_auto_watches_for_five_minutes() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -561,6 +579,7 @@ fn progress_auto_watches_for_five_minutes() {
/// client's auto-watch is dead until the player toggles the box. Ours ships the string.
#[test]
fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
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");
@@ -572,7 +591,8 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
assert_eq!(s.eval::<String>("return AUTO_QUEST_WATCH").unwrap(), "1");
@@ -600,6 +620,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
/// disables Abandon, and shows the centered empty-state message instead of `EmptyQuestLogFrame`.
#[test]
fn empty_quest_log_hides_rows_and_disables_abandon() {
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");
@@ -611,7 +632,8 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
@@ -650,6 +672,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
/// of the last choice row, the fixed reward below that, unused rows/choices past the count hidden.
#[test]
fn reward_rows_follow_the_refs_two_per_row_layout() {
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");
@@ -661,7 +684,8 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
let mut state = eight_entries();
@@ -806,6 +830,7 @@ fn overflowing_entry() -> QuestLogState {
/// the mechanism, just through the shipped window instead of a synthetic fixture.
#[test]
fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
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");
@@ -817,7 +842,8 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -867,6 +893,7 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
/// window's bucket covering the detail rect, so the spin lands on it outright.
#[test]
fn wheel_over_the_detail_pane_changes_vertical_scroll() {
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");
@@ -878,7 +905,8 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -913,6 +941,7 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
/// reader's scroll position out from under them.
#[test]
fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not() {
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");
@@ -924,7 +953,8 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -968,6 +998,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
/// renders the full stat head — the identical lines a vendor row/bag slot gets for this item.
#[test]
fn reward_row_hover_serves_the_shared_item_tooltip() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::ItemTemplateView;
let mut s = UiScript::new().unwrap();
@@ -987,7 +1018,8 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -1062,6 +1094,7 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
/// dialog's dark buttons, pixel-diffed against the raw UI-DialogBox-Button-Up art).
#[test]
fn popup_children_inherit_the_dialog_stratum() {
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");
@@ -1110,6 +1143,7 @@ fn popup_children_inherit_the_dialog_stratum() {
/// arm runs last — opening the room closes the log, exactly as it does in play.
#[test]
fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
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");
@@ -1121,7 +1155,8 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
@@ -1190,6 +1225,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
/// eaten it. The select-and-repaint tail (l.503-504) runs either way.
#[test]
fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_it_closed() {
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");
@@ -1201,7 +1237,8 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
@@ -1281,6 +1318,7 @@ fn entries_sharable(ids: &[u32]) -> QuestLogState {
/// both together light it (ref `QuestLogFrame.lua:299-305`).
#[test]
fn share_quest_needs_both_a_sharable_selection_and_a_party() {
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");
@@ -1292,7 +1330,8 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
// Solo, quest 1 sharable and selected by the auto-selection.
@@ -1355,6 +1394,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
/// but `GetNumPartyMembers() > 0` is not.
#[test]
fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
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");
@@ -1366,7 +1406,8 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
@@ -1383,6 +1424,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
/// selection move, so an index/id confusion shows up here.
#[test]
fn share_quest_click_queues_the_selected_quests_id() {
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");
@@ -1394,7 +1436,8 @@ fn share_quest_click_queues_the_selected_quests_id() {
// 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, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(entries_sharable(&[1, 5]));
+21 -1
View File
@@ -32,7 +32,8 @@ fn setup() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "FriendsFrame.xml");
load_xml(&s, "RaidFrame.xml");
s
@@ -140,6 +141,7 @@ fn open_raid_tab(s: &mut UiScript) {
/// so in its header, and the tab strip is the one place a missing pane is visible without a raid.
#[test]
fn the_fourth_tab_opens_the_raid_pane() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
assert_eq!(
s.eval::<i64>("return FriendsFrame.numTabs").unwrap(),
@@ -168,6 +170,7 @@ fn the_fourth_tab_opens_the_raid_pane() {
/// Closing the window closes the Raid Info flyout with it — the ref's fourth satellite.
#[test]
fn hiding_the_window_closes_the_raid_info_panel() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
s.run("RaidInfoFrame:Show()").unwrap();
@@ -187,6 +190,7 @@ fn hiding_the_window_closes_the_raid_info_panel() {
/// leader of an actual party — the three states, in order.
#[test]
fn convert_to_raid_is_live_only_for_a_party_leader() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
assert!(visible(&s, "RaidFrameRaidDescription"), "solo: the blurb");
@@ -256,6 +260,7 @@ fn convert_to_raid_is_live_only_for_a_party_leader() {
/// menu action downstream addresses a row by the index this seating implies.
#[test]
fn the_grid_seats_each_row_in_its_own_subgroups_next_free_slot() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -323,6 +328,7 @@ fn the_grid_seats_each_row_in_its_own_subgroups_next_free_slot() {
/// the names still read, the clicks still fire.
#[test]
fn a_rows_colour_is_offline_then_dead_then_class() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(
@@ -395,6 +401,7 @@ fn a_rows_colour_is_offline_then_dead_then_class() {
/// field: a 40-row rebuild per point of damage taken is what the other choice costs.
#[test]
fn a_units_health_and_level_repaint_only_that_row() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
let mut raid = twelve();
@@ -445,6 +452,7 @@ fn a_units_health_and_level_repaint_only_that_row() {
/// outright out of one — they share their seat with Convert To Raid.
#[test]
fn ready_check_is_the_leaders_and_add_member_is_everyones() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -494,6 +502,7 @@ fn ready_check_is_the_leaders_and_add_member_is_everyones() {
/// needs a live cursor this harness has no way to place.
#[test]
fn dragging_a_row_moves_swaps_or_springs_back() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -538,6 +547,7 @@ fn dragging_a_row_moves_swaps_or_springs_back() {
/// never stops.
#[test]
fn only_the_leader_may_drag() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
let raid = twelve();
@@ -569,6 +579,7 @@ fn only_the_leader_may_drag() {
/// Left-clicking a row targets that row's `raidN` token — the same token the tooltip reads.
#[test]
fn left_clicking_a_row_targets_its_raid_token() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -586,6 +597,7 @@ fn left_clicking_a_row_targets_its_raid_token() {
/// opened on the wrong index would offer Promote on somebody who is already an assistant.
#[test]
fn the_row_menu_offers_the_rank_verbs_by_rank() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -633,6 +645,7 @@ fn the_row_menu_offers_the_rank_verbs_by_rank() {
/// the kick by the row index.
#[test]
fn the_menu_verbs_queue_the_right_requests() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -673,6 +686,7 @@ fn the_menu_verbs_queue_the_right_requests() {
/// the server has answered with something.
#[test]
fn the_raid_info_panel_lists_the_saved_lockouts() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
// The first UPDATE_INSTANCE_INFO only arms the ref's latch (`RaidFrame.hasRaidInfo`).
@@ -743,6 +757,7 @@ fn the_raid_info_panel_lists_the_saved_lockouts() {
/// left live over an empty panel is exactly what the director saw.
#[test]
fn a_player_with_no_lockouts_loses_the_raid_info_button_on_the_second_answer() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
@@ -801,6 +816,7 @@ fn a_player_with_no_lockouts_loses_the_raid_info_button_on_the_second_answer() {
/// the bar is shown.
#[test]
fn the_scroll_bar_is_seated_on_the_trough_the_panel_draws_behind_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
s.fire_event("UPDATE_INSTANCE_INFO", Vec::new());
@@ -854,6 +870,7 @@ fn the_scroll_bar_is_seated_on_the_trough_the_panel_draws_behind_it() {
/// including the No button's argument-less call, which is the reference's own spelling.
#[test]
fn the_ready_check_popup_opens_and_answers() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -905,6 +922,7 @@ fn the_ready_check_popup_opens_and_answers() {
/// the window's geometry guarded by nothing.
#[test]
fn the_geometry_matches_both_reference_files() {
let _data = benilla_formats::wow_data_or_skip!();
// The FrameXML half: the pane, its two buttons, the info flyout and the lockout row template.
if let Some(reference) = super::framexml_diff::reference("RaidFrame.xml") {
super::framexml_diff::assert_geometry_matches(
@@ -952,6 +970,7 @@ fn the_geometry_matches_both_reference_files() {
/// the row goes home, the pane forgets it, and the next row still drags.
#[test]
fn a_drag_carried_off_the_window_edge_ends_instead_of_gluing_the_row_to_the_cursor() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
@@ -1032,6 +1051,7 @@ fn a_drag_carried_off_the_window_edge_ends_instead_of_gluing_the_row_to_the_curs
/// mattered — never drags a SECOND row. This one does all four.
#[test]
fn one_drag_does_not_cost_the_next_one() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
open_raid_tab(&mut s);
let mut raid = vec![row("Me", 2, 1, "Warrior", true, false)];
@@ -0,0 +1,314 @@
//! **The return-shape gate** — `reference/1.12-shapes.tsv` against what this client actually
//! answers (decision 1842).
//!
//! `reference_surface` compares `_G` to `_G`, so a wrong *name* cannot land. Nothing compared
//! *shapes*, and that gap produced six decisions in two days — 1818, 1819, 1830, 1834, 1836 and
//! 1840 — every one of them a right name with a wrong arity, invisible to every gate we had and
//! invisible to the chain's own files too, because a Lua caller that uses one value of two never
//! notices the second.
//!
//! The table is wow-re's `re/audit/binding-shapes.tsv`, vendored: 1722 rows over all 82 registrar
//! tables, one per registered binding, generated and differentially tested against that repo's
//! other harvester. It carries the gate rule **per row** rather than in prose, which is the whole
//! reason it can be enforced instead of remembered:
//!
//! * `arity_conf = exact` — sound. This is the column to gate on.
//! * `kinds_conf` — trustworthy only where an independent push count agreed; not gated here.
//! * `argc_conf` — **never** gate on it. It is a deliberate over-flagging superset: any binding
//! whose direct callee reads a Lua index is marked `lower-bound`, so a `lower-bound` row says
//! nothing about the real argument count.
//!
//! ## Two things that make this narrower than it looks, both on purpose
//!
//! **Only query-shaped names are probed.** The gate calls each binding to count what it answers,
//! and calling an arbitrary global for its arity would mean calling `AbandonQuest`, `Quit` and
//! `AcceptBattlefieldPort` for theirs. Names are filtered to the query verbs — `Get*`, `Is*`,
//! `Has*`, `Can*`, `Unit*`, `Num*` — which is a loss of coverage, never a wrong assertion.
//!
//! **A call that raises is skipped, not failed.** 53 of the 83 unit-table bindings gate their
//! arguments and raise on a nil token (1834/1836), so a no-argument probe of those is expected to
//! throw; that says nothing about arity. Only a call that *returns* is measured.
//!
//! ## The name is not a key
//!
//! **60 names are registered from more than one table and 21 differ in arity across
//! registrations** — `GetBuildInfo` is arity 5 in the glue table `0x8373b8` and 3 in the in-game
//! core `0x83de68`; `GetAddOnInfo` is 8 and 7. This VM is the in-game one, so a name carrying rows
//! from both is read at its **non-glue** row, and a name whose remaining rows still disagree is
//! skipped rather than guessed at.
//!
//! That hazard is the reason the table is keyed on `fn` upstream, and it is a live merge hazard for
//! `1.12-globals.tsv`, which is name-keyed.
/// Names the gate knows are wrong and does not yet assert — **a list that may only shrink**.
///
/// **It is empty.** It held five entries whose miss-branch *contents* the shapes table does not
/// record — four globals and `Model:GetFogColor` — and 1845 answered all five at the bytes rather
/// than guessing them. The mechanism stays because the next divergence the gate finds will want it:
/// an entry needs an arity, a reason, and a dispatched question.
const NOT_YET_ASSERTED: &[(&str, usize, &str)] = &[];
/// The widget half's own shrinking list — see [`NOT_YET_ASSERTED`], same rules. Also empty:
/// `GetFogColor` came off it with 1845, which found the fog colour was a packed `0xAARRGGBB` dword
/// all along.
const WIDGET_NOT_YET_ASSERTED: &[&str] = &[];
/// The glue registrar table. This VM is the in-game one, so a duplicated name is read at its
/// other row (see the module doc).
const GLUE_TABLE: &str = "0x8373b8";
struct Row {
name: String,
table_va: String,
arity: usize,
}
fn rows() -> Vec<Row> {
let tsv = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-shapes.tsv"
);
let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv");
text.lines()
.filter(|l| !l.starts_with('#') && !l.starts_with("name\t"))
.filter_map(|l| {
let f: Vec<&str> = l.split('\t').collect();
// name fn pair_va table_va table_kind argc argc_conf arity arity_conf kinds …
if f.len() < 9 || f[4] != "global" || f[8] != "exact" {
return None;
}
Some(Row {
name: f[0].to_string(),
table_va: f[3].to_string(),
arity: f[7].parse().ok()?,
})
})
.collect()
}
/// Every registered global whose arity the reference states exactly, answered by this client.
#[test]
fn every_query_binding_answers_the_reference_s_return_arity() {
let all = rows();
assert!(
all.len() > 800,
"the vendored table looks wrong: {}",
all.len()
);
// Resolve the duplicated names the way the module doc describes.
let mut by_name: std::collections::HashMap<&str, Vec<&Row>> = std::collections::HashMap::new();
for r in &all {
by_name.entry(&r.name).or_default().push(r);
}
let s = benilla_ui::script::UiScript::new().expect("VM");
let mut checked = 0usize;
let mut mismatches: Vec<String> = Vec::new();
for (name, rs) in &by_name {
// Query verbs only — the gate calls what it measures.
if !["Get", "Is", "Has", "Can", "Unit", "Num"]
.iter()
.any(|p| name.starts_with(p))
{
continue;
}
let candidates: Vec<&&Row> = if rs.len() == 1 {
rs.iter().collect()
} else {
rs.iter().filter(|r| r.table_va != GLUE_TABLE).collect()
};
// Still ambiguous after dropping glue: skip rather than guess.
let Some(first) = candidates.first() else {
continue;
};
if candidates.iter().any(|r| r.arity != first.arity) {
continue;
}
let want = first.arity;
// A call that raises says nothing about arity — 53 of the unit bindings gate their
// arguments and raise on a nil token (1834/1836).
let probe = format!(
"if type({name}) ~= 'function' then return -1 end \
local ok, n = pcall(function() return select('#', {name}()) end) \
if not ok then return -1 end return n"
);
let got: i64 = match s.eval(&probe) {
Ok(n) => n,
Err(_) => continue,
};
if got < 0 {
continue;
}
checked += 1;
if got as usize != want {
if let Some((_, known, _)) = NOT_YET_ASSERTED.iter().find(|(n, ..)| n == name) {
assert_eq!(
*known, want,
"{name} is on the not-yet-asserted list at a stale arity — the table now says \
{want}"
);
continue;
}
mismatches.push(format!("{name}: answers {got}, reference states {want}"));
}
}
assert!(
checked >= 40,
"the gate measured only {checked} bindings — it has stopped covering anything"
);
// The list may only SHRINK. An entry that now agrees is a fix nobody deleted the note for,
// and leaving it would let the next real divergence hide behind it.
let stale: Vec<&str> = NOT_YET_ASSERTED
.iter()
.map(|(n, ..)| *n)
.filter(|n| {
by_name.get(n).is_some_and(|rs| {
let want = rs[0].arity;
s.eval::<i64>(&format!(
"local ok, n = pcall(function() return select('#', {n}()) end) \
if not ok then return -1 end return n"
))
.is_ok_and(|got| got >= 0 && got as usize == want)
})
})
.collect();
assert!(
stale.is_empty(),
"these now answer the reference's arity and must come off NOT_YET_ASSERTED: {stale:?}"
);
assert!(
mismatches.is_empty(),
"{} of {checked} probed bindings answer the wrong number of values:\n {}",
mismatches.len(),
mismatches.join("\n ")
);
}
/// The widget registrar tables, each mapped to a Lua expression that produces one instance.
///
/// The table address IS the widget class — the reference registers one `{name, fn}` table per
/// widget family, and a method name can appear in several of them with different arities (the
/// `name-not-unique` case). So a widget method is probed on **its own class**, never on whichever
/// object happens to answer to the name.
///
/// Identified by each table's distinctive members: `GetTexCoord` is Texture's, `GetChecked` is
/// CheckButton's, `GetHyperlinkFormat` is SimpleHTML's, `GetScrollChild` is ScrollFrame's, and so
/// on. A table with no entry here is skipped, which costs coverage and never correctness.
const WIDGET_PROBES: &[(&str, &str, &str)] = &[
("0x878ec0", "Frame", "PGFrame"),
("0x879d00", "Button", "PGButton"),
("0x87bf74", "CheckButton", "PGCheck"),
("0x87bb68", "EditBox", "PGEdit"),
("0x87b260", "Slider", "PGSlider"),
("0x87b010", "StatusBar", "PGStatus"),
("0x87b3c0", "ScrollFrame", "PGScroll"),
("0x87ba80", "SimpleHTML", "PGHtml"),
("0x87abb0", "ColorSelect", "PGColor"),
("0x87b960", "MessageFrame", "PGMessage"),
("0x87b5c0", "ScrollingMessageFrame", "PGScrollMsg"),
("0x878948", "PlayerModel", "PGModel"),
];
/// The two region classes, which are made by a frame rather than by `CreateFrame`.
const REGION_PROBES: &[(&str, &str)] = &[
("0x87c128", "PGFrame:CreateTexture('PGTex')"),
("0x87c1d8", "PGFrame:CreateFontString('PGFS')"),
];
/// **The widget half of the gate** (decision 1843) — the half that would have caught 1840's
/// `GetTexCoord` without a byte read.
///
/// Same rule as the global half: `arity_conf = exact` only, query verbs only, a call that raises is
/// skipped. The difference is the receiver: each row is probed on an instance of the class its
/// registrar table belongs to, because the same method name lives in several tables with different
/// shapes.
#[test]
fn every_widget_method_answers_the_reference_s_return_arity() {
let tsv = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-shapes.tsv"
);
let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv");
let s = benilla_ui::script::UiScript::new().expect("VM");
let mut made: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
for (table, kind, name) in WIDGET_PROBES {
if s.run(&format!(
"{name} = CreateFrame(\"{kind}\", \"{name}\", UIParent)"
))
.is_ok()
{
made.insert(table, (*name).to_string());
}
}
for (table, expr) in REGION_PROBES {
let var = format!("PGR{}", made.len());
if s.run(&format!("{var} = {expr}")).is_ok() {
made.insert(table, var);
}
}
assert!(
made.len() >= 8,
"only {} widget classes could be instantiated — the probe set is broken",
made.len()
);
let mut checked = 0usize;
let mut mismatches: Vec<String> = Vec::new();
for line in text
.lines()
.filter(|l| !l.starts_with('#') && !l.starts_with("name\t"))
{
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 9 || f[4] != "widget" || f[8] != "exact" {
continue;
}
let (name, table) = (f[0], f[3]);
if !["Get", "Is", "Has", "Can", "Num"]
.iter()
.any(|p| name.starts_with(p))
{
continue;
}
let Some(obj) = made.get(table) else { continue };
let Ok(want) = f[7].parse::<usize>() else {
continue;
};
let probe = format!(
"if type({obj}.{name}) ~= 'function' then return -1 end \
local ok, n = pcall(function() return select('#', {obj}:{name}()) end) \
if not ok then return -1 end return n"
);
let Ok(got) = s.eval::<i64>(&probe) else {
continue;
};
if got < 0 {
continue;
}
checked += 1;
if got as usize != want {
if WIDGET_NOT_YET_ASSERTED.contains(&name) {
continue;
}
mismatches.push(format!(
"{obj}:{name} answers {got}, reference states {want}"
));
}
}
assert!(
checked >= 60,
"the widget gate measured only {checked} methods — it has stopped covering anything"
);
assert!(
mismatches.is_empty(),
"{} of {checked} probed widget methods answer the wrong number of values:\n {}",
mismatches.len(),
mismatches.join("\n ")
);
}
+8 -3
View File
@@ -223,7 +223,8 @@ pub(super) const CHARACTER_UI: &[&str] = &[
"UiPanels.xml", // CharacterFrameTabButtonTemplate, UIPanelWindows, Show/HideUIPanel
"GameTooltip.xml",
"Cooldown.xml", // CooldownFrameTemplate + CooldownFrame_SetTimer, per equipment slot
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The unit frames' four right-click dropdowns call `UIDropDownMenu_Initialize` at LOAD, so the
// kit and the menu table it initialises from both precede them — the manifest's own order
// (175 → 189 → 193 → 263).
@@ -252,7 +253,10 @@ pub(super) const CHARACTER_UI: &[&str] = &[
// (`CharacterFrame.lua:25-32`), unguarded. A missing page is `attempt to index a nil value` on
// the very first `ToggleCharacter` — load-clean, then dead on the first click. Their own
// template dependencies come with them.
"ScrollTemplates.xml", // SkillFrame's faux list + trough
"ScrollTemplates.xml", // SkillFrame's faux list + trough
// The four options templates off the chain, then ours for the one it does not carry
// (`UIOptionsCheckButtonTemplate` — decision 1841).
"Interface\\FrameXML\\OptionsFrameTemplates.xml",
"OptionsFrameTemplates.xml", // ReputationFrame's detail check boxes
"Interface\\FrameXML\\CharacterFrame.xml",
"Interface\\FrameXML\\PaperDollFrame.xml",
@@ -294,7 +298,8 @@ pub(super) const BAG_UI: &[&str] = &[
// `UpdateMicroButtons` — the KEYRING's own OnShow/OnHide calls it (ContainerFrame.lua l.117,
// l.137), because in the reference the keyring's existence moves the micro-button row.
"MicroMenu.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\ContainerFrame.xml",
// `PaperDollItemSlotButtonTemplate` and the `PaperDollItemSlotButton_*` family behind it,
// which every bag button inherits and runs — resolved at load, so this has to precede the bar
@@ -1913,9 +1913,12 @@ fn the_raid_mark_helper_maps_each_index_to_its_cell() {
for (i, l, r, t, b) in cells {
s.run(&format!("SetRaidTargetIconTexture(RTMark, {i})"))
.unwrap();
let got: (f64, f64, f64, f64) = s.eval("return RTMark:GetTexCoord()").unwrap();
// `GetTexCoord` answers EIGHT (UL, LL, UR, LR as x,y pairs) since 1840; the old
// `(l, r, t, b)` rect is `ULx, URx, ULy, LLy` — positions 1, 5, 2, 4.
let (gl, gt, _, gb, gr, ..): (f64, f64, f64, f64, f64, f64, f64, f64) =
s.eval("return RTMark:GetTexCoord()").unwrap();
assert_eq!(
got,
(gl, gr, gt, gb),
(l, r, t, b),
"mark {i} must sample the cell at ({l}, {r}, {t}, {b})"
);
@@ -36,7 +36,8 @@ fn load_popup_frames(s: &UiScript) {
"Interface\\FrameXML\\TargetFrame.xml",
"Interface\\FrameXML\\PetFrame.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"FriendsFrame.xml",
] {
load_xml(s, file);
@@ -16,7 +16,8 @@ fn harness() -> UiScript {
"Fonts.xml",
"MoneyFrame.xml",
"UiPanels.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad
"ScrollTemplates.xml",
@@ -52,6 +53,7 @@ fn landmark(name: &str, icon: u32, uv: (f32, f32)) -> WorldMapLandmarkView {
/// `points_of_interest` row ships, so this is the guard-directions case exactly.
#[test]
fn a_landmark_draws_its_poi_icon_at_its_map_position() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_world_map_landmarks(vec![landmark("Stormwind Warrior Trainer", 6, (0.25, 0.5))]);
s.run("WorldMapFrame_Update()").unwrap();
@@ -68,7 +70,9 @@ fn a_landmark_draws_its_poi_icon_at_its_map_position() {
);
// Cell 6 of the 8×8 grid = column 6, row 0.
let (l, r, t, b): (f32, f32, f32, f32) = s
// `GetTexCoord` answers EIGHT (UL, LL, UR, LR as x,y pairs) since 1840; the old
// `(l, r, t, b)` rect is `ULx, URx, ULy, LLy` — positions 1, 5, 2, 4.
let (l, t, _, b, r, ..): (f32, f32, f32, f32, f32, f32, f32, f32) = s
.eval("return WorldMapFramePOI1Texture:GetTexCoord()")
.unwrap();
assert_eq!(
@@ -92,6 +96,7 @@ fn a_landmark_draws_its_poi_icon_at_its_map_position() {
/// destroyed, and the same frame is re-seated when the list shrinks back.
#[test]
fn the_poi_pool_grows_and_parks_its_tail() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_world_map_landmarks(vec![
landmark("The Bank", 6, (0.1, 0.1)),
@@ -134,6 +139,7 @@ fn the_poi_pool_grows_and_parks_its_tail() {
/// guard's directions never do, a battleground node's "In Conflict" would.
#[test]
fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_world_map_landmarks(vec![landmark("Lion's Pride Inn", 6, (0.5, 0.5))]);
s.run("WorldMapFrame_Update()").unwrap();
@@ -169,6 +175,7 @@ fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() {
/// guard's marker never carries one.
#[test]
fn the_landmark_getter_returns_the_references_five_values() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
let mut with_status = landmark("Stables", 9, (0.25, 0.75));
with_status.description = "In Conflict".into();
@@ -201,6 +208,7 @@ fn the_landmark_getter_returns_the_references_five_values() {
/// is open far more often than a guard has just given directions.
#[test]
fn no_landmarks_draws_nothing() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run("WorldMapFrame_Update()").unwrap();
assert_eq!(s.eval::<i64>("return GetNumMapLandmarks()").unwrap(), 0);
@@ -217,6 +225,7 @@ fn no_landmarks_draws_nothing() {
/// getting that sign wrong mirrors every blip about the top edge without failing anything else.
#[test]
fn party_blips_sit_at_their_map_positions_and_hide_when_absent() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
// `IsShown`, not `IsVisible`: the map itself is closed in this harness (the POI test's own
// idiom), so every child would read invisible through its hidden ancestor.
@@ -294,6 +303,7 @@ fn party_blips_sit_at_their_map_positions_and_hide_when_absent() {
/// dropdown lists carry `toplevel="true"` with no parent at all.
#[test]
fn the_maps_own_furniture_survives_the_hide_that_showing_it_performs() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1600.0, 900.0);
let failures = super::load_default_ui(&s);
+14 -2
View File
@@ -20,14 +20,15 @@ mod common;
/// the `MoneyTypeInfo` table this window registers `AUCTION_DEPOSIT` into, The `BenillaMoneyInput_*` money-entry
/// helpers used to mean loading MerchantFrame.xml as well; 1751 moved that kit to MoneyFrame.xml
/// on its way to the chain, so the dependency is gone.
const FILES: [&str; 8] = [
const FILES: [&str; 9] = [
"Fonts.xml",
"MoneyFrame.xml",
"UiPanels.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"AuctionFrame.xml",
];
@@ -112,6 +113,7 @@ fn seat_player(s: &mut UiScript, money: u64) {
#[test]
fn auction_frame_loads_and_key_regions_exist() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_ui(&s);
for name in [
@@ -158,6 +160,7 @@ fn auction_frame_loads_and_key_regions_exist() {
/// it ShowUIPanel takes its unregistered branch and the window flips `IsShown()` without landing.
#[test]
fn the_window_is_registered_doublewide() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_ui(&s);
assert_eq!(
@@ -169,6 +172,7 @@ fn the_window_is_registered_doublewide() {
#[test]
fn auction_house_show_opens_the_window_on_the_browse_tab() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
seat_player(&mut s, 500_000);
@@ -226,6 +230,7 @@ fn auction_house_show_opens_the_window_on_the_browse_tab() {
/// The part that proves it works: a fed snapshot paints the Browse rows.
#[test]
fn the_browse_list_populates_from_the_fed_snapshot() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
seat_player(&mut s, 500_000);
@@ -325,6 +330,7 @@ fn the_browse_list_populates_from_the_fed_snapshot() {
/// only when the purse can actually cover it.
#[test]
fn the_bid_and_buyout_gates_read_the_purse() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
// 2 gold. Row 1 costs 10s to bid and 50s to buy out — affordable. Row 2 wants 5 gold.
@@ -408,6 +414,7 @@ fn the_bid_and_buyout_gates_read_the_purse() {
/// that is not about affordability.
#[test]
fn you_cannot_bid_on_your_own_auction() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
seat_player(&mut s, 10_000_000);
@@ -438,6 +445,7 @@ fn you_cannot_bid_on_your_own_auction() {
/// setting a level band and clicking a class row queue no query at all until Search fires.
#[test]
fn search_reads_the_filters_and_nothing_queries_before_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
seat_player(&mut s, 0);
@@ -487,6 +495,7 @@ fn search_reads_the_filters_and_nothing_queries_before_it() {
/// own arithmetic over the stack's vendor value and the RUN TIME — never over what you ask for.
#[test]
fn the_create_gate_and_the_deposit() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
seat_player(&mut s, 100_000);
@@ -629,6 +638,7 @@ fn the_create_gate_and_the_deposit() {
/// Closing the window ends the session client-side and takes both confirmations with it.
#[test]
fn hiding_the_window_closes_the_session() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
seat_player(&mut s, 0);
@@ -657,6 +667,7 @@ fn hiding_the_window_closes_the_session() {
/// batch out of 120 fills all 8 slots, so the turners stay hidden until the list is scrolled down.
#[test]
fn paging_shows_the_turners_only_at_the_end_of_the_list() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
seat_player(&mut s, 0);
@@ -712,6 +723,7 @@ fn paging_shows_the_turners_only_at_the_end_of_the_list() {
/// API surface rather than an equivalent one.
#[test]
fn a_row_hover_goes_through_the_reference_tooltip_verb() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_auction(Some(state(vec![row("Copper Bar", 100, 500, 0, "Someone")])));
+4 -2
View File
@@ -18,7 +18,7 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// The reader's dependency prefix, in the manifest's own order. `ScrollTemplates.xml` and
/// `UIPanelTemplates.xml` joined it with decisions 1337/1338: the page sits in a real ScrollFrame
/// now, whose template is in the second and whose `ScrollFrame_OnLoad` is in the first.
const FILES: [&str; 8] = [
const FILES: [&str; 9] = [
// `ITEM_TEXT_FROM`, which the reference's READY arm concatenates into the creator tail — and
// `attempt to concatenate a nil value` kills the handler before it reaches its `ShowUIPanel`,
// so the window simply never opens. Our deleted copy carried the string as a local fallback.
@@ -30,7 +30,8 @@ const FILES: [&str; 8] = [
// page and title ink. 1.12 keeps it in UIParent.lua and ours does the same (1751 window 9).
"UIParent.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\ItemTextFrame.xml",
];
@@ -322,6 +323,7 @@ fn the_reported_html_page_draws_as_blocks_not_as_its_own_markup() {
/// with. It skips on a machine with no client data, like every other archive-backed sweep.
#[test]
fn the_reported_book_crest_draws_at_the_blps_own_size() {
let _data = benilla_formats::wow_data_or_skip!();
let data = benilla_formats::wow_data_or_skip!();
let chain = std::sync::Mutex::new(benilla_formats::open_chain(&data).expect("open chain"));
+3 -2
View File
@@ -20,7 +20,7 @@ use benilla_ui::script::{
/// `ItemButtonTemplate.xml` is the `SetItemButton*` family the talent buttons grey through (the
/// reference's own verb; see TalentFrame.xml's header) — it sits at .toc line 32, above every
/// other entry here bar `Fonts.xml`.
const FILES: [&str; 12] = [
const FILES: [&str; 13] = [
// `PLAYER_LEVEL` and the rest of the strings the stock file formats through.
"Interface\\FrameXML\\GlobalStrings.lua",
"Fonts.xml",
@@ -36,7 +36,8 @@ const FILES: [&str; 12] = [
// Slider AND its `<OnMouseWheel>`. Nothing else in the tree declares it, and a missing template
// is a loader WARNING, not an error, so without this the window still built — just with no
// scrollbar and no wheel (decision 1833). `load_ui_strict` below is what makes that loud.
"UIPanelTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"ScrollTemplates.xml",
// Stock `TalentFrame_OnShow` opens with `SetButtonPulse(TalentMicroButton, 0, 1)` and then
// `UpdateMicroButtons()` — both live here, and a nil `TalentMicroButton` throws out of OnShow
+22 -1
View File
@@ -208,7 +208,28 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_function(|lua, key: Value| {
let model = lua.app_data_ref::<Model>().expect("model");
let Some(i) = resolve(&model, &key) else {
return Ok(MultiValue::new());
// **The two argument forms miss differently** (decision 1845), and ours answered
// zero values for both.
//
// A NUMERIC index out of range **raises**, and the bounds check is *unsigned* — so
// index `0` raises too, not just a negative or an over-count. That is not an arity
// divergence at all, which is why a gate comparing counts alone would have pushed
// this toward a placeholder branch that does not exist for numbers.
if matches!(key, Value::Integer(_) | Value::Number(_)) {
return Err(mlua::Error::runtime("GetAddOnInfo: index out of range"));
}
// The STRING form is not existence-checked at all: it answers seven placeholders,
// two of which are not nil. Slot 4 is `enabled` in-game (the glue table's fourth is
// `url`, and its eighth is an appended `newVersion` — nothing shifts).
return Ok(MultiValue::from_vec(vec![
lua_str(lua, "NoSuchAddon")?,
Value::Nil,
Value::Nil,
Value::Nil,
Value::Nil,
lua_str(lua, "MISSING")?,
lua_str(lua, "INSECURE")?,
]));
};
// The one arbiter (decision 1292): loaded short-circuit, then `AddOn_CanLoad` in the
// in-game flavour — so NOT_DEMAND_LOADED and INTERFACE_VERSION are reachable here,
+15 -3
View File
@@ -98,9 +98,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
};
let Some(slot) = slot else {
// Not joined: the number 0, and nothing else. `channelName` is unread by every
// caller on this branch (all four sites bail on the number first).
return Ok(MultiValue::from_vec(vec![Value::Integer(0)]));
// **`0, nil, 0` — three values, not one.** This used to push the number alone,
// reasoning that `channelName` is unread by every caller on this branch. True of
// the callers; not true of the client. `0x4a05e0` pushes three on every path, and
// slot 2 is neither the empty string nor the argument echoed back:
// `0x4a0659 xor edx,edx` then `lua_pushstring(NULL)`, which tail-jumps to
// `lua_pushnil`. Decision 1845.
//
// "Not joined" is also wider than a bad index: the lookup answers NULL while the
// join-pending word is non-zero, so a channel already in the list but not yet
// CONFIRMED reads `0, nil, 0` identically — which is what `joined` models here.
return Ok(MultiValue::from_vec(vec![
Value::Integer(0),
Value::Nil,
Value::Integer(0),
]));
};
let name = name_at(&model, slot).unwrap_or_default().to_string();
Ok(MultiValue::from_vec(vec![
+30 -13
View File
@@ -35,13 +35,14 @@ use super::Model;
const VERSION: &str = "1.12.1";
const BUILD: &str = "5875";
const BUILD_DATE: &str = "Sep 19 2006";
/// The `## Interface` number a 1.12 `.toc` declares — and what `benilla.toc` declares too.
const TOC_VERSION: i64 = 11200;
// There was a `TOC_VERSION = 11200` here, and `GetBuildInfo` pushed it as a fourth value. The
// in-game registrar pushes three (decision 1842); the interface number is a later expansion's
// return, and `benilla.toc`'s own `## Interface` line is where that number belongs.
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
// version, build, date, tocversion
// version, build, date — three, and no fourth (decision 1842)
g.set(
"GetBuildInfo",
lua.create_function(|lua, ()| {
@@ -49,7 +50,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::String(lua.create_string(VERSION)?),
Value::String(lua.create_string(BUILD)?),
Value::String(lua.create_string(BUILD_DATE)?),
Value::Integer(TOC_VERSION),
// **THREE, not four.** The reference's in-game `GetBuildInfo 0x4884a0` pushes
// `(string, string, string)`; the interface/TOC number is a later-expansion return
// and 1.12 has no fourth value here. (Its GLUE twin `0x46cd70` pushes five — the
// same name with a different shape per registrar table, which is why 1842's gate
// keys on the table and not the name.) Decision 1842.
]))
})?,
)?;
@@ -202,20 +207,32 @@ impl super::UiScript {
mod tests {
use crate::script::UiScript;
/// `GetBuildInfo` answers as the 1.12.1 client, in the reference's 4-value shape.
/// `GetBuildInfo` answers as the 1.12.1 client: **three strings, and no fourth value.**
///
/// The shape is what addons destructure (`local version, build, date, toc = GetBuildInfo()`),
/// and the `tocversion` is the one they compare against their own `## Interface` to decide
/// whether to run at all — so a wrong or missing fourth value makes an addon disable itself.
/// This used to assert four and explain the fourth as the `tocversion` an addon compares
/// against its own `## Interface` — which is a TBC-and-later idiom. The in-game registrar
/// `0x4884a0` pushes `(string, string, string)` and nothing else (decision 1842); the
/// interface number arrives in a later expansion.
///
/// The corpus agrees, and it is worth the count: **458 sites** write
/// `local version = GetBuildInfo()` and take one value. **Four** destructure a fourth — and on
/// the real 1.12 client those four read `nil`, which is what their authors shipped against.
/// Handing them `11200` was benilla being more generous than the client, which is the superset
/// 1189 exists to stop.
///
/// **The same name has a different shape in the glue registrar** (`0x46cd70`, five strings).
/// This VM is the in-game one. That pair is why 1842's gate keys on the table, not the name.
#[test]
fn get_build_info_answers_as_the_1_12_1_client() {
let s = UiScript::new().unwrap();
assert_eq!(
s.eval::<Vec<String>>(
"local v, b, d, t = GetBuildInfo() return { v, b, d, tostring(t) }"
)
.unwrap(),
vec!["1.12.1", "5875", "Sep 19 2006", "11200"]
s.eval::<Vec<String>>("return { GetBuildInfo() }").unwrap(),
vec!["1.12.1", "5875", "Sep 19 2006"]
);
assert_eq!(
s.eval::<i64>("return select('#', GetBuildInfo())").unwrap(),
3,
"three values, never a fourth"
);
}
+18 -13
View File
@@ -309,23 +309,28 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
)?;
m.set(
"SetFogColor",
lua.create_function(|lua, (this, r, g, b): (Table, Value, Value, Value)| {
let c = (num(&r), num(&g), num(&b));
with_model(lua, &this, |m| m.fog_color = Some(c))
})?,
lua.create_function(
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<Value>)| {
// FIVE arguments, and the fifth is the alpha — the same numeric path and the same
// `[0,1]` clamp as the components, not a flag. It is **guarded with a default of
// `1.0`** where r/g/b are read unconditionally and yield `0.0` when absent, so
// `SetFogColor(r, g, b)` sets alpha 1.0. Getting that backwards renders the fog
// invisible. Decision 1845.
let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u32;
let a = a.as_ref().map_or(1.0, num);
let packed = (q(a) << 24) | (q(num(&r)) << 16) | (q(num(&g)) << 8) | q(num(&b));
with_model(lua, &this, |m| m.fog_color = packed)
},
)?,
)?;
m.set(
"GetFogColor",
lua.create_function(|lua, this: Table| {
let c = with_model(lua, &this, |m| m.fog_color)?;
Ok(match c {
Some((r, g, b)) => MultiValue::from_vec(vec![
Value::Number(f64::from(r)),
Value::Number(f64::from(g)),
Value::Number(f64::from(b)),
]),
None => MultiValue::new(),
})
// FOUR values, always — the packed dword unpacked. Never set reads `1, 1, 1, 1`,
// because the `CSimpleModel` ctor's terminal write is `0xffffffff` (decision 1845).
let packed = with_model(lua, &this, |m| m.fog_color)?;
let ch = |shift: u32| f64::from((packed >> shift) & 0xff) / 255.0;
Ok((ch(16), ch(8), ch(0), ch(24)))
})?,
)?;
+8 -2
View File
@@ -456,8 +456,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"GetLootMethod",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
// **`freeforall`, not `group`.** The three cells this reads sit past `.data`'s raw end
// in the zero-filled tail, so a client that has never been in a group answers the
// zeroth method — which is `freeforall`. Decision 1840.
let method = if model.party.loot_method.is_empty() {
"group"
"freeforall"
} else {
model.party.loot_method.as_str()
};
@@ -846,7 +849,10 @@ mod tests {
let (method, master) = s
.eval::<(String, Option<i64>)>("return GetLootMethod()")
.unwrap();
assert_eq!(method, "group");
// **`freeforall` is the UNGROUPED answer**, not `group`: the cells the reference reads
// sit in `.data`'s zero-filled tail, so a client that has never been in a group reports
// the zeroth method (decision 1840). The `group` above is a real party's, and stays.
assert_eq!(method, "freeforall");
assert_eq!(master, None);
}
+3 -1
View File
@@ -318,7 +318,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// GetRewardSpell() → nil (spell-reward rows out of scope in v1, decision 0088).
g.set(
"GetRewardSpell",
lua.create_function(|_, ()| Ok(Value::Nil))?,
// THREE values on every reachable path, and the reference's own kinds include
// `(nil,nil,nil)` — so the empty answer is three nils, not one (decision 1842).
lua.create_function(|_, ()| Ok((Value::Nil, Value::Nil, Value::Nil)))?,
)?;
// ── Intents ───────────────────────────────────────────────────────────────────────────────────
+3 -1
View File
@@ -652,7 +652,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// ── v1 stubs (each the seam of a named later slice — see the module doc) ─────────────────────
g.set(
"GetQuestLogRewardSpell",
lua.create_function(|_, ()| Ok(Value::Nil))?,
// THREE values on every reachable path, and the reference's own kinds include
// `(nil,nil,nil)` — so the empty answer is three nils, not one (decision 1842).
lua.create_function(|_, ()| Ok((Value::Nil, Value::Nil, Value::Nil)))?,
)?;
g.set(
"IsUnitOnQuest",
+30 -4
View File
@@ -503,13 +503,39 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let rh = region_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
let [l, r, t, b] = model
// EIGHT values — a 4-iteration loop of 2 pushes, `mov eax,8`. There is no 4-value
// getter in this API at all: the 4-argument `(minX, maxX, minY, maxY)` min/max rect is
// **setter-only**, and returning it here was a shape that exists nowhere in the client.
// Order is `SetTexCoord`'s own usage string (`0x87c538`):
// `ULx, ULy, LLx, LLy, URx, URy, LRx, LRy`. Decision 1840.
//
// `GetTexCoord` has zero call sites in 1.12 FrameXML, so nothing on the chain could
// ever have caught this — only the binary could.
let corners = model
.region_data
.get(&rh)
.and_then(|d| d.tex_coords)
.map(|tc| tc.edges())
.unwrap_or([0.0, 1.0, 0.0, 1.0]);
Ok((l, r, t, b))
.map_or([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], |tc| {
match tc {
// Stored per corner in SCREEN order `[TL, TR, BR, BL]`; Lua wants
// UL, LL, UR, LR.
crate::script::types::TexCoords::Corners(c) => [c[0], c[3], c[1], c[2]],
crate::script::types::TexCoords::Rect(_) => {
let [l, r, t, b] = tc.edges();
[[l, t], [l, b], [r, t], [r, b]]
}
}
});
Ok((
corners[0][0],
corners[0][1],
corners[1][0],
corners[1][1],
corners[2][0],
corners[2][1],
corners[3][0],
corners[3][1],
))
})?,
)?;
Ok(())
+15 -1
View File
@@ -532,7 +532,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
.find(|e| e.rep_list_id == slot)
});
let Some(e) = watched else {
return Ok(MultiValue::from_vec(vec![Value::Nil]));
// FIVE values on the nothing-watched branch too — `0x4d6820` has four guards onto
// one label and no raise, so the shape is `nil, 0, 0, 0, 0` (decision 1845).
//
// Worth carrying with it: the `[0, 64)` bound is **not in the binding**.
// `0x4d5620` maps an out-of-range index to `0`, so the "nothing watched" sentinel
// passes the `< 0` and `> max` guards and dies at the NULL-DBC-row guard instead.
// A port that range-checks inside the binding implements a branch the reference
// does not have.
return Ok(MultiValue::from_vec(vec![
Value::Nil,
Value::Integer(0),
Value::Integer(0),
Value::Integer(0),
Value::Integer(0),
]));
};
Ok(MultiValue::from_vec(vec![
Value::String(lua.create_string(&e.name)?),
@@ -410,14 +410,41 @@ fn the_light_tuple_is_opaque_and_survives_the_round_trip() {
(1.0, 0.7_f32 as f64)
);
// Fog is the same shape, and unset fog returns NOTHING rather than three zeros — a pane with no
// fog and a pane fogged to black are different states.
// **Fog is NOT the same shape, and this used to assert the opposite.** It read "unset fog
// returns NOTHING rather than three zeros — a pane with no fog and a pane fogged to black are
// different states", which is a good argument about a model the client does not have: there is
// no unset state. The fog colour is one packed `0xAARRGGBB` dword whose ctor writes
// `0xffffffff`, so a fresh pane reads **four** values, `1, 1, 1, 1` (decision 1845).
assert_eq!(
s.eval::<usize>("return table.getn({ MLight:GetFogColor() })")
.unwrap(),
0
4
);
assert_eq!(
s.eval::<(f64, f64, f64, f64)>("return MLight:GetFogColor()")
.unwrap(),
(1.0, 1.0, 1.0, 1.0),
"never set is white and opaque, not four zeros"
);
// Three arguments set alpha to **1.0**, not 0 — the fifth is guarded with that default where
// r/g/b are read unconditionally. The round trip is LOSSY by 8 bits a channel, because the
// store is that packed dword: 0.1 does not survive, 0.2 does.
s.run("MLight:SetFogColor(0.1, 0.2, 0.3)").unwrap();
let (r, g, b): (f64, f64, f64) = s.eval("return MLight:GetFogColor()").unwrap();
assert_eq!((r as f32, g as f32, b as f32), (0.1, 0.2, 0.3));
let (r, g, b, a): (f64, f64, f64, f64) = s.eval("return MLight:GetFogColor()").unwrap();
assert_eq!(a, 1.0, "the omitted alpha defaults to 1.0");
for (got, want) in [(r, 0.1), (g, 0.2), (b, 0.3)] {
assert!(
(got - want).abs() <= 1.0 / 255.0,
"within one 8-bit step of {want}, got {got}"
);
}
// …and the alpha really is the fifth argument, on the same clamp as the rest.
s.run("MLight:SetFogColor(1, 1, 1, 0)").unwrap();
assert_eq!(
s.eval::<f64>("local _, _, _, a = MLight:GetFogColor() return a")
.unwrap(),
0.0
);
}
@@ -32,8 +32,11 @@ fn set_tex_coord_changes_extracted_uv() {
tex.content
);
let (l, r, t, b): (f32, f32, f32, f32) = s.eval("return t:GetTexCoord()").unwrap();
assert_eq!((l, r, t, b), (0.1, 0.6, 0.2, 0.8));
// EIGHT values, per corner, in `SetTexCoord`'s own usage order — `ULx, ULy, LLx, LLy, URx,
// URy, LRx, LRy` (decision 1840). A Rect stored as `[l, r, t, b]` comes back out as its four
// corners, which is the only shape the reference has: there is no 4-value getter.
let got: (f32, f32, f32, f32, f32, f32, f32, f32) = s.eval("return t:GetTexCoord()").unwrap();
assert_eq!(got, (0.1, 0.2, 0.1, 0.8, 0.6, 0.2, 0.6, 0.8));
// The 8-arg affine form (arg order UL, LL, UR, LR) lands as per-corner UVs in screen order
// [TL, TR, BR, BL] — here a 90° rotation of the full texture.
+6 -2
View File
@@ -47,8 +47,12 @@ pub enum TexCoords {
}
impl TexCoords {
/// The tightest axis-aligned `[left, right, top, bottom]` containing the mapping — what the
/// 4-value `GetTexCoord()` reports (exact for `Rect`, the bounding box for `Corners`).
/// The tightest axis-aligned `[left, right, top, bottom]` containing the mapping (exact for
/// `Rect`, the bounding box for `Corners`).
///
/// **Not what `GetTexCoord()` reports** — that answers eight per-corner values, and the 4-value
/// rect it used to return is a shape the reference has nowhere (decision 1840). This is the
/// renderer's and the app's convenience view, and it keeps its callers.
pub fn edges(&self) -> [f32; 4] {
match *self {
TexCoords::Rect(e) => e,
+25 -6
View File
@@ -7,7 +7,7 @@ use mlua::{Lua, Value};
use super::super::Model;
use super::{
check_unit_token, classification_word, grey_band, level_reads_unknown, pick_unit_token,
power_token, with_unit, SelectionRequest,
with_unit, SelectionRequest,
};
/// The two class ids `GetComboPoints 0x51a190` accepts — the literals `4` and `0xb` it compares
@@ -160,11 +160,23 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
token,
r#"Usage: UnitName("unit")"#,
)?);
// TWO values on every live path (`eax = 2` at `0x5170ae` and `0x517289`): the name and
// the **realm**. Slot 2 is nil for a same-realm player, and *structurally* nil for a
// pet, creature, game object or item — `0x609210` writes the out-parameter only on its
// PLAYER branch. benilla is single-realm, so the second is always nil here; the day a
// cross-realm name arrives it is `UnitState`'s to carry, and `UnitPopup.lua:106`'s
// `name.."-"..server` join is what will read it. Decision 1840.
//
// Not to be confused with `showServerName`: that is the parameter of FrameXML's own
// `GetUnitName(unit, showServerName)` wrapper, which calls this binding with ONE
// argument. The engine's real second argument is a strict `LUA_TBOOLEAN` and is not
// modelled — no consumer passes it.
let name = with_unit(lua, &token, None, |u| u.name.clone())?;
match name {
Some(n) => Ok(Value::String(lua.create_string(&n)?)),
None => Ok(Value::Nil),
}
let name = match name {
Some(n) => Value::String(lua.create_string(&n)?),
None => Value::Nil,
};
Ok((name, Value::Nil))
})?,
)?;
@@ -931,8 +943,15 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
token,
r#"Usage: UnitPowerType("unit")"#,
)?);
// ONE value, not the Era pair. `0x517940` has four live `ret`s and `eax = 1` at every
// one, four `lua_pushnumber` sites and zero `lua_pushstring` — the `(type, "MANA")`
// tuple does not exist in 5875 (decision 1840). `power_token` stays: 1819's per-resource
// event names are its real caller.
//
// The miss default is the NUMBER 0, the same value as Mana and never nil, which is
// load-bearing rather than tidy: `UnitFrame.lua:122` indexes `ManaBarColor[...]` with it.
let ty = with_unit(lua, &token, 0u8, |u| u.power_type)?;
Ok((i64::from(ty), power_token(ty).to_string()))
Ok(i64::from(ty))
})?,
)?;
+9 -7
View File
@@ -400,10 +400,11 @@ fn a_dead_unit_reports_dead_and_zero_health() {
fn power_bindings_read_the_active_type() {
let mut s = UiScript::new().unwrap();
s.set_unit("player", Some(player())); // rage 35/100
// ONE value. The Era `(type, "RAGE")` pair does not exist in 5875 — `0x517940` pushes a
// number at every one of its four live `ret`s and never a string (decision 1840).
assert_eq!(
s.eval::<(i64, String)>(r#"return UnitPowerType("player")"#)
.unwrap(),
(1, "RAGE".into())
s.eval::<i64>(r#"return UnitPowerType("player")"#).unwrap(),
1
);
assert_eq!(s.eval::<i64>(r#"return UnitMana("player")"#).unwrap(), 35);
assert_eq!(
@@ -420,11 +421,12 @@ fn power_bindings_read_the_active_type() {
.unwrap(),
"the Era spellings must not linger beside the 1.12 ones"
);
// An absent unit: type reads as mana, values 0.
// An absent unit: the NUMBER 0 — the same value Mana has, and never nil. Load-bearing rather
// than tidy: stock `UnitFrame.lua:122` writes `ManaBarColor[UnitPowerType(unitFrame.unit)]`,
// and a nil there indexes nothing.
assert_eq!(
s.eval::<(i64, String)>(r#"return UnitPowerType("target")"#)
.unwrap(),
(0, "MANA".into())
s.eval::<i64>(r#"return UnitPowerType("target")"#).unwrap(),
0
);
}
+8 -2
View File
@@ -466,10 +466,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
.unwrap_or_else(|| cont.map_file.clone()),
}),
};
Ok(match file {
// THREE values, always. `0x4a7e30` has one `ret` and not a single conditional jump —
// there is no miss branch at all, the emptiness lives in two helpers — so the
// world-level answer is `nil, 0, 0` rather than one nil. (`arity_conf = exact`;
// decision 1845.) The two zeros are the map's texture dimensions, which no caller
// reads on the world level.
let file = match file {
Some(f) => Value::String(lua.create_string(&f)?),
None => Value::Nil,
})
};
Ok((file, 0i64, 0i64))
})?,
)?;
+13 -5
View File
@@ -410,10 +410,18 @@ pub struct ModelState {
/// no lighting model, and inventing a typed one would be asserting a scene semantics we have
/// not verified.
pub light: Option<Vec<f32>>,
/// `SetFogColor(r, g, b)` — `None` until set. The client's fog surface is seven verbs, not two:
/// this pair plus `SetFogNear`/`GetFogNear`/`SetFogFar`/`GetFogFar`/`ClearFog`, which the
/// struct doc names as unbuilt.
pub fog_color: Option<(f32, f32, f32)>,
/// `SetFogColor(r, g, b, a)` as the reference stores it: **one packed `0xAARRGGBB` dword**,
/// which is why its getter is four values wide and why a Set→Get round trip is **lossy** —
/// eight bits per channel (decision 1845).
///
/// `0xffff_ffff` is the ctor's own terminal write, so the never-set answer is `1, 1, 1, 1` and
/// not four zeros. It used to be `Option<(f32, f32, f32)>` here: three components, no alpha,
/// and `None` for unset — every one of those three wrong.
///
/// The client's fog surface is seven verbs, not two: this pair plus
/// `SetFogNear`/`GetFogNear`/`SetFogFar`/`GetFogFar`/`ClearFog`, which the struct doc names as
/// unbuilt.
pub fog_color: u32,
}
/// A fresh `Model` pane: no content, sequence 0, unrotated, unit scale, camera 0, at the origin.
@@ -433,7 +441,7 @@ impl Default for ModelState {
camera: 0,
position: (0.0, 0.0, 0.0),
light: None,
fog_color: None,
fog_color: 0xffff_ffff,
}
}
}