the tooltip and five windows go stock, and the battleground keeps score

- the tooltip is the reference's own file: `GameTooltip.xml` loads off the chain, ours is deleted, and every verb it calls was already bound, so this is a plain swap; retired with it: the `$parentThicken` plate fill that thickened the plate from the art's ~73% to ~84% opacity — the reference's plate is the raw `UI-Tooltip-Background` tint, and that is what ships; the one engine fact the transcription had been hiding: the reference's instance declares no size, and a frame with one anchor and no size resolves to NO rect under the reference's own layout law — our declared size had been a harness crutch (1968); and the dressing room goes stock with `<DressUpModel>` as a widget kind (factory `0x495c00`): a `PlayerModel` plus its own `Undress`/`Dress`/`TryOn`, where `Undress` strips the body and not the hands, and Close is read off the window since the stock OnHide queues nothing (1969)
- the mail window and the auction house are the reference's own: `MailFrame.xml` loads off the chain and brings the stationery picker, invoice pane and autocomplete ours had left out; the stationery family is built off wow-re's carve over a list computed off `Stationery.dbc` rows × the player's bags (`0x622270`, not the bank); the selection rides `CMSG_SEND_MAIL` field 6 and a send with none silently aborts; its two events fire where the client fires them (1970); `Blizzard_AuctionUI` loads on demand from the `AUCTION_HOUSE_SHOW` arm, and three root fixes its Lua surfaced: `GetAuctionItemClasses()` is static — read at the addon's load, before any session, so the class tree is a login-scoped push; `SetAuctionCompareItem` (`0x535d70`) on the vendor compare's one body; `NEW_AUCTION_UPDATE` resets the create form; its dressing-room pane shares the app's one room (1971)
- the battleground keeps score: `WorldStateFrame.xml` loads off the chain and ours is deleted; the stock file is three things where ours was one: the readout, the capture bars, and the battleground scoreboard this client never had, built off wow-re's round — `MSG_PVP_LOG_DATA` both ways (throttled to 5000 ms client-side, `0x4aa170`) and `CMSG_LEAVE_BATTLEFIELD`; eleven score verbs over a board the app pushes, where the filter is a SORT, not a compaction — team first, killing blows descending, deaths ascending, honor descending, name — so the filtered count shrinks while `GetBattlefieldScore` still reaches every wire row; `HIDE_OUTDOOR_WORLD_STATE` joins the options uvar block, a capture point is a bar, and the scoreboard and battlefield-minimap toggles become bound specs at the install's own `SHIFT-SPACE`/`SHIFT-M` (1972)
- the trade skill and craft windows go stock as `Blizzard_TradeSkillUI`/`Blizzard_CraftUI` on demand, from the reference's own UIParent arms; our two transcriptions are deleted; the four item-link verbs are built off wow-re's carve on the client's link builder (`0x52adb0`) — zero values on any miss, `GetTradeSkillReagentItemLink` raising Blizzard's own typo'd Usage, kept, and `GetCraftItemLink` the `|Henchant:` link in fixed white; `SetTextColor`'s channels coerce nil to 0 on FontString, Font and Button where ours had raised (1973); and the Inspect, Talent, Macro and Raid addons load on demand — `benilla.toc` lists no `Interface\AddOns` row, and below level 10 the talent addon is never even loaded; the gates walk every reached addon and found `Blizzard_GMSurveyUI`'s four engine verbs nothing answers, reachable only through `GMSURVEY_DISPLAY`, which nothing fires (1889; 1967)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:59:04 +07:00
co-authored by Claude Fable 5.1
parent f4951470a3
commit c965b5425d
126 changed files with 3324 additions and 8156 deletions
File diff suppressed because it is too large Load Diff
-977
View File
@@ -1,977 +0,0 @@
<Ui>
<!--
benilla's Enchanting window (decision 0437 phase 3): the 1.12 `CraftFrame`, re-expressed from
the reference `Blizzard_CraftUI.xml`/`.lua` (session scratchpad `ref-ui/`, decision 0246
discipline — never committed) onto our own engine/feeds. Geometry/art-paths/structure below are
QUOTED from the ref (line citations inline); the Lua is the ref's logic turned onto benilla's
Era API (`GetCraftName`/`GetCraftDisplaySkillLine`/`GetNumCrafts`/`GetCraftInfo`/reagents/
`GetCraftSpellFocus`/`DoCraft`/`CloseCraft`, decision 0437's engine surface,
`crates/benilla-ui/src/script/craft.rs`). The exact twin of `TradeSkillFrame.xml` — same
engine-gap substitutions, same FLAT-list INTERIM, same doc conventions — this header only calls
out where CRAFT'S OWN ref actually differs (verified against `Blizzard_CraftUI.lua`, not
assumed from TradeSkill's shape).
NOT an NPC session (unlike Gossip/Merchant/Trainer): the window opens client-side off your own
cast of an effect-47 opener spell — no NPC, no server round trip to open it (0437 §"What
scoping verified", TradeSkillFrame.xml's own identical header note). It registers
`CRAFT_SHOW`/`CRAFT_UPDATE`/`CRAFT_CLOSE` on ITSELF (TradeSkillFrame.xml's own precedent: this
engine has no separate FrameXML driver that dispatches SHOW for you).
Modeled on the two known-good dressed windows for the framing (MerchantFrame.xml's own header
comment, TradeSkillFrame.xml's own precedent for this exact art family — both windows are
`ClassTrainerFrame`-styled 384×512 four-quadrant windows and share BYTE-IDENTICAL geometry for
every region this file also carries: the portrait, the four border quadrants' Size/Anchors, the
skill-border/horizontal-bar decorative art, the rank StatusBar block, the persistent selection
highlight, the 8-row list + its ScrollFrame, the detail header art, the reagent grid's anchor
chain (down to the SAME "Reagent7 anchors off Reagent6, not Reagent5" ref quirk), and the
Create/Exit button positions):
- MerchantFrame.xml/TradeSkillFrame.xml — the 384×512 four-quadrant window art (no
<Backdrop>: the art IS the frame), the "player" portrait binding (`SetPortraitTexture(region,
"player")` — ref l.89-91 confirms Craft ALSO binds "player", not "npc": this is not an NPC
session), the UIPanel slot system (carries NO hardcoded <Anchors> — ShowUIPanel/SetLeftFrame
SetPoints it), OnShow/OnHide PlaySound pair.
- TradeSkillFrame.xml — the FLAT layout constraint (below), the faux-scroll list kit
(FauxScrollFrameTemplate, ScrollTemplates.xml), the locally-reconstructed
UIPanelButtonTemplate/close-button/item-button idioms (no such templates ship in this house
dialect — TalentFrame.xml's own ENGINE API note, restated per-window, StackSplit.xml's own
"own local virtual template, not a cross-file inherits" convention), the persistent
selection-highlight Frame, and the `button.craftIndex`/`Button:SetTextColor` engine-gap
substitutions (deviation 7 below).
FLAT layout — a CHOICE inherited from TradeSkillFrame.xml, which inherited it from
TrainerFrame.xml, which stated it as a house constraint it never was. A runtime-shown child
<Frame> DOES render its own <Layers> FontStrings in this engine (probed; decision 1517). The
detail-pane FontStrings + the title live on BenillaCraftFrame's OWN <Layers> (OVERLAY, above
the window's own BACKGROUND/BORDER/ARTWORK art groups) because that is how this window was
built, not because nesting fails. The ref's
`CraftDetailScrollFrame`/`CraftDetailScrollChildFrame` therefore don't exist here — the detail
is a FIXED (non-scrolling) pane, computed once from the flattened ScrollFrame+ScrollChild
offsets (TOPLEFT 20,-234 + each child's own ref-quoted offset — BYTE-IDENTICAL to TradeSkill's
own detail-scroll anchor, since both refs place it there). Per-region regions that DO need
Show()/Hide() live on Button/StatusBar/Texture children (those render their own Layers) — the
rank bar, the detail icon, and the 8 reagent slots.
v1 DEVIATIONS (pinned to decision 0437's in-flight wow-re §5 dispatch where noted):
1. FLAT LIST, NO HEADERS. `GetCraftInfo`'s `craftType` is never `"header"` in v1 (the engine
API contract) — the ref's header BRANCH (the row's fold icon/texture swap, the row-text
21px-right reposition loop) is still dropped outright, not even as a no-op:
`Expand`/`CollapseCraftSkillLine` exist but are wired as true engine-side NO-OPS
(`craft.rs`'s own module doc + `Expand/CollapseCraftSkillLine` comment) — the 1.12 craft
list is single-group, so the real client's own header path is unexercised here; a click
changes nothing observable beyond the button's own local state.
`CraftExpandButtonFrame`/`CraftCollapseAllButton` (ref l.265-325) now SHIP, for fidelity —
this deviation's own earlier revision omitted them entirely; closed this session
(TradeSkillFrame.xml's own identical CollapseAll tab, restored there the same session).
`BenillaCraftFrame_Update`'s own restored tail (ref l.248-282, minus the row-text-reposition
loop, still dropped above) counts headers to size the tab's texture — since `GetCraftInfo`
never returns `"header"`, that count is always 0, so the tail's own `numHeaders > 0` branch
(ref l.270) always takes the ELSE arm: `BenillaCraftExpandButtonFrame` ends up HIDDEN in
practice, a faithful-but-currently-inert outcome (this file's own deviation 2 "ships for
fidelity, never actually visible yet" shape). `CraftTypeColor["header"]`/`["used"]`/
`["none"]` are kept in the transcribed color table for fidelity but are unreachable this way
(TradeSkillFrame.xml's own identical posture). v1 still ships no filter dropdowns.
2. THE SPELL-FOCUS TOOL NEVER REDDENS. `BuildColoredListString` is REUSED, not redefined — it
is already a true Lua GLOBAL after `TradeSkillFrame.xml` loads (`if not
BuildColoredListString then function BuildColoredListString(...) ... end end`, no `local`),
and `TradeSkillFrame.xml`'s own header comment names this file as the second consumer.
`CraftFrame.xml` is loaded AFTER `TradeSkillFrame.xml` in `ui_script/mod.rs`'s load list, so
this file calls it directly with no guard/redefinition. It reds an unmet tool byte-faithfully,
but `GetCraftSpellFocus`'s `hasTool` has no client-side proximity check yet (0437 INTERIM,
pinned to the §5 dispatch, TradeSkillFrame.xml's own identical note) — v1 never actually
reds a tool in practice.
3. NO DRESSING-ROOM / CHAT-LINK INSERT. The ref's Ctrl+click `DressUpItemLink` / Shift+click
`ChatFrameEditBox:Insert` on `CraftIcon`/`CraftReagent*` (ref l.44-51, 555-563) are dropped
— the cursor-item arc hasn't landed (MerchantFrame.xml's/TradeSkillFrame.xml's own identical
"a no-op here until the cursor-item arc" precedent). `CraftIcon`'s `this.hasItem = 1`
OnLoad flag (ref l.553) is dropped too — it is read only by `UIParent.lua`'s own generic
Ctrl-click dressing-room handler (grepped), which this dropped branch never reaches; a real,
now-inert vestige, not a benilla omission.
4. NO `SKILL_LINES_CHANGED` self-listen on the rank StatusBar (ref l.253-260, TradeSkillFrame's
own identical deviation): the skill-up feedback/diff-watcher is 0437 phase 4, not yet built
— the bar's value/color/text refresh only rides this window's own SHOW/UPDATE/selection
paint.
5. NO `PLAYER_ENTERING_WORLD` hide (TradeSkillFrame.xml's own identical choice for this session
shape) — opening this window is your own client-side cast, not an NPC session.
6. NO BEAST TRAINING / NO TRAINING POINTS (0437's own scope line — the pet arc owns Beast
Training's CraftFrame use). Every ref region/branch that exists ONLY to serve a pet ability
row is omitted, not stubbed: `CraftFramePointsLabel`/`CraftFramePointsText`
(`Craft_UpdateTrainingPoints`, ref l.428-438), the per-row `Craft<n>Cost` FontString (always
blank in v1 since `GetCraftInfo`'s `trainingPointCost` is a hardcoded `0` — `craft.rs`'s own
module doc), and the Requirements line's pet-level fallback branch (`requiredLevel`, also a
hardcoded `0` — ref l.364-369, unreachable). `UNIT_PET_TRAINING_POINTS` is not registered
(nothing left to drive it). `GetCraftButtonToken()` always returns `"CREATE"` — Beast
Training's `"TRAIN"` token is the only other value the ref ever returns, and that branch is
out of scope (`craft.rs`'s own note).
7. ENGINE API gaps (TradeSkillFrame.xml's own "no engine equivalent" posture, restated per
substitution actually used here):
- (CLOSED — the row name is the button's own label now.) An earlier revision recorded
`Button:SetTextColor`/`:LockHighlight`/`:UnlockHighlight` as having no engine method and
routed row colouring through a `$parentName` child FontString. The engine has shipped
all three for a long time; the note went stale and the workaround outlived it, costing
this window the white hover/selected label the real client has (decision 1605). The row
name is now a `<ButtonText>` (the ref's `Craft<n>Text`) under the base's
`<NormalFont>/<HighlightFont>/<DisabledFont>` trio, and Update calls `SetTextColor` /
`LockHighlight` / `UnlockHighlight` where the ref does (l.190/234/236). `$parentSubText`
stays a child FontString, as it is in the ref, and is whitened by hand for the selected
row (l.232). NOT taken: the ref's per-row `craftButton.r/g/b` stash and the base's own
`OnEnter`/Craft's `OnLeave` subtext recolour dance (l.191-193, xml l.22-26) — with
Beast Training out of scope (deviation 6) `craftSubSpellName` is always `""`, so every
one of those paints an empty string. The row still carries NO hover tooltip: the ref
has none either. (An earlier revision of this line added "TradeSkill's own row hover IS
a real ref behavior; Craft's is not" — WRONG about *tooltips*, and never checked:
`TradeSkillSkillButtonTemplate` overrides `OnClick` alone, and the shared base
`ClassTrainerSkillButtonTemplate` (`Interface\FrameXML\ClassTrainerFrameTemplates.xml`)
only recolours `$parentSubText` on hover. NEITHER window's rows tooltip.)
- `Button:GetID()`/`:SetID()` have no native method (same grep) — the ref's
`craftButton:SetID(craftIndex)` + `this:GetID()` pair becomes a plain Lua field,
`button.craftIndex`, set in the Update loop and read by the click handler
(`ClassTrainerSkillButtonTemplate`'s own `this.r/g/b`-restoring `OnLeave` — a color dance
made moot by the FontString substitution above — is dropped for the same reason
TradeSkillFrame.xml's own row `OnLeave` dropped it: nothing ever recolors the row on
hover in this engine, so there is nothing to restore).
- `EditBox:SetNumber`/`SecondsToTime` are NOT needed here at all: Craft has no repeat-count
input (deviation 8) and no per-recipe cooldown concept (no `CraftSkillCooldown` region
exists in the ref) — TradeSkillFrame.xml's own engine-gap shims for those simply don't
apply to this window.
8. NO CREATE ALL (a REAL ref difference, not an engine gap — verified: `CraftCreateButton`'s
OnClick is bare `DoCraft(GetCraftSelectionIndex())`, ref l.667, no count argument
anywhere). This window ships ONE action button; no repeat-count EditBox, no -/+ buttons, no
`CraftCreateAllButton`, no `GetCraftskillRepeatCount`/`UPDATE_CRAFTRECAST` — `craft.rs`'s
own module doc.
9. Dead ref code dropped, not transcribed (TradeSkillFrame.xml's own `reagentToAnchorTo`
precedent for exactly this move): `CraftSubTypeColor` + its `subColor` local (ref l.15-22,
189) are computed but never read anywhere in the reference — grepped, confirmed zero other
uses — a real vestige. The row-width scrollbar toggle (ref l.174-187, `293` vs `323`) is
dropped the same way TradeSkill's was: the ScrollFrame itself is always visible in both
engines (only its *scrollbar* toggles on overflow), so rows are fixed at 293
(TradeSkillFrame.xml's own row width).
10. SELECTION SURVIVES `CRAFT_SHOW` (a deliberate deviation from Craft's OWN ref, flagged):
the real `CraftFrame_Show` (ref l.26-49) unconditionally re-selects index 1 on every open,
discarding whatever was selected before. Our engine's `set_craft` already PRESERVES the
selection across a re-push by spell id (`craft.rs`'s own doc, TradeSkill's identical
`set_trade_skill` law) specifically so re-opening the same skill line doesn't visibly jump
the detail pane — forcing index 1 on every Show would fight that mechanism for no benefit.
`BenillaCraftFrame_Show` therefore mirrors `BenillaTradeSkillFrame_Show`'s own pattern
(keep a nonzero selection, else pick 1) instead of Craft's own ref line-for-line.
11. THE ITEM-TARGET PICK UX NOTE (0437 §"What scoping verified", `craft.rs`'s own
`needs_item_target` doc): `DoCraft` on an `ENCHANT_ITEM`-family recipe arms an APP-SIDE
item-target pick (the `TARGET_FLAG_ITEM` cast-target flow) — this XML plays NO part in
that flow; `CraftCreateButton_OnClick` below is the same bare `DoCraft(...)` call
regardless of which recipe is selected, exactly like the ref's own button.
12. GlobalStrings the ref reads (`REQUIRES_LABEL`, `SPELL_REAGENTS`, `PARENS_TEMPLATE`,
`CREATE`, `EXIT`) are declared LOCALLY below as the byte-exact 1.12 enUS values (session
scratchpad extraction, decision 0246's discipline, line-cited per constant),
TradeSkillFrame.xml's own idiom — with ONE exception: `CREATE` is declared as a REAL Lua
GLOBAL (`CREATE = CREATE or "Create"`, guarded so a second file loading it stays
idempotent), not `local`, because `getglobal(GetCraftButtonToken())` (ref l.106, faithfully
kept in `BenillaCraftFrame_Update` below) needs an actual `_G["CREATE"]` entry to
resolve — a genuine structural requirement of that ref call, not a style choice.
`GRAY_FONT_COLOR`/`HIGHLIGHT_FONT_COLOR` are Fonts.xml globals (CharacterFrame.xml's own
precedent for reading them unqualified).
Registers at the left UIPanel slot (`UiPanels.xml`). The ref's OWN registration
(`Blizzard_CraftUI.lua:24`, `UIPanelWindows["CraftFrame"] = { area = "left", pushable = 4 }`) is
`pushable = 4` — VERIFIED at that line (NOT `3`; `3` is `TradeSkillFrame`'s own line 13 value, a
different window/depth — grepped both files directly to confirm before using either number).
ESC-closable through the standard UIPanel chain. `CloseCraft()` fires on hide for ANY reason
(ESC, close X, Exit, `CRAFT_CLOSE`, or another left-slot window displacing it) —
TradeSkillFrame.xml's own `CloseTradeSkill`-on-hide precedent, adapted (no server round trip per
0437, purely client-side session teardown).
-->
<Script><![CDATA[
-- ── Constants (ref Blizzard_CraftUI.lua:2-4) ────────────────────────────────────────────────
local CRAFTS_DISPLAYED = 8 -- CRAFTS_DISPLAYED (ref l.2)
local MAX_CRAFT_REAGENTS = 8 -- MAX_CRAFT_REAGENTS (ref l.3)
local CRAFT_SKILL_HEIGHT = 16 -- CRAFT_SKILL_HEIGHT (ref l.4)
-- The recipe-row color table (ref l.6-13), transcribed verbatim including "header"/"used"/
-- "none" — all UNREACHABLE in v1 (this file's header comment, deviation 1). CraftSubTypeColor
-- (ref l.15-22) is DROPPED outright — dead ref code, deviation 9.
CraftTypeColor = {}
CraftTypeColor["optimal"] = { r = 1.00, g = 0.50, b = 0.25 }
CraftTypeColor["medium"] = { r = 1.00, g = 1.00, b = 0.00 }
CraftTypeColor["easy"] = { r = 0.25, g = 0.75, b = 0.25 }
CraftTypeColor["trivial"] = { r = 0.50, g = 0.50, b = 0.50 }
CraftTypeColor["used"] = { r = 0.50, g = 0.50, b = 0.50 }
CraftTypeColor["header"] = { r = 1.00, g = 0.82, b = 0 }
CraftTypeColor["none"] = { r = 0.25, g = 0.75, b = 0.25 }
-- ── GlobalStrings the ref reads (byte-exact 1.12 enUS, this file's header comment cites the
-- extraction + the CREATE-must-be-global exception) — chunk-local except CREATE. ─────────────
local REQUIRES_LABEL = "Requires:" -- REQUIRES_LABEL (GlobalStrings.lua:3339)
local PARENS_TEMPLATE = "(%s)" -- PARENS_TEMPLATE (2984)
local EXIT_LABEL = "Exit" -- EXIT (1933)
CREATE = CREATE or "Create" -- CREATE (871) -- REAL global, see header comment
-- BuildColoredListString is REUSED from TradeSkillFrame.xml (already a true global by the time
-- this file loads — this file's header comment, deviation 2). No redefinition here.
-- ── Detail pane ─────────────────────────────────────────────────────────────────────────────
-- No crafts known at all (ref l.131-150): blank the whole detail pane. Regions on this window's
-- own Layers "hide" via SetText("") — TradeSkillFrame.xml's own convention, which predates
-- region Show/Hide (region.rs has the real pair now); equivalent either way.
function BenillaCraftFrame_HideDetails()
getglobal("BenillaCraftFrameTitleText"):SetText(GetCraftName())
getglobal("BenillaCraftName"):SetText("")
getglobal("BenillaCraftRequirements"):SetText("")
getglobal("BenillaCraftDescription"):SetText("")
getglobal("BenillaCraftIcon"):Hide()
for i = 1, MAX_CRAFT_REAGENTS do
getglobal("BenillaCraftReagent" .. i):Hide()
end
getglobal("BenillaCraftCreateButton"):Disable()
end
-- Fill the detail pane for the selected recipe `id` (ref CraftFrame_SetSelection, l.285-408;
-- the header branch is dropped — deviation 1, never reached). Also the SOURCE of the selection
-- highlight's color (set once here, ref l.305-306).
function BenillaCraftFrame_SetSelection(id)
if not id or id == 0 then
BenillaCraftFrame_HideDetails()
return
end
SelectCraft(id)
if GetCraftSelectionIndex() > GetNumCrafts() then
return
end
local craftName, craftSubSpellName, craftType = GetCraftInfo(id)
local color = CraftTypeColor[craftType]
if color then
getglobal("BenillaCraftHighlight"):SetVertexColor(color.r, color.g, color.b)
end
-- General info (ref l.308-320).
getglobal("BenillaCraftFrameTitleText"):SetText(GetCraftName())
getglobal("BenillaCraftName"):SetText(craftName)
local icon = getglobal("BenillaCraftIcon")
icon:Show()
icon:SetNormalTexture(GetCraftIcon(id) or BENILLA_FALLBACK_ICON)
-- The description + the Reagents label's DYNAMIC reposition (ref l.312-318, re-expressed
-- here): when there's a description, the label sits 10px below its bottom edge; when
-- there isn't, it sits directly where the (blank) description would have been.
if GetCraftDescription(id) then
getglobal("BenillaCraftDescription"):SetText(GetCraftDescription(id))
getglobal("BenillaCraftReagentLabel"):SetPoint("TOPLEFT", "BenillaCraftDescription", "BOTTOMLEFT", 0, -10)
else
getglobal("BenillaCraftDescription"):SetText(" ")
getglobal("BenillaCraftReagentLabel"):SetPoint("TOPLEFT", "BenillaCraftDescription", "TOPLEFT", 0, 0)
end
-- Reagents (ref l.321-359): up to MAX_CRAFT_REAGENTS, gray-out + creatable gate.
local creatable = true
local numReagents = GetCraftNumReagents(id)
for i = 1, MAX_CRAFT_REAGENTS do
local reagent = getglobal("BenillaCraftReagent" .. i)
if i <= numReagents then
local reagentName, reagentTexture, need, have = GetCraftReagentInfo(id, i)
if not reagentName or not reagentTexture then
-- Item template not landed yet for this reagent — hide it, like the ref does
-- for any nil reagent (engine API note, decision 0437).
reagent:Hide()
else
reagent:Show()
reagent.icon:SetTexture(reagentTexture)
reagent.name:SetText(reagentName)
have = have or 0
if have < need then
reagent.icon:SetVertexColor(0.5, 0.5, 0.5)
reagent.name:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
creatable = false
else
reagent.icon:SetVertexColor(1.0, 1.0, 1.0)
reagent.name:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
end
local haveText = have
if have >= 100 then haveText = "*" end
reagent.count:SetText(haveText .. " /" .. need)
end
else
reagent:Hide()
end
end
-- Requires: line (ref l.361-372) — ONE concatenated FontString, unlike TradeSkill's split
-- Label+Text pair (Craft's own ref genuinely differs here — this file's header comment
-- points at TradeSkillFrame.xml's split for comparison). The pet-level fallback branch
-- (ref l.364-369) is unreachable in v1 (deviation 6) and dropped.
local requiredTotems = BuildColoredListString(GetCraftSpellFocus(id))
if requiredTotems then
getglobal("BenillaCraftRequirements"):SetText(REQUIRES_LABEL .. " " .. requiredTotems)
else
getglobal("BenillaCraftRequirements"):SetText("")
end
if creatable then
getglobal("BenillaCraftCreateButton"):Enable()
else
getglobal("BenillaCraftCreateButton"):Disable()
end
end
-- ── The list page (ref CraftFrame_Update, l.99-283 — the header/expand-icon/training-points
-- tail is dropped: deviations 1/6, never reached in v1) ────────────────────────────────────────
-- `<OnVerticalScroll>` is its own chunk and cannot see this file's
-- `local CRAFT_SKILL_HEIGHT`, so the pitch is handed over here (1868).
function BenillaCraftList_OnVerticalScroll()
FauxScrollFrame_OnVerticalScroll(CRAFT_SKILL_HEIGHT, BenillaCraftFrame_Update)
end
function BenillaCraftFrame_Update()
getglobal("BenillaCraftFrameTitleText"):SetText(GetCraftName())
getglobal("BenillaCraftCreateButton"):SetText(getglobal(GetCraftButtonToken()))
local numCrafts = GetNumCrafts()
if numCrafts == 0 then
BenillaCraftFrame_HideDetails()
else
getglobal("BenillaCraftCollapseAllButton"):Enable() -- ref l.158 (no matching Disable()
-- anywhere in Craft's own ref — a real asymmetry vs. TradeSkill's own l.86/94 pair,
-- kept as the ref has it, not "fixed").
end
local scrollFrame = getglobal("BenillaCraftListScrollFrame")
FauxScrollFrame_Update(scrollFrame, numCrafts, CRAFTS_DISPLAYED, CRAFT_SKILL_HEIGHT)
BenillaScrollTrough_Follow("BenillaCraftListScrollFrame")
local craftOffset = FauxScrollFrame_GetOffset(scrollFrame)
-- The skill-rank StatusBar (ref l.107-123): Show/Hide are real here (real widgets, not
-- flattened Layer regions).
local name, rank, maxRank = GetCraftDisplaySkillLine()
local rankFrame = getglobal("BenillaCraftRankFrame")
if name then
getglobal("BenillaCraftRankFrameSkillName"):SetText(name)
rankFrame:SetStatusBarColor(0.0, 0.0, 1.0, 0.5)
getglobal("BenillaCraftRankFrameBackground"):SetVertexColor(0.0, 0.0, 0.75, 0.5)
rankFrame:SetMinMaxValues(0, maxRank)
rankFrame:SetValue(rank)
getglobal("BenillaCraftRankFrameSkillRank"):SetText(rank .. "/" .. maxRank)
rankFrame:Show()
getglobal("BenillaCraftSkillBorderLeft"):Show()
getglobal("BenillaCraftSkillBorderRight"):Show()
else
rankFrame:Hide()
getglobal("BenillaCraftSkillBorderLeft"):Hide()
getglobal("BenillaCraftSkillBorderRight"):Hide()
end
local highlight = getglobal("BenillaCraftHighlightFrame")
highlight:Hide()
local selection = GetCraftSelectionIndex()
for i = 1, CRAFTS_DISPLAYED do
local craftIndex = i + craftOffset
local button = getglobal("BenillaCraft" .. i)
if craftIndex <= numCrafts then
local craftName, craftSubSpellName, craftType, numAvailable = GetCraftInfo(craftIndex)
button.craftIndex = craftIndex
local color = CraftTypeColor[craftType]
if color then
button:SetTextColor(color.r, color.g, color.b) -- ref l.190
button.subtext:SetTextColor(color.r, color.g, color.b) -- ref l.195
end
if (numAvailable or 0) == 0 then
button:SetText(" " .. craftName)
else
button:SetText(" " .. craftName .. " [" .. numAvailable .. "]")
end
if craftSubSpellName and craftSubSpellName ~= "" then
button.subtext:SetText(format(PARENS_TEMPLATE, craftSubSpellName))
else
button.subtext:SetText("")
end
button:Show()
-- TWO things paint a selected row, and the ref does both here (l.229-236): the
-- glow bar is BenillaCraftHighlightFrame, and the WHITE LABEL is the row's own
-- LockHighlight() — the template's `<HighlightFont inherits="GameFontHighlight">`
-- instance winning over the difficulty colour SetTextColor just wrote, which
-- reaches the NORMAL instance only. The subtext is whitened by hand because it is
-- a child FontString, not the button's label, exactly as the ref does it (l.232).
if selection == craftIndex then
highlight:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
highlight:Show()
button:LockHighlight()
button.subtext:SetTextColor(1.0, 1.0, 1.0)
else
button:UnlockHighlight()
end
else
button.craftIndex = nil
button:Hide()
end
end
-- Count headers + how many stay folded (ref l.249-259), to drive the CollapseAll tab's own
-- texture below — restored, deviation 1 (still functionally inert: GetCraftInfo never
-- returns "header" in v1, so numHeaders is always 0 here).
local numHeaders, notExpanded = 0, 0
for i = 1, numCrafts do
local craftName, craftSubSpellName, craftType, numAvailable, isExpanded = GetCraftInfo(i)
if craftName and craftType == "header" then
numHeaders = numHeaders + 1
if not isExpanded then
notExpanded = notExpanded + 1
end
end
end
-- If all headers are not expanded then show the collapse texture, otherwise the expand one
-- (ref l.261-267).
local collapseAll = getglobal("BenillaCraftCollapseAllButton")
if notExpanded ~= numHeaders then
collapseAll.collapsed = nil
collapseAll:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
else
collapseAll.collapsed = true
collapseAll:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
end
-- Show the tab only when there are headers to fold (ref l.269-282; the row-text-reposition
-- loops there stay dropped, deviation 1) — always Hidden in v1 since numHeaders is always 0.
if numHeaders > 0 then
getglobal("BenillaCraftExpandButtonFrame"):Show()
else
getglobal("BenillaCraftExpandButtonFrame"):Hide()
end
end
-- A row click selects it (ref CraftButton_OnClick + SetSelection's non-header path; the
-- header-fold branch is dropped, deviation 1). Left-button only. NO OnEnter/OnLeave here —
-- Craft's OWN ref never adds a row-hover tooltip (verified, header comment deviation 7).
function BenillaCraftSkillButton_OnClick(button, mouseButton)
if mouseButton ~= "LeftButton" then return end
local index = button.craftIndex
if not index then return end
BenillaCraftFrame_SetSelection(index)
BenillaCraftFrame_Update()
end
-- The window's wheel handler (bubbled up the parent chain from a row) → one row via the faux
-- kit (TradeSkillFrame.xml's own note: the engine bubbles a spin up the parent chain, so the
-- handler lives on the window, not a child).
function BenillaCraftFrame_OnMouseWheel(delta)
BenillaFauxScrollFrame_OnMouseWheel(getglobal("BenillaCraftListScrollFrame"), delta)
end
-- ── The CollapseAll tab (ref CraftCollapseAllButton_OnClick, l.417-425) — TradeSkillFrame.xml's
-- own identical shape: toggles Expand/CollapseCraftSkillLine(0) off the button's own `.collapsed`
-- flag (re-derived every Update() above), no direct BenillaCraftFrame_Update() call (the ref
-- doesn't call one either). UNLIKE TradeSkill's own pair, `Expand`/`CollapseCraftSkillLine` are
-- true engine no-ops here (header comment, deviation 1) — no touched flag, no CRAFT_UPDATE, so a
-- click changes nothing observably beyond this button's own local state until the next Update()
-- runs some other way. ──────────────────────────────────────────────────────────────────────
function BenillaCraftCollapseAllButton_OnClick(self)
if self.collapsed then
self.collapsed = nil
ExpandCraftSkillLine(0)
else
self.collapsed = true
FauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
CollapseCraftSkillLine(0)
end
end
-- ── The detail icon (ref CraftIcon OnEnter, l.564-568 — the Ctrl/Shift dressing-room branches
-- dropped, deviation 3): SetCraftSpell, NOT SetCraftItem — Craft has no product-item concept
-- (craft.rs's own module doc). The binding behind it is a SELECTOR into the shared spell/item
-- builders (`SetCraftSpell 0x533e90`); a CREATE_ITEM recipe therefore renders the product's
-- full ITEM tooltip here even though the row's ICON is the spell's (Law D) — the two are
-- different laws on the same row, byte-verified. ─────────────────────────────────────────────
function BenillaCraftIcon_OnEnter(button)
if GetCraftSelectionIndex() ~= 0 then
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
GameTooltip:SetCraftSpell(GetCraftSelectionIndex())
GameTooltip:Show()
end
end
-- ── The reagent slots (ref CraftItemTemplate OnEnter, l.31-35 — the dressing-room/chat-link
-- OnClick branches dropped, deviation 3) ────────────────────────────────────────────────────
function BenillaCraftReagent_OnLoad(button, index)
button.reagentIndex = index
local n = button:GetName()
button.icon = getglobal(n .. "Icon")
button.name = getglobal(n .. "Name")
button.count = getglobal(n .. "Count")
end
function BenillaCraftReagent_OnEnter(button)
GameTooltip:SetOwner(button, "ANCHOR_TOPLEFT")
GameTooltip:SetCraftItem(GetCraftSelectionIndex(), button.reagentIndex)
GameTooltip:Show()
end
-- ── Create / Exit (ref l.654-670 — no Create All, no repeat-count input: deviation 8) ───────
function BenillaCraftCreateButton_OnClick()
-- Bare index → spell id, exactly the ref's own OnClick (ref l.667). An ENCHANT_ITEM recipe
-- arms an app-side item-target pick from here — this XML plays no part in that (header
-- comment, deviation 11).
DoCraft(GetCraftSelectionIndex())
end
function BenillaCraftCancelButton_OnClick()
HideUIPanel(getglobal("BenillaCraftFrame"))
end
-- ── The window lifecycle (MerchantFrame.xml/TradeSkillFrame.xml's own shape) ────────────────
function BenillaCraftFrame_OnLoad(frame)
frame:RegisterEvent("CRAFT_SHOW")
frame:RegisterEvent("CRAFT_UPDATE")
frame:RegisterEvent("CRAFT_CLOSE")
-- SPELLS_CHANGED (ref l.58/92-93): a real, non-Beast-Training event (a newly learned enchant
-- rank should refresh the list) — registered for fidelity; forward-compatible if this
-- engine doesn't fire it yet (TradeSkillFrame.xml's own UNIT_PORTRAIT_UPDATE posture).
-- UNIT_PET_TRAINING_POINTS (ref l.59) is NOT registered — nothing left to drive it
-- (deviation 6, Craft_UpdateTrainingPoints doesn't exist in v1).
frame:RegisterEvent("SPELLS_CHANGED")
frame:RegisterEvent("UNIT_PORTRAIT_UPDATE")
local scrollFrame = getglobal("BenillaCraftListScrollFrame")
end
-- Fresh session paint (ref CraftFrame_Show, l.26-49 — the header index-select branch is
-- dropped, deviation 1; the selection-reset itself is ALSO overridden, deviation 10: this
-- keeps a persisted selection instead of always forcing index 1).
function BenillaCraftFrame_Show(frame)
getglobal("BenillaCraftCreateButton"):Disable()
if GetCraftSelectionIndex() == 0 then
if GetNumCrafts() > 0 then
BenillaCraftFrame_SetSelection(1)
end
else
BenillaCraftFrame_SetSelection(GetCraftSelectionIndex())
end
FauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
SetPortraitTexture(getglobal("BenillaCraftFramePortrait"), "player")
BenillaCraftFrame_Update()
ShowUIPanel(frame)
end
function BenillaCraftFrame_OnEvent(frame)
if event == "CRAFT_SHOW" then
BenillaCraftFrame_Show(frame)
elseif event == "CRAFT_UPDATE" then
-- ref l.68-87: re-anchor the selection if it's fallen off a (possibly shorter) list.
local selection = GetCraftSelectionIndex()
if selection > 1 and selection <= GetNumCrafts() then
BenillaCraftFrame_SetSelection(selection)
elseif GetNumCrafts() > 0 then
BenillaCraftFrame_SetSelection(1)
FauxScrollFrame_SetOffset(getglobal("BenillaCraftListScrollFrame"), 0)
end
BenillaCraftFrame_Update()
elseif event == "CRAFT_CLOSE" then
HideUIPanel(frame)
elseif event == "SPELLS_CHANGED" then
BenillaCraftFrame_Update()
elseif event == "UNIT_PORTRAIT_UPDATE" then
if arg1 == "player" then
SetPortraitTexture(getglobal("BenillaCraftFramePortrait"), "player")
end
end
end
function BenillaCraftFrameCloseButton_OnClick()
HideUIPanel(getglobal("BenillaCraftFrame"))
end
]]></Script>
<!--
One list row (ref CraftButtonTemplate inherits ClassTrainerSkillButtonTemplate, l.4-28 for the
template, l.344-415 for the instances): a 293×16 text row, name + a parenthesized rank subtext
column (unlike TradeSkill's own row template, which carries no subname — this file's header
comment) — TradeSkillFrame.xml's own BenillaTradeSkillSkillButtonTemplate shape, plus the fixed
subtext column (TrainerFrame.xml's own Name/SubText fixed-column precedent, since this engine
has no FontString-width-based dynamic anchor). NO OnEnter/OnLeave — Craft's own ref adds no row
hover tooltip (header comment, deviation 7).
-->
<Button name="BenillaCraftSkillButtonTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentSubText" inherits="GameFontNormalSmall" justifyH="LEFT">
<Size><AbsDimension x="99" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="190" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<!-- THE ROW NAME IS THE BUTTON'S OWN ButtonText (the ref's `Craft<n>Text`), under the base's
font TRIO — that is the whole white-on-hover / white-on-selected mechanism, and no script
does it: `GameFontHighlight` is white, the engine swaps the highlighted instance in under
the cursor, and `LockHighlight()` holds it there for the selected row. It was a
`$parentName` FontString until decision 1605, on this file's own deviation 7 note that
`Button:SetTextColor`/`LockHighlight` had no engine method — long since untrue.
TradeSkillFrame.xml's row template is the twin, and both keep their own column geometry
(LEFT+4 / 180×16 here) rather than the base's auto-size, so the switch moved no pixel. -->
<ButtonText name="$parentText">
<Size><AbsDimension x="180" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
<DisabledFont inherits="GameFontDisable" justifyH="LEFT"/>
<Scripts>
<OnLoad>
self.subtext = getglobal(self:GetName() .. "SubText")
</OnLoad>
<OnClick>BenillaCraftSkillButton_OnClick(self, arg1)</OnClick>
</Scripts>
</Button>
<!--
One reagent slot. The ref's `CraftItemTemplate` (l.29-53) inherits `QuestItemTemplate` and
overrides **only scripts** — same as TradeSkillFrame's `TradeSkillItemTemplate`, so the two
windows' slots really are the byte-identical layout this comment always claimed; what they
share is QuestItemTemplate's shape, which neither window had until B250 (decision 1595).
A 39×39 icon, the `UI-QuestItemNameFrame` plate riding its right edge, the name centred ON the
plate, and the "have /need" count on the icon's own BOTTOMRIGHT. This file keeps its OWN copy
(StackSplit.xml's "not a cross-file inherits" note), kept in sync by hand.
-->
<Button name="BenillaCraftReagentTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="147" y="41"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon">
<Size><AbsDimension x="39" y="39"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
</Texture>
<!-- Declared AFTER the icon (same parent+level ⇒ z-tiebreak): the plate's 128×64
texture starts 10px INSIDE the icon's right edge, but its art only begins 11
texels in, so the visible plate opens 1px clear of the icon. -->
<Texture name="$parentNameFrame" file="Interface\QuestFrame\UI-QuestItemNameFrame">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentIcon" relativePoint="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
</Texture>
<FontString name="$parentName" inherits="GameFontHighlight" justifyH="LEFT">
<Size><AbsDimension x="90" y="36"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentNameFrame"><Offset><AbsDimension x="15" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentCount" inherits="NumberFontNormal" justifyH="RIGHT">
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="$parentIcon"><Offset><AbsDimension x="-4" y="1"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnter>BenillaCraftReagent_OnEnter(self)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!-- A panel action button (Create/Exit): the UIPanelButtonTemplate face locally reconstructed
(this house dialect ships no such template — TalentFrame.xml's own ENGINE API note),
TradeSkillFrame.xml's identical-shaped BenillaTradeSkillPanelButtonTemplate — its OWN local
copy per this house's per-window convention (StackSplit.xml's own "not a cross-file inherits"
note), not a cross-file reuse of that other window's virtual template. -->
<Button name="BenillaCraftPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="80" y="22"/></Size>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<Frame name="BenillaCraftFrame" hidden="true">
<!-- The real 384×512 window. No <Anchors> — ShowUIPanel/SetLeftFrame SetPoints it to the left
slot (MerchantFrame.xml/TradeSkillFrame.xml's own convention). -->
<Size><AbsDimension x="384" y="512"/></Size>
<!-- Transcribed for fidelity (ref l.65-67); no loader wiring exists yet for the hit-rect
shrink — TalentFrame.xml's own "transcribe the hook even if nothing drives it yet"
posture. -->
<HitRectInsets><AbsInset left="0" right="34" top="0" bottom="75"/></HitRectInsets>
<Layers>
<!-- "player" portrait 60×60 TOPLEFT (7,-6) (ref l.70-81), BACKGROUND so the window art's
ring frames it. Bound via SetPortraitTexture(region, "player") in Show/OnEvent — not an
NPC session (header comment). -->
<Layer level="BACKGROUND">
<Texture name="BenillaCraftFramePortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<!-- The four quadrant slabs that ARE the window art (ref l.83-125): ALL FOUR from
ClassTrainerFrame's shared art — unlike TradeSkill's own mixed set (its BotLeft comes
from TradeSkillFrame's own art instead), Craft's ref uses one uniform art family
throughout, transcribed exactly as given. -->
<Layer level="BORDER">
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"/></Anchors></Texture>
<!-- Title: GetCraftName() directly — unlike TradeSkill's "%s"-formatted TRADE_SKILL_TITLE,
Craft's ref sets this raw (ref l.101: CraftFrameTitleText:SetText(GetCraftName())).
"Enchanting" is a harmless static placeholder until the first paint. -->
<FontString name="BenillaCraftFrameTitleText" inherits="GameFontNormal" justifyH="CENTER" text="Enchanting">
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
<!-- The skill-border + horizontal-divider decorative bars (ref l.126-174), sampled from the
same two shared ClassTrainer/TradeSkill textures by TexCoord — byte-identical to
TradeSkillFrame.xml's own equivalents. -->
<Layer level="ARTWORK">
<Texture name="BenillaCraftSkillBorderLeft" file="Interface\TradeSkillFrame\UI-TradeSkill-SkillBorder">
<Size><AbsDimension x="256" y="8"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="63" y="-50"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.25"/>
</Texture>
<Texture name="BenillaCraftSkillBorderRight" file="Interface\TradeSkillFrame\UI-TradeSkill-SkillBorder">
<Size><AbsDimension x="28" y="8"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftSkillBorderLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.109375" top="0.25" bottom="0.5"/>
</Texture>
<Texture name="BenillaCraftHorizontalBarLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="256" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-221"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.25"/>
</Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="75" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftHorizontalBarLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.29296875" top="0.25" bottom="0.5"/>
</Texture>
</Layer>
<!-- The FIXED detail-pane FontStrings + header art (ref CraftDetailScrollChildFrame,
l.456-537), flattened onto this window's OWN Layers at OVERLAY (the flat-layout gap,
this file's header comment) — computed from the ref's ScrollFrame anchor (TOPLEFT
20,-234, byte-identical to TradeSkillFrame's own) + each region's own ScrollChild-
relative offset. Declaration order matters (same parent+level ⇒ z-tiebreak,
TradeSkillFrame.xml's own note): the header art is declared BEFORE ReagentLabel so the
"Reagents: " banner text paints over its lower edge, exactly as the reference does.
CraftCost (ref l.483-491, the per-recipe training-point cost line) is DROPPED — deviation 6,
always blank in v1. -->
<Layer level="OVERLAY">
<FontString name="BenillaCraftName" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="244" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="70" y="-239"/></Offset></Anchor></Anchors>
</FontString>
<!-- ONE concatenated FontString (Craft's own ref, unlike TradeSkill's split
Label+Text pair — header comment). -->
<FontString name="BenillaCraftRequirements" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraftName" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
<Texture name="BenillaCraftDetailHeaderLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-DetailHeaderLeft">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="20" y="-231"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-DetailHeaderRight">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraftDetailHeaderLeft" relativePoint="TOPRIGHT"/></Anchors>
</Texture>
<!-- The recipe's own description (ref l.516-527) — TradeSkill's detail pane has no
analogue for this region (header comment). Its BOTTOMLEFT is the ReagentLabel's
DYNAMIC anchor target, re-pointed every SetSelection (SetSelection's own Lua). -->
<FontString name="BenillaCraftDescription" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="290" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="25" y="-284"/></Offset></Anchor></Anchors>
</FontString>
<!-- STATIC always-shown label (TradeSkillFrame.xml's own precedent: the ref's
Show/Hide-on-reagent-count dance is dropped, every recipe here has ≥1 reagent in
practice) — its POSITION is dynamic (SetSelection), its text/visibility is not. -->
<FontString name="BenillaCraftReagentLabel" inherits="GameFontNormalSmall" text="Reagents: ">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraftDescription" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The top-right close X (ref a UIPanelCloseButton at TOPRIGHT -29,-8, l.688-696; the
minimize-button art stands in — this house dialect ships no such template,
TradeSkillFrame.xml's own precedent). -->
<Button name="BenillaCraftFrameCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-29" y="-8"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts><OnClick>BenillaCraftFrameCloseButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The skill-rank StatusBar (ref CraftRankFrame, l.196-264): blue fill, "name rank/max"
text, the UI-Character-Skills-Bar[Border] art — byte-identical to TradeSkillFrame's own.
SKILL_LINES_CHANGED self-listen dropped (deviation 4). -->
<StatusBar name="BenillaCraftRankFrame" drawLayer="BACKGROUND" minValue="0" maxValue="1" defaultValue="0">
<Size><AbsDimension x="268" y="15"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="73" y="-37"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="BenillaCraftRankFrameSkillName" inherits="GameFontNormalSmall">
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="6" y="1"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaCraftRankFrameSkillRank" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="128" y="0"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftRankFrameSkillName" relativePoint="RIGHT"><Offset><AbsDimension x="13" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
<Layer level="BACKGROUND">
<Texture name="BenillaCraftRankFrameBackground"><Color r="1.0" g="1.0" b="1.0" a="0.2"/></Texture>
</Layer>
</Layers>
<Frames>
<Button name="BenillaCraftRankFrameBorder">
<Size><AbsDimension x="281" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorder"/>
</Button>
</Frames>
<BarTexture name="BenillaCraftRankFrameBar" file="Interface\PaperDollInfoFrame\UI-Character-Skills-Bar"/>
<BarColor r="0.25" g="0.25" b="0.75"/>
</StatusBar>
<!-- The expand/collapse-all tab (ref CraftExpandButtonFrame, l.265-325): verbatim
geometry/art — TradeSkillFrame.xml's own identical tab frame, EXCEPT the button itself
is 22×22 here (Craft's own ref value, not TradeSkill's 40×22 — each window's own ref,
not copy-pasted). TrainerFrame.xml's own BenillaTrainerCollapseAllButton is the
reconstruction precedent this mirrors (standalone Button, not this file's row template —
header comment, deviation 1). Always ends up Hidden in v1 (Update()'s own tail, header
comment) — ships for fidelity, not yet reachable. -->
<Frame name="BenillaCraftExpandButtonFrame">
<Size><AbsDimension x="54" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-71"/></Offset></Anchor></Anchors>
<Layers><Layer level="BACKGROUND">
<Texture name="BenillaCraftExpandTabLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-ExpandTab-Left"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors></Texture>
<Texture name="BenillaCraftExpandTabMiddle" file="Interface\QuestFrame\UI-QuestLogSortTab-Middle"><Size><AbsDimension x="38" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaCraftExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLogSortTab-Right"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaCraftExpandTabMiddle" relativePoint="RIGHT"/></Anchors></Texture>
</Layer></Layers>
<Frames>
<Button name="BenillaCraftCollapseAllButton" text="All">
<Size><AbsDimension x="22" y="22"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-MinusButton-Up"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></NormalTexture>
<HighlightTexture file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></HighlightTexture>
<ButtonText><Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="18" y="0"/></Offset></Anchor></Anchors></ButtonText>
<NormalFont inherits="GameFontNormalSmall" justifyH="LEFT"/>
<Scripts>
<OnClick>BenillaCraftCollapseAllButton_OnClick(self)</OnClick>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- The persistent selection highlight (ref CraftHighlightFrame, l.331-343): 293×16,
ARTWORK-layer UI-Listbox-Highlight2. Declared BEFORE the rows so the row text paints
OVER the glow (TradeSkillFrame.xml's own z-tiebreak note). Vertex-colored by the
selected recipe's CraftTypeColor (SetSelection), positioned onto the selected visible
row (Update). -->
<Frame name="BenillaCraftHighlightFrame" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers><Layer level="ARTWORK">
<Texture name="BenillaCraftHighlight" file="Interface\Buttons\UI-Listbox-Highlight2">
<Anchors><Anchor point="TOPLEFT"/><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer></Layers>
</Frame>
<!-- 8 list rows (ref Craft1..8, l.344-415): 293×16, row1 TOPLEFT (22,-96); rows 2-8 chain
TOPLEFT→prior BOTTOMLEFT with no gap — byte-identical to TradeSkillFrame's own. -->
<Button name="BenillaCraft1" inherits="BenillaCraftSkillButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-96"/></Offset></Anchor></Anchors>
</Button>
<Button name="BenillaCraft2" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaCraft3" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaCraft4" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaCraft5" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaCraft6" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaCraft7" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaCraft8" inherits="BenillaCraftSkillButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraft7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- The faux-scroll frame (FauxScrollFrameTemplate, ScrollTemplates.xml): invisible
container over the 8 rows; only its right-hand Slider scroll bar renders. Sized to the
8-row span (CRAFTS_DISPLAYED × CRAFT_SKILL_HEIGHT), TradeSkillFrame.xml's own
same-TOPLEFT-as-row1 convention. updateFunc set in BenillaCraftFrame_OnLoad. -->
<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>
<Scripts>
<!-- The reference's kit writes `frame.offset` ONLY from here; its
`updateFunc` field does not exist, so without this the bar moves and
the list does not follow (1868). -->
<OnVerticalScroll>BenillaCraftList_OnVerticalScroll()</OnVerticalScroll>
</Scripts>
</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
identical reuse) at the flattened header art's top-left. No made-count overlay — Craft
has no min/max-made concept (craft.rs's own module doc), unlike TradeSkill's own icon. -->
<Button name="BenillaCraftIcon" hidden="true">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="28" y="-237"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\Buttons\UI-EmptySlot"><Size><AbsDimension x="64" y="64"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="-13" y="13"/></Offset></Anchor></Anchors></Texture>
</Layer>
</Layers>
<Scripts>
<OnEnter>BenillaCraftIcon_OnEnter(self)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!-- Up to 8 reagents, the ref's own 2-column grid + anchor chain (l.578-649): odd slots
chain down the left column (TOPLEFT→prior BOTTOMLEFT, -2 gap), even slots sit LEFT of
the odd slot in the same row. Reagent7 chains off Reagent6 (not Reagent5) — a literal
ref quirk shared with TradeSkillFrame's own reagent grid (this file's header comment,
deviation 9's sibling note): harmless, since 6's row-mate LEFT anchor to 5 already puts
it at 5's own row height. Reagent1's own offset is (0,-3) here — Craft's ref literal
value, NOT TradeSkill's own (-5,-3) (transcribed from each window's own ref, not
copy-pasted). -->
<Button name="BenillaCraftReagent1" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraftReagentLabel" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 1)</OnLoad></Scripts>
</Button>
<Button name="BenillaCraftReagent2" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftReagent1" relativePoint="RIGHT"/></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 2)</OnLoad></Scripts>
</Button>
<Button name="BenillaCraftReagent3" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraftReagent1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 3)</OnLoad></Scripts>
</Button>
<Button name="BenillaCraftReagent4" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftReagent3" relativePoint="RIGHT"/></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 4)</OnLoad></Scripts>
</Button>
<Button name="BenillaCraftReagent5" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraftReagent3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 5)</OnLoad></Scripts>
</Button>
<Button name="BenillaCraftReagent6" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftReagent5" relativePoint="RIGHT"/></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 6)</OnLoad></Scripts>
</Button>
<Button name="BenillaCraftReagent7" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaCraftReagent6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 7)</OnLoad></Scripts>
</Button>
<Button name="BenillaCraftReagent8" inherits="BenillaCraftReagentTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaCraftReagent7" relativePoint="RIGHT"/></Anchors>
<Scripts><OnLoad>BenillaCraftReagent_OnLoad(self, 8)</OnLoad></Scripts>
</Button>
<!-- The action row (ref l.654-696): Create / Exit only — NO Create All, NO -/+, NO
repeat-count EditBox (deviation 8). Positions byte-identical to TradeSkillFrame's own
Create/Cancel buttons (ref l.658-663/676-681 vs Craft's own l.658-663/675-680 — the
SAME two constants in both refs). -->
<Button name="BenillaCraftCreateButton" inherits="BenillaCraftPanelButtonTemplate" text="Create">
<Anchors><Anchor point="CENTER" relativeTo="BenillaCraftFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="224" y="-422"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BenillaCraftCreateButton_OnClick()</OnClick></Scripts>
</Button>
<Button name="BenillaCraftCancelButton" inherits="BenillaCraftPanelButtonTemplate" text="Exit">
<Anchors><Anchor point="CENTER" relativeTo="BenillaCraftFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="305" y="-422"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BenillaCraftCancelButton_OnClick()</OnClick></Scripts>
</Button>
</Frames>
<!-- The window lifecycle (MerchantFrame.xml/TradeSkillFrame.xml's own shape): OnHide calls
CloseCraft() for ANY hide reason. The list wheel lives on the WINDOW (the engine bubbles a
spin up the parent chain). -->
<Scripts>
<OnLoad>BenillaCraftFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaCraftFrame_OnEvent(self)</OnEvent>
<OnMouseWheel>BenillaCraftFrame_OnMouseWheel(arg1)</OnMouseWheel>
<OnShow>PlaySound("igCharacterInfoOpen")</OnShow>
<OnHide>CloseCraft(); PlaySound("igCharacterInfoClose")</OnHide>
</Scripts>
</Frame>
</Ui>
@@ -1,318 +0,0 @@
<Ui>
<!--
The dressing room (decision 1060) — ref `DressUpFrame.xml` / `DressUpFrame.lua`, quoted.
CTRL-left-click an item anywhere (bags, the paper doll, a chat link, the bank) and it lands
on your own character here, over the gear you are actually wearing. The window's own
instruction line says exactly that, in Blizzard's words:
`DRESSUP_FRAME_INSTRUCTIONS` = "CTRL-Left Click additional items to display them on your
character" (ref-GlobalStrings.lua:944) — which is also where the modifier is settled: CTRL
previews, SHIFT posts the link to chat (`ContainerFrame.lua:565-571`).
THE MODEL PANE is a booth-texture stand-in, not the ref's live `<DressUpModel>` widget —
the settled doctrine (0105/0118) and exactly what the character window's own pane does
(`CharacterFrame.xml`'s header). So the ref's three model METHODS have no widget to live on
and become the engine globals `BenillaDressUpModel_Dress/TryOn/Close/SetFacing`
(`benilla-ui/src/script/dressup.rs` has the why); `DressUpItem`/`DressUpItemLink` keep their
real global names, because every ctrl-click site in the UI calls those.
Departures, each named:
- No drag (the ref is `movable` with StartMoving/StopMovingOrSizing): benilla has no
user-moved-frame affordance yet — the window sits in the left UIPanel slot, which the ref
registers it into too (`UIParent.lua:41`, transcribed in UiPanels.xml).
- `DressUpModel:SetUnit("player")` and `:Dress()` are ONE verb here
(`BenillaDressUpModel_Dress`): our model is always the player's, so "point it at the
player" and "put the player's own gear back on it" are the same effect.
- The ref's `scale="2.0"` on the model widget is a 3D-widget zoom with no meaning for a
baked pane — the booth frames the body from its own bounds (`framing::body_frame`), the
same law the paper doll is framed by. The bake is framed at THIS pane's 316×351 aspect,
not square, or the UI's stretch would leave every character 11% too tall (decision 1069).
- The pane ANIMATES, as the ref's live widget does — Stand looping (decision 1069; the look
call 0822 §5 left open, made by the director). Its booth is TRANSPARENT for the same
reason a widget is: the dark room behind the character is the `DressUpBackground-<Race>`
art below, and Reset/Close sit on that art *inside* this pane's rect (both anchor sets
transcribed byte-identically from the ref, which is why the overlap is the ref's too).
-->
<Script><![CDATA[
-- GlobalStrings this window uses (ref-GlobalStrings.lua, cited per line; the
-- `X = X or "..."` idiom is this codebase's convention — CharacterFrame.xml/InspectFrame.xml).
DRESSUP_FRAME = DRESSUP_FRAME or "Dressing Room" -- :943
DRESSUP_FRAME_INSTRUCTIONS = DRESSUP_FRAME_INSTRUCTIONS
or "CTRL-Left Click additional items to display them on your character" -- :944
RESET = RESET or "Reset" -- :3340
CLOSE = CLOSE or "Close" -- :760
-- ref DressUpFrame.lua:2-8. The `not IsVisible()` gate is the reference's own state reset:
-- opening the room fresh re-dresses the model in the player's real gear, and only then
-- tries the clicked item on. Both halves ride ONE ordered intent queue app-side, so the
-- reset can never overtake the try-on (dressup.rs's module doc).
function DressUpItem(item)
if not DressUpFrame:IsVisible() then
ShowUIPanel(DressUpFrame)
BenillaDressUpModel_Dress() -- ref: DressUpModel:SetUnit("player")
end
BenillaDressUpModel_TryOn(item) -- ref: DressUpModel:TryOn(item)
end
-- ref DressUpFrame.lua:10-16, verbatim: pull the bare item id out of the `|Hitem:<id>:…`
-- payload and hand THAT to TryOn. Every ctrl-click site in the UI calls this one function,
-- which is why it keeps the reference's global name.
function DressUpItemLink(link)
if not link then
return
end
local item = gsub(link, ".*item:(%d+).*", "%1", 1)
DressUpItem(tonumber(item))
end
-- ref DressUpFrame.lua:18-32 including its own "-- HACK!!!" comment: gnomes borrow the
-- dwarf backdrop and trolls the orc one (there is no Gnome/Troll art), and an unknown race
-- falls back to Orc. Quoted, not corrected.
function DressUpTexturePath()
local race, fileName = UnitRace("player")
if fileName == "Gnome" or fileName == "GNOME" then
fileName = "Dwarf"
elseif fileName == "Troll" or fileName == "TROLL" then
fileName = "Orc"
end
if not fileName then
fileName = "Orc"
end
return "Interface\\DressUpFrame\\DressUpBackground-" .. fileName
end
-- ref DressUpFrame.lua:34-40. Called on OnShow as well as OnLoad: at load time the app has
-- usually not pushed the player's unit snapshot yet, so `UnitRace` answers nil and the ref's
-- own fallback would freeze an Orc backdrop onto every character (the same one-tick nil the
-- character window's level line documents).
function SetDressUpBackground()
local texture = DressUpTexturePath()
DressUpBackgroundTopLeft:SetTexture(texture .. 1)
DressUpBackgroundTopRight:SetTexture(texture .. 2)
DressUpBackgroundBotLeft:SetTexture(texture .. 3)
DressUpBackgroundBotRight:SetTexture(texture .. 4)
end
-- The pane's facing — the paper doll's pair of handlers, over this room's own booth yaw
-- (ref Model_OnLoad/Model_RotateLeft/Model_RotateRight/Model_OnUpdate, UIParent.lua:1421-1462;
-- CharacterFrame.xml carries the full note on the ref's sign quirk, quoted here too).
function BenillaDressUpModel_OnLoad(frame)
frame.facing = 0.61 -- ref UIParent.lua:1422, the literal default facing
BenillaDressUpModel_SetFacing(frame.facing)
end
function BenillaDressUpModel_RotateLeft(frame)
frame.facing = frame.facing - 0.03
BenillaDressUpModel_SetFacing(frame.facing)
PlaySound("igInventoryRotateCharacter")
end
function BenillaDressUpModel_RotateRight(frame)
frame.facing = frame.facing + 0.03
BenillaDressUpModel_SetFacing(frame.facing)
PlaySound("igInventoryRotateCharacter")
end
-- The held-button spin (ref Model_OnUpdate) — half a turn per second while a rotate button
-- reads PUSHED, the ref's own opposite-sign held branches included.
function BenillaDressUpModel_OnUpdate(frame, elapsed)
local step = elapsed * 2 * PI * ROTATIONS_PER_SECOND
if getglobal(frame:GetName() .. "RotateLeftButton"):GetButtonState() == "PUSHED" then
frame.facing = frame.facing + step
if frame.facing < 0 then
frame.facing = frame.facing + (2 * PI)
end
BenillaDressUpModel_SetFacing(frame.facing)
end
if getglobal(frame:GetName() .. "RotateRightButton"):GetButtonState() == "PUSHED" then
frame.facing = frame.facing - step
if frame.facing > (2 * PI) then
frame.facing = frame.facing - (2 * PI)
end
BenillaDressUpModel_SetFacing(frame.facing)
end
end
]]></Script>
<!-- A panel action button (Reset/Close): the ref's UIPanelButtonTemplate face, locally
reconstructed — this house dialect ships no such template, and each window keeps its own
copy (CraftFrame.xml's identical note). -->
<Button name="BenillaDressUpPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="80" y="22"/></Size>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<Frame name="DressUpFrame" enableMouse="true" parent="UIParent" toplevel="true" hidden="true">
<!-- The ref's 384×512 window. No <Anchors>: ShowUIPanel seats it in the left slot, this
house's convention for every panel window (MerchantFrame.xml/CraftFrame.xml). -->
<Size><AbsDimension x="384" y="512"/></Size>
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="45"/></HitRectInsets>
<Layers>
<!-- The "player" portrait, 60×60 at TOPLEFT (7,-6) (ref l.20-33) — BACKGROUND so the
window art's ring frames it; bound in OnShow, like every other portrait here. -->
<Layer level="BACKGROUND">
<Texture name="DressUpFramePortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<!-- The window art: the general-panel top pair + the skill-frame bottom pair, the ref's
own four slabs (l.34-72). -->
<Layer level="ARTWORK">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="256" y="0"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\SkillFrame-BotLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-256"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\SkillFrame-BotRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="256" y="-256"/></Offset></Anchor></Anchors>
</Texture>
<FontString name="DressUpFrameTitleText" inherits="GameFontHighlight" text="DRESSUP_FRAME">
<Size><AbsDimension x="300" y="14"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="DressUpFrameDescriptionText" inherits="GameFontNormalSmall" text="DRESSUP_FRAME_INSTRUCTIONS">
<Size><AbsDimension x="260" y="36"/></Size>
<Anchors>
<Anchor point="CENTER" relativeTo="DressUpFrameTitleText" relativePoint="BOTTOM"><Offset><AbsDimension x="10" y="-22"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<!-- The race backdrop behind the model, four quadrants (ref l.73-118); the files are
chosen per race by SetDressUpBackground above. -->
<Layer level="OVERLAY">
<Texture name="DressUpBackgroundTopLeft">
<Size><AbsDimension x="256" y="255"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-76"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="DressUpBackgroundTopRight">
<Size><AbsDimension x="62" y="255"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft" relativePoint="TOPRIGHT"/></Anchors>
</Texture>
<Texture name="DressUpBackgroundBotLeft">
<Size><AbsDimension x="256" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="DressUpBackgroundBotRight">
<Size><AbsDimension x="62" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The corner close X: the ref's UIPanelCloseButton seat (CENTER of TOPRIGHT, -46,-25 —
ref l.120-127) on the minimize-button art every close button in this house uses. -->
<Button name="DressUpFrameCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-46" y="-25"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts>
<OnClick>HideUIPanel(DressUpFrame)</OnClick>
</Scripts>
</Button>
<!-- Close (ref l.128-146) and Reset (l.147-165, chained off Close's LEFT). Reset is the
ref's `DressUpModel:Dress()` + its gsTitleOptionOK click. -->
<Button name="DressUpFrameCancelButton" inherits="BenillaDressUpPanelButtonTemplate" text="CLOSE">
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="305" y="-422"/></Offset></Anchor></Anchors>
<Scripts>
<OnClick>HideUIPanel(DressUpFrame)</OnClick>
</Scripts>
</Button>
<Button name="DressUpFrameResetButton" inherits="BenillaDressUpPanelButtonTemplate" text="RESET">
<Anchors><Anchor point="RIGHT" relativeTo="DressUpFrameCancelButton" relativePoint="LEFT"/></Anchors>
<Scripts>
<OnClick>
BenillaDressUpModel_Dress()
PlaySound("gsTitleOptionOK")
</OnClick>
</Scripts>
</Button>
<!-- The model pane (ref the <DressUpModel> widget, l.166-207): 316×351 anchored BOTTOM
(-11,+85). A plain Frame sampling the "dressup" booth bake (header). -->
<Frame name="DressUpModelFrame">
<Size><AbsDimension x="316" y="351"/></Size>
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="-11" y="85"/></Offset></Anchor></Anchors>
<Layers>
<!-- BACKGROUND, not ARTWORK — the booth quad is an OPAQUE stand-in for a widget
the reference renders outside the layer batches, so it must sit as low as a
region goes or it swallows same-level siblings (decision 1070; the pet
page's diet icon is where that bit). This window is the one where the
change is visible: the CLOSE/RESET buttons are declared BEFORE the pane and
overlap its lower edge, so at ARTWORK the pane was painting over them. -->
<Layer level="BACKGROUND">
<Texture name="$parentTexture">
<Size><AbsDimension x="316" y="351"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- Rotate buttons (ref l.183-206): 35×35 at the WINDOW's TOPLEFT (21,-75), left
then right chained off its TOPRIGHT. Both edges registered, so one tap fires
OnClick twice and holding hands the spin to the pane's OnUpdate — the ref's
own tap-and-hold (decision 0638 §3). -->
<Button name="DressUpModelFrameRotateLeftButton">
<Size><AbsDimension x="35" y="35"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="21" y="-75"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-RotationLeft-Button-Up"/>
<PushedTexture file="Interface\Buttons\UI-RotationLeft-Button-Down"/>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
<Scripts>
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
<OnClick>BenillaDressUpModel_RotateLeft(getglobal("DressUpModelFrame"))</OnClick>
</Scripts>
</Button>
<Button name="DressUpModelFrameRotateRightButton">
<Size><AbsDimension x="35" y="35"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="DressUpModelFrameRotateLeftButton" relativePoint="TOPRIGHT"/></Anchors>
<NormalTexture file="Interface\Buttons\UI-RotationRight-Button-Up"/>
<PushedTexture file="Interface\Buttons\UI-RotationRight-Button-Down"/>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
<Scripts>
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
<OnClick>BenillaDressUpModel_RotateRight(getglobal("DressUpModelFrame"))</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
BenillaDressUpModel_OnLoad(self)
BenillaSetBoothTexture(getglobal(self:GetName() .. "Texture"), "dressup")
</OnLoad>
<OnUpdate>BenillaDressUpModel_OnUpdate(self, arg1)</OnUpdate>
</Scripts>
</Frame>
</Frames>
<Scripts>
<OnLoad>SetDressUpBackground()</OnLoad>
<OnShow>
SetDressUpBackground()
SetPortraitTexture(DressUpFramePortrait, "player")
PlaySound("igCharacterInfoOpen")
</OnShow>
<!-- Closing empties the booth (dressup.rs's `Close` intent): the ref's widget keeps its
state while hidden, but its next DressUpItem re-issues SetUnit("player") *because*
the frame wasn't visible, so that state is never observable. -->
<OnHide>
BenillaDressUpModel_Close()
PlaySound("igCharacterInfoClose")
</OnHide>
</Scripts>
</Frame>
</Ui>
@@ -1,567 +0,0 @@
<Ui>
<!--
The GameTooltip (decision 0274): a real <GameTooltip> engine widget — the line stack,
SetOwner anchor law, auto-size + right-flush, IsOwned, FadeOut, and the OnTooltipCleared/
OnTooltipAddMoney script protocol all live engine-side (benilla-ui script/tooltip.rs); the
line pairs are engine-created named regions (GameTooltipTextLeftN/RightN — real globals,
like the template pairs reference Lua addresses by name). This file declares the two
TEMPLATES and their instances: the plate (backdrop art + colors QUOTED from
ref-GameTooltip.xml/.lua, line cites inline), the money coin trio, and the Lua that is
genuinely FrameXML's in the real client (GameTooltip_SetDefaultAnchor,
GameTooltip_AddNewbieTip, SetTooltipMoney) — plus the P0 item-stat renderer, which
moves engine-side in 0274 P1 with the full template feed.
The frame's global name is the real one — GameTooltip — so transcribed reference code
(MinimapCluster's zone hover, UnitFrame's TextLeft1 recolor) works verbatim.
The reference splits this across two files — Interface\FrameXML\GameTooltipTemplate.xml
holds the virtual GameTooltipTemplate (l.3-627) and ShoppingTooltipTemplate (l.628-1217);
GameTooltip.xml <Include>s it and declares only the instances. Both templates live here
instead, ABOVE their instances, because that is the whole of their reach: the tooltip file
already loads before every window that hovers, and keeping them in one file means the
template is registered by the time the instances (and, later, ItemRef.xml's) expand it
without a second manifest entry to keep in order. Same registry, same splice, one file.
-->
<Script><![CDATA[
-- Tooltip default colours (VERIFIED, ref-GameTooltip.lua l.3-4). The border is white, the
-- background the dark blue tint; applied to the Backdrop in OnLoad exactly as the real
-- GameTooltip_OnLoad does (ref-GameTooltip.lua l.79-82).
TOOLTIP_DEFAULT_COLOR = { r = 1, g = 1, b = 1 }
TOOLTIP_DEFAULT_BACKGROUND_COLOR = { r = 0.09, g = 0.09, b = 0.19 }
-- GameTooltip_SetDefaultAnchor's position math (ref-GameTooltip.lua l.6,73-77): the
-- bottom-right screen corner, -CONTAINER_OFFSET_X - 13 in x, CONTAINER_OFFSET_Y in y.
DEFAULT_TOOLTIP_POSITION = -13
-- The shopping-compare pair count (ref GameTooltip.lua l.7; PaperDollFrame's listener
-- bounds arg2 by it).
NUM_SHOPPING_TOOLTIPS = 2
-- "Show Detailed Tooltips" — ref UIOptionsFrame.lua l.100-101, inside UIOptionsFrame_Init:
-- SHOW_NEWBIE_TIPS = "1"; RegisterForSave("SHOW_NEWBIE_TIPS");
-- It is a FrameXML *uvar*, not a CVar, and 1.12 ships it ON — so the detailed second line
-- IS the default reference tooltip, not an opt-in. (An earlier note in this corpus had this
-- backwards; decision 0661.) The options window's Interface page drives it now
-- (decision 1136), and the assignment below is the ONLY statement of the default: the saved
-- chunk executes over it at load (1128) and the row captures it as its Defaults value at
-- OnLoad — which runs first — so "1" is written down in exactly one place, here.
SHOW_NEWBIE_TIPS = "1"
RegisterForSave("SHOW_NEWBIE_TIPS")
-- The bracketed name out of an item hyperlink "|cff…|Hitem:117|h[Tough Jerky]|h|r"; if the
-- string has no bracket it is already a plain name (the merchant/loot rows hand plain names).
function BenillaTooltip_ItemName(link)
if not link then return nil end
local _, _, name = strfind(link, "%[(.-)%]")
return name or link
end
-- The shared quality→colour table, `ITEM_QUALITY_COLORS` — Fonts.xml's, matching
-- `ui_items.rs`'s `quality_color` and the client's `GetItemQualityColor`. Unknown/nil
-- quality falls back to common white. All FrameXML files load before any hover fires, so
-- the table is defined by the time this runs regardless of file order.
--
-- This read `BENILLA_LOOT_QUALITY_COLORS` until 1751 took the loot window to the player's
-- chain. That name was only ever an alias our own `LootFrame.xml` set to this same table
-- ("the local name kept for this file's existing readers"), and the stock file does not
-- set it — so the alias went from harmless indirection to a nil the moment its definer
-- stopped shipping. The `t and` guard would have made that silent: every tooltip would
-- have quietly gone white.
function BenillaTooltip_QualityColor(quality)
local t = ITEM_QUALITY_COLORS
local c = (t and (t[quality] or t[1])) or { r = 1, g = 1, b = 1 }
return c.r, c.g, c.b
end
-- Copper → "Ng Ms Kc" text for a price/value line (only the non-zero high coins, always the
-- copper). math.floor if present, else a truncation fallback.
function BenillaTooltip_MoneyString(copper)
copper = copper or 0
local mfloor = (math and math.floor) or function(x) return x - (x % 1) end
local g = mfloor(copper / 10000)
local s = mfloor((copper - g * 10000) / 100)
local c = copper - g * 10000 - s * 100
local out = ""
if g > 0 then out = g .. "g " end
if s > 0 or g > 0 then out = out .. s .. "s " end
return out .. c .. "c"
end
-- The default anchor (ref-GameTooltip.lua l.73-77, quoted): owner with no auto-anchor,
-- then the bottom-right screen corner, above the bag row - CONTAINER_OFFSET_X/Y are the
-- manage pass's live values (UIParent.xml). The engine's ANCHOR_NONE drops the previous
-- owner anchor (tooltip.rs SetOwner), so no ClearAllPoints - the ref has none either.
function GameTooltip_SetDefaultAnchor(tooltip, parent)
tooltip:SetOwner(parent, "ANCHOR_NONE")
-- Read the two managed globals FRESH, and fall back AT USE rather than assigning a
-- copy: this file used to open with `CONTAINER_OFFSET_X = CONTAINER_OFFSET_X or 0`,
-- which is a second written statement of numbers UIParent.xml owns. Harmless in the
-- shipped load order (UIParent.xml is manifest 42, this file later) but it is the same
-- shape as the shadow pair that froze the bag stack at the no-bars corner, so the law
-- of 1499 forbids it outright: nothing outside UIParent.xml assigns a managed offset.
-- The `or` defaults below cover the tooltip-only harnesses that load no UIParent.
local offsetX = CONTAINER_OFFSET_X or 0
local offsetY = CONTAINER_OFFSET_Y or 70
tooltip:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT",
-offsetX + DEFAULT_TOOLTIP_POSITION, offsetY)
tooltip.default = 1
end
-- The two-line UI-element tooltip (spec: ref-GameTooltip.lua l.107-123): a white label,
-- then the long explanation wrapped underneath in NORMAL_FONT_COLOR — the whole reason a
-- 1.12 hover over the ping meter or a micro button is a paragraph and not four words.
-- With detailed tips on it seats at the DEFAULT corner, not beside the owner; with them off
-- it collapses to the bare label at the owner's right. `noNormalText` says "the explanation
-- is the whole tooltip" (the XP bar) — with tips off such a hover shows nothing at all.
--
-- Signature note: the ref reads the firing frame from the `this` global; the corpus
-- convention is to keep the ref NAME and take that frame as a leading parameter
-- (GameTooltip_OnHide(self) — same shift; GameTooltip_ClearMoney went back to the reference's no-argument form in 1937). Reference code
-- pasted in verbatim must therefore have the owner threaded in as arg 1.
-- **The reference's exact signature, because a signature IS api (1199).** Ours used to take
-- the owner as arg 1; the reference takes none and reads `this`. Outfitter calls it the
-- reference's way (`OutfitterMinimapButton:OnEnter` -> `GameTooltip_AddNewbieTip(text, ...)`),
-- so its TEXT landed in our owner slot and `SetOwner` raised
-- "bad argument #2: error converting Lua string to table". Found by the use-probe, not by
-- reading: it only fires when something hovers.
--
-- Our own callers mostly hold the owner in a local that is NOT the firing frame (`bar`,
-- `button`, `frame`), so they use the explicit-owner variant below rather than `this`.
function GameTooltip_AddNewbieTip(normalText, r, g, b, newbieText, noNormalText)
BenillaTooltip_AddNewbieTipFor(this, normalText, r, g, b, newbieText, noNormalText)
end
-- The explicit-owner form our FrameXML uses, and where the behaviour above actually lives:
-- the owner is threaded in rather than read off `this`. Kept under a Benilla name so it can
-- never be mistaken for a reference global an addon might call or replace.
function BenillaTooltip_AddNewbieTipFor(owner, normalText, r, g, b, newbieText, noNormalText)
-- Detailed tips OFF is the short shape: the label alone, beside whatever it labels.
-- A caller that says its label is worthless on its own then has nothing left to show.
if SHOW_NEWBIE_TIPS ~= "1" then
if not noNormalText then
GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
GameTooltip:SetText(normalText, r, g, b)
end
return
end
-- Detailed tips ON: a paragraph, and paragraphs go to the screen corner instead of
-- crowding the thing they explain.
GameTooltip_SetDefaultAnchor(GameTooltip, owner)
if normalText then
-- Label in the caller's colour; the explanation under it, wrapped, always in
-- NORMAL_FONT_COLOR — the two lines are deliberately not the same colour.
GameTooltip:SetText(normalText, r, g, b)
GameTooltip:AddLine(newbieText, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
else
-- No label, so the explanation IS the tooltip: it takes the caller's colour, plus
-- SetText's alpha and wrap flags (args 5 and 6) — the wrap the second line above
-- would otherwise have carried.
GameTooltip:SetText(newbieText, r, g, b, 1, 1)
end
GameTooltip:Show()
end
-- ref GameTooltip_OnHide (ref-GameTooltip.lua l.100-104, quoted): the plate colours
-- reset for the next owner and the default-corner flag drops. The ref's OnHide also
-- hides ShoppingTooltip1/2 - that pair lands with the compare-tooltip slice (0274 P4).
--
-- `self or this`: the corpus calls this the REFERENCE way, with no argument at all
-- (TipBuddy.xml l.1412, atsw.xml l.3420, and TipBuddy.lua l.2520 which REPLACES the global
-- and calls the saved original bare). The signature note above explains the corpus-wide
-- shift to a leading parameter; nothing lets us edit an addon, and `this` is exactly what
-- the reference reads here, so the two spellings mean the same thing.
function GameTooltip_OnHide(self)
self = self or this
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
self.default = nil
end
-- The embedded money row — the ref's SetTooltipMoney (ref-GameTooltip.lua l.84-92) over our
-- shared coin kit (BenillaMoney_Set, MerchantFrame.xml): a blank line, the coin trio seated
-- at the money line's LEFT +4, and the money's width as the tooltip's minimum width. The
-- engine fires OnTooltipAddMoney with the computed price (wow-re ui/scratch/tooltip-money.md
-- 0x52b650@0x52e376: the engine computes, FrameXML renders) and this is its handler; Lua
-- call sites (repair cost) call it directly, exactly like the ref's ContainerFrame does.
-- ref GameTooltip.lua:84-92, verbatim, on the reference's own widget (1937). The money row
-- is the $parentMoneyFrame SmallMoneyFrameTemplate instance below, not a coin trio.
function SetTooltipMoney(frame, money)
frame:AddLine(" ", 1.0, 1.0, 1.0)
local numLines = frame:NumLines()
local moneyFrame = getglobal(frame:GetName() .. "MoneyFrame")
moneyFrame:SetPoint("LEFT", frame:GetName() .. "TextLeft" .. numLines, "LEFT", 4, 0)
moneyFrame:Show()
MoneyFrame_Update(moneyFrame:GetName(), money)
frame:SetMinimumWidth(moneyFrame:GetWidth())
end
-- The money row clears whenever the content does (the ref wires GameTooltip_ClearMoney to
-- OnTooltipCleared; the engine fires it on ClearLines/SetOwner/Hide). BenillaMoney_Clear
-- ships in MerchantFrame.xml (the money kit's home) — guard for engine tests that load
-- this file alone; in the app every file is loaded.
-- ref GameTooltip.lua:94-99, verbatim: reads `this`, takes no argument. The one XML
-- caller (OnTooltipCleared) is a handler, so `this` IS the tooltip.
function GameTooltip_ClearMoney()
local moneyFrame = getglobal(this:GetName() .. "MoneyFrame")
if moneyFrame then
moneyFrame:Hide()
end
end
-- The item content channels (SetItemById/SetBagItem/SetMerchantItem/SetBuybackItem) are
-- ENGINE methods on the GameTooltip widget (benilla-ui script/tooltip_item.rs, decision
-- 0274 P1): one shared renderer — the real client's own shape (0x52b650 behind 8 of the
-- 9 Set*Item bindings, wow-re ui/scratch/tooltip-money.md; decision 0126's one-renderer
-- law) — over the full item-template store, with the red failed-requirement law and the
-- engine-fired sell-price money row (merchant open + real instance + repair off).
-- The reaction color table (ref ReputationFrame.lua l.3-12, quoted) — the unit-name
-- recolor's palette; guarded so a later reputation window's own definition wins.
FACTION_BAR_COLORS = FACTION_BAR_COLORS or {
[1] = { r = 0.8, g = 0.3, b = 0.22 },
[2] = { r = 0.8, g = 0.3, b = 0.22 },
[3] = { r = 0.75, g = 0.27, b = 0 },
[4] = { r = 0.9, g = 0.7, b = 0 },
[5] = { r = 0, g = 0.6, b = 0.1 },
[6] = { r = 0, g = 0.6, b = 0.1 },
[7] = { r = 0, g = 0.6, b = 0.1 },
[8] = { r = 0, g = 0.6, b = 0.1 },
}
-- GameTooltip_UnitColor (ref-GameTooltip.lua l.8-71): creatures color by reaction;
-- players read white (the ref's own shipping state — its hostile/PvP player branches
-- are the FACTION_BAR_COLORS reds/greens, kept for when PvP states stream). The engine
-- prints the name gold and fires UPDATE_MOUSEOVER_UNIT; this recolors line 1 (the
-- byte-verified split — the name COLOR is FrameXML's, decision 0276).
function GameTooltip_UnitColor(unit)
local reaction = UnitReaction(unit, "player")
if reaction and FACTION_BAR_COLORS[reaction] then
local c = FACTION_BAR_COLORS[reaction]
return c.r, c.g, c.b
end
return 1.0, 1.0, 1.0
end
-- The health bar's own fill colour (spec: ref-HealthBar.lua, the whole file).
--
-- Two colourings in one handler. `smooth` asks for the health RAMP — green at the top of
-- the range, red at the bottom, and between them a two-segment fade that pins one channel
-- at full and slides the other, so the halfway mark reads solid yellow. Without it the bar
-- is flat green whatever it reads. Blue is 0 either way, which is what keeps the fade
-- inside the red-yellow-green wedge and never lets it wash out.
--
-- **Nothing passes `smooth` — not us and not the reference.** Verified: our only caller is
-- the status bar's `<OnValueChanged>` below (one argument), and 1.12's own two callers are
-- ref-GameTooltipTemplate.xml l.593 and ref-UnitFrame.lua l.181, both of which forward
-- `value` alone. So the ramp has never coloured a bar in this game; it is kept because it
-- is the contract an addon replacing a unit frame may still call with two arguments.
--
-- A value outside the bar's own range leaves the colour untouched rather than being clamped
-- to an end of the ramp — the reference's own guard, kept as the reference has it.
function HealthBar_OnValueChanged(value, smooth)
if not value then return end
local bar = this
local min, max = bar:GetMinMaxValues()
if value < min or value > max then return end
local r, g = 0.0, 1.0
if smooth then
local filled = 0
if max > min then filled = (value - min) / (max - min) end
if filled > 0.5 then
r, g = (1.0 - filled) * 2, 1.0
else
r, g = 1.0, filled * 2
end
end
bar:SetStatusBarColor(r, g, 0.0)
end
-- `self or this` for the same reason as GameTooltip_OnHide: an addon inheriting
-- GameTooltipTemplate re-declares <OnLoad> to add its own line and opens it with the
-- reference's bare `GameTooltip_OnLoad();` (TipBuddy.xml l.1402, atsw.xml l.3417) — a nil
-- self there is a load ERROR, and the plate it was about to tint never gets its colours.
-- The body is the ref's two lines and NOTHING else. It used to end with a `self:Hide()` we
-- had added, which the reference (l.79-82, the whole function) does not have — inert for
-- our four tooltips, since every one of them carries `hidden="true"` and the loader's
-- attribute pass runs Hide() long before OnLoad fires, but NOT inert for an addon: two of
-- the 27 declare `hidden="false"` on purpose (PowerAuras.xml l.20, SimpleActionSets.xml
-- l.349) and never call Show(), so the extra line hid a tooltip the real client leaves up.
-- A helper that quietly does more than the reference's is exactly the kind of thing that
-- only shows up when someone else's code calls it.
function GameTooltip_OnLoad(self)
self = self or this
-- The plate colours (ref GameTooltip_OnLoad, ref-GameTooltip.lua l.79-82).
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
end
]]></Script>
<!--
GameTooltipTemplate (ref-GameTooltipTemplate.xml l.3-627) — the shared tooltip SHAPE, and
the single most-wanted template in the addon corpus: 27 addons name it in an XML
`inherits=` (TipBuddy, AtlasLoot, CT_BarMod, MikScrollingBattleText, TheoryCraft, …) and
two more reach it through CreateFrame("GameTooltip", name, nil, "GameTooltipTemplate").
Every one of those declarations is a <GameTooltip> element, so the template's job is to
hand them the PLATE and the STATUS BAR; the line pairs they then read
(`getglobal(tip:GetName().."TextLeft1")`) are engine-created per instance and can never
carry the template's name.
What is here is what the reference's template carries and all our instances shared, moved
verbatim from the GameTooltip instance below: the backdrop plate, the Thicken deviation
(its rationale note travels WITH it — this is the shared plate, not one window's), the
$parentStatusBar, and the OnLoad/OnHide pair (ref l.611-616).
What is deliberately NOT here, and stays on the GameTooltip instance:
- The money row and the OnTooltipAddMoney/OnTooltipCleared pair. Since 1937 the row IS the
reference's widget (a $parentMoneyFrame SmallMoneyFrameTemplate), but it still lives on
the GameTooltip INSTANCE, where the reference's template carries it (l.598-608) so that
ItemRefTooltip and every addon tooltip get the coin line too. That hoist is the remaining
divergence here. It used to wait for the director; the 2026-09-03 ruling (faithful wins)
resolves it, and it is its own unit because 27 corpus addons inherit this template.
- OnTooltipSetDefaultAnchor (ref l.617-619). The engine fires it by name on `GameTooltip`
alone (script/tooltip_unit.rs looks the frame up), so on any other instance it is dead
weight.
`clampedToScreen="true"` is the reference's own attribute and is a no-op for us: decision
0352 made the flag default-ON for every GameTooltip-kind frame, because the reference plate
observably clamps though its XML never sets it. Kept because it is what the reference says.
-->
<GameTooltip name="GameTooltipTemplate" frameStrata="TOOLTIP" clampedToScreen="true" hidden="true" virtual="true">
<!-- The tooltip plate (ref-GameTooltipTemplate.xml l.4-14 / ref-GameTooltip.xml l.33-43):
UI-Tooltip-Background tiled, UI-Tooltip-Border edge, EdgeSize 16, TileSize 16, insets
5/5/5/5. Colours are set in OnLoad (SetBackdropColor/BorderColor) — no <Color> here, so
the plate starts ctor-white and OnLoad tints it. -->
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize><AbsValue val="16"/></EdgeSize>
<TileSize><AbsValue val="16"/></TileSize>
<BackgroundInsets><AbsInset left="5" right="5" top="5" bottom="5"/></BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="BACKGROUND">
<!-- DELIBERATE deviation (director's call): the real plate is UI-Tooltip-Background's
baked 187/255 alpha (~73% opaque — measured from the BLP; the tint can only
lower it) and read too see-through here. This solid fill in the same navy,
inset to the backdrop's 5px background insets, thickens the plate to ~84%
against the world. The `a` is the knob; delete the region for the raw quote. -->
<Texture name="$parentThicken">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="5" y="-5"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="5"/></Offset></Anchor>
</Anchors>
<Color r="0.09" g="0.09" b="0.19" a="0.4"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- **The 30 pre-declared line pairs, exactly as ref-GameTooltipTemplate.xml l.17-627.**
They exist so `GameTooltipTextLeft1` is a REAL GLOBAL FROM LOAD, hidden, before any
line has been added. Ours were engine-created purely on demand, so cold they were
nil — and the corpus pattern that breaks on is a GUARD, not a read:
`Participant/Resurrection.lua:294` and `CT_RaidAssist/CT_RADetectSpells.lua:47` both
say `GameTooltipTextLeft1:IsVisible()` to ask whether the tooltip is showing
anything. On the reference that is safe and answers false; for us it raised on the
check itself. 7 corpus addons index the name (decision 1220).
The engine ADOPTS these (`script::tooltip::ensure_lines` — the same path
ShoppingTooltipTemplate's ladder already uses): their faces ride, the engine owns
justify, visibility and anchors, so no anchors are declared here. Faces match what
`ensure_lines` picks for a created line — header for pair 1, GameTooltipText after —
which is why this changes nothing on screen. `NumLines()` is `num_lines`, not the
pool size, and `layout_tooltips` sizes from `num_lines`, so a declared-but-unfilled
pair adds no height and no width. -->
<FontString name="$parentTextLeft1" inherits="GameTooltipHeaderText" hidden="true"/>
<FontString name="$parentTextRight1" inherits="GameTooltipHeaderText" hidden="true"/>
<FontString name="$parentTextLeft2" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight2" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft3" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight3" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft4" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight4" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft5" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight5" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft6" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight6" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft7" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight7" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft8" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight8" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft9" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight9" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft10" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight10" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft11" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight11" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft12" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight12" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft13" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight13" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft14" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight14" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft15" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight15" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft16" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight16" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft17" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight17" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft18" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight18" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft19" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight19" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft20" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight20" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft21" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight21" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft22" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight22" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft23" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight23" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft24" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight24" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft25" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight25" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft26" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight26" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft27" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight27" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft28" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight28" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft29" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight29" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextLeft30" inherits="GameTooltipText" hidden="true"/>
<FontString name="$parentTextRight30" inherits="GameTooltipText" hidden="true"/>
</Layer>
</Layers>
<Frames>
<!-- The mouseover health bar (ref-GameTooltipTemplate.xml l.575-597, quoted): 8px
tall, hanging below the plate, UI-TargetingFrame-BarFill, colored by
HealthBar_OnValueChanged. The ENGINE drives min/max/value from the live unit
snapshot (the byte law's HEALTH watcher — SetUnit + every set_unit push), finding
it by the `<name>StatusBar` global — so it has to be the TEMPLATE's, not one
instance's: TipBuddy replaces the whole unit tooltip and both `parent=` and
`relativeTo=` a `TipBuddyTooltipStatusBar` it never declares
(TipBuddy.xml l.2320, l.2591) — a name only this element can publish. -->
<StatusBar name="$parentStatusBar" hidden="true">
<Size><AbsDimension x="0" y="8"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="-1"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnValueChanged>HealthBar_OnValueChanged(arg1)</OnValueChanged>
</Scripts>
<BarTexture name="$parentTexture" file="Interface\TargetingFrame\UI-TargetingFrame-BarFill"/>
</StatusBar>
</Frames>
<Scripts>
<!-- ref-GameTooltipTemplate.xml l.611-616. An instance that needs more re-declares the
handler and calls the same helper first (the reference's own shape, and ours:
GameTooltip adds its event registration, ItemRefTooltip its padding) — the loader
walks every <Scripts> block in splice order, so the instance's SetScript lands
last and wins. -->
<OnLoad>GameTooltip_OnLoad(self)</OnLoad>
<OnHide>GameTooltip_OnHide(this)</OnHide>
</Scripts>
</GameTooltip>
<GameTooltip name="GameTooltip" inherits="GameTooltipTemplate" parent="UIParent" frameStrata="TOOLTIP" hidden="true">
<!-- A default size so the plate resolves the first frame it is shown; the engine's
auto-size pass takes over once the lines are measured (one frame's latency). NO
load-time anchor (the ref instance carries none): every show path seats the plate
first - SetOwner's anchor law for owned hovers, OnTooltipSetDefaultAnchor for world
hovers - and a leftover static anchor is exactly what parked the world tooltip at
screen center when the default-anchor script wasn't wired. -->
<Size><AbsDimension x="120" y="32"/></Size>
<Frames>
<!-- The reference's own money row (GameTooltipTemplate.xml:598-607): ONE SmallMoneyFrameTemplate
instance, hidden until SetTooltipMoney seats and shows it. Replaces our three hand-built
$parentMoneyCoin1..3 — the System B shape 1937 retires from this window. -->
<Frame name="$parentMoneyFrame" inherits="SmallMoneyFrameTemplate" hidden="true">
<Scripts><OnLoad>SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("STATIC");</OnLoad></Scripts>
</Frame>
</Frames>
<Scripts>
<OnLoad>
GameTooltip_OnLoad(self)
self:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
</OnLoad>
<!-- The engine renders the mouseover unit's lines gold and fires this; the recolor by
reaction is FrameXML's half (ref-GameTooltip.xml l.12-16, quoted). -->
<OnEvent>
if event == "UPDATE_MOUSEOVER_UNIT" then
getglobal(this:GetName() .. "TextLeft1"):SetTextColor(GameTooltip_UnitColor("mouseover"))
end
</OnEvent>
<!-- The default corner for world hovers (ref-GameTooltipTemplate.xml l.617-619,
quoted): the engine fires this before rendering a mouseover unit/object, and
the handler seats the plate at the screen's bottom-right. THE wiring whose
absence parked the world tooltip on the character. -->
<OnTooltipSetDefaultAnchor>GameTooltip_SetDefaultAnchor(this, UIParent)</OnTooltipSetDefaultAnchor>
<!-- ref GameTooltip.xml OnHide (l.18-22, quoted): the color reset + the compare pair
drops with its anchor host. -->
<OnHide>
GameTooltip_OnHide(this)
ShoppingTooltip1:Hide()
ShoppingTooltip2:Hide()
</OnHide>
<!-- The engine-fired money protocol (ref-GameTooltipTemplate.xml l.620-625): the engine
computes a price and fires OnTooltipAddMoney(arg1 = copper); OnTooltipCleared fires
whenever the content clears (ClearLines/SetOwner/Hide) and drops the coin row. -->
<OnTooltipAddMoney>SetTooltipMoney(this, arg1)</OnTooltipAddMoney>
<OnTooltipCleared>GameTooltip_ClearMoney()</OnTooltipCleared>
</Scripts>
</GameTooltip>
<!--
ShoppingTooltipTemplate (ref-GameTooltipTemplate.xml l.628-1217) — the shopping-compare
pair's own template, a SEPARATE virtual from GameTooltipTemplate exactly as the reference
has it (its instances, ref-GameTooltip.xml l.24-25, inherit this and not the main one).
It repeats the plate rather than inheriting it because the reference's does, and because
inheriting GameTooltipTemplate would hand the compare plates its OnHide, its status bar and
(later) its money row — none of which the compare cut has.
The declared TextLeft/Right1-4 pairs are this template's own small-font ladder, faces
quoted verbatim (including its odd GameFontNormal cells) — the engine ADOPTS declared pairs
and clones the last pair's faces for lines it grows past them (tooltip.rs ensure_lines).
Same plate as the main tooltip, including the Thicken deviation (its rationale is on
GameTooltipTemplate above); no coin trio — the compare cut ends before money.
No addon in the corpus names this template (0 of 218, against GameTooltipTemplate's 27), so
it earns its keep as OUR de-duplication, not as an addon surface: the two instances below
were byte-identical apart from the name.
-->
<GameTooltip name="ShoppingTooltipTemplate" frameStrata="TOOLTIP" hidden="true" virtual="true">
<Size><AbsDimension x="120" y="32"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize><AbsValue val="16"/></EdgeSize>
<TileSize><AbsValue val="16"/></TileSize>
<BackgroundInsets><AbsInset left="5" right="5" top="5" bottom="5"/></BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentThicken">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="5" y="-5"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="5"/></Offset></Anchor>
</Anchors>
<Color r="0.09" g="0.09" b="0.19" a="0.4"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentTextLeft1" inherits="GameFontNormalSmall" hidden="true"/>
<FontString name="$parentTextRight1" inherits="GameFontNormal" hidden="true"/>
<FontString name="$parentTextLeft2" inherits="GameFontNormal" hidden="true"/>
<FontString name="$parentTextRight2" inherits="GameFontHighlightSmall" hidden="true"/>
<FontString name="$parentTextLeft3" inherits="GameFontHighlightSmall" hidden="true"/>
<FontString name="$parentTextRight3" inherits="GameFontHighlightSmall" hidden="true"/>
<FontString name="$parentTextLeft4" inherits="GameTooltipTextSmall" hidden="true"/>
<FontString name="$parentTextRight4" inherits="GameFontHighlightSmall" hidden="true"/>
</Layer>
</Layers>
<Scripts>
<OnLoad>GameTooltip_OnLoad(self)</OnLoad>
</Scripts>
</GameTooltip>
<!-- The shopping-compare pair (ref GameTooltip.xml l.24-25, quoted down to the one-line shape):
two more GameTooltip widgets the engine seats on the paperdoll slot when
SHOW_COMPARE_TOOLTIP fires (CharacterFrame.xml's listener). -->
<GameTooltip name="ShoppingTooltip1" inherits="ShoppingTooltipTemplate" parent="UIParent" frameStrata="TOOLTIP" hidden="true"/>
<GameTooltip name="ShoppingTooltip2" inherits="ShoppingTooltipTemplate" parent="UIParent" frameStrata="TOOLTIP" hidden="true"/>
</Ui>
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -185,12 +185,12 @@
return button.label
end
-- ref MainMenuBarMicroButton's OnEnter (l.13), verbatim bar the owner shift:
-- BenillaTooltip_AddNewbieTipFor(this.tooltipText, 1.0, 1.0, 1.0, this.newbieText)
-- ref MainMenuBarMicroButton's OnEnter (l.13), verbatim — the reference's helper reads
-- the firing frame off `this`, which is `button` here (the XML hands it `self`):
-- GameTooltip_AddNewbieTip(this.tooltipText, 1.0, 1.0, 1.0, this.newbieText);
function BenillaMicroButton_OnEnter(button)
if not GameTooltip_AddNewbieTip then return end -- engine tests load file subsets
BenillaTooltip_AddNewbieTipFor(button, BenillaMicroButton_TooltipText(button), 1.0, 1.0, 1.0,
button.newbieText)
GameTooltip_AddNewbieTip(BenillaMicroButton_TooltipText(button), 1.0, 1.0, 1.0, button.newbieText)
end
-- Is a panel up? `getglobal` because every panel this asks about loads AFTER this file —
@@ -216,6 +216,18 @@
-- load — compares against the string, so on a fresh 1.12 install the row paints unchecked
-- and the auto-watch is dead until the box is clicked; the string ships the advertised
-- default as the real one. A recorded deviation (1804's rule), not a quiet one.
-- "Show Detailed Tooltips" — ref UIOptionsFrame.lua l.100-101, inside UIOptionsFrame_Init.
-- A FrameXML uvar, not a CVar, and 1.12 ships it ON: the explanatory second line IS the
-- default reference tooltip, not an opt-in (0661). It lived in our GameTooltip.xml until
-- the tooltip went stock (1968); the reference keeps it here, with the Interface page.
SHOW_NEWBIE_TIPS = "1"
RegisterForSave("SHOW_NEWBIE_TIPS")
-- "Hide World PvP Objectives" — ref UIOptionsFrame.lua's `HIDE_OUTDOOR_WORLD_STATE = "0"`,
-- the gate the stock WorldStateAlwaysUpFrame_Update opens the outdoor readout on
-- (`== "0" or instanceType == "pvp"`). Here since that frame went stock (1972); a nil
-- reads as hidden, which is how the tower counters would vanish outdoors.
HIDE_OUTDOOR_WORLD_STATE = "0"
RegisterForSave("HIDE_OUTDOOR_WORLD_STATE")
QUEST_FADING_DISABLE = "0"
RegisterForSave("QUEST_FADING_DISABLE")
AUTO_QUEST_WATCH = "1"
File diff suppressed because it is too large Load Diff
+143 -18
View File
@@ -35,6 +35,12 @@
this:RegisterEvent("VARIABLES_LOADED")
this:RegisterEvent("TRAINER_SHOW")
this:RegisterEvent("TRAINER_CLOSED")
this:RegisterEvent("AUCTION_HOUSE_SHOW")
this:RegisterEvent("AUCTION_HOUSE_CLOSED")
this:RegisterEvent("TRADE_SKILL_SHOW")
this:RegisterEvent("TRADE_SKILL_CLOSE")
this:RegisterEvent("CRAFT_SHOW")
this:RegisterEvent("CRAFT_CLOSE")
this:RegisterEvent("GUILD_INVITE_REQUEST")
this:RegisterEvent("GUILD_INVITE_CANCEL")
this:RegisterEvent("MIRROR_TIMER_START")
@@ -335,17 +341,22 @@
return loaded
end
-- ref UIParent.lua:223, whose slot this is — immediately above `UIParent_OnEvent`, the
-- same neighbour it has there. It moved here from our own InspectFrame.xml when 1832
-- retired that file: the name belongs to UIParent in the reference, and the window it
-- opens is now the stock LoadOnDemand `Blizzard_InspectUI` off the player's chain.
--
-- The reference's body is `InspectFrame_LoadUI()` and then a guarded `InspectFrame_Show`.
-- We have no load-on-demand — benilla.toc loads that addon with everything else — so the
-- LoadUI call has nothing to do and is left out rather than stubbed (1203). The guard is
-- kept exactly as the reference writes it, and it is not decoration: it is what makes this
-- safe in the ~25 per-window harnesses that load UIParent.xml without the inspect window.
-- ref UIParent.lua l.170-172 and l.223-228, verbatim — the inspect window's loader and its
-- opener, immediately above `UIParent_OnEvent` as there. They moved here from our own
-- InspectFrame.xml when 1832 retired that file: the names belong to UIParent in the
-- reference. The window is the stock `Blizzard_InspectUI`, a LoadOnDemand registry row
-- off the player's chain (`ui_script::addons`, 1957) that `InspectFrame_LoadUI` loads on
-- the first call — the reference's own timing since 1967; from 1832 until then benilla.toc
-- listed the addon with everything else and the call was left out rather than stubbed
-- (1203). The guard is the reference's own, and it is not decoration: a harness that loads
-- UIParent.xml with no addon seated gets the reference's "failed to load" message from
-- `UIParentLoadAddOn` above and then, through this guard, nothing else.
function InspectFrame_LoadUI()
UIParentLoadAddOn("Blizzard_InspectUI");
end
function InspectUnit(unit)
InspectFrame_LoadUI();
if ( InspectFrame_Show ) then
InspectFrame_Show(unit);
end
@@ -357,14 +368,19 @@
-- addon, so deleting our window file would have taken the name with it.
--
-- The LEVEL GATE is the reference's, and it is real behaviour rather than a guard: below
-- level 10 the key and the micro button do nothing at all. `TalentFrame_LoadUI` is left
-- out (we load the addon eagerly, 1203 — build it or leave it out, never stub it); the
-- `TalentFrame_Toggle` guard is kept verbatim, and it is what lets the ~25 harnesses that
-- load UIParent.xml without the talent window do so safely.
-- level 10 the key and the micro button do nothing at all — the addon is not even loaded.
-- `TalentFrame_LoadUI` (ref l.185-187) loads the LoadOnDemand row the first time (1967;
-- until then benilla.toc listed the addon and the call was left out, 1203). The
-- `TalentFrame_Toggle` guard is verbatim too.
function TalentFrame_LoadUI()
UIParentLoadAddOn("Blizzard_TalentUI");
end
function ToggleTalentFrame()
if ( UnitLevel("player") < 10 ) then
return;
end
TalentFrame_LoadUI();
if ( TalentFrame_Toggle ) then
TalentFrame_Toggle();
end
@@ -387,14 +403,80 @@
UIParentLoadAddOn("Blizzard_TrainerUI")
end
-- ref UIParent.lua l.154-156, verbatim — the auction house's loader, reached from the
-- AUCTION_HOUSE_SHOW arm below; `Blizzard_AuctionUI` is a registry row off the chain (1971).
function AuctionFrame_LoadUI()
UIParentLoadAddOn("Blizzard_AuctionUI");
end
-- ref UIParent.lua l.158-160 and l.216-221, verbatim — the battlefield minimap's loader
-- and its toggle (the TOGGLEBATTLEFIELDMINIMAP binding); the stock WorldStateFrame.lua
-- calls the loader when a battleground's world states arrive (1972). The addon is a
-- registry row off the chain like the others; its pane is a `<Minimap>`-kind widget this
-- engine draws nothing into yet.
function BattlefieldMinimap_LoadUI()
UIParentLoadAddOn("Blizzard_BattlefieldMinimap");
end
function ToggleBattlefieldMinimap()
BattlefieldMinimap_LoadUI();
if ( BattlefieldMinimap_Toggle ) then
BattlefieldMinimap_Toggle();
end
end
-- ref UIParent.lua l.166-168 and l.190-192, verbatim — the crafting windows' loaders,
-- reached from the TRADE_SKILL_SHOW / CRAFT_SHOW arms below; `Blizzard_TradeSkillUI` and
-- `Blizzard_CraftUI` are registry rows off the chain (1973).
function CraftFrame_LoadUI()
UIParentLoadAddOn("Blizzard_CraftUI");
end
function TradeSkillFrame_LoadUI()
UIParentLoadAddOn("Blizzard_TradeSkillUI");
end
-- ref UIParent.lua's own `BuildColoredListString`, verbatim — the "Requires: X, Y" line
-- both crafting addons build their tool/reagent lists with (a missing tool in red). It
-- lived as a guarded global in our TradeSkillFrame.xml until 1973 retired that file.
function BuildColoredListString(...)
if ( arg.n == 0 ) then
return nil;
end
-- Takes input where odd items are the text and even items determine whether the arg should be colored or not
local string;
if ( arg[2] ) then
string = arg[1];
else
string = RED_FONT_COLOR_CODE.. arg[1]..FONT_COLOR_CODE_CLOSE;
end
for i=3, arg.n, 2 do
if ( arg[i+1] ) then
-- If meets the condition
string = string..", "..arg[i];
else
-- If doesn't meet the condition
string = string..", "..RED_FONT_COLOR_CODE..arg[i]..FONT_COLOR_CODE_CLOSE;
end
end
return string;
end
-- ref UIParent.lua's own `ShowMacroFrame`, moved here from our MacroFrame.xml when 1848
-- retired that file — the third such squatter after `InspectUnit` (1832) and
-- `ToggleTalentFrame` (1833), and by now the expected shape.
--
-- `MacroFrame_LoadUI` is left out: the addon loads eagerly from benilla.toc, so there is
-- nothing for it to do, and 1203 says leave it out rather than stub it. The
-- `MacroFrame_LoadUI` (ref l.176-178) loads the LoadOnDemand row the first time (1967;
-- until then benilla.toc listed the addon and the call was left out, 1203). The
-- `MacroFrame_Show` guard is the reference's own.
function MacroFrame_LoadUI()
UIParentLoadAddOn("Blizzard_MacroUI");
end
function ShowMacroFrame()
MacroFrame_LoadUI();
if ( MacroFrame_Show ) then
MacroFrame_Show();
end
@@ -408,8 +490,9 @@
-- `RAID_ROSTER_UPDATE` and again at `PLAYER_LOGIN` with a raid up (l.34, l.39), so its
-- absence is a raise every time the roster moves. It is a FrameXML function, not an engine
-- verb, and this file is our stand-in for `UIParent.lua` — the same relationship the three
-- above have. `UIParentLoadAddOn` is just above, and it no-ops on an addon that is
-- already loaded, which ours is: benilla.toc takes `Blizzard_RaidUI` eagerly (1874).
-- above have. `UIParentLoadAddOn` is just above; the addon is a LoadOnDemand registry
-- row (1957) that this loads on the first roster update — the reference's timing since
-- 1967 (1874 had listed it in benilla.toc, so this was a no-op on a loaded addon).
function RaidFrame_LoadUI()
UIParentLoadAddOn("Blizzard_RaidUI");
end
@@ -448,6 +531,48 @@
end
return
end
-- The auction arms (ref UIParent.lua l.575-587), verbatim — the addon loads on the first
-- auctioneer and its own `AuctionFrame_Show` reads the hello the app applied (1971).
if ( event == "AUCTION_HOUSE_SHOW" ) then
AuctionFrame_LoadUI();
if ( AuctionFrame_Show ) then
AuctionFrame_Show();
end
return;
end
if ( event == "AUCTION_HOUSE_CLOSED" ) then
if ( AuctionFrame_Hide ) then
AuctionFrame_Hide();
end
return;
end
-- The trade skill and craft arms (ref UIParent.lua l.605-632), verbatim (1973).
if ( event == "TRADE_SKILL_SHOW" ) then
TradeSkillFrame_LoadUI();
if ( TradeSkillFrame_Show ) then
TradeSkillFrame_Show();
end
return;
end
if ( event == "TRADE_SKILL_CLOSE" ) then
if ( TradeSkillFrame_Hide ) then
TradeSkillFrame_Hide();
end
return;
end
if ( event == "CRAFT_SHOW" ) then
CraftFrame_LoadUI();
if ( CraftFrame_Show ) then
CraftFrame_Show();
end
return;
end
if ( event == "CRAFT_CLOSE" ) then
if ( CraftFrame_Hide ) then
CraftFrame_Hide();
end
return;
end
-- The guild invitation (ref UIParent.lua l.296-303, verbatim): the dialog rode a driver
-- frame in our FriendsFrame.xml until that file became the reference's (1959).
if ( event == "GUILD_INVITE_REQUEST" ) then
+3 -10
View File
@@ -144,16 +144,9 @@
-- Blizzard_MacroUI.lua:7). whileDead is the ref's and it earns its keep: writing a macro
-- during a corpse run is a real thing people do.
UIPanelWindows["MacroFrame"] = { area = "left", pushable = 5, whileDead = 1 }
-- The crafting window (decision 0437 phase 2, TradeSkillFrame.xml): NOT an NPC session (it
-- opens off your own cast, TradeSkillFrame.xml's own header comment) — the ref Lua's own
-- registration line (Blizzard_TradeSkillUI.lua:13) is `{ area = "left", pushable = 3 }`,
-- quoted verbatim.
UIPanelWindows["BenillaTradeSkillFrame"] = { area = "left", pushable = 3 }
-- The Enchanting window (decision 0437 phase 3, CraftFrame.xml): the ref Lua's own
-- registration line (`Blizzard_CraftUI.lua:24`), quoted verbatim — `pushable = 4`, NOT `3`
-- (grepped both ref files directly to confirm: `3` is TradeSkillFrame's own line-13 value,
-- a different window/depth — CraftFrame.xml's own header comment has the same note).
UIPanelWindows["BenillaCraftFrame"] = { area = "left", pushable = 4 }
-- The trade skill and craft windows register THEMSELVES: `Blizzard_TradeSkillUI.lua:13`
-- and `Blizzard_CraftUI.lua:24` write their own `UIPanelWindows` rows when the addon loads,
-- which since 1973 is the reference's own file on the first TRADE_SKILL_SHOW / CRAFT_SHOW.
-- The game menu (decision 0674, GameMenuFrame.xml) — the ref's own registration line
-- verbatim (`UIPanelWindows["GameMenuFrame"] = { area = "center", pushable = 0,
-- whileDead = 1 }`, UIParent.lua l.15), and the FIRST **center**-area row benilla ships.
@@ -1,280 +0,0 @@
<Ui>
<!--
The always-up world-state readout — report B190's second half: the alliance/horde progress
line an outdoor-PvP zone or a battleground shows at the top of the screen. In Eastern
Plaguelands that is the two tower counters ("Towers Controlled: 3"); in Warsong Gulch the
flag captures; in Arathi Basin bases and resources.
The SPEC is the reference's two C bindings, byte-carved (wow-re
system/ui/scratch/worldstate-ui-law.md, 0x4c5a40 / 0x4c5a70) and re-expressed here:
GetNumWorldStateUI() -> how many rows the current scope admits
GetWorldStateUIInfo(i) -> TEN values, in this order:
uiState, text, icon, dynamicIcon,
tooltip, dynamicTooltip, extendedUI,
extUIState1, extUIState2, extUIState3
Note what that return is NOT: there is no uiType and no hidden in 1.12 — those are a later
expansion of the API, and reading this list as if they were there shifts every value by two.
`text` arrives already macro-expanded (the host runs the reference's %<id>w expander);
every other string is its DBC column verbatim, and never nil — an empty column is "".
Which rows exist at all is the host's (crates/benilla-app/src/world_state_ui.rs), including
the gate that makes this frame look broken if you don't know about it: the world-PvP rows
(Eastern Plaguelands, Silithus) are Type 1, which the reference admits only while the player
has joined the zone-dependent DEFENSE chat channel. Leave LocalDefense and the readout goes
away while the map icons stay.
THE GEOMETRY IS THE REFERENCE'S, and it is not what you would guess (wow-re
worldstate-ui-law.md §12, carved 2026-08-25; decision 1604 folds it back). The first pass
of this frame invented a snug 20x20 icon in a 22px row, and that was wrong in a way that
showed: EVERY icon these rows name is a sprite authored into the UPPER-LEFT corner of a
power-of-two canvas with the rest transparent — AllianceTower/HordeTower are a 16x16 sprite
at (3,2) of a 32x32 file, UI-PVP-Alliance/Horde a ~40x40 crest in a 64x64 one. Squeeze that
whole canvas into a snug box and the visible art floats up and to the left of the box,
away from its label. NOTHING IS CROPPED to fix that — there is no <TexCoords> on any of
these regions, and a texture with none samples the full 0..1 quad (CSimpleTexture's ctor
0x76fc40 stamps it). The compensation is geometric, and these three numbers are one tuned
system: the icon slot is OVERSIZED to 42x42 (a 32px file upscaled 1.3125x), hung SIX UNITS
OFF the row's left edge, and the text is seated TEN UNITS ABOVE the slot's centreline. The
upper-left ink then lands as a ~21x21 square whose centre sits just under the text's, which
is what "icon next to its label" is supposed to look like.
THERE ARE TWO ICON SLOTS, fed by two different DBC columns, and shipped content never puts
the same art in both:
Icon (+0x0c) -> the 42x42 slot below. AllianceTower, HordeTower,
UI-PVP-Alliance, UI-PVP-Horde.
DynamicIcon (+0x64) -> a SEPARATE 32x32 button off the row's right edge, shown only
while uiState == 2. HordeFlag/AllianceFlag, Warsong Gulch only.
So the dynamic icon does NOT replace the static one — both are on screen together, which is
the Warsong Gulch look: the faction shield with its capture count, and a flashing enemy
flag to its right while a carrier is running. `uiState == 2` is the flag-taken value; the
first pass swapped the art on `uiState ~= 0`, which fired on the ordinary state 1 and
replaced a shield that should never have moved.
OUT, named: the CAPTUREPOINT extended UI — the sliding bar over
Interface\WorldStateFrame\WorldState-CaptureBar that the one Eastern Plaguelands progress
row drives. Its two states come through the binding already (extUIState1/2 carry the
alliance/horde fractions), so it is a widget to add, not a seam to change. NOW KNOWN, and
worth writing down before someone builds it: in the reference that row never reaches this
frame at all — the update path branches on a non-empty extendedUI FIRST, and id 138 is the
only always-up-class row with one, so EPL's real readout is two text rows here plus a
separate bar anchored under the MinimapCluster. Until that bar exists we keep showing the
row as its text line (decision 1590 §7's stand-in, unchanged) so the progress number is on
screen; it costs a third line the reference doesn't draw, and it goes when the bar lands.
Also out: the battleground scoreboard (the Type 2 rows), which the reference builds from the
same DBC through a DIFFERENT handler and a different pair of bindings whose bail leg is not
even the same shape — GetNumBattlefieldStats/GetBattlefieldStatInfo, a separate slice.
-->
<Script>
<![CDATA[
NUM_ALWAYS_UP_ROWS = 0;
-- The reference's own numbers (worldstate-ui-law.md §12). The row is an anchor node, not a
-- box: its 42-wide icon slot hangs off its left edge and its text runs past its right, so
-- none of these are "the readout's size" — they are the offsets that seat corner-authored
-- art against a label. The pitch is the template height, because rows chain TOP to the
-- previous row's BOTTOM with no gap.
WORLD_STATE_ROW_HEIGHT = 24;
-- The dynamic-icon flash: 0.5 s in, 0.5 s out, indefinitely.
WORLD_STATE_FLASH_HALF = 0.5;
-- The flash overlay's ramp. The art (…Flash.blp) is DXT1 with no alpha channel — black IS
-- the transparency — which is why it draws ADD; the pulse is the region's own alpha.
function WorldStateDynamicIcon_OnUpdate(elapsed)
local t = (this.flashTime or 0) + elapsed;
local period = 2 * WORLD_STATE_FLASH_HALF;
while ( t >= period ) do
t = t - period;
end
this.flashTime = t;
local level = t / WORLD_STATE_FLASH_HALF;
if ( level > 1 ) then
level = 2 - level;
end
getglobal(this:GetName() .. "Flash"):SetAlpha(level);
end
-- Rebuild the readout from the host's list. Cheap enough to run whole on every fire: the
-- list is at most a handful of rows, and the events are a zone's worth apart.
function WorldStateAlwaysUpFrame_Update()
local numUI = GetNumWorldStateUI();
-- Grow the row pool on demand and never shrink it, the way every pooled list here
-- does: the tail from a busier zone is parked hidden rather than destroyed. Row 1
-- hangs off the container; every later row chains to the one above it, so the pitch is
-- the template's height and lives in exactly one place.
if ( numUI > NUM_ALWAYS_UP_ROWS ) then
for i = NUM_ALWAYS_UP_ROWS + 1, numUI do
local row = CreateFrame("Frame", "WorldStateAlwaysUpFrame" .. i,
WorldStateAlwaysUpFrame, "WorldStateAlwaysUpTemplate");
if ( i == 1 ) then
row:SetPoint("TOP", WorldStateAlwaysUpFrame, "TOP", -23, -20);
else
row:SetPoint("TOP", getglobal("WorldStateAlwaysUpFrame" .. (i - 1)),
"BOTTOM", 0, 0);
end
end
NUM_ALWAYS_UP_ROWS = numUI;
end
for i = 1, NUM_ALWAYS_UP_ROWS do
local row = getglobal("WorldStateAlwaysUpFrame" .. i);
if ( i > numUI ) then
row:Hide();
else
local uiState, text, icon, dynamicIcon, tooltip, dynamicTooltip = GetWorldStateUIInfo(i);
local name = row:GetName();
-- The static slot takes its column verbatim. An EMPTY column is not a special
-- case: SetTexture("") clears the art and draws nothing while the authored
-- 42x42 rect — and so everything anchored to it — stays exactly where it was.
-- The slot is reserved and blank, never collapsed; a row that slid its text
-- left into the empty slot would ragged the readout's left edge, and the
-- reference does no such thing.
getglobal(name .. "Icon"):SetTexture(icon);
getglobal(name .. "Text"):SetText(text);
row.tooltip = tooltip;
-- The dynamic slot is a SEPARATE button off the row's right edge, and it is
-- the flag-taken state alone that shows it.
local dynamic = getglobal(name .. "DynamicIcon");
if ( dynamicIcon ~= "" and uiState == 2 ) then
getglobal(dynamic:GetName() .. "Texture"):SetTexture(dynamicIcon);
getglobal(dynamic:GetName() .. "Flash"):SetTexture(dynamicIcon .. "Flash");
dynamic.tooltip = dynamicTooltip;
dynamic.flashTime = 0;
dynamic:Show();
else
dynamic:Hide();
end
row:Show();
end
end
-- OURS, and non-visual: the reference's container is a bare 10x10 anchor node it never
-- resizes, so its own bottom says nothing about how far the rows reach. We keep the
-- authored size but grow the height to the rows' real extent, so "how much of the top
-- of the screen is this using?" is a question the frame can answer — the dev cost pill
-- asks it, and seats itself below (crates/benilla-app/src/perf/hud.rs). Rows anchor to
-- the container's TOP, which is fixed, so nothing moves.
if ( numUI > 0 ) then
WorldStateAlwaysUpFrame:SetHeight(20 + numUI * WORLD_STATE_ROW_HEIGHT);
WorldStateAlwaysUpFrame:Show();
else
WorldStateAlwaysUpFrame:Hide();
end
end
function WorldStateAlwaysUpFrame_OnEvent()
WorldStateAlwaysUpFrame_Update();
end
]]>
</Script>
<!-- One row: the oversized icon slot, its line of text, and the dynamic-icon button that only
Warsong Gulch ever lights. Grown on demand into WorldStateAlwaysUpFrame by the update
above. Every offset here is the reference's — see the header for why they are not the
snug numbers you would pick by eye. -->
<Frame name="WorldStateAlwaysUpTemplate" virtual="true" enableMouse="true">
<Size><AbsDimension x="45" y="24"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon">
<Size><AbsDimension x="42" y="42"/></Size>
<Anchors>
<Anchor point="LEFT">
<Offset><AbsDimension x="-6" y="0"/></Offset>
</Anchor>
</Anchors>
</Texture>
<FontString name="$parentText" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentIcon" relativePoint="RIGHT">
<Offset><AbsDimension x="-12" y="10"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentDynamicIcon" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="RIGHT">
<Offset><AbsDimension x="40" y="9"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentTexture">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentFlash" alphaMode="ADD">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnUpdate>
WorldStateDynamicIcon_OnUpdate(arg1);
</OnUpdate>
<OnEnter>
if ( this.tooltip and this.tooltip ~= "" ) then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");
GameTooltip:SetText(this.tooltip);
GameTooltip:Show();
end
</OnEnter>
<OnLeave>
GameTooltip:Hide();
</OnLeave>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnEnter>
if ( this.tooltip and this.tooltip ~= "" ) then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");
GameTooltip:SetText(this.tooltip);
GameTooltip:Show();
end
</OnEnter>
<OnLeave>
GameTooltip:Hide();
</OnLeave>
</Scripts>
</Frame>
<!-- The container. Top-centre of the screen, which is where an outdoor-PvP zone's counters and
a battleground's score belong: the minimap owns the top-right and the readout must not
fight it. The reference declares it 10x10 and never touches it again — it is an anchor
node, and rows overflow it freely (see the update for the one thing we do differently, and
why). Hidden until a scope admits a row. -->
<Frame name="WorldStateAlwaysUpFrame" parent="UIParent" toplevel="true" hidden="true">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="UIParent" relativePoint="TOP">
<Offset><AbsDimension x="-5" y="-15"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
-- UPDATE_WORLD_STATES is the reference's own event for this list (0x20e). The
-- entering-world fire is what paints the readout on a login that lands inside a
-- scope the server has already sent states for.
this:RegisterEvent("UPDATE_WORLD_STATES");
this:RegisterEvent("PLAYER_ENTERING_WORLD");
</OnLoad>
<OnEvent>
WorldStateAlwaysUpFrame_OnEvent();
</OnEvent>
</Scripts>
</Frame>
</Ui>
+88 -99
View File
@@ -166,13 +166,22 @@ Interface\FrameXML\MoneyInputFrame.xml
# reference them. The StaticPopup engine and every dialog it used to carry are the reference's
# own file now, far below at the reference's own slot after UIPanelTemplates (decision 1960).
UiPanels.xml
# The tooltip: a hover window the bag/merchant/loot/unit frames drive. Loaded before them so
# `GameTooltip` + its verbs exist when their OnEnter handlers fire (runtime, so order
# is not strictly required — but it keeps the dependency legible). It also declares the two
# virtual templates (GameTooltipTemplate, ShoppingTooltipTemplate), which IS an order
# requirement: ItemRef.xml below expands one, and so do the 27 corpus addons that name
# GameTooltipTemplate in an XML inherits=.
GameTooltip.xml
# ══ THE TOOLTIP — the reference's own, off the player's chain (1751; decision 1968) ══
# GameTooltip.xml sources HealthBar.lua and GameTooltip.lua and <Include>s GameTooltipTemplate.xml:
# the two virtual templates (GameTooltipTemplate, ShoppingTooltipTemplate — thirty declared line
# pairs each, the status bar, the money row), the GameTooltip / ShoppingTooltip1 / ShoppingTooltip2
# instances and SmallTextTooltip. The widget itself is this engine's (0274); every verb the file
# calls was already bound. Its slot is the reference's own: FrameXML.toc l.13, after MoneyFrame
# (l.9-10, `SmallMoneyFrameTemplate` resolves at load) and before the panel kit. The order
# requirement the old header named still holds: ItemRef.xml below expands GameTooltipTemplate,
# and so do the 27 corpus addons that name it in an XML inherits=.
#
# What our transcription carried that the reference does not, and where each went (1968): the
# `$parentThicken` plate fill (a look deviation, retired — the plate is the reference's art);
# `SHOW_NEWBIE_TIPS`'s default, which is UIOptionsFrame.lua's and sits in OptionsFrame.xml's uvar
# block with its family; `FACTION_BAR_COLORS`' fallback table — the reference's is
# ReputationFrame.lua's, on the chain below; `NUM_SHOPPING_TOOLTIPS` — PaperDollFrame.lua's.
Interface\FrameXML\GameTooltip.xml
# ══ THE DROPDOWN KIT — the reference's own, off the player's chain (1751) ══
# UIDropDownMenuTemplate + the DropDownList frames + the whole `UIDropDownMenu_*` API — before its
# first customers (the unit popups below, the world map's pickers, the trainer filter). The stock
@@ -559,11 +568,14 @@ Interface\FrameXML\CastingBarFrame.xml
# 1.12 never reaches it — vmangos refuses to send that packet and substitutes a full START,
# saying so in `Player::SendMirrorTimers` — and `ui_mirror.rs`'s Pause edge carries the note.
Interface\FrameXML\MirrorTimer.xml
# The always-up world-state readout (decision 1586, report B190): the tower/flag/base counters an
# outdoor-PvP zone or a battleground shows at top-centre, driven by the UPDATE_WORLD_STATES event
# `crate::world_state_ui` fires. Needs Fonts (GameFontNormalSmall) and GameTooltip; sits beside the
# mirror timers, which share its top-centre lane.
WorldStateFrame.xml
# ══ THE WORLD-STATE FRAMES — the reference's own, off the player's chain (1751; decision 1972) ══
# WorldStateFrame.xml + WorldStateFrame.lua: the always-up readout 1586 built (the tower/flag/base
# counters at top-centre, `UPDATE_WORLD_STATES` from `crate::world_state_ui`), the capture bars,
# and the battleground SCOREBOARD ours never had — `WorldStateScoreFrame`, over the score family
# 1972 built (`MSG_PVP_LOG_DATA`, the eleven verbs, `UPDATE_BATTLEFIELD_SCORE`) and UIParent.xml's
# `BattlefieldMinimap_LoadUI`. Needs Fonts, GameTooltip, the panel kit (its close button, tabs,
# faux scroll frame) — all above; sits beside the mirror timers, which share its top-centre lane.
Interface\FrameXML\WorldStateFrame.xml
# ══ THE DURABILITY ALERT — the reference's own, off the player's chain (1751, window 4) ══
# The "armor guy" under the minimap: `DurabilityFrame`, its eleven glyph textures, and
# `DurabilityFrame_SetAlerts` / `_OnUpdate` with `INVENTORY_ALERT_STATUS_SLOTS` and
@@ -752,42 +764,34 @@ Interface\FrameXML\SkillFrame.xml
# `HonorFrameTemplates.xml` is `<Include>`d by `HonorFrame.xml` and inherited by the LoadOnDemand
# `Blizzard_InspectUI`, which loads later still.
Interface\FrameXML\HonorFrame.xml
# ══ THE INSPECT WINDOW — the reference's own, off the player's chain (1751, window 24) ══
# Another player's paper doll (decision 0631), and the FIRST LoadOnDemand ADDON we take off the
# chain rather than a FrameXML file. Everything below it in this manifest that names a
# `Blizzard_*` addon follows the shape set here.
# ══ THE LoadOnDemand ADDONS — Inspect, Talent, Macro, Raid: NO ROW HERE (1751; 1967) ══
# `Blizzard_InspectUI`, `Blizzard_TalentUI`, `Blizzard_MacroUI` and `Blizzard_RaidUI` are not
# listed, exactly as the reference's own `FrameXML.toc` lists no addon: each is `LoadOnDemand: 1`
# on the player's chain — inside patch.MPQ; the loose `Interface\AddOns\Blizzard_*\` folders on
# disk hold one `.pub` decoy each, which is what kept these four windows on transcriptions for
# months — and `ui_script::addons` seats every such addon as a registry row (1957) that
# `UIParentLoadAddOn` loads the first time its opener runs. The openers are the reference's own,
# in UIParent.xml, its home for them: `InspectUnit` → `InspectFrame_LoadUI`, `ToggleTalentFrame`
# → `TalentFrame_LoadUI`, `ShowMacroFrame` → `MacroFrame_LoadUI`, and the stock RaidFrame.lua's
# `RaidFrame_LoadUI` on the first roster update. 1832, 1833, 1848 and 1874 took the four off the
# chain but listed them here with everything else, and left the LoadUI calls out rather than
# stub them (1203); 1967 gives them the reference's load timing and the calls back.
#
# **The addon is inside patch.MPQ, not the loose folder.** `Interface\AddOns\Blizzard_InspectUI\`
# on disk holds one `Blizzard_InspectUI.pub` decoy and nothing else; the real four files sit in
# the archive, which the chain mounts. Believing the decoy is what kept this window (and Macro,
# Talent, Raid, Trainer) on a transcription for months.
#
# No LoadOnDemand: we list the addon's files here and they load with everything else. The
# reference reaches them through `InspectUnit` → `InspectFrame_LoadUI`; ours keeps `InspectUnit`
# (in UIParent.xml, the reference's own home for that name) and drops the LoadUI call.
#
# The addon's own toc order, which is load order — the `.xml` files pull their `.lua` siblings
# through `<Script file=.../>` relative to the addon directory, exactly as a FrameXML file does.
# Its toc lists a fourth entry, `Localization.lua`, which is omitted here because it holds no code
# at all: the whole file is the comment `-- This file is executed at the end of addon load`.
#
# Dependencies are unchanged from the file this replaces: Fonts/UiPanels/GameTooltip, plus
# `CharacterFrameTabButtonTemplate` from UiPanels.xml, plus ItemButtonTemplate.xml far above
# (`InspectPaperDollItemSlotButtonTemplate` inherits it, resolved at LOAD). And the one that
# binds hardest is the entry directly above: the honor page inherits HonorFrame.xml's five row
# templates — the reference has the identical edge, which is why its own toc has the same order.
#
# What we GAIN by swapping: `InspectGuildText` and `InspectTitleText`, two FontStrings our
# transcription never had.
Interface\AddOns\Blizzard_InspectUI\Blizzard_InspectUI.xml
Interface\AddOns\Blizzard_InspectUI\InspectPaperDollFrame.xml
Interface\AddOns\Blizzard_InspectUI\InspectHonorFrame.xml
# The dressing room (decision 1060): the ctrl-click item preview. Loaded right after the
# windows whose slots feed it (the character window above, the bags before that) and before
# everything else that ctrl-clicks into it — order is legibility only, since `DressUpItemLink`
# is a global resolved at call time. Needs UiPanels (the panel manager, its left-slot row) and
# Fonts; its booth pane needs nothing loaded at all.
DressUpFrame.xml
# What those headers recorded that still holds: the inspect honor page inherits HonorFrame.xml's
# five row templates, the macro icon chooser inherits `ClassTrainerListScrollFrameTemplate`
# (1862) — FrameXML files listed above, so resolved long before any addon loads, which is also
# why the reference's own toc has the same order. The gates over chain files
# (`ui_script::reference_ui`) walk these addons' files after the manifest's entries — a reached
# addon is part of the shipped interface, row or no row.
# ══ THE DRESSING ROOM — the reference's own, off the player's chain (1751; decision 1969) ══
# DressUpFrame.xml + DressUpFrame.lua: the ctrl-click item preview (1060). Its `<DressUpModel>`
# is a real widget kind now — PlayerModel's three verbs plus its own `TryOn`/`Dress`/`Undress`
# (table 0x84f190), queued as intents the app composes the booth's look from — and its pane draws
# the "dressup" booth by the reference's own name (`portrait::model_pane_booth`). The rotate
# buttons and OnUpdate drive `Model_RotateLeft/Right/OnUpdate` from UIParent.xml above, and the
# Close/Reset buttons inherit UIPanelCloseButton/UIPanelButtonTemplate from the panel kit above.
# Slot as before: after the windows whose slots feed it; `DressUpItemLink` resolves at call time.
Interface\FrameXML\DressUpFrame.xml
# ══ THE SPELLBOOK — the reference's own, off the player's chain (1751; decision 1952) ══
# SpellBookFrame.xml + SpellBookFrame.lua: the book, the twelve page buttons, the skill-line tabs,
# the Spell/Pet toggle tabs. Its whole engine surface was already ours (1924's UpdateSpells, 1931's
@@ -800,23 +804,6 @@ Interface\FrameXML\SpellBookFrame.xml
# <Model> (UI-AutoCastButton.mdx) and this engine's <Model> draws no pixels; the shine benilla
# draws is its own token texture (1383/1392/1393). BENILLA_ALLOW_OWN_UI=1.
SpellBookAdapters.xml
# ══ THE TALENT WINDOW — the reference's own, off the player's chain (1751, window 25) ══
# The second LoadOnDemand addon (1832 was the first, and its header carries the mechanics).
# `Blizzard_TalentUI` is FIVE files behind one manifest line: the `.xml` sources its own `.lua`
# and `<Include>`s `Blizzard_TalentUITemplates.xml`, which is where the four templates every
# talent button, branch, arrow and tab inherits actually live. Its toc's only other entry is the
# empty `Localization.lua`.
#
# It needs NOTHING built — every engine verb the five files call was already registered, checked
# name by name against our bindings. What it needs LOADED is UIPanelTemplates (its scroll frame
# is `UIPanelScrollFrameTemplate`, its close and cancel buttons `UIPanelCloseButton` /
# `UIPanelButtonTemplate`) and Fonts, both far above.
#
# `ToggleTalentFrame` moved to UIParent.xml, the reference's own home for it — see 1833.
#
# What we GAIN: `TalentFrameTab4` and `TalentFrameTab5`. The reference lays out five tab slots
# where our transcription stopped at the three a 1.12 class actually has.
Interface\AddOns\Blizzard_TalentUI\Blizzard_TalentUI.xml
# ══ THE SOCIAL WINDOW — the reference's own, off the player's chain (1751; decision 1959) ══
# FriendsFrame.xml + FriendsFrame.lua: the O-key window's four tabs — friends/ignore, who, the
# guild roster with its control and info panes, and the raid tab whose pane is the stock
@@ -831,8 +818,10 @@ Interface\FrameXML\FriendsFrame.xml
# a child cannot be declared before its parent — the reference splits it the same way and for the
# same reason (its FrameXML.toc has RaidFrame.xml on the very next line). Decision 1549.
#
# TWO entries replace one of ours: the pane itself, and the LoadOnDemand half that carries the
# group frames and the tear-off pullouts. 1839 measured the swap — 83 of our 84 frame names were
# The pane is the entry here; the LoadOnDemand half that carries the group frames and the
# tear-off pullouts, `Blizzard_RaidUI`, is a registry row that the stock RaidFrame.lua loads on
# the first roster update (the addons header far above; 1967). 1839 measured the swap — 83 of our
# 84 frame names were
# already the reference's, 20 of our 22 Lua functions were, and NOTHING needed building. The one
# name that was not is `BenillaRaidDropDown`, and the reference has no raid drop-down at all: it
# reuses `FriendsDropDown`, which FriendsFrame.xml declares four manifest lines above.
@@ -840,7 +829,6 @@ Interface\FrameXML\FriendsFrame.xml
# GAINED: `RaidPulloutFrameTemplate`, `RaidPulloutButtonTemplate`, `RaidBuffButtonTemplate` and the
# fifteen `RaidPullout*` verbs — the tear-off group pullouts we never transcribed.
Interface\FrameXML\RaidFrame.xml
Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml
# ══ THE GOSSIP WINDOW — the reference's own, off the player's chain (1751) ══════════════════
# No engine verb, no widget method, no shadowed name — all three instruments clean. It was
# deferred once on `GossipResize` reading `GetTextHeight()` in the same repaint that sets the
@@ -856,11 +844,18 @@ Interface\FrameXML\GossipFrame.xml
# That is why the notes below say "grouping" rather than "reuses that file's helpers": the
# windows still cluster here, but nothing depends on this file any more.
Interface\FrameXML\MerchantFrame.xml
# The mail window (decision 0544 P1/P2): the mailbox inbox, open-letter, and send tab over
# the Era mail API (`benilla-ui/src/script/mail.rs`) + `crate::ui_mail`'s feed/drain. Kept
# beside MerchantFrame in the NPC-window cluster; its postage/enclosed/COD coin displays use
# the BenillaMoney_* kit, which is defined in MoneyFrame.xml far above.
MailFrame.xml
# ══ THE MAIL WINDOW — the reference's own, off the player's chain (1751; decision 1970) ══
# MailFrame.xml + MailFrame.lua: the inbox, the open letter (with the auction invoice pane and
# the letter-copy button), the send tab with its stationery picker and recipient autocomplete,
# over the Era mail API (`benilla-ui/src/script/mail.rs`) + `crate::ui_mail`'s feed/drain. What
# the swap built: the stationery family (`GetNumStationeries`/`GetStationeryInfo`/
# `SelectStationery`/`GetSelectedStationeryTexture`, wow-re `stationery-bindings.md`) and the two
# events the stock file listens for that nothing fired — `MAIL_SEND_INFO_UPDATE` on an attachment
# change and `CLOSE_INBOX_ITEM(index)` when a take empties a mail. Its templates are all on the
# chain above: the panel kit, ItemButtonTemplate, MoneyFrame/MoneyInputFrame, UIRadioButtonTemplate
# (UIPanelTemplates.xml), InvoiceTextFontNormal (Fonts.xml) and FriendsFrameTabTemplate
# (FriendsFrame.xml, which the reference's own toc also seats before this, l.71 → l.86).
Interface\FrameXML\MailFrame.xml
# ══ THE TRADE WINDOW — the reference's own, off the player's chain (1751; decision 1966) ══
# TradeFrame.xml + TradeFrame.lua: both columns' seven slots over the trade API
# (`benilla-ui/src/script/trade.rs`) + `crate::ui_trade`'s feed/drain, the recipient's
@@ -951,29 +946,23 @@ Interface\FrameXML\TaxiFrame.xml
# either moves — `ui_stable.rs`, `names.rs` and `net/apply/npc.rs` all reach it by stock names
# already, and `portrait`'s booth keys on `PetStableModel`, which both spell the same.
Interface\FrameXML\PetStable.xml
# The auction house (decision 1511): the three-tab doublewide window over the auction API
# (`benilla-ui/src/script/auction.rs`) + the app's feed/drain. Kept in the "NPC session
# windows grouped" cluster beside Trainer/Bank/Taxi, and loaded AFTER MoneyFrame (its eight
# money displays are SmallMoneyFrameTemplate, and it registers the AUCTION_DEPOSIT money type
# into that file's MoneyTypeInfo), after MerchantFrame (grouping only now — that file's global
# chain's MoneyInputFrame_* for its four money inputs, 1882), after ScrollTemplates (four faux lists), after
# UIPanelTemplates (UIPanelButtonTemplate / UIPanelCloseButton / InputBoxTemplate /
# UICheckButtonTemplate), after UIDropDownMenu (the rarity capsule) and after UiPanels (the
# panel manager's `doublewide` seat, `CharacterFrameTabButtonTemplate`, the PanelTemplates
# tab kit and the StaticPopup engine its two confirmations register into) — all above.
AuctionFrame.xml
# The crafting window (decision 0437 phase 2): NOT an NPC session (opens off your own cast of
# an effect-47 opener spell, TradeSkillFrame.xml's own header comment) — loaded right after
# TrainerFrame per the same "player-state/profession window" grouping and because it depends
# on nothing TrainerFrame doesn't already need (Fonts/UiPanels/GameTooltip/ScrollTemplates,
# all loaded above).
TradeSkillFrame.xml
# The Enchanting window (decision 0437 phase 3): the exact twin of TradeSkillFrame above —
# same "opens off your own cast, not an NPC session" shape (CraftFrame.xml's own header
# comment) — loaded right after it so `BuildColoredListString` (a guarded GLOBAL
# TradeSkillFrame.xml defines) already exists when CraftFrame.xml reuses it without
# redefining (CraftFrame.xml's own header comment, deviation 2).
CraftFrame.xml
# ══ THE AUCTION HOUSE — `Blizzard_AuctionUI`, a LoadOnDemand addon: NO ROW HERE (1751; 1971) ══
# The three-tab doublewide window is the reference's own addon off the player's chain, seated as a
# registry row (1957) and loaded on the first AUCTION_HOUSE_SHOW through UIParent.xml's own
# `AuctionFrame_LoadUI` arm (ref UIParent.lua l.154-156, l.575-587), exactly like the trainer,
# inspect, talent, macro and raid addons above. Its engine surface was already whole (decision
# 1511's auction API); what it needed loaded is on the chain above: the panel kit, MoneyFrame /
# MoneyInputFrame, UIDropDownMenu, and the dressing-room pane it embeds — `<DressUpModel
# name="AuctionDressUpModel">`, the 1969 widget kind, drawing the same booth as the dressing room.
# ══ THE CRAFTING WINDOWS — `Blizzard_TradeSkillUI` and `Blizzard_CraftUI`, LoadOnDemand: NO ROW HERE (1751; 1973) ══
# The trade skill window (Blacksmithing, Tailoring, … and Poisons) and the craft window (Enchanting,
# Beast Training) are the reference's own addons off the player's chain, registry rows (1957) loaded
# on the first TRADE_SKILL_SHOW / CRAFT_SHOW through UIParent.xml's `TradeSkillFrame_LoadUI` /
# `CraftFrame_LoadUI` arms (ref UIParent.lua l.166-168, l.190-192, l.605-632). Both inherit the
# ClassTrainerFrameTemplates.xml kit far above and register their own panel rows. What 1973 built:
# the four item-link verbs (`GetTradeSkillItemLink`, `GetTradeSkillReagentItemLink`,
# `GetCraftItemLink` — the `|Henchant:` link — and `GetCraftReagentItemLink`, wow-re
# `tradeskill-craft-item-links.md`) and `SetHyperlink`'s enchant leg. 0437's transcriptions are gone.
# ══ THE LOOT WINDOW — the reference's own, off the player's chain (1751) ════════════════════
# It never needed an engine VERB. It needed a widget TYPE: `<LootButton>` is a real registered
# `CreateFrame` type (decision 1799), and registering it as a plain Button would have loaded the
@@ -1041,8 +1030,8 @@ Interface\FrameXML\QuestTimerFrame.xml
Interface\FrameXML\ChatFrame.xml
Interface\FrameXML\FloatingChatFrame.xml
# ══ THE MACRO WINDOW — the reference's own, off the player's chain (1751, window 32) ══
# The fourth LoadOnDemand addon (1832's header carries the mechanics). ONE manifest line: its toc
# lists `Blizzard_MacroUI.xml`, which sources its own `.lua`, plus the empty `Localization.lua`.
# A LoadOnDemand addon with no row here since 1967 (the addons header far above); what stays in
# this slot is the FrameXML file it needs loaded first.
#
# Nothing to build. 1835 recorded `SaveMacros` as a missing engine verb and it is not one — the
# only occurrence in the reference's own file is COMMENTED OUT (`--SaveMacros();` in
@@ -1063,9 +1052,9 @@ Interface\FrameXML\FloatingChatFrame.xml
# `FauxScrollFrame_GetOffset` (`return frame.offset`, no `or 0` — our deleted copy had that
# fallback and was hiding this) handed `MacroPopupFrame_Update` a nil to multiply. Clicking
# "Change Name/Icon" raised. The reference's own `FrameXML.toc` loads this at its line 92, well
# before any addon; ours takes it here, immediately above its one consumer. Decision 1862.
# before any addon; ours takes it here — its one consumer, `Blizzard_MacroUI`, is a LoadOnDemand
# row now (1967) and loads after everything. Decision 1862.
Interface\FrameXML\ClassTrainerFrameTemplates.xml
Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml
# The script error log (decision 1495): the window `/errors` opens — the list B293's two
# reporters asked for, which the reference's one-message-at-a-time ScriptErrors modal cannot
# be. Ours entirely (1.12 has no counterpart), reading the retained log on the model rather
+18 -10
View File
@@ -1069,6 +1069,24 @@ const TABLE: &[Spec] = &[
None,
None
),
// The battleground scoreboard and the battlefield minimap — the Interface block's last two
// (Bindings.xml l.630-635), both Lua-bodied: `ToggleWorldStateScoreFrame` is the stock
// WorldStateFrame.lua's since 1972 and `ToggleBattlefieldMinimap` UIParent.xml's (ref
// UIParent.lua l.216-221). Defaults byte-real from `WTF\DefaultBindings.wtf`.
spec!(
"TOGGLEWORLDSTATESCORES",
INTERFACE,
Kind::Edge("ToggleWorldStateScoreFrame()"),
Some("SHIFT-SPACE"),
None
),
spec!(
"TOGGLEBATTLEFIELDMINIMAP",
INTERFACE,
Kind::Edge("ToggleBattlefieldMinimap()"),
Some("SHIFT-M"),
None
),
// ── Miscellaneous (BINDING_HEADER_MISC) ─────────────────────────────────────────────
spec!(
"MINIMAPZOOMIN",
@@ -1829,16 +1847,6 @@ pub(crate) static ABSENT: &[Absent] = &[
),
// ── Action bar ──────────────────────────────────────────────────────────────────────
// ── Interface ───────────────────────────────────────────────────────────────────────
absent!(
"TOGGLEWORLDSTATESCORES",
["ToggleWorldStateScoreFrame"],
"no battlegrounds"
),
absent!(
"TOGGLEBATTLEFIELDMINIMAP",
["ToggleBattlefieldMinimap"],
"no battlegrounds"
),
// ── Misc ────────────────────────────────────────────────────────────────────────────
absent!(
"TOGGLEFPS",
+3
View File
@@ -120,6 +120,7 @@ mod ui_action;
mod ui_auction;
mod ui_aura;
mod ui_bank;
mod ui_battlefield_score;
mod ui_bind_confirm;
mod ui_binder;
mod ui_cast;
@@ -209,6 +210,7 @@ use ui_action::UiActionPlugin;
use ui_auction::UiAuctionPlugin;
use ui_aura::UiAuraPlugin;
use ui_bank::UiBankPlugin;
use ui_battlefield_score::BattlefieldScorePlugin;
use ui_binder::UiBinderPlugin;
use ui_cast::UiCastPlugin;
use ui_char::UiCharPlugin;
@@ -641,6 +643,7 @@ pub fn run(build: BuildId) -> AppExit {
// The dialog engine's own verbs (decision 1963): the pet trainer's question, the instance
// boot clock, the area spirit healer, the battleground queue, the meeting stone.
.add_plugins(UiDialogVerbsPlugin)
.add_plugins(BattlefieldScorePlugin)
// Being summoned (decision 1747): SMSG_SUMMON_REQUEST's latch, the CONFIRM_SUMMON dialog it
// raises, and the CMSG_SUMMON_RESPONSE its Accept sends. The binder's twin one line up — a
// server-asked question whose only wire answer is yes — and here for that reason.
+8
View File
@@ -1274,6 +1274,12 @@ pub(crate) enum ClientCommand {
map_id: u32,
accept: bool,
},
/// `RequestBattlefieldScoreData()` — `MSG_PVP_LOG_DATA`, empty (decision 1972).
RequestBattlefieldScoreData,
/// `LeaveBattlefield()` — `CMSG_LEAVE_BATTLEFIELD`: the active slot's map (decision 1972).
LeaveBattlefield {
map_id: u32,
},
/// `CancelMeetingStoneRequest()` — `CMSG 0x293`, empty.
MeetingStoneLeave,
/// `ConfirmPetUnlearn()` — `CMSG_PET_UNLEARN` with the latched trainer guid.
@@ -1567,6 +1573,8 @@ pub(crate) enum ClientCommand {
receiver: String,
subject: String,
body: String,
/// The selected `Stationery.dbc` id (`CMSG_SEND_MAIL`'s sixth field, 1970).
stationery: u32,
item_guid: u64,
money: u32,
cod: u32,
+3
View File
@@ -258,6 +258,7 @@ pub(super) fn apply_net_updates(
ResMut<crate::ui_dialog_verbs::AreaSpiritHealer>,
ResMut<crate::ui_dialog_verbs::BattlefieldQueue>,
ResMut<crate::ui_dialog_verbs::MeetingStone>,
ResMut<crate::ui_battlefield_score::BattlefieldScoreboard>,
),
),
// The guard's directions marker (`SMSG_GOSSIP_POI`) and the map id it has to be
@@ -481,6 +482,7 @@ pub(super) fn apply_net_updates(
mut area_spirit,
mut battlefield_queue,
mut meeting_stone,
mut battlefield_scoreboard,
),
),
mut poi_marker,
@@ -959,6 +961,7 @@ pub(super) fn apply_net_updates(
area_spirit.on_time(healer, ms, std::time::Instant::now());
}
SessionEvent::BattlefieldStatus(status) => battlefield_queue.apply(status),
SessionEvent::PvpLogData(data) => battlefield_scoreboard.apply(data),
SessionEvent::MeetingStoneSetQueue { area, status } => {
meeting_stone.apply(area, status);
}
+53 -5
View File
@@ -4,7 +4,7 @@
//! dispatcher. The `UiScript` events these ultimately drive are fired by [`crate::ui_mail::feed_mail`]
//! (the feed owns the VM), so these arms only mutate resources + send the wire re-syncs.
use benilla_protocol::messages::{mail_action, mail_error, MailListEntry};
use benilla_protocol::messages::{mail_action, mail_error, mail_message_type, MailListEntry};
use crate::net::{ClientCommand, NetCommands};
use crate::ui_items::{EquipError, EquipErrors};
@@ -42,14 +42,43 @@ pub(super) fn mail_list(mails: Vec<MailListEntry>, mail: &mut MailOpen, commands
.collect();
}
/// `MAIL_CHECK_MASK_COD_PAYMENT` — the `checked` bit (`byte[rec+0x148] & 8`) that marks a mail as
/// the money a COD taker paid; taking that money empties the mail (wow-re, 1970).
const CHECKED_COD_PAYMENT: u32 = 8;
/// The two take legs' "this mail is now empty" decision, off the client's own bytes (wow-re
/// `stationery-bindings.md` §8, 1970): a take that empties the mail sends `CMSG_MAIL_DELETE`
/// itself, then `CLOSE_INBOX_ITEM(index)`, then `MAIL_INBOX_UPDATE`. The money leg (`0x4ad6b0`)
/// purges iff the mail is a COD payment, or an auction notice with no item; the item leg
/// (`0x4ad7b0`) purges iff the money is gone and the mail is an auction notice or carries no
/// letter text. A plain letter you took the money from stays open, husk and all — that is the
/// reference too (the stock `OpenMailFrame_OnHide` deletes a copied, emptied letter on close).
///
/// The item leg's second conjunct reads `[rec+0x114] == 0 && [rec+0x25c] == 0`; `+0x114` is the
/// letter's text id and `+0x25c` a second no-text field the carve did not name (INFERRED to be the
/// fetched body). This takes the text id alone, which can only purge a mail the client also would
/// when that second field is zero whenever the first is.
fn take_empties(entry: &MailListEntry, action: u32) -> bool {
let auction = entry.message_type == mail_message_type::AUCTION;
match action {
mail_action::MONEY_TAKEN => {
entry.checked & CHECKED_COD_PAYMENT != 0 || (auction && entry.item.is_none())
}
mail_action::ITEM_TAKEN => entry.money == 0 && (auction || entry.item_text_id == 0),
_ => false,
}
}
/// `SessionEvent::SendMailResult` (`SMSG_SEND_MAIL_RESULT`) — route per action/error (decision 0544
/// P2). action == SEND queues a [`MailSendAck`] for the feed (MAIL_FAILED always, MAIL_SEND_SUCCESS
/// on OK, else the red error line). A successful take/return/delete re-syncs the inbox with a fresh
/// `CMSG_GET_MAIL_LIST` (the reference client's inbox-refresh moment); an EQUIP_ERROR routes to the
/// existing inventory-error surface; any other failure surfaces the red error line.
/// on OK, else the red error line). A successful take applies to the local row and, when it
/// empties the mail, deletes it the way the client does ([`take_empties`]); every successful
/// take/return/delete then re-syncs the inbox with a fresh `CMSG_GET_MAIL_LIST` (the reference
/// client's inbox-refresh moment); an EQUIP_ERROR routes to the existing inventory-error surface;
/// any other failure surfaces the red error line.
#[allow(clippy::too_many_arguments)]
pub(super) fn send_mail_result(
_mail_id: u32,
mail_id: u32,
action: u32,
error: u32,
equip_error: Option<u32>,
@@ -82,6 +111,25 @@ pub(super) fn send_mail_result(
// A take/return/delete result.
match error {
mail_error::OK => {
// The take landed on the local row first (the client clears `[rec+0x140]` /
// `[rec+0x120]` before deciding), and an emptied mail is purged client-side:
// `CMSG_MAIL_DELETE`, then `CLOSE_INBOX_ITEM(index)` from the feed (1970).
if let Some(pos) = mail.mails.iter().position(|e| e.message_id == mail_id) {
match action {
mail_action::MONEY_TAKEN => mail.mails[pos].money = 0,
mail_action::ITEM_TAKEN => mail.mails[pos].item = None,
_ => {}
}
if take_empties(&mail.mails[pos], action) {
if let Some(mailbox) = mail.mailbox {
let _ = commands
.0
.send(ClientCommand::MailDelete { mailbox, mail_id });
}
mail.close_inbox.push(pos as u32 + 1);
mail.mails.remove(pos);
}
}
// Re-sync the inbox: the taken money/item or removed row is gone server-side.
if let Some(mailbox) = mail.mailbox {
let _ = commands.0.send(ClientCommand::GetMailList { mailbox });
+9 -4
View File
@@ -1071,6 +1071,10 @@ fn writer_loop(
ClientCommand::BattlefieldPort { map_id, accept } => {
w.battlefield_port(map_id, accept)
}
ClientCommand::RequestBattlefieldScoreData => {
w.request_battlefield_score_data()
}
ClientCommand::LeaveBattlefield { map_id } => w.leave_battlefield(map_id),
ClientCommand::MeetingStoneLeave => w.meeting_stone_leave(),
ClientCommand::PetUnlearn { trainer } => w.pet_unlearn(trainer),
ClientCommand::BankerActivate { guid } => w.banker_activate(guid),
@@ -1162,15 +1166,16 @@ fn writer_loop(
receiver,
subject,
body,
stationery,
item_guid,
money,
cod,
} => w.send_mail(
mailbox, &receiver, &subject, &body,
// stationery/package: vmangos discards both — player mail is always
// stored MAIL_STATIONERY_DEFAULT (41, decision 0544) regardless of what
// rides the wire here.
41, 0, item_guid, money, cod,
// The stationery the player selected (1970) and package 0 — vmangos
// discards both and stores MAIL_STATIONERY_DEFAULT (41, decision 0544),
// but the wire carries what the client chose, as the reference's does.
stationery, 0, item_guid, money, cod,
),
ClientCommand::MailTakeMoney { mailbox, mail_id } => {
w.mail_take_money(mailbox, mail_id)
+22 -7
View File
@@ -162,7 +162,7 @@ pub(super) fn refresh_hud_snapshot(
/// right, the action bar the bottom, the chat dock the bottom left).
///
/// **Asked of the frame, not recomputed from its numbers.** Its row pitch and anchor live in
/// `assets/ui/WorldStateFrame.xml`; mirroring them here would be two copies to keep in step, so
/// the stock `WorldStateFrame.xml` (1972); mirroring them here would be two copies to keep in step, so
/// this reads the resolved edge the layout actually produced (`GetBottom`, y-up). One tiny chunk
/// at 4 Hz, only while the HUD is drawing — the same shape as [`crate::hover_log`]'s tooltip
/// probe.
@@ -181,12 +181,25 @@ pub(super) fn refresh_hud_snapshot(
/// (~47 µs on a 200-frame tree — `layout_methods::settle`'s own measurement), four times a second.
/// Every other frame it is a chunk load and a table lookup.
pub(crate) fn top_centre_claimed(script: &UiScript, win_h: f32) -> f32 {
// The stock `WorldStateAlwaysUpFrame` is a permanently shown container (1972); what the
// readout actually occupies is its ROWS — `AlwaysUpFrame<n>`, built on demand and hidden when
// the scope admits nothing — so the claim is the lowest shown row's bottom, or nothing.
const CHUNK: &str = r#"
local f = WorldStateAlwaysUpFrame
if not (f and f:IsVisible()) then return -1 end
local bottom, screen = f:GetBottom(), GetScreenHeight()
if not bottom or not screen or screen <= 0 then return -1 end
return (screen - bottom) / screen
local lowest, i = nil, 1
while true do
local r = getglobal("AlwaysUpFrame" .. i)
if not r then break end
if r:IsShown() then
local b = r:GetBottom()
if b and (not lowest or b < lowest) then lowest = b end
end
i = i + 1
end
local screen = GetScreenHeight()
if not lowest or not screen or screen <= 0 then return -1 end
return (screen - lowest) / screen
"#;
let frac: f32 = script.eval::<f64>(CHUNK).unwrap_or(-1.0) as f32;
if !(0.0..=1.0).contains(&frac) {
@@ -349,10 +362,12 @@ mod tests {
claimed > 8.0,
"two rows reach past the pill's own seat, so the pill must move: {claimed}"
);
// The frame's own geometry, read back the way the probe reads it: the container's top offset
// plus one row per pushed row. Asserted against the XML rather than restated as constants.
// The frame's own geometry, read back the way the probe reads it: the second (lowest)
// row's resolved bottom. Asserted against the stock XML rather than restated as constants.
let expected: f32 = s
.eval::<f64>("return (20 + WORLD_STATE_ROW_HEIGHT * 2 + 15) / GetScreenHeight()")
.eval::<f64>(
"return (GetScreenHeight() - AlwaysUpFrame2:GetBottom()) / GetScreenHeight()",
)
.expect("the frame's own numbers") as f32
* SCREEN_H;
assert!(
+1 -1
View File
@@ -371,7 +371,7 @@ pub(super) fn sync_dressup_booth(
//
// And the other half of `SetRotation`: the turn-in-place shuffle
// ([`super::booth::drive_booth_turn`], 1559). The dressing room wires the same held-arrow
// `OnUpdate` the character window does (`BenillaDressUpModel_OnUpdate`), so it steps its feet
// `OnUpdate` the character window does (the stock `Model_OnUpdate`, 1969), so it steps its feet
// the same way. Keyed on the yaw alone — this block also runs for a re-bake, which is a
// `RefreshUnit` in the reference and does not turn the model.
if booth.turn.faced != Some(preview.yaw) {
+9 -3
View File
@@ -160,8 +160,9 @@ const STABLE_SLOT: &str = "stable";
///
/// The names, and where each is declared: `CharacterModelFrame` (stock `PaperDollFrame.xml`),
/// `PetModelFrame` (stock `PetPaperDollFrame.xml`), `PetStableModel` (stock `PetStable.xml`, the
/// reference's own file since 1751), `DressUpModel` (stock `DressUpFrame.xml`'s name, still our
/// file), and `InspectModelFrame` (stock `InspectPaperDollFrame.xml`, out of the LoadOnDemand
/// reference's own file since 1751), `DressUpModel` (stock `DressUpFrame.xml`, 1969),
/// `AuctionDressUpModel` (the auction addon's `Blizzard_AuctionDressUp.xml`, 1971), and
/// `InspectModelFrame` (stock `InspectPaperDollFrame.xml`, out of the LoadOnDemand
/// `Blizzard_InspectUI` — the reference's own since 1832).
///
/// **Every name here is now the reference's.** This note used to carry a `BenillaInspectModelFrame`
@@ -169,12 +170,17 @@ const STABLE_SLOT: &str = "stable";
/// take the name from. It is not packed — the `.pub` is only what the loose
/// `Interface\AddOns\Blizzard_InspectUI\` folder holds, while the real `.xml` and `.lua` sit
/// inside `patch.MPQ`, which the chain mounts. 1832 migrated the window and the prefix went with it.
const MODEL_PANE_BOOTHS: [(&str, &str); 5] = [
const MODEL_PANE_BOOTHS: [(&str, &str); 6] = [
("CharacterModelFrame", PAPERDOLL_SLOT),
("PetModelFrame", PETDOLL_SLOT),
("InspectModelFrame", INSPECT_SLOT),
("PetStableModel", STABLE_SLOT),
("DressUpModel", dressup::DRESSUP_SLOT),
// The auction house's embedded dressing room (`Blizzard_AuctionDressUp.xml`, 1971) — a second
// `<DressUpModel>` sharing the one booth: the app keeps ONE dressing-room look (its
// `TryOn`/`Dress` intents are not per-widget), so the two panes show the same substitutions.
// A deliberate approximation, named: the reference's two widgets each clone their own model.
("AuctionDressUpModel", dressup::DRESSUP_SLOT),
];
/// The booth a named model pane samples, or `None` for a pane no window has claimed.
+18 -9
View File
@@ -576,6 +576,7 @@ fn feed_auction(
mut last_open: Local<crate::ui_script::VmMemo<Option<u64>>>,
mut last_can_query: Local<crate::ui_script::VmMemo<bool>>,
mut last_sell: Local<crate::ui_script::VmMemo<Option<(i64, u32)>>>,
mut last_classes: Local<crate::ui_script::VmMemo<Vec<AuctionCategory>>>,
) {
let Some(mut script) = script else {
return;
@@ -585,6 +586,17 @@ fn feed_auction(
let last_can_query = last_can_query.get(&script);
let last_sell = last_sell.get(&script);
// The Browse tab's class tree is login-scoped: the stock `AuctionFrameBrowse_OnLoad` reads
// `GetAuctionItemClasses()` at the addon's LOAD — the first auctioneer, before the session's
// snapshot lands — and the client answers off `ItemClass.dbc` with no session at all (1971).
// Pushed once the two catalogs exist and whenever they change (a diff, like the inbox's).
let classes_now = categories(catalogs.0.as_deref(), catalogs.1.as_deref());
let last_classes = last_classes.get(&script);
if *last_classes != classes_now {
*last_classes = classes_now.clone();
script.set_auction_item_classes(classes_now);
}
// The queued client messages (decision 1523). Resolved against the VM's own GlobalStrings and
// shown on the surface the message's catalog row names — the `ui_quest` shape (0669).
//
@@ -622,7 +634,6 @@ fn feed_auction(
}
let self_guid = self_q.iter().next().map(|g| g.0);
let (classes, subclasses) = (&catalogs.0, &catalogs.1);
let rolls = crate::items::RollCatalogs {
props: catalogs.2.as_deref(),
enchants: catalogs.3.as_deref(),
@@ -647,7 +658,6 @@ fn feed_auction(
});
AuctionState {
lists,
categories: categories(classes.as_deref(), subclasses.as_deref()),
// The rate this house charges, keyed by the id the hello reply carried.
deposit_percent: houses
.as_deref()
@@ -681,13 +691,12 @@ fn feed_auction(
// One routine invalidates all three lists in the reference too — the three events fire
// together rather than being diffed apart, and each tab's handler repaints only if it
// is the visible one.
for event in [
"AUCTION_ITEM_LIST_UPDATE",
"AUCTION_BIDDER_LIST_UPDATE",
"AUCTION_OWNED_LIST_UPDATE",
] {
script.fire_event(event, vec![]);
}
// Three literal fires, not a loop over names: the producer tripwire
// (`reference_ui::every_event_a_chain_file_registers_has_a_producer`) reads fire
// sites by their literal, and the stock addon registers all three (1971).
script.fire_event("AUCTION_ITEM_LIST_UPDATE", vec![]);
script.fire_event("AUCTION_BIDDER_LIST_UPDATE", vec![]);
script.fire_event("AUCTION_OWNED_LIST_UPDATE", vec![]);
}
}
*last = fresh;
@@ -0,0 +1,238 @@
//! The battleground **scoreboard** feed (decision 1972; wow-re `battlefield-verb-family.md`):
//! the app's half of the stock `WorldStateFrame.lua` score frame — the name-resolution barrier,
//! the team derivation, the column headers, the request throttle and the leave.
//!
//! - **The board arrives raw** (`MSG_PVP_LOG_DATA`, GUIDs and numbers, wire order) and the
//! reference does nothing with it until EVERY row's name has resolved (§6.1: the last name
//! arrival is what rebuilds and fires). Ours asks the name cache for each row every frame it
//! is unresolved and pushes the board the frame the last one lands.
//! - **Team is derived, never wire data** (§6.2): race → faction; `0` Horde, `1` Alliance, `-1`
//! for a race the tables do not carry. Race and class strings come off the same traits the
//! name query answered with.
//! - **Columns are `WorldStateUI.dbc` rows** (`worldstate-ui-law.md`, the `0x2D4` status-3 arm):
//! in table order, the first contiguous run of rows whose `MapID` is the battleground's map or
//! `-1` and whose `Type` is 2; the text raw, the icon, the tooltip.
//! - **`UPDATE_BATTLEFIELD_SCORE`** fires here — on a pushed board, and on the status-3 arrival
//! the queue flags — BEFORE the queue feed fires `UPDATE_BATTLEFIELD_STATUS` (§4.2's order).
//! - **The request is throttled to 5000 ms** (§5.1) and the leave carries the active map (§5.3).
use std::time::{Duration, Instant};
use bevy::prelude::*;
use benilla_protocol::messages::PvpLogData;
use benilla_ui::script::{BattlefieldScoreRow, BattlefieldScores, BattlefieldStatColumn, UiScript};
use crate::names::NameCache;
use crate::net::{ClientCommand, NetCommands};
use crate::ui_dialog_verbs::BattlefieldQueue;
use crate::ui_script::UiInput;
use crate::world_state_ui::WorldStateUiRes;
/// `RequestBattlefieldScoreData`'s throttle — `0x4aa170`: `now + 0x1388`.
const REQUEST_THROTTLE: Duration = Duration::from_millis(5000);
/// The last `MSG_PVP_LOG_DATA`, and the request stamp.
#[derive(Resource, Default)]
pub(crate) struct BattlefieldScoreboard {
log: Option<PvpLogData>,
last_request: Option<Instant>,
}
impl BattlefieldScoreboard {
/// `SessionEvent::PvpLogData` — the whole board, replacing the last.
pub(crate) fn apply(&mut self, data: PvpLogData) {
self.log = Some(data);
}
}
/// The scoreboard's column headers for `map`: `WorldStateUI.dbc` in table order, the first
/// contiguous run of `(MapID == map || MapID == -1) && Type == 2` — once a run has started, the
/// first non-matching row ends it (`0x4aa9fe`).
pub(crate) fn score_columns(catalog: &WorldStateUiRes, map: u32) -> Vec<BattlefieldStatColumn> {
let mut out = Vec::new();
for (_, row) in catalog.0.rows() {
let matches = (row.map_id == map || row.map_id == u32::MAX) && row.ui_type == 2;
if matches {
out.push(BattlefieldStatColumn {
text: row.text.clone(),
icon: row.icon.clone(),
tooltip: row.tooltip.clone(),
});
} else if !out.is_empty() {
break;
}
}
out
}
/// Resolve the raw board through the name cache; `None` while any name is still in flight.
fn resolve_board(
log: &PvpLogData,
names: &mut NameCache,
commands: &NetCommands,
) -> Option<Vec<BattlefieldScoreRow>> {
let mut rows = Vec::with_capacity(log.rows.len());
for r in &log.rows {
let name = names.resolve(r.guid, commands)?.to_string();
let (race, class) = names
.player_traits(r.guid)
.map_or((0, 0), |(race, class, _)| (race, class));
// `0` Horde, `1` Alliance, `-1` neither — the reference's mask walk lands on the same
// three values (§6.2), and every playable race has a fixed side in 1.12.
let faction = match crate::ui_unit::race_faction_group(race) {
Some("Alliance") => 1,
Some(_) => 0,
None => -1,
};
let mut stats = [0u32; 8];
for (slot, v) in stats.iter_mut().zip(&r.stats) {
*slot = *v;
}
rows.push(BattlefieldScoreRow {
name,
killing_blows: r.killing_blows,
honorable_kills: r.honorable_kills,
deaths: r.deaths,
honor_gained: r.honor_gained,
faction,
rank: i32::try_from(r.rank).unwrap_or(i32::MAX),
race: crate::ui_unit::race_names(race).map(|(n, _)| n.to_string()),
class: crate::ui_unit::class_names(class).map(|(n, _)| n.to_string()),
stats,
});
}
Some(rows)
}
fn feed_battlefield_score(
script: Option<NonSendMut<UiScript>>,
board: Res<BattlefieldScoreboard>,
mut queue: ResMut<BattlefieldQueue>,
catalog: Option<Res<WorldStateUiRes>>,
mut names: ResMut<NameCache>,
commands: Res<NetCommands>,
mut last: Local<crate::ui_script::VmMemo<Option<BattlefieldScores>>>,
) {
let Some(mut script) = script else {
return;
};
let now = Instant::now();
script.set_battlefield_run_time_ms(queue.run_time_ms(now));
let fresh = board.log.as_ref().and_then(|log| {
let rows = resolve_board(log, &mut names, &commands)?;
let columns = queue
.active_map()
.zip(catalog.as_deref())
.map(|(map, cat)| score_columns(cat, map))
.unwrap_or_default();
Some(BattlefieldScores {
rows,
ended: log.ended,
winner: log.winner.unwrap_or(0),
columns,
})
});
let last = last.get(&script);
let status_rebuild = queue.take_score_dirty();
let changed = fresh.is_some() && fresh != *last;
if changed {
script.set_battlefield_scores(fresh.clone().expect("checked"));
*last = fresh;
}
if changed || status_rebuild {
script.fire_event("UPDATE_BATTLEFIELD_SCORE", vec![]);
}
}
fn drain_battlefield_score(
script: Option<NonSendMut<UiScript>>,
mut board: ResMut<BattlefieldScoreboard>,
queue: Res<BattlefieldQueue>,
commands: Res<NetCommands>,
) {
let Some(mut script) = script else {
return;
};
if script.take_battlefield_score_requests() > 0 {
let now = Instant::now();
let throttled = board
.last_request
.is_some_and(|t| now.duration_since(t) < REQUEST_THROTTLE);
if !throttled {
board.last_request = Some(now);
let _ = commands.0.send(ClientCommand::RequestBattlefieldScoreData);
}
}
if script.take_battlefield_leave_requests() > 0 {
let _ = commands.0.send(ClientCommand::LeaveBattlefield {
map_id: queue.active_map().unwrap_or(0),
});
}
}
pub(crate) struct BattlefieldScorePlugin;
impl Plugin for BattlefieldScorePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<BattlefieldScoreboard>().add_systems(
Update,
(
// Before the queue feed: the score event precedes the status event on the
// status-3 message, as in the client.
feed_battlefield_score
.before(crate::ui_dialog_verbs::feed_dialog_verbs)
.before(UiInput),
drain_battlefield_score.after(UiInput),
),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use benilla_formats::{WorldStateUiCatalog, WorldStateUiRow};
fn row(map_id: u32, ui_type: u32, text: &str) -> WorldStateUiRow {
WorldStateUiRow {
map_id,
area_id: 0,
icon: String::new(),
text: text.into(),
tooltip: String::new(),
state_variable: 0,
ui_type,
dynamic_icon: String::new(),
dynamic_tooltip: String::new(),
extended_ui: String::new(),
extended_ui_state: [0; 3],
}
}
/// The column scan: table order, the map's or the wildcard's `Type == 2` rows, and the first
/// non-matching row after the run has started ends it.
#[test]
fn the_columns_are_the_first_contiguous_run_of_type_two_rows_for_the_map() {
let cat = WorldStateUiRes(WorldStateUiCatalog::from_rows(vec![
(1, row(489, 0, "always-up")),
(2, row(529, 2, "other map")),
(3, row(489, 2, "Flags Captured")),
(4, row(u32::MAX, 2, "Flags Returned")),
(5, row(489, 0, "a gap")),
(6, row(489, 2, "after the gap")),
]));
let cols: Vec<String> = score_columns(&cat, 489)
.into_iter()
.map(|c| c.text)
.collect();
assert_eq!(cols, ["Flags Captured", "Flags Returned"]);
// Another map keeps the wildcard row alone — the reference's `-1` rows serve every map.
let cols: Vec<String> = score_columns(&cat, 30)
.into_iter()
.map(|c| c.text)
.collect();
assert_eq!(cols, ["Flags Returned"]);
}
}
+1 -1
View File
@@ -441,7 +441,7 @@ fn chat_vm() -> benilla_ui::script::UiScript {
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
// The UIMenu kit is the reference's own file since 1751 window 21, so this reads both
// stores through the one loader that speaks them.
+9
View File
@@ -285,6 +285,15 @@ fn feed_craft(
return;
}
script.set_craft(fresh.clone());
// The reagent templates `GetCraftReagentItemLink` reads, pre-asked as the trade-skill feed
// pre-asks its own (1973).
if let Some(f) = &fresh {
script.ask_item_templates(
f.recipes
.iter()
.flat_map(|r| r.reagents.iter().map(|re| re.item)),
);
}
match (&*last, &fresh) {
(None, Some(f)) => {
debug!("ui_craft: window opens — {} recipe(s)", f.recipes.len());
+45 -2
View File
@@ -135,18 +135,60 @@ impl AreaSpiritHealer {
pub(crate) struct BattlefieldQueue {
slots: [Option<BattlefieldStatus>; 3],
changed: bool,
/// The slot the player is IN (`[0x8457cc]`, the status-3 arm), and the instance's clocks
/// (`[0xb6ebb8]`/`[0xb6ebbc]`, §4.2): the run-time stamp `now − Δ₂` and the expiration
/// `now + Δ₁`. Any other status for that slot resets both (1972).
active: Option<(usize, u32)>,
run_started: Option<Instant>,
/// The status-3 arm rebuilds the scoreboard and fires `UPDATE_BATTLEFIELD_SCORE` before
/// `UPDATE_BATTLEFIELD_STATUS` (§4.2's ordering) — the score feed reads this first.
score_dirty: bool,
}
impl BattlefieldQueue {
/// `SMSG_BATTLEFIELD_STATUS`: an out-of-range slot aborts the handler; a zero map clears.
pub(crate) fn apply(&mut self, status: BattlefieldStatus) {
let Some(slot) = self.slots.get_mut(status.slot as usize) else {
let index = status.slot as usize;
let Some(slot) = self.slots.get_mut(index) else {
return;
};
match status.in_progress {
Some((_, elapsed_ms)) if status.map_id != 0 => {
self.active = Some((index, status.map_id));
self.run_started =
Some(Instant::now() - std::time::Duration::from_millis(u64::from(elapsed_ms)));
self.score_dirty = true;
}
_ => {
if self.active.is_some_and(|(i, _)| i == index) {
self.active = None;
self.run_started = None;
}
}
}
*slot = (status.map_id != 0).then_some(status);
self.changed = true;
}
/// The map of the battleground the player is in — `LeaveBattlefield`'s payload; `None` = 0.
pub(crate) fn active_map(&self) -> Option<u32> {
self.active.map(|(_, map)| map)
}
/// `GetBattlefieldInstanceRunTime()`: ms since the status-3 stamp, 0 with none.
pub(crate) fn run_time_ms(&self, now: Instant) -> u32 {
self.run_started.map_or(0, |t| {
now.saturating_duration_since(t)
.as_millis()
.min(u128::from(u32::MAX)) as u32
})
}
/// The status-3 arm's scoreboard rebuild, once per arrival.
pub(crate) fn take_score_dirty(&mut self) -> bool {
std::mem::take(&mut self.score_dirty)
}
/// The map id `AcceptBattlefieldPort` sends for a 1-based slot, if the slot holds a queue.
fn map_id(&self, index: u8) -> Option<u32> {
self.slots
@@ -173,7 +215,7 @@ impl MeetingStone {
}
}
fn feed_dialog_verbs(
pub(crate) fn feed_dialog_verbs(
script: Option<NonSendMut<UiScript>>,
mut pet: ResMut<PetUnlearnState>,
mut boot: ResMut<InstanceBoot>,
@@ -416,6 +458,7 @@ mod tests {
status: 2,
time_ms: Some(0),
in_progress: None,
queued: None,
};
q.apply(status(1, 489));
q.apply(status(5, 30));
+93 -7
View File
@@ -1,12 +1,14 @@
//! The app-side **dressing-room feed** (decision 1060): the bridge between the window's intents
//! (`BenillaDressUpModel_Dress/TryOn/Close`, [`DressUpIntent`]) and the booth's look
//! ([`DressUpPreview`]).
//! The app-side **dressing-room feed** (decisions 1060, 1969): the bridge between the stock
//! window's `DressUpModel` widget verbs ([`DressUpIntent`], queued engine-side) and the booth's
//! look ([`DressUpPreview`]).
//!
//! Three jobs, each frame, before the VM ticks ([`UiInput`]):
//!
//! - **Apply the intents, in order.** `Dress` drops every substitution (the ref's
//! `SetUnit("player")` on open and `Dress()` on Reset); `TryOn(id)` records one; `Close` empties
//! the room. Order matters — `DressUpItem` resets *then* tries on in the same breath.
//! `SetUnit("player")` on open and `Dress()` on Reset); `Undress` strips every worn piece but
//! the hands; `TryOn(id)` records one; `Close` — derived here from the window going invisible,
//! the stock file having no hook of ours — empties the room. Order matters — `DressUpItem`
//! resets *then* tries on in the same breath.
//! - **Resolve each tried-on item to a display.** An item id is all a `|Hitem:` link carries, so the
//! display id / inventory type come from the ask-once template cache
//! ([`Items::template`] — `CMSG_ITEM_QUERY_SINGLE` on a miss, exactly as the reference's
@@ -139,6 +141,10 @@ fn lanes_coexist(main: u8, off: u8) -> bool {
pub(crate) struct DressUpRoom {
/// Open? `false` = the window is closed and the booth stays empty (the `Close` intent).
open: bool,
/// `Undress()` has stripped the body: the player's own worn pieces are not part of the look
/// until the next `Dress`. The hands are untouched — the widget clears components bodyslots
/// `0..0xb` and no hand lane (wow-re `dressup-model-equipment.md` §5).
bare: bool,
/// Resolved substitutions by equipment slot — `(display id, inventory type)`, the same pair the
/// enum-shaped array carries.
worn: [Option<CharEnumItem>; 19],
@@ -159,16 +165,29 @@ impl DressUpRoom {
match intent {
DressUpIntent::Dress => {
self.open = true;
self.bare = false;
self.worn = Default::default();
self.held_order.clear();
self.pending.clear();
}
DressUpIntent::Undress => {
self.open = true;
self.bare = true;
// Every worn piece off — base (`bare`, read by `player_look`) and tried-on alike —
// while a held substitution, like a held base item, stays in its hand.
for slot in 0..self.worn.len() {
if !HELD_SLOTS.contains(&slot) {
self.worn[slot] = None;
}
}
}
DressUpIntent::TryOn(item) => {
self.open = true;
self.pending.push(item);
}
DressUpIntent::Close => {
self.open = false;
self.bare = false;
self.worn = Default::default();
self.held_order.clear();
self.pending.clear();
@@ -231,8 +250,23 @@ fn feed_dressup(
for intent in script.take_dressup_intents() {
room.apply(intent);
}
// The pane's rotate buttons own the yaw; the booth mirrors it (the paper doll's own law, 0208 §5).
preview.yaw = script.dressup_yaw();
// The window went away: the room empties and the booth stops baking. Read off the frame
// itself — the file is the reference's and has no hook of ours in its OnHide (1969); the
// reference's widget keeps its state while hidden, but its next `DressUpItem` re-issues
// `SetUnit("player")` precisely because the frame was not visible, so nothing kept is ever
// observable.
// Two windows drive the one room since 1971: the dressing room and the auction house's
// embedded dress-up pane (`AuctionDressUpFrame`); the booth empties when neither is up.
if room.open
&& !script.frame_visible("DressUpFrame")
&& !script.frame_visible("AuctionDressUpFrame")
{
room.apply(DressUpIntent::Close);
}
// The pane's rotate buttons own the yaw through the stock `Model_RotateLeft/Right/OnUpdate`,
// which write `DressUpModel:SetRotation`; the booth mirrors the pane (the paper doll's own
// law, 0208 §5 / 1751).
preview.yaw = script.model_pane_facing("DressUpModel");
room.resolve_pending(&mut items, &commands);
@@ -272,6 +306,11 @@ fn player_look(
equipment[idx] = worn;
continue;
}
// Undressed: the player's own worn pieces are off; the hands (attachments, not bodyslots)
// keep what they hold.
if room.bare && !HELD_SLOTS.contains(&idx) {
continue;
}
// …and past that point we are dressing the player's OWN gear, which is where the two
// equipment-display preferences apply — and where the reference applies them too, by
// construction rather than by a test (decision 1472). `DressUpModel::SetUnit 0x476cb0`
@@ -430,6 +469,53 @@ mod tests {
assert!(!room.open, "closing empties the room (the booth goes dark)");
}
/// `DressUpModel:Undress()` strips every worn piece — the player's own and a tried-on one —
/// and no hand lane (bodyslots `0..0xb` only, wow-re §5); a later try-on lands on the bare
/// body, and `Dress()` puts the player's own gear back.
#[test]
fn undress_strips_the_body_but_not_the_hands_and_a_try_on_lands_on_the_bare_body() {
let (cmds, _rx) = commands();
let mut items = Items::default();
items.insert_template(1000, Some(worn("Worn Chest", 5000, 5)));
items.insert_template(2000, Some(worn("Shiny Chest", 7000, 5)));
items.insert_template(3000, Some(worn("Sword", 6000, 13)));
items.insert_template(4000, Some(worn("Worn Helm", 8000, 1)));
let store = player(&[(4, 1000), (15, 3000), (0, 4000)]);
let mut room = DressUpRoom::default();
room.apply(DressUpIntent::Dress);
room.apply(DressUpIntent::TryOn(2000));
room.resolve_pending(&mut items, &cmds);
room.apply(DressUpIntent::Undress);
let look = player_look(&store, &net(), &room, &mut items, &cmds).unwrap();
assert_eq!(look.equipment[4].display_id, 0, "the tried-on chest is off");
assert_eq!(look.equipment[0].display_id, 0, "the worn helm is off");
assert_eq!(
look.equipment[15].display_id, 6000,
"the sword stays in the hand"
);
room.apply(DressUpIntent::TryOn(2000));
room.resolve_pending(&mut items, &cmds);
let look = player_look(&store, &net(), &room, &mut items, &cmds).unwrap();
assert_eq!(
look.equipment[4].display_id, 7000,
"a try-on lands on the bare body"
);
assert_eq!(
look.equipment[0].display_id, 0,
"…which stays bare elsewhere"
);
room.apply(DressUpIntent::Dress);
let look = player_look(&store, &net(), &room, &mut items, &cmds).unwrap();
assert_eq!(
look.equipment[4].display_id, 5000,
"Dress puts the player's own chest back"
);
assert_eq!(look.equipment[0].display_id, 8000);
}
/// An item whose template has not answered yet stays PENDING rather than previewing nothing:
/// linked-in-chat items are the normal case here, and the first click on one always misses the
/// cache. It lands on the frame the answer does — and the ask goes out exactly once.
+111 -24
View File
@@ -50,7 +50,7 @@ use std::collections::{HashMap, HashSet};
use benilla_protocol::messages::{mail_error, mail_message_type, MailListEntry};
use bevy::prelude::*;
use benilla_ui::script::{MailInboxRow, MailState, UiScript};
use benilla_ui::script::{MailInboxRow, MailState, ScriptValue, StationeryView, UiScript};
use crate::entities::ItemDisplays;
use crate::items::Items;
@@ -124,6 +124,9 @@ pub(crate) struct MailOpen {
last_list_query: Option<f64>,
/// SEND-action results the net bridge queued for the feed to fire ([`MailSendAck`]).
pub(crate) send_acks: Vec<MailSendAck>,
/// 1-based inbox rows a take just emptied and purged (`net::apply::mail`) — the feed fires
/// `CLOSE_INBOX_ITEM(index)` for each, ahead of the `MAIL_INBOX_UPDATE` the purge causes.
pub(crate) close_inbox: Vec<u32>,
/// Refusals (a take/return/delete failure) the feed shows, as message keys — resolved at the
/// feed, which is where the VM's `GlobalStrings` can be read
/// ([`crate::ui_action::keyed_line`]).
@@ -434,6 +437,51 @@ fn resolve_row(
}
/// Build the Lua-facing snapshot from [`MailOpen`] — `None` when no mailbox is open.
/// The usable stationery list (wow-re `stationery-bindings.md` §`0x4ad970`, 1970): every
/// `Stationery.dbc` row that is always available (`Flags & 1`) or whose item the player carries
/// (bags, not the bank — the client's `0x622270`), and whose item template is cached — the name,
/// icon and price come off the template — sorted by BuyPrice ascending, the client's comparator
/// (`0x4ada90`). A carried paper costs nothing to use (`cost` nil). Templates are asked once here
/// so the list fills from the second frame after world enter, mailbox or no mailbox.
fn stationeries(
catalog: &Stationery,
self_q: &Query<(&ObjectStore, &crate::net::Guid), With<SelfPlayer>>,
items: &mut Items,
icons: Option<&ItemDisplays>,
commands: &NetCommands,
) -> Vec<StationeryView> {
let store = self_q.iter().next().map(|(s, _)| s);
let mut out: Vec<(u32, StationeryView)> = Vec::new();
for row in catalog.0.rows() {
let Some(t) = items.template(row.item, 0, commands).cloned() else {
continue;
};
let carried = store.is_some_and(|s| {
crate::ui_items::count_of(
&s.0,
items,
row.item,
crate::ui_items::InventoryScope::CARRIED,
) > 0
});
if row.flags & 1 == 0 && !carried {
continue;
}
out.push((
t.buy_price,
StationeryView {
id: row.id,
name: t.name.clone(),
icon: crate::ui_items::item_icon(icons, t.display_info_id).unwrap_or_default(),
cost: (!carried).then_some(t.buy_price),
texture: row.texture.clone(),
},
));
}
out.sort_by_key(|(price, v)| (*price, v.id));
out.into_iter().map(|(_, v)| v).collect()
}
#[allow(clippy::too_many_arguments)] // mirrors `resolve_row`'s cache set
fn snapshot(
mail: &MailOpen,
@@ -472,6 +520,17 @@ fn snapshot(
/// Push the current mail into the VM and fire the show/update/close + send-result events on a
/// transition or content change (an async sender/item/body landing, a mark-read flip, a fresh
/// list). Diffed against a `Local` memory, exactly like the merchant/gossip feeds.
/// The feed's tail, bundled — the signature sits at the 16-SystemParam ceiling: the random-suffix
/// roll's catalogs (1547), where a refusal lands (1815), and the usable stationery list's memo
/// (1970), pushed on change like the inbox.
#[derive(bevy::ecs::system::SystemParam)]
struct MailFeedExtras<'w, 's> {
props: Option<Res<'w, crate::items::RandomProperties>>,
enchants: Option<Res<'w, crate::items::Enchants>>,
sink: crate::ui_action::MessageSink<'w>,
stationeries: Local<'s, crate::ui_script::VmMemo<Vec<StationeryView>>>,
}
#[allow(clippy::too_many_arguments)]
fn feed_mail(
script: Option<NonSendMut<UiScript>>,
@@ -490,18 +549,17 @@ fn feed_mail(
self_q: Query<(&crate::net::ObjectStore, &crate::net::Guid), With<crate::net::SelfPlayer>>,
states: Res<crate::world_state::WorldStates>,
// The random-suffix roll's catalogs (1547): an enclosed item's rolled name.
// The roll catalogs and the message sink, paired (the 16-SystemParam ceiling this signature
// sits at): the random-suffix roll (1547), and where a refusal lands (1815).
rolls_and_sink: (
Option<Res<crate::items::RandomProperties>>,
Option<Res<crate::items::Enchants>>,
crate::ui_action::MessageSink,
),
extras: MailFeedExtras,
) {
let Some(mut script) = script else {
return;
};
let (props, enchants, mut sink) = rolls_and_sink;
let MailFeedExtras {
props,
enchants,
mut sink,
stationeries: mut last_stationeries,
} = extras;
let rolls = crate::items::RollCatalogs {
props: props.as_deref(),
enchants: enchants.as_deref(),
@@ -520,6 +578,12 @@ fn feed_mail(
// MAIL_SEND_SUCCESS on OK (resets the form) or the refusal's own line on a failure. The
// success also SAYS so — `ERR_MAIL_SENT`, the yellow info line `0x4ad0b1` shows before it
// resets the form (decision 1821).
// A take emptied and purged a row: `CLOSE_INBOX_ITEM(index)` — the stock handler hides the
// open letter when it is that row — ahead of the `MAIL_INBOX_UPDATE` the changed list fires
// below, the client's own order (wow-re `stationery-bindings.md` §8, 1970).
for index in std::mem::take(&mut mail.close_inbox) {
script.fire_event("CLOSE_INBOX_ITEM", vec![ScriptValue::Int(i64::from(index))]);
}
for ack in std::mem::take(&mut mail.send_acks) {
script.fire_event("MAIL_FAILED", vec![]);
let key = if ack.ok {
@@ -565,7 +629,24 @@ fn feed_mail(
if changed {
script.set_mail(fresh.clone());
}
// The send tab's usable stationery (1970): `Stationery.dbc` rows the player may use — the
// always-available one, or one whose item they carry — whose item template is cached, priced
// by BuyPrice ascending. The templates are asked ahead of any mailbox (the catalog is five
// rows) so the list is whole the frame the window opens: the stock `SendMailFrame_Reset`
// selects row 1 on show, and an empty list then would leave every send silently unsent.
let usable = stationery
.as_deref()
.map(|catalog| stationeries(catalog, &self_q, &mut items, icons.as_deref(), &commands))
.unwrap_or_default();
let memo = last_stationeries.get(&script);
if *memo != usable {
*memo = usable.clone();
script.set_mail_stationeries(usable);
}
if opened {
// The selection is cleared on open AND close (`0x4ace07`, 1970); the stock tab's reset
// on MAIL_SHOW re-selects row 1.
script.clear_stationery();
script.fire_event("MAIL_SHOW", vec![]);
// The compose tab resets on open — a fresh window carries no stale attachment/money.
script.clear_send_mail_item();
@@ -573,6 +654,7 @@ fn feed_mail(
// MAIL_SEND_SUCCESS on open too, so the page-turn sound on open is faithful, not a bug.
script.fire_event("MAIL_SEND_SUCCESS", vec![]);
} else if closed {
script.clear_stationery();
script.fire_event("MAIL_CLOSED", vec![]);
// The close core's tail (wow-re `0x4acdad`/`0x4acdb1`, decision 0913): a session where a
// mail was read — or one arrived while the window was open — re-asks the server, and the
@@ -728,23 +810,28 @@ fn drain_mail(
// SendMail: resolve the attachment's (bag, slot) to the wire item guid at send time (the
// reference re-reads the slot when the send fires — a lazy resolve, decision 0216/0544).
if let Some(req) = script.take_mail_send() {
let item_guid = req
.item
.and_then(|(bag, slot)| {
self_q.iter().next().and_then(|s| {
crate::ui_items::slot_guid(&s.0, bag, (slot.max(1) - 1) as u8, &items)
})
let item_guid = req.item.and_then(|(bag, slot)| {
self_q.iter().next().and_then(|s| {
crate::ui_items::slot_guid(&s.0, bag, (slot.max(1) - 1) as u8, &items)
})
.unwrap_or(0);
let _ = commands.0.send(ClientCommand::SendMail {
mailbox,
receiver: req.target,
subject: req.subject,
body: req.body,
item_guid,
money: req.money,
cod: req.cod,
});
if req.item.is_some() && item_guid.is_none() {
// The attached item is gone from its slot: `ERR_ITEM_NOT_FOUND`, no packet, the
// attachment dropped and `MAIL_SEND_INFO_UPDATE` fired (`0x4ae98d`, 1970).
mail.errors.push("ERR_ITEM_NOT_FOUND");
script.drop_send_mail_item();
} else {
let _ = commands.0.send(ClientCommand::SendMail {
mailbox,
receiver: req.target,
subject: req.subject,
body: req.body,
stationery: req.stationery,
item_guid: item_guid.unwrap_or(0),
money: req.money,
cod: req.cod,
});
}
}
if script.take_mail_close() {
@@ -24,7 +24,7 @@ fn shipped_action_bar_drives_end_to_end() {
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
let frames = super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml")
+ super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml")
@@ -190,7 +190,7 @@ fn load_action_bar(s: &UiScript) {
super::test_ui::load_ui(s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(s, "GameTooltip.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\GameTooltip.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -1359,7 +1359,7 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
@@ -1528,7 +1528,7 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -1750,7 +1750,7 @@ fn bonus_bar_turnaround_continues_from_position() {
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -1832,7 +1832,7 @@ fn the_page_arrows_do_not_steal_each_other_s_clicks() {
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
+27 -13
View File
@@ -390,16 +390,13 @@ const BLIZZARD_ADDONS: [&str; 12] = [
];
/// The Blizzard addons the chain carries as LoadOnDemand registry rows — the ones the reference
/// reaches through `LoadAddOn` from `UIParentLoadAddOn` (1957). Two exclusions, both by shape:
/// an addon `benilla.toc` lists with everything else (the eager trio the InspectUI header
/// describes) already loaded and is not a row here; an addon whose toc is not LoadOnDemand
/// would need the startup walk to read the chain, which it does not yet — that one stays with
/// its own file until its window migrates.
/// reaches through `LoadAddOn` from `UIParentLoadAddOn` (1957). `benilla.toc` lists none of them,
/// like the reference's own `FrameXML.toc` (1967). One exclusion, by shape: an addon whose toc is
/// not LoadOnDemand would need the startup walk to read the chain, which it does not yet — that
/// one stays with its own file until its window migrates.
fn chain_addons() -> Vec<Addon> {
let eager = super::manifest::chain_addons(&Addon::builtin().toc.files);
BLIZZARD_ADDONS
.iter()
.filter(|name| !eager.iter().any(|e| e.eq_ignore_ascii_case(name)))
.filter_map(|name| {
let bytes = super::reference_ui::read(&format!("Interface/AddOns/{name}/{name}.toc"))?;
let toc = Toc::parse(&benilla_ui::source::decode(&bytes));
@@ -982,8 +979,9 @@ impl Walk {
#[cfg(test)]
mod tests {
/// The chain's Blizzard LoadOnDemand addons are registry rows (1957): every row is
/// LoadOnDemand and read off the chain, `Blizzard_TrainerUI` is among them, and an addon
/// the manifest lists eagerly is not a row twice.
/// LoadOnDemand and read off the chain, the eight windows the interface opens through them
/// are among the rows, and the manifest lists no addon at all — the reference's own
/// `FrameXML.toc` has none, every Blizzard addon loads on demand (1967).
#[test]
fn the_chain_carries_blizzards_load_on_demand_addons_as_registry_rows() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -1006,13 +1004,29 @@ mod tests {
a.name
);
}
let eager = super::super::manifest::chain_addons(&Addon::builtin().toc.files);
for e in &eager {
for name in [
"Blizzard_AuctionUI",
"Blizzard_CraftUI",
"Blizzard_InspectUI",
"Blizzard_MacroUI",
"Blizzard_RaidUI",
"Blizzard_TalentUI",
"Blizzard_TradeSkillUI",
"Blizzard_TrainerUI",
] {
assert!(
!rows.iter().any(|a| a.name.eq_ignore_ascii_case(e)),
"{e} is listed in benilla.toc and must not also be a row"
rows.iter().any(|a| a.name == name),
"{name} is a row the interface opens on demand"
);
}
assert!(
!Addon::builtin()
.toc
.files
.iter()
.any(|f| f.replace('/', "\\").starts_with("Interface\\AddOns\\")),
"benilla.toc lists a Blizzard addon eagerly; the reference loads every one on demand"
);
// The glue's list is the player's folder alone.
assert!(installed(None)
.iter()
@@ -405,6 +405,10 @@ fn bag_tooltip_hangs_left_when_the_slot_sits_in_the_right_half() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The stock tooltip declares no size: it sizes from its lines through the font engine, as
// the client's does (1968) — a harness that reads its rect needs one; the fixed-width
// font is that engine here.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
for file in BAG_UI {
load_xml(&s, file);
}
@@ -79,7 +79,7 @@ fn shipped_xml() -> Vec<(String, String)> {
out.sort();
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751), so
// the floor sits well under the count rather than one step above it (1956).
assert!(out.len() >= 20, "only {} xml files swept", out.len());
assert!(out.len() >= 15, "only {} xml files swept", out.len());
out
}
@@ -32,7 +32,7 @@ fn harness() -> UiScript {
// does not, so a session without the tooltip loses the whole repaint.
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
// `SecondsToTimeAbbrev`, which 1.12 keeps in UIParent.lua and so do we since window 18.
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
@@ -394,7 +394,7 @@ fn managed_positions_track_the_bottom_bar_stack() {
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml"); // the dropdown kit's MenuBackdrop reads TOOLTIP_DEFAULT_COLOR
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // the dropdown kit's MenuBackdrop reads TOOLTIP_DEFAULT_COLOR
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
@@ -160,6 +160,10 @@ fn shipped_character_frame_drives_end_to_end() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The stock tooltip declares no size: it sizes from its lines through the font engine, as
// the client's does (1968) — a harness that reads its rect needs one; the fixed-width
// font is that engine here.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
for f in super::test_ui::CHARACTER_UI {
super::test_ui::load_ui_strict(&s, f);
}
@@ -1207,7 +1211,7 @@ fn an_addons_tab_click_selects_through_the_generic_entry_point() {
// The reference's `PanelTemplates_SelectTab` ends with `if GameTooltip:IsOwned(tab)` —
// an arm our deleted copy omitted ("our tabs set no tooltip"), so selecting a tab now needs
// the tooltip to exist (1860).
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
@@ -44,7 +44,7 @@ fn chat_with_menu() -> UiScript {
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -22,7 +22,7 @@ fn chat_ui() -> UiScript {
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -21,7 +21,7 @@ fn chat_frame() -> UiScript {
// the dropdown kit's own dependency — its MenuBackdrop reads `TOOLTIP_DEFAULT_COLOR`.)
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
@@ -1002,7 +1002,7 @@ fn the_chat_menu_builds_its_rows_on_the_references_kit() {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
r"Interface\FrameXML\UIMenu.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
@@ -22,7 +22,7 @@ fn picker() -> UiScript {
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -22,7 +22,7 @@ fn load_combo_frame() -> UiScript {
load_xml(&s, "UIParent.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
@@ -68,7 +68,7 @@ fn setup() -> UiScript {
// The stock DELETE_GOOD_ITEM's OnHide hands focus back to `ChatFrameEditBox` (1960).
load_xml(&s, r"Interface\FrameXML\UIMenu.xml");
load_xml(&s, r"Interface\FrameXML\ChatFrame.xml");
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
load_xml(&s, r"Interface\FrameXML\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, r"Interface\FrameXML\FloatingChatFrame.xml"); // declares ChatFrameEditBox
@@ -40,13 +40,15 @@ const ROOM_UI: &[&str] = &[
"Interface\\FrameXML\\ItemRef.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\StackSplitFrame.xml",
"DressUpFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
// The reference's own room (1969): its Close/Reset buttons inherit the panel kit's templates,
// which resolve at load — so after it, as the manifest has it.
"Interface\\FrameXML\\DressUpFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\FloatingChatFrame.xml",
@@ -69,7 +71,7 @@ fn load_room(s: &UiScript) {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Cooldown.xml",
] {
load_xml(s, file);
@@ -114,7 +116,6 @@ fn shown_paper_doll() -> UiScript {
super::test_ui::load_ui_strict(&s, file);
}
for file in [
"DressUpFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\UIMenu.xml",
@@ -122,6 +123,8 @@ fn shown_paper_doll() -> UiScript {
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
// The reference's room (1969), after the panel kit its buttons inherit from.
"Interface\\FrameXML\\DressUpFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\FloatingChatFrame.xml",
@@ -416,9 +419,9 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
let _ = s.take_dressup_intents();
s.resolve();
// The pane's OnLoad seeded the ref's default facing.
// The pane's OnLoad (`Model_OnLoad`) seeded the ref's default facing on the widget itself.
assert!(
(s.dressup_yaw() - 0.61).abs() < 1e-6,
(s.model_pane_facing("DressUpModel") - 0.61).abs() < 1e-6,
"ref UIParent.lua:1422"
);
@@ -431,29 +434,28 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
);
// One tap of rotate-left: OnClick on press AND release, −0.03 each.
let before = s.dressup_yaw();
let before = s.model_pane_facing("DressUpModel");
let (x, y) = s
.eval::<(f32, f32)>(
"return (DressUpModelFrameRotateLeftButton:GetLeft() \
+ DressUpModelFrameRotateLeftButton:GetRight()) / 2, \
(DressUpModelFrameRotateLeftButton:GetTop() \
+ DressUpModelFrameRotateLeftButton:GetBottom()) / 2",
"return (DressUpModelRotateLeftButton:GetLeft() \
+ DressUpModelRotateLeftButton:GetRight()) / 2, \
(DressUpModelRotateLeftButton:GetTop() \
+ DressUpModelRotateLeftButton:GetBottom()) / 2",
)
.unwrap();
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
assert!(
(s.dressup_yaw() - (before - 0.06)).abs() < 1e-5,
(s.model_pane_facing("DressUpModel") - (before - 0.06)).abs() < 1e-5,
"a tap fires OnClick twice: {} → {}",
before,
s.dressup_yaw()
s.model_pane_facing("DressUpModel")
);
s.run("HideUIPanel(DressUpFrame)").unwrap();
assert_eq!(
s.take_dressup_intents(),
vec![DressUpIntent::Close],
"closing the window empties the booth"
);
// Closing the window is what empties the booth, and the app reads it off the frame — the
// stock file's OnHide plays its sound and queues nothing (1969).
assert!(!s.frame_visible("DressUpFrame"), "the room is hidden");
assert!(s.take_dressup_intents().is_empty());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -24,7 +24,7 @@ fn load_dropdown_kit(s: &UiScript) {
"UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
] {
load_xml(s, file);
@@ -26,7 +26,7 @@ fn harness() -> UiScript {
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"MinimapCluster.xml",
// The reference's own file. This module carried a private disk-only `load_xml` until the
// swap, which structurally could not name a chain entry — [`super::test_ui::load_ui`] is
@@ -403,7 +403,7 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"MinimapCluster.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\DurabilityFrame.xml",
@@ -114,7 +114,7 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -200,7 +200,7 @@ fn escape_closes_the_options_window_before_opening_the_menu() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml"); // the Keybindings page's faux-scroll kit (1008)
load_xml(&s, "KeyBindingsPage.xml");
@@ -19,7 +19,7 @@ fn exp_bar_harness() -> UiScript {
"UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Cooldown.xml",
@@ -39,6 +39,10 @@ fn exp_bar_harness() -> UiScript {
] {
super::test_ui::load_ui(&s, file);
}
// 1.12 ships detailed tips ON — `SHOW_NEWBIE_TIPS = "1"` is UIOptionsFrame_Init's (ref
// UIOptionsFrame.lua l.100; ours sits in OptionsFrame.xml's uvar block, 1968), and a harness
// without the options file says so itself, the way the reference's tooltip would read it.
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
s
}
@@ -185,13 +185,6 @@ const KNOWN: &[Known] = &[
// the reference inherits `TextStatusBar` — true of our transcription, and no longer true of
// anything. `TargetFrame.xml` is the reference's own now and its ToT bars inherit
// `TextStatusBar` like every other unit bar. The gate found them itself, which is its job.
Known {
frame: "WorldStateAlwaysUpFrame",
flag: Flag::Mouse,
why:
"the reference's PvP objective banner has OnEnter/OnLeave for its tooltip. Wants that \
handler, not the flag.",
},
// ── We take the mouse where the reference does not ─────────────────────────────────────────
//
// The merchant rows' divergence RETIRED (1751): our `MerchantFrame.xml` is gone and the
@@ -232,24 +225,13 @@ const KNOWN: &[Known] = &[
// frame it was built to report (1751 §5: the drift instruments retire with the copies).
// ── parent ─────────────────────────────────────────────────────────────────────────────────
//
// Three, and each is a seat inside the SAME tree the reference seats it in — which is the
// question this flag exists to ask (decision 1757). A frame whose seat crosses the boundary
// between UIParent's tree and the top level is a defect, because `SetFullScreenFrame` hides
// `UIParent` and everything below it; a frame seated one rung along inside that tree is not.
Known {
frame: "SendMailBodyEditBox",
flag: Flag::Parent,
why: "the reference interposes SendMailScrollChildFrame between the pane and its content; \
our mail panes are the render approximation MailFrame.xml names at the site (flat \
art, no live scrollbar), so the body hangs off SendMailScrollFrame directly. Wants \
the real scroll child, not a re-seat.",
},
Known {
frame: "OpenMailInvoiceFrame",
flag: Flag::Parent,
why: "as SendMailBodyEditBox — OpenMailScrollChildFrame is the scroll child we do not \
build",
},
// Empty since 1970. The rows that lived here were seats inside the SAME tree the reference
// seats them in — the question this flag exists to ask (decision 1757): a frame whose seat
// crosses the boundary between UIParent's tree and the top level is a defect, because
// `SetFullScreenFrame` hides `UIParent` and everything below it; a frame seated one rung
// along inside that tree is not. The last two were our mail transcription's flat body panes,
// which hung their content off the scroll frame with no scroll child between; the mail
// window is the reference's own now, scroll children and all.
];
/// The extracted reference FrameXML directory, or `None` when the install isn't there.
@@ -593,8 +575,11 @@ fn the_shipped_frames_carry_the_references_flags() {
// 1948 retired our ChatFrame.xml, 1952 our SpellBookFrame.xml, 1953 our PetActionBar.xml,
// 1956 our SkillFrame.xml, 1958 our UnitPopup.xml and 1959 our FriendsFrame.xml (their
// frames are the reference's own now), which took the paired count from the low 400s to
// the high 150s; the floor guards the pairing, not the census.
compared > 120,
// the high 150s; 1966 (TradeFrame), 1968 (GameTooltip), 1969 (DressUpFrame) and 1970
// (MailFrame) to the high 80s. The floor guards the pairing, not the census — it comes
// down with every window that migrates, and reaches zero with the last file of ours
// that declares a reference-named frame.
compared > 60,
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
);
assert!(
@@ -9,15 +9,11 @@
use benilla_ui::script::{FriendInfo, SocialRequest, SocialState, UiScript, WhoInfo};
use super::test_ui::load_ui_strict as load_xml;
/// The window's own manifest slice, in `load_default_ui` order.
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for f in super::test_ui::SOCIAL_UI {
load_xml(&s, f);
}
super::test_ui::load_social_ui(&mut s);
s
}
@@ -563,7 +563,7 @@ fn escape_during_a_countdown_cancels_it_and_does_not_open_the_menu() {
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\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -614,7 +614,7 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
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\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -680,7 +680,7 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
],
@@ -809,7 +809,7 @@ fn backpack() -> ContainerState {
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\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -82,7 +82,7 @@ fn setup() -> UiScript {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // PASS/NEED/GREED + item hovers
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
@@ -563,7 +563,7 @@ fn managed_positions_engage_for_the_bare_frame_name() {
// `TOOLTIP_DEFAULT_COLOR`, which the chain's dropdown backdrops read in their OnLoad — the
// dropdown kit rides in with the loot window now (1838), so this bespoke setup needs it too.
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_group_loot(&s);
let bottom = |s: &UiScript| s.eval::<f64>("return GroupLootFrame1:GetBottom()").unwrap();
@@ -618,7 +618,7 @@ fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
let mut s = setup();
load_group_loot(&s);
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink, the shared shift-insert helper
load_xml(&s, "DressUpFrame.xml");
load_xml(&s, "Interface\\FrameXML\\DressUpFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -22,8 +22,6 @@
use benilla_ui::script::{GuildState, ScriptValue, UiScript, UnitState};
use super::test_ui::load_ui_strict as load_xml;
/// The guild engine API, stood in for in Lua (see the module header).
///
/// One mutable table, `BenillaGuildFixture`, is the whole model; every getter reads it and every
@@ -203,9 +201,7 @@ fn setup() -> UiScript {
..UnitState::default()
}),
);
for f in super::test_ui::SOCIAL_UI {
load_xml(&s, f);
}
super::test_ui::load_social_ui(&mut s);
s
}
@@ -26,7 +26,7 @@ fn setup() -> UiScript {
"ScrollTemplates.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
// Before UiPanels.xml: the shared StaticPopup carries a `SmallMoneyFrameTemplate` coin
// row, whose OnLoad calls `SmallMoneyFrame_OnLoad` — the TOC's own order (1580's
// talent-wipe fixture hit this first).
@@ -379,18 +379,20 @@ fn shown_inspect_honor_page() -> UiScript {
"Interface\\FrameXML\\StaticPopup.xml",
// `InspectUnit`'s home since 1832.
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
// Before the inspect addon — its honor page inherits this file's row templates and calls
// its two shared painters, and `inherits=` resolves at load (the manifest's own order).
// The stock inspect slot buttons inherit this (1832).
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"Interface\\FrameXML\\HonorFrame.xml",
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
] {
load_xml(&s, file);
}
// The window is a LoadOnDemand addon, reached the way the app reaches it: seated off the
// chain as a registry row (1957) and loaded by the reference's own `InspectFrame_LoadUI`
// (UIParent.xml; 1967).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_InspectUI");
s.run("InspectFrame_LoadUI()").unwrap();
s.set_unit("target", Some(alliance_player()));
// The PLAYER snapshot is required even though this page is about somebody else, and the reason
// is the reference's own asymmetry: `BenillaHonorPane_SetRank` passes the inspected token for
@@ -127,7 +127,7 @@ fn armed() -> UiScript {
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
@@ -137,18 +137,11 @@ fn armed() -> UiScript {
// this entry far above the inspect window all along (decision 1832).
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
);
// The window is a LoadOnDemand addon, reached the way the app reaches it: seated off the
// chain as a registry row (1957) and loaded by the reference's own `InspectFrame_LoadUI`
// (UIParent.xml; 1967).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_InspectUI");
s.run("InspectFrame_LoadUI()").unwrap();
s.set_unit("target", Some(target_unit()));
s.set_inspect(Some(inspect_view("target")));
// 4 yards away (d² = 16) — comfortably inside the verified 100.0.
@@ -160,7 +153,7 @@ fn armed() -> UiScript {
#[test]
fn shipped_inspect_frame_loads_clean() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
let mut s = UiScript::new().unwrap();
// `PLAYER_LEVEL`, the template the stock level line formats through — the reference keeps its
// strings here, and our retired file had the sentence written into it (1832).
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
@@ -177,7 +170,7 @@ fn shipped_inspect_frame_loads_clean() {
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
@@ -187,18 +180,11 @@ fn shipped_inspect_frame_loads_clean() {
// this entry far above the inspect window all along (decision 1832).
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
);
// The window is a LoadOnDemand addon, reached the way the app reaches it: seated off the
// chain as a registry row (1957) and loaded by the reference's own `InspectFrame_LoadUI`
// (UIParent.xml; 1967).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_InspectUI");
s.run("InspectFrame_LoadUI()").unwrap();
// All 19 slots exist and carry their GetInventorySlotInfo id (1..=19, no ammo slot).
// The stock `InspectPaperDollItemSlotButton_OnLoad` puts it on the frame's own ID
// (`this:SetID(id)`); our retired file kept it in an `invSlotId` field of its own (1832).
@@ -98,7 +98,7 @@ fn the_self_menu_row_gates_on_the_binding_and_confirms_before_sending() {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua", // `TEXT`, which StaticPopup.lua and UnitPopup.lua read at file scope
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
@@ -111,10 +111,14 @@ fn the_self_menu_row_gates_on_the_binding_and_confirms_before_sending() {
"Interface\\FrameXML\\TargetFrame.xml",
"Interface\\FrameXML\\PetFrame.xml",
r"Interface\FrameXML\RaidFrame.xml",
r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml",
] {
load_xml(&s, file);
}
// The window is a LoadOnDemand addon, reached the way the app reaches it: seated off the
// chain as a registry row (1957) and loaded by the reference's own `RaidFrame_LoadUI`
// (UIParent.xml; 1967).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_RaidUI");
s.run("RaidFrame_LoadUI()").unwrap();
s.resolve();
assert!(s.errors().is_empty(), "load errors: {:?}", s.errors());
@@ -54,7 +54,7 @@ pub(crate) fn harness() -> UiScript {
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"UIParent.xml",
@@ -609,7 +609,7 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
for file in [
"UIParent.xml", // BenillaChatEdit_InsertLink, the shared shift-insert helper
"Interface\\FrameXML\\LootFrame.xml",
"DressUpFrame.xml",
"Interface\\FrameXML\\DressUpFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
@@ -97,7 +97,7 @@ fn harness_with(player: &str) -> UiScript {
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// The chain's `PanelTemplates_SelectTab` reaches for `GameTooltip` unguarded.
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"UIParent.xml", // `ShowMacroFrame` lives here now
// **ScrollTemplates BEFORE UIPanelTemplates, the manifest's own order.** Ours still
// carries dead `FauxScrollFrame_*` copies the chain overrides by loading after (1846's
@@ -109,10 +109,14 @@ fn harness_with(player: &str) -> UiScript {
// `ClassTrainerListScrollFrameTemplate` — the icon chooser's scroll frame inherits it.
r"Interface\FrameXML\ClassTrainerFrameTemplates.xml",
"MicroMenu.xml", // stock `MacroFrame_OnShow`/`_OnHide` drive the micro button
r"Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml",
] {
super::test_ui::load_ui(&s, file);
}
// The window is a LoadOnDemand addon, reached the way the app reaches it: seated off the
// chain as a registry row (1957) and loaded by the reference's own `MacroFrame_LoadUI`
// (UIParent.xml; 1967).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_MacroUI");
s.run("MacroFrame_LoadUI()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// The icon chooser's list is the app's push; three entries is enough to index into.
s.set_macro_icons(vec![
@@ -178,31 +178,6 @@ pub(crate) fn load_default_ui(script: &UiScript) -> Vec<String> {
/// per-entry rather than per-run so the manifest's order is the load order verbatim — the whole
/// reason the list is a manifest and not two lists.
///
/// Both stores answer `<Include>` / `<Script file=>` the same way: the loader resolves a reference
/// against the *including document's own* directory in its source's path space (1186), which for a
/// chain document means `Interface\FrameXML\ContainerFrame.xml`'s `<Script
/// file="ContainerFrame.lua"/>` reaches `Interface\FrameXML\ContainerFrame.lua` without anything
/// here knowing about it.
/// The Blizzard addons the manifest lists off the chain (`Interface\AddOns\<Name>\…` rows), by
/// name, in first-appearance order — the eager trio the InspectUI header describes, which the
/// registry must not also carry as LoadOnDemand rows (`addons::chain_addons`, 1957).
pub(crate) fn chain_addons(files: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for file in files {
let norm = file.replace('/', "\\");
let Some(rest) = norm.strip_prefix("Interface\\AddOns\\") else {
continue;
};
let Some((name, _)) = rest.split_once('\\') else {
continue;
};
if !out.iter().any(|n| n == name) {
out.push(name.to_string());
}
}
out
}
fn load_manifest(script: &UiScript, files: &[String]) -> Vec<String> {
let builtin = Addon::builtin();
let reference = reference_ui::addon(
@@ -565,6 +565,10 @@ fn merchant_switch_plays_close_then_open_and_queues_the_consumable_close() {
fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The stock tooltip declares no size: it sizes from its lines through the font engine, as
// the client's does (1968) — a harness that reads its rect needs one; the fixed-width
// font is that engine here.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
@@ -1138,7 +1142,7 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
for file in [
"UIParent.xml", // BenillaChatEdit_InsertLink, the shared shift-insert helper
"Interface\\FrameXML\\MerchantFrame.xml",
"DressUpFrame.xml",
"Interface\\FrameXML\\DressUpFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
@@ -32,7 +32,7 @@ fn harness_with(extra: &[&str]) -> UiScript {
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
@@ -258,6 +258,10 @@ fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
fn every_micro_button_hovers_with_its_reference_explanation() {
let mut s = harness_with(&["UIParent.xml"]);
set_player_level(&mut s, 10);
// 1.12 ships detailed tips ON — `SHOW_NEWBIE_TIPS = "1"` is UIOptionsFrame_Init's (ref
// UIOptionsFrame.lua l.100; ours sits in OptionsFrame.xml's uvar block, 1968), and a harness
// without the options file says so itself, the way the reference's tooltip would read it.
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
s.resolve();
for (button, newbie) in [
@@ -270,7 +274,8 @@ fn every_micro_button_hovers_with_its_reference_explanation() {
("MainMenuMicroButton", "NEWBIE_TOOLTIP_MAINMENU"),
("HelpMicroButton", "NEWBIE_TOOLTIP_HELP"),
] {
s.run(&format!("BenillaMicroButton_OnEnter({button})"))
// A hover is a handler: the reference's helper reads the firing frame off `this`.
s.run(&format!("this = {button} BenillaMicroButton_OnEnter(this)"))
.unwrap();
assert_eq!(
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
@@ -26,7 +26,7 @@ fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
// half (the PVP tint slice, decision 0287) touches it from OnLoad on.
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
// OnLoad seeds the +/- state from the live zoom. The CVar default is 3 — a middle level, so
@@ -91,7 +91,7 @@ fn tracking_frame_follows_get_tracking_texture_across_player_auras_changed() {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
let vis = |s: &UiScript| {
@@ -141,7 +141,7 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript {
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
s
@@ -254,7 +254,7 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
s.resolve();
@@ -300,7 +300,7 @@ fn get_ping_position_is_nil_until_there_is_a_ping() {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
assert_eq!(
@@ -22,7 +22,7 @@ fn load_action_bar(s: &UiScript) {
"UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
@@ -466,7 +466,7 @@ fn multibar_hover_renders_the_buttons_own_action() {
"UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
@@ -60,7 +60,7 @@ fn harness_on(mut s: UiScript) -> UiScript {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml", // the Keybindings page's faux-scroll kit
"KeyBindingsPage.xml", // the Keybindings body's templates + script (1008)
@@ -757,7 +757,7 @@ fn interface_harness() -> UiScript {
// own backdrop reads `GameTooltip`'s TOOLTIP_DEFAULT_COLOR. `harness_on` loads two of
// these again after these — re-running a UI file is what `/reload` does, and the
// loader takes it.
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
@@ -826,7 +826,7 @@ fn chat_harness() -> UiScript {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"Interface\\FrameXML\\GlobalStrings.lua",
@@ -861,7 +861,7 @@ fn actionbars_harness() -> UiScript {
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
@@ -2109,6 +2109,10 @@ fn hover_label(s: &mut UiScript, frame: &str) {
#[test]
fn a_hovered_row_raises_its_1_12_description_on_the_era_seat() {
let mut s = harness_on(audio_harness());
// The stock tooltip declares no size: it sizes from its lines through the font engine, as
// the client's does (1968) — a harness that reads its rect needs one; the fixed-width
// font is that engine here.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
s.run("OPTION_TOOLTIP_GAMEFIELD_DESELECT = \"Checking this will prevent the deselection.\"")
.unwrap();
s.run("ERA_WINDOW_SCALE = 1").unwrap();
@@ -48,7 +48,7 @@ fn harness() -> UiScript {
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -858,7 +858,7 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// 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
@@ -24,7 +24,7 @@ fn harness(extra: &[&str]) -> UiScript {
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
for f in extra {
@@ -231,6 +231,10 @@ fn get_net_stats_reports_the_pushed_latency() {
fn hovering_the_meter_shows_the_live_latency() {
let mut s = harness(&["UIParent.xml"]);
s.set_latency_ms(Some(42));
// 1.12 ships detailed tips ON — `SHOW_NEWBIE_TIPS = "1"` is UIOptionsFrame_Init's (ref
// UIOptionsFrame.lua l.100; ours sits in OptionsFrame.xml's uvar block, 1968), and a harness
// without the options file says so itself, the way the reference's tooltip would read it.
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
s.resolve();
assert_eq!(
@@ -28,7 +28,7 @@ pub(super) fn load_pet_bar(s: &UiScript) {
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -30,7 +30,7 @@ fn load_pet_frame() -> UiScript {
load_xml(&s, "UIParent.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
@@ -112,7 +112,7 @@ fn setup() -> UiScript {
// `ChatFrameEditBox`, which the reference's own purchase button indexes on every click to
// decide where focus goes after the name box closes — a nil there raises before the charter is
// bought. Ours guarded it; the reference does not.
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrops
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrops
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // ChatFrame's seven dropdowns inherit its template
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
+11 -11
View File
@@ -22,7 +22,7 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -93,7 +93,7 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -192,7 +192,7 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -295,7 +295,7 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -375,7 +375,7 @@ fn greeting_goodbye_button_closes_the_window() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -433,7 +433,7 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -562,7 +562,7 @@ fn write_on_still_fades_when_instant_text_is_off() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -640,7 +640,7 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -724,7 +724,7 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -825,7 +825,7 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -839,7 +839,7 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
load_xml(&s, "Interface\\FrameXML\\DressUpFrame.xml"); // DressUpItemLink lives here
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml"); // ChatFrameEditBox lives here
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
@@ -89,7 +89,7 @@ fn shipped_questlog_frame_loads_clean() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -123,7 +123,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
@@ -333,7 +333,7 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -481,7 +481,7 @@ fn watch_guards_no_op_without_erroring() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -598,7 +598,7 @@ fn progress_auto_watches_for_five_minutes() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -683,7 +683,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -749,7 +749,7 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -828,7 +828,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -893,7 +893,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1082,7 +1082,7 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1157,7 +1157,7 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1222,7 +1222,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1299,7 +1299,7 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1459,7 +1459,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1481,7 +1481,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
load_xml(&s, "Interface\\FrameXML\\DressUpFrame.xml"); // DressUpItemLink lives here
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml"); // ChatFrameEditBox lives here
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
@@ -1558,7 +1558,7 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1668,7 +1668,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1755,7 +1755,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -1796,7 +1796,7 @@ fn share_quest_click_queues_the_selected_quests_id() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
@@ -16,17 +16,13 @@ use benilla_ui::script::{
UiScript,
};
use super::test_ui::load_ui_strict as load_xml;
/// The window's slice of the manifest, in `load_default_ui` order. `UIParent.xml` is in it for
/// two functions the pane really calls — `MouseIsOver` (the drag's hover sweep) and
/// `SecondsToTime` (the lockout rows) — and for the `READY_CHECK` arm that opens the popup.
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for f in super::test_ui::SOCIAL_UI {
load_xml(&s, f);
}
super::test_ui::load_social_ui(&mut s);
s
}
+194 -18
View File
@@ -1224,13 +1224,18 @@ mod tests {
// Everything the manifest pulls OFF THE CHAIN, and every name each of those defines —
// including the `.lua` a chain `.xml` sources, which is where most of them live.
let toc = &super::super::addons::Addon::builtin().toc.files;
let pos: std::collections::HashMap<&String, usize> =
toc.iter().enumerate().map(|(i, f)| (f, i)).collect();
// Load order: a manifest entry at its line, a reached addon's file after everything.
let chain = gated_chain_entries();
let pos: std::collections::HashMap<&String, usize> = chain
.iter()
.enumerate()
.map(|(k, f)| (f, toc.iter().position(|t| t == f).unwrap_or(toc.len() + k)))
.collect();
let mut chain_home: std::collections::HashMap<String, (String, usize)> =
std::collections::HashMap::new();
let mut chain_frames: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
for entry in &chain {
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or(entry);
let mut cands = vec![entry.clone()];
if let Some(stem) = entry.strip_suffix(".xml") {
@@ -1453,6 +1458,19 @@ mod tests {
seat_a_player(&mut s);
let failures = super::super::manifest::load_default_ui(&s);
assert!(failures.is_empty(), "the shipped manifest: {failures:#?}");
// The whole interface is the manifest AND every LoadOnDemand addon it reaches (1967):
// what `MacroFrame_SaveMacro` answers to is `Blizzard_MacroUI.lua`, loaded on the first
// `ShowMacroFrame`, and a call into it from ActionBarFrame.lua is answered exactly then.
for name in reached_addons() {
super::super::test_ui::seat_chain_addon(&mut s, &name);
s.run(&format!("UIParentLoadAddOn(\"{name}\")")).unwrap();
assert!(
s.eval::<bool>(&format!("return IsAddOnLoaded(\"{name}\") == 1"))
.unwrap(),
"{name}: reached, seated, and did not load: {:?}",
s.errors()
);
}
let have: std::collections::HashSet<String> = s
.eval::<Vec<String>>(
"local t = {} for k in pairs(_G) do table.insert(t, k) end return t",
@@ -1471,16 +1489,6 @@ mod tests {
// reads clean. Each belongs to whichever window's migration left it, and each is one
// binding or one sourced file away.
const KNOWN: &[(&str, &str, &str)] = &[
(
"ClassTrainerFrameTemplates.xml",
"ClassTrainerSkillButton_OnClick",
"defined by `Blizzard_TrainerUI`, which is a LoadOnDemand addon here as it is in \
the reference (1957): it loads on the first trainer, through UIParent's \
TRAINER_SHOW arm, and the skill-button template that carries this handler is \
instantiated only by that addon, so no click can reach the name before its \
definer exists. The reference is in exactly this state until its addon loads. \
Permanent by design, not a gap to close.",
),
(
"ContainerFrame.xml",
"KeyRingButtonIDToInvSlotID",
@@ -1523,6 +1531,114 @@ mod tests {
update; our `inventory_alerts` snapshot is recomputed on every inventory push \
instead, so the recompute exists and only the Lua verb that forces one does not.",
),
(
"Blizzard_GMSurveyUI.xml",
"GMSurveyAnswerSubmit",
"one of the GM survey's four engine verbs, none built: the survey window opens on \
GMSURVEY_DISPLAY, which the stock HelpFrame.lua registers and nothing fires — the \
trigger is ticket status 3 on SMSG_GMTICKET_GETTICKET, which vmangos never sends \
(1889; the producer gate carries the event). Gated since the addon became a reached \
LoadOnDemand row (1967).",
),
(
"Blizzard_GMSurveyUI.xml",
"GMSurveyCommentSubmit",
"one of the GM survey's four engine verbs, none built: the survey window opens on \
GMSURVEY_DISPLAY, which the stock HelpFrame.lua registers and nothing fires — the \
trigger is ticket status 3 on SMSG_GMTICKET_GETTICKET, which vmangos never sends \
(1889; the producer gate carries the event). Gated since the addon became a reached \
LoadOnDemand row (1967).",
),
(
"Blizzard_GMSurveyUI.xml",
"GMSurveyQuestion",
"one of the GM survey's four engine verbs, none built: the survey window opens on \
GMSURVEY_DISPLAY, which the stock HelpFrame.lua registers and nothing fires — the \
trigger is ticket status 3 on SMSG_GMTICKET_GETTICKET, which vmangos never sends \
(1889; the producer gate carries the event). Gated since the addon became a reached \
LoadOnDemand row (1967).",
),
(
"Blizzard_GMSurveyUI.xml",
"GMSurveySubmit",
"one of the GM survey's four engine verbs, none built: the survey window opens on \
GMSURVEY_DISPLAY, which the stock HelpFrame.lua registers and nothing fires — the \
trigger is ticket status 3 on SMSG_GMTICKET_GETTICKET, which vmangos never sends \
(1889; the producer gate carries the event). Gated since the addon became a reached \
LoadOnDemand row (1967).",
),
(
"Blizzard_BattlefieldMinimap.xml",
"CreateMiniWorldMapArrowFrame",
"the world map's arrow-frame family (Create/Position/Show/UpdateWorldMapArrowFrames), engine verbs the reference's WorldMapFrame.lua also calls — unbuilt; ours draws the map's arrows app-side, and they land with the world map's own migration. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"Blizzard_BattlefieldMinimap.xml",
"GetBattlefieldFlagPosition",
"the battlefield minimap's flag-position getter (0x4ac230, structural row only; the position family is uncarved). The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"Blizzard_BattlefieldMinimap.xml",
"GetBattlefieldMapIconScale",
"the battlefield minimap's icon-scale getter (uncarved; reads the queue slot's Map.dbc row). The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"Blizzard_BattlefieldMinimap.xml",
"GetBattlefieldPosition",
"the battlefield minimap's player-position getter (wow-re `bindings.md` 0x4abf90, a structural row — the position family is outside 195cd099's carve). The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"Blizzard_BattlefieldMinimap.xml",
"PositionMiniWorldMapArrowFrame",
"as CreateMiniWorldMapArrowFrame — the arrow-frame family. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"Blizzard_BattlefieldMinimap.xml",
"ShowMiniWorldMapArrowFrame",
"as CreateMiniWorldMapArrowFrame — the arrow-frame family. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"Blizzard_BattlefieldMinimap.xml",
"UpdateWorldMapArrowFrames",
"as CreateMiniWorldMapArrowFrame — the arrow-frame family. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"Blizzard_BattlefieldMinimap.xml",
"WorldMap_GetPOITextureCoords",
"a stock WorldMapFrame.lua function; our WorldMapFrame.xml is still ours and defines its own POI atlas walk, so the name arrives with the world map's migration. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
battleground's world states); its pane is a <Minimap>-kind widget this engine \
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
its update would reach.",
),
(
"StaticPopup.xml",
"ReplaceTradeEnchant",
@@ -1531,9 +1647,8 @@ mod tests {
),
];
let toc = &super::super::addons::Addon::builtin().toc.files;
let mut missing: Vec<(String, String)> = Vec::new();
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
for entry in &gated_chain_entries() {
// `GlobalStrings.lua` is 4000 lines of `NAME = "…";` and nothing else — it calls no
// global at all. What it DOES contain is every format specifier and every English
// sentence in the interface, and this scanner's `name(` shape reads `%d (`, `%s (` and
@@ -1694,7 +1809,7 @@ mod tests {
.collect();
let mut orphans = Vec::new();
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
for entry in &gated_chain_entries() {
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or(entry);
let Some(stem) = leaf.strip_suffix(".xml") else {
continue;
@@ -1963,6 +2078,68 @@ mod tests {
/// The chain for a path, `assets/ui` for a bare name — and an include resolves against the
/// INCLUDING document's own directory in its own source's path space, which is the rule the
/// loader follows (1186).
/// The reference's LoadOnDemand Blizzard addons this interface REACHES — every `Blizzard_*`
/// name a `UIParentLoadAddOn("…")` literal names in our own files or in a manifest chain
/// entry's sources (the reference's `*_LoadUI` loaders in UIParent.xml, the options window's
/// combat-text load). They have no manifest row, exactly as the reference's `FrameXML.toc`
/// has none, so this is how the gates know which addon files are part of the shipped
/// interface (1967). An addon nothing loads is an unbuilt window, not a migrated one.
fn reached_addons() -> Vec<String> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
let mut out: Vec<String> = Vec::new();
for entry in &super::super::addons::Addon::builtin().toc.files {
let texts = if super::is_chain_entry(entry) {
entry_sources(entry)
} else {
std::fs::read_to_string(dir.join(entry))
.into_iter()
.collect()
};
for text in texts {
let text = strip_comments(&text);
for (i, _) in text.match_indices("UIParentLoadAddOn(\"Blizzard_") {
let rest = &text[i + "UIParentLoadAddOn(\"".len()..];
if let Some(end) = rest.find('"') {
let name = rest[..end].to_string();
if !out.contains(&name) {
out.push(name);
}
}
}
}
}
out.sort();
out
}
/// Every chain file the shipped interface loads, in load order: the manifest's chain entries,
/// then each reached addon's files (`Interface\AddOns\<name>\<file>`, the addon's own toc
/// order) — LoadOnDemand loads after everything. The set every gate over chain files walks
/// (1967); an addon an opener names that the chain does not carry is a finding, not a skip.
fn gated_chain_entries() -> Vec<String> {
let mut out: Vec<String> = super::super::addons::Addon::builtin()
.toc
.files
.iter()
.filter(|f| super::is_chain_entry(f))
.cloned()
.collect();
for name in reached_addons() {
let bytes =
super::read(&format!("Interface/AddOns/{name}/{name}.toc")).unwrap_or_else(|| {
panic!("{name}: reached by UIParentLoadAddOn, not on the chain")
});
let toc = benilla_ui::toc::Toc::parse(&benilla_ui::source::decode(&bytes));
for file in &toc.files {
out.push(format!(
"Interface\\AddOns\\{name}\\{}",
file.replace('/', "\\")
));
}
}
out
}
fn entry_sources(entry: &str) -> Vec<String> {
fn read_one(path: &str, chain: bool) -> Option<String> {
if chain {
@@ -2306,10 +2483,9 @@ mod tests {
}
}
let toc = &super::super::addons::Addon::builtin().toc.files;
let mut dead: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
for entry in &gated_chain_entries() {
for text in entry_sources(entry) {
let mut from = 0;
while let Some(i) = text[from..].find("RegisterEvent(") {
@@ -32,7 +32,7 @@ fn every_shipped_ui_xml_parses() {
assert!(
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751),
// so the floor sits well under the count rather than one step above it (1956).
checked >= 20,
checked >= 15,
"only {checked} xml files swept — sweep broke"
);
}
@@ -428,8 +428,10 @@ fn every_shipped_texture_path_resolves_in_the_client_archives() {
}
}
}
// Never let the sweep pass by matching nothing — the shipped UI names hundreds of textures.
assert!(refs.len() >= 200, "only {} texture paths swept", refs.len());
// Never let the sweep pass by matching nothing — the shipped UI still names over a hundred
// textures (173 after 1971 retired the auction transcription, fewer again after 1973 the
// crafting pair; the count falls with every window that migrates, and the floor follows it).
assert!(refs.len() >= 80, "only {} texture paths swept", refs.len());
// The shape half: a doubled separator is the Lua escaping written into XML, and it resolves to
// nothing. Checked without the client so a data-less machine still catches this exact class.
@@ -622,9 +624,10 @@ fn every_archive_path_a_shipped_lua_chunk_names_survives_its_own_escaping() {
}
}
}
// Never let the sweep pass by matching nothing.
// Never let the sweep pass by matching nothing (19 after 1971; the floor follows the census
// down as windows migrate).
assert!(
paths.len() >= 20,
paths.len() >= 10,
"only {} archive paths swept out of the shipped Lua",
paths.len()
);
@@ -729,9 +732,11 @@ fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
}
}
}
// Never let the sweep pass by matching nothing: 23 key-shaped values across six windows is the
// floor as of 0991, and a regex that stops matching is exactly how this guard would retire.
assert!(keys >= 23, "only {keys} key-shaped text= values swept");
// Never let the sweep pass by matching nothing: 23 key-shaped values across six windows was
// the floor as of 0991; ONE is left after 1971, and a regex that stops matching is exactly how
// this guard would retire — so the floor is one, and the guard retires with the last file of
// ours that writes a key-shaped `text=`.
assert!(keys >= 1, "only {keys} key-shaped text= values swept");
}
/// **No shipped script hands a GlobalStrings KEY to a text sink as if it were the string.**
@@ -840,7 +845,7 @@ fn no_shipped_script_sets_a_global_string_key_as_display_text() {
assert!(offenders.is_empty(), "{}", offenders.join("\n"));
// The sweep must never pass by finding nothing to sweep.
// The same walk floor as above (1956).
assert!(swept >= 20, "only {swept} xml files swept — sweep broke");
assert!(swept >= 15, "only {swept} xml files swept — sweep broke");
}
/// **The `$parentTextureFrame` idiom's contract, over the whole shipped UI**: a frame whose art is
@@ -1438,8 +1443,8 @@ fn every_declared_parent_really_attaches() {
let declared = shipped_frame_parents();
assert!(
// A sanity floor for the scan, not a census — the declarations retire with the files
// that carry them (1751); 67 before 1938, 30 after 1956.
declared.len() >= 15,
// that carry them (1751); 67 before 1938, 30 after 1956, 14 after 1970.
declared.len() >= 10,
"only {} parent declarations found — the scan broke",
declared.len()
);
@@ -1576,6 +1581,9 @@ fn the_shipped_manifest_opens_the_macro_icon_picker() {
s.resolve();
let _ = s.errors();
// The window is a LoadOnDemand addon the app seats at setup (1957) and `ShowMacroFrame`
// loads through the reference's own `MacroFrame_LoadUI` (1967).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_MacroUI");
s.run("ShowMacroFrame()").unwrap();
s.run("MacroButton1:Click()").unwrap();
s.run("MacroEditButton:Click()").unwrap();
@@ -99,7 +99,7 @@ pub(super) fn spellbook_ui(w: f32, h: f32) -> UiScript {
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -139,7 +139,7 @@ fn shipped_spellbook_loads_clean() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
// The reference's own file, off the chain, with the one adapter it needs from this engine.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -692,7 +692,7 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
"Interface\\FrameXML\\StaticPopup.xml",
// `ShowMacroFrame` lives here since 1848.
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Cooldown.xml",
// **ScrollTemplates BEFORE UIPanelTemplates, which is the manifest's own order.** Ours
// still carries dead `FauxScrollFrame_*` copies that the chain overrides by loading after
@@ -719,7 +719,6 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
r"Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml",
// The reference's spellbook shows and hides the multibar grids (MultiActionBars.lua),
// whose file wants the options window's uvars — the same tail `spellbook_ui` carries.
"Interface\\FrameXML\\UIDropDownMenu.xml",
@@ -731,6 +730,11 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
] {
load_xml(&s, file);
}
// The window is a LoadOnDemand addon, reached the way the app reaches it: seated off the
// chain as a registry row (1957) and loaded by the reference's own `MacroFrame_LoadUI`
// (UIParent.xml; 1967).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_MacroUI");
s.run("MacroFrame_LoadUI()").unwrap();
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
// The file's own book plus a third Fire spell that is PASSIVE — book id 3, which the ref's
@@ -22,7 +22,7 @@ fn harness() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the dropdown kit's MenuBackdrop)
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the dropdown kit's MenuBackdrop)
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the unit popups' kit (TargetFrameDropDown's template)
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
@@ -22,7 +22,7 @@ fn taxi_script() -> UiScript {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml"); // TaxiNodeOnButtonEnter's tooltip + SetTooltipMoney
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TaxiNodeOnButtonEnter's tooltip + SetTooltipMoney
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // BenillaErrorsFrame_AddMessage — DrawOneHopLines' refusal
// Three the reference's own TaxiFrame leans on that our transcription did not:
// · GlobalStrings — `ERR_TAXINOPATHS` is a GlobalString, and `AddMessage(nil)` draws an
+19 -9
View File
@@ -152,7 +152,7 @@ pub(super) const MERCHANT_UI: &[&str] = &[
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml", // app load order: tooltip before merchant
"Interface\\FrameXML\\GameTooltip.xml", // app load order: tooltip before merchant
];
pub(super) const LOOT_UI: &[&str] = &[
@@ -167,7 +167,7 @@ pub(super) const LOOT_UI: &[&str] = &[
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml", // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
"Interface\\FrameXML\\GameTooltip.xml", // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
"Interface\\FrameXML\\UIDropDownMenu.xml", // GroupLootDropDown's OnLoad calls UIDropDownMenu_Initialize
// `UnitPopup.xml` reads ITEM_QUALITY_COLORS at FILE SCOPE for its three loot-threshold rows,
// exactly as the reference's own UnitPopup.lua:47-49 does — so its declarer has to precede it.
@@ -243,7 +243,7 @@ pub(super) const CHARACTER_UI: &[&str] = &[
r"Interface\FrameXML\MoneyFrame.xml",
"UIParent.xml", // Model_OnLoad/_Rotate*/_OnUpdate — the model panes' turntable
"UiPanels.xml", // CharacterFrameTabButtonTemplate, UIPanelWindows, Show/HideUIPanel
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Cooldown.xml", // CooldownFrameTemplate + CooldownFrame_SetTimer, per equipment slot
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -301,9 +301,9 @@ pub(super) const CHARACTER_UI: &[&str] = &[
/// The social window's slice of the manifest (1959): everything the reference's
/// `FriendsFrame.xml` + `RaidFrame.xml` reach at load or on show — `TEXT` and `GetText`, the
/// bar chain under `UpdateMicroButtons`, the chat window whose edit box `FriendsFrame_SendMessage`
/// opens, the unit menu, the party frames `RaidFrame_OnLoad` reconciles, and the raid tab's
/// LoadOnDemand addon — in the manifest's own order. Shared by the friends, guild and raid
/// harnesses.
/// opens, the unit menu, the party frames `RaidFrame_OnLoad` reconciles — in the manifest's own
/// order; the raid tab's LoadOnDemand addon follows through [`load_social_ui`]. Shared by the
/// friends, guild and raid harnesses.
pub(super) const SOCIAL_UI: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
@@ -317,7 +317,7 @@ pub(super) const SOCIAL_UI: &[&str] = &[
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"ScrollTemplates.xml",
@@ -342,9 +342,19 @@ pub(super) const SOCIAL_UI: &[&str] = &[
"Interface\\FrameXML\\PartyFrame.xml",
"Interface\\FrameXML\\FriendsFrame.xml",
"Interface\\FrameXML\\RaidFrame.xml",
"Interface\\AddOns\\Blizzard_RaidUI\\Blizzard_RaidUI.xml",
];
/// Load [`SOCIAL_UI`], then the raid tab's LoadOnDemand addon the way the app reaches it: seated
/// off the chain as a registry row (1957) and loaded by the reference's own `RaidFrame_LoadUI`
/// (UIParent.xml; 1967). Needs client data — the caller has checked with `wow_data_or_skip!`.
pub(super) fn load_social_ui(s: &mut UiScript) {
for f in SOCIAL_UI {
load_ui_strict(s, f);
}
seat_chain_addon(s, "Blizzard_RaidUI");
s.run("RaidFrame_LoadUI()").unwrap();
}
pub(super) const BAG_UI: &[&str] = &[
// The reference's own localized strings — `BACKPACK_TOOLTIP`, `EQUIP_CONTAINER`, `KEYRING`,
// the `*_FONT_COLOR_CODE` pair. The app loads this at VM setup, ahead of the manifest
@@ -370,7 +380,7 @@ pub(super) const BAG_UI: &[&str] = &[
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Cooldown.xml",
// The bag BAR declares `parent="MainMenuBarArtFrame"`, resolved at LOAD — so without this the
// six buttons fall back to UIParent and sit at a level no production run ever puts them at.
@@ -24,10 +24,19 @@ fn harness(extra: &[&str]) -> UiScript {
load_xml(&s, "UIParent.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
// `FACTION_BAR_COLORS`, which the stock `GameTooltip_UnitColor` indexes on every unit hover:
// the reference defines it at ReputationFrame.lua's file scope (1968).
load_xml(&s, r"Interface\FrameXML\ReputationFrame.lua");
for f in extra {
load_xml(&s, f);
}
// The stock tooltip declares no size: it sizes from its lines through the font engine, as
// the client's does (1968) — every test here reads its rect, so the fixed-width font is
// that engine. And 1.12 ships detailed tips ON (`SHOW_NEWBIE_TIPS = "1"`, UIOptionsFrame_Init's;
// ours in OptionsFrame.xml's uvar block) — a harness without the options file says so itself.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
s
}
@@ -46,7 +46,7 @@ fn harness() -> UiScript {
"UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
] {
load_xml(&s, f);
}
@@ -193,11 +193,12 @@ fn an_addon_tooltip_from_the_template_gets_the_plate() {
.any(|p| p.eq_ignore_ascii_case(r"Interface\Tooltips\UI-Tooltip-Border")),
"…and its border: {drawn:?}"
);
// The Thicken deviation rides the template too — it is the shared plate, not one window's.
// The template's children resolve against the CALLER's name — the status bar the reference's
// template declares (`$parentStatusBar`), which TipBuddy anchors to by that global.
assert!(
s.eval::<bool>(r#"return getglobal("MyTipThicken") ~= nil"#)
s.eval::<bool>(r#"return getglobal("MyTipStatusBar") ~= nil"#)
.unwrap(),
"$parentThicken resolves against the CALLER's name"
"$parentStatusBar resolves against the CALLER's name"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -288,9 +289,9 @@ fn createframe_with_the_template_is_the_same_tooltip() {
"Equip: Improves your chance to hit by 1%."
);
assert!(
s.eval::<bool>(r#"return getglobal("BetterCharacterStatsTooltipThicken") ~= nil"#)
s.eval::<bool>(r#"return getglobal("BetterCharacterStatsTooltipStatusBar") ~= nil"#)
.unwrap(),
"the plate came through CreateFrame's fourth argument too"
"the template's children came through CreateFrame's fourth argument too"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -64,7 +64,7 @@ fn load_tot() -> UiScript {
load_xml(&s, "UIParent.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
@@ -56,7 +56,7 @@ fn trainer_script_base() -> UiScript {
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"ScrollTemplates.xml",
@@ -27,7 +27,9 @@ fn load_unit_frames(s: &UiScript) {
load_xml(s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(s, "GameTooltip.xml");
load_xml(s, "Interface\\FrameXML\\GameTooltip.xml");
// `FACTION_BAR_COLORS` for the stock `GameTooltip_UnitColor` — ReputationFrame.lua's (1968).
load_xml(s, r"Interface\FrameXML\ReputationFrame.lua");
load_xml(s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(s, "Interface\\FrameXML\\BasicControls.xml"); // `TEXT`, which UnitPopup.lua reads at file scope
load_xml(s, "Interface\\FrameXML\\UnitPopup.xml");
@@ -1220,7 +1222,8 @@ fn the_party_art_paints_over_the_bars() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\ReputationFrame.lua"); // FACTION_BAR_COLORS (1968)
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE and its
// declarer is UIParent (ref UIParent.lua:65) since 1888.
@@ -29,7 +29,7 @@ fn load_popup_frames(s: &UiScript) {
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
@@ -494,7 +494,7 @@ fn load_pet_menu_frames(s: &UiScript) {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua", // `TEXT`, read at file scope below
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
@@ -23,7 +23,7 @@ fn harness() -> UiScript {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad
"ScrollTemplates.xml",
// The blip templates, which WorldMapFrame.xml instantiates with inherits=. Not
@@ -26,11 +26,15 @@ pub(crate) fn harness() -> UiScript {
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"WorldStateFrame.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\WorldStateFrame.xml", // the reference's own (1972)
] {
load_xml(&s, f);
}
// The outdoor readout is gated on the options uvar `HIDE_OUTDOOR_WORLD_STATE == "0"`
// (UIOptionsFrame.lua's default, ours in OptionsFrame.xml's uvar block); a harness without
// the options file says so itself.
s.run("HIDE_OUTDOOR_WORLD_STATE = \"0\"").unwrap();
s
}
@@ -164,7 +168,16 @@ fn the_dynamic_icon_is_a_second_slot_lit_only_by_the_taken_state() {
..Default::default()
};
for quiet in [0, 1, 3] {
// A zero state is not a quiet row but NO row: the stock `WorldStateAlwaysUpFrame_Update`
// draws only `state > 0` (WorldStateFrame.lua) — our transcription had painted the shield at
// zero too, and the reference's file retired that (1972).
push(&mut s, vec![flag_row(0)]);
let art = textures(&mut s);
assert!(
!art.iter().any(|p| p.contains("UI-PVP-Alliance")),
"state 0 — the row is not drawn at all: {art:?}"
);
for quiet in [1, 3] {
push(&mut s, vec![flag_row(quiet)]);
let art = textures(&mut s);
assert!(
@@ -233,6 +246,24 @@ fn the_dynamic_icon_is_a_second_slot_lit_only_by_the_taken_state() {
/// icon is optional, and the row must not collapse when the DBC column is empty.
#[test]
fn a_row_without_an_icon_still_shows_its_text() {
let mut s = harness();
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
push(
&mut s,
vec![WorldStateUiView {
ui_state: 1,
text: "Progress: 60".into(),
..Default::default()
}],
);
assert!(texts(&mut s).contains(&"Progress: 60".to_string()));
}
/// An `ExtendedUI` row is not a text row at all — the stock update routes it to the extended
/// kit (`ExtendedUI["CAPTUREPOINT"]`), which builds a capture BAR and never prints the text.
/// Our transcription had shown the text; the reference's file draws the bar (1972).
#[test]
fn a_capture_point_row_is_a_bar_not_a_line_of_text() {
let mut s = harness();
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
push(
@@ -245,7 +276,13 @@ fn a_row_without_an_icon_still_shows_its_text() {
..Default::default()
}],
);
assert!(texts(&mut s).contains(&"Progress: 60".to_string()));
assert!(!texts(&mut s).contains(&"Progress: 60".to_string()));
assert!(
s.eval::<bool>("return WorldStateCaptureBar1 ~= nil and WorldStateCaptureBar1:IsShown()")
.unwrap(),
"the capture bar was built and shown"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The bindings' own edges, read from Lua the way an addon would. Two are easy to get wrong and
@@ -389,9 +426,7 @@ fn the_visible_ink_sits_beside_its_label_not_adrift_of_it() {
let label_mid_y = (text.top + text.bottom) * 0.5;
let (row_top, row_bottom) = s
.eval::<(f64, f64)>(
"local r = WorldStateAlwaysUpFrame1 return r:GetTop(), r:GetBottom()",
)
.eval::<(f64, f64)>("local r = AlwaysUpFrame1 return r:GetTop(), r:GetBottom()")
.expect("the row resolved");
let row_h = (row_top - row_bottom) as f32;
+8
View File
@@ -445,6 +445,14 @@ fn feed_trade_skill(
return;
}
script.set_trade_skill(fresh.clone());
// The client has every product's and reagent's template cached by the time its list shows,
// and `GetTradeSkillItemLink`/`GetTradeSkillReagentItemLink` never query — so the feed asks
// for the templates the store lacks when the list lands, and the verbs read the answers (1973).
if let Some(f) = &fresh {
script.ask_item_templates(f.recipes.iter().flat_map(|r| {
std::iter::once(r.product_item).chain(r.reagents.iter().map(|re| re.item))
}));
}
match (&*last, &fresh) {
(None, Some(f)) => {
debug!(
+48 -24
View File
@@ -32,7 +32,7 @@ const FILES: &[&str] = &[
// `auctionRowName` colours each row from ITEM_QUALITY_COLORS, whose declarer is
// UIParent (ref UIParent.lua:65) since 1888 put the font registry on the chain.
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -41,9 +41,32 @@ const FILES: &[&str] = &[
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"AuctionFrame.xml",
// The reference's own addon (1971), its toc order: the window (which <Include>s its
// templates) and the embedded dress-up pane. An integration test has no addon registry, so
// the files load as chain files, the way the manifest's own entries do.
// The embedded dress-up pane's OnLoad calls the dressing room's own `DressUpTexturePath`
// (stock DressUpFrame.lua, on the chain since 1969) — the manifest seats that window above.
"Interface\\FrameXML\\DressUpFrame.xml",
"Interface\\AddOns\\Blizzard_AuctionUI\\Blizzard_AuctionUI.xml",
"Interface\\AddOns\\Blizzard_AuctionUI\\Blizzard_AuctionDressUp.xml",
];
/// [`load_ui`] with the Browse tab's class tree seated first: the stock addon reads
/// `GetAuctionItemClasses()` at its LOAD (`AuctionFrameBrowse_OnLoad`), before any session, so
/// the classes are login-scoped and must be there before the files are (1971).
fn load_ui_with_classes(s: &mut UiScript) {
s.set_auction_item_classes(vec![AuctionCategory {
class_id: 4,
name: "Armor".into(),
subclasses: vec![AuctionSubCategory {
sub_id: 1,
name: "Cloth".into(),
has_inv_types: true,
}],
}]);
load_ui(s);
}
fn load_ui(script: &UiScript) {
// `common::load_ui`, not a local read: a manifest entry carrying a path separator is the
// REFERENCE's own file and has to come off the player's chain, which `std::fs::read` under
@@ -95,15 +118,6 @@ fn state(mut rows: Vec<AuctionItemRow>) -> AuctionState {
lists[OWNER] = AuctionListState::default();
AuctionState {
lists,
categories: vec![AuctionCategory {
class_id: 4,
name: "Armor".into(),
subclasses: vec![AuctionSubCategory {
sub_id: 1,
name: "Cloth".into(),
has_inv_types: true,
}],
}],
deposit_percent: 5,
}
}
@@ -126,8 +140,8 @@ 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);
let mut s = UiScript::new().unwrap();
load_ui_with_classes(&mut s);
for name in [
// The window, its three panes and its three tabs.
"AuctionFrame",
@@ -173,8 +187,8 @@ fn auction_frame_loads_and_key_regions_exist() {
#[test]
fn the_window_is_registered_doublewide() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_ui(&s);
let mut s = UiScript::new().unwrap();
load_ui_with_classes(&mut s);
assert_eq!(
s.eval::<String>("return UIPanelWindows['AuctionFrame'].area")
.unwrap(),
@@ -186,7 +200,7 @@ fn the_window_is_registered_doublewide() {
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);
load_ui_with_classes(&mut s);
seat_player(&mut s, 500_000);
s.set_auction(Some(state(vec![row(
"Linen Cloth",
@@ -244,7 +258,7 @@ fn auction_house_show_opens_the_window_on_the_browse_tab() {
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);
load_ui_with_classes(&mut s);
seat_player(&mut s, 500_000);
s.set_auction(Some(state(vec![
// No bids yet: the row shows the seller's opening price as the current bid.
@@ -344,7 +358,7 @@ fn the_browse_list_populates_from_the_fed_snapshot() {
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);
load_ui_with_classes(&mut s);
// 2 gold. Row 1 costs 10s to bid and 50s to buy out — affordable. Row 2 wants 5 gold.
seat_player(&mut s, 20_000);
s.set_auction(Some(state(vec![
@@ -428,7 +442,7 @@ fn the_bid_and_buyout_gates_read_the_purse() {
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);
load_ui_with_classes(&mut s);
seat_player(&mut s, 10_000_000);
s.set_auction(Some(state(vec![row(
"Linen Cloth",
@@ -459,7 +473,7 @@ fn you_cannot_bid_on_your_own_auction() {
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);
load_ui_with_classes(&mut s);
seat_player(&mut s, 0);
s.set_auction(Some(state(vec![])));
s.set_auction_can_query(true);
@@ -509,7 +523,7 @@ fn search_reads_the_filters_and_nothing_queries_before_it() {
fn the_create_gate_and_the_deposit() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
load_ui_with_classes(&mut s);
seat_player(&mut s, 100_000);
s.set_auction(Some(state(vec![])));
s.fire_event("AUCTION_HOUSE_SHOW", vec![]);
@@ -589,6 +603,9 @@ fn the_create_gate_and_the_deposit() {
// this exercises.
s.run("SplitContainerItem(0, 1, 2)").unwrap();
s.run("AuctionsItemButton:Click()").unwrap();
// The stock create pane paints the slot on NEW_AUCTION_UPDATE, which the app fires the frame
// the sell slot changes (`ui_auction`'s feed); a harness with no app fires it itself.
s.fire_event("NEW_AUCTION_UPDATE", vec![]);
assert_eq!(
s.eval::<String>("return AuctionsItemButtonName:GetText()")
.unwrap(),
@@ -604,6 +621,13 @@ fn the_create_gate_and_the_deposit() {
.eval::<bool>("return AuctionsItemButtonCount:IsShown()")
.unwrap());
// Dropping an item RESETS the form — the stock handler seeds the start price from the item's
// sell price and zeroes the buyout (`AuctionSellItemButton_OnEvent`) — so the prices are typed
// after the drop, as a player types them.
s.run("MoneyInputFrame_SetCopper(StartPrice, 10000)")
.unwrap();
s.run("MoneyInputFrame_SetCopper(BuyoutPrice, 5000)")
.unwrap();
// With an item in the slot, the buyout-under-start error is the one the form explains out loud.
s.run("AuctionsFrameAuctions_ValidateAuction()").unwrap();
assert!(
@@ -651,7 +675,7 @@ fn the_create_gate_and_the_deposit() {
fn hiding_the_window_closes_the_session() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
load_ui_with_classes(&mut s);
seat_player(&mut s, 0);
s.set_auction(Some(state(vec![])));
s.fire_event("AUCTION_HOUSE_SHOW", vec![]);
@@ -680,7 +704,7 @@ fn hiding_the_window_closes_the_session() {
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);
load_ui_with_classes(&mut s);
seat_player(&mut s, 0);
let mut rows = Vec::new();
for i in 0..50 {
@@ -736,7 +760,7 @@ fn paging_shows_the_turners_only_at_the_end_of_the_list() {
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);
load_ui_with_classes(&mut s);
s.set_auction(Some(state(vec![row("Copper Bar", 100, 500, 0, "Someone")])));
s.fire_event("AUCTION_HOUSE_SHOW", vec![]);
s.fire_event("AUCTION_ITEM_LIST_UPDATE", vec![]);
+1 -1
View File
@@ -50,7 +50,7 @@ const FILES: &[&str] = &[
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
// `CooldownFrameTemplate` and `CooldownFrame_SetTimer`. Not scenery: every one of the 20 slot
// buttons runs `PaperDollItemSlotButton_Update` from its own OnLoad, which calls
// `CooldownFrame_SetTimer` on `$parentCooldown` unconditionally (`PaperDollFrame.lua:692`) —
+13 -6
View File
@@ -27,8 +27,13 @@ const FILES: &[&str] = &[
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"MailFrame.xml",
"Interface\\FrameXML\\GameTooltip.xml",
r"Interface\FrameXML\ItemButtonTemplate.xml", // the send tab's attachment slot inherits it
// The stock tabs inherit `FriendsFrameTabTemplate`, and `inherits=` resolves at load — so the
// social window and the kit it needs come first, as the reference's toc has them (1970).
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\FriendsFrame.xml",
"Interface\\FrameXML\\MailFrame.xml",
];
fn load_ui(script: &UiScript) {
@@ -415,7 +420,8 @@ fn closing_a_taken_husk_deletes_it() {
);
}
/// The expiry text pluralizes like the reference (GetText("DAYS_ABBR"): "Day"/"Days").
/// The expiry text pluralizes like the reference (`GetText("DAYS_ABBR", nil, n)`: "Day"/"Days") —
/// and carries the reference's own trailing space before the colour close (MailFrame.lua l.144).
#[test]
fn expiry_text_pluralizes_days() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -431,12 +437,12 @@ fn expiry_text_pluralizes_days() {
assert_eq!(
s.eval::<String>("return MailItem1ExpireTime:GetText()")
.unwrap(),
"|cff20ff2029 Days|r"
"|cff20ff2029 Days |r"
);
assert_eq!(
s.eval::<String>("return MailItem2ExpireTime:GetText()")
.unwrap(),
"|cff20ff201 Day|r"
"|cff20ff201 Day |r"
);
assert!(s.take_errors().is_empty());
}
@@ -516,7 +522,8 @@ fn money_button_hover_shows_the_amount_tooltip() {
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
s.run("MailItem1Button:Click()").unwrap();
s.run("BenillaOpenMailMoneyButton_OnEnter(OpenMailMoneyButton)")
// The hover is the button's own inline handler (stock MailFrame.xml), reading `this`.
s.run("this = OpenMailMoneyButton OpenMailMoneyButton:GetScript(\"OnEnter\")()")
.unwrap();
assert!(
s.eval::<bool>("return GameTooltip:IsShown()").unwrap(),
+1 -1
View File
@@ -32,7 +32,7 @@ const FILES: &[&str] = &[
"UiPanels.xml",
// `ToggleTalentFrame` lives here now, not in the window's own file (decision 1833).
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
// `UIPanelScrollFrameTemplate` — the stock scroll frame's whole substance: its `$parentScrollBar`
// 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
+1 -1
View File
@@ -29,7 +29,7 @@ const FILES: &[&str] = &[
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
// The stock slot updates go through ItemButtonTemplate.lua's SetItemButton* helpers (1966).
r"Interface\FrameXML\ItemButtonTemplate.xml",
"Interface\\FrameXML\\TradeFrame.xml",
+71 -69
View File
@@ -31,11 +31,25 @@ const FILES: &[&str] = &[
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"TradeSkillFrame.xml",
"CraftFrame.xml",
// The stock windows open from UIParent's own TRADE_SKILL_SHOW / CRAFT_SHOW arms (its
// `*_LoadUI` then `*_Show`), not from a registration of their own — so UIParent.xml is a
// dependency of the event path this suite drives (1973). With no addon registry here the
// load reports ADDON_LOAD_FAILED through `message`, which is a shown frame, not an error.
"UIParent.xml",
// The reference's own addons (1973), off the chain — both inherit the trainer window's
// list/detail kit, which the manifest seats above them. An integration test has no addon
// registry, so the files load as chain files, the way the manifest's own entries do.
r"Interface\FrameXML\ClassTrainerFrameTemplates.xml",
// The reagent slots inherit `QuestItemTemplate` (147x41) through the addons' own row templates,
// and the selection paints them with `SetItemButtonTexture`/`SetItemButtonCount`, which are
// ItemButtonTemplate.lua's — both chain files the manifest seats above the addons.
r"Interface\FrameXML\ItemButtonTemplate.xml",
r"Interface\FrameXML\QuestFrameTemplates.xml",
r"Interface\AddOns\Blizzard_TradeSkillUI\Blizzard_TradeSkillUI.xml",
r"Interface\AddOns\Blizzard_CraftUI\Blizzard_CraftUI.xml",
];
fn load_ui(script: &UiScript) {
@@ -110,8 +124,7 @@ fn collapse_all_tab_and_filter_dropdowns_work_end_to_end() {
s.set_trade_skill(Some(state()));
s.fire_event("TRADE_SKILL_SHOW", vec![]);
assert!(
s.eval::<bool>("return BenillaTradeSkillFrame:IsShown()")
.unwrap(),
s.eval::<bool>("return TradeSkillFrame:IsShown()").unwrap(),
"the window opens on TRADE_SKILL_SHOW"
);
// 2 headers + 3 recipes.
@@ -119,32 +132,32 @@ fn collapse_all_tab_and_filter_dropdowns_work_end_to_end() {
// The CollapseAll tab carries its GlobalString text (a cross-chunk local would render nil).
assert_eq!(
s.eval::<String>("return BenillaTradeSkillCollapseAllButton:GetText()")
s.eval::<String>("return TradeSkillCollapseAllButton:GetText()")
.unwrap(),
"All"
);
// Fold everything through the tab: the click never calls Update() itself — the engine's
// touched flag + TRADE_SKILL_UPDATE (pump) is the whole repaint path, the ref's own contract.
s.run("BenillaTradeSkillCollapseAllButton:Click()").unwrap();
s.run("TradeSkillCollapseAllButton:Click()").unwrap();
pump(&mut s);
assert_eq!(
s.eval::<i64>("return GetNumTradeSkills()").unwrap(),
2,
"collapse-all leaves only the two headers"
);
s.run("BenillaTradeSkillCollapseAllButton:Click()").unwrap();
s.run("TradeSkillCollapseAllButton:Click()").unwrap();
pump(&mut s);
assert_eq!(s.eval::<i64>("return GetNumTradeSkills()").unwrap(), 5);
// The dropdown capsules default to the "All …" texts (the OnShow/Initialize dance).
assert_eq!(
s.eval::<String>("return BenillaTradeSkillSubClassDropDownText:GetText()")
s.eval::<String>("return TradeSkillSubClassDropDownText:GetText()")
.unwrap(),
"All Subclasses"
);
assert_eq!(
s.eval::<String>("return BenillaTradeSkillInvSlotDropDownText:GetText()")
s.eval::<String>("return TradeSkillInvSlotDropDownText:GetText()")
.unwrap(),
"All Slots"
);
@@ -160,7 +173,7 @@ fn collapse_all_tab_and_filter_dropdowns_work_end_to_end() {
);
// A REAL menu-row click: open the SubClass menu, click "Trade Goods" (row 3: All + 2 groups).
s.run("ToggleDropDownMenu(1, nil, BenillaTradeSkillSubClassDropDown)")
s.run("ToggleDropDownMenu(1, nil, TradeSkillSubClassDropDown)")
.unwrap();
s.run("DropDownList1Button3:Click()").unwrap();
pump(&mut s);
@@ -175,16 +188,16 @@ fn collapse_all_tab_and_filter_dropdowns_work_end_to_end() {
"Trade Goods"
);
// The capsule follows the picked row on the next initialize (OnShow re-runs it).
s.run("BenillaTradeSkillSubClassDropDown:Hide() BenillaTradeSkillSubClassDropDown:Show()")
s.run("TradeSkillSubClassDropDown:Hide() TradeSkillSubClassDropDown:Show()")
.unwrap();
assert_eq!(
s.eval::<String>("return BenillaTradeSkillSubClassDropDownText:GetText()")
s.eval::<String>("return TradeSkillSubClassDropDownText:GetText()")
.unwrap(),
"Trade Goods"
);
// The "All Subclasses" row (row 1) restores the full list.
s.run("ToggleDropDownMenu(1, nil, BenillaTradeSkillSubClassDropDown)")
s.run("ToggleDropDownMenu(1, nil, TradeSkillSubClassDropDown)")
.unwrap();
s.run("DropDownList1Button1:Click()").unwrap();
pump(&mut s);
@@ -200,18 +213,17 @@ fn craft_collapse_tab_loads_with_text_and_stays_hidden_for_a_flat_list() {
load_ui(&s);
assert_eq!(
s.eval::<String>("return BenillaCraftCollapseAllButton:GetText()")
s.eval::<String>("return CraftCollapseAllButton:GetText()")
.unwrap(),
"All"
);
s.fire_event("CRAFT_SHOW", vec![]);
assert!(
s.eval::<bool>("return BenillaCraftFrame:IsShown()")
.unwrap(),
s.eval::<bool>("return CraftFrame:IsShown()").unwrap(),
"the craft window opens on CRAFT_SHOW"
);
assert!(
!s.eval::<bool>("return BenillaCraftExpandButtonFrame:IsShown()")
!s.eval::<bool>("return CraftExpandButtonFrame:IsShown()")
.unwrap(),
"zero headers → the tab hides (ref l.269-282's own scan)"
);
@@ -240,13 +252,13 @@ fn reagent_slots_carry_the_questitemtemplate_shape_in_both_windows() {
s.set_trade_skill(Some(state()));
s.fire_event("TRADE_SKILL_SHOW", vec![]);
s.fire_event("CRAFT_SHOW", vec![]);
for w in ["BenillaTradeSkillReagent", "BenillaCraftReagent"] {
for w in ["TradeSkillReagent", "CraftReagent"] {
for i in 1..=3 {
s.run(&format!("{w}{i}:Show()")).unwrap();
}
}
for w in ["BenillaTradeSkillReagent", "BenillaCraftReagent"] {
for w in ["TradeSkillReagent", "CraftReagent"] {
let num = |expr: &str| {
s.eval::<f64>(&format!("return {expr}"))
.unwrap_or_else(|e| panic!("{expr}: {e}"))
@@ -276,22 +288,22 @@ fn reagent_slots_carry_the_questitemtemplate_shape_in_both_windows() {
"{w} row step"
);
// The icon: 39×39 flush in the row's TOPLEFT corner.
// The icon (`$parentIconTexture`, the reference's own region name): 39×39 flush in the row's TOPLEFT corner.
assert_eq!(
(
num(&format!("{w}1Icon:GetWidth()")),
num(&format!("{w}1Icon:GetHeight()"))
num(&format!("{w}1IconTexture:GetWidth()")),
num(&format!("{w}1IconTexture:GetHeight()"))
),
(39.0, 39.0),
"{w}1 icon"
);
assert_eq!(
num(&format!("{w}1Icon:GetLeft()")),
num(&format!("{w}1IconTexture:GetLeft()")),
num(&format!("{w}1:GetLeft()")),
"{w}1 icon flush left"
);
assert_eq!(
num(&format!("{w}1Icon:GetTop()")),
num(&format!("{w}1IconTexture:GetTop()")),
num(&format!("{w}1:GetTop()")),
"{w}1 icon flush top"
);
@@ -313,7 +325,7 @@ fn reagent_slots_carry_the_questitemtemplate_shape_in_both_windows() {
"{w}1 plate size"
);
assert_eq!(
num(&format!("{w}1NameFrame:GetLeft()")) - num(&format!("{w}1Icon:GetRight()")),
num(&format!("{w}1NameFrame:GetLeft()")) - num(&format!("{w}1IconTexture:GetRight()")),
-10.0,
"{w}1 plate rides the icon's right edge"
);
@@ -338,17 +350,17 @@ fn reagent_slots_carry_the_questitemtemplate_shape_in_both_windows() {
// The count rides the ICON's bottom-right corner (-4, +1) — not a line below the name.
assert_eq!(
num(&format!("{w}1Count:GetRight()")) - num(&format!("{w}1Icon:GetRight()")),
num(&format!("{w}1Count:GetRight()")) - num(&format!("{w}1IconTexture:GetRight()")),
-4.0,
"{w}1 count x"
);
assert_eq!(
num(&format!("{w}1Count:GetBottom()")) - num(&format!("{w}1Icon:GetBottom()")),
num(&format!("{w}1Count:GetBottom()")) - num(&format!("{w}1IconTexture:GetBottom()")),
1.0,
"{w}1 count y"
);
assert!(
num(&format!("{w}1Count:GetBottom()")) >= num(&format!("{w}1Icon:GetBottom()")),
num(&format!("{w}1Count:GetBottom()")) >= num(&format!("{w}1IconTexture:GetBottom()")),
"{w}1 count sits ON the icon, not below the row"
);
}
@@ -360,7 +372,7 @@ fn reagent_slots_carry_the_questitemtemplate_shape_in_both_windows() {
/// Neither was subtle. Both survived because this suite drove the window's tabs, dropdowns and
/// reagent slots without ever clicking or hovering a LIST ROW:
///
/// * `BenillaTradeSkillFrame_Update` addressed `BenillaTradeSkillHighlight` — the *texture* — for
/// * `TradeSkillFrame_Update` addressed `TradeSkillHighlight` — the *texture* — for
/// the Hide/SetPoint/Show that the reference does on `TradeSkillHighlightFrame`, the *frame*
/// (Blizzard_TradeSkillUI.lua l.99/142-143; only l.200's `SetVertexColor` is the texture's).
/// The frame is declared `hidden="true"`, so it never once became visible and no selection ever
@@ -392,22 +404,22 @@ fn a_row_click_shows_the_selection_glow_and_a_row_hover_shows_nothing() {
// any click — exactly what the director's screenshot should have shown and didn't.
let glow_on_row = |s: &UiScript, n: i64| {
let (glow, row) = (
s.eval::<f64>("return BenillaTradeSkillHighlightFrame:GetTop()")
s.eval::<f64>("return TradeSkillHighlightFrame:GetTop()")
.unwrap(),
s.eval::<f64>(&format!("return BenillaTradeSkillSkill{n}:GetTop()"))
s.eval::<f64>(&format!("return TradeSkillSkill{n}:GetTop()"))
.unwrap(),
);
(glow - row).abs() < 0.01
};
assert!(
s.eval::<bool>("return BenillaTradeSkillHighlightFrame:IsShown()")
s.eval::<bool>("return TradeSkillHighlightFrame:IsShown()")
.unwrap(),
"the show-time auto-selection glows — the FRAME, not just its texture"
);
assert!(glow_on_row(&s, 2), "and it is parked on the first recipe");
// A click on the OTHER Mail recipe moves it, rather than leaving it at the window's TOPLEFT.
s.run("BenillaTradeSkillSkill3:Click()").unwrap();
s.run("TradeSkillSkill3:Click()").unwrap();
pump(&mut s);
assert_eq!(
s.eval::<i64>("return GetTradeSkillSelectionIndex()")
@@ -416,17 +428,17 @@ fn a_row_click_shows_the_selection_glow_and_a_row_hover_shows_nothing() {
"the click selected row 3"
);
assert!(
s.eval::<bool>("return BenillaTradeSkillHighlightFrame:IsShown()")
s.eval::<bool>("return TradeSkillHighlightFrame:IsShown()")
.unwrap()
&& glow_on_row(&s, 3),
"the glow followed the click to row 3"
);
// Fold every group away and no recipe row is visible to carry it.
s.run("BenillaTradeSkillCollapseAllButton:Click()").unwrap();
s.run("TradeSkillCollapseAllButton:Click()").unwrap();
pump(&mut s);
assert!(
!s.eval::<bool>("return BenillaTradeSkillHighlightFrame:IsShown()")
!s.eval::<bool>("return TradeSkillHighlightFrame:IsShown()")
.unwrap(),
"no recipe row on screen → no glow (headers never take the selection)"
);
@@ -434,30 +446,20 @@ fn a_row_click_shows_the_selection_glow_and_a_row_hover_shows_nothing() {
// The hover half. Positive control first, so a `GetScript` that answered nil for everything
// could not quietly pass the real assertions below: the reagent slot DOES tooltip on hover.
assert!(
s.eval::<bool>("return BenillaTradeSkillReagent1:GetScript(\"OnEnter\") ~= nil")
s.eval::<bool>("return TradeSkillReagent1:GetScript(\"OnEnter\") ~= nil")
.unwrap(),
"control: a reagent slot has an OnEnter, so GetScript reports real handlers"
);
// The list row has no hover script at all, in the template or as a global.
for handler in ["OnEnter", "OnLeave"] {
assert!(
s.eval::<bool>(&format!(
"return BenillaTradeSkillSkill2:GetScript(\"{handler}\") == nil"
))
.unwrap(),
"a list row must carry no {handler} — the reference's rows never tooltip"
);
}
for f in [
"BenillaTradeSkillSkillButton_OnEnter",
"BenillaTradeSkillSkillButton_OnLeave",
] {
assert!(
s.eval::<bool>(&format!("return getglobal(\"{f}\") == nil"))
.unwrap(),
"{f} must not exist"
);
}
// The list row's hover is the trainer template's own — it recolours `$parentSubText` and
// nothing else (ClassTrainerFrameTemplates.xml): driven with `this` set, it opens no tooltip.
s.run("GameTooltip:Hide() this = TradeSkillSkill2 TradeSkillSkill2:GetScript(\"OnEnter\")()")
.unwrap();
assert!(
!s.eval::<bool>("return GameTooltip:IsShown()").unwrap(),
"a list row's hover opens no tooltip — the reference's rows never tooltip"
);
s.run("this = TradeSkillSkill2 TradeSkillSkill2:GetScript(\"OnLeave\")()")
.unwrap();
}
/// **The recipe list lights its rows white — under the cursor, and on the selected one.** The
@@ -498,14 +500,14 @@ fn a_hovered_or_selected_recipe_row_paints_its_label_white() {
// The label really is the button's own ButtonText now — a child FontString would leave
// GetFontString() nil and take every per-state font with it.
assert!(
s.eval::<bool>("return BenillaTradeSkillSkill2:GetFontString() ~= nil")
s.eval::<bool>("return TradeSkillSkill2:GetFontString() ~= nil")
.unwrap(),
"the row label is the Button's ButtonText, the only region per-state fonts reach"
);
let row_color = |s: &mut UiScript, n: i64| -> [f32; 4] {
let text = s
.eval::<String>(&format!("return BenillaTradeSkillSkill{n}:GetText()"))
.eval::<String>(&format!("return TradeSkillSkill{n}:GetText()"))
.unwrap();
s.resolve();
s.extract()
@@ -528,11 +530,11 @@ fn a_hovered_or_selected_recipe_row_paints_its_label_white() {
s.resolve();
let (x, y) = (
s.eval::<f64>(&format!(
"return (BenillaTradeSkillSkill{n}:GetLeft() + BenillaTradeSkillSkill{n}:GetRight()) / 2"
"return (TradeSkillSkill{n}:GetLeft() + TradeSkillSkill{n}:GetRight()) / 2"
))
.unwrap(),
s.eval::<f64>(&format!(
"return (BenillaTradeSkillSkill{n}:GetTop() + BenillaTradeSkillSkill{n}:GetBottom()) / 2"
"return (TradeSkillSkill{n}:GetTop() + TradeSkillSkill{n}:GetBottom()) / 2"
))
.unwrap(),
);
@@ -567,7 +569,7 @@ fn a_hovered_or_selected_recipe_row_paints_its_label_white() {
);
// Click row 3: the white follows the selection, and row 2 falls back to its difficulty colour.
s.run("BenillaTradeSkillSkill3:Click()").unwrap();
s.run("TradeSkillSkill3:Click()").unwrap();
pump(&mut s);
park_cursor_off_the_list(&mut s);
assert_eq!(
@@ -624,14 +626,14 @@ fn a_hovered_or_selected_craft_row_paints_its_label_white() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
s.eval::<bool>("return BenillaCraft1:GetFontString() ~= nil")
s.eval::<bool>("return Craft1:GetFontString() ~= nil")
.unwrap(),
"the craft row name is the Button's ButtonText"
);
let row_color = |s: &mut UiScript, n: i64| -> [f32; 4] {
let text = s
.eval::<String>(&format!("return BenillaCraft{n}:GetText()"))
.eval::<String>(&format!("return Craft{n}:GetText()"))
.unwrap();
s.resolve();
s.extract()
@@ -651,7 +653,7 @@ fn a_hovered_or_selected_craft_row_paints_its_label_white() {
const MEDIUM: [f32; 4] = [1.0, 1.0, 0.0, 1.0]; // CraftTypeColor["medium"]
// Row 1 is the show-time selection; row 2 is not.
s.run("SelectCraft(1); BenillaCraftFrame_Update()").unwrap();
s.run("SelectCraft(1); CraftFrame_Update()").unwrap();
s.resolve();
s.mouse_move(1000.0, 20.0);
assert_eq!(
@@ -668,16 +670,16 @@ fn a_hovered_or_selected_craft_row_paints_its_label_white() {
// Hover row 2.
s.resolve();
let (x, y) = (
s.eval::<f64>("return (BenillaCraft2:GetLeft() + BenillaCraft2:GetRight()) / 2")
s.eval::<f64>("return (Craft2:GetLeft() + Craft2:GetRight()) / 2")
.unwrap(),
s.eval::<f64>("return (BenillaCraft2:GetTop() + BenillaCraft2:GetBottom()) / 2")
s.eval::<f64>("return (Craft2:GetTop() + Craft2:GetBottom()) / 2")
.unwrap(),
);
s.mouse_move(x as f32, y as f32);
assert_eq!(row_color(&mut s, 2), WHITE, "hovered: white");
// And the selection follows a click, releasing the old row's lock.
s.run("SelectCraft(2); BenillaCraftFrame_Update()").unwrap();
s.run("SelectCraft(2); CraftFrame_Update()").unwrap();
s.resolve();
s.mouse_move(1000.0, 20.0);
assert_eq!(row_color(&mut s, 2), WHITE);
+3 -1
View File
@@ -98,7 +98,9 @@ pub use stable_slot_prices::{load_stable_slot_prices, StableSlotPrices};
mod page_text_material;
pub use page_text_material::{load_page_text_material_catalog, PageTextMaterialCatalog};
mod stationery;
pub use stationery::{load_stationery_catalog, StationeryCatalog, STATIONERY_DEFAULT};
pub use stationery::{
load_stationery_catalog, StationeryCatalog, StationeryRow, STATIONERY_DEFAULT,
};
mod lock;
pub use lock::{
load_lock_catalog, LockCatalog, LockSlot, GO_STATE_ACTIVE, GO_STATE_ACTIVE_ALTERNATIVE,
+48 -12
View File
@@ -7,12 +7,15 @@
//!
//! The 5875 schema was read byte-level from the real `patch.MPQ` file (VERIFIED at decision time):
//! WDBC header `record_count = 5`, `field_count = 4`, `record_size = 16`, string block 62 bytes —
//! four 4-byte fields, the third a string ref. Only fields 0 (`ID`) and 2 (`Texture`) are consumed
//! here; fields 1 and 3 are unsigned columns whose *meaning* is unconfirmed (field 1 reads like a
//! linked item id, field 3 a small enum) and which the reader never needs — declared as filler so
//! the field indices align. The verified rows: `1/41 → STATIONERYTEST`, `61 → GMSTATIONERY`,
//! `62 → AUCTIONSTATIONERY`, `64 → STATIONERY_VAL` (both target BLPs exist in the archive;
//! MPQ path lookup is case-insensitive, so the uppercase DBC string resolves the mixed-case file).
//! four 4-byte fields, the third a string ref: `{ID, ItemID, Texture, Flags}`. Fields 1 and 3
//! were filler until wow-re carved the send side (`ui/scratch/stationery-bindings.md`, 1970):
//! `ItemID` is the stationery ITEM the player buys or carries to use the paper, and `Flags & 1`
//! marks the one always available (`41 Default Stationery`, BuyPrice 0). The client's usable list
//! is `(Flags & 1 || the player carries ItemID) && the item's template is cached`, sorted by
//! BuyPrice ascending — the `GetNumStationeries`/`GetStationeryInfo` surface. The verified rows:
//! `1/41 → STATIONERYTEST`, `61 → GMSTATIONERY`, `62 → AUCTIONSTATIONERY`, `64 → STATIONERY_VAL`
//! (both target BLPs exist in the archive; MPQ path lookup is case-insensitive, so the uppercase
//! DBC string resolves the mixed-case file).
use std::collections::HashMap;
@@ -29,8 +32,23 @@ const STATIONERY: &str = "DBFilesClient\\Stationery.dbc";
/// [`StationeryCatalog::DEFAULT_TEXTURE`] fallback.
pub const STATIONERY_DEFAULT: u32 = 41;
/// `Stationery.dbc`: stationery id → texture basename (the `Interface\Stationery\<basename>N` stem).
/// One `Stationery.dbc` row (the module doc's four columns).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StationeryRow {
/// The stationery id a mail carries on the wire and `SelectStationery` stores.
pub id: u32,
/// The stationery item — bought or carried to use this paper.
pub item: u32,
/// The texture basename (the `Interface\Stationery\<basename>N` stem).
pub texture: String,
/// `& 1`: always available, carried or not.
pub flags: u32,
}
/// `Stationery.dbc`: stationery id → texture basename (the `Interface\Stationery\<basename>N` stem),
/// and the rows whole for the send side's usable list.
pub struct StationeryCatalog {
rows: Vec<StationeryRow>,
by_id: HashMap<u32, String>,
}
@@ -48,6 +66,17 @@ impl StationeryCatalog {
.map(String::as_str)
.unwrap_or(Self::DEFAULT_TEXTURE)
}
/// The texture basename for a stationery id the table carries — `None` for an id gap, which
/// is what `GetSelectedStationeryTexture` answers with (no default there).
pub fn texture_of(&self, id: u32) -> Option<&str> {
self.by_id.get(&id).map(String::as_str)
}
/// Every row, in DBC order.
pub fn rows(&self) -> &[StationeryRow] {
&self.rows
}
}
/// Load `Stationery.dbc` into an id → basename map (see the module doc for the verified schema).
@@ -57,17 +86,24 @@ pub fn load_stationery_catalog(chain: &mut Chain) -> Result<StationeryCatalog> {
.context("reading Stationery.dbc")?;
let mut schema = Schema::new("Stationery");
schema.add_field(SchemaField::new("ID", FieldType::UInt32));
// Field 1: an unsigned column (reads like a linked item id) — unconsumed filler.
schema.add_field(SchemaField::new("Item", FieldType::UInt32));
schema.add_field(SchemaField::new("Texture", FieldType::String));
// Field 3: a small unsigned enum — unconsumed filler.
schema.add_field(SchemaField::new("Flags", FieldType::UInt32));
let set = parse(&bytes, schema, "Stationery.dbc")?;
let mut rows = Vec::new();
let mut by_id = HashMap::new();
for r in set.records() {
if let (Some(id), Some(tex)) = (u32_at(r, 0), str_at(&set, r, 2)) {
by_id.insert(id, tex);
if let (Some(id), Some(item), Some(tex), Some(flags)) =
(u32_at(r, 0), u32_at(r, 1), str_at(&set, r, 2), u32_at(r, 3))
{
by_id.insert(id, tex.clone());
rows.push(StationeryRow {
id,
item,
texture: tex,
flags,
});
}
}
Ok(StationeryCatalog { by_id })
Ok(StationeryCatalog { rows, by_id })
}
+3
View File
@@ -480,6 +480,9 @@ pub enum SessionEvent {
/// One battleground queue slot's state (`SMSG_BATTLEFIELD_STATUS`), the client's three-slot
/// queue that `AcceptBattlefieldPort` answers out of (decision 1963).
BattlefieldStatus(crate::messages::BattlefieldStatus),
/// The battleground scoreboard (`MSG_PVP_LOG_DATA`), rows in wire order — the app resolves
/// the names, derives each row's team and pushes the sorted board (decision 1972).
PvpLogData(crate::messages::PvpLogData),
/// The meeting-stone queue state (`SMSG 0x295`): the area queued for and a status byte
/// (decision 1963).
MeetingStoneSetQueue { area: u32, status: u8 },
@@ -784,6 +784,7 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
vec![SessionEvent::AreaSpiritHealerTime { healer, ms }]
}
ServerPacket::BattlefieldStatus(status) => vec![SessionEvent::BattlefieldStatus(status)],
ServerPacket::PvpLogData(data) => vec![SessionEvent::PvpLogData(data)],
ServerPacket::MeetingStoneSetQueue { area, status } => {
vec![SessionEvent::MeetingStoneSetQueue { area, status }]
}
@@ -17,14 +17,21 @@ pub struct BattlefieldStatus {
/// Read into `+0x10`; no Lua reader was found for it (parked by the carve).
pub unknown: u32,
pub status: u32,
/// Status 2: the queue wait so far, in ms (`+0x14 = now + v`).
/// Status 2: the port deadline's delta, in ms (`+0x14 = now + Δ`, `GetBattlefieldPortExpiration`).
pub time_ms: Option<u32>,
/// Status 3: the two dwords the client parks beside the selected battlefield.
/// Status 3 (`battlefield-verb-family.md` §4.2, 1972): `(Δ₁, Δ₂)` — the instance's expiration
/// delta (`[0xb6ebb8] = now + Δ₁`, `GetBattlefieldInstanceExpiration`) and its elapsed run time
/// (`[0xb6ebbc] = now − Δ₂`, `GetBattlefieldInstanceRunTime`).
pub in_progress: Option<(u32, u32)>,
/// Status 1 (§4.2): `(estimated wait ms, raw; Δ waited)` — `[slot+0x18]` and
/// `[slot+0x1c] = now − Δ`, the `GetBattlefieldEstimatedWaitTime`/`GetBattlefieldTimeWaited` pair.
/// 1963's reader dropped this tail; the reference reads it (1972).
pub queued: Option<(u32, u32)>,
}
/// Parse `SMSG_BATTLEFIELD_STATUS`: `u32 slot`, `u32 mapId`, and — only for a non-zero map —
/// `u8 bracket`, `u32`, `u32 status`, then the status-conditional tail.
/// `u8 bracket`, `u32`, `u32 status`, then the status-conditional tail: two `u32` for status 1,
/// one for status 2, two for status 3.
pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<BattlefieldStatus> {
let slot = read_u32_le(r)?;
let map_id = read_u32_le(r)?;
@@ -37,6 +44,7 @@ pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<Battlefie
status: 0,
time_ms: None,
in_progress: None,
queued: None,
});
}
let bracket = read_u8(r)?;
@@ -52,6 +60,11 @@ pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<Battlefie
} else {
None
};
let queued = if status == 1 {
Some((read_u32_le(r)?, read_u32_le(r)?))
} else {
None
};
Ok(BattlefieldStatus {
slot,
map_id,
@@ -60,9 +73,82 @@ pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<Battlefie
status,
time_ms,
in_progress,
queued,
})
}
/// One scoreboard row of `MSG_PVP_LOG_DATA` (VERIFIED at the bytes, handler `0x4aab30`; wow-re
/// `battlefield-verb-family.md` §2.3/§4.3, 1972). The client's 0x40-byte block, wire order.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PvpLogRow {
pub guid: u64,
/// Wire field 2 — read before the kills, stored at `+0x1c`.
pub rank: u32,
pub killing_blows: u32,
pub honorable_kills: u32,
pub deaths: u32,
pub honor_gained: u32,
/// The extra-stat dwords, at most eight stored — the client consumes and discards the rest.
pub stats: Vec<u32>,
}
/// `MSG_PVP_LOG_DATA` inbound: the whole scoreboard, rows in wire order (nothing here sorts).
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct PvpLogData {
/// `u8 != 0` — the battleground has ended; `LeaveBattlefield` sends nothing until this is set.
pub ended: bool,
/// Read only when `ended`: `0` = Horde, `1` = Alliance (`GetBattlefieldWinner`).
pub winner: Option<u8>,
pub rows: Vec<PvpLogRow>,
}
/// Parse `MSG_PVP_LOG_DATA` (§4.3): `u8 ended`, `u8 winner` iff ended, `u32 count`, `count` rows of
/// `u64 guid, u32 rank, u32 kb, u32 hk, u32 deaths, u32 honor, u32 statCount, statCount × u32`.
/// The client stores no more than eight stats per row and clamps nothing else — a count past its
/// 80 blocks is its own anomaly (§10); ours keeps every row the wire carries.
pub(super) fn read_pvp_log_data(r: &mut impl Read) -> io::Result<PvpLogData> {
let ended = read_u8(r)? != 0;
let winner = if ended { Some(read_u8(r)?) } else { None };
let count = read_u32_le(r)?;
let mut rows = Vec::with_capacity(count.min(80) as usize);
for _ in 0..count {
let guid = crate::wire::read_u64_le(r)?;
let rank = read_u32_le(r)?;
let killing_blows = read_u32_le(r)?;
let honorable_kills = read_u32_le(r)?;
let deaths = read_u32_le(r)?;
let honor_gained = read_u32_le(r)?;
let stat_count = read_u32_le(r)?;
let mut stats = Vec::with_capacity(stat_count.min(8) as usize);
for i in 0..stat_count {
let v = read_u32_le(r)?;
if i < 8 {
stats.push(v);
}
}
rows.push(PvpLogRow {
guid,
rank,
killing_blows,
honorable_kills,
deaths,
honor_gained,
stats,
});
}
Ok(PvpLogData {
ended,
winner,
rows,
})
}
/// Body of `CMSG_LEAVE_BATTLEFIELD` (VERIFIED, `0x4abe60`): `u32 mapId` — the active slot's map,
/// or the literal 0 when no slot is active.
pub fn leave_battlefield(map_id: u32) -> Vec<u8> {
map_id.to_le_bytes().to_vec()
}
/// Body of `CMSG_BATTLEFIELD_PORT` (VERIFIED, `0x4ab3b0`): `u32 mapId` then a genuinely
/// one-byte `accept`, normalised to 0/1 before it reaches the wire.
pub fn battlefield_port(map_id: u32, accept: bool) -> Vec<u8> {
@@ -100,3 +186,60 @@ mod tests {
assert_eq!(battlefield_port(489, false), vec![0xE9, 1, 0, 0, 0]);
}
}
#[cfg(test)]
mod pvp_log_tests {
use super::*;
/// The scoreboard reader: the winner byte only when ended, the rank BEFORE the kills, and no
/// more than eight stats kept while every one is consumed.
#[test]
fn the_scoreboard_reads_its_conditional_winner_and_keeps_eight_stats() {
let mut body = vec![1u8, 0u8, 1, 0, 0, 0];
body.extend_from_slice(&7u64.to_le_bytes());
for v in [5u32, 3, 9, 2, 120, 9] {
body.extend_from_slice(&v.to_le_bytes());
}
for v in 1u32..=9 {
body.extend_from_slice(&v.to_le_bytes());
}
let d = read_pvp_log_data(&mut body.as_slice()).unwrap();
assert!(d.ended);
assert_eq!(d.winner, Some(0));
assert_eq!(d.rows.len(), 1);
let r = &d.rows[0];
assert_eq!(
(
r.guid,
r.rank,
r.killing_blows,
r.honorable_kills,
r.deaths,
r.honor_gained
),
(7, 5, 3, 9, 2, 120)
);
assert_eq!(r.stats, vec![1, 2, 3, 4, 5, 6, 7, 8]);
let body = vec![0u8, 0, 0, 0, 0];
let d = read_pvp_log_data(&mut body.as_slice()).unwrap();
assert!(!d.ended && d.winner.is_none() && d.rows.is_empty());
assert_eq!(leave_battlefield(489), vec![0xE9, 1, 0, 0]);
}
/// Status 1 carries the two wait dwords 1963's reader dropped.
#[test]
fn a_queued_status_carries_its_wait_pair() {
let mut body = vec![0u8, 0, 0, 0];
body.extend_from_slice(&489u32.to_le_bytes());
body.push(3);
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&1u32.to_le_bytes());
body.extend_from_slice(&30000u32.to_le_bytes());
body.extend_from_slice(&5000u32.to_le_bytes());
let s = read_battlefield_status(&mut body.as_slice()).unwrap();
assert_eq!(s.status, 1);
assert_eq!(s.queued, Some((30000, 5000)));
assert!(s.time_ms.is_none() && s.in_progress.is_none());
}
}

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