Files
benilla-octo/crates/benilla/assets/ui/GossipFrame.xml
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

402 lines
26 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 `BenillaGossipFrame`, its panel registration
(UiPanels.xml's UIPanelWindows["BenillaGossipFrame"], 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 `BenillaGossipRow1..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 skip the ref's
per-row `GossipResize` auto-height (ref l.130-132, `SetHeight(GetTextHeight()+2)`) and its
"too many quests" message() (ref l.67-69/91-93/115-117): no word-wrap engine to measure against
yet (same gap the greeting text already lives with), and the shared 8-row cap below silently
drops the overflow exactly like the option rows already did before this change.
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.
Real-window features we OMIT (no feed, or no scroll/wrap engine yet):
the scroll frame itself (ref l.223-436 — our short greeting needs no scrolling),
and the BotLeftPatch material art. We keep the real geometry of everything we DO ship — including
the quest-row section now (see below).
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 BenillaGossipRow1..8 buttons (ref's single
-- GossipTitleButton pool, l.24-29/63-128) — mirrors the option-only cap this already was.
BENILLA_GOSSIP_MAX = 8
-- 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).
BENILLA_GOSSIP_ICONS = {
gossip = "Interface\\GossipFrame\\GossipGossipIcon",
vendor = "Interface\\GossipFrame\\VendorGossipIcon",
taxi = "Interface\\GossipFrame\\TaxiGossipIcon",
trainer = "Interface\\GossipFrame\\TrainerGossipIcon",
binder = "Interface\\GossipFrame\\BinderGossipIcon",
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
-- 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 == "quest" then
SelectGossipQuest(button.itemIndex)
else
SelectGossipOption(button.itemIndex)
end
end
function BenillaGossipFrame_Update()
getglobal("BenillaGossipGreetingText"):SetText(GetGossipText() or "")
local numQuests = GetNumGossipQuests()
-- 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("BenillaGossipRow" .. i)
if i <= numQuests then
-- Quest rows fill first (ref l.27-28: available then active, already ordered by
-- the app before it reaches GetGossipQuestInfo) — the row's OWN 1-based position
-- IS its quest-list index, so no separate local counter is needed.
local title, active = GetGossipQuestInfo(i)
button.kind = "quest"
button.itemIndex = i
button.label:SetText(title)
button.icon:SetTexture(active 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)
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
end
function BenillaGossipFrame_OnLoad(frame)
-- 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("BenillaGossipNpcNameText"):SetText(name or "")
end
function BenillaGossipFrame_OnEvent(frame)
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("BenillaGossipFrame"))
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).
-->
<Button name="BenillaGossipRowTemplate" 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="BenillaGossipFrame" 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>
<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>
<!-- Ref GossipFrame.xml:191-202: 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>
<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="BenillaGossipNpcNameText" inherits="GameFontHighlight" justifyH="CENTER">
<Anchors>
<Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-23"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Greeting body: the real GossipGreetingText (ref l.241-252) — QuestFont, LEFT, its
width pinned to the real 270 so word-wrap engages once that engine lands (line
breaks today come from the wire's $B macro → \n). Positioned at the real scroll
frame's text origin: window TOPLEFT +(23,-81) (scroll frame, ref l.230) then
+(10,-10) inside the scroll child (ref l.248) = (33,-91). -->
<FontString name="BenillaGossipGreetingText" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="33" y="-91"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The shared row pool: the real GossipTitleButtonTemplate stack (ref l.256-309), now
serving quest rows AND option rows off the SAME 8 buttons (decision 0088 §3 — the
ref's own shape, ref l.24-29/63-128: quest rows fill first, options after). Row 1
anchors to the GREETING FONTSTRING's BOTTOMLEFT +(-10,-20) — the real anchor verbatim
(ref l.258-261): the greeting's height is its measured wrapped text (the engine's
measure round-trip), so a 3-line greeting pushes the rows down exactly as the real
client does (the fixed-offset bridge used before overprinted multi-line greetings —
caught by the ui-gossip capture fixture). The rest stack on the prior row's BOTTOMLEFT
(ref l.267 — no gap; the real 16px rows touch) — since it's one static anchor chain
over one shared pool, however many quest rows BenillaGossipFrame_Update shows this
repaint, the option rows that follow land right below them with no runtime
repositioning (decision 0088 §2 flags runtime SetPoint as NOT propagating to
dependents on this engine; a static XML chain has no such issue — verified by the
layout assertions in the engine test below). -->
<Button name="BenillaGossipRow1" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipGreetingText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-10" y="-20"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<Button name="BenillaGossipRow2" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow1" relativePoint="BOTTOMLEFT"/></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<Button name="BenillaGossipRow3" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow2" relativePoint="BOTTOMLEFT"/></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<Button name="BenillaGossipRow4" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow3" relativePoint="BOTTOMLEFT"/></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<Button name="BenillaGossipRow5" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow4" relativePoint="BOTTOMLEFT"/></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<Button name="BenillaGossipRow6" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow5" relativePoint="BOTTOMLEFT"/></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<Button name="BenillaGossipRow7" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow6" relativePoint="BOTTOMLEFT"/></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<Button name="BenillaGossipRow8" inherits="BenillaGossipRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaGossipRow7" relativePoint="BOTTOMLEFT"/></Anchors>
<Scripts><OnLoad>BenillaGossipRow_OnLoad(self)</OnLoad></Scripts>
</Button>
<!-- 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="BenillaGossipCloseButton">
<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). -->
<Button name="BenillaGossipGoodbyeButton">
<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>
</Frames>
<Scripts>
<OnLoad>BenillaGossipFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaGossipFrame_OnEvent(self)</OnEvent>
<!-- 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>