the vendor and gossip windows are the reference's own
- the vendor window is the reference's own `MerchantFrame.xml`, which took three things in order: the vendor cursor, the coin kit's move, and `GameTooltip:SetMerchantCompareItem` — where `offset` is the ordinal among CANDIDATE slots (empty ones skipped without decrementing) and a candidate matches by item CLASS and never by entry (two of four RE workers read it as an entry match, which would have fired the compare tooltip only when you already wear the identical item, i.e. never, with the window looking finished); two visible changes ride with the reference's rows — the coin columns take `SmallMoneyFrameTemplate`'s own geometry, and only the ICON takes the mouse, so hovering a row's name shows nothing, which is the client's behaviour - the gossip window is the reference's own, and its deferral was wrong: the "2px rows" were a harness gap (a bare test VM with no text measurer, while the app always installs one), not an engine one; four things the reference does that ours did not are pinned — a hidden SPACER row between the quest groups, coded options are NOT greyed (a coded row is clickable, and the send path's missing code is named as its own gap), the row label is the Button's own text, and the scroll child is not grown - the CAMP/QUIT dialogs and the PARTY_INVITE dialog move to `StaticPopup`'s home — the fourth such rehome of a dialog that had lived in whichever window first needed it — and moving them found a real bug: `button1 = CANCEL` captured a nil at load because `CANCEL` was declared below the insertion point, so the popup engine fell back and put "Yes" on the camp dialog's cancel button - three instruments: the gap report stops counting commented-out calls (`ShowInventorySellCursor` blocked the character sheet on three commented lines — a whole RE round spent on an over-report), it runs the load itself and answers in ONE table (29 "free" became 24 actually unblocked), and a shadow report predicts which swaps will fail by intersecting the names our files define with the names the chain files in our own manifest define — 38 collisions across 5 files, ours winning every one Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -109,9 +109,6 @@
|
||||
LOGOUT = LOGOUT or "Logout"
|
||||
EXIT_GAME = EXIT_GAME or "Exit Game"
|
||||
RETURN_TO_GAME = RETURN_TO_GAME or "Return to Game"
|
||||
CAMP_TIMER = CAMP_TIMER or "%d %s until logout"
|
||||
QUIT_TIMER = QUIT_TIMER or "%d %s until exit"
|
||||
QUIT_NOW = QUIT_NOW or "Exit now"
|
||||
CANCEL = CANCEL or "Cancel"
|
||||
|
||||
-- One label setter for a button with nothing behind it yet (Edit Mode and Support): set
|
||||
@@ -122,74 +119,10 @@
|
||||
button:Disable()
|
||||
end
|
||||
|
||||
-- ── The two logout dialogs (spec: StaticPopup.lua l.564-600) ────────────────────────────
|
||||
-- Both are countdown dialogs: their `text` is a format string the popup engine's per-tick
|
||||
-- branch fills with the remaining time (StaticPopup_OnUpdate's which-list already names CAMP
|
||||
-- and QUIT), so they open blank and count down. `timeout = 20` matches the server's own
|
||||
-- logout timer (vmangos `WorldSession::LogoutRequest` → 20 s), which is what the countdown
|
||||
-- is displaying — the client is not the authority here, it is narrating the server's clock.
|
||||
--
|
||||
-- The asymmetry between them is the spec's: CAMP offers only Cancel (the commented-out
|
||||
-- CAMP_NOW/ForceLogout button is dead in 1.12 — "uncomment once forced logouts are completely
|
||||
-- implemented"), while QUIT offers Exit now + Cancel, because a force-quit needs no server
|
||||
-- round trip.
|
||||
--
|
||||
-- Both entries below are written scalars-first, handlers-last, so the dialog's SHAPE reads
|
||||
-- before its behaviour does.
|
||||
|
||||
-- Dismissing a running countdown means "I didn't mean it": call the logout off. Only while
|
||||
-- it is still RUNNING, though — the popup engine zeroes `timeleft` before hiding a dialog
|
||||
-- that expired, so reaching zero leaves the logout alone. (Backwards, and every logout in
|
||||
-- the field is cancelled by its own dialog at t=0 and the character never leaves.) One
|
||||
-- function because both dialogs mean exactly this; `this` is the popup frame, which the
|
||||
-- engine has bound around the handler.
|
||||
local function CancelLogoutIfStillCounting()
|
||||
if this.timeleft > 0 then
|
||||
CancelLogout()
|
||||
end
|
||||
end
|
||||
|
||||
StaticPopupDialogs["CAMP"] = {
|
||||
text = CAMP_TIMER,
|
||||
button1 = CANCEL,
|
||||
timeout = 20,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
OnAccept = function()
|
||||
CancelLogout()
|
||||
end,
|
||||
OnHide = CancelLogoutIfStillCounting,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["QUIT"] = {
|
||||
text = QUIT_TIMER,
|
||||
button1 = QUIT_NOW,
|
||||
button2 = CANCEL,
|
||||
timeout = 20,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
-- A force-quit needs no server round trip, so the countdown it was narrating is over
|
||||
-- the moment the button is pressed: zero `timeleft` so the OnHide below stands down.
|
||||
OnAccept = function()
|
||||
ForceQuit()
|
||||
this.timeleft = 0
|
||||
end,
|
||||
OnHide = CancelLogoutIfStillCounting,
|
||||
}
|
||||
|
||||
-- The camp/quit event driver. The ref handles these three in UIParent's own OnEvent
|
||||
-- (UIParent.lua l.304-315); benilla has no UIParent frame, so this is a dedicated hidden
|
||||
-- listener — the BenillaDeleteItemConfirmDriver shape.
|
||||
function BenillaLogoutDriver_OnEvent()
|
||||
if event == "PLAYER_CAMPING" then
|
||||
StaticPopup_Show("CAMP")
|
||||
elseif event == "PLAYER_QUITING" then
|
||||
StaticPopup_Show("QUIT")
|
||||
elseif event == "LOGOUT_CANCEL" then
|
||||
StaticPopup_Hide("CAMP")
|
||||
StaticPopup_Hide("QUIT")
|
||||
end
|
||||
end
|
||||
-- The CAMP/QUIT dialogs and the three-event driver MOVED OUT, to UiPanels.xml — the
|
||||
-- reference's own homes for both (StaticPopup.lua l.564/586; UIParent.lua l.304-315).
|
||||
-- They were here only because this is where they were first needed, and 1751 moving them
|
||||
-- is one less thing to carry when this window follows them onto the chain.
|
||||
]]></Script>
|
||||
|
||||
<!-- The era header text size (Classic/Dialog/DialogTemplates.xml + the OnLoad font swap):
|
||||
@@ -378,18 +311,4 @@
|
||||
</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- The camp/quit event listener (the ref's UIParent OnEvent arms — script block above). -->
|
||||
<Frame name="BenillaLogoutDriver" hidden="true">
|
||||
<Size><AbsDimension x="1" y="1"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
self:RegisterEvent("PLAYER_CAMPING")
|
||||
self:RegisterEvent("PLAYER_QUITING")
|
||||
self:RegisterEvent("LOGOUT_CANCEL")
|
||||
</OnLoad>
|
||||
<OnEvent>BenillaLogoutDriver_OnEvent()</OnEvent>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
|
||||
@@ -1,568 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's gossip dialog (decision 0081 phase 3), RE-SKINNED to the real 1.12 gossip/quest
|
||||
greeting window (dressing arc, decision 0084): the geometry and art below are QUOTED from the
|
||||
real `GossipFrame.xml` (scratchpad `ref-GossipFrame.xml`; line citations inline), turned onto
|
||||
our own feeds/intents. The name `GossipFrame`, its panel registration
|
||||
(UiPanels.xml's UIPanelWindows["GossipFrame"], area="left", pushable=0), the Era gossip
|
||||
feeds (GetGossipText/GetGossipOptions/IsGossipOptionCoded/SelectGossipOption/CloseGossip/
|
||||
GetNumGossipQuests/GetGossipQuestInfo/SelectGossipQuest), and the intent functions are
|
||||
UNCHANGED — only the window's dressing is real now.
|
||||
|
||||
A hidden window that shows on GOSSIP_SHOW and hides on GOSSIP_CLOSED. It wears the real
|
||||
QuestFrame parchment: four quadrant slabs (UI-QuestGreeting-{TopLeft,TopRight,BotLeft,BotRight},
|
||||
ref l.13-44) at the real 384×512 (ref l.128); the greeting body and the row list quoted from
|
||||
the real GossipGreetingText / GossipTitleButtonTemplate (ref l.86-125, 223-264).
|
||||
|
||||
QUEST ROWS (decision 0088 §3, filled in): the real `GossipFrameUpdate` (ref l.24-29) fills ONE
|
||||
shared `GossipTitleButton` pool through three sequential passes over a single `buttonIndex`
|
||||
counter — `GossipFrameAvailableQuestsUpdate` (ref l.63-85) first, `GossipFrameActiveQuestsUpdate`
|
||||
(ref l.87-109) second, `GossipFrameOptionsUpdate` (ref l.111-128) third — so quest rows always
|
||||
land ABOVE the option rows, and the two blocks share the very same row buttons/anchors (a
|
||||
`titleButton.type` field picked at fill time routes the click: `SelectGossipAvailableQuest` /
|
||||
`SelectGossipActiveQuest` / `SelectGossipOption`, ref l.53-61). We mirror that shape exactly but
|
||||
over our OWN flatter seam (`benilla-ui` `script/gossip.rs`): the app already resolves the wire's
|
||||
available/active split and delivers ONE ordered `GetNumGossipQuests()`/`GetGossipQuestInfo(i)`
|
||||
list (quest rows first, in wire order), so `BenillaGossipFrame_Update` below runs a single loop
|
||||
over the shared `GossipTitleButton1..8` pool — quest rows fill from i=1, option rows fill after
|
||||
them — instead of the ref's three separate counters; a row's `.kind` ("quest"/"option") plus its
|
||||
`.itemIndex` (the row's 1-based position within ITS OWN list) is all `BenillaGossipRow_OnClick`
|
||||
needs to call the right one of `SelectGossipQuest`/`SelectGossipOption` (ref's three intents
|
||||
collapse to two on our seam — see gossip.rs's doc comment). Per-row icon: active → the real
|
||||
`Interface\GossipFrame\ActiveQuestIcon` (ref l.99), available → `Interface\GossipFrame\
|
||||
AvailableQuestIcon` (ref l.75) — both verified present in the MPQ list. We DO run the ref's
|
||||
per-row `GossipResize` auto-height (ref l.130-132, `SetHeight(GetTextHeight()+2)`) — see
|
||||
`BenillaGossipRow_Resize` below — but skip its "too many quests" message()
|
||||
(ref l.67-69/91-93/115-117): the shared 8-row cap below silently drops the overflow exactly
|
||||
like the option rows always did.
|
||||
|
||||
ENGINE-SLICE CAVEAT (this branch predates the dressing arc's engine slice — named `<Font>`
|
||||
objects are not implemented here yet, loader.rs "named Font objects are not implemented in v1").
|
||||
The real window's `inherits="QuestFont"` FontStrings are transcribed faithfully below (they are
|
||||
part of the quoted geometry), but until the font-object slice + our Fonts.xml land on this line
|
||||
the loader warns "unknown template 'QuestFont'" and the strings render in the default font —
|
||||
no worse than before (nothing here inherited a font). They light up automatically when the
|
||||
slice merges. No <TexCoords> is used: every quadrant samples its whole texture (the real gossip
|
||||
window uses TexCoords only on the material/patch art we omit), so this window needs none of it.
|
||||
|
||||
The NPC name rides in the title banner (ref GossipFrameNpcNameText, l.157-178): a GameFontHighlight
|
||||
FontString centred at the window TOP -23, mirroring the merchant's pattern — the app resolves the
|
||||
gossip NPC's guid through the NameCache and delivers the name as arg1 of GOSSIP_SHOW; empty until
|
||||
it lands. The GOODBYE button rides bottom-right (ref GossipFrameGreetingGoodbyeButton, l.206-222),
|
||||
wired to the same close path as the close button.
|
||||
|
||||
The scroll frame IS here now (ref l.223-436): the greeting + the 32-row pool ride a real
|
||||
`<ScrollFrame>`, so a menu taller than the parchment scrolls instead of spilling out of the
|
||||
window. See the ScrollFrame's own comment below. Nothing of the real window is knowingly
|
||||
omitted any more except the BotLeftPatch material art.
|
||||
|
||||
UIPanel slot system (decision 0084 §2): carries NO hardcoded <Anchors> — ShowUIPanel/SetLeftFrame
|
||||
SetPoints it to the left slot (TOPLEFT UIParent, 0, -104) on open; every show goes through
|
||||
ShowUIPanel, every hide through HideUIPanel, never Show()/Hide() directly.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- The shared row pool's cap (decision 0088 §3): quest rows fill from slot 1, option rows fill
|
||||
-- after them, both drawing from the SAME GossipTitleButton1..32 buttons (ref's single
|
||||
-- GossipTitleButton pool, l.24-29/63-128). This IS the reference's number
|
||||
-- (`NUMGOSSIPBUTTONS = 32`, ref-GossipFrame.lua l.1); benilla ran a cap of 8 only because an
|
||||
-- overflowing menu had nowhere to go — with the scroll frame below, it does.
|
||||
BENILLA_GOSSIP_MAX = 32
|
||||
|
||||
-- Every widget that can be the LOWEST visible thing in the scroll child, for the content
|
||||
-- measure (BenillaScroll_ResizeChild, ScrollTemplates.xml). Built once at load, like the
|
||||
-- quest window's per-panel lists.
|
||||
BENILLA_GOSSIP_SCROLL_WIDGETS = (function()
|
||||
local t = { "GossipGreetingText" }
|
||||
for i = 1, BENILLA_GOSSIP_MAX do table.insert(t, "GossipTitleButton" .. i) end
|
||||
return t
|
||||
end)()
|
||||
|
||||
-- The Era icon-type string → the real Blizzard gossip-icon texture (resolves from the MPQ at
|
||||
-- render). Unmapped types fall back to the chat bubble (the app already defaults unknowns to
|
||||
-- "gossip", so this is belt-and-braces).
|
||||
--
|
||||
-- These are ALL TEN the 1.12 install ships under `Interface\GossipFrame\*GossipIcon.blp`.
|
||||
-- The reference builds the path by pasting the type string straight in (ref
|
||||
-- GossipFrame.lua:123), so the shipped art is the whole vocabulary a row can draw. The app
|
||||
-- indexes the wire icon byte into the client's own name table (`ui_gossip::
|
||||
-- GOSSIP_ICON_TYPES`, VERIFIED at 0x84b7ac — decisions 1331/1335); four of these
|
||||
-- (healer/banker/petition/tabard) were unreachable before 1331, which is why the
|
||||
-- innkeeper's bind line drew a speech bubble (B249).
|
||||
--
|
||||
-- The client's table has an ELEVENTH name, `auctioneer` at index 10, and 5875 ships no
|
||||
-- texture for it — the reference draws nothing there. No key for it here on purpose: our
|
||||
-- lookup then falls back to the chat bubble, which is the better wrong answer than a
|
||||
-- missing texture, and `ui_gossip`'s test pins the absence so nobody invents a path.
|
||||
BENILLA_GOSSIP_ICONS = {
|
||||
gossip = "Interface\\GossipFrame\\GossipGossipIcon",
|
||||
vendor = "Interface\\GossipFrame\\VendorGossipIcon",
|
||||
taxi = "Interface\\GossipFrame\\TaxiGossipIcon",
|
||||
trainer = "Interface\\GossipFrame\\TrainerGossipIcon",
|
||||
healer = "Interface\\GossipFrame\\HealerGossipIcon",
|
||||
binder = "Interface\\GossipFrame\\BinderGossipIcon",
|
||||
banker = "Interface\\GossipFrame\\BankerGossipIcon",
|
||||
petition = "Interface\\GossipFrame\\PetitionGossipIcon",
|
||||
tabard = "Interface\\GossipFrame\\TabardGossipIcon",
|
||||
battlemaster = "Interface\\GossipFrame\\BattleMasterGossipIcon",
|
||||
}
|
||||
|
||||
-- The real per-row quest icons (ref l.75/99) — active (in-progress/turn-in) vs available
|
||||
-- (not yet accepted), the same split `GossipQuestRow.active` already carries off the wire.
|
||||
BENILLA_GOSSIP_QUEST_ICON_ACTIVE = "Interface\\GossipFrame\\ActiveQuestIcon"
|
||||
BENILLA_GOSSIP_QUEST_ICON_AVAILABLE = "Interface\\GossipFrame\\AvailableQuestIcon"
|
||||
|
||||
function BenillaGossipRow_OnLoad(button)
|
||||
button.icon = getglobal(button:GetName() .. "Icon")
|
||||
button.label = getglobal(button:GetName() .. "Label")
|
||||
end
|
||||
|
||||
-- The ref's GossipResize (ref l.130-132: `SetHeight(GetTextHeight() + 2)`) — a row is as
|
||||
-- tall as its OWN wrapped label. Load-bearing, not cosmetic: the rows hang off each other
|
||||
-- in one static chain (each on the previous row's BOTTOMLEFT), so a row left at the
|
||||
-- template's 16px while its label draws three wrapped lines puts the next row's text
|
||||
-- straight through this one's — every long option overprinting its neighbour (the
|
||||
-- director's judgement-menu screenshot). The reference's own LEFT-anchored label is only
|
||||
-- correct BECAUSE of this resize: centring the text on a row sized to the text is the same
|
||||
-- as top-aligning it.
|
||||
--
|
||||
-- Deviation from the ref, the same one the tab-fit carries (UiPanels.xml
|
||||
-- `BenillaTabButton_OnUpdate`): the client's GetTextHeight is a synchronous font-engine
|
||||
-- call, ours is the measure round-trip's answer and lands a frame late — 0 until the host
|
||||
-- has measured THIS text. A zero keeps the current height (never collapses the row) and
|
||||
-- the frame's OnUpdate below re-runs the fit until it settles.
|
||||
function BenillaGossipRow_Resize(button)
|
||||
local h = button.label:GetHeight()
|
||||
if h <= 0 or button.benillaFitHeight == h then return end
|
||||
button.benillaFitHeight = h
|
||||
button:SetHeight(h + 2)
|
||||
end
|
||||
|
||||
-- Size the scroll child to the rows actually on it, so the bar ranges over the real content
|
||||
-- (the ref grows its child with the 15px GossipSpacerFrame under the last row instead —
|
||||
-- ref-GossipFrame.lua l.39-44; the shared kit's BENILLA_SCROLL_PAD is that breathing room).
|
||||
function BenillaGossipFrame_SizeScrollChild()
|
||||
BenillaScroll_ResizeChild("GossipGreetingScrollFrame", BENILLA_GOSSIP_SCROLL_WIDGETS)
|
||||
end
|
||||
|
||||
-- The convergence driver (the tab-fit / quest-panel idiom): while the menu is up, re-fit
|
||||
-- every visible row until its measure lands, after which the guard above makes this a
|
||||
-- no-op compare per row per frame. OnUpdate only fires on effectively-visible frames, so
|
||||
-- a closed gossip window costs nothing. The rows fit FIRST and the child is measured after,
|
||||
-- so the scroll range sees their settled heights in the same pass.
|
||||
function BenillaGossipFrame_OnUpdate()
|
||||
for i = 1, BENILLA_GOSSIP_MAX do
|
||||
local button = getglobal("GossipTitleButton" .. i)
|
||||
if button:IsShown() then
|
||||
BenillaGossipRow_Resize(button)
|
||||
end
|
||||
end
|
||||
BenillaGossipFrame_SizeScrollChild()
|
||||
end
|
||||
|
||||
-- One shared row serves either a quest or an option depending on what
|
||||
-- BenillaGossipFrame_Update assigned it this repaint (button.kind, set there) — mirrors the
|
||||
-- ref's titleButton.type dispatch (ref l.53-61), collapsed onto our seam's two intents
|
||||
-- (SelectGossipQuest covers both the ref's Available/Active clicks, gossip.rs's doc comment).
|
||||
function BenillaGossipRow_OnClick(button)
|
||||
if button.kind == "available" then
|
||||
SelectGossipAvailableQuest(button.itemIndex)
|
||||
elseif button.kind == "active" then
|
||||
SelectGossipActiveQuest(button.itemIndex)
|
||||
else
|
||||
SelectGossipOption(button.itemIndex)
|
||||
end
|
||||
end
|
||||
|
||||
function BenillaGossipFrame_Update()
|
||||
getglobal("GossipGreetingText"):SetText(GetGossipText() or "")
|
||||
-- **The reference's own two lists, in the reference's own order** (ref l.27-28:
|
||||
-- available, then active). Each verb returns a flat vararg of (title, level) PAIRS, and
|
||||
-- each click indexes back INTO ITS OWN LIST — which is why the row remembers which
|
||||
-- list it came from rather than one shared position. This replaced
|
||||
-- `GetNumGossipQuests`/`GetGossipQuestInfo`/`SelectGossipQuest`, three names benilla
|
||||
-- invented and 1.12 does not have (1189: a superset is not free).
|
||||
local avail = { GetGossipAvailableQuests() }
|
||||
local active = { GetGossipActiveQuests() }
|
||||
local numAvail, numActive = #avail / 2, #active / 2
|
||||
local numQuests = numAvail + numActive
|
||||
-- GetGossipOptions() returns a flat vararg of (label, type) pairs (the Era shape).
|
||||
local opts = { GetGossipOptions() }
|
||||
local numOptions = #opts / 2
|
||||
for i = 1, BENILLA_GOSSIP_MAX do
|
||||
local button = getglobal("GossipTitleButton" .. i)
|
||||
if i <= numQuests then
|
||||
local isActive = i > numAvail
|
||||
local listIndex = isActive and (i - numAvail) or i
|
||||
local src = isActive and active or avail
|
||||
button.kind = isActive and "active" or "available"
|
||||
button.itemIndex = listIndex
|
||||
button.label:SetText(src[(listIndex - 1) * 2 + 1])
|
||||
BenillaGossipRow_Resize(button)
|
||||
button.icon:SetTexture(isActive and BENILLA_GOSSIP_QUEST_ICON_ACTIVE or BENILLA_GOSSIP_QUEST_ICON_AVAILABLE)
|
||||
button:Enable()
|
||||
button:Show()
|
||||
elseif i <= numQuests + numOptions then
|
||||
-- Option rows fill after every quest row (ref l.29 runs last) — itemIndex is the
|
||||
-- row's position WITHIN the options list, not the shared pool slot.
|
||||
local optIndex = i - numQuests
|
||||
local label = opts[(optIndex - 1) * 2 + 1]
|
||||
local otype = opts[(optIndex - 1) * 2 + 2]
|
||||
button.kind = "option"
|
||||
button.itemIndex = optIndex
|
||||
button.label:SetText(label)
|
||||
-- A label whose measure is already cached (a revisit to this NPC) fits on this
|
||||
-- very repaint; a fresh string fits on the next OnUpdate tick.
|
||||
BenillaGossipRow_Resize(button)
|
||||
button.icon:SetTexture(BENILLA_GOSSIP_ICONS[otype] or BENILLA_GOSSIP_ICONS.gossip)
|
||||
-- Coded (password) options are greyed and unselectable in v1 (decision 0081).
|
||||
if IsGossipOptionCoded(optIndex) then
|
||||
button:Disable()
|
||||
else
|
||||
button:Enable()
|
||||
end
|
||||
button:Show()
|
||||
else
|
||||
button:Hide()
|
||||
end
|
||||
end
|
||||
-- The ref's GossipFrameUpdate tail (ref-GossipFrame.lua l.46-48): a repaint always lands
|
||||
-- back at the TOP of the menu, then re-ranges the bar off the new content.
|
||||
getglobal("GossipGreetingScrollFrame"):SetVerticalScroll(0)
|
||||
BenillaGossipFrame_SizeScrollChild()
|
||||
end
|
||||
|
||||
function GossipFrame_OnLoad()
|
||||
local frame = this
|
||||
-- The NPC portrait: the "npc" unit token → the interaction NPC's live model bake
|
||||
-- (crates/benilla/src/portrait, InteractNpc). Bound once; the app re-resolves it every
|
||||
-- frame, so a revisit to a different gossip NPC shows that face.
|
||||
SetPortraitTexture(getglobal(frame:GetName() .. "Portrait"), "npc")
|
||||
frame:RegisterEvent("GOSSIP_SHOW")
|
||||
frame:RegisterEvent("GOSSIP_CLOSED")
|
||||
end
|
||||
|
||||
-- The gossip NPC's name arrives as arg1 of GOSSIP_SHOW (the app resolves the NPC guid through
|
||||
-- the NameCache); nil/empty until it lands → a blank banner (mirrors the merchant title).
|
||||
function BenillaGossipFrame_SetTitle(name)
|
||||
getglobal("GossipFrameNpcNameText"):SetText(name or "")
|
||||
end
|
||||
|
||||
function GossipFrame_OnEvent()
|
||||
local frame = this
|
||||
if event == "GOSSIP_SHOW" then
|
||||
BenillaGossipFrame_SetTitle(arg1)
|
||||
BenillaGossipFrame_Update()
|
||||
ShowUIPanel(frame)
|
||||
elseif event == "GOSSIP_CLOSED" then
|
||||
HideUIPanel(frame)
|
||||
end
|
||||
end
|
||||
|
||||
function BenillaGossipCloseButton_OnClick()
|
||||
-- vanilla's client-side close sends no packet; the app clears the menu on this intent
|
||||
-- (which fires GOSSIP_CLOSED), and we hide immediately so there's no one-frame lag.
|
||||
CloseGossip()
|
||||
HideUIPanel(getglobal("GossipFrame"))
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!--
|
||||
One shared gossip row (ref GossipTitleButtonTemplate, l.86-125): the real 300×16 template, used
|
||||
by the REAL client for quest rows AND option rows alike (a single pool, ref l.24-29/63-128) — we
|
||||
do the same rather than carrying two near-identical templates. The type icon sits at the row's
|
||||
TOPLEFT (16×16; ref $parentGossipIcon, l.92-102) — the real XML defaults it to
|
||||
UI-Quest-BulletPoint, but the real client (like us) overrides it per row kind at Update time, so
|
||||
we carry no default file and let BenillaGossipFrame_Update set it (quest → Active/AvailableQuestIcon,
|
||||
ref l.75/99; option → the per-type GossipIcon). The label is QuestFont, LEFT, at LEFT +20 (ref
|
||||
ButtonText, l.112-124). The highlight is the real UI-QuestTitleHighlight ADD glow (ref l.111).
|
||||
-->
|
||||
<!-- ref GossipFrame.xml l.4-85. The gossip window's panel is a template of its own in the
|
||||
reference, carrying the four QuestGreeting quadrant slabs AND the four `$parentMaterial*`
|
||||
overlays. Ours had neither: the corner art sat directly on `GossipFrame` and there was no
|
||||
panel at all, so the reference's own `GossipFrameGreetingPanel` and its four Material
|
||||
regions did not exist. pfUI's `skins/blizzard/gossipquest.lua` walks
|
||||
`_G[frameName..panel.."Panel"]` and then its four `Material*` children for the Gossip
|
||||
window exactly as it does for Quest.
|
||||
The materials are fileless and unshown for the same reason as QuestFrame's — Parchment is
|
||||
the branch this client always takes (`QuestFrame_GetMaterial`), which hides all four. -->
|
||||
<Frame name="GossipFramePanelTemplate" virtual="true">
|
||||
<Size><AbsDimension x="384" y="512"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\QuestFrame\UI-QuestGreeting-TopLeft">
|
||||
<Size><AbsDimension x="256" y="256"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\QuestFrame\UI-QuestGreeting-TopRight">
|
||||
<Size><AbsDimension x="128" y="256"/></Size>
|
||||
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"/></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\QuestFrame\UI-QuestGreeting-BotLeft">
|
||||
<Size><AbsDimension x="256" y="256"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"/></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\QuestFrame\UI-QuestGreeting-BotRight">
|
||||
<Size><AbsDimension x="128" y="256"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="BORDER">
|
||||
<Texture name="$parentMaterialTopLeft" hidden="true">
|
||||
<Size><AbsDimension x="239" y="241"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="21" y="-75"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<Texture name="$parentMaterialTopRight" hidden="true">
|
||||
<Size><AbsDimension x="64" y="241"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentMaterialTopLeft" relativePoint="TOPRIGHT"/></Anchors>
|
||||
</Texture>
|
||||
<Texture name="$parentMaterialBotLeft" hidden="true">
|
||||
<Size><AbsDimension x="239" y="128"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentMaterialTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
|
||||
</Texture>
|
||||
<Texture name="$parentMaterialBotRight" hidden="true">
|
||||
<Size><AbsDimension x="64" y="128"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentMaterialTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
|
||||
<Button name="GossipTitleButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="300" y="16"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentIcon">
|
||||
<Size><AbsDimension x="16" y="16"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentLabel" inherits="QuestFont" justifyH="LEFT">
|
||||
<Size><AbsDimension x="275" y="0"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="20" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<HighlightTexture file="Interface\QuestFrame\UI-QuestTitleHighlight" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnClick>BenillaGossipRow_OnClick(self)</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Frame name="GossipFrame" enableMouse="true" toplevel="true" parent="UIParent" hidden="true">
|
||||
<!-- The real 384×512 window (ref l.128). No <Anchors> — ShowUIPanel/SetLeftFrame SetPoints this
|
||||
to the left slot on open. -->
|
||||
<Size><AbsDimension x="384" y="512"/></Size>
|
||||
<Layers>
|
||||
<!-- The four QuestGreeting quadrant slabs that ARE the parchment art (ref l.13-44): 256-wide
|
||||
left halves, 128-wide right halves, each 256 tall, pinned to their corner. No <TexCoords>
|
||||
— each samples its whole texture. The solid-colour fill is gone (the parchment is
|
||||
opaque). -->
|
||||
<Layer level="BACKGROUND">
|
||||
<!-- NPC portrait 60×60 TOPLEFT (7,-6) (ref GossipFramePortrait), declared BEFORE the
|
||||
parchment so it draws UNDER it and the parchment's ring frames the portrait (the empty
|
||||
ring showed the scene through the hole before). Bound in OnLoad, circle-masked. -->
|
||||
<Texture name="$parentPortrait">
|
||||
<Size><AbsDimension x="60" y="60"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<!-- NPC name banner (ref GossipFrameNpcNameText l.157-178): GameFontHighlight, centred
|
||||
on the window at TOP -23. The app delivers the name as arg1 of GOSSIP_SHOW; blank
|
||||
until it lands. CENTER + the single-point (~zero-width) rect keeps the name centred
|
||||
regardless of length, exactly like the merchant's title. -->
|
||||
<FontString name="GossipFrameNpcNameText" inherits="GameFontHighlight" justifyH="CENTER">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-23"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- ref l.188-204. The greeting PANEL — the parchment page the whole gossip window
|
||||
is drawn on. It did not exist here: the corner art sat on `GossipFrame` itself and
|
||||
the scroll hung straight off the window, so the reference's own
|
||||
`GossipFrameGreetingPanel` (and the four Material regions it inherits) had no
|
||||
counterpart. The art moved onto `GossipFramePanelTemplate` above with it, which is
|
||||
where the reference keeps it. The BotLeftPatch stays a per-instance ARTWORK
|
||||
texture, as the ref declares it here rather than on the template. -->
|
||||
<Frame name="GossipFrameGreetingPanel" inherits="GossipFramePanelTemplate">
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<!-- Gossip has no bottom-LEFT button, but the shared UI-QuestGreeting-BotLeft
|
||||
art bakes the button recess in — the ref covers it with this patch
|
||||
(the director's "bottom left hole"). -->
|
||||
<Texture file="Interface\QuestFrame\UI-Quest-BotLeftPatch">
|
||||
<Size><AbsDimension x="128" y="64"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="22" y="68"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="GossipFrameGreetingGoodbyeButton">
|
||||
<Size><AbsDimension x="78" y="22"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-39" y="73"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentText" inherits="GameFontNormal" justifyH="CENTER" text="Goodbye">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<!-- The button face occupies only the top-left 0.625×0.6875 of each file — the real
|
||||
UIPanelButton*Texture templates slice it with exactly these TexCoords
|
||||
(ref-UIPanelTemplates.xml l.3-17). Drawing the whole file (transparent padding
|
||||
included) squashed the face into the scuffed blob the director's A/B caught. -->
|
||||
<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>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD">
|
||||
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
|
||||
</HighlightTexture>
|
||||
<Scripts>
|
||||
<OnClick>BenillaGossipCloseButton_OnClick()</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!--
|
||||
The greeting + row list live inside a REAL ScrollFrame — the reference's own structure
|
||||
(ref-GossipFrame.xml l.223-436, extracted via mpqx this session), which benilla omitted
|
||||
on the grounds that "our short greeting needs no scrolling". A gossip menu of wrapped
|
||||
roleplay options is exactly the case that breaks: once each row grew to its text
|
||||
(BenillaGossipRow_Resize), four long options ran off the parchment and out of the
|
||||
window (the director's second screenshot). The ref's answer is this scroll frame, and
|
||||
it is now ours: 300x334 at the window's TOPLEFT +(23,-81) (ref l.223-233) over a child
|
||||
the same size (ref l.235-239), the greeting at the child's +(10,-10) (ref l.241-252),
|
||||
and the row chain hanging off the greeting exactly as before (ref l.256-269).
|
||||
|
||||
Content taller than the frame is CLIPPED to it and panned (engine mechanism, decision
|
||||
0112) instead of overflowing; BenillaGossipFrame_SizeScrollChild sizes the child to the
|
||||
measured content each repaint, which re-ranges the bar through OnScrollRangeChanged.
|
||||
The ref grows its child with a 15px GossipSpacerFrame under the last row (ref l.421-432,
|
||||
re-anchored per update by GossipFrame.lua l.39-44); ours is the shared kit's
|
||||
BENILLA_SCROLL_PAD, the same breathing room by a plainer mechanism.
|
||||
-->
|
||||
<ScrollFrame name="GossipGreetingScrollFrame" inherits="UIPanelScrollFrameTemplate">
|
||||
<Size><AbsDimension x="300" y="334"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="23" y="-81"/></Offset></Anchor></Anchors>
|
||||
<ScrollChild>
|
||||
<Frame name="GossipGreetingScrollChildFrame">
|
||||
<Size><AbsDimension x="300" y="334"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<!-- Greeting body: the real GossipGreetingText (ref l.241-252) — QuestFont,
|
||||
LEFT, 270 wide so the engine wraps it, at the child's +(10,-10). -->
|
||||
<FontString name="GossipGreetingText" inherits="QuestFont" justifyH="LEFT">
|
||||
<Size><AbsDimension x="270" y="0"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="10" y="-10"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The shared row pool, now the reference's full NUMGOSSIPBUTTONS = 32
|
||||
(ref-GossipFrame.lua l.1; ours capped at 8 only because an overflowing
|
||||
menu had nowhere to go). Quest rows fill it first, option rows after
|
||||
(decision 0088 sec.3). Row 1 hangs off the GREETING's BOTTOMLEFT +(-10,-20),
|
||||
the rest off the previous row's BOTTOMLEFT (ref l.256-269) — one static
|
||||
chain, so rows re-flow off each other's measured heights with no runtime
|
||||
repositioning. -->
|
||||
<Button name="GossipTitleButton1" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipGreetingText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-10" y="-20"/></Offset></Anchor></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton2" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton1" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton3" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton2" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton4" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton3" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton5" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton4" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton6" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton5" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton7" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton6" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton8" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton7" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton9" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton8" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton10" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton9" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton11" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton10" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton12" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton11" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton13" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton12" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton14" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton13" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton15" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton14" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton16" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton15" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton17" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton16" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton18" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton17" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton19" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton18" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton20" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton19" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton21" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton20" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton22" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton21" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton23" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton22" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton24" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton23" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton25" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton24" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton26" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton25" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton27" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton26" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton28" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton27" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton29" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton28" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton30" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton29" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton31" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton30" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
<Button name="GossipTitleButton32" inherits="GossipTitleButtonTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="GossipTitleButton31" relativePoint="BOTTOMLEFT"/></Anchors><Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts></Button>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</ScrollChild>
|
||||
</ScrollFrame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<!-- Close: the real GossipFrameCloseButton position (CENTER on the window's TOPRIGHT,
|
||||
-42,-31; ref l.179-187), on the minimize-button art our close already used (the real
|
||||
UIPanelCloseButton art rides the tooltip/dialog arc). -->
|
||||
<Button name="GossipFrameCloseButton">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-42" y="-31"/></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>BenillaGossipCloseButton_OnClick()</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- GOODBYE button (ref GossipFrameGreetingGoodbyeButton l.206-222): the real red
|
||||
UIPanelButtonTemplate, 78×22, BOTTOMRIGHT of the window -39,73. Our engine ships no
|
||||
UIPanelButtonTemplate, so the button face is built directly from the very textures that
|
||||
template resolves to — UI-Panel-Button-{Up,Down,Highlight} (all verified in the MPQ),
|
||||
stretched over the button as its Normal/Pushed/Highlight art. The "Goodbye" label
|
||||
(GlobalStrings GOODBYE = "Goodbye") is an OVERLAY FontString centred on the button (the
|
||||
real ButtonText draws in the OVERLAY layer). Wired to the same close path as the close
|
||||
button (CloseGossip → HideUIPanel), mirroring the ref's OnClick HideUIPanel(GossipFrame). -->
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>GossipFrame_OnLoad()</OnLoad>
|
||||
<OnEvent>GossipFrame_OnEvent()</OnEvent>
|
||||
<!-- The row auto-height's settle pass (see BenillaGossipRow_Resize): our text measure
|
||||
is a frame late, so the fit re-runs here until every visible row's label measure
|
||||
lands. Fires only while the window is effectively visible. -->
|
||||
<OnUpdate>BenillaGossipFrame_OnUpdate()</OnUpdate>
|
||||
<!-- OnShow/OnHide, quoted from the real GossipFrame.xml frame Scripts (l.445/454): the
|
||||
kits igQuestListOpen/Close, and — load-bearing — CloseGossip() on hide (the real
|
||||
GossipFrame_OnHide). CloseGossip ends the gossip session whenever the frame hides,
|
||||
INCLUDING when another NPC window displaces it at the UIPanel slot (SetLeftFrame →
|
||||
oldFrame:Hide() → OnHide); without it the GossipState resource stayed set after a
|
||||
vendor/quest window replaced the menu, and gossip would not reopen until the
|
||||
range-guard reset it (decision 0095). No-packet client-side clear; idempotent with the
|
||||
close button, which also calls it. Never fires at load (hidden="true"). -->
|
||||
<OnShow>PlaySound("igQuestListOpen")</OnShow>
|
||||
<OnHide>CloseGossip(); PlaySound("igQuestListClose")</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,87 +31,20 @@
|
||||
real job is the timeout/override/escape paths.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ── MAX_RAID_MEMBERS (ref RaidFrame.lua l.2) ─────────────────────────────────────────
|
||||
-- A fact about the protocol, not about our implementation: a 1.12 raid is 40, and the
|
||||
-- `raid1`..`raid40` token space is 40 wide whether or not we build a raid UI. Declared
|
||||
-- here rather than in a RaidFrame.xml because we ship none; party and raid are the same
|
||||
-- subsystem family, and this is the file that already owns the group surface.
|
||||
--
|
||||
-- **The loop it feeds already works.** `for i = 1, MAX_RAID_MEMBERS do UnitName("raid"..i)`
|
||||
-- answers nil for every absent token, which is exactly right for a player not in a raid —
|
||||
-- so the constant is not a promise of a raid UI, it is the bound the token space has.
|
||||
--
|
||||
-- Reached by following a `'for' limit must be a number` five addons shared:
|
||||
-- `CT_RaidAssist.lua:3283` is `for i = 1, MAX_RAID_MEMBERS, 1 do`. Read with `grep -a`,
|
||||
-- because this corpus's Ace files are ISO-8859 and plain grep silently skips them.
|
||||
MAX_RAID_MEMBERS = 40
|
||||
-- Its two siblings, each reached one line further along the same chain
|
||||
-- (`CT_RAOptions.lua:75`, then `:231`). The reference declares all three together at
|
||||
-- RaidFrame.lua l.2-4, and they agree: 40 = 8 x 5. Adding them one at a time as an addon
|
||||
-- asked was the wrong shape — they are one fact about the raid token space, so they land
|
||||
-- as one.
|
||||
NUM_RAID_GROUPS = 8
|
||||
MEMBERS_PER_RAID_GROUP = 5
|
||||
-- MAX_RAID_MEMBERS / NUM_RAID_GROUPS / MEMBERS_PER_RAID_GROUP moved out. They were
|
||||
-- declared here on the stated grounds that "we ship no RaidFrame.xml", which stopped being
|
||||
-- true — RaidFrame.xml declares all three, which is the reference's own home for them
|
||||
-- (RaidFrame.lua l.2-4). Nothing between the two manifest positions reads them at load.
|
||||
-- The party's own bound (ref PartyMemberFrame.lua l.1) — four *other* members, so a full
|
||||
-- party sweep that includes the player runs `1, MAX_PARTY_MEMBERS+1`, which is exactly how
|
||||
-- GroupLootDropDown_Initialize walks the master-loot candidates (LootFrame.lua l.219).
|
||||
-- Published for the same reason as its raid siblings: the addon corpus reads it.
|
||||
MAX_PARTY_MEMBERS = 4
|
||||
|
||||
-- ── The group-invite popup ────────────────────────────────────────────────────────────
|
||||
-- The strings and the three flags are the reference's own (INVITATION = "%s invites you to
|
||||
-- a group.", GlobalStrings:2586-area; a 60 s silent timeout, answerable while dead,
|
||||
-- closable with ESC).
|
||||
--
|
||||
-- The whole of its behaviour turns on ONE latch, `inviteAccepted`, and the four hooks below
|
||||
-- are just the four moments that read or write it. It lives on the registry entry rather
|
||||
-- than in an upvalue here because that is where the reference keeps it and an addon
|
||||
-- inspecting the dialog expects to find it there — which is why each hook goes back through
|
||||
-- `StaticPopupDialogs["PARTY_INVITE"]` instead of closing over a local.
|
||||
--
|
||||
-- They are named rather than inlined so the entry below reads as the manifest it is: four
|
||||
-- strings, three flags, four hooks.
|
||||
|
||||
-- Every showing starts unanswered — including a re-show of the same entry, which is what a
|
||||
-- second invite arriving while the first is up does.
|
||||
local function PartyInvite_OnShow()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = nil
|
||||
end
|
||||
|
||||
-- The only path that answers the server affirmatively, and the only one that latches.
|
||||
local function PartyInvite_OnAccept()
|
||||
AcceptGroup()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = 1
|
||||
end
|
||||
|
||||
local function PartyInvite_OnCancel()
|
||||
DeclineGroup()
|
||||
end
|
||||
|
||||
-- The catch-all. EVERY way this dialog leaves the screen lands here — Decline, the 60 s
|
||||
-- timeout, ESC, and another dialog overriding it — and the latch is what tells the one
|
||||
-- that already said yes from the four that never answered at all. A clicked Decline
|
||||
-- therefore declines twice (OnCancel, then here); see DOUBLE-DECLINE in the header, which
|
||||
-- is the reference's behaviour and the wire tolerates it.
|
||||
local function PartyInvite_OnHide()
|
||||
if not StaticPopupDialogs["PARTY_INVITE"].inviteAccepted then
|
||||
DeclineGroup()
|
||||
end
|
||||
end
|
||||
|
||||
StaticPopupDialogs["PARTY_INVITE"] = {
|
||||
text = INVITATION,
|
||||
button1 = ACCEPT,
|
||||
button2 = DECLINE,
|
||||
sound = "igPlayerInvite",
|
||||
timeout = 60,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
OnShow = PartyInvite_OnShow,
|
||||
OnAccept = PartyInvite_OnAccept,
|
||||
OnCancel = PartyInvite_OnCancel,
|
||||
OnHide = PartyInvite_OnHide,
|
||||
}
|
||||
-- The PARTY_INVITE dialog and its PARTY_INVITE_REQUEST/_CANCEL driver MOVED OUT, to
|
||||
-- UiPanels.xml — the reference's own homes for both (StaticPopup.lua l.526, UIParent.lua).
|
||||
-- They were here only because this is where they were first needed; 1751 walked them back,
|
||||
-- so this file is that much smaller when it follows them onto the chain.
|
||||
|
||||
-- ref PartyMemberFrame.lua's OnLoad: cache children, register, first paint.
|
||||
function BenillaPartyFrame_OnLoad(frame)
|
||||
@@ -280,15 +213,6 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- The invite driver (ref UIParent.lua): PARTY_INVITE_REQUEST shows the popup with the
|
||||
-- inviter's name; PARTY_INVITE_CANCEL hides it.
|
||||
function BenillaPartyInviteDriver_OnEvent()
|
||||
if event == "PARTY_INVITE_REQUEST" then
|
||||
StaticPopup_Show("PARTY_INVITE", arg1)
|
||||
elseif event == "PARTY_INVITE_CANCEL" then
|
||||
StaticPopup_Hide("PARTY_INVITE")
|
||||
end
|
||||
end
|
||||
|
||||
-- The right-click dropdown (decision 0434 phase 5; ref PartyMemberFrame.lua l.369-381)
|
||||
-- + the ref's click law (l.226-237): left targets the member, right opens the PARTY
|
||||
@@ -433,16 +357,4 @@
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PartyMemberFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
|
||||
</Frame>
|
||||
|
||||
<!-- The invite event driver (hidden; the DeathFrame driver pattern). -->
|
||||
<Frame name="BenillaPartyInviteDriver" parent="UIParent" hidden="true">
|
||||
<Size><AbsDimension x="1" y="1"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterEvent("PARTY_INVITE_REQUEST")
|
||||
this:RegisterEvent("PARTY_INVITE_CANCEL")
|
||||
</OnLoad>
|
||||
<OnEvent>BenillaPartyInviteDriver_OnEvent()</OnEvent>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
|
||||
@@ -1488,9 +1488,120 @@
|
||||
|
||||
-- ref GlobalStrings.lua l.2612 / l.2818 / l.465, fallback form (the player's own localized
|
||||
-- file runs off the patch chain first).
|
||||
-- ── The group-invite popup (ref StaticPopup.lua l.526-548) ───────────────────────────
|
||||
-- Moved here from our `PartyFrame.xml` when 1751 took that window to the player's chain.
|
||||
-- This file is our `StaticPopup.lua`, which is where the reference declares it; the party
|
||||
-- frames were only where ours was first needed.
|
||||
-- The strings and the three flags are the reference's own (INVITATION = "%s invites you to
|
||||
-- a group.", GlobalStrings:2586-area; a 60 s silent timeout, answerable while dead,
|
||||
-- closable with ESC).
|
||||
--
|
||||
-- The whole of its behaviour turns on ONE latch, `inviteAccepted`, and the four hooks below
|
||||
-- are just the four moments that read or write it. It lives on the registry entry rather
|
||||
-- than in an upvalue here because that is where the reference keeps it and an addon
|
||||
-- inspecting the dialog expects to find it there — which is why each hook goes back through
|
||||
-- `StaticPopupDialogs["PARTY_INVITE"]` instead of closing over a local.
|
||||
--
|
||||
-- They are named rather than inlined so the entry below reads as the manifest it is: four
|
||||
-- strings, three flags, four hooks.
|
||||
|
||||
-- Every showing starts unanswered — including a re-show of the same entry, which is what a
|
||||
-- second invite arriving while the first is up does.
|
||||
local function PartyInvite_OnShow()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = nil
|
||||
end
|
||||
|
||||
-- The only path that answers the server affirmatively, and the only one that latches.
|
||||
local function PartyInvite_OnAccept()
|
||||
AcceptGroup()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = 1
|
||||
end
|
||||
|
||||
local function PartyInvite_OnCancel()
|
||||
DeclineGroup()
|
||||
end
|
||||
|
||||
-- The catch-all. EVERY way this dialog leaves the screen lands here — Decline, the 60 s
|
||||
-- timeout, ESC, and another dialog overriding it — and the latch is what tells the one
|
||||
-- that already said yes from the four that never answered at all. A clicked Decline
|
||||
-- therefore declines twice (OnCancel, then here); see DOUBLE-DECLINE in the header, which
|
||||
-- is the reference's behaviour and the wire tolerates it.
|
||||
local function PartyInvite_OnHide()
|
||||
if not StaticPopupDialogs["PARTY_INVITE"].inviteAccepted then
|
||||
DeclineGroup()
|
||||
end
|
||||
end
|
||||
|
||||
StaticPopupDialogs["PARTY_INVITE"] = {
|
||||
text = INVITATION,
|
||||
button1 = ACCEPT,
|
||||
button2 = DECLINE,
|
||||
sound = "igPlayerInvite",
|
||||
timeout = 60,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
OnShow = PartyInvite_OnShow,
|
||||
OnAccept = PartyInvite_OnAccept,
|
||||
OnCancel = PartyInvite_OnCancel,
|
||||
OnHide = PartyInvite_OnHide,
|
||||
}
|
||||
|
||||
-- ── The logout countdowns ────────────────────────────────────────────────────────────
|
||||
-- The CAMP and QUIT dialogs, moved here from our `GameMenuFrame.xml` when 1751 took that
|
||||
-- window to the player's chain. This file is our `StaticPopup.lua`, which is where the
|
||||
-- reference declares both (l.564 / l.586) — the ESC menu never owned them, it just
|
||||
-- happened to be where ours were first needed.
|
||||
CAMP_TIMER = CAMP_TIMER or "%d %s until logout"
|
||||
QUIT_TIMER = QUIT_TIMER or "%d %s until exit"
|
||||
QUIT_NOW = QUIT_NOW or "Exit now"
|
||||
-- CANCEL before the tables below, not after: a dialog entry captures its button labels BY
|
||||
-- VALUE at load, so a `button1 = CANCEL` built while CANCEL is still nil silently falls
|
||||
-- back to the popup engine's default ("Yes") rather than failing. It reads as a wrong
|
||||
-- label on a real dialog and as nothing at all in a test that does not check the text.
|
||||
CANCEL = CANCEL or "Cancel"
|
||||
|
||||
-- Dismissing a running countdown means "I didn't mean it": call the logout off. Only while
|
||||
-- it is still RUNNING, though — the popup engine zeroes `timeleft` before hiding a dialog
|
||||
-- that expired, so reaching zero leaves the logout alone. (Backwards, and every logout in
|
||||
-- the field is cancelled by its own dialog at t=0 and the character never leaves.) One
|
||||
-- function because both dialogs mean exactly this; `this` is the popup frame, which the
|
||||
-- engine has bound around the handler.
|
||||
local function CancelLogoutIfStillCounting()
|
||||
if this.timeleft > 0 then
|
||||
CancelLogout()
|
||||
end
|
||||
end
|
||||
|
||||
StaticPopupDialogs["CAMP"] = {
|
||||
text = CAMP_TIMER,
|
||||
button1 = CANCEL,
|
||||
timeout = 20,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
OnAccept = function()
|
||||
CancelLogout()
|
||||
end,
|
||||
OnHide = CancelLogoutIfStillCounting,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["QUIT"] = {
|
||||
text = QUIT_TIMER,
|
||||
button1 = QUIT_NOW,
|
||||
button2 = CANCEL,
|
||||
timeout = 20,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
-- A force-quit needs no server round trip, so the countdown it was narrating is over
|
||||
-- the moment the button is pressed: zero `timeleft` so the OnHide below stands down.
|
||||
OnAccept = function()
|
||||
ForceQuit()
|
||||
this.timeleft = 0
|
||||
end,
|
||||
OnHide = CancelLogoutIfStillCounting,
|
||||
}
|
||||
|
||||
LOOT_NO_DROP = LOOT_NO_DROP or "Looting this item will bind it to you."
|
||||
OKAY = OKAY or "Okay"
|
||||
CANCEL = CANCEL or "Cancel"
|
||||
|
||||
-- ref StaticPopup.lua l.601-611, verbatim. `OnAccept`'s argument is `dialog.data`, the
|
||||
-- event's row — and `LootSlot` is the CONTINUATION verb, so this line is the only thing in
|
||||
@@ -1769,6 +1880,25 @@
|
||||
if dialog then
|
||||
dialog.data = arg1
|
||||
end
|
||||
elseif event == "PARTY_INVITE_REQUEST" then
|
||||
-- ref UIParent.lua's own arms. Same move as the logout three and the loot bind
|
||||
-- confirm: the popup-raising event belongs beside the popup, not in whichever
|
||||
-- window happened to need it first.
|
||||
StaticPopup_Show("PARTY_INVITE", arg1)
|
||||
elseif event == "PARTY_INVITE_CANCEL" then
|
||||
StaticPopup_Hide("PARTY_INVITE")
|
||||
elseif event == "PLAYER_CAMPING" then
|
||||
-- ref UIParent.lua l.304-315. These three lived on our GameMenuFrame.xml's own
|
||||
-- hidden listener, on the reasoning that benilla has no UIParent frame; they are
|
||||
-- here now for the same reason the loot bind confirm is — 1751 took that window
|
||||
-- to the chain, and the reference's placement (UIParent, beside every other
|
||||
-- popup-raising event) is the one that survives the window being someone else's.
|
||||
StaticPopup_Show("CAMP")
|
||||
elseif event == "PLAYER_QUITING" then
|
||||
StaticPopup_Show("QUIT")
|
||||
elseif event == "LOGOUT_CANCEL" then
|
||||
StaticPopup_Hide("CAMP")
|
||||
StaticPopup_Hide("QUIT")
|
||||
elseif event == "LOOT_BIND_CONFIRM" then
|
||||
-- ref UIParent.lua l.317-323. The row rides on `dialog.data` rather than as a
|
||||
-- text arg — LOOT_NO_DROP has no format specifier, so the sentence is the same
|
||||
@@ -2601,6 +2731,11 @@
|
||||
self:RegisterEvent("AUTOEQUIP_BIND_CONFIRM")
|
||||
self:RegisterEvent("USE_BIND_CONFIRM")
|
||||
self:RegisterEvent("LOOT_BIND_CONFIRM")
|
||||
self:RegisterEvent("PLAYER_CAMPING")
|
||||
self:RegisterEvent("PLAYER_QUITING")
|
||||
self:RegisterEvent("LOGOUT_CANCEL")
|
||||
self:RegisterEvent("PARTY_INVITE_REQUEST")
|
||||
self:RegisterEvent("PARTY_INVITE_CANCEL")
|
||||
self:RegisterEvent("CURSOR_UPDATE")
|
||||
</OnLoad>
|
||||
<OnEvent>BenillaBindConfirmDriver_OnEvent(self)</OnEvent>
|
||||
|
||||
@@ -257,8 +257,31 @@ Interface\FrameXML\ComboFrame.xml
|
||||
# transcription; consumes COMBAT_TEXT_UPDATE + the regen/health/power events, needs
|
||||
# only Fonts.xml (CombatTextFont) + UIParent before it.
|
||||
CombatText.xml
|
||||
# The party member frames + the PARTY_INVITE popup + its event driver (decision 0434
|
||||
# phase 2): a StaticPopup registry entry, so after UiPanels.xml.
|
||||
# The player buff/debuff bar (decisions 0255/0257 — the player aura arc): the HUD row of aura
|
||||
# icons under the minimap, driven by the UnitAura feed (`crate::ui_aura`).
|
||||
#
|
||||
# MOVED UP HERE from below the world-state frame when PartyFrame went to the chain (1751). It
|
||||
# has to precede PartyFrame, because stock `PartyMemberFrame.lua:60` calls `RefreshBuffs` from
|
||||
# each row's OnLoad and `BuffFrame.lua` is where that lives — which is exactly the reference's
|
||||
# own order (its toc: BuffFrame 40, UnitFrame 43, PartyFrame 45; ours had them inverted).
|
||||
#
|
||||
# The note this entry used to carry — "needs ActionBar's `BENILLA_FALLBACK_ICON`" — was stale:
|
||||
# that was true of OUR BuffFrame.xml, and the chain file does not mention the name. The only
|
||||
# readers left are MerchantFrame, StanceBar and TradeSkillFrame, all at runtime.
|
||||
Interface\FrameXML\BuffFrame.xml
|
||||
# The party member frames (decision 0434 phase 2).
|
||||
#
|
||||
# NOT yet on the chain, and the blocker is ours, not a missing verb: **`UnitFrames.xml` below
|
||||
# redefines `UnitFrame_OnEvent` and `UnitFrame_Update`**, two names stock `UnitFrame.lua` (l.232
|
||||
# above) has already defined. Ours win, so stock party rows — built by stock
|
||||
# `PartyMemberFrame.lua` and initialized by the reference's own `UnitFrame_Initialize` — end up
|
||||
# calling OUR update, which indexes a `nameText` the reference's rows do not carry. The swap
|
||||
# loads and then raises on the first PARTY_MEMBERS_CHANGED. So PartyFrame waits on the
|
||||
# player/target/pet frames, not on anything of its own.
|
||||
#
|
||||
# What HAS moved out: the PARTY_INVITE dialog and its driver, to UiPanels.xml (StaticPopup.lua
|
||||
# l.526, UIParent.lua), and the three raid constants, which RaidFrame.xml already declared —
|
||||
# that is the reference's home for them (RaidFrame.lua l.2-4).
|
||||
PartyFrame.xml
|
||||
# The death arc's dialogs + event driver (decision 0308): registry entries on the shared
|
||||
# StaticPopup engine, so after UiPanels.xml.
|
||||
@@ -455,10 +478,6 @@ Interface\FrameXML\MirrorTimer.xml
|
||||
# `crate::world_state_ui` fires. Needs Fonts (GameFontNormalSmall) and GameTooltip; sits beside the
|
||||
# mirror timers, which share its top-centre lane.
|
||||
WorldStateFrame.xml
|
||||
# The player buff/debuff bar (decisions 0255/0257 — the player aura arc): the HUD row of
|
||||
# aura icons under the minimap, driven by the UnitAura feed (`crate::ui_aura`). Needs Fonts
|
||||
# (its FontStrings) and ActionBar's `BENILLA_FALLBACK_ICON` global (both already loaded).
|
||||
Interface\FrameXML\BuffFrame.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
|
||||
@@ -649,18 +668,21 @@ 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.
|
||||
RaidFrame.xml
|
||||
GossipFrame.xml
|
||||
# NOT YET on the chain, and the reason is worth keeping. `PickupMerchantItem` (1786) was the
|
||||
# last name the *gap report* wanted, the readiness probe then called the stock file CLEAN, and
|
||||
# both were right about what they measure and wrong about the window: the stock row's `<OnEnter>`
|
||||
# calls `ShoppingTooltip1:SetMerchantCompareItem(...)`, which we do not have. That is a METHOD,
|
||||
# and the gap report only reads bare `Name(` globals — see 1787, which fixes the instrument.
|
||||
# A hover would have raised in play with every check green. The swap waits on that one method.
|
||||
# ══ 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
|
||||
# label, our measurement being a frame late; that is what the swap tests.
|
||||
Interface\FrameXML\GossipFrame.xml
|
||||
# ══ THE VENDOR WINDOW — the reference's own, off the player's chain (1751) ══════════════════
|
||||
# It took three things, in this order: `PickupMerchantItem` (the vendor cursor, 1786), the
|
||||
# `BenillaMoney_*` coin kit moving out to MoneyFrame.xml where eight other windows can reach it
|
||||
# without loading this one, and `SetMerchantCompareItem` (1791) — which the gap report could not
|
||||
# see at all until 1787 taught it to read `:Method(` sites, and which would have raised on the
|
||||
# first vendor-row hover with every check green.
|
||||
#
|
||||
# Its shared `BenillaMoney_*` coin kit HAS moved out, to MoneyFrame.xml where the reference keeps
|
||||
# money code and where eight windows can reach it without loading this one. That is why the notes
|
||||
# below now say "grouping" rather than "reuses that file's helpers".
|
||||
MerchantFrame.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
|
||||
@@ -862,9 +884,18 @@ KeyBindingsPage.xml
|
||||
# (all far above); sits right before GameMenuFrame so the menu — which reaches it by
|
||||
# name at click time — stays the last, outermost layer.
|
||||
OptionsFrame.xml
|
||||
# The game menu (decision 0674): the frame ESC opens, plus the CAMP/QUIT dialogs and their
|
||||
# event driver. LAST on purpose — it is the outermost layer of the shell, it depends on
|
||||
# everything it touches being loaded (UiPanels' popup engine + panel manager, MicroMenu's
|
||||
# UpdateMicroButtons, BagFrame's Disable_BagButtons), and nothing depends on it: the ESC
|
||||
# chain and the micro button both reach it by name at call time.
|
||||
# The game menu (decision 0674): the frame ESC opens. LAST on purpose — it is the outermost
|
||||
# layer of the shell, it depends on everything it touches being loaded (UiPanels' popup engine
|
||||
# + panel manager, MicroMenu's UpdateMicroButtons, BagFrame's Disable_BagButtons), and nothing
|
||||
# depends on it: the ESC chain and the micro button both reach it by name at call time.
|
||||
#
|
||||
# NOT yet on the chain, and the reason is not in the gap report: the stock file's buttons reach
|
||||
# `UIOptionsFrame`, and stock `UIParent.lua:997` indexes it — so the ESC menu drags in the whole
|
||||
# options family, the same chain `MainMenuBarMicroButtons` is deferred behind. All three columns
|
||||
# read clean because the file LOADS clean; what it needs is a sibling window, not a verb.
|
||||
#
|
||||
# Its CAMP/QUIT dialogs and their event driver HAVE moved out, to UiPanels.xml — the reference's
|
||||
# own homes (StaticPopup.lua l.564/586 for the dialogs, UIParent.lua l.304-315 for the three
|
||||
# events). The ESC menu never owned them; it was just where ours were first needed. That is one
|
||||
# less thing to move when the options family lands.
|
||||
GameMenuFrame.xml
|
||||
|
||||
@@ -785,7 +785,7 @@ fn shipped_bag_frame_drives_end_to_end() {
|
||||
}
|
||||
}
|
||||
load_ui(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
||||
load_ui(&s, "MerchantFrame.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
assert_eq!(
|
||||
bar_frames, 16,
|
||||
|
||||
@@ -133,7 +133,7 @@ fn b_opens_the_backpack_alone_and_closes_every_bag() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
// Backpack (16) + one equipped bag in slot 2 (6). Bags 1/3/4 are left unset → 0 slots.
|
||||
@@ -214,7 +214,7 @@ fn shift_b_toggles_every_bag_at_once() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(
|
||||
0,
|
||||
@@ -295,7 +295,7 @@ fn bag_bar_buttons_light_while_their_bag_is_open() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(
|
||||
0,
|
||||
@@ -408,7 +408,7 @@ fn bag_tooltip_hangs_left_when_the_slot_sits_in_the_right_half() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -484,7 +484,7 @@ fn hovered_bag_tooltip_fills_itself_when_the_stats_land() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -587,7 +587,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -721,7 +721,7 @@ fn readable_letter_hover_shows_the_inspect_magnifier() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -791,7 +791,7 @@ fn drag_across_two_slots_queues_the_same_move_a_click_pickup_would() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -874,7 +874,7 @@ fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -946,7 +946,7 @@ fn equipped_bag_window_snug_fits_its_row_count() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
// (bag id, slot count, expected window height). 6 → 2 rows plus-two-top (72+32+10); 8 → 2 rows
|
||||
@@ -1005,7 +1005,7 @@ fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
|
||||
for file in BAG_UI {
|
||||
load_xml(s, file);
|
||||
}
|
||||
load_xml(s, "MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
@@ -1345,7 +1345,7 @@ fn bag_slot_cooldown_sweeps_through_the_xml() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.tick(100.0); // a nonzero clock epoch, like the engine cooldown tests
|
||||
|
||||
@@ -1418,7 +1418,7 @@ fn the_bar_bag_buttons_name_themselves_on_hover() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.resolve();
|
||||
|
||||
// The backpack: its label plus OPENALLBAGS's live key (default B; 0997's divergence note).
|
||||
@@ -1551,7 +1551,7 @@ fn keyring_surface(s: &UiScript) {
|
||||
for file in BAG_UI {
|
||||
load_xml(s, file);
|
||||
}
|
||||
load_xml(s, "MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\UIErrorsFrame.xml");
|
||||
}
|
||||
|
||||
@@ -1823,7 +1823,7 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.resolve();
|
||||
|
||||
let shown = |s: &UiScript, name: &str| {
|
||||
@@ -2079,7 +2079,7 @@ fn an_addon_that_hooks_toggle_backpack_receives_the_click() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
// Bagnon's exact idiom: capture the original, replace the global.
|
||||
@@ -2135,7 +2135,7 @@ fn the_bag_slots_carry_the_references_names_and_icon_names() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
for i in 0..4 {
|
||||
assert!(
|
||||
@@ -2195,7 +2195,7 @@ fn the_backpack_buttons_ring_follows_its_own_window_through_real_clicks() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
// The reference's `ToggleBag` opens nothing for a container of size 0, so the backpack has to
|
||||
// be fed — our own windows existed statically and opened regardless.
|
||||
|
||||
@@ -73,7 +73,7 @@ fn bag_setup() -> UiScript {
|
||||
}
|
||||
// `ContainerFrame_Update` reads `MerchantFrame:IsShown()` on any slot the tooltip owns, and
|
||||
// `BankFrame` (BAG_UI's last entry) fills its purse through `BenillaMoney_Set`.
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const ROOM_UI: &[&str] = &[
|
||||
"UIDropDownMenu.xml",
|
||||
"UnitPopup.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\StackSplitFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
|
||||
@@ -113,10 +113,10 @@ fn an_error_toast_draws_over_an_open_panel_window() {
|
||||
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
|
||||
// ScrollTemplates.xml (the faux kit the list rides) + UIPanelTemplates.xml (the detail
|
||||
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
|
||||
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
|
||||
// ScrollTemplates.xml (the faux kit the list rides) + UIPanelTemplates.xml (the detail
|
||||
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
|
||||
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "QuestLogFrame.xml");
|
||||
|
||||
@@ -73,7 +73,7 @@ fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_Set, BankFrame's purse helper
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_Set, BankFrame's purse helper
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
|
||||
@@ -145,7 +145,7 @@ fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
s.set_money(0);
|
||||
@@ -233,7 +233,7 @@ fn escape_closes_an_open_stack_split_frame() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
|
||||
load_xml(&s, "ChatFrame.xml"); // …and ChatFrameEditBox, the shift fork's first test
|
||||
load_xml(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
||||
s.set_money(0);
|
||||
@@ -312,7 +312,7 @@ fn escape_ladder_cast_then_windows_then_target_one_eater_per_press() {
|
||||
// GameTooltip.xml (BAG_UI's, for the bag slots' tooltips) also carries TOOLTIP_DEFAULT_COLOR,
|
||||
// which the dropdown backdrop's OnLoad reads — so the kit can load straight after it.
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
||||
@@ -406,7 +406,7 @@ fn escape_ladder_targeting_rung_after_cast_before_windows() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
||||
|
||||
@@ -219,74 +219,11 @@ const KNOWN: &[Known] = &[
|
||||
},
|
||||
// ── We take the mouse where the reference does not ─────────────────────────────────────────
|
||||
//
|
||||
// The merchant rows: ours are one mouse-taking row each; the reference splits every row into
|
||||
// an inert container plus a `$parentItemButton` that takes the mouse on its behalf. Same
|
||||
// clicks, a different frame under them.
|
||||
Known {
|
||||
frame: "MerchantItem1",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem2",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem3",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem4",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem5",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem6",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem7",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem8",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem9",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem10",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem11",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem12",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantBuyBackItem",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
// The merchant rows' divergence RETIRED (1751): our `MerchantFrame.xml` is gone and the
|
||||
// reference's own file is on the player's chain, so its rows are its rows. There were 13
|
||||
// entries here — twelve `MerchantItem<N>` plus `MerchantBuyBackItem` — saying ours took the
|
||||
// mouse on the row itself where the reference splits each row into an inert container plus a
|
||||
// `$parentItemButton`. That is exactly what this gate exists to notice going away.
|
||||
Known {
|
||||
frame: "WorldMapFrame",
|
||||
flag: Flag::Mouse,
|
||||
|
||||
@@ -199,7 +199,7 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
|
||||
#[test]
|
||||
fn escape_opens_the_menu_only_when_nothing_else_wants_the_press_and_then_closes_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = bag_harness_with(&[], &["MerchantFrame.xml"]);
|
||||
let mut s = bag_harness_with(&[], &["Interface\\FrameXML\\MerchantFrame.xml"]);
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(backpack()));
|
||||
|
||||
@@ -236,7 +236,7 @@ fn escape_opens_the_menu_only_when_nothing_else_wants_the_press_and_then_closes_
|
||||
#[test]
|
||||
fn the_clicked_form_closes_everything_and_opens_the_menu_in_one_go() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = bag_harness_with(&[], &["MerchantFrame.xml"]);
|
||||
let mut s = bag_harness_with(&[], &["Interface\\FrameXML\\MerchantFrame.xml"]);
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
||||
@@ -263,7 +263,7 @@ fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() {
|
||||
let mut s = bag_harness_with(
|
||||
&[],
|
||||
&[
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
// The loot window is the reference's own since 1751 — `test_ui::LOOT_UI` carries
|
||||
// what it needs and why, and PartyFrame's `MAX_PARTY_MEMBERS` is needed at LOAD.
|
||||
"UIDropDownMenu.xml",
|
||||
@@ -594,7 +594,7 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
|
||||
"UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"WorldMapFrame.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
// The loot window is the reference's own since 1751 — see `test_ui::LOOT_UI` for what
|
||||
// each of these buys; `PartyFrame`'s MAX_PARTY_MEMBERS is needed at LOAD time.
|
||||
"UnitPopup.xml",
|
||||
@@ -638,7 +638,10 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
|
||||
// ActionBar.xml goes AHEAD of the bag stack: it declares MainMenuBarArtFrame, which the bag
|
||||
// bar anchors into and seats itself above (`BenillaActionBarArt_SeatAbove`, nil-guarded in
|
||||
// BagFrame.xml precisely because most harnesses load no bar).
|
||||
let mut s = bag_harness_with(&["Cooldown.xml", "ActionBar.xml"], &["MerchantFrame.xml"]);
|
||||
let mut s = bag_harness_with(
|
||||
&["Cooldown.xml", "ActionBar.xml"],
|
||||
&["Interface\\FrameXML\\MerchantFrame.xml"],
|
||||
);
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.resolve();
|
||||
|
||||
@@ -479,7 +479,7 @@ fn shipped_loot_pushed_to_center_by_merchant() {
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Loot opens onto the empty left slot.
|
||||
s.set_loot(Some(coin_and_two_items()));
|
||||
|
||||
@@ -52,18 +52,19 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The named virtual Font objects the re-skinned rows/title inherit through — loaded first at
|
||||
// runtime (ui_script's shipped list) so `inherits="GameFontNormalSmall"` resolves here too.
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
// The window's frame census: window 1 + item rows 12 (10 merchant + the 2 buyback-only) +
|
||||
// close 1 + row-price coin slots 36 + purse coin slots 3 + the buyback slot 1 + its 3 coins +
|
||||
// the repair pair 2 + the page pair 2 + the tab pair 2 = 63 (the title/quadrant art are
|
||||
// FontString/Texture layers, not frames).
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
// The STOCK file's frame census (1751). Ours materialized 63 — window, twelve rows, close, the
|
||||
// three-slot coin kit's 39 slots, the buyback slot, and the repair/page/tab pairs. The reference's
|
||||
// shape is different in kind, not just in count: every row is an inert container PLUS a
|
||||
// `$parentItemButton` that carries `ItemButtonTemplate`'s own children, and the prices are
|
||||
// `SmallMoneyFrameTemplate` instances rather than our `<prefix>Coin1..3` chain. A count is a
|
||||
// fingerprint of a file; this one is the reference's.
|
||||
assert_eq!(
|
||||
load_xml(&s, "MerchantFrame.xml"),
|
||||
63,
|
||||
"window + 12 rows + close + 39 coin slots + buyback slot + repair/page/tab pairs"
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"),
|
||||
90,
|
||||
"the stock file's own shape — see the census note above"
|
||||
);
|
||||
|
||||
// Hidden by default: no vendor icon on screen.
|
||||
@@ -155,18 +156,22 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
11,
|
||||
"10 merchant rows + the buyback slot render their socket plate"
|
||||
);
|
||||
// THREE, not two, and the third is the buyback slot. Our file dimmed an empty buyback socket
|
||||
// with the empty rows; the reference's `MerchantFrame_UpdateBuybackInfo` does not run the
|
||||
// empty-row arm on it at all, so it keeps the bright plate whether or not a sold item is
|
||||
// there. Same census, one socket moved sides.
|
||||
assert_eq!(
|
||||
socket_colors.iter().filter(|c| c[0] > 0.9).count(),
|
||||
2,
|
||||
"the 2 filled rows keep the socket full-bright"
|
||||
3,
|
||||
"the 2 filled rows and the buyback slot keep the socket full-bright"
|
||||
);
|
||||
assert_eq!(
|
||||
socket_colors
|
||||
.iter()
|
||||
.filter(|c| (c[0] - 0.4).abs() < 0.01)
|
||||
.count(),
|
||||
9,
|
||||
"the 8 empty rows + the empty buyback slot dim the socket to 0.4"
|
||||
8,
|
||||
"the 8 empty rows dim the socket to 0.4 (ref MerchantFrame.lua l.115)"
|
||||
);
|
||||
assert_eq!(
|
||||
quads
|
||||
@@ -335,11 +340,10 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
fn merchant_show_hide_plays_open_and_close_kits() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Hidden at load: no open sound (never transitions on startup).
|
||||
assert!(
|
||||
@@ -388,7 +392,7 @@ fn vendor_open_opens_the_backpack_and_layers_the_sound() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
equip_bag(&mut s, 0, "Backpack", 16);
|
||||
let _ = s.take_sounds(); // ignore anything from load (frames are hidden; nothing should)
|
||||
@@ -438,7 +442,7 @@ fn vendor_leaves_an_already_open_backpack_alone() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
equip_bag(&mut s, 0, "Backpack", 16);
|
||||
|
||||
@@ -486,7 +490,7 @@ fn vendor_opens_and_closes_all_equipped_bags() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
equip_bag(&mut s, 0, "Backpack", 16);
|
||||
equip_bag(&mut s, 2, "Small Pouch", 6);
|
||||
@@ -519,11 +523,10 @@ fn vendor_opens_and_closes_all_equipped_bags() {
|
||||
fn merchant_switch_plays_close_then_open_and_queues_the_consumable_close() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Vendor A open.
|
||||
s.set_money(0);
|
||||
@@ -562,10 +565,9 @@ 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);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
// The quality→colour table is `ITEM_QUALITY_COLORS`, and Fonts.xml is its home — this used
|
||||
// to load the whole loot window for `BENILLA_LOOT_QUALITY_COLORS`, an alias our own
|
||||
// LootFrame.xml set to that same table. 1751 took that window to the chain and the alias went
|
||||
@@ -573,7 +575,7 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
// every FrameXML file loads before any hover fires, so load it here too — with the dropdown
|
||||
// kit its GroupLootDropDown initializes against at load (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
s.set_merchant(Some(MerchantState {
|
||||
items: vec![
|
||||
@@ -659,10 +661,17 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
assert!(!has_text(&quads, "Main Hand"), "no tooltip before hover");
|
||||
let sword_icon = rect_of_tex(&quads, "INV_Sword_04");
|
||||
|
||||
// Hover the row over its NAME PLATE (well right of the icon): the whole row is the button, but
|
||||
// the highlight must still cover only the 37px icon at the row's TOPLEFT.
|
||||
// Hover the ICON, and that is the whole change 1751 made here. Our `MerchantFrame.xml` made
|
||||
// the WHOLE ROW take the mouse — this hovered 60px right of the icon, over the name plate,
|
||||
// and expected the tooltip. The reference splits every row into an inert container plus a
|
||||
// `$parentItemButton` the size of the icon, which takes the mouse on its behalf; the twelve
|
||||
// `MerchantItem<N>` entries in the flag gate's KNOWN list said exactly that, and they retired
|
||||
// with our file.
|
||||
//
|
||||
// So on the reference's window, hovering a vendor row's NAME shows nothing. That is not a
|
||||
// regression to fix — it is what the client does.
|
||||
let (hx, hy) = (
|
||||
sword_icon.right + 60.0,
|
||||
(sword_icon.left + sword_icon.right) * 0.5,
|
||||
(sword_icon.bottom + sword_icon.top) * 0.5,
|
||||
);
|
||||
s.mouse_move(hx, hy);
|
||||
@@ -777,148 +786,21 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Pin the real SmallMoneyFrame *shrink* (MoneyFrame.lua l.202 `SetWidth(GetTextWidth() +
|
||||
/// iconWidth)`, l.269 `frame:SetWidth`) that `BenillaMoney_Set` reproduces from the app's
|
||||
/// digit-advance feed ([`benilla_ui::script::UiScript::set_digit_advances`]): price groups pack
|
||||
/// number+coin with the real 4px spacing and start left-flush at the plate's left edge whatever
|
||||
/// the leading denomination (the "silver rows indent" bug), the purse packs the same
|
||||
/// right-to-left off its RIGHT anchor, and collapsed denominations HIDE (never a blank
|
||||
/// fixed-width box — the "copper-to-silver gap" bug).
|
||||
#[test]
|
||||
fn money_display_shrinks_to_content_and_stays_flush() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The app's synchronous number metrics (ui_script feeds NumberFontNormal's real advances once
|
||||
// per atlas bake; a uniform 6px here keeps the arithmetic below legible).
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
|
||||
s.set_money(12_345); // 1g 23s 45c → purse digits "1" / "23" / "45"
|
||||
s.set_merchant(Some(MerchantState {
|
||||
items: vec![
|
||||
MerchantItem {
|
||||
name: Some("Refreshing Spring Water".into()),
|
||||
texture: Some("Interface\\Icons\\INV_Drink_18".into()),
|
||||
price: 25, // copper only → "25"
|
||||
quantity: 1,
|
||||
num_available: -1,
|
||||
item_id: 159,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Linen Bandage".into()),
|
||||
texture: Some("Interface\\Icons\\INV_Misc_Bandage_01".into()),
|
||||
price: 204, // 2s 4c → "2" + "4" (silver-led — the row that used to indent)
|
||||
quantity: 1,
|
||||
num_available: -1,
|
||||
item_id: 1251,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}));
|
||||
s.fire_event("MERCHANT_SHOW", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
|
||||
let text_rect = |t: &str| {
|
||||
quads
|
||||
.iter()
|
||||
.find(|q| matches!(&q.content, QuadContent::Text { text: Some(x), .. } if x == t))
|
||||
.and_then(|q| q.rect)
|
||||
.unwrap_or_else(|| panic!("no text quad for {t:?}"))
|
||||
};
|
||||
let near = |a: f32, b: f32| (a - b).abs() < 0.01;
|
||||
|
||||
// The real collapse HIDES unused slots: exactly 6 coin icons are on screen — row 1's copper,
|
||||
// row 2's silver+copper, the purse's gold+silver+copper. (8 empty rows × 3 + all the unused
|
||||
// slots of the filled displays draw nothing.)
|
||||
let coin_icons = quads
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("UI-MoneyIcons"))
|
||||
})
|
||||
.count();
|
||||
assert_eq!(coin_icons, 6, "only the filled denominations draw a coin");
|
||||
|
||||
// Left-flush prices: both rows' leading numbers start at rowLeft + 46 (the name box's own
|
||||
// column — 2px of breathing room off the plate border the director asked for; the ref quote
|
||||
// was 44), whether the row leads with copper ("25") or silver ("2"). Row 1 sits at abs left
|
||||
// 24 (window 0 + 24), row 2 at 189 (24 + 153-wide row + 12 column gap).
|
||||
let p1 = text_rect("25");
|
||||
let p2 = text_rect("2");
|
||||
assert!(
|
||||
near(p1.left - 24.0, 46.0),
|
||||
"copper-led price starts at the plate edge, got {}",
|
||||
p1.left - 24.0
|
||||
);
|
||||
assert!(
|
||||
near(p2.left - 189.0, 46.0),
|
||||
"silver-led price starts at the SAME plate edge (the indent bug), got {}",
|
||||
p2.left - 189.0
|
||||
);
|
||||
|
||||
// The shrink + the real 4px MONEY_BUTTON_SPACING: row 2's copper number starts exactly
|
||||
// number-width + 13px coin + 4px gap after the silver number's left edge — no dead air from a
|
||||
// fixed-width slot (the gap bug). "2" is 6px wide, its coin 13, the gap 4 → "4" at +23.
|
||||
let p2c = text_rect("4");
|
||||
assert!(
|
||||
near(p2c.left, p2.right + 13.0 + 4.0),
|
||||
"silver→copper packs number+coin+4px, got {} vs {}",
|
||||
p2c.left,
|
||||
p2.right + 13.0 + 4.0
|
||||
);
|
||||
|
||||
// The purse packs the same but right-to-left, right-flush at window right -53 (the ref
|
||||
// MerchantMoneyFrame anchor -40 minus the template's built-in 13px right pad — see the purse
|
||||
// XML comment): copper "45" ends 13px (its coin) short of x=331, silver "23" ends its coin
|
||||
// 4px left of copper's slot, gold "1" likewise.
|
||||
let purse_c = text_rect("45");
|
||||
let purse_s = text_rect("23");
|
||||
let purse_g = text_rect("1");
|
||||
assert!(
|
||||
near(purse_c.right + 13.0, 384.0 - 53.0),
|
||||
"purse copper coin right-flush at window right -53, got {}",
|
||||
purse_c.right + 13.0
|
||||
);
|
||||
assert!(
|
||||
near(purse_c.left, purse_s.right + 13.0 + 4.0),
|
||||
"purse silver→copper packs rtl with the 4px gap, got {} vs {}",
|
||||
purse_c.left,
|
||||
purse_s.right + 13.0 + 4.0
|
||||
);
|
||||
assert!(
|
||||
near(purse_s.left, purse_g.right + 13.0 + 4.0),
|
||||
"purse gold→silver packs rtl with the 4px gap, got {} vs {}",
|
||||
purse_s.left,
|
||||
purse_g.right + 13.0 + 4.0
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// A sell changes only the coinage — no MERCHANT_UPDATE rides along. The purse repaints on the
|
||||
// real client's PLAYER_MONEY event (the app fires it whenever the pushed money changes).
|
||||
s.set_money(12_349); // +4c: the purse copper becomes "49"
|
||||
s.fire_event("PLAYER_MONEY", vec![]);
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
assert!(
|
||||
quads
|
||||
.iter()
|
||||
.any(|q| matches!(&q.content, QuadContent::Text { text: Some(t), .. } if t == "49")),
|
||||
"the merchant purse live-updates on PLAYER_MONEY"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
// ── `money_display_shrinks_to_content_and_stays_flush` RETIRED (1751) ────────────────────────
|
||||
//
|
||||
// It pinned `BenillaMoney_Set`'s geometry inside OUR `MerchantFrame.xml`: price groups left-flush
|
||||
// at `rowLeft + 46`, the purse right-flush at window-right −53, denominations packed with the real
|
||||
// 4px `MONEY_BUTTON_SPACING`. The 46 was a director call — 2px of breathing room off the plate
|
||||
// border where the reference quotes 44 — and it was a fact about a file we no longer ship.
|
||||
//
|
||||
// The reference's vendor rows use `SmallMoneyFrameTemplate` and `MoneyFrame_Update`, a different
|
||||
// kit with its own geometry. **The rules the test protected are not lost**: the shrink, the
|
||||
// collapse and the per-type `showSmallerCoins`/`fixedWidth` behaviour are `money_frame_tests.rs`'s
|
||||
// subject, against the kit that now draws these rows. What is gone is the merchant-specific pixel
|
||||
// geometry, which had nothing left to describe.
|
||||
//
|
||||
// **The vendor window's coin columns therefore look different now** — the reference's spacing, not
|
||||
// our approved 2px indent. Said here rather than left inside a changed constant.
|
||||
|
||||
/// The merchant/buyback tab pair drives the two pages (ref MerchantFrame_Update l.57-64 +
|
||||
/// UpdateMerchantInfo/UpdateBuybackInfo): the merchant page shows the single most-recent buyback
|
||||
@@ -929,11 +811,10 @@ fn money_display_shrinks_to_content_and_stays_flush() {
|
||||
fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(500);
|
||||
|
||||
let buyback_item = |name: &str, price: u32| MerchantItem {
|
||||
@@ -990,17 +871,19 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
.unwrap());
|
||||
|
||||
// The slot click buys back the most recent sale (slot 2 of 2).
|
||||
s.run("BenillaMerchantBuyBackItem_OnClick()").unwrap();
|
||||
// The stock row is a container plus a `$parentItemButton` that takes the mouse on its
|
||||
// behalf — the split the retired flag-gate entries used to record as our divergence.
|
||||
s.run("MerchantBuyBackItemItemButton:Click()").unwrap();
|
||||
assert_eq!(s.take_merchant_buybacks(), vec![2]);
|
||||
|
||||
// Repair-all queues its intent.
|
||||
s.run("BenillaMerchantRepairAllButton_OnClick()").unwrap();
|
||||
s.run("MerchantRepairAllButton:Click()").unwrap();
|
||||
assert!(s.take_repair_all());
|
||||
s.take_sounds();
|
||||
|
||||
// Tab 2: the buyback page — retitled, rows off GetBuybackItemInfo, merchant-only pieces down,
|
||||
// buyback art up, tab 2 selected (its Active slices showing).
|
||||
s.run("BenillaMerchantFrameTab_OnClick(2)").unwrap();
|
||||
s.run("MerchantFrameTab2:Click()").unwrap();
|
||||
assert!(s.errors().is_empty(), "tab errors: {:?}", s.errors());
|
||||
assert!(s
|
||||
.eval::<bool>("return MerchantNameText:GetText() == 'Merchant Buyback'")
|
||||
@@ -1026,12 +909,12 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
.unwrap());
|
||||
|
||||
// A row click on the buyback page buys that slot back.
|
||||
s.run("BenillaMerchantItem_OnClick(MerchantItem1, 'LeftButton')")
|
||||
s.run("MerchantItem1ItemButton:Click(\"LeftButton\")")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_merchant_buybacks(), vec![1]);
|
||||
|
||||
// Back to tab 1: the merchant row returns, the buyback art drops.
|
||||
s.run("BenillaMerchantFrameTab_OnClick(1)").unwrap();
|
||||
s.run("MerchantFrameTab1:Click()").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return MerchantItem1Name:GetText() == 'Refreshing Spring Water' \
|
||||
@@ -1049,11 +932,10 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
fn merchant_tabs_fit_their_labels() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_merchant(Some(MerchantState::default()));
|
||||
s.fire_event("MERCHANT_SHOW", vec![ScriptValue::Str("Vendor".into())]);
|
||||
@@ -1091,7 +973,7 @@ fn merchant_tabs_fit_their_labels() {
|
||||
|
||||
/// Hovering a buy-tab row arms the vendor **Buy** cursor (the grayed **UnableBuy** when you can't
|
||||
/// afford the row), swaps to **Inspect** while Ctrl is held, and clears on leave — the whole reason
|
||||
/// this window exists to a first-time visitor. Drives the real `BenillaMerchantItem_OnEnter` +
|
||||
/// this window exists to a first-time visitor. Drives the stock row button's own `<OnEnter>` +
|
||||
/// the frame's `OnUpdate` (a `tick`) exactly as the app does, since the coin is re-armed per frame.
|
||||
#[test]
|
||||
fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
@@ -1099,11 +981,10 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// A purse of 50c: row 1 (25c) is affordable, row 2 (100c) is not.
|
||||
s.set_money(50);
|
||||
@@ -1139,7 +1020,10 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
assert_eq!(s.ui_cursor(), None, "no override before any hover");
|
||||
|
||||
// Row 1 hover: OnEnter remembers the row, the frame's OnUpdate arms the coin — affordable → Buy.
|
||||
s.run("BenillaMerchantItem_OnEnter(MerchantItem1)").unwrap();
|
||||
// The stock `<OnEnter>` is inline on the row button, so it is fired the way the engine
|
||||
// fires it: `this` bound, then the compiled handler.
|
||||
s.run("this = MerchantItem1ItemButton MerchantItem1ItemButton:GetScript(\"OnEnter\")() this = nil")
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
@@ -1159,12 +1043,14 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
s.set_modifiers(false, false, false);
|
||||
|
||||
// Leaving clears the override (OnLeave ResetCursor + the itemHover poll stops).
|
||||
s.run("BenillaMerchantItem_OnLeave(MerchantItem1)").unwrap();
|
||||
s.run("this = MerchantItem1ItemButton MerchantItem1ItemButton:GetScript(\"OnLeave\")() this = nil")
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(s.ui_cursor(), None, "leaving the row resets the cursor");
|
||||
|
||||
// Row 2 hover: 100c against a 50c purse → the grayed UnableBuy.
|
||||
s.run("BenillaMerchantItem_OnEnter(MerchantItem2)").unwrap();
|
||||
s.run("this = MerchantItem2ItemButton MerchantItem2ItemButton:GetScript(\"OnEnter\")() this = nil")
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
s.ui_cursor(),
|
||||
@@ -1188,11 +1074,10 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
fn trade_recipient_money_renders_the_digit_not_ellipsis() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml"); // the BenillaMoney_* helpers
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // the BenillaMoney_* helpers
|
||||
load_xml(&s, "TradeFrame.xml");
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
|
||||
@@ -1244,13 +1129,12 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
|
||||
const WATER_LINK: &str = "|cffffffff|Hitem:159:0:0:0|h[Refreshing Spring Water]|h|r";
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml", // BenillaChatEdit_InsertLink, the shared shift-insert helper
|
||||
"GameTooltip.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
|
||||
@@ -734,7 +734,7 @@ fn interface_harness() -> UiScript {
|
||||
"ActionBar.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"Interface\\FrameXML\\BuffFrame.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"QuestFrame.xml",
|
||||
"QuestLogFrame.xml",
|
||||
],
|
||||
|
||||
@@ -42,6 +42,11 @@ fn frame_rect(quads: &[ExtractedQuad], w: f32, h: f32) -> benilla_ui::layout::Re
|
||||
fn shipped_gossip_frame_drives_end_to_end() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare
|
||||
// VM sizes every row from the previous frame's box. See
|
||||
// `shipped_gossip_rows_grow_to_their_wrapped_labels` for why this is a harness gap and not an
|
||||
// engine one.
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
@@ -54,9 +59,9 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
// button + the GOODBYE button. The greeting and the NPC-name banner are FontString layers (the
|
||||
// real GossipGreetingText ref l.241 / GossipFrameNpcNameText ref l.170) — not their own frames.
|
||||
assert_eq!(
|
||||
load_xml(&s, "GossipFrame.xml"),
|
||||
41,
|
||||
"window + greeting panel + scroll + bar (+2 arrows) + child + 32 rows + close + goodbye"
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"),
|
||||
43,
|
||||
"the stock file's own shape (1751) — ours materialized 41"
|
||||
);
|
||||
|
||||
// Hidden by default: no gossip icon on screen.
|
||||
@@ -101,8 +106,18 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
.unwrap(),
|
||||
"Greetings, traveler. How may I help you?"
|
||||
);
|
||||
// Row 1 shown + enabled; row 2 shown + disabled (coded); row 3+ hidden (no quest rows this menu,
|
||||
// so the shared pool starts filling from option 1 at row 1 — decision 0088 §3).
|
||||
// Both rows shown and BOTH ENABLED — including the coded one, and that is a divergence
|
||||
// retiring rather than a regression.
|
||||
//
|
||||
// Our `GossipFrame.xml` greyed a coded option, on decision 0081's "coded options are greyed,
|
||||
// never selected". **1.12 does no such thing**: `GossipFrameOptionsUpdate` (ref l.111-128) has
|
||||
// no coded handling at all — it sets the text, resizes, sets the icon from the option TYPE and
|
||||
// shows. There is no `Disable()` anywhere in the file.
|
||||
//
|
||||
// The consequence is worth naming: a coded option is now clickable, and our
|
||||
// `GossipSelectOption` still sends no code (0081 v1). The server sees a select without the
|
||||
// code it asked for. That is a named gap in the send path, not something the window should be
|
||||
// lying about — the reference lets you click it.
|
||||
let states: (bool, bool, bool, bool) = s
|
||||
.eval(
|
||||
"return GossipTitleButton1:IsVisible(), GossipTitleButton1:IsEnabled() ~= 0,\n\
|
||||
@@ -111,8 +126,8 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
states,
|
||||
(true, true, false, false),
|
||||
"coded row disabled, extras hidden"
|
||||
(true, true, true, false),
|
||||
"the reference greys nothing; extras hidden"
|
||||
);
|
||||
|
||||
s.resolve();
|
||||
@@ -177,8 +192,14 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
let icon_rect = quads
|
||||
.iter()
|
||||
.find(|q| {
|
||||
// Case-folded, because the reference concatenates the option TYPE verbatim into
|
||||
// `Interface\GossipFrame\<Type>GossipIcon` and our `GetGossipOptions` answers the
|
||||
// lowercase Era icon name. The asset VFS folds case, so the art loads either way; only
|
||||
// the path STRING differs, and our own `GossipFrame.xml` used to capitalise it before
|
||||
// building the path. Whether 1.12's own binding answers lowercase or capitalised is an
|
||||
// open fidelity question, not something to settle by editing the feed to match a test.
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("VendorGossipIcon"))
|
||||
if p.to_ascii_lowercase().contains("vendorgossipicon"))
|
||||
})
|
||||
.and_then(|q| q.rect)
|
||||
.expect("vendor option icon visible after GOSSIP_SHOW");
|
||||
@@ -203,7 +224,7 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
|
||||
// The close button queues a close intent (the app clears state; here we drive the hide too)
|
||||
// through HideUIPanel, which vacates the left slot.
|
||||
s.run("BenillaGossipCloseButton_OnClick()").unwrap();
|
||||
s.run("GossipFrameCloseButton:Click()").unwrap();
|
||||
assert!(s.take_gossip_close());
|
||||
assert!(!s.eval::<bool>("return GossipFrame:IsVisible()").unwrap());
|
||||
assert!(
|
||||
@@ -231,7 +252,7 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
s.set_gossip(Some(GossipMenu {
|
||||
greeting: "A word, traveler.".into(),
|
||||
@@ -259,20 +280,24 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// Rows 1-2 carry the quest titles (active first, matching the order the menu supplied them —
|
||||
// benilla's seam already flattens available/active into one ordered list, ui_gossip.rs), row 3
|
||||
// the option, row 4+ hidden.
|
||||
let (r1_text, r1_vis, r2_text, r2_vis, r3_text, r3_vis, r4_vis): (
|
||||
let (r1_text, r1_vis, r2_vis, r3_text, r3_vis, r4_vis, r5_text, r5_vis, r6_vis): (
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
) = s
|
||||
.eval(
|
||||
"return GossipTitleButton1Label:GetText(), GossipTitleButton1:IsVisible(),\n\
|
||||
GossipTitleButton2Label:GetText(), GossipTitleButton2:IsVisible(),\n\
|
||||
GossipTitleButton3Label:GetText(), GossipTitleButton3:IsVisible(),\n\
|
||||
GossipTitleButton4:IsVisible()",
|
||||
"return GossipTitleButton1:GetText(), GossipTitleButton1:IsVisible(),\n\
|
||||
GossipTitleButton2:IsVisible(),\n\
|
||||
GossipTitleButton3:GetText(), GossipTitleButton3:IsVisible(),\n\
|
||||
GossipTitleButton4:IsVisible(),\n\
|
||||
GossipTitleButton5:GetText(), GossipTitleButton5:IsVisible(),\n\
|
||||
GossipTitleButton6:IsVisible()",
|
||||
)
|
||||
.unwrap();
|
||||
// **AVAILABLE quests lead, then ACTIVE** — the reference's own order (`GossipFrameUpdate` calls
|
||||
@@ -280,14 +305,24 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// this window started reading the reference's two list verbs instead of the single ordered
|
||||
// `GetGossipQuestInfo` benilla invented. The packet listed the active quest first; the window
|
||||
// does not.
|
||||
// **Each group is followed by a HIDDEN SPACER row, and the button index skips it** — the
|
||||
// reference's own layout, and something our `GossipFrame.xml` did not do. Both
|
||||
// `GossipFrameAvailableQuestsUpdate` and `…ActiveQuestsUpdate` end by hiding
|
||||
// `GossipTitleButton<buttonIndex>` and then incrementing past it (ref l.80-84, l.104-108), so
|
||||
// the groups are separated by a blank row's worth of space rather than butting together.
|
||||
//
|
||||
// With one available quest, one active and one option that lays out as:
|
||||
// 1 available · 2 hidden spacer · 3 active · 4 hidden spacer · 5 option · 6+ hidden.
|
||||
assert_eq!((r1_text.as_str(), r1_vis), ("A Threat Within", true));
|
||||
assert_eq!((r2_text.as_str(), r2_vis), ("Report to Goldshire", true));
|
||||
assert!(!r2_vis, "the available group's trailing spacer");
|
||||
assert_eq!((r3_text.as_str(), r3_vis), ("Report to Goldshire", true));
|
||||
assert!(!r4_vis, "the active group's trailing spacer");
|
||||
assert_eq!(
|
||||
(r3_text.as_str(), r3_vis),
|
||||
(r5_text.as_str(), r5_vis),
|
||||
("Let me browse your goods.", true),
|
||||
"the option row sits below both quest rows"
|
||||
"the option row sits below both quest groups"
|
||||
);
|
||||
assert!(!r4_vis, "no fourth row: 2 quests + 1 option");
|
||||
assert!(!r6_vis, "nothing past the option row");
|
||||
|
||||
// The per-row icon matches active vs available (ref l.75/99), verified via the resolved quads
|
||||
// rather than the Lua-side texture string, so the assertion also proves the rows actually paint.
|
||||
@@ -331,8 +366,8 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// that back to the whole-menu position the app's queue speaks — 1, the packet's own order. The
|
||||
// two numbers being different for the same click is exactly what the single-list
|
||||
// `SelectGossipQuest` we retired could not express.
|
||||
s.run("BenillaGossipRow_OnClick(GossipTitleButton2)")
|
||||
.unwrap();
|
||||
// Button 3, not 2: the reference's hidden spacer sits at 2 (see the layout note above).
|
||||
s.run("GossipTitleButton3:Click()").unwrap();
|
||||
assert_eq!(s.take_gossip_quest_selects(), vec![1]);
|
||||
assert!(
|
||||
s.take_gossip_selects().is_empty(),
|
||||
@@ -355,6 +390,17 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// **A measurer, because the reference measures inside the tick it sets the text.**
|
||||
// `GossipResize` reads `titleButton:GetTextHeight()` on the line after `SetText`
|
||||
// (`GossipFrame.lua:130-137`), and without a `TextMeasure` installed a bare VM answers the
|
||||
// PREVIOUS frame's box — the row comes out 2px tall.
|
||||
//
|
||||
// This is what deferred the gossip window through 1751: it was read as "our measurement is a
|
||||
// frame late", an engine gap. It is not. `UiScript::fill_measures` closes the loop inline
|
||||
// whenever a measurer is installed, which the app always does
|
||||
// (`ui_script::extract`'s `AtlasMeasurer`); only a bare test VM does not. The gap was in the
|
||||
// harness, and it hid behind a window we had written to measure a frame later.
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
@@ -362,7 +408,7 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// Three long options — the shape of a real judgement/roleplay menu, every one of them wrapping
|
||||
// at the row label's 275 px width.
|
||||
@@ -424,7 +470,7 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
|
||||
let row = |i: u32| -> (f32, f32, f32) {
|
||||
s.eval::<(f32, f32, f32)>(&format!(
|
||||
"return GossipTitleButton{i}:GetTop(), GossipTitleButton{i}:GetBottom(), \
|
||||
GossipTitleButton{i}Label:GetHeight()"
|
||||
GossipTitleButton{i}:GetTextHeight()"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
@@ -473,7 +519,7 @@ fn gossip_show_hide_plays_open_and_close_kits() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// Hidden at load: no open sound (never transitions on startup).
|
||||
assert!(
|
||||
@@ -514,9 +560,14 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
|
||||
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
s.set_gossip(Some(GossipMenu {
|
||||
greeting: "Well met.".into(),
|
||||
@@ -601,9 +652,14 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
|
||||
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Vendor open at the left slot; clear any startup/open intents.
|
||||
s.set_merchant(Some(MerchantState::default()));
|
||||
@@ -648,7 +704,12 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
|
||||
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// The synthetic pushable=7 loot stand-in opens first onto the empty left slot. `CreateFrame`
|
||||
// frames start shown by default (matching the real client) — every shipped panel frame is
|
||||
@@ -754,7 +815,7 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
// and UIPanelButtonTemplate, and an `inherits=` is resolved at LOAD.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BankFrame.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// The gossip menu is open on the banker (its bank option showing).
|
||||
s.set_gossip(Some(GossipMenu {
|
||||
@@ -818,11 +879,16 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare
|
||||
// VM sizes every row from the previous frame's box. See
|
||||
// `shipped_gossip_rows_grow_to_their_wrapped_labels` for why this is a harness gap and not an
|
||||
// engine one.
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// Eight wrapping options: ~4 lines each, far past the 334 px scroll frame.
|
||||
let long = |n: usize| GossipOptionView {
|
||||
@@ -863,7 +929,14 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// The child grew past the 334 px frame, so there is a range to scroll …
|
||||
// There is a range to scroll — which is the property that matters and the one the window
|
||||
// exists to have.
|
||||
//
|
||||
// The child's own HEIGHT is no longer part of the assertion. Ours grew it past the 334px frame
|
||||
// in `BenillaGossipFrame_SizeScrollChild`, a helper the reference does not have: stock leaves
|
||||
// the scroll child at the frame's size and lets the rows' own extents drive the range. So the
|
||||
// child measures 334 and the range is non-zero, where ours had both. Asserting the range alone
|
||||
// is asserting the behaviour rather than one implementation's way of reaching it.
|
||||
let (child_h, range): (f32, f32) = s
|
||||
.eval(
|
||||
"return GossipGreetingScrollChildFrame:GetHeight(), \
|
||||
@@ -871,8 +944,8 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
child_h > 334.0 && range > 0.0,
|
||||
"content overflows the 334px frame: child {child_h}, range {range}"
|
||||
range > 0.0,
|
||||
"the overflowing menu has somewhere to scroll: child {child_h}, range {range}"
|
||||
);
|
||||
// … and the bar is up for it (it stays hidden when everything fits — the fit case is covered by
|
||||
// `shipped_gossip_frame_drives_end_to_end`'s two-option menu).
|
||||
@@ -969,7 +1042,7 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// The addon's three lines, in the order an addon writes them.
|
||||
s.run(
|
||||
|
||||
@@ -22,7 +22,7 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// The BenillaMoney_* purse helpers the quest reward/progress panels repaint through live in
|
||||
// MerchantFrame.xml (the same documented cross-window dep the bag tests load).
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -79,7 +79,7 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -156,7 +156,7 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -239,7 +239,7 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -302,7 +302,7 @@ fn greeting_goodbye_button_closes_the_window() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -337,7 +337,7 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
s.set_quest(Some(QuestState {
|
||||
@@ -446,7 +446,7 @@ fn write_on_still_fades_when_instant_text_is_off() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
s.eval::<()>(r#"QUEST_FADING_DISABLE = "0""#).unwrap();
|
||||
@@ -507,7 +507,7 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -570,7 +570,7 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -654,7 +654,7 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
|
||||
@@ -85,7 +85,7 @@ fn shipped_questlog_frame_loads_clean() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -106,7 +106,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -290,7 +290,7 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -410,7 +410,7 @@ fn watch_guards_no_op_without_erroring() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // the guards' red-line surface
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
@@ -504,7 +504,7 @@ fn progress_auto_watches_for_five_minutes() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -566,7 +566,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -605,7 +605,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -655,7 +655,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -811,7 +811,7 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -873,7 +873,7 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -919,7 +919,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -982,7 +982,7 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
|
||||
// Fonts.xml alone is the source now. Formerly: it
|
||||
// needs the dropdown kit its GroupLootDropDown initializes against (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1116,7 +1116,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1196,7 +1196,7 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1287,7 +1287,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1361,7 +1361,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1389,7 +1389,7 @@ fn share_quest_click_queues_the_selected_quests_id() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
|
||||
@@ -389,10 +389,17 @@ mod tests {
|
||||
/// mostly noise. Here the manifest is loaded first and `_G` is read after, so the answer is
|
||||
/// what this client *actually* answers to.
|
||||
///
|
||||
/// **Three columns, and the third one exists because its absence cost a swap.** `engine=` and
|
||||
/// `fx=` read bare `Name(` globals. `method=` reads `:Name(` calls against the method surface
|
||||
/// this engine actually exposes — built by walking `getmetatable(w).__index` over one widget
|
||||
/// of every type, plus the tooltip.
|
||||
/// **Four columns, because a window can be blocked four ways and this could once see one.**
|
||||
/// `engine=` and `fx=` read bare `Name(` globals. `method=` reads `:Name(` calls against the
|
||||
/// method surface this engine actually exposes. `LOAD:` is the stock file loaded on top of our
|
||||
/// manifest in a fresh VM — the same pass [`chain_readiness_report`] makes, run here so the
|
||||
/// answer is in one table.
|
||||
///
|
||||
/// That last one is 1790, and it is the same mistake as the third column one step later.
|
||||
/// `<LootButton>` and `<TaxiRouteFrame>` are element TAGS: no census of `Name(` or `:Name(`
|
||||
/// can reach them, so `LootFrame.xml` sat in this report's "needs NO engine work" list while
|
||||
/// the readiness probe was printing `4 issue(s)` for it in a different table. Both tables were
|
||||
/// right. Joining them was left to whoever read them, and I read it wrong.
|
||||
///
|
||||
/// Until 1787 the method sites were **silently dropped**: a name in neither the engine nor the
|
||||
/// FrameXML half of `1.12-globals.tsv` was assumed to be a widget method and skipped, on the
|
||||
@@ -403,10 +410,15 @@ mod tests {
|
||||
/// file loaded, every check passed, and hovering a vendor row would have raised in play. Both
|
||||
/// instruments were right about what they measure. Neither measured the window.
|
||||
///
|
||||
/// Two crudenesses remain, stated because they decide how to read the output. Both scans count
|
||||
/// a call inside an XML comment (1.12 comments out whole blocks — `GuildRegistrarFrame`'s
|
||||
/// tabard button is one), so they can over-report; and neither can see a name reached through
|
||||
/// `getglobal`, so they can under-report. The `method=` column is also receiver-blind: it asks
|
||||
/// One crudeness remains, stated because it decides how to read the output: neither scan can
|
||||
/// see a name reached through `getglobal`, so both can under-report.
|
||||
///
|
||||
/// They no longer over-report on comments. Both counted commented-out calls until 1789 — 1.12
|
||||
/// comments out whole blocks, and `PaperDollFrame.lua:754-756`'s `ShowInventorySellCursor` is
|
||||
/// three commented lines that this report named as that window's last engine gap. A real
|
||||
/// binding, never called, blocking a window that was not blocked. [`strip_comments`] takes
|
||||
/// Lua `--`/`--[[ ]]` and XML `<!-- -->` out first; it does not track string literals, so it
|
||||
/// can only ever under-report, which is the safe direction here. The `method=` column is also receiver-blind: it asks
|
||||
/// "does ANY widget type answer to this name", not "does *this* receiver", so a method that
|
||||
/// exists on the wrong type still reads as present. It is for ranking work, not for proving a
|
||||
/// window done — [`chain_readiness_report`] and the window's own tests are that, and the
|
||||
@@ -444,6 +456,79 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// Strip what is not code before either scan. Both `Name(` and `:Name(` counted calls
|
||||
// inside comments until 1789: the round that built `PickupMerchantItem` also asked wow-re
|
||||
// for `ShowInventorySellCursor`, which this report had named as `PaperDollFrame.xml`'s
|
||||
// last engine gap — and the answer was that stock `PaperDollFrame.lua:754-756` has the
|
||||
// call **commented out**, all three lines. A real binding, never called, blocking a window
|
||||
// that was not blocked.
|
||||
//
|
||||
// Line-based and deliberately simple: Lua `--` to end of line (but not `--[[`, which opens
|
||||
// a block), Lua `--[[ … ]]` blocks, and XML `<!-- … -->` blocks. It does not track string
|
||||
// literals, so a `"--"` inside a string truncates that line — which can only ever cause an
|
||||
// UNDER-report, the safe direction for an instrument that ranks work.
|
||||
let strip_comments = |text: &str| -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut in_xml = false;
|
||||
let mut in_lua_block = false;
|
||||
for line in text.lines() {
|
||||
let mut rest = line;
|
||||
let mut kept = String::new();
|
||||
loop {
|
||||
if in_xml {
|
||||
match rest.find("-->") {
|
||||
Some(i) => {
|
||||
in_xml = false;
|
||||
rest = &rest[i + 3..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else if in_lua_block {
|
||||
match rest.find("]]") {
|
||||
Some(i) => {
|
||||
in_lua_block = false;
|
||||
rest = &rest[i + 2..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else {
|
||||
let xml = rest.find("<!--");
|
||||
let lua = rest.find("--");
|
||||
match (xml, lua) {
|
||||
(Some(x), Some(l)) if x <= l => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(_, Some(l)) => {
|
||||
kept.push_str(&rest[..l]);
|
||||
if rest[l..].starts_with("--[[") {
|
||||
in_lua_block = true;
|
||||
rest = &rest[l + 4..];
|
||||
} else {
|
||||
// A plain `--` comment runs to end of line.
|
||||
rest = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(x), None) => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
if !in_xml && !in_lua_block {
|
||||
kept.push_str(rest);
|
||||
}
|
||||
out.push_str(&kept);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
// `:Name(` — a method call, receiver unknown.
|
||||
let called_methods = |text: &str| -> std::collections::HashSet<String> {
|
||||
let b: Vec<char> = text.chars().collect();
|
||||
@@ -598,8 +683,15 @@ mod tests {
|
||||
};
|
||||
|
||||
println!("\n=== 1751 gap report — what each unmigrated window would cost ===");
|
||||
#[allow(clippy::type_complexity)] // (engine count, file, engine, fx, method)
|
||||
let mut rows: Vec<(usize, String, Vec<String>, Vec<String>, Vec<String>)> = Vec::new();
|
||||
#[allow(clippy::type_complexity)] // (blockers, file, engine, fx, method, load errors)
|
||||
let mut rows: Vec<(
|
||||
usize,
|
||||
String,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
)> = Vec::new();
|
||||
for f in &stock {
|
||||
if migrated.contains(f) {
|
||||
continue;
|
||||
@@ -610,6 +702,7 @@ mod tests {
|
||||
text.push_str(&String::from_utf8_lossy(&b));
|
||||
}
|
||||
}
|
||||
let text = strip_comments(&text);
|
||||
let own: std::collections::HashSet<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
@@ -637,33 +730,69 @@ mod tests {
|
||||
}
|
||||
// The method half. `own`/`have` do not apply: a method is never a global, so the only
|
||||
// question is whether this engine's widgets answer to the name.
|
||||
// …and does it LOAD? A window can be blocked on a global, on a widget method, or on a
|
||||
// widget TYPE — and the two scans above see only the first two. `<LootButton>` and
|
||||
// `<TaxiRouteFrame>` are element tags: nothing in a `Name(` or `:Name(` census can
|
||||
// reach them, and `LootFrame.xml` sat in this report's "needs NO engine work" list
|
||||
// while `chain_readiness_report` was printing `4 issue(s)` for it in another table.
|
||||
//
|
||||
// Both tables were right. Joining them was left to whoever read them, and I got it
|
||||
// wrong (1790). So this one runs the load itself — the same fresh-VM-plus-manifest
|
||||
// pass the readiness probe does — and reports it in the same row.
|
||||
let loads = {
|
||||
let mut probe = UiScript::new().expect("VM");
|
||||
probe.set_screen_size(1024.0, 768.0);
|
||||
let base = super::super::manifest::load_default_ui(&probe);
|
||||
assert!(base.is_empty(), "the shipped manifest itself: {base:#?}");
|
||||
probe.resolve();
|
||||
let before = probe.errors().len();
|
||||
let path = format!("Interface\\FrameXML\\{f}");
|
||||
let addon = super::addon(vec![path.clone()]);
|
||||
let mut said = addon.load_files(&probe, std::slice::from_ref(&path));
|
||||
probe.resolve();
|
||||
said.extend(probe.errors().into_iter().skip(before));
|
||||
said
|
||||
};
|
||||
|
||||
let mut asked: Vec<String> = called_methods(&text).into_iter().collect();
|
||||
asked.sort();
|
||||
let known = answers(&s, &asked);
|
||||
let mut meth: Vec<String> = asked.into_iter().filter(|m| !known.contains(m)).collect();
|
||||
meth.sort();
|
||||
rows.push((eng.len() + meth.len(), f.clone(), eng, fx, meth));
|
||||
rows.push((
|
||||
eng.len() + meth.len() + loads.len(),
|
||||
f.clone(),
|
||||
eng,
|
||||
fx,
|
||||
meth,
|
||||
loads,
|
||||
));
|
||||
}
|
||||
rows.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
|
||||
for (n, f, eng, fx, meth) in &rows {
|
||||
println!("{n:>3} engine {f}");
|
||||
for (n, f, eng, fx, meth, loads) in &rows {
|
||||
println!("{n:>3} blocker(s) {f}");
|
||||
if !eng.is_empty() {
|
||||
println!(" engine: {}", eng.join(" "));
|
||||
}
|
||||
if !meth.is_empty() {
|
||||
println!(" method: {}", meth.join(" "));
|
||||
}
|
||||
for e in loads {
|
||||
let one = e.replace('\n', " ");
|
||||
println!(" LOAD: {}", &one[..one.len().min(150)]);
|
||||
}
|
||||
if !fx.is_empty() {
|
||||
println!(" fx: {}", fx.join(" "));
|
||||
}
|
||||
}
|
||||
// "Free" now means free of BOTH — the count above is engine names plus missing methods,
|
||||
// because a window blocked on a widget method is exactly as blocked as one missing a
|
||||
// global, and reading it as free is the mistake 1787 is about.
|
||||
// "Free" means free of ALL THREE. A window blocked on a widget method, or on a widget type
|
||||
// its XML declares, is exactly as blocked as one missing a global — and the `fx=` column
|
||||
// is deliberately NOT counted, because a FrameXML function another stock file defines
|
||||
// arrives with that file rather than needing to be built.
|
||||
let free: Vec<&String> = rows.iter().filter(|r| r.0 == 0).map(|r| &r.1).collect();
|
||||
println!(
|
||||
"\n=== {} windows need NO engine work at all (no missing global AND no missing \
|
||||
method) ===",
|
||||
"\n=== {} windows are UNBLOCKED — no missing global, no missing method, and the \
|
||||
stock file loads clean on top of our manifest ===",
|
||||
free.len()
|
||||
);
|
||||
for f in free {
|
||||
@@ -671,6 +800,109 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **Every function of ours that shadows one the player's own chain already defines.**
|
||||
///
|
||||
/// A window that reads unblocked in [`chain_gap_report`] can still fail to swap, and the third
|
||||
/// reason found (after a missing widget type and a missing widget method) is this one: our
|
||||
/// `assets/ui` file defines a global the reference defines too, our manifest loads a CHAIN file
|
||||
/// that also defines it, and whichever lands second wins. `PartyFrame.xml` is the worked
|
||||
/// example — `UnitFrames.xml` redefines `UnitFrame_OnEvent`/`UnitFrame_Update` nine manifest
|
||||
/// lines after stock `UnitFrame.lua` defines them, so a stock party row built by the
|
||||
/// reference's own `UnitFrame_Initialize` calls OUR update and indexes a field its rows do not
|
||||
/// carry. It loads clean and raises on the first event.
|
||||
///
|
||||
/// Shadowing is not automatically wrong — where we ship a file the reference would have
|
||||
/// shipped, defining its names is the whole job. It is wrong precisely when **both** copies
|
||||
/// load, which is what this reports: a name ours defines that a chain entry in our own
|
||||
/// manifest also defines.
|
||||
///
|
||||
/// Run it before attempting a swap. It predicts which ones will fail without attempting them.
|
||||
#[test]
|
||||
#[ignore = "instrument: run by hand before attempting a window swap"]
|
||||
fn shadowed_reference_functions() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
|
||||
let defined_in = |text: &str| -> Vec<String> {
|
||||
text.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
.map(|r| {
|
||||
r.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect::<String>()
|
||||
})
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 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();
|
||||
let mut chain_home: std::collections::HashMap<String, (String, usize)> =
|
||||
std::collections::HashMap::new();
|
||||
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
|
||||
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or(entry);
|
||||
let mut cands = vec![entry.clone()];
|
||||
if let Some(stem) = entry.strip_suffix(".xml") {
|
||||
cands.push(format!("{stem}.lua"));
|
||||
}
|
||||
for cand in cands {
|
||||
let Some(b) = super::read(&cand.replace('\\', "/")) else {
|
||||
continue;
|
||||
};
|
||||
let text = String::from_utf8_lossy(&b).into_owned();
|
||||
for name in defined_in(&text) {
|
||||
let at = pos[entry];
|
||||
chain_home.entry(name).or_insert_with(|| {
|
||||
(cand.rsplit('/').next().unwrap_or(leaf).to_string(), at)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// …against everything OUR files define.
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
let mut hits: Vec<(String, String, String, bool)> = Vec::new();
|
||||
for entry in toc.iter().filter(|f| !super::is_chain_entry(f)) {
|
||||
let Ok(text) = std::fs::read_to_string(dir.join(entry)) else {
|
||||
continue;
|
||||
};
|
||||
for name in defined_in(&text) {
|
||||
if let Some((home, at)) = chain_home.get(&name) {
|
||||
// Load order settles it: the later definition is the one that stands.
|
||||
let ours_wins = pos[entry] > *at;
|
||||
hits.push((name, entry.clone(), home.clone(), ours_wins));
|
||||
}
|
||||
}
|
||||
}
|
||||
hits.sort();
|
||||
hits.dedup();
|
||||
|
||||
println!("\n=== names ours redefines that a CHAIN entry already defines ===");
|
||||
println!("{:<36} {:<28} {:<34} winner", "name", "ours", "chain");
|
||||
for (name, ours, home, ours_wins) in &hits {
|
||||
let w = if *ours_wins { "OURS" } else { "the chain's" };
|
||||
println!("{name:<36} {ours:<28} {home:<34} {w}");
|
||||
}
|
||||
|
||||
// Grouped by our file, because that is the unit of work: a window cannot swap while OUR
|
||||
// file is still standing on the names its stock counterpart needs.
|
||||
let mut by_file: std::collections::BTreeMap<&String, usize> =
|
||||
std::collections::BTreeMap::new();
|
||||
for (_, ours, _, _) in &hits {
|
||||
*by_file.entry(ours).or_default() += 1;
|
||||
}
|
||||
println!(
|
||||
"\n=== {} collisions, across {} of our files ===",
|
||||
hits.len(),
|
||||
by_file.len()
|
||||
);
|
||||
for (f, n) in &by_file {
|
||||
println!(" {n:>3} {f}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A path is a chain entry; a bare name is ours. The one-line rule the manifest rests on.
|
||||
#[test]
|
||||
fn a_separator_is_what_makes_an_entry_the_players_own_file() {
|
||||
|
||||
@@ -814,6 +814,14 @@ fn every_texture_frame_outranks_its_status_bars() {
|
||||
}
|
||||
// Player + target (health/power) and four party members (health/mana) — never let this pass by
|
||||
// matching nothing, which is exactly how a renamed frame would silently retire the check.
|
||||
//
|
||||
// **This number drops to 6 the day `PartyFrame.xml` goes to the chain, and that will be a real
|
||||
// reduction rather than a rename.** Stock `PartyFrameTemplates.xml` has no
|
||||
// `$parentTextureFrame` at all: it gets the same result a different way, with the art inside a
|
||||
// pair of ANONYMOUS nested `<Frame setAllPoints="true">` wrappers, so it outranks the bars by
|
||||
// nesting depth rather than by a level on a named frame. There is nothing to look up, so the
|
||||
// party rows will move from this assertion's cover to the loader's nesting rule. Written down
|
||||
// now, while the reason is in front of somebody, rather than discovered as a number to lower.
|
||||
assert!(
|
||||
checked >= 12,
|
||||
"only {checked} texture-frame/bar pairs checked — the name sweep found nothing"
|
||||
|
||||
@@ -110,6 +110,28 @@ fn read(req: &str) -> Option<Vec<u8>> {
|
||||
/// constant by hand would pass and teach nothing about the real load.
|
||||
///
|
||||
/// Needs client data, like [`BAG_UI`]: open with `benilla_formats::wow_data_or_skip!()`.
|
||||
/// What the **vendor window** needs before `Interface\FrameXML\MerchantFrame.xml` will load and
|
||||
/// behave — the same shape as [`BAG_UI`] and [`LOOT_UI`], and it grew for 1751 the same way.
|
||||
///
|
||||
/// The two that fail in the ways worth naming:
|
||||
///
|
||||
/// * **`BasicControls.xml`** — for `TEXT()`, the reference's identity-function wrapper, which
|
||||
/// stock `MerchantFrame.lua:70` calls while building every row. Absent, it raises on the first
|
||||
/// `MerchantFrame_UpdateMerchantInfo`, which is the first thing the window does when it shows.
|
||||
/// * **`Interface\FrameXML\ItemButtonTemplate.xml`** — the stock rows' `$parentItemButton`
|
||||
/// inherits it, and a missing template is a loader *warning*: the rows load clean with no art.
|
||||
///
|
||||
/// Needs client data: open with `benilla_formats::wow_data_or_skip!()`.
|
||||
pub(super) const MERCHANT_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml",
|
||||
"BasicControls.xml", // TEXT()
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml", // app load order: tooltip before merchant
|
||||
];
|
||||
|
||||
pub(super) const LOOT_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml", // ITEM_QUALITY_COLORS — the row-name palette
|
||||
|
||||
@@ -32,7 +32,7 @@ fn harness() -> UiScript {
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"CharacterFrame.xml",
|
||||
] {
|
||||
@@ -58,7 +58,7 @@ fn harness_with_bags() -> UiScript {
|
||||
}
|
||||
for f in [
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"CharacterFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
|
||||
@@ -30,7 +30,7 @@ fn trainer_script() -> UiScript {
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the kit's MenuBackdrop), app order
|
||||
load_xml(&s, "UIDropDownMenu.xml"); // the filter dropdown's kit
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the faux-scroll bar kit
|
||||
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_Set/_Clear/_SetColor live here
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_Set/_Clear/_SetColor live here
|
||||
load_xml(&s, "TrainerFrame.xml");
|
||||
s.run(
|
||||
"TRAINER_FILTER_AVAILABLE = 1 TRAINER_FILTER_UNAVAILABLE = 1 TRAINER_FILTER_USED = 1 \
|
||||
|
||||
@@ -593,7 +593,7 @@ fn shipped_target_frame_runs_the_level_law() {
|
||||
// GetDifficultyColor's own load chain (the quest log window, its ref home).
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "QuestLogFrame.xml");
|
||||
// The player at level 3, both feeds (the snapshot UnitLevel("player") reads; the req state
|
||||
// the −1 gate and GetQuestGreenRange read) — the app keeps the two in step.
|
||||
|
||||
@@ -17,9 +17,10 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
|
||||
/// Every one of these is a real dependency: UiPanels for the panel manager + tab kit + StaticPopup
|
||||
/// engine, ScrollTemplates for the faux lists, UIPanelTemplates for the button/input/checkbox
|
||||
/// templates, UIDropDownMenu for the rarity capsule, MoneyFrame for `SmallMoneyFrameTemplate` +
|
||||
/// the `MoneyTypeInfo` table this window registers `AUCTION_DEPOSIT` into, MerchantFrame for the
|
||||
/// `BenillaMoneyInput_*` money-entry helpers.
|
||||
const FILES: [&str; 9] = [
|
||||
/// the `MoneyTypeInfo` table this window registers `AUCTION_DEPOSIT` into, The `BenillaMoneyInput_*` money-entry
|
||||
/// helpers used to mean loading MerchantFrame.xml as well; 1751 moved that kit to MoneyFrame.xml
|
||||
/// on its way to the chain, so the dependency is gone.
|
||||
const FILES: [&str; 8] = [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
@@ -27,7 +28,6 @@ const FILES: [&str; 9] = [
|
||||
"UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"MerchantFrame.xml",
|
||||
"AuctionFrame.xml",
|
||||
];
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
|
||||
use benilla_ui::script::{ItemTextState, UiScript};
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
|
||||
|
||||
/// The reader's dependency prefix, in the manifest's own order. `ScrollTemplates.xml` and
|
||||
@@ -442,18 +445,11 @@ fn a_quest_givers_gossip_displaces_the_open_reader() {
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
load_ui(&s);
|
||||
// The other rival, loaded exactly as the app's manifest does (after the reader's prefix).
|
||||
{
|
||||
let dir = std::path::Path::new(UI_DIR);
|
||||
let text = std::fs::read_to_string(dir.join("GossipFrame.xml")).unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"GossipFrame.xml: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
// The other rival — the reference's own window since 1751, so it goes through the shared
|
||||
// both-stores loader rather than a disk read off `assets/ui`. A hand-rolled reader here is
|
||||
// exactly what `tests/common` exists to replace: it broke the moment this file became the
|
||||
// player's own, and a disk-only provider would leave `GossipFrame.lua`'s globals nil besides.
|
||||
common::load_ui(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// The note is open and holding the left slot.
|
||||
s.set_item_text(Some(letter()));
|
||||
|
||||
@@ -12,12 +12,11 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
|
||||
/// The mail window's load prefix — the app's own order (`ui_script/mod.rs`), members only.
|
||||
/// MerchantFrame.xml rides along because MailFrame.xml reuses its global `BenillaMoney_*` coin
|
||||
/// helpers (postage display), so a load error in either fails here.
|
||||
const FILES: [&str; 6] = [
|
||||
const FILES: [&str; 5] = [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
"MerchantFrame.xml",
|
||||
"MailFrame.xml",
|
||||
];
|
||||
|
||||
|
||||
@@ -13,12 +13,11 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
|
||||
/// The trade window's load prefix — the app's own order (`ui_script/mod.rs`), members only.
|
||||
/// MerchantFrame.xml rides along because TradeFrame.xml reuses its global `BenillaMoney_*` coin
|
||||
/// helpers (the two gold displays), so a load error in either fails here.
|
||||
const FILES: [&str; 6] = [
|
||||
const FILES: [&str; 5] = [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
"MerchantFrame.xml",
|
||||
"TradeFrame.xml",
|
||||
];
|
||||
|
||||
|
||||
@@ -355,3 +355,206 @@ fn set_trade_skill_item_indexes_visible_rows_not_raw_recipes() {
|
||||
// rather than a neighbouring recipe's tooltip appearing under the cursor.
|
||||
assert_eq!(name_at(&mut s, 4, ""), "Alpha Cloak Reagent");
|
||||
}
|
||||
|
||||
/// `SetMerchantCompareItem(index [, offset])` — the shopping tooltip the stock vendor row raises.
|
||||
///
|
||||
/// The contract that decides ghost tooltips versus none is the RETURN, so that is what this pins:
|
||||
/// the **number** 1 on success, `nil` on every failure, one value always. Stock's
|
||||
/// `if ( ShoppingTooltip1:SetMerchantCompareItem(id, 1) ) then … end` reads it directly, so a
|
||||
/// boolean `false` where nil belongs shows an empty tooltip and a truthy nil-case shows two.
|
||||
#[test]
|
||||
fn set_merchant_compare_item_answers_one_or_nil_per_candidate_slot() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
|
||||
// Both finger slots worn, both class 4 (ARMOR) like the ring on the shelf.
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
for (slot, id, name) in [(11usize, 7000u32, "Old Loop"), (12, 7001, "Older Loop")] {
|
||||
inv[slot] = Some(InvSlotView {
|
||||
item_id: id,
|
||||
name: Some(name.into()),
|
||||
quality: 1,
|
||||
..Default::default()
|
||||
});
|
||||
s.set_item_template(
|
||||
id,
|
||||
ItemTemplateView {
|
||||
name: name.into(),
|
||||
quality: 1,
|
||||
class: 4,
|
||||
inventory_type: 11,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
// …and a shield, class 4, in the off hand — the case that makes offset 2 nil for a WEAPON.
|
||||
inv[17] = Some(InvSlotView {
|
||||
item_id: 7100,
|
||||
name: Some("Battered Buckler".into()),
|
||||
quality: 1,
|
||||
..Default::default()
|
||||
});
|
||||
s.set_item_template(
|
||||
7100,
|
||||
ItemTemplateView {
|
||||
name: "Battered Buckler".into(),
|
||||
quality: 1,
|
||||
class: 4, // ARMOR
|
||||
inventory_type: 14,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
inv[16] = Some(InvSlotView {
|
||||
item_id: 7101,
|
||||
name: Some("Bent Sword".into()),
|
||||
quality: 1,
|
||||
..Default::default()
|
||||
});
|
||||
s.set_item_template(
|
||||
7101,
|
||||
ItemTemplateView {
|
||||
name: "Bent Sword".into(),
|
||||
quality: 1,
|
||||
class: 2, // WEAPON
|
||||
inventory_type: 13,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.set_inventory_slots(inv);
|
||||
|
||||
s.set_merchant(Some(MerchantState {
|
||||
items: vec![
|
||||
MerchantItem {
|
||||
name: Some("Shiny Loop".into()),
|
||||
item_id: 8000,
|
||||
..Default::default()
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Sharp Sword".into()),
|
||||
item_id: 8001,
|
||||
..Default::default()
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Unseen Thing".into()),
|
||||
item_id: 8999, // no template — the uncached leg
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}));
|
||||
s.set_item_template(
|
||||
8000,
|
||||
ItemTemplateView {
|
||||
name: "Shiny Loop".into(),
|
||||
quality: 3,
|
||||
class: 4,
|
||||
inventory_type: 11, // finger — two candidate slots
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.set_item_template(
|
||||
8001,
|
||||
ItemTemplateView {
|
||||
name: "Sharp Sword".into(),
|
||||
quality: 3,
|
||||
class: 2,
|
||||
inventory_type: 13, // one-hand — main hand then off hand
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// The two shopping tooltips are ordinary GameTooltip frames — the engine knows nothing about
|
||||
// them (the substring `ShoppingTooltip` does not occur in the image at all).
|
||||
s.run(
|
||||
r#"
|
||||
CreateFrame("GameTooltip", "ShoppingTooltip1"):Hide()
|
||||
CreateFrame("GameTooltip", "ShoppingTooltip2"):Hide()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let call =
|
||||
|s: &mut UiScript, expr: &str| s.eval::<String>(&format!("return type({expr})")).unwrap();
|
||||
|
||||
// A ring against two worn rings: both offsets answer, and the answer is the NUMBER 1.
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return tostring(ShoppingTooltip1:SetMerchantCompareItem(1, 1))"#)
|
||||
.unwrap(),
|
||||
"1",
|
||||
"the truthy leg is lua_pushnumber(1.0), not a boolean"
|
||||
);
|
||||
assert_eq!(
|
||||
call(&mut s, "ShoppingTooltip2:SetMerchantCompareItem(1, 2)"),
|
||||
"number"
|
||||
);
|
||||
// …and it filled with the WORN item, headed by the gray compare line.
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
r#"local n = ShoppingTooltip1:NumLines()
|
||||
for i = 1, n do
|
||||
if getglobal("ShoppingTooltip1TextLeft"..i):GetText() == "Currently Equipped" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false"#
|
||||
)
|
||||
.unwrap(),
|
||||
"the fill is the equipped item's own tooltip with the CURRENTLY_EQUIPPED header"
|
||||
);
|
||||
|
||||
// A one-hand weapon: main hand matches (class 2), the off-hand SHIELD does not (class 4), so
|
||||
// offset 2 is nil. This is the reference's class test doing the work, and it is the case a
|
||||
// slot-only reading would get wrong.
|
||||
assert_eq!(
|
||||
call(&mut s, "ShoppingTooltip1:SetMerchantCompareItem(2, 1)"),
|
||||
"number"
|
||||
);
|
||||
assert_eq!(
|
||||
call(&mut s, "ShoppingTooltip1:SetMerchantCompareItem(2, 2)"),
|
||||
"nil",
|
||||
"a shield is not a candidate for a weapon — class, not slot"
|
||||
);
|
||||
|
||||
// Offset defaults to 1 when absent, and does NOT raise.
|
||||
assert_eq!(
|
||||
call(&mut s, "ShoppingTooltip1:SetMerchantCompareItem(1)"),
|
||||
"number"
|
||||
);
|
||||
// An explicit 0 or a negative is nil — the counter starts below zero and only decrements.
|
||||
for bad in ["0", "-1"] {
|
||||
assert_eq!(
|
||||
call(
|
||||
&mut s,
|
||||
&format!("ShoppingTooltip1:SetMerchantCompareItem(1, {bad})")
|
||||
),
|
||||
"nil",
|
||||
"offset {bad}"
|
||||
);
|
||||
}
|
||||
// Out of range, and the uncached template, are both nil — never an error.
|
||||
for expr in [
|
||||
"ShoppingTooltip1:SetMerchantCompareItem(0, 1)",
|
||||
"ShoppingTooltip1:SetMerchantCompareItem(99, 1)",
|
||||
"ShoppingTooltip1:SetMerchantCompareItem(3, 1)",
|
||||
] {
|
||||
assert_eq!(call(&mut s, expr), "nil", "{expr}");
|
||||
}
|
||||
// 2.7 re-bases in f64 and THEN truncates: 2.7 - 1 = 1.7 -> row 1, the sword. Doing it the
|
||||
// other way round would land on row 2.
|
||||
assert_eq!(
|
||||
call(&mut s, "ShoppingTooltip1:SetMerchantCompareItem(2.7, 1)"),
|
||||
"number"
|
||||
);
|
||||
|
||||
// A non-number index RAISES, with the client's own usage text.
|
||||
let e = s
|
||||
.run(r#"ShoppingTooltip1:SetMerchantCompareItem({}, 1)"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(e.contains("Usage: SetMerchantCompareItem"), "{e}");
|
||||
// …but a numeric STRING is a number to `lua_isnumber`.
|
||||
assert_eq!(
|
||||
call(&mut s, r#"ShoppingTooltip1:SetMerchantCompareItem("1", 1)"#),
|
||||
"number"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,24 +161,30 @@ fn render_by_id(
|
||||
/// `ui_items::find_equip_slot` (the equip-click fit rule) — one law, two consumers.
|
||||
fn equip_slots_for(inventory_type: u32) -> &'static [u32] {
|
||||
match inventory_type {
|
||||
1 => &[1], // head
|
||||
2 => &[2], // neck
|
||||
3 => &[3], // shoulder
|
||||
4 => &[4], // shirt
|
||||
5 | 20 => &[5], // chest / robe
|
||||
6 => &[6], // waist
|
||||
7 => &[7], // legs
|
||||
8 => &[8], // feet
|
||||
9 => &[9], // wrist
|
||||
10 => &[10], // hands
|
||||
11 => &[11, 12], // finger
|
||||
12 => &[13, 14], // trinket
|
||||
16 => &[15], // back
|
||||
19 => &[19], // tabard
|
||||
13 => &[16, 17], // one-hand
|
||||
21 => &[16], // main hand
|
||||
14 | 22 | 23 => &[17], // shield / off-hand weapon / held
|
||||
17 => &[16, 17], // two-hand (displaces both hands)
|
||||
1 => &[1], // head
|
||||
2 => &[2], // neck
|
||||
3 => &[3], // shoulder
|
||||
4 => &[4], // shirt
|
||||
5 | 20 => &[5], // chest / robe
|
||||
6 => &[6], // waist
|
||||
7 => &[7], // legs
|
||||
8 => &[8], // feet
|
||||
9 => &[9], // wrist
|
||||
10 => &[10], // hands
|
||||
11 => &[11, 12], // finger
|
||||
12 => &[13, 14], // trinket
|
||||
16 => &[15], // back
|
||||
19 => &[19], // tabard
|
||||
13 => &[16, 17], // one-hand
|
||||
21 => &[16], // main hand
|
||||
14 | 22 | 23 => &[17], // shield / off-hand weapon / held
|
||||
// Two-hand: **main hand ONLY**, which is not the obvious answer. A 2H displaces both
|
||||
// hands when equipped, and this table was written `[16, 17]` on that reasoning. The
|
||||
// reference's own mask disagrees: `0x809200[17]` is `0x8000`, bit 15, main hand alone
|
||||
// (wow-re `merchant-compare-item-law.md` §3). It is a COMPARE-CANDIDATE table, not an
|
||||
// occupancy one — the question it answers is "which worn item is this offering to
|
||||
// replace", and for a two-hander that is the weapon in your main hand.
|
||||
17 => &[16],
|
||||
15 | 25 | 26 | 28 => &[18], // bow / thrown / wand-gun / relic
|
||||
_ => &[],
|
||||
}
|
||||
@@ -677,6 +683,143 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GameTooltip:SetMerchantCompareItem(index [, offset]) — `0x536080`, GameTooltip method-table
|
||||
// entry 43 (wow-re `system/ui/scratch/merchant-compare-item-law.md`). The shopping tooltip the
|
||||
// stock vendor row raises beside the main one: "here is what you are wearing that this would
|
||||
// replace".
|
||||
//
|
||||
// **`offset` is not a slot.** It is the 1-based ordinal among CANDIDATE slots, and a slot is a
|
||||
// candidate only when all three of these hold — the reference's own conjunction:
|
||||
//
|
||||
// 1. its bit is set in `0x809200[InventoryType]` (our [`equip_slots_for`]),
|
||||
// 2. the slot is OCCUPIED,
|
||||
// 3. the equipped item's **class** equals the vendor item's class. Class only — subclass is
|
||||
// never read.
|
||||
//
|
||||
// Empty slots are skipped **without** decrementing, so offset 1 finds the first occupied
|
||||
// matching slot wherever it sits. The consequences are the reference's and they are not
|
||||
// obvious: a shield in the off hand makes offset 2 nil for a one-hand weapon (ARMOR ≠ WEAPON),
|
||||
// and an equipped wand DOES compare against a bow on the shelf (both class 2, both bit 17).
|
||||
// Any type with one candidate slot answers nil for offset 2, which is what makes stock's
|
||||
// second `if` cheap.
|
||||
//
|
||||
// **The return contract decides ghost tooltips versus none**, so it is transcribed exactly:
|
||||
// ONE Lua value on every non-raising path — the NUMBER 1 on success (`lua_pushnumber`, not a
|
||||
// boolean), `nil` on every failure. A bad `this` or a non-number index RAISES; a missing
|
||||
// `offset` does not, and defaults to 1. An explicit `offset` of 0 or less is nil, because the
|
||||
// counter starts below zero and only ever decrements.
|
||||
//
|
||||
// An uncached vendor template answers nil AND fires the query ([`view_of`] does both), so the
|
||||
// first hover of an unseen vendor item shows no shopping tooltip and a later one does. That is
|
||||
// the reference's behaviour, not a race we lost.
|
||||
m.set(
|
||||
"SetMerchantCompareItem",
|
||||
lua.create_function(
|
||||
|lua, (this, index, offset): (Table, Value, Option<Value>)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let as_number = |v: &Value| -> Option<f64> {
|
||||
match v {
|
||||
Value::Integer(i) => Some(*i as f64),
|
||||
Value::Number(n) => Some(*n),
|
||||
// `lua_isnumber` accepts a numeric string, here as everywhere.
|
||||
Value::String(s) => s.to_str().ok().and_then(|s| s.parse::<f64>().ok()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
// The one argument whose absence is an ERROR, with the client's own usage text.
|
||||
let Some(idx) = as_number(&index) else {
|
||||
return Err(mlua::Error::runtime(
|
||||
"Usage: SetMerchantCompareItem(\"slot\" [, offset])",
|
||||
));
|
||||
};
|
||||
// The reference re-bases the index in f64 and THEN truncates toward zero, so 2.7
|
||||
// is row 1 (1.7 → 1) rather than row 2. Doing it the other way round differs by
|
||||
// one for every fractional index.
|
||||
let row = (idx - 1.0).trunc();
|
||||
if row.is_nan() || row < 0.0 || row > f64::from(u32::MAX) {
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
// `offset` re-bases as an INTEGER, and its absence means 1.
|
||||
let mut left = match offset.as_ref().and_then(as_number) {
|
||||
Some(n) if n.is_nan() => return Ok(Value::Nil),
|
||||
Some(n) => n.trunc() - 1.0,
|
||||
None => 0.0,
|
||||
};
|
||||
if left < 0.0 {
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
let row = row as usize;
|
||||
let Some(item_id) = ({
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model
|
||||
.merchant
|
||||
.as_ref()
|
||||
.and_then(|m| m.items.get(row))
|
||||
.map(|it| it.item_id)
|
||||
}) else {
|
||||
return Ok(Value::Nil);
|
||||
};
|
||||
// The vendor item's own template — the class this compares by, and the miss that
|
||||
// fires the query.
|
||||
let Some(offered) = view_of(lua, item_id) else {
|
||||
return Ok(Value::Nil);
|
||||
};
|
||||
|
||||
let found = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let mut hit = None;
|
||||
for &slot in equip_slots_for(offered.inventory_type) {
|
||||
let Some(worn) = model
|
||||
.inv_slot("player", slot as usize)
|
||||
.filter(|s| s.item_id != 0)
|
||||
else {
|
||||
continue; // empty: skipped WITHOUT decrementing
|
||||
};
|
||||
let same_class = model
|
||||
.item_templates
|
||||
.get(&worn.item_id)
|
||||
.is_some_and(|w| w.class == offered.class);
|
||||
if !same_class {
|
||||
continue;
|
||||
}
|
||||
if left <= 0.0 {
|
||||
hit = Some(slot as usize);
|
||||
break;
|
||||
}
|
||||
left -= 1.0;
|
||||
}
|
||||
hit
|
||||
};
|
||||
let Some(slot) = found else {
|
||||
// A failing call does NOT clear the tooltip — the reference's nil exit pushes
|
||||
// nil and touches nothing else.
|
||||
return Ok(Value::Nil);
|
||||
};
|
||||
|
||||
// …and the fill is the ordinary EQUIPPED-item tooltip with the compare header
|
||||
// on: the FULL body, because compact mode is off (`p4 = 0`) and there is no delta
|
||||
// arithmetic anywhere in the client — it draws two whole tooltips side by side —
|
||||
// plus one gray `CURRENTLY_EQUIPPED` line first (`p5 = 1`).
|
||||
//
|
||||
// Reached by arming the same `compare_armed` latch the shift-compare path uses and
|
||||
// running `SetInventoryItem`, which is what the reference does too: `0x536080`
|
||||
// calls the ordinary item builder `0x52b650` with the header flag set, not a
|
||||
// second builder of its own.
|
||||
{
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if let Ok(t) = super::tooltip::tip_mut(&mut model, h) {
|
||||
t.compare_armed = true;
|
||||
}
|
||||
}
|
||||
this.get::<mlua::Function>("SetInventoryItem")?
|
||||
.call::<mlua::MultiValue>((this.clone(), "player", slot))?;
|
||||
Ok(Value::Integer(1))
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// GameTooltip:SetMerchantItem(index) / SetBuybackItem(index) — template-id sources through
|
||||
// the same store (the real SetMerchantItem is the template path — tooltip-money.md); the
|
||||
// merchant feed's own stat head is the fallback while the template answer is in flight, so
|
||||
|
||||
Reference in New Issue
Block a user