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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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 ")
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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")])));
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user