every dialog is the reference's own, and the trainer loads on demand

- every dialog is the reference's own: `StaticPopup.xml` + `StaticPopup.lua` load off the player's chain, and 0308's engine with its 37 dialogs leaves `UiPanels.xml`; all 76 of the reference's dialogs are the stock table's, 28 of which no file of ours had ever registered (the resurrect family among them); the nine hidden listener frames that existed only because benilla once had no `UIParent_OnEvent` come home to it verbatim from UIParent.lua, and the seven files that were nothing but a dialog and its driver are deleted; what the swap surfaced: the reference capitalises the countdown's unit word ("5 Minutes until release") where ours had lowercased it, and ESC in the typed delete confirm's box is swallowed — the entry names no escape handler; arms for events this engine never fires (PLAYER_SKINNED, TRADE_REQUEST) wait on their feeds rather than being armed blind (1960)
- the social window and the unit menus are the reference's own files: `FriendsFrame.xml` loads off the chain and our transcription is deleted — its twelve dialogs re-homed, `FRIENDLIST_SHOW` fired as the answer to an explicit `ShowFriends()`, and `Get`/`SetLookingForGroup` a client-side flag until their bodies are carved; `UnitPopup.xml` loads off the chain, our row catalogue and its driver frame go, and nine verbs are built for it — `HasFullControl` off the player-control flag the reference reads at `0x51a158`, `UnitPlayerOrPetInParty`/`InRaid` over a new unit owner, and the six pet one-shots, each pressing the bar's own reaction or command slot word through the same latch and `CMSG_PET_ACTION`, confirmed by wow-re's carve; the Guild Leave/Promote confirms 1958 believed nothing registered were our social file's all along, and the duplicate went with it (1958, 1959)
- the trainer loads on demand: our `TrainerFrame.xml` is deleted and no row replaces it — `Blizzard_TrainerUI` loads off the player's own chain through the reference's `ClassTrainerFrame_LoadUI`, and the LoadOnDemand `Blizzard_*` addons are registry rows read off the chain; the timing IS the mechanism: the list builder writes the state-filter mask to 3 on every `SMSG_TRAINER_LIST` (`0x4d75d9`) and the stock addon pushes its saved filters exactly once, on its own ADDON_LOADED — which fires on the session's first trainer, after that packet, so the saved filters win that open and an eager load would never have applied them; the loader holds an anchor to a sibling not built yet and applies it once the subtree exists; and `UNIT_FLAGS` fires only on a change between two snapshots — wow-re's trio found the per-field watch bridge runs no notify pass on the create leg, correcting 1953 (1957)
- the additive window, built and withdrawn the same hour: under `BLEND_ADD` every additive particle draw commutes with every other, so same-key additive draws can be regrouped exactly — built in the effect lane's merge walk and measured on the crowd rig (Stormwind, forty partybots): the adjacency merge already folds about half the effect items and the regroup found two more folds in a frame of ninety-three, so the pre-regroup walk is what runs; what stays: an `fx=` items/merged census on the FPS probe line, a `DRESS_PART` line per drawn part of the self body on the dress census, and the regression the A/B uncovered: 1948's stock chat migration dropped the drain's plain-line sender, so every `WOW_PROBE_CHAT` rig — the crowd rig's forty `.partybot add`s among them — had gone silent on the server; a probe's slash-less line reaches the wire as SAY again (1955)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:57:24 +07:00
co-authored by Claude Fable 5.1
parent a43e2c9a88
commit 404708859a
116 changed files with 2215 additions and 8202 deletions
@@ -1,69 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The innkeeper's "make this your home?" dialog (decision 1331): a registry entry on the
shared StaticPopup engine (UiPanels.xml) plus its UIParent event arm, written against the
patch-chain FrameXML (mpqx extraction, decision 0246) — StaticPopup.lua l.1308-1322 (the
entry) and UIParent.lua l.125 / l.547-548 (the registration and the show arm). The string
is GlobalStrings.lua l.840, verbatim.
It is NOT part of GossipFrame.xml, and the reason is the wire: selecting the innkeeper's
gossip line makes the server CLOSE the gossip menu and then ask. By the time this dialog
goes up the window that raised it is already gone, so it belongs with the other free-
standing confirms rather than with the panel.
The whole flow behind it is two packets — SMSG_BINDER_CONFIRM in, CMSG_BINDER_ACTIVATE out
— and only the second one binds anything (`crate::ui_binder`'s header carries the law).
Accepting is therefore the ONLY path to a moved hearthstone; there is no client-side bind
and nothing to undo on Cancel, which is why this entry has no OnCancel.
-->
<Script><![CDATA[
-- ── Strings (GlobalStrings.lua, verbatim) ───────────────────────────────────────────────
local CONFIRM_BINDER = "Do you want to make %s your new home?" -- l.840
local ACCEPT = "Accept" -- l.10
local CANCEL = "Cancel" -- l.464
-- ── The confirm — answering to StaticPopup.lua l.1308-1322 ─────────────────────────────
-- This is the dialog's ONLY teardown, and it is a poll rather than an event: walk out of
-- the innkeeper's range and the question leaves the screen by itself, with no packet
-- either way. `CheckBinderDist()` is the engine binding behind it
-- (`benilla-ui/src/script/binder.rs`), false as soon as the app's NPC-session guard has
-- closed the question. Named rather than inlined into the entry, because "the question
-- expires when you walk away" is the interesting half of this file.
local function hideOnceOutOfRange(_, dialog)
if not CheckBinderDist() then
dialog:Hide()
end
end
StaticPopupDialogs["CONFIRM_BINDER"] = {
text = CONFIRM_BINDER,
button1 = ACCEPT,
button2 = CANCEL,
OnAccept = function()
ConfirmBinder()
end,
OnUpdate = hideOnceOutOfRange,
timeout = 0,
hideOnEscape = 1,
}
-- ── The UIParent event arm (UIParent.lua l.547-548) ────────────────────────────────────
-- arg1 is the area name the "%s" takes.
function BenillaBinderConfirmDriver_OnEvent()
if event == "CONFIRM_BINDER" then
StaticPopup_Show("CONFIRM_BINDER", arg1)
end
end
]]></Script>
<Frame name="BenillaBinderConfirmDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("CONFIRM_BINDER")
</OnLoad>
<OnEvent>BenillaBinderConfirmDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -1,93 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The "someone wants to summon you" dialog (decision 1747): a registry entry on the shared
StaticPopup engine (UiPanels.xml) plus its UIParent event arm, written against the
patch-chain FrameXML (mpqx extraction, decision 0246) — StaticPopup.lua l.1336-1357 (the
entry) and UIParent.lua l.126 / l.551-552 (the registration and the show arm). The string is
GlobalStrings.lua l.852, verbatim.
It belongs to no window, like the duel and binder confirms beside it: a warlock's Ritual of
Summoning, a meeting stone and a GM's `.summon` all arrive as the same SMSG_SUMMON_REQUEST
with nothing on screen.
**The event carries NO arguments** — unlike every other confirm here. The engine keeps the
request in four globals and the dialog reads them back through GetSummonConfirmSummoner(),
GetSummonConfirmAreaName() and GetSummonConfirmTimeLeft() (`crate::ui_summon`'s header
carries the byte pin). That is also why this entry's text starts BLANK: it is one of the
engine's countdown dialogs, so StaticPopup_Show writes " " and the OnUpdate tick composes
the real line every frame from those getters plus the remaining time.
**Accepting is the only packet in the flow.** There is no decline opcode in 1.12 and no
CancelSummon binding, so Cancel and ESC send nothing and the offer simply expires on the
server's own two-minute timer — which is also what the countdown running out means. That is
why this entry, like CONFIRM_BINDER, has no OnCancel.
-->
<Script><![CDATA[
-- ── Strings (GlobalStrings.lua, verbatim) ───────────────────────────────────────────────
-- %s summoner · %s area · %d count · %s the count's unit word (StaticPopupTimeUnit).
local CONFIRM_SUMMON =
"%s wants to summon you to %s. The spell will be cancelled in %d %s." -- l.852
local ACCEPT = "Accept" -- l.11
local CANCEL = "Cancel" -- l.465
-- ── The confirm — answering to StaticPopup.lua l.1336-1357 ─────────────────────────────
-- OnShow seeds `timeleft` from the engine, which is what arms the popup engine's countdown
-- branch; from there the dialog ticks it down locally and hides itself at zero, sending
-- nothing. GetSummonConfirmTimeLeft() returns whole seconds, already truncated by the
-- binding (`0x48b660`), so this is the reference's own resolution and not a rounding of ours.
--
-- The OnUpdate is a COMBAT LOCK, not a teardown: you may not accept a summon while
-- fighting, so Button1 disables and re-enables while the dialog stays up. Every other
-- confirm in this folder polls to decide whether to *hide*; this one polls to decide
-- whether the answer is available. Cancel stays live throughout.
local function lockAcceptWhileInCombat(_, dialog)
local button = getglobal(dialog:GetName() .. "Button1")
if UnitAffectingCombat("player") then
button:Disable()
else
button:Enable()
end
end
StaticPopupDialogs["CONFIRM_SUMMON"] = {
text = CONFIRM_SUMMON,
button1 = ACCEPT,
button2 = CANCEL,
OnShow = function()
this.timeleft = GetSummonConfirmTimeLeft()
end,
OnAccept = function()
ConfirmSummon()
end,
OnUpdate = lockAcceptWhileInCombat,
timeout = 0,
-- A summon is worth interrupting a fly-by for: without this the whole question would be
-- cancelled unseen by StaticPopup_Show's cinematic gate, and the offer would expire
-- while the player watched a cutscene.
interruptCinematic = 1,
-- v1-inert in our engine (UiPanels.xml's header lists it) — transcribed because it is
-- part of the entry an addon may read.
notClosableByLogout = 1,
hideOnEscape = 1,
}
-- ── The UIParent event arm (UIParent.lua l.551-552) ────────────────────────────────────
-- No arguments: the getters above are the payload.
function BenillaConfirmSummonDriver_OnEvent()
if event == "CONFIRM_SUMMON" then
StaticPopup_Show("CONFIRM_SUMMON")
end
end
]]></Script>
<Frame name="BenillaConfirmSummonDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("CONFIRM_SUMMON")
</OnLoad>
<OnEvent>BenillaConfirmSummonDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
-310
View File
@@ -1,310 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The death arc's UI (decision 0308 slices 1/3, plus 1746's self-resurrect): our death
dialogs, registered on the shared StaticPopup engine (UiPanels.xml), plus the event driver
that raises and clears them.
The patch-chain FrameXML (mpqx extraction, decision 0246) is the SPECIFICATION for what
each dialog says, offers and answers — StaticPopup.lua l.375-505 (DEATH + RESURRECT×3),
l.1134-1201 (XP_LOSS×2 + RECOVER_CORPSE*), UIParent.lua l.237-262 + l.399-436 (the event
arms). The field names below are the published registry API that addons write into, so
they are the reference's and cannot be renamed; the code that fills them is ours. Six of
the eight dialogs share one disposition and are stamped with it rather than repeating it,
the three resurrect offers and the two spirit-healer confirms are each one shape built
more than once, and the driver is a single event switch. Strings are the reference's own
(GlobalStrings.lua), which is data, not authorship.
The engine-side events feeding this: PLAYER_DEAD/ALIVE/UNGHOST +
RESURRECT_REQUEST + CONFIRM_XP_LOSS fire from `death.rs`'s state machine;
CORPSE_IN_RANGE/OUT_OF_RANGE/IN_INSTANCE join with the slice-2 corpse-run range gate
(registered now — they simply don't fire yet). Deferred with 0308 §8: the SKINNED/
SKINNED_REPOP pair (battleground insignia).
-->
<Script><![CDATA[
-- ── Strings (GlobalStrings.lua, verbatim; %% escapes as shipped) ─────────────────────────
local DEATH_RELEASE_TIMER = "%d %s until release"
local DEATH_RELEASE_NOTIMER = "You have died. Release to the nearest graveyard?"
local DEATH_RELEASE = "Release Spirit"
-- Vestigial on this build, and kept anyway because it is what the reference registers:
-- HasSoulstone() returns a Spell.dbc NAME whenever it returns anything, and OnShow stamps
-- that over button2 — so the only case this default could render is one that cannot
-- happen (DisplayButton2 shows the button on exactly the same call). It is the pre-1.6
-- wording, from before "Self-resurrection spells show their name on the button" (decision
-- 1746); GlobalStrings still ships it with the comment "Use your soulstone if you have one".
local USE_SOULSTONE = "Reincarnate"
local RECOVER_CORPSE_TEXT = "Resurrect now?"
local RECOVER_CORPSE_TIMER = "%d %s until resurrection"
local RECOVER_CORPSE_INSTANCE_TEXT = "You must enter the instance to recover your corpse"
local RESURRECT_REQUEST_TEXT = "%s wants to resurrect you. You will be afflicted with resurrection sickness."
local RESURRECT_REQUEST_TIMER = "%s wants to resurrect you and will be able to in %d %s. You will be afflicted with resurrection sickness."
local RESURRECT_REQUEST_NO_SICKNESS = "%s wants to resurrect you"
local RESURRECT_REQUEST_NO_SICKNESS_TIMER = "%s wants to resurrect you and will be able to in %d %s"
local CONFIRM_XP_LOSS = "If you find your corpse, you can resurrect for no penalty. If I resurrect you all of your items will take 25%% durability damage (equipped and inventory) and you will be afflicted by %s of Resurrection Sickness."
local CONFIRM_XP_LOSS_AGAIN = "Remember, if you find your corpse there is no penalty. Are you sure you want to incur %s of Resurrection Sickness and have all your items take 25%% durability damage?"
local CONFIRM_XP_LOSS_NO_SICKNESS = "If you find your corpse, you can resurrect for no penalty. If I resurrect you all of your items will take 25%% durability damage (equipped and inventory)."
local CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS = "Remember, if you find your corpse there is no penalty. Are you sure you want to have all your items take 25% durability damage?"
local ACCEPT = "Accept"
local DECLINE = "Decline"
local CANCEL = "Cancel"
-- ── The corpse-side disposition ──────────────────────────────────────────────────────────
-- Six of these eight dialogs are about a body on the floor, and every one of them wants
-- the same three things from the engine: it must be legal to raise while you are dead
-- (`whileDead` — that is the whole point of the arc), it takes precedence over a running
-- cinematic, and a logout must not quietly close it out from under you. Stamped on here
-- instead of retyped six times; the fields still land on the entry, so an addon reading
-- StaticPopupDialogs["DEATH"].whileDead sees exactly what the reference's does.
local function register_corpse_dialog(which, entry)
entry.whileDead = 1
entry.interruptCinematic = 1
entry.notClosableByLogout = 1
StaticPopupDialogs[which] = entry
end
-- How long a resurrect offer stands once it becomes answerable. It is both the engine's
-- own countdown (`timeout`) and the value OnShow re-seeds past the recovery delay.
local RESURRECT_OFFER_SECONDS = 60
-- ── The release dialog (spec: StaticPopup.lua l.375-414) ─────────────────────────────────
-- OnShow seeds `timeleft` from GetReleaseTimeRemaining(), which the engine renders through
-- its per-tick DEATH text; −1 means this map has no forced release (an instance), so the
-- static wording goes in and nothing counts down. `timeout = 0` because the countdown is
-- the SERVER's to enforce — it force-releases at 6:00 (0308 §1) — and there is deliberately
-- no hideOnEscape: you cannot dismiss dying. Button2 appears only when the server owes us
-- a self-resurrect (PLAYER_SELF_RES_SPELL non-zero); without one a cancel falls through
-- to the release.
register_corpse_dialog("DEATH", {
text = DEATH_RELEASE_TIMER,
button1 = DEATH_RELEASE,
button2 = USE_SOULSTONE,
DisplayButton2 = function()
return HasSoulstone()
end,
OnShow = function()
local name = this:GetName()
this.timeleft = GetReleaseTimeRemaining()
if this.timeleft == -1 then
getglobal(name .. "Text"):SetText(DEATH_RELEASE_NOTIMER)
end
-- The self-res names its own button, out of Spell.dbc: "Use Soulstone" (3026,
-- 20758-20761), "Reincarnation" (21169), "Twisting Nether" (23700).
local soulstone = HasSoulstone()
if soulstone then
getglobal(name .. "Button2"):SetText(soulstone)
end
end,
OnAccept = function()
RepopMe()
end,
OnCancel = function(data, reason)
-- Only a real click on button2 is an answer. "override" and "timeout" are the
-- engine taking the dialog away, and neither is the player choosing anything.
if reason ~= "clicked" then
return
end
-- Button2 is the self-resurrect (decision 1746): CMSG_SELF_RES, and the server
-- casts whatever PLAYER_SELF_RES_SPELL holds. Re-asked rather than remembered
-- from OnShow, exactly as the reference does — the field is the server's and can
-- have been spent since the dialog opened.
if HasSoulstone() then
UseSoulstone()
else
RepopMe()
end
end,
timeout = 0,
cancels = "RECOVER_CORPSE",
})
-- ── At the corpse (spec: StaticPopup.lua l.1182-1202) — slice 2's range gate raises these.
-- Accept is gated behind the recovery delay and, once taken, the dialog STAYS: reclaiming
-- is the server's to confirm, and it does so with the descriptor deltas that stand you up.
register_corpse_dialog("RECOVER_CORPSE", {
StartDelay = GetCorpseRecoveryDelay,
delayText = RECOVER_CORPSE_TIMER,
text = RECOVER_CORPSE_TEXT,
button1 = ACCEPT,
OnAccept = function()
RetrieveCorpse()
return 1
end,
timeout = 0,
})
-- Your body is inside a dungeon you are standing outside of: a notice, not an offer, so
-- it carries no button at all.
register_corpse_dialog("RECOVER_CORPSE_INSTANCE", {
text = RECOVER_CORPSE_INSTANCE_TEXT,
timeout = 0,
})
-- ── Somebody else's offer (spec: StaticPopup.lua l.416-505) ──────────────────────────────
-- Three registry keys, one shape: with sickness, without, and without a recovery timer to
-- wait out. They differ only in the wording and in whether the offer is delay-gated, so
-- they are built rather than written three times.
--
-- Refusing an offer (or letting it lapse) has to undo the `cancels = "DEATH"` that raising
-- it did — otherwise a declined resurrect leaves you dead with nothing on screen to
-- release from.
local function restore_release_dialog()
if UnitIsDead("player") then
StaticPopup_Show("DEATH")
end
end
local function resurrect_offer(offer_text, delay_text)
return {
text = offer_text,
button1 = ACCEPT,
button2 = DECLINE,
OnAccept = function()
AcceptResurrect()
end,
OnCancel = function()
DeclineResurrect()
restore_release_dialog()
end,
-- The countdown. Given a `delay_text` the offer is gated: Accept stays dead and
-- that text counts the corpse-recovery cooldown down; without one (the no-timer
-- variant) Accept is live at once. Either way the offer lapses a minute after it
-- becomes answerable, which is why OnShow re-seeds `timeleft` past the gate
-- instead of leaving `timeout`'s minute to start the moment it appears.
StartDelay = delay_text and GetCorpseRecoveryDelay or nil,
delayText = delay_text,
OnShow = function()
this.timeleft = GetCorpseRecoveryDelay() + RESURRECT_OFFER_SECONDS
end,
timeout = RESURRECT_OFFER_SECONDS,
cancels = "DEATH",
hideOnEscape = 1,
}
end
register_corpse_dialog("RESURRECT",
resurrect_offer(RESURRECT_REQUEST_TEXT, RESURRECT_REQUEST_TIMER))
register_corpse_dialog("RESURRECT_NO_SICKNESS",
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS, RESURRECT_REQUEST_NO_SICKNESS_TIMER))
register_corpse_dialog("RESURRECT_NO_TIMER",
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS))
-- ── The spirit healer's price (spec: StaticPopup.lua l.1134-1180) ────────────────────────
-- Both variants ask twice. The first Accept rewrites the dialog to a blunter second ask
-- and clears `data`; the truthy return keeps it open. The second Accept — now with no data
-- — sends the answer. So `data` is at once the payload and the "which ask is this" flag,
-- which is why the driver below hands the no-sickness variant a bare 1.
--
-- The second ask is a FUNCTION, not a string, because only one of the two has anything to
-- substitute: the no-sickness wording carries a bare "25% durability" that format() would
-- read as a conversion and choke on.
--
-- Neither offer outlives walking away from the healer, so both poll the distance.
local function close_when_out_of_reach(elapsed, dialog)
if not CheckSpiritHealerDist() then
dialog:Hide()
end
end
local function spirit_healer_confirm(first_ask, second_ask)
return {
text = first_ask,
button1 = ACCEPT,
button2 = CANCEL,
OnAccept = function(data)
if not data then
AcceptXPLoss()
return
end
-- `this` is the button the engine clicked; the dialog is its parent.
local dialog = this:GetParent()
getglobal(dialog:GetName() .. "Text"):SetText(second_ask(data))
dialog.data = nil
return 1
end,
OnUpdate = close_when_out_of_reach,
timeout = 0,
exclusive = 1,
whileDead = 1,
showAlert = 1,
hideOnEscape = 1,
}
end
StaticPopupDialogs["XP_LOSS"] = spirit_healer_confirm(CONFIRM_XP_LOSS, function(sickness)
return format(CONFIRM_XP_LOSS_AGAIN, sickness)
end)
StaticPopupDialogs["XP_LOSS_NO_SICKNESS"] = spirit_healer_confirm(
CONFIRM_XP_LOSS_NO_SICKNESS,
function()
return CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS
end)
-- ── The driver (spec: UIParent.lua l.237-262 + l.399-436) ───────────────────────────────
-- One switch over the arc's events. The reference spreads these arms through the whole of
-- UIParent_OnEvent; ours has a frame of its own (below) and nothing else to share it with.
function BenillaDeathDriver_OnEvent()
if event == "PLAYER_DEAD" then
if not StaticPopup_Visible("DEATH") then
CloseAllWindows(1)
if GetReleaseTimeRemaining() > 0 or GetReleaseTimeRemaining() == -1 then
StaticPopup_Show("DEATH")
end
end
elseif event == "PLAYER_ALIVE" then
StaticPopup_Hide("DEATH")
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
elseif event == "PLAYER_UNGHOST" then
StaticPopup_Hide("RESURRECT")
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
StaticPopup_Hide("RESURRECT_NO_TIMER")
elseif event == "RESURRECT_REQUEST" then
if ResurrectHasSickness() then
StaticPopup_Show("RESURRECT", arg1)
elseif ResurrectHasTimer() then
StaticPopup_Show("RESURRECT_NO_SICKNESS", arg1)
else
StaticPopup_Show("RESURRECT_NO_TIMER", arg1)
end
elseif event == "CONFIRM_XP_LOSS" then
-- If the healer would charge resurrection sickness, its duration goes into the
-- text; if not there is nothing to quote, and the bare 1 handed over instead is
-- only the "this is still the first ask" marker the two-step above reads.
local sickness = GetResSicknessDuration()
local dialog
if sickness then
dialog = StaticPopup_Show("XP_LOSS", sickness)
else
dialog = StaticPopup_Show("XP_LOSS_NO_SICKNESS")
end
if dialog then
dialog.data = sickness or 1
end
-- The confirm covers the healer's gossip window (spec: UIParent.lua l.412).
local gossip = getglobal("GossipFrame")
if gossip and gossip:IsVisible() then
HideUIPanel(gossip)
end
elseif event == "CORPSE_IN_RANGE" then
StaticPopup_Show("RECOVER_CORPSE")
elseif event == "CORPSE_IN_INSTANCE" then
StaticPopup_Show("RECOVER_CORPSE_INSTANCE")
elseif event == "CORPSE_OUT_OF_RANGE" then
StaticPopup_Hide("RECOVER_CORPSE")
StaticPopup_Hide("RECOVER_CORPSE_INSTANCE")
StaticPopup_Hide("XP_LOSS")
end
end
]]></Script>
<Frame name="BenillaDeathDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("PLAYER_DEAD")
self:RegisterEvent("PLAYER_ALIVE")
self:RegisterEvent("PLAYER_UNGHOST")
self:RegisterEvent("RESURRECT_REQUEST")
self:RegisterEvent("CONFIRM_XP_LOSS")
self:RegisterEvent("CORPSE_IN_RANGE")
self:RegisterEvent("CORPSE_IN_INSTANCE")
self:RegisterEvent("CORPSE_OUT_OF_RANGE")
</OnLoad>
<OnEvent>BenillaDeathDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -1,94 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The duel UI (decision 0633): the two duel dialogs on our shared StaticPopup engine
(UiPanels.xml) plus the duel-event driver. The patch-chain FrameXML (mpqx extraction,
decision 0246) is the SPECIFICATION — StaticPopup.lua l.1103-1120 (DUEL_REQUESTED +
DUEL_OUTOFBOUNDS), UIParent.lua l.98-101 (the registrations) + l.378-393 (the event arms)
— and what it fixes is contract: the two dialog keys, the StaticPopupDialogs field names,
the sound kit and the two timeouts are all addressed by name, so they are reproduced
exactly while the entries around them are written here in our own shape. The strings are
the shipped enUS values from GlobalStrings.lua, quoted as data.
Duels have no window of their own — these two dialogs ARE the whole duel UI. Everything
else is engine-side: the "Duel starting: N" countdown and the outcome line are
CHAT_MSG_SYSTEM lines `crate::ui_duel` composes (byte-verified: WoW.exe's own duel TU emits
both through the chat path with type 0x0A, not through any frame), and the four events
below are fired from that module's state edges.
The DUEL_OUTOFBOUNDS dialog is text-only — no buttons, timeout 10 — and its text is filled
by the StaticPopup engine's per-tick countdown branch, which already names it in both of
its special-case lists (UiPanels.xml: the show-time " " seeding and the OnUpdate re-format).
The 10 s is the client's dialog timeout, and it coincides with the server's own forfeit
window (`Player::CheckDuelDistance`) rather than driving it: the popup expiring is cosmetic,
the forfeit is the server's.
-->
<Script><![CDATA[
-- ── Strings (the shipped enUS GlobalStrings.lua values, quoted as data) ─────────────────
local DUEL_REQUESTED = "%s has challenged you to a duel." -- l.957
local DUEL_OUTOFBOUNDS_TIMER = "Exiting duel area, you will forfeit in %d %s." -- l.956
local ACCEPT = "Accept"
local DECLINE = "Decline"
-- ── The challenge dialog (spec: StaticPopup.lua l.1103-1116) ────────────────────────────
-- Written scalars-first, handlers-last: what the dialog IS, then what answering it does.
--
-- No accept-guard flag (unlike PARTY_INVITE): the entry declines only on an explicit
-- OnCancel, so a timeout or an override hides it WITHOUT sending anything. That is not an
-- oversight to "fix" — the server's own 60 s is not a thing either; an unanswered challenge
-- simply sits until one side acts.
StaticPopupDialogs["DUEL_REQUESTED"] = {
text = DUEL_REQUESTED,
button1 = ACCEPT,
button2 = DECLINE,
sound = "igPlayerInvite",
timeout = 60,
hideOnEscape = 1,
-- The two buttons are the two replies. ESC arrives here as well — hideOnEscape routes
-- an escape through OnCancel — so backing out and declining are deliberately one path.
OnAccept = function()
AcceptDuel()
end,
OnCancel = function()
CancelDuel()
end,
}
-- ── The out-of-bounds warning (spec: StaticPopup.lua l.1117-1120) ───────────────────────
-- Two fields and nothing else: no buttons, because there is nothing to answer, and no
-- text of its own — the engine's countdown branch writes it every tick.
StaticPopupDialogs["DUEL_OUTOFBOUNDS"] = {
text = DUEL_OUTOFBOUNDS_TIMER,
timeout = 10,
}
-- ── The duel event arms (spec: UIParent.lua l.378-393) ──────────────────────────────────
function BenillaDuelDriver_OnEvent()
if event == "DUEL_REQUESTED" then
StaticPopup_Show("DUEL_REQUESTED", arg1)
elseif event == "DUEL_OUTOFBOUNDS" then
StaticPopup_Show("DUEL_OUTOFBOUNDS")
elseif event == "DUEL_INBOUNDS" then
StaticPopup_Hide("DUEL_OUTOFBOUNDS")
elseif event == "DUEL_FINISHED" then
-- Both, unconditionally: a duel that ends while the challenge popup is still up
-- (the other side cancelled first) must not leave it there.
StaticPopup_Hide("DUEL_REQUESTED")
StaticPopup_Hide("DUEL_OUTOFBOUNDS")
end
end
]]></Script>
<Frame name="BenillaDuelDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("DUEL_REQUESTED")
self:RegisterEvent("DUEL_OUTOFBOUNDS")
self:RegisterEvent("DUEL_INBOUNDS")
self:RegisterEvent("DUEL_FINISHED")
</OnLoad>
<OnEvent>BenillaDuelDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -1,93 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The two enchant-apply confirms (decision 0928): registry entries on the shared StaticPopup
engine (UiPanels.xml) plus their UIParent event arms, transcribed from the patch-chain
FrameXML (mpqx extraction, decision 0246) — StaticPopup.lua l.1240-1251 (BIND_ENCHANT) and
l.1252-1263 (REPLACE_ENCHANT), UIParent.lua l.110-111 (the registrations), l.437-442 (the
show arms) and l.449-453 (the CURRENT_SPELL_CAST_CHANGED hide). Strings verbatim from
GlobalStrings.lua l.415 / l.3324.
They are NOT the Craft window's, which is why they live in their own file and load with the
shared dialogs rather than with CraftFrame.xml: the gate that raises them (`0x495d60`) is
the generic item-target bind, so a rogue applying a poison and a shaman imbuing a mace
reach them without any profession window open at all.
Both are pure client-side gating. There is no CMSG_REPLACE_ENCHANT in 5875 (wow-re's
tradeskill node: no CDataStore put/Send anywhere in `0x48d2e0`/`0x48d300`/`0x495d60`
/`0x6e5b40` outside the shared SendCast) — the popups withhold the *same* cast and their
Yes re-issues it. `BindEnchant()` re-enters the gate with its confirmed flag set, so a Yes
here can raise the replace popup next; `ReplaceEnchant()` skips the gate entirely.
The third sibling, TRADE_REPLACE_ENCHANT (event 404, fired at `0x496230` with a third
numeric arg), belongs to the trade-window enchant path — a different wire shape
(TARGET_FLAG_TRADE_ITEM + the slot sentinel) we do not build yet, so it is not here.
-->
<Script><![CDATA[
-- ── Strings (GlobalStrings.lua, verbatim) ───────────────────────────────────────────────
local BIND_ENCHANT = "Enchanting this item will bind it to you." -- l.415
local REPLACE_ENCHANT = "Do you want to replace \"%s\" with \"%s\"?" -- l.3324
local OKAY = "Okay"
local CANCEL = "Cancel"
local YES = "Yes"
local NO = "No"
-- ── The bind confirm — StaticPopup.lua l.1240-1251, verbatim ───────────────────────────
-- No OnCancel: declining sends nothing and needs no teardown, because the gate returned
-- before BindTarget and the targeting word is still standing. The cursor simply stays up.
StaticPopupDialogs["BIND_ENCHANT"] = {
text = BIND_ENCHANT,
button1 = OKAY,
button2 = CANCEL,
OnAccept = function()
BindEnchant()
end,
timeout = 0,
exclusive = 1,
showAlert = 1,
hideOnEscape = 1,
}
-- ── The replace confirm — StaticPopup.lua l.1252-1263, verbatim ────────────────────────
-- arg1 = the enchant already on the item, arg2 = the one about to replace it, in that
-- order (the fire site pushes old then new: `0x703f50(0x193, "%s%s", old, new)`).
StaticPopupDialogs["REPLACE_ENCHANT"] = {
text = REPLACE_ENCHANT,
button1 = YES,
button2 = NO,
OnAccept = function()
ReplaceEnchant()
end,
timeout = 0,
exclusive = 1,
showAlert = 1,
hideOnEscape = 1,
}
-- ── The UIParent event arms (UIParent.lua l.437-453, verbatim) ─────────────────────────
function BenillaEnchantConfirmDriver_OnEvent()
if event == "BIND_ENCHANT" then
StaticPopup_Show("BIND_ENCHANT")
elseif event == "REPLACE_ENCHANT" then
StaticPopup_Show("REPLACE_ENCHANT", arg1, arg2)
elseif event == "CURRENT_SPELL_CAST_CHANGED" then
-- The pending cast the popups are asking about is gone (cancelled, replaced, or
-- committed), so the question is moot. This is the ref's ONLY teardown for them.
StaticPopup_Hide("BIND_ENCHANT")
StaticPopup_Hide("REPLACE_ENCHANT")
end
end
]]></Script>
<Frame name="BenillaEnchantConfirmDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("BIND_ENCHANT")
self:RegisterEvent("REPLACE_ENCHANT")
self:RegisterEvent("CURRENT_SPELL_CAST_CHANGED")
</OnLoad>
<OnEvent>BenillaEnchantConfirmDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
File diff suppressed because it is too large Load Diff
@@ -1,64 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The escort-quest confirm (decision 1733): one StaticPopup entry on our shared engine
(UiPanels.xml) plus its event driver. The patch-chain FrameXML is the SPECIFICATION —
StaticPopup.lua l.727-737 (the QUEST_ACCEPT entry) and UIParent.lua l.93 + l.353-356 (the
registration and the one-line arm) — and it fixes the dialog key, the field names and the
one verb by name, so those are reproduced exactly.
WHY THIS IS ITS OWN FILE, next to the (stock, since 1944) QuestFrame.xml and QuestLogFrame.xml rather than inside
either. It belongs to neither window. The confirm has no window at all — it is raised by
`crate::ui_quest_share` off `SMSG_QUEST_CONFIRM_ACCEPT`, exactly as DuelFrame.xml's two
dialogs are raised off the duel packets, and answering it sends a packet neither quest
window knows about. Putting it in QuestFrame.xml would say the questgiver panel owns it
(it does not — no panel is open when this arrives) and putting it in QuestLogFrame.xml
would say the log window does (it does not either: the log is closed for most players when
a party member starts an escort).
WHAT RAISES IT. `QUEST_FLAGS_PARTY_ACCEPT` (0x2) quests — escorts. When a party member
accepts one, the server asks every other eligible member whether they want it too
(vmangos `QuestHandler.cpp:172-193`). That is a DIFFERENT flow from the quest-log's *Share
Quest* button, which needs no dialog at all: a pushed quest arrives on the ordinary
questgiver detail panel (QuestFrame.xml) with the sharer's player guid where an NPC's
would be, and Accept/Decline there are the whole answer.
THE ONE ASYMMETRY, and it is the reference's, not ours: Yes calls ConfirmAcceptQuest() and
sends `CMSG_QUEST_CONFIRM_ACCEPT`; **No sends nothing at all**. There is no OnCancel below
because there is no packet for one — the server's pending-share latch is simply cleared by
the next thing that touches it. The same is true of ESC (hideOnEscape).
-->
<Script><![CDATA[
-- The shipped enUS GlobalStrings.lua values, quoted as data (l.3227, and the YES/NO pair).
-- First %s is the party member's name, second is the quest title — the argument order
-- QUEST_ACCEPT_CONFIRM fires with.
local QUEST_ACCEPT = "%s is starting %s\nWould you like to as well?"
StaticPopupDialogs["QUEST_ACCEPT"] = {
text = QUEST_ACCEPT,
button1 = "Yes", -- YES
button2 = "No", -- NO
OnAccept = function()
ConfirmAcceptQuest()
end,
timeout = 0,
exclusive = 1,
hideOnEscape = 1,
}
-- spec: UIParent.lua l.353-356 — one arm, both args straight through.
function BenillaQuestShareDriver_OnEvent()
if event == "QUEST_ACCEPT_CONFIRM" then
StaticPopup_Show("QUEST_ACCEPT", arg1, arg2)
end
end
]]></Script>
<Frame name="BenillaQuestShareDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>self:RegisterEvent("QUEST_ACCEPT_CONFIRM")</OnLoad>
<OnEvent>BenillaQuestShareDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -1,82 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
The class trainer's "unlearn all of your talents?" dialog (decision 1580): a registry entry
on the shared StaticPopup engine (UiPanels.xml) plus its UIParent event arm, written against
the patch-chain FrameXML (mpqx extraction, decision 0246) — StaticPopup.lua l.1289-1304 (the
entry) and UIParent.lua l.123 / l.533-538 (the registration and the show arm). The string is
GlobalStrings.lua l.853, verbatim.
It is BinderConfirm.xml's twin, one line above it in the TOC, and free-standing for the
identical reason: selecting the trainer's "I wish to unlearn my talents" line makes the
server CLOSE the gossip menu and then ask, so the window that raised this dialog is already
gone by the time it appears.
The whole flow behind it is one two-way opcode — MSG_TALENT_WIPE_CONFIRM in with the
trainer's guid and the cost, the same opcode back out with that guid — and only the second
one unlearns anything (`crate::ui_talent_wipe`'s header carries the law). Accepting is
therefore the ONLY path to a reset spec; there is nothing to undo on Cancel, which is why
this entry has no OnCancel.
It is also the first entry in the registry to raise a MONEY frame, because the cost is the
interesting half of the question and the sentence below deliberately does not name it — the
engine shows the coin row for `hasMoneyFrame`, and the arm at the bottom fills it.
-->
<Script><![CDATA[
-- ── Strings (GlobalStrings.lua, verbatim) ───────────────────────────────────────────────
local CONFIRM_TALENT_WIPE =
"Do you want to unlearn all of your talents? The cost will increase each time you do it." -- l.853
local ACCEPT = "Accept" -- l.10
local CANCEL = "Cancel" -- l.464
-- The dialog's only teardown, and like the innkeeper's it is a poll rather than an event:
-- walk away from the trainer and the question leaves the screen by itself, with no packet
-- either way. `CheckTalentMasterDist()` is the engine binding behind it
-- (`benilla-ui/src/script/talent.rs`), false as soon as the app's NPC-session guard has
-- closed the question. Named rather than inlined, because "the question expires when you
-- walk away" is the interesting half of this file.
local function hideOnceOutOfRange(_, dialog)
if not CheckTalentMasterDist() then
dialog:Hide()
end
end
-- ── The confirm — answering to StaticPopup.lua l.1289-1304 ─────────────────────────────
StaticPopupDialogs["CONFIRM_TALENT_WIPE"] = {
text = CONFIRM_TALENT_WIPE,
button1 = ACCEPT,
button2 = CANCEL,
OnAccept = function()
ConfirmTalentWipe()
end,
OnUpdate = hideOnceOutOfRange,
hasMoneyFrame = 1,
timeout = 0,
hideOnEscape = 1,
}
-- ── The UIParent event arm (UIParent.lua l.533-538) ────────────────────────────────────
-- arg1 is the cost in COPPER, and it goes to the money frame rather than into the text:
-- the sentence above is fixed. The nil guard is the reference's own — Show returns nothing
-- when the dialog is refused (dead without whileDead, or no free instance), and there is
-- then no money frame to fill.
function BenillaTalentWipeConfirmDriver_OnEvent()
if event == "CONFIRM_TALENT_WIPE" then
local dialog = StaticPopup_Show("CONFIRM_TALENT_WIPE")
if dialog then
MoneyFrame_Update(dialog:GetName() .. "MoneyFrame", arg1)
end
end
end
]]></Script>
<Frame name="BenillaTalentWipeConfirmDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("CONFIRM_TALENT_WIPE")
</OnLoad>
<OnEvent>BenillaTalentWipeConfirmDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -1,892 +0,0 @@
<Ui>
<!--
benilla's class/profession trainer window (decisions 0237/0247/0251), a faithful port of the
TBC-backported `Blizzard_TrainerUI` addon the director's client runs (0246): a client-sorted,
collapsible **skill-line tree** with a **dropdown** state filter and a real **scroll bar**. The
window art/geometry are the real 1.12 `ClassTrainerFrame` (MerchantFrame.xml's four-quadrant-slab
idiom); the list/sort/collapse/filter logic is transcribed from `Blizzard_TrainerUI.lua` onto
benilla's engine (`crates/benilla-ui/src/script/trainer.rs`, which builds the tree — decision
0247) + the shared faux-scroll kit (ScrollTemplates.xml, decision 0251). This REPLACES the
earlier flat-list placeholder (git has it): flat state-as-category rows + inline sort checkboxes
+ a chunk-local scroll offset, which stood in until the tree/scrollbar/dropdown landed.
Modeled on the two known-good dressed windows for the framing:
- MerchantFrame.xml — the 384×512 four-quadrant window art (no <Backdrop>: the art IS the
frame), the NPC-session lifecycle (RegisterEvent trio + PLAYER_MONEY, SHOW/UPDATE/CLOSED
dispatch, OnShow/OnHide PlaySound with CloseTrainer() on hide), the "npc" model-portrait
binding, and the reference's own two money frames (detail cost + player purse). The close X reuses
the minimize-button art (this engine ships no UIPanelCloseButton).
- QuestLogFrame.xml — the list-row Button template shape ($parent-named child regions wired in
<OnLoad>) and the persistent selection-highlight Frame (a child Frame whose ARTWORK Texture
renders when Show()n, moved onto the selected visible row).
WHAT'S NEW vs. the placeholder (decision 0247's tree, 0251's scroll bar):
- THE LIST IS A TREE. GetTrainerServiceInfo(i) now returns interleaved "header" rows (a skill
line's name, +/- fold) and service rows (name/state), 1-based into the VISIBLE tree. Update
renders each row by type; a header click folds its group, a service click selects it.
- THE FILTER IS A DROPDOWN. BenillaTrainerFilterDropDown (over UIDropDownMenu.xml's kit) with
three keepShownOnClick check rows (Available/Unavailable/Already Known) driving the engine's
state-visibility filter — replacing the three inline sort checkboxes. Its three states are
SAVED VARIABLES (decision 1128): defaults 1/1/0 at file scope — "Already Known" starts OFF,
the shipped 1.12.1 Blizzard_TrainerUI.lua:6-9's own values — restored from
benilla/saved-variables.lua at load, and pushed into the engine on show because the list
builder resets its mask on every packet.
- THE SCROLL BAR IS REAL. BenillaTrainerListScrollFrame (FauxScrollFrameTemplate): a
draggable Slider bar + up/down step arrows over the fixed 11 rows (the faux-scroll re-binds
rows to a moving offset — it never really scrolls a child). The wheel still lives on the
WINDOW (the engine bubbles a spin up the parent chain, so a spin over a row reaches here).
- COLLAPSE WORKS. The "All" tab collapses/expands every group (Collapse/ExpandTrainerSkillLine(0));
its +/- icon reflects whether all groups are folded (the backport's numHeaders/notExpanded).
FLAT layout — a CHOICE, and it was written down here as a constraint that does not exist.
This block used to claim "a child <Frame> shown at runtime does not render its own <Layers>
FontStrings in this engine". That is FALSE, and it was propagated from here into two more
windows before anyone probed it (decision 1517): MailFrame's own SendMailTitleText lives in a
runtime-shown pane's Layers and has rendered correctly the whole time. The detail-pane
FontStrings + the title/greeting do live on BenillaTrainerFrame's OWN <Layers>, and that is
fine — it is simply one way to lay this out, not the only one available. Per-row regions live
on the row Button template. The detail is a FIXED (non-scrolling) pane
(our descriptions are empty for now) — no real ScrollFrame, unlike the ref's DetailScrollFrame.
Adaptations / deviations (flagged):
- TITLE: the trainer's NAME rides arg1 of TRAINER_SHOW/UPDATE (the app resolves the creature
name ask-once via the NameCache — the MerchantFrame vendor-name precedent); "Trainer" until
it lands.
- PORTRAIT: the "npc" unit-token model bake (MerchantFrame's binding — the interaction NPC's face).
- GlobalStrings the ref reads (REQUIRES_LABEL, TRAINER_REQ_*, COSTS_LABEL, the labels) are
defined LOCALLY below as the byte-exact 1.12 enUS values (interface.MPQ GlobalStrings.lua,
line-cited); NORMAL/GRAY/HIGHLIGHT_FONT_COLOR are Fonts.xml globals.
- The affordability RED-TINT of the cost is the reference's own SetMoneyFrameColor (1937).
- The profession-confirm StaticPopup is skipped for v1 (Train buys directly); a later refinement.
Registers at the left UIPanel slot like Gossip/Merchant/Quest/QuestLog (UiPanels.xml) — opening
it competes for/replaces those. It IS an NPC session: CloseTrainer() fires on hide, and the
standardized range guard (crate::ui_session) client-side-closes it when the player walks away.
-->
<Script><![CDATA[
-- ── GlobalStrings / labels the ref reads (byte-exact 1.12 enUS, interface.MPQ) ────────────────
-- Chunk-local upvalues: the global functions below close over them; inline XML handlers (separate
-- chunks) can't, so every handler routes to a function here. `_RED` variants redden only the
-- number/name (met = white, unmet = red), exactly as the real requirements line reads.
local PARENS_TEMPLATE = "(%s)" -- GlobalStrings.lua:2468
local REQUIRES_LABEL = "Requires:" -- REQUIRES_LABEL (2768)
local UNKNOWN_LABEL = "Unknown" -- UNKNOWN (3860)
local FILTER_LABEL = "Filter" -- FILTER (1297)
local TRAINER_REQ_LEVEL = "Level |cffffffff%d|r" -- TRAINER_REQ_LEVEL (3739)
local TRAINER_REQ_LEVEL_RED = "Level |cffff2020%d|r" -- TRAINER_REQ_LEVEL_RED (3740)
local TRAINER_REQ_SKILL_RANK = "%s (|cffffffff%d|r)" -- TRAINER_REQ_SKILL_RANK (3741)
local TRAINER_REQ_SKILL_RANK_RED = "%s (|cffff2020%d|r)" -- TRAINER_REQ_SKILL_RANK_RED (3742)
local TRAINER_REQ_ABILITY = "|cffffffff%s|r" -- TRAINER_REQ_ABILITY (3737)
local TRAINER_REQ_ABILITY_RED = "|cffff2020%s|r" -- TRAINER_REQ_ABILITY_RED (3738)
-- ── The state filter's three SAVED VARIABLES (decision 1128) ─────────────────────────────────
-- The reference's own globals, values and mechanism: Blizzard_TrainerUI.lua:6-9 assigns these
-- at FILE SCOPE and its `.toc` declares them `## SavedVariables`; the client executes the
-- saved file over them AFTER this file loads and BEFORE the load event, so a remembered value
-- wins over the default. benilla's ported UI is one FrameXML tree rather than addons, so the
-- declaration rides `RegisterForSave` (FrameXML's own API for its 38 globals) and the same
-- ordering is the app's load seam. File scope is load-bearing: assign these in OnLoad and the
-- default would clobber the restored value every launch.
TRAINER_FILTER_AVAILABLE = 1
TRAINER_FILTER_UNAVAILABLE = 1
TRAINER_FILTER_USED = 0
RegisterForSave("TRAINER_FILTER_AVAILABLE")
RegisterForSave("TRAINER_FILTER_UNAVAILABLE")
RegisterForSave("TRAINER_FILTER_USED")
local LABEL_AVAILABLE = "Available" -- AVAILABLE (84)
local LABEL_UNAVAILABLE = "Unavailable" -- UNAVAILABLE (3840)
local LABEL_USED = "Already Known" -- USED (3868)
-- The font colour codes the dropdown rows wrap their labels in (GlobalStrings.lua: the *_CODE
-- family) — green/red/gray, closed with |r.
local GREEN_CODE = "|cff20ff20"
local RED_CODE = "|cffff2020"
local GRAY_CODE = "|cff808080"
local CODE_CLOSE = "|r"
-- CLASS_TRAINER_SKILLS_DISPLAYED / CLASS_TRAINER_SKILL_HEIGHT (ref Blizzard_TrainerUI.lua:2-3).
local BENILLA_TRAINER_ROWS = 11
local BENILLA_TRAINER_ROW_HEIGHT = 16
-- ── Detail pane ─────────────────────────────────────────────────────────────────────────────
-- Nothing selected → blank every detail region: a FontString "hides" via SetText(""), while the
-- icon Button + coin frames Hide. Predates region Show/Hide (region.rs has the real pair now);
-- equivalent either way, so the blank-text idiom stays.
function BenillaTrainer_HideDetails()
getglobal("BenillaTrainerSkillName"):SetText("")
getglobal("BenillaTrainerSkillRequirements"):SetText("")
getglobal("BenillaTrainerSkillDescription"):SetText("")
getglobal("BenillaTrainerCostLabel"):Hide()
getglobal("BenillaTrainerSkillIcon"):Hide()
getglobal("ClassTrainerDetailMoneyFrame"):Hide()
getglobal("BenillaTrainerTrainButton"):Disable()
end
-- Fill the detail pane for the selected service `id` (1-based visible index) — the transcribed
-- ClassTrainer_SetSelection detail half: name/sub name, the Requires: string (built term-for-term
-- as the ref does), the icon, the cost coins + label, the description, and Train's enable state.
function BenillaTrainer_ShowDetails(id)
local name = GetTrainerServiceInfo(id)
getglobal("BenillaTrainerSkillName"):SetText(name or UNKNOWN_LABEL)
-- The detail header shows the spell NAME only — no "(Rank N)" beside it. A deliberate
-- divergence from the ref (which sets a ClassTrainerSubSkillName subtext here): the rank is
-- already on the spell's list row, so a second copy floated to the right of the name just
-- read as clutter (director's call).
local icon = getglobal("BenillaTrainerSkillIcon")
icon:SetNormalTexture(GetTrainerServiceIcon(id) or BENILLA_FALLBACK_ICON)
icon:Show()
-- Requirements string (ref term-for-term): level gate, skill gate, each prerequisite ability;
-- each red when unmet (hasReq nil). Our feed has no pet-learn / step requirements.
local requirements = ""
local separator = ""
local reqLevel = GetTrainerServiceLevelReq(id)
if reqLevel and reqLevel > 1 then
separator = ", "
if (UnitLevel("player") or 0) >= reqLevel then
requirements = requirements .. format(TRAINER_REQ_LEVEL, reqLevel)
else
requirements = requirements .. format(TRAINER_REQ_LEVEL_RED, reqLevel)
end
end
local skill, rank, hasReq = GetTrainerServiceSkillReq(id)
if skill then
if hasReq then
requirements = requirements .. separator .. format(TRAINER_REQ_SKILL_RANK, skill, rank)
else
requirements = requirements .. separator .. format(TRAINER_REQ_SKILL_RANK_RED, skill, rank)
end
separator = ", "
end
local numReq = GetTrainerServiceNumAbilityReq(id)
for i = 1, numReq do
local ability, abilityHas = GetTrainerServiceAbilityReq(id, i)
if ability then
if abilityHas then
requirements = requirements .. separator .. format(TRAINER_REQ_ABILITY, ability)
else
requirements = requirements .. separator .. format(TRAINER_REQ_ABILITY_RED, ability)
end
separator = ", "
end
end
if requirements ~= "" then
getglobal("BenillaTrainerSkillRequirements"):SetText(REQUIRES_LABEL .. " " .. requirements)
else
getglobal("BenillaTrainerSkillRequirements"):SetText("")
end
-- ref Blizzard_TrainerUI.lua (the "Money Frame and cost" block), in full (1937): a free
-- service hides the frame AND the label and pulls the description up to the label's TOPLEFT;
-- a costed one shows both, seats the description under the label, and tints the frame white
-- when the purse covers it, red when not. The frame is repainted either way.
local moneyCost = GetTrainerServiceCost(id)
local costFrame = getglobal("ClassTrainerDetailMoneyFrame")
local costLabel = getglobal("BenillaTrainerCostLabel")
local description = getglobal("BenillaTrainerSkillDescription")
if moneyCost and moneyCost > 0 then
costFrame:Show()
costLabel:Show()
description:SetPoint("TOPLEFT", costLabel, "BOTTOMLEFT", 0, -10)
if (GetMoney() or 0) >= moneyCost then
SetMoneyFrameColor("ClassTrainerDetailMoneyFrame", 1.0, 1.0, 1.0)
else
SetMoneyFrameColor("ClassTrainerDetailMoneyFrame", 1.0, 0.1, 0.1)
end
else
costFrame:Hide()
costLabel:Hide()
description:SetPoint("TOPLEFT", costLabel, "TOPLEFT", 0, 0)
end
MoneyFrame_Update("ClassTrainerDetailMoneyFrame", moneyCost or 0)
getglobal("BenillaTrainerSkillDescription"):SetText(GetTrainerServiceDescription(id))
-- Train enable (ref): only an available AND affordable service. The talent/profession-point
-- gate is dropped (talentPts always 0; the profession-confirm is a later refinement).
local _, _, serviceType = GetTrainerServiceInfo(id)
local affordable = (GetMoney() or 0) >= (moneyCost or 0)
if serviceType == "available" and affordable then
getglobal("BenillaTrainerTrainButton"):Enable()
else
getglobal("BenillaTrainerTrainButton"):Disable()
end
end
-- ── The list page ────────────────────────────────────────────────────────────────────────────
-- Pick the first learnable (available) service, else the first non-header service (ref
-- ClassTrainer_SelectFirstLearnableSkill — the backport hardcodes index 2, the first service
-- under the first header; we find it, robust to which group leads / a leading collapsed group).
local function selectFirstService()
local n = GetNumTrainerServices()
for i = 1, n do
local _, _, serviceType = GetTrainerServiceInfo(i)
if serviceType == "available" then
SelectTrainerService(i)
return
end
end
for i = 1, n do
local _, _, serviceType = GetTrainerServiceInfo(i)
if serviceType ~= "header" then
SelectTrainerService(i)
return
end
end
end
-- Set the collapse-all tab's +/- icon from whether every group is folded (ref
-- ClassTrainerFrame_Update's numHeaders/notExpanded tail).
local function updateCollapseAllIcon(numServices)
local numHeaders, notExpanded = 0, 0
for i = 1, numServices do
local name, _, serviceType, isExpanded = GetTrainerServiceInfo(i)
if name and serviceType == "header" then
numHeaders = numHeaders + 1
if not isExpanded then notExpanded = notExpanded + 1 end
end
end
local b = getglobal("BenillaTrainerCollapseAllButton")
if numHeaders > 0 and notExpanded == numHeaders then
b.collapsed = 1
b:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
else
b.collapsed = nil
b:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
end
end
function BenillaTrainerFrame_Update()
getglobal("BenillaTrainerGreetingText"):SetText(GetTrainerGreetingText())
getglobal("ClassTrainerMoneyFrame"):Show() -- ref ClassTrainerFrame_Show; type PLAYER paints itself
local numServices = GetNumTrainerServices()
if numServices == 0 then
getglobal("BenillaTrainerCollapseAllButton"):Disable()
else
getglobal("BenillaTrainerCollapseAllButton"):Enable()
end
-- The faux-scroll bar reconciles to the row count, then hands back the top-row offset.
local scrollFrame = getglobal("BenillaTrainerListScrollFrame")
FauxScrollFrame_Update(scrollFrame, numServices, BENILLA_TRAINER_ROWS, BENILLA_TRAINER_ROW_HEIGHT)
local skillOffset = FauxScrollFrame_GetOffset(scrollFrame)
getglobal("BenillaTrainerSkillHighlight"):Hide()
-- Fill the visible rows (ref ClassTrainerFrame_Update's row loop). Header rows show the skill
-- line name + a +/- fold; service rows blank the fold, indent the name two spaces, and colour
-- name+subtext by state (available green / used gray / unavailable red).
for i = 1, BENILLA_TRAINER_ROWS do
local button = getglobal("BenillaTrainerService" .. i)
local index = i + skillOffset
if index <= numServices then
local name, subText, serviceType, isExpanded = GetTrainerServiceInfo(index)
if not name then name = UNKNOWN_LABEL end
-- Every row is unlocked here and the selected one re-locked below (ref l.183/190).
-- The lock is what paints a selected row's name white: the template's
-- `<HighlightFont inherits="GameFontHighlight">` instance wins over the state
-- colour SetTextColor writes, which reaches the NORMAL instance only. It reaches
-- nothing else on a service row — the branch below blanks that row's fold glow.
button:UnlockHighlight()
if serviceType == "header" then
button:SetText(name)
button:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
button.subtext:SetText("")
if isExpanded then
button:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
else
button:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
end
button.foldHighlight:SetTexture("Interface\\Buttons\\UI-PlusButton-Hilight")
else
button:SetNormalTexture("")
button.foldHighlight:SetTexture("")
button:SetText(" " .. name)
if subText and subText ~= "" then
button.subtext:SetText(format(PARENS_TEMPLATE, subText))
else
button.subtext:SetText("")
end
-- The rank rides the NAME's right edge, wherever the name ended (ref l.157,
-- B253's flow row) — re-anchored every repaint because the name's width is
-- the thing that changes.
button.subtext:SetPoint("LEFT", getglobal(button:GetName() .. "Text"), "RIGHT", 10, 0)
if serviceType == "available" then
button:SetTextColor(0, 1.0, 0)
button.subR, button.subG, button.subB = 0, 0.6, 0
elseif serviceType == "used" then
button:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
button.subR, button.subG, button.subB = 0.5, 0.5, 0.5
else
button:SetTextColor(0.9, 0, 0)
button.subR, button.subG, button.subB = 0.6, 0, 0
end
button.subtext:SetTextColor(button.subR, button.subG, button.subB)
end
button.serviceIndex = index
button:Show()
else
button.serviceIndex = nil
button:UnlockHighlight()
button:SetText("")
button.subtext:SetText("")
button:SetNormalTexture("")
button.foldHighlight:SetTexture("")
button:Hide()
end
end
-- Selection highlight + detail (ref l.146-186). The glow rides the selected SERVICE row while
-- it is inside the visible window; a header selection shows no glow. The detail fills only for
-- a selected service.
local selection = GetTrainerSelectionIndex()
local selType
if selection >= 1 and selection <= numServices then
_, _, selType = GetTrainerServiceInfo(selection)
end
if selType and selType ~= "header"
and selection > skillOffset and selection <= skillOffset + BENILLA_TRAINER_ROWS then
local selButton = getglobal("BenillaTrainerService" .. (selection - skillOffset))
local hl = getglobal("BenillaTrainerSkillHighlight")
local tex = getglobal("BenillaTrainerSkillHighlightTexture")
if selType == "available" then
tex:SetVertexColor(0, 1.0, 0)
elseif selType == "used" then
tex:SetVertexColor(0.5, 0.5, 0.5)
else
tex:SetVertexColor(0.9, 0, 0)
end
hl:SetPoint("TOPLEFT", selButton, "TOPLEFT", 0, 0)
hl:Show()
-- The whole selected row goes white, and it is the ref's own pair of moves (l.183-184):
-- LockHighlight for the NAME (the button's label, so the white comes from its
-- HighlightFont instance, not from a repaint that the next Update would undo) and an
-- explicit whiten for the SUBTEXT, which is a child FontString the lock cannot reach.
-- An earlier revision called the white name a deliberate divergence "the ref does not
-- do"; the ref does it, through the lock this window had no engine method for at the
-- time (decision 1605). subR/subG/subB go white too so a hover-then-leave keeps the
-- subtext white.
selButton:LockHighlight()
selButton.subtext:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
selButton.subR, selButton.subG, selButton.subB =
HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b
end
if selType and selType ~= "header" then
BenillaTrainer_ShowDetails(selection)
else
BenillaTrainer_HideDetails()
end
updateCollapseAllIcon(numServices)
end
-- The window's wheel handler (bubbled up the parent chain from a row) → one row via the faux kit.
function BenillaTrainerFrame_OnMouseWheel(delta)
BenillaFauxScrollFrame_OnMouseWheel(getglobal("BenillaTrainerListScrollFrame"), delta)
end
-- A row click: a header folds/unfolds its group (Collapse/ExpandTrainerSkillLine take the header's
-- display index); a service selects (ref ClassTrainerSkillButton_OnClick + SetSelection's header
-- branch). Left-button only.
function BenillaTrainerSkillButton_OnClick(button, mouseButton)
if mouseButton ~= "LeftButton" then return end
local index = button.serviceIndex
if not index then return end
local _, _, serviceType, isExpanded = GetTrainerServiceInfo(index)
if serviceType == "header" then
if isExpanded then
CollapseTrainerSkillLine(index)
else
ExpandTrainerSkillLine(index)
end
else
SelectTrainerService(index)
end
BenillaTrainerFrame_Update()
end
-- Hover recolors a service row's subtext to HIGHLIGHT, restoring its state colour on leave (ref
-- ClassTrainerSkillButton_OnEnter/OnLeave). Harmless over a header (its subtext is blank).
function BenillaTrainerSkillButton_OnEnter(button)
button.subtext:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
end
function BenillaTrainerSkillButton_OnLeave(button)
button.subtext:SetTextColor(button.subR or 0.5, button.subG or 0.5, button.subB or 0.5)
end
-- ── The Train button ─────────────────────────────────────────────────────────────────────────
-- Buy the selected service, then repaint (the row flips to "used" once the app pushes the fresh
-- snapshot). The tradeskill profession-confirm dialog is a later refinement.
function BenillaTrainerTrainButton_OnClick()
BuyTrainerService(GetTrainerSelectionIndex())
BenillaTrainerFrame_Update()
end
-- ── The state-filter dropdown (ref ClassTrainerFrameFilterDropDown_*) ────────────────────────
function BenillaTrainerFilterDropDown_OnLoad()
UIDropDownMenu_Initialize(BenillaTrainerFilterDropDown, BenillaTrainerFilterDropDown_Initialize)
UIDropDownMenu_SetText(FILTER_LABEL, BenillaTrainerFilterDropDown)
UIDropDownMenu_SetWidth(96, BenillaTrainerFilterDropDown)
end
function BenillaTrainerFilterDropDown_Initialize()
local function addRow(kind, label, code)
local info = {}
info.text = code .. label .. CODE_CLOSE
info.value = kind
info.func = BenillaTrainerFilterDropDown_OnClick
info.checked = GetTrainerServiceTypeFilter(kind) and 1 or nil
info.keepShownOnClick = 1
UIDropDownMenu_AddButton(info)
end
addRow("available", LABEL_AVAILABLE, GREEN_CODE)
addRow("unavailable", LABEL_UNAVAILABLE, RED_CODE)
addRow("used", LABEL_USED, GRAY_CODE)
end
-- A row's func fires with the row button as `this`, carrying the PRE-click state (this.value /
-- this.checked): the kit's UIDropDownMenuButton_OnClick runs the func FIRST and only then, for a
-- keepShownOnClick row, flips the check texture + this.checked itself. So the func's whole job is
-- to flip the ENGINE filter to the inverse of the pre-click check, reset scroll, and repaint —
-- the visible check is the kit's to move, and the next open re-derives every row from
-- GetTrainerServiceTypeFilter anyway.
--
-- It must NOT re-Initialize the open menu here. Doing so re-ran AddButton, which sets
-- `button.checked` (and the Check texture) from the fresh engine state — so the kit's flip
-- immediately afterwards inverted the ALREADY-correct row and put it back. The check never
-- appeared to move, and because `this.checked` then read stale-true forever, a filter switched
-- off could never be switched back on.
function BenillaTrainerFilterDropDown_OnClick()
-- The engine filter AND the saved global, together: the global is what survives the
-- restart and what BenillaTrainerFrame_ApplyFilter pushes back after the per-packet
-- reset, so writing only the engine would lose the toggle at the next list (the ref's
-- own setglobal pair, Blizzard_TrainerUI.lua:526-530).
local on = 0
if not this.checked then
on = 1
end
setglobal("TRAINER_FILTER_" .. strupper(this.value), on)
SetTrainerServiceTypeFilter(this.value, on)
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
BenillaTrainerFrame_Update()
end
-- ── The collapse-all tab ─────────────────────────────────────────────────────────────────────
-- Fold / unfold every group (Collapse/ExpandTrainerSkillLine(0), ref
-- ClassTrainerCollapseAllButton_OnClick).
function BenillaTrainerCollapseAllButton_OnClick()
local b = getglobal("BenillaTrainerCollapseAllButton")
if b.collapsed then
b.collapsed = nil
ExpandTrainerSkillLine(0)
else
b.collapsed = 1
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
CollapseTrainerSkillLine(0)
end
BenillaTrainerFrame_Update()
end
-- ── The NPC-session lifecycle (MerchantFrame.xml's own shape) ────────────────────────────────
function BenillaTrainerFrame_OnLoad(frame)
-- The NPC portrait: the "npc" unit token → the trainer's live model bake
-- (crates/benilla/src/portrait, InteractNpc) — MerchantFrame's own binding.
SetPortraitTexture(getglobal(frame:GetName() .. "Portrait"), "npc")
frame:RegisterEvent("TRAINER_SHOW")
frame:RegisterEvent("TRAINER_UPDATE")
frame:RegisterEvent("TRAINER_CLOSED")
frame:RegisterEvent("PLAYER_MONEY")
-- The list repaint is wired in the scroll frame's own `<OnVerticalScroll>` below, the
-- reference's way; `updateFunc` was our kit's field and the reference has none (1860).
end
-- Push the three saved globals into the engine — the ref's ClassTrainerFrame <OnShow>
-- (Blizzard_TrainerUI.xml:531-533), and it is NOT redundant: the list builder rewrites the
-- engine's filter mask to its own default on every SMSG_TRAINER_LIST (byte-verified,
-- `0x4d75d9 mov ds:0xb73a1c,3`; decision 1128), so this is what puts the player's remembered
-- choice back. Called on show and at the top of TRAINER_SHOW — our repaint runs before
-- ShowUIPanel, so applying it only in OnShow would leave one frame painted under the reset.
function BenillaTrainerFrame_ApplyFilter()
SetTrainerServiceTypeFilter("available", TRAINER_FILTER_AVAILABLE)
SetTrainerServiceTypeFilter("unavailable", TRAINER_FILTER_UNAVAILABLE)
SetTrainerServiceTypeFilter("used", TRAINER_FILTER_USED)
end
-- The scroll frame's `<OnVerticalScroll>` lives in its own chunk and cannot see this
-- file's `local BENILLA_TRAINER_ROW_HEIGHT`, so the pitch is handed over here rather than
-- written as a literal in the XML (1860). The reference has the same shape with globals:
-- `QUESTLOG_QUEST_HEIGHT` and friends are file-scope globals in its own FrameXML.
function BenillaTrainerList_OnVerticalScroll()
FauxScrollFrame_OnVerticalScroll(BENILLA_TRAINER_ROW_HEIGHT, BenillaTrainerFrame_Update)
end
function BenillaTrainerFrame_SetTitle(name)
if name and name ~= "" then
getglobal("BenillaTrainerTitleText"):SetText(name)
else
getglobal("BenillaTrainerTitleText"):SetText("Trainer")
end
end
function BenillaTrainerFrame_OnEvent(frame)
if event == "TRAINER_SHOW" then
-- SHOW FIRST, then populate — the reference's own order, and not a style choice.
-- `FauxScrollFrame_Update` (the chain's since 1837) gates everything that sets the
-- bar's range on `frame:IsVisible()`, so a list repainted before its window is up
-- leaves `SetMinMaxValues` unset and both arrows dead. The reference relies on this
-- order hard enough to test it: `MerchantFrame.lua:22-27` does `ShowUIPanel(this)`
-- and then treats `not this:IsVisible()` as the panel system refusing, and closes.
-- Decision 1860.
ShowUIPanel(frame)
-- Fresh session: top of the list, a first selection (ref ClassTrainerFrame_Show).
BenillaTrainerFrame_ApplyFilter()
BenillaTrainerFrame_SetTitle(arg1)
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
if GetTrainerSelectionIndex() == 0 then
selectFirstService()
end
BenillaTrainerFrame_Update()
elseif event == "TRAINER_UPDATE" then
BenillaTrainerFrame_SetTitle(arg1)
-- Re-anchor the selection if it fell off the (possibly shorter) list.
if GetTrainerSelectionIndex() == 0 then
selectFirstService()
end
BenillaTrainerFrame_Update()
elseif event == "TRAINER_CLOSED" then
HideUIPanel(frame)
elseif event == "PLAYER_MONEY" then
-- A buy changes affordability without a TRAINER_UPDATE — repaint the detail's cost tint.
if BenillaTrainerFrame:IsVisible() then
BenillaTrainerFrame_Update()
end
end
end
function BenillaTrainerCloseButton_OnClick()
CloseTrainer()
HideUIPanel(getglobal("BenillaTrainerFrame"))
end
]]></Script>
<!--
One row (ref ClassTrainerSkillButtonTemplate): a 293×16 text row — the +/- fold slot at the left
(used for header rows, blanked per-row for services), the name, and the rank subtext beside it.
NO item icon (the list rows carry none; the spell icon is the DETAIL pane's). <OnLoad> wires the
child regions + the fold highlight onto `self` (the QuestLogFrame.xml row-template shape).
THE ROW IS A FLOW, NOT TWO COLUMNS (B253). The name carries **no width**, so it never wraps:
a FontString given an explicit width wraps at it, and a 16px row cannot grow — which is how
"Handstitched Leather Pants" came to draw two lines over the row beneath it. The reference's
own row (`ClassTrainerFrameTemplates.xml`, read off the player's chain, never transcribed)
makes the name the Button's `<ButtonText>` at width **0** and then SetPoints the subtext to
that text's RIGHT + 10 from Lua, per row — so a long name simply runs on and pushes its
"(Rank N)" along. We express the same law declaratively: no size on either string, the
subtext anchored to the name's right edge. The 160/188 fixed columns this replaces were
benilla's invention, and both halves of them were wrong — the cap wrapped, and the column
put the rank at a fixed x the reference never uses.
-->
<Button name="BenillaTrainerServiceTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Layers>
<Layer level="ARTWORK">
<!-- NO <Anchors> — Update SetPoints this to the row's own label every repaint, the
ref's own move (`skillSubText:SetPoint("LEFT", <row>Text, "RIGHT", 10, 0)`,
Blizzard_TrainerUI.lua l.157). A `relativeTo="$parentText"` here CANNOT work and
fails quietly: `<Layers>` is loader step 5 and the `<ButtonText>` is step 5b, so
the label does not exist yet and the anchor falls back to the row (the loader
does warn — `resolve_target`'s "does not resolve" line). The ref anchors it at
runtime for its own reason anyway: the rank must follow a name whose width is
only known once the name is measured (B253). -->
<FontString name="$parentSubText" inherits="GameFontNormalSmall" justifyH="LEFT"/>
</Layer>
</Layers>
<!-- THE SERVICE NAME IS THE BUTTON'S OWN ButtonText (the ref's `ClassTrainerSkill<n>Text`),
under the base's font TRIO — that is what turns a row white under the cursor and holds
the selected row white, with no script involved: `GameFontHighlight` is white, the engine
swaps the highlighted font instance in on hover, and `LockHighlight()` pins it for the
selection. It was a `$parentName` FontString until decision 1605, on a stale note that
`Button:SetTextColor`/`LockHighlight` had no engine method. Carries NO `<Size>` — B253's
flow row: the name measures to its own text and `$parentSubText` follows its right edge,
which is the reference's own `<ButtonText>`-at-width-0 plus per-row
`SetPoint("LEFT", <row>Text, "RIGHT", 10, 0)`. TradeSkillFrame.xml and CraftFrame.xml are
the twins (both fixed-column, so both keep an explicit width). -->
<ButtonText name="$parentText" justifyH="LEFT">
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="22" y="0"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
<DisabledFont inherits="GameFontDisable" justifyH="LEFT"/>
<!-- The +/- fold slot + its hover glow: meaningful for header rows (Update sets +/- there),
blanked for services. -->
<NormalTexture file="Interface\Buttons\UI-MinusButton-UP">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</NormalTexture>
<HighlightTexture name="$parentHighlight" file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</HighlightTexture>
<Scripts>
<OnLoad>
self.subtext = getglobal(self:GetName() .. "SubText")
self.foldHighlight = self:GetHighlightTexture()
</OnLoad>
<OnClick>BenillaTrainerSkillButton_OnClick(self, arg1)</OnClick>
<OnEnter>BenillaTrainerSkillButton_OnEnter(self)</OnEnter>
<OnLeave>BenillaTrainerSkillButton_OnLeave(self)</OnLeave>
</Scripts>
</Button>
<!-- A panel action button (Train/Exit): the UIPanelButtonTemplate face, label via text=. -->
<Button name="BenillaTrainerPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="80" y="22"/></Size>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<Frame name="BenillaTrainerFrame" hidden="true">
<!-- The real 384×512 window. No <Anchors> — ShowUIPanel SetPoints this to the left slot. -->
<Size><AbsDimension x="384" y="512"/></Size>
<Layers>
<!-- NPC portrait 60×60 TOPLEFT (7,-6) (ref), BACKGROUND so the window art's ring frames it.
Bound in OnLoad via SetPortraitTexture(region, "npc") — the trainer's live model bake. -->
<Layer level="BACKGROUND">
<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>
<!-- The four quadrant slabs that ARE the window art (ref): 256-wide left halves, 128-wide
right halves, each 256 tall, pinned to their corner, whole-texture. -->
<Layer level="BORDER">
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"/></Anchors></Texture>
<!-- Title: the NPC's name, fed as arg1 of TRAINER_SHOW/UPDATE via the NameCache; the
static "Trainer" shows until the creature-name query lands. -->
<FontString name="BenillaTrainerTitleText" inherits="GameFontNormal" justifyH="CENTER" text="Trainer">
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<!-- Greeting (GetTrainerGreetingText), GameFontHighlight LEFT/TOP, 260×26, TOPLEFT (76,-38). -->
<FontString name="BenillaTrainerGreetingText" inherits="GameFontHighlight" justifyH="LEFT" justifyV="TOP">
<Size><AbsDimension x="260" y="26"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="76" y="-38"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
<!-- The horizontal divider between the list and the detail pane (ref): 256+75 wide, 16 tall,
TOPLEFT (15,-275), sampled from UI-ClassTrainer-HorizontalBar by TexCoord. -->
<Layer level="ARTWORK">
<Texture name="BenillaTrainerHorizontalBar" file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="256" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-275"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.25"/>
</Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="75" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerHorizontalBar" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.29296875" top="0.25" bottom="0.5"/>
</Texture>
</Layer>
<!-- The FIXED detail-pane FontStrings (ref DetailScrollChildFrame), flattened onto this
window's own Layers (the flat-layout gap). Blanked/filled by Hide/ShowDetails.
THE WIDTHS ARE THE REFERENCE'S, not ours (B253). A FontString wraps at whatever
width it is given, so an invented narrow one is a wrap bug waiting for a long name:
the detail name at 150 broke "Handstitched Leather Pants" over two lines. The
reference's detail child is 296 wide with its name/requirements at **244** and its
description at **290**; our flattened anchors put the name at the same window x (68)
the child's 46 resolves to, so the same numbers land in the same place — and, unlike
the 290/300 they replace, they stay inside the pane instead of running past its
right edge at 358/330. -->
<Layer level="OVERLAY">
<FontString name="BenillaTrainerSkillName" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="244" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="68" y="-293"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTrainerSkillRequirements" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="244" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerSkillName" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTrainerCostLabel" inherits="GameFontNormalSmall" justifyH="LEFT" text="COSTS_LABEL">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="30" y="-340"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTrainerSkillDescription" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="290" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerCostLabel" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The top-right close X (ref a UIPanelCloseButton at TOPRIGHT (-29,-8); the minimize-button
art stands in). -->
<Button name="BenillaTrainerCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-29" y="-8"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts><OnClick>BenillaTrainerCloseButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The expand/collapse-all tab (ref ClassTrainerExpandButtonFrame): a tab-shaped frame
carrying the collapse-all button — now functional (Collapse/ExpandTrainerSkillLine(0)). -->
<Frame name="BenillaTrainerExpandButtonFrame">
<Size><AbsDimension x="54" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-70"/></Offset></Anchor></Anchors>
<Layers><Layer level="BACKGROUND">
<Texture name="BenillaTrainerExpandTabLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-ExpandTab-Left"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors></Texture>
<Texture name="BenillaTrainerExpandTabMiddle" file="Interface\QuestFrame\UI-QuestLogSortTab-Middle"><Size><AbsDimension x="38" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLogSortTab-Right"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerExpandTabMiddle" relativePoint="RIGHT"/></Anchors></Texture>
</Layer></Layers>
<Frames>
<Button name="BenillaTrainerCollapseAllButton" text="All">
<Size><AbsDimension x="40" y="22"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-MinusButton-Up"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></NormalTexture>
<HighlightTexture file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></HighlightTexture>
<ButtonText><Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="18" y="0"/></Offset></Anchor></Anchors></ButtonText>
<NormalFont inherits="GameFontNormalSmall" justifyH="LEFT"/>
<Scripts><OnClick>BenillaTrainerCollapseAllButton_OnClick()</OnClick></Scripts>
</Button>
</Frames>
</Frame>
<!-- The state-filter dropdown (ref ClassTrainerFrameFilterDropDown): the UIDropDownMenu capsule
whose three keepShownOnClick check rows drive the engine's state-visibility filter —
replacing the three inline sort checkboxes. TOPRIGHT (-26,-64), the ref's spot. -->
<Frame name="BenillaTrainerFilterDropDown" inherits="UIDropDownMenuTemplate">
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-26" y="-64"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>BenillaTrainerFilterDropDown_OnLoad()</OnLoad>
<OnShow>BenillaTrainerFilterDropDown_OnLoad()</OnShow>
</Scripts>
</Frame>
<!-- The persistent selection highlight (ref ClassTrainerSkillHighlightFrame): 293×16,
ARTWORK-layer UI-Listbox-Highlight2. Declared BEFORE the rows so the row text paints OVER
the glow (same parent + level ⇒ declaration order is the z-tiebreak). Moved onto the
selected visible service row + vertex-colored by state. -->
<Frame name="BenillaTrainerSkillHighlight" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers><Layer level="ARTWORK">
<Texture name="BenillaTrainerSkillHighlightTexture" file="Interface\Buttons\UI-Listbox-Highlight2">
<Anchors><Anchor point="TOPLEFT"/><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer></Layers>
</Frame>
<!-- 11 tree rows (ref ClassTrainerSkill1..11): 293×16, row1 TOPLEFT (22,-100); rows 2-11 chain
TOPLEFT→prior BOTTOMLEFT with no gap. -->
<Button name="BenillaTrainerService1" inherits="BenillaTrainerServiceTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
</Button>
<Button name="BenillaTrainerService2" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService3" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService4" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService5" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService6" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService7" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService8" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService9" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService10" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService11" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService10" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- The faux-scroll frame (`FauxScrollFrameTemplate`, the REFERENCE's since 1837):
invisible container over the 11 rows; only its right-hand Slider scroll bar renders.
Sized to the 11-row span so the bar sits in the window's right gutter.
THE OWNER DECLARES `<OnVerticalScroll>`, which is how the reference's kit works and
the one thing our transcription did differently: ours carried a `frame.updateFunc`
field that `BenillaFauxScrollBar_OnValueChanged` read, and the reference has no such
field. Its chain is the bar's own OnValueChanged
(`this:GetParent():SetVerticalScroll(arg1)`, UIPanelTemplates.xml) -> this handler ->
`FauxScrollFrame_OnVerticalScroll(itemHeight, updateFunction)`, which is the only
thing that writes `frame.offset`. Without it the arrows and the thumb move the bar
and the list never follows (1860). 16 is the row pitch
(`BenillaTrainerServiceTemplate`). -->
<ScrollFrame name="BenillaTrainerListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="176"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
<Scripts>
<OnVerticalScroll>BenillaTrainerList_OnVerticalScroll()</OnVerticalScroll>
</Scripts>
</ScrollFrame>
<!-- The detail spell icon (ref ClassTrainerSkillIcon): a 37×37 button over a 64×64 UI-EmptySlot
ring, at the detail area's top-left. Icon via SetNormalTexture; Hide()n when unselected.
The hover is the ref's own, verbatim (Blizzard_TrainerUI.xml:452-459): owner is the icon
button, anchor ANCHOR_RIGHT, subject `ClassTrainerFrame.selectedService` — the SELECTED
row, which is why the tooltip describes the selection no matter which row the mouse
passed over on its way here. GetTrainerSelectionIndex() is our engine-held equivalent of
that field; it reads 0 with nothing selected, and SetTrainerService no-ops on 0. This is
the ONLY SetTrainerService call site in the shipped client — the list rows carry no
tooltip of their own (their template's OnEnter only recolours the subtext). -->
<Button name="BenillaTrainerSkillIcon" hidden="true">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="26" y="-291"/></Offset></Anchor></Anchors>
<Layers><Layer level="BACKGROUND">
<Texture file="Interface\Buttons\UI-EmptySlot"><Size><AbsDimension x="64" y="64"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="-13" y="13"/></Offset></Anchor></Anchors></Texture>
</Layer></Layers>
<Scripts>
<OnEnter>
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetTrainerService(GetTrainerSelectionIndex())
GameTooltip:Show()
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!-- ref Blizzard_TrainerUI.xml:463-476, and the reference's own NAME: the cost, a
SmallMoneyFrameTemplate of type STATIC at the same LEFT->cost-label RIGHT (5,0) seat,
painted by MoneyFrame_Update on every detail refresh. Retires the detail coin trio (1937). -->
<Frame name="ClassTrainerDetailMoneyFrame" inherits="SmallMoneyFrameTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerCostLabel" relativePoint="RIGHT"><Offset><AbsDimension x="5" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("STATIC");</OnLoad></Scripts>
</Frame>
<!-- ref Blizzard_TrainerUI.xml:338-345, and the reference's own NAME: the purse, a
SmallMoneyFrameTemplate with no OnLoad of its own (type PLAYER from the template; repaints
on PLAYER_MONEY). Same BOTTOMRIGHT->window BOTTOMLEFT (180,86) seat; the anchor targets OUR
window name until this file migrates. Retires BenillaTrainerMoneyCoin1..3 (1937). -->
<Frame name="ClassTrainerMoneyFrame" inherits="SmallMoneyFrameTemplate">
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="BenillaTrainerFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="180" y="86"/></Offset></Anchor></Anchors>
</Frame>
<!-- The Train button (ref ClassTrainerTrainButton): UIPanelButton face + "Train", 80×22, in
the detail area's bottom-right. Enabled only for an affordable available service. -->
<Button name="BenillaTrainerTrainButton" inherits="BenillaTrainerPanelButtonTemplate" text="Train">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="184" y="-409"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BenillaTrainerTrainButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The Exit button (ref ClassTrainerCancelButton): hides the window; OnHide fires CloseTrainer(). -->
<Button name="BenillaTrainerCancelButton" inherits="BenillaTrainerPanelButtonTemplate" text="Exit">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="265" y="-409"/></Offset></Anchor></Anchors>
<Scripts><OnClick>HideUIPanel(getglobal("BenillaTrainerFrame"))</OnClick></Scripts>
</Button>
</Frames>
<!-- The NPC-session lifecycle (MerchantFrame.xml's shape): OnHide calls CloseTrainer() — it ends
the client-side session whenever this window hides for ANY reason (ESC, close X, Exit,
TRAINER_CLOSED, or another left-slot window displacing it). The list wheel lives on the
WINDOW (the engine bubbles a spin up the parent chain, so a spin over a row reaches here). -->
<Scripts>
<OnLoad>BenillaTrainerFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaTrainerFrame_OnEvent(self)</OnEvent>
<OnMouseWheel>BenillaTrainerFrame_OnMouseWheel(arg1)</OnMouseWheel>
<OnShow>BenillaTrainerFrame_ApplyFilter(); PlaySound("igCharacterInfoOpen")</OnShow>
<OnHide>CloseTrainer(); PlaySound("igCharacterInfoClose")</OnHide>
</Scripts>
</Frame>
</Ui>
+258 -1
View File
@@ -33,11 +33,48 @@
<Scripts>
<OnLoad>
this:RegisterEvent("VARIABLES_LOADED")
this:RegisterEvent("TRAINER_SHOW")
this:RegisterEvent("TRAINER_CLOSED")
this:RegisterEvent("GUILD_INVITE_REQUEST")
this:RegisterEvent("GUILD_INVITE_CANCEL")
this:RegisterEvent("MIRROR_TIMER_START")
this:RegisterEvent("PLAYER_ENTERING_WORLD")
this:RegisterEvent("READY_CHECK")
this:RegisterEvent("START_LOOT_ROLL")
this:RegisterEvent("CONFIRM_LOOT_ROLL")
-- The dialog-raising events below (ref UIParent.lua l.76-127), registered here
-- since the dialog engine became the reference's file (1960); what the engine does
-- not produce yet is left unregistered and named in that record, never armed blind.
this:RegisterEvent("PLAYER_DEAD")
this:RegisterEvent("PLAYER_ALIVE")
this:RegisterEvent("PLAYER_UNGHOST")
this:RegisterEvent("RESURRECT_REQUEST")
this:RegisterEvent("PARTY_INVITE_REQUEST")
this:RegisterEvent("PARTY_INVITE_CANCEL")
this:RegisterEvent("PLAYER_CAMPING")
this:RegisterEvent("PLAYER_QUITING")
this:RegisterEvent("LOGOUT_CANCEL")
this:RegisterEvent("LOOT_BIND_CONFIRM")
this:RegisterEvent("EQUIP_BIND_CONFIRM")
this:RegisterEvent("AUTOEQUIP_BIND_CONFIRM")
this:RegisterEvent("USE_BIND_CONFIRM")
this:RegisterEvent("DELETE_ITEM_CONFIRM")
this:RegisterEvent("QUEST_ACCEPT_CONFIRM")
this:RegisterEvent("CURSOR_UPDATE")
this:RegisterEvent("DUEL_REQUESTED")
this:RegisterEvent("DUEL_OUTOFBOUNDS")
this:RegisterEvent("DUEL_INBOUNDS")
this:RegisterEvent("DUEL_FINISHED")
this:RegisterEvent("CONFIRM_XP_LOSS")
this:RegisterEvent("CORPSE_IN_RANGE")
this:RegisterEvent("CORPSE_IN_INSTANCE")
this:RegisterEvent("CORPSE_OUT_OF_RANGE")
this:RegisterEvent("BIND_ENCHANT")
this:RegisterEvent("REPLACE_ENCHANT")
this:RegisterEvent("CURRENT_SPELL_CAST_CHANGED")
this:RegisterEvent("CONFIRM_TALENT_WIPE")
this:RegisterEvent("CONFIRM_BINDER")
this:RegisterEvent("CONFIRM_SUMMON")
</OnLoad>
<OnEvent>UIParent_OnEvent(event)</OnEvent>
<!-- The reference's own UIParent OnUpdate line (its UIParent.xml l.19): the floating
@@ -45,7 +82,7 @@
once a frame, since the chat window became the reference's (1948). The fade and
flash kits keep their own tick sources in UiPanels.xml. The guard is for a VM that
loaded this file without the chat files — a test's, never the manifest's. -->
<OnUpdate>if FCF_OnUpdate then FCF_OnUpdate(arg1) end</OnUpdate>
<OnUpdate>if FCF_OnUpdate then FCF_OnUpdate(arg1) end if ButtonPulse_OnUpdate then ButtonPulse_OnUpdate(arg1) end if UnitPopup_OnUpdate then UnitPopup_OnUpdate(arg1) end</OnUpdate>
</Scripts>
</Frame>
@@ -337,6 +374,13 @@
UIParentLoadAddOn("Blizzard_GMSurveyUI")
end
-- ref UIParent.lua l.162-164, verbatim — the trainer window's loader, reached from the
-- TRAINER_SHOW arm below. The addon is a LoadOnDemand registry row read off the chain
-- (`ui_script::addons`, 1957), so this is the reference's own mechanism, not a stand-in.
function ClassTrainerFrame_LoadUI()
UIParentLoadAddOn("Blizzard_TrainerUI")
end
-- ref UIParent.lua's own `ShowMacroFrame`, moved here from our MacroFrame.xml when 1848
-- retired that file — the third such squatter after `InspectUnit` (1832) and
-- `ToggleTalentFrame` (1833), and by now the expected shape.
@@ -379,6 +423,35 @@
end
return
end
-- The trainer arms (ref UIParent.lua l.590-602), verbatim: `Blizzard_TrainerUI` is a
-- LoadOnDemand addon off the player's chain, loaded here on the first trainer the
-- way the reference loads it — which is when its ADDON_LOADED arm pushes the saved
-- filters, after the list packet that resets them (1957). The guards are the
-- reference's own, and are what keeps the per-window harnesses that load this file
-- without the trainer safe.
if ( event == "TRAINER_SHOW" ) then
ClassTrainerFrame_LoadUI()
if ( ClassTrainerFrame_Show ) then
ClassTrainerFrame_Show()
end
return
end
if ( event == "TRAINER_CLOSED" ) then
if ( ClassTrainerFrame_Hide ) then
ClassTrainerFrame_Hide()
end
return
end
-- The guild invitation (ref UIParent.lua l.296-303, verbatim): the dialog rode a driver
-- frame in our FriendsFrame.xml until that file became the reference's (1959).
if ( event == "GUILD_INVITE_REQUEST" ) then
StaticPopup_Show("GUILD_INVITE", arg1, arg2)
return
end
if ( event == "GUILD_INVITE_CANCEL" ) then
StaticPopup_Hide("GUILD_INVITE")
return
end
-- The mirror bars' START driver (ref UIParent.lua l.97 + l.374-377). It lived on a
-- hidden `MirrorTimerDriver` frame in our own MirrorTimer.xml until 1751 window 7,
-- on the reasoning that "our UIParent.xml is script-only — it has no frame of its own
@@ -428,6 +501,190 @@
end
return
end
-- ── The dialog-raising arms, ref UIParent.lua l.237-268 and l.284-360, l.378-453,
-- l.533-552, verbatim (1960). Until the dialog engine became the reference's own file
-- these rode nine hidden driver frames — one per dialog file, plus two in UiPanels.xml
-- — each explaining that it existed because benilla had no UIParent OnEvent to put the
-- arm in. It does, and the reference's placement is the one that survives the dialogs
-- being someone else's. Two guards are ours, each marked. ─────────────────────────────
if ( event == "PLAYER_DEAD" ) then
if ( not StaticPopup_Visible("DEATH") ) then
CloseAllWindows(1)
if ( GetReleaseTimeRemaining() > 0 or GetReleaseTimeRemaining() == -1 ) then
StaticPopup_Show("DEATH")
end
end
return
end
if ( event == "PLAYER_ALIVE" ) then
StaticPopup_Hide("DEATH")
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
return
end
if ( event == "PLAYER_UNGHOST" ) then
StaticPopup_Hide("RESURRECT")
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
StaticPopup_Hide("RESURRECT_NO_TIMER")
StaticPopup_Hide("SKINNED")
StaticPopup_Hide("SKINNED_REPOP")
return
end
if ( event == "RESURRECT_REQUEST" ) then
if ( ResurrectHasSickness() ) then
StaticPopup_Show("RESURRECT", arg1)
elseif ( ResurrectHasTimer() ) then
StaticPopup_Show("RESURRECT_NO_SICKNESS", arg1)
else
StaticPopup_Show("RESURRECT_NO_TIMER", arg1)
end
return
end
if ( event == "PARTY_INVITE_REQUEST" ) then
StaticPopup_Show("PARTY_INVITE", arg1)
return
end
if ( event == "PARTY_INVITE_CANCEL" ) then
StaticPopup_Hide("PARTY_INVITE")
return
end
if ( event == "PLAYER_CAMPING" ) then
StaticPopup_Show("CAMP")
return
end
if ( event == "PLAYER_QUITING" ) then
StaticPopup_Show("QUIT")
return
end
if ( event == "LOGOUT_CANCEL" ) then
StaticPopup_Hide("CAMP")
StaticPopup_Hide("QUIT")
return
end
if ( event == "LOOT_BIND_CONFIRM" ) then
local dialog = StaticPopup_Show("LOOT_BIND")
if ( dialog ) then
dialog.data = arg1
end
return
end
if ( event == "EQUIP_BIND_CONFIRM" ) then
StaticPopup_Hide("AUTOEQUIP_BIND")
local dialog = StaticPopup_Show("EQUIP_BIND")
if ( dialog ) then
dialog.data = arg1
end
return
end
if ( event == "AUTOEQUIP_BIND_CONFIRM" ) then
StaticPopup_Hide("EQUIP_BIND")
local dialog = StaticPopup_Show("AUTOEQUIP_BIND")
if ( dialog ) then
dialog.data = arg1
end
return
end
if ( event == "USE_BIND_CONFIRM" ) then
StaticPopup_Show("USE_BIND")
return
end
if ( event == "DELETE_ITEM_CONFIRM" ) then
-- Check quality
if ( arg2 >= 3 ) then
StaticPopup_Show("DELETE_GOOD_ITEM", arg1)
else
StaticPopup_Show("DELETE_ITEM", arg1)
end
return
end
if ( event == "QUEST_ACCEPT_CONFIRM" ) then
StaticPopup_Show("QUEST_ACCEPT", arg1, arg2)
return
end
if ( event == "CURSOR_UPDATE" ) then
StaticPopup_Hide("EQUIP_BIND")
StaticPopup_Hide("AUTOEQUIP_BIND")
return
end
if ( event == "DUEL_REQUESTED" ) then
StaticPopup_Show("DUEL_REQUESTED", arg1)
return
end
if ( event == "DUEL_OUTOFBOUNDS" ) then
StaticPopup_Show("DUEL_OUTOFBOUNDS")
return
end
if ( event == "DUEL_INBOUNDS" ) then
StaticPopup_Hide("DUEL_OUTOFBOUNDS")
return
end
if ( event == "DUEL_FINISHED" ) then
StaticPopup_Hide("DUEL_REQUESTED")
StaticPopup_Hide("DUEL_OUTOFBOUNDS")
return
end
if ( event == "CONFIRM_XP_LOSS" ) then
local resSicknessTime = GetResSicknessDuration()
if ( resSicknessTime ) then
local dialog = StaticPopup_Show("XP_LOSS", resSicknessTime)
if ( dialog ) then
dialog.data = resSicknessTime
end
else
local dialog = StaticPopup_Show("XP_LOSS_NO_SICKNESS")
if ( dialog ) then
dialog.data = 1
end
end
-- Ours: the reference's `HideUIPanel(GossipFrame)` unguarded — the per-window
-- harnesses load this file without the gossip window.
if ( GossipFrame ) then
HideUIPanel(GossipFrame)
end
return
end
if ( event == "CORPSE_IN_RANGE" ) then
StaticPopup_Show("RECOVER_CORPSE")
return
end
if ( event == "CORPSE_IN_INSTANCE" ) then
StaticPopup_Show("RECOVER_CORPSE_INSTANCE")
return
end
if ( event == "CORPSE_OUT_OF_RANGE" ) then
StaticPopup_Hide("RECOVER_CORPSE")
StaticPopup_Hide("RECOVER_CORPSE_INSTANCE")
StaticPopup_Hide("XP_LOSS")
return
end
if ( event == "BIND_ENCHANT" ) then
StaticPopup_Show("BIND_ENCHANT")
return
end
if ( event == "REPLACE_ENCHANT" ) then
StaticPopup_Show("REPLACE_ENCHANT", arg1, arg2)
return
end
if ( event == "CURRENT_SPELL_CAST_CHANGED" ) then
StaticPopup_Hide("BIND_ENCHANT")
StaticPopup_Hide("REPLACE_ENCHANT")
StaticPopup_Hide("TRADE_REPLACE_ENCHANT")
return
end
if ( event == "CONFIRM_TALENT_WIPE" ) then
local dialog = StaticPopup_Show("CONFIRM_TALENT_WIPE")
if ( dialog ) then
MoneyFrame_Update(dialog:GetName().."MoneyFrame", arg1)
end
return
end
if ( event == "CONFIRM_BINDER" ) then
StaticPopup_Show("CONFIRM_BINDER", arg1)
return
end
if ( event == "CONFIRM_SUMMON" ) then
StaticPopup_Show("CONFIRM_SUMMON")
return
end
-- The ready-check popup's opener (decision 1549), ref UIParent.lua l.570-572. The
-- event is the engine's own `0x218`, fired by the `MSG_RAID_READY_CHECK` open
-- handler; the frame and `ShowReadyCheck` live in RaidFrame.xml, so this is the one
File diff suppressed because it is too large Load Diff
-885
View File
@@ -1,885 +0,0 @@
<!--
The unit right-click popups (decision 0434 phase 5) — benilla's own implementation of the
engine the 1.12 FrameXML `UnitPopup.lua` SPECIFIES. That file is the specification this one
matches, not the source of this text. What is faithful is everything observable: the row
catalogue and every row's facts (label, interact-distance class, colours, atlas cells), the
membership and ORDER of every menu, which rows a menu hides and when, which rows grey out and
when, and what each row does when it is clicked. How that is written is ours — a row
catalogue walked by a builder, and three keyed tables (hide / grey / click) where the
reference runs one long `elseif value == "…"` ladder each. The globals the reference
publishes (`UnitPopupButtons`, `UnitPopupMenus`, `UnitPopupShown`, `UnitPopup_ShowMenu`, …)
keep their names and shapes exactly, because addons index, call and replace them by name.
The menus ride the dropdown kit's MENU displayMode + level-2 submenus (UIDropDownMenu.xml).
The owner frames (TargetFrameDropDown, PlayerFrameDropDown, PartyMemberFrameNDropDown) live
with their frames in UnitFrames.xml / PartyFrame.xml — this file is the shared engine, like
the ref's load split. The OnUpdate driver frame at the bottom stands in for the ref's
UIParent.xml OnUpdate hook (the DeathFrame hidden-driver pattern; benilla's UIParent carries
no OnUpdate).
DEVIATIONS, deliberate (each waits for its arc):
- NO ADDED ROWS. A "PVP" row briefly sat at the head of SELF (decision 0646 §3); decision 0652
took it back out. 1.12.1 has no PvP toggle in any menu — `TogglePVP()`'s single caller in the
whole shipped UI is `SlashCmdList["PVP"]`, and `UnitPopup.lua` contains the string "PVP"
exactly zero times — so the row was a deviation that made this menu read wrong against the
reference. `/pvp` is the toggle the client has.
- DEFERRED ENTRIES: rows whose backing system doesn't exist yet are HIDDEN outright (the
BENILLA_UNITPOPUP_DEFERRED set below) rather than shown-dead: TARGET (the friends
list). TRADE came out
with decision 0592 P1, DUEL with 0633, INSPECT with 0631, GUILD_PROMOTE/GUILD_LEAVE with
the guild arc (1257 — their hide predicates and click rows are below), RESET_INSTANCES with
the instance-lockout arc (1748 — its predicate is the reference's own
`CanShowResetInstances()`, whose four terms are now real), FOLLOW with 0890's
follow-up — and
that last entry's stated reason ("its CMSG family is its own slice") was simply WRONG: 0890
verified auto-follow sends nothing at all, it is pure client-side movement, so the row was
waiting on a wire slice that does not exist. It was the motion law it really needed.
Everything a menu SHOWS works. The PET menu opened with decision 1066: its four rows and the
one predicate that forks them are below, its opener is UnitFrames.xml's PetFrameDropDown, and
the two dialogs behind Abandon and Rename are registered in PetActionBar.xml (with the pet
arc, like every other dialog in this codebase — this file must stay loadable without the
popup engine). The RAID_* rank rows opened with decision 1549: their hide-always predicate is
gone, they carry the reference's own per-row rules and click handlers, and their opener is
RaidFrame.xml's row right-click.
- Lua 5.1: the ref's 5.0 `for i, v in t do` iteration is ipairs/pairs here; strsub is
string.sub. `TEXT` is kept because the reference publishes it as the localization hook every
displayed string passes through (it is the identity, and BasicControls.xml defines the real
one) — row labels are declared through it here for the same reason.
- The newbie tooltips on rows are OUT with the kit's (default-off cvar look).
- CheckInteractDistance now really gates (decision 0631): the binding landed with the inspect
row, off wow-re's §5-VERIFIED `check_interact_dist2` table {10², 11.1111², 10², 30²}. So every
SHOWN row with a non-zero `dist` grays out of range now — INSPECT (dist 1, 10 yd), TRADE
(dist 2, 11.11 yd) and DUEL (dist 3, 10 yd) — where TRADE and DUEL shipped ungated only
because the binding was nil when they landed. The nil-guard in the enable pass below is
kept as-is.
-->
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Script><![CDATA[
-- The localization identity every displayed string passes through (BasicControls.xml
-- defines the real one; this stands in when it is not loaded).
if ( not TEXT ) then
TEXT = function(s) return s end
end
UNITPOPUP_TIMEOUT = 5;
-- ── The row catalogue ──────────────────────────────────────────────────────────────
-- `UnitPopupButtons` is the flat key → row map the reference publishes: addons index it,
-- and overwrite entries in it, by name. Each row is declared once, from its key, the
-- label to draw, and its interact-distance class — 0 for no range check at all, 1..4 for
-- the client's own CheckInteractDistance classes, which the enable pass consults. Extra
-- presentation attributes (a leading spacer, a submenu arrow, a check box, a colour, an
-- icon and its atlas cell) ride in the fourth field and are copied through untouched.
UnitPopupButtons = { };
local function declareRow(key, label, dist, attributes)
local row = attributes or { };
row.text = TEXT(label);
row.dist = dist or 0;
UnitPopupButtons[key] = row;
end
-- { key, label, interact-distance class, extra attributes }
local ROWS = {
{ "CANCEL", CANCEL, 0, { space = 1 } },
{ "TRADE", TRADE, 2 },
{ "INSPECT", INSPECT, 1 },
{ "TARGET", TARGET },
{ "DUEL", DUEL, 3, { space = 1 } },
{ "WHISPER", WHISPER },
{ "INVITE", PARTY_INVITE },
{ "UNINVITE", PARTY_UNINVITE },
{ "PROMOTE", PARTY_PROMOTE },
{ "GUILD_PROMOTE", GUILD_PROMOTE },
{ "GUILD_LEAVE", GUILD_LEAVE },
{ "LEAVE", PARTY_LEAVE },
{ "FOLLOW", FOLLOW, 4 },
-- The pet verbs. Only the last four are reachable — no 1.12 menu holds the six
-- stance/command rows above them (the reference declares them and builds no menu
-- from them either), so nothing here can click them. They stay declared because the
-- catalogue is what addons index.
{ "PET_PASSIVE", PET_PASSIVE },
{ "PET_DEFENSIVE", PET_DEFENSIVE },
{ "PET_AGGRESSIVE", PET_AGGRESSIVE },
{ "PET_WAIT", PET_WAIT },
{ "PET_FOLLOW", PET_FOLLOW },
{ "PET_ATTACK", PET_ATTACK },
{ "PET_DISMISS", PET_DISMISS },
{ "PET_ABANDON", PET_ABANDON },
{ "PET_PAPERDOLL", PET_PAPERDOLL },
{ "PET_RENAME", PET_RENAME },
-- Loot: the parent row opens a submenu, the five below it are that submenu.
{ "LOOT_METHOD", LOOT_METHOD, 0, { nested = 1 } },
{ "FREE_FOR_ALL", LOOT_FREE_FOR_ALL },
{ "ROUND_ROBIN", LOOT_ROUND_ROBIN },
{ "MASTER_LOOTER", LOOT_MASTER_LOOTER },
{ "GROUP_LOOT", LOOT_GROUP_LOOT },
{ "NEED_BEFORE_GREED", LOOT_NEED_BEFORE_GREED },
{ "RESET_INSTANCES", RESET_INSTANCES },
-- The quality floor, likewise — and each threshold row wears its own quality colour.
{ "LOOT_THRESHOLD", LOOT_THRESHOLD, 0, { nested = 1 } },
{ "LOOT_PROMOTE", LOOT_PROMOTE },
{ "ITEM_QUALITY2_DESC", ITEM_QUALITY2_DESC, 0, { color = ITEM_QUALITY_COLORS[2] } },
{ "ITEM_QUALITY3_DESC", ITEM_QUALITY3_DESC, 0, { color = ITEM_QUALITY_COLORS[3] } },
{ "ITEM_QUALITY4_DESC", ITEM_QUALITY4_DESC, 0, { color = ITEM_QUALITY_COLORS[4] } },
-- The raid rank rows. Their labels are the plain verbs, not RAID_-prefixed strings.
{ "RAID_LEADER", NEW_LEADER },
{ "RAID_PROMOTE", PROMOTE },
{ "RAID_DEMOTE", DEMOTE },
{ "RAID_REMOVE", REMOVE },
-- The marks' parent row; the eight marks themselves are declared below, and clearing
-- the mark is a row of its own.
{ "RAID_TARGET_ICON", RAID_TARGET_ICON, 0, { nested = 1 } },
{ "RAID_TARGET_NONE", NONE, 0, { checkable = 1 } },
};
for _, spec in ipairs(ROWS) do
declareRow(spec[1], spec[2], spec[3], spec[4]);
end
-- The eight marks ride ONE atlas: four quarter-cells across, two down, filled left to
-- right and top to bottom in mark order. So a mark's cell follows from its number, and
-- the only facts worth writing per mark are its name and its colour.
local RAID_MARK_TEXTURE = "Interface\\TargetingFrame\\UI-RaidTargetingIcons";
local RAID_MARK_CELL = 0.25;
local RAID_MARKS = {
{ RAID_TARGET_1, 1.0, 0.92, 0 },
{ RAID_TARGET_2, 0.98, 0.57, 0 },
{ RAID_TARGET_3, 0.83, 0.22, 0.9 },
{ RAID_TARGET_4, 0.04, 0.95, 0 },
{ RAID_TARGET_5, 0.7, 0.82, 0.875 },
{ RAID_TARGET_6, 0, 0.71, 1 },
{ RAID_TARGET_7, 1.0, 0.24, 0.168 },
{ RAID_TARGET_8, 0.98, 0.98, 0.98 },
};
for mark, spec in ipairs(RAID_MARKS) do
local cell = mark - 1;
local atlasRow = math.floor(cell / 4);
local atlasColumn = cell - atlasRow * 4;
declareRow("RAID_TARGET_" .. mark, spec[1], 0, {
checkable = 1,
color = { r = spec[2], g = spec[3], b = spec[4] },
icon = RAID_MARK_TEXTURE,
tCoordLeft = atlasColumn * RAID_MARK_CELL,
tCoordRight = (atlasColumn + 1) * RAID_MARK_CELL,
tCoordTop = atlasRow * RAID_MARK_CELL,
tCoordBottom = (atlasRow + 1) * RAID_MARK_CELL,
});
end
-- ── The menus ──────────────────────────────────────────────────────────────────────
-- Which rows a menu holds, and in what order. This IS the menu's observable shape, so
-- both the membership and the order are the reference's exactly. The last two are the
-- level-2 submenus, keyed by the row that opens them.
UnitPopupMenus = {
SELF = { "LOOT_METHOD", "LOOT_THRESHOLD", "LOOT_PROMOTE", "LEAVE", "RESET_INSTANCES", "RAID_TARGET_ICON", "CANCEL" },
PET = { "PET_PAPERDOLL", "PET_RENAME", "PET_ABANDON", "PET_DISMISS", "CANCEL" },
PARTY = { "WHISPER", "PROMOTE", "LOOT_PROMOTE", "UNINVITE", "INSPECT", "TRADE", "FOLLOW", "DUEL", "RAID_TARGET_ICON", "CANCEL" },
PLAYER = { "WHISPER", "INSPECT", "INVITE", "TRADE", "FOLLOW", "DUEL", "RAID_TARGET_ICON", "CANCEL" },
RAID = { "RAID_LEADER", "RAID_PROMOTE", "RAID_DEMOTE", "RAID_REMOVE", "CANCEL" },
FRIEND = { "WHISPER", "INVITE", "TARGET", "GUILD_PROMOTE", "GUILD_LEAVE", "CANCEL" },
RAID_TARGET_ICON = { "RAID_TARGET_1", "RAID_TARGET_2", "RAID_TARGET_3", "RAID_TARGET_4", "RAID_TARGET_5", "RAID_TARGET_6", "RAID_TARGET_7", "RAID_TARGET_8", "RAID_TARGET_NONE" },
LOOT_METHOD = { "FREE_FOR_ALL", "ROUND_ROBIN", "MASTER_LOOTER", "GROUP_LOOT", "NEED_BEFORE_GREED", "CANCEL" },
LOOT_THRESHOLD = { "ITEM_QUALITY2_DESC", "ITEM_QUALITY3_DESC", "ITEM_QUALITY4_DESC", "CANCEL" },
};
-- Row N of the menu being opened is shown (1) or hidden (0). This is only a seed — the
-- hide pass rewrites every entry it walks, and grows the table for a menu longer than
-- the reference's nine (PARTY's ten rows do exactly that).
UnitPopupShown = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
-- The row that PICKS each loot method → the token the API speaks. Used twice: to gate the
-- five submenu rows, and to send the choice.
local LOOT_METHOD_TOKENS = {
FREE_FOR_ALL = "freeforall",
ROUND_ROBIN = "roundrobin",
MASTER_LOOTER = "master",
GROUP_LOOT = "group",
NEED_BEFORE_GREED = "needbeforegreed",
};
-- The label the parent Loot Method row wears, keyed by the token GetLootMethod() answers
-- with. (The reference's newbie tooltips are out with the kit's.)
UnitLootMethod = {
freeforall = { text = LOOT_FREE_FOR_ALL },
roundrobin = { text = LOOT_ROUND_ROBIN },
master = { text = LOOT_MASTER_LOOTER },
group = { text = LOOT_GROUP_LOOT },
needbeforegreed = { text = LOOT_NEED_BEFORE_GREED },
};
-- The dropdowns whose open menu this engine owns — the enable pass ignores anybody
-- else's. (FriendsDropDown is ItemRef.xml's stand-in and drives its menu directly.)
UnitPopupFrames = {
"PlayerFrameDropDown",
"TargetFrameDropDown",
"PetFrameDropDown",
"PartyMemberFrame1DropDown",
"PartyMemberFrame2DropDown",
"PartyMemberFrame3DropDown",
"PartyMemberFrame4DropDown",
};
-- The DEFERRED set (header): entries hidden until their backing arc lands.
BENILLA_UNITPOPUP_DEFERRED = {
TARGET = 1,
};
-- ── Shared helpers ─────────────────────────────────────────────────────────────────
-- The marks and the row that opens them share one key prefix, and three passes ask the
-- same question, so it gets a name. RAID_TARGET_ICON answers YES on purpose: the parent
-- row is leader business exactly like the marks under it.
local function isRaidMarkRow(key)
return string.sub(key, 1, 12) == "RAID_TARGET_";
end
-- The party facts the hide and enable passes both ask about, read once per pass instead
-- of once per row. `canCoop` needs the menu's unit, so it is only meaningful when one is
-- passed (the enable pass never asks).
local function partyState(menu)
local party = {
inParty = 0,
isLeader = 0,
isAssistant = 0,
canCoop = 0,
};
if ( (GetNumPartyMembers() > 0) or (GetNumRaidMembers() > 0) ) then
party.inParty = 1;
end
if ( party.inParty == 1 and IsPartyLeader() ) then
party.isLeader = 1;
end
if ( IsRaidOfficer() ) then
party.isAssistant = 1;
end
if ( menu and menu.unit and UnitCanCooperate("player", menu.unit) ) then
party.canCoop = 1;
end
return party;
end
-- Record which menu a level is showing, so the enable pass can find it again.
local function rememberOpenMenu(level, menu)
OPEN_DROPDOWNMENUS[level] = { which = menu.which, unit = menu.unit };
end
-- The dropdown kit wants a text colour as three separate fields, and a row that has no
-- colour must leave all three alone.
local function applyTextColor(info, color)
if ( color ) then
info.textR, info.textG, info.textB = color.r, color.g, color.b;
end
end
-- One catalogue row → one dropdown info table. Both menu levels want the same
-- presentation off a row, so it is read in one place; each caller adds what only its own
-- level decides (the owner, the checked state, the submenu arrow). Returns the row
-- alongside, since callers go on to ask it questions.
local function rowInfo(key)
local row = UnitPopupButtons[key];
local info = {
text = row.text,
value = key,
func = UnitPopup_OnClick,
icon = row.icon,
tCoordLeft = row.tCoordLeft,
tCoordRight = row.tCoordRight,
tCoordTop = row.tCoordTop,
tCoordBottom = row.tCoordBottom,
};
applyTextColor(info, row.color);
return info, row;
end
-- ── Which rows a menu shows at all ─────────────────────────────────────────────────
-- Every row that can be hidden owns one predicate answering "hide this?" from the menu
-- being opened plus the party facts. A row with no predicate is always shown. The
-- conditions are the reference's, row for row; only the ladder is gone.
local HIDE_ROW = { };
-- You cannot trade with, duel, inspect or follow someone you cannot cooperate with — an
-- enemy-faction player is already attackable, so none of the four has anything to offer.
-- (Ref UnitPopup.lua l.285/304-307/311-319. INSPECT and FOLLOW were MISSING until
-- decision 1033: without them both stayed shown for *every* unit — including one that
-- does not exist — so PARTY's shown-count never fell to zero, ShowMenu's "nothing but
-- CANCEL, don't show the menu" early-out never fired at load, and each party dropdown's
-- OnLoad-time initialize ran on to its closing PlaySound.)
local function hideWithoutCooperation(menu, party)
return party.canCoop == 0;
end
HIDE_ROW["TRADE"] = hideWithoutCooperation;
HIDE_ROW["DUEL"] = hideWithoutCooperation;
HIDE_ROW["INSPECT"] = hideWithoutCooperation;
HIDE_ROW["FOLLOW"] = hideWithoutCooperation;
-- Nothing to say to yourself, or to someone you cannot cooperate with.
HIDE_ROW["WHISPER"] = function(menu, party)
return (menu.unit and party.canCoop == 0) or menu.name == UnitName("player");
end
-- The units an invite would be pointless against: the party we already have, and us.
local INVITE_POINTLESS_AGAINST = { "party1", "party2", "party3", "party4", "player" };
HIDE_ROW["INVITE"] = function(menu, party)
if ( menu.unit ) then
return party.canCoop == 0;
end
-- Addressed by NAME (a chat name, a /who row): no unit to test, so compare names.
for _, unit in ipairs(INVITE_POINTLESS_AGAINST) do
if ( menu.name == UnitName(unit) ) then
return true;
end
end
end
-- Party bookkeeping rows: some need a party at all, some need to be leading it.
local function hideOutsideAParty(menu, party)
return party.inParty == 0;
end
HIDE_ROW["LEAVE"] = hideOutsideAParty;
HIDE_ROW["LOOT_METHOD"] = hideOutsideAParty;
HIDE_ROW["LOOT_THRESHOLD"] = hideOutsideAParty;
-- "Reset all instances", ref UnitPopup.lua l.377-380 (decision 1748) — TWO conditions,
-- and the second is not `hideUnlessLeading`: a SOLO player may reset (there is no party
-- to hold anyone inside), a party LEADER may, and a party member may not. The engine half
-- is the interesting one: `CanShowResetInstances()` is true only while we hold a
-- permanent bind, are not standing in a 5-man, and left one within the last 25 hours.
HIDE_ROW["RESET_INSTANCES"] = function(menu, party)
return not CanShowResetInstances() or (party.inParty == 1 and party.isLeader == 0);
end
local function hideUnlessLeading(menu, party)
return party.inParty == 0 or party.isLeader == 0;
end
HIDE_ROW["PROMOTE"] = hideUnlessLeading;
HIDE_ROW["UNINVITE"] = hideUnlessLeading;
-- The five loot-method rows: in a party always, and either we set the method or this row
-- IS the method already in force — so a member still gets to SEE which one is on.
for rowKey, method in pairs(LOOT_METHOD_TOKENS) do
HIDE_ROW[rowKey] = function(menu, party)
return party.inParty == 0 or (party.isLeader == 0 and GetLootMethod() ~= method);
end
end
-- Handing master loot over: only a leader under master loot, and never to whoever
-- already holds it (party member or, at index 0, us).
HIDE_ROW["LOOT_PROMOTE"] = function(menu, party)
local lootMethod, lootMaster = GetLootMethod();
return party.inParty == 0 or party.isLeader == 0 or lootMethod ~= "master"
or (lootMaster and menu.unit == "party" .. lootMaster)
or (menu.unit == "player" and lootMaster and lootMaster == 0);
end
-- The pet menu forks on ONE predicate, and Dismiss reads it INVERTED (decision 1066,
-- ref UnitPopup.lua l.402-417): a hunter's pet can be abandoned, so it offers
-- paperdoll/rename/abandon and hides dismiss; a warlock's demon cannot, so it offers only
-- dismiss. Rename wants both bits, because its own is one-shot — the server clears it
-- after the first rename.
local function hideUnlessAbandonable()
return not PetCanBeAbandoned();
end
HIDE_ROW["PET_PAPERDOLL"] = hideUnlessAbandonable;
HIDE_ROW["PET_ABANDON"] = hideUnlessAbandonable;
HIDE_ROW["PET_RENAME"] = function()
return not PetCanBeAbandoned() or not PetCanBeRenamed();
end
HIDE_ROW["PET_DISMISS"] = function()
return PetCanBeAbandoned();
end
-- ── The four raid rank rows (decision 1549, ref UnitPopup.lua l.382-401) ──────────────
--
-- Every one of them re-reads the CLICKED ROW's rank through `GetRaidRosterInfo(userData)`
-- — which is why the RAID menu is the only one in this file opened with a fourth
-- argument, and why that argument is the raid ROW INDEX rather than a name. The rules,
-- ref-verbatim:
--
-- New Leader leader only, and not on the leader themself (rank 2)
-- Promote leader only, and only on a plain member (rank 0)
-- Demote leader only, and only on an assistant (rank 1)
-- Remove leader OR assistant, and never on the leader
--
-- The first three also require `menu.name`: a row whose name has not resolved cannot be
-- addressed by the by-name bindings behind them. Remove does not, because it acts on the
-- index directly.
local function raidRank(menu)
local _, rank = GetRaidRosterInfo(menu.userData);
return rank;
end
-- `party.isLeader` is 0/1, not a boolean, and **0 is truthy in Lua** — so these compare
-- against 0 rather than saying `not party.isLeader`, exactly as every other predicate in
-- this file does and exactly as the reference's own `(isLeader == 0)` does.
HIDE_ROW["RAID_LEADER"] = function(menu, party)
return party.isLeader == 0 or raidRank(menu) == 2 or not menu.name;
end
HIDE_ROW["RAID_PROMOTE"] = function(menu, party)
return party.isLeader == 0 or raidRank(menu) ~= 0 or not menu.name;
end
HIDE_ROW["RAID_DEMOTE"] = function(menu, party)
return party.isLeader == 0 or raidRank(menu) ~= 1 or not menu.name;
end
HIDE_ROW["RAID_REMOVE"] = function(menu, party)
return (party.isLeader == 0 and party.isAssistant == 0) or raidRank(menu) == 2;
end
-- The two GUILD rows, ref UnitPopup.lua l.323-330 (decision 1257's guild arc un-defers
-- them). Both gate on `GuildFrame:IsVisible()`, which is the whole point of them: these
-- rows exist for right-clicking a GUILD ROSTER row, not a unit, so they are hidden
-- everywhere else — a friend-list or /who row opens the same FRIEND menu and must not
-- offer them.
-- · GUILD_PROMOTE — hand over the guild. Guild master only, and never on yourself.
-- · GUILD_LEAVE — leave the guild. Only ever offered on YOURSELF.
-- `GuildFrame and` is a nil-guard, not a behaviour change: FriendsFrame.xml loads after
-- this file in the manifest, so in a real session the frame always exists and the
-- condition is exactly the reference's. It is here because this file must stay usable
-- when loaded on its own (the same reason FriendsFrame.xml guards `UpdateMicroButtons`).
HIDE_ROW["GUILD_PROMOTE"] = function(menu)
return not IsGuildLeader() or menu.name == UnitName("player")
or not (GuildFrame and GuildFrame:IsVisible());
end
HIDE_ROW["GUILD_LEAVE"] = function(menu)
return menu.name ~= UnitName("player")
or not (GuildFrame and GuildFrame:IsVisible());
end
-- Marking is leader/assistant business, and a mark on an enemy-faction player nobody in
-- the group can cooperate with is pointless — except on our OWN menu, which marks us.
local function hideRaidMarkRow(menu, party)
if ( party.inParty == 0 or (party.isLeader == 0 and party.isAssistant == 0) ) then
return true;
end
if ( menu.which ~= "SELF" and UnitExists("target") and not UnitInParty("target") ) then
return UnitIsPlayer("target")
and not UnitCanCooperate("player", "target")
and not UnitIsUnit("target", "player");
end
end
-- A row's own predicate, or the shared one every raid-mark row uses (the reference keys
-- those by PREFIX, so an addon's own mark row is covered too).
local function hidePredicate(key)
local hide = HIDE_ROW[key];
if ( hide ) then
return hide;
end
if ( isRaidMarkRow(key) ) then
return hideRaidMarkRow;
end
end
function UnitPopup_ShowMenu(dropdownMenu, which, unit, name, userData)
-- Wake the enable-pass driver: it hides itself the moment no unit menu of ours is
-- open (UnitPopup_OnUpdate's early-out), and this function is the one opener of
-- every unit menu — the six dropdowns' initialize functions all land here.
BenillaUnitPopupDriver:Show();
dropdownMenu.which = which;
dropdownMenu.unit = unit;
if ( unit and not name ) then
name = UnitName(unit);
end
dropdownMenu.name = name;
dropdownMenu.userData = userData;
-- No realm half: benilla has no cross-realm names yet (the reference reads one out of
-- UnitName's second return and appends it to whispers and targets).
dropdownMenu.server = nil;
UnitPopup_HideButtons();
-- Nothing but Cancel left? Then there is no menu worth opening.
local count = 0;
for index, value in ipairs(UnitPopupMenus[which]) do
if ( UnitPopupShown[index] == 1 and value ~= "CANCEL" ) then
count = count + 1;
end
end
if ( count < 1 ) then
return;
end
-- The two nested rows wear the live setting as their own label, and open only for
-- whoever can change it — a member still gets to read them.
local lootMethod = GetLootMethod();
dropdownMenu.selectedLootMethod = UnitLootMethod[lootMethod].text;
dropdownMenu.selectedLootThreshold = getglobal("ITEM_QUALITY" .. GetLootThreshold() .. "_DESC");
local methodRow, thresholdRow = UnitPopupButtons["LOOT_METHOD"], UnitPopupButtons["LOOT_THRESHOLD"];
methodRow.text = dropdownMenu.selectedLootMethod;
thresholdRow.text = dropdownMenu.selectedLootThreshold;
local leads = ((GetNumPartyMembers() > 0) or (GetNumRaidMembers() > 0)) and IsPartyLeader();
methodRow.nested = leads and 1 or nil;
thresholdRow.nested = leads and 1 or nil;
-- A level-2 open is a submenu of the row that owns it: the value the kit hands back
-- names both the menu and the owner. Every row in it is shown — the hide pass ran
-- against the parent menu.
if ( UIDROPDOWNMENU_MENU_LEVEL == 2 ) then
local submenu = UIDROPDOWNMENU_MENU_VALUE;
dropdownMenu.which = submenu;
rememberOpenMenu(UIDROPDOWNMENU_MENU_LEVEL, dropdownMenu);
for index, value in ipairs(UnitPopupMenus[submenu]) do
local info = rowInfo(value);
info.owner = submenu;
-- The live loot method and threshold tick themselves; a mark row ticks when
-- the unit already wears it — a mark's POSITION in the icon menu is its
-- number, which is why the row index is what gets compared.
if ( info.text == dropdownMenu.selectedLootMethod
or info.text == dropdownMenu.selectedLootThreshold ) then
info.checked = 1;
elseif ( isRaidMarkRow(value) and GetRaidTargetIndex(unit) == index ) then
info.checked = 1;
end
UIDropDownMenu_AddButton(info, UIDROPDOWNMENU_MENU_LEVEL);
end
return;
end
-- The unit's name titles the menu.
if ( unit or name ) then
UIDropDownMenu_AddButton({
text = name or TEXT(UNKNOWN),
isTitle = 1,
notCheckable = 1,
});
end
rememberOpenMenu(UIDROPDOWNMENU_MENU_LEVEL, dropdownMenu);
for index, value in ipairs(UnitPopupMenus[which]) do
if ( UnitPopupShown[index] == 1 ) then
local info, row = rowInfo(value);
info.owner = which;
if ( not row.checkable ) then
info.notCheckable = 1;
end
if ( row.nested ) then
info.hasArrow = 1;
end
-- The threshold row wears the live threshold's own quality colour.
if ( value == "LOOT_THRESHOLD" ) then
applyTextColor(info, ITEM_QUALITY_COLORS[GetLootThreshold()]);
end
if ( isRaidMarkRow(value) and GetRaidTargetIndex("target") == index ) then
info.checked = 1;
end
UIDropDownMenu_AddButton(info);
end
end
PlaySound("igMainMenuOpen");
end
function UnitPopup_HideButtons()
local menu = getglobal(UIDROPDOWNMENU_INIT_MENU);
local party = partyState(menu);
for index, value in ipairs(UnitPopupMenus[menu.which]) do
local hide = hidePredicate(value);
-- The DEFERRED set is asked first, so nothing can resurrect one of its rows.
if ( BENILLA_UNITPOPUP_DEFERRED[value] or (hide and hide(menu, party)) ) then
UnitPopupShown[index] = 0;
else
UnitPopupShown[index] = 1;
end
end
end
-- ── Which shown rows are clickable right now ───────────────────────────────────────
-- The enable pass runs every frame over the open menus, so a row that can go grey owns a
-- predicate too: "grey this out?". Range is handled for every row at once, off the row's
-- own interact-distance class.
local GREY_ROW = { };
GREY_ROW["LEAVE"] = function(menu, party)
return party.inParty == 0;
end
-- In a group, only the leader or an assistant may bring someone else in.
GREY_ROW["INVITE"] = function(menu, party)
return party.inParty == 1 and party.isLeader == 0 and party.isAssistant == 0;
end
local function greyUnlessLeading(menu, party)
return party.inParty == 0 or party.isLeader == 0;
end
GREY_ROW["UNINVITE"] = greyUnlessLeading;
GREY_ROW["PROMOTE"] = greyUnlessLeading;
GREY_ROW["WHISPER"] = function(menu)
return menu.unit and not UnitIsConnected(menu.unit);
end
-- Ref UnitPopup.lua l.502-505: greyed while either party is dead or a ghost, or while we
-- lack full control (feared/charmed) — `HasFullControl` is nil-guarded like
-- CheckInteractDistance is below, because benilla models no loss of control yet, so the
-- clause is inert rather than an error. The row also carries dist = 3, so the shared
-- range leg gates it too.
GREY_ROW["DUEL"] = function(menu)
return UnitIsDeadOrGhost("player")
or (HasFullControl and not HasFullControl())
or UnitIsDeadOrGhost(menu.unit);
end
-- Only a leader under master loot can hand the job on, and never to its current holder.
GREY_ROW["LOOT_PROMOTE"] = function(menu, party)
local lootMethod, lootMaster = GetLootMethod();
if ( party.inParty == 0 or party.isLeader == 0 or lootMethod ~= "master" ) then
return true;
end
local master = "player";
if ( lootMaster and lootMaster ~= 0 ) then
master = "party" .. lootMaster;
end
return menu.unit and UnitIsUnit(menu.unit, master);
end
-- Out of interact range first (dist 0 means the row has no range at all), then whatever
-- the row itself says.
local function rowEnabled(menu, key, party)
local row = UnitPopupButtons[key];
if ( row.dist > 0 and CheckInteractDistance ) then
if ( not CheckInteractDistance(menu.unit, row.dist) ) then
return false;
end
end
local grey = GREY_ROW[key];
return not (grey and grey(menu, party));
end
-- Somebody else's dropdown is not ours to enable and disable.
local function ownsOpenMenu()
for _, frame in ipairs(UnitPopupFrames) do
if ( UIDROPDOWNMENU_OPEN_MENU == frame ) then
return true;
end
end
end
-- The per-frame enable/disable pass over every open unit-popup menu (driven by the
-- BenillaUnitPopupDriver below — the ref hooks it off UIParent's OnUpdate).
function UnitPopup_OnUpdate(elapsed)
if ( not DropDownList1:IsVisible() or not ownsOpenMenu() ) then
-- Nothing of ours to scan: put the driver back to sleep until the next
-- UnitPopup_ShowMenu (visible frames are the only ones ticked — tick.rs — so
-- this stops the per-frame pass entirely; decision 1396's class).
BenillaUnitPopupDriver:Hide();
return;
end
local party = partyState();
for level, menu in pairs(OPEN_DROPDOWNMENUS) do
if ( menu ) then
-- The kit numbers buttons by SHOWN position, so only shown rows advance the
-- count; the +1 steps over the title the level-1 menu puts at button 1. (The
-- reference offsets every level alike, level 2's title-less lists included.)
local count = 0;
for index, value in ipairs(UnitPopupMenus[menu.which]) do
if ( UnitPopupShown[index] == 1 ) then
count = count + 1;
if ( rowEnabled(menu, value, party) ) then
UIDropDownMenu_EnableButton(level, count + 1);
else
UIDropDownMenu_DisableButton(level, count + 1);
end
end
end
end
end
end
-- ── What a row does when it is clicked ─────────────────────────────────────────────
-- One handler per row, keyed by the row's own name. A row with no handler does nothing —
-- which is how Cancel, the nested parent rows, and every DEFERRED row are expressed
-- (header: TARGET, RESET_INSTANCES).
local CLICK_ROW = { };
CLICK_ROW["WHISPER"] = function(menu)
ChatFrame_SendTell(menu.name);
end
-- A targeted invite goes by token; a chat name or /who row has only the name.
CLICK_ROW["INVITE"] = function(menu)
if ( menu.unit ) then
InviteToParty(menu.unit);
else
InviteByName(menu.name);
end
end
CLICK_ROW["UNINVITE"] = function(menu)
UninviteFromParty(menu.unit);
end
CLICK_ROW["PROMOTE"] = function(menu)
PromoteToPartyLeader(menu.unit);
end
CLICK_ROW["LEAVE"] = function()
LeaveParty();
end
-- Resetting goes behind a confirm (ref UnitPopup_OnClick l.620-621): the send is
-- irreversible and takes every dungeon we own at once. The dialog is registered with the
-- rest of the instance arc, in RaidFrame.xml.
CLICK_ROW["RESET_INSTANCES"] = function()
StaticPopup_Show("CONFIRM_RESET_INSTANCES");
end
-- Picking a loot method sets it and writes the choice back onto the parent row — list 1,
-- button 2, the fixed place LOOT_METHOD sits in the SELF menu. Master loot is the one
-- method that also names who holds it.
for rowKey, method in pairs(LOOT_METHOD_TOKENS) do
CLICK_ROW[rowKey] = function(menu)
if ( method == "master" ) then
SetLootMethod(method, menu.name);
else
SetLootMethod(method);
end
UIDropDownMenu_SetButtonText(1, 2, UnitPopupButtons[rowKey].text);
UIDropDownMenu_Refresh(menu, nil, 1);
end
end
CLICK_ROW["LOOT_PROMOTE"] = function(menu)
SetLootMethod("master", menu.name);
end
-- The threshold rows start at uncommon, so the clicked row's own id is one below the
-- quality it sets; the parent row (list 1, button 3) takes that quality's colour.
local function chooseLootThreshold(menu, key, clicked)
local quality = clicked:GetID() + 1;
SetLootThreshold(quality);
local color = ITEM_QUALITY_COLORS[quality];
UIDropDownMenu_SetButtonText(1, 3, UnitPopupButtons[key].text, color.r, color.g, color.b);
end
CLICK_ROW["ITEM_QUALITY2_DESC"] = chooseLootThreshold;
CLICK_ROW["ITEM_QUALITY3_DESC"] = chooseLootThreshold;
CLICK_ROW["ITEM_QUALITY4_DESC"] = chooseLootThreshold;
-- Every row of the icon submenu marks the unit; None clears it (mark 0). Registering
-- against that menu's own membership is also what keeps the parent RAID_TARGET_ICON row
-- out of it — it opens the submenu, it does not mark.
local function setRaidMark(menu, key)
local mark = string.sub(key, 13);
if ( mark == "NONE" ) then
mark = 0;
end
SetRaidTargetIcon(menu.unit, tonumber(mark));
end
for _, key in ipairs(UnitPopupMenus["RAID_TARGET_ICON"]) do
CLICK_ROW[key] = setRaidMark;
end
-- Challenge to a duel (decision 0633): the app resolves the unit token → player guid →
-- a cast of the duel spell (ref UnitPopup_OnClick l.559, StartDuelUnit).
CLICK_ROW["DUEL"] = function(menu)
StartDuelUnit(menu.unit);
end
-- Offer to trade (decision 0592 P1): the app resolves the unit token → player guid →
-- CMSG_INITIATE_TRADE.
CLICK_ROW["TRADE"] = function(menu)
InitiateTrade(menu.unit);
end
-- Open the inspect window (decision 0631) — InspectUnit gates on CanInspect and sends
-- CMSG_INSPECT.
CLICK_ROW["INSPECT"] = function(menu)
InspectUnit(menu.unit);
end
-- Begin auto-following, by NAME and EXACT. That `1` is the resolver's exact-only flag,
-- and it matters: this menu already knows the unit's name to the letter, so unlike
-- `/follow rag` it must not prefix-match its way onto a bystander. Client-side movement
-- only, no wire.
CLICK_ROW["FOLLOW"] = function(menu)
FollowByName(menu.name, 1);
end
-- The four PET rows (decision 1066). The asymmetry is the reference's and it is the right
-- one: DISMISS runs immediately, ABANDON goes behind a confirm. Sending a summon away
-- costs nothing; giving up a tamed pet is permanent, and the server deletes it
-- (`PET_SAVE_AS_DELETED`).
CLICK_ROW["PET_DISMISS"] = function()
PetDismiss();
end
CLICK_ROW["PET_ABANDON"] = function()
StaticPopup_Show("ABANDON_PET");
end
CLICK_ROW["PET_PAPERDOLL"] = function()
ToggleCharacter("PetPaperDollFrame");
end
CLICK_ROW["PET_RENAME"] = function()
StaticPopup_Show("RENAME_PET");
end
-- The two GUILD arms, ref UnitPopup_OnClick l.571-575 (decision 1257). Both go behind a
-- confirm, and both are addressed by NAME — a guild roster row has no unit token behind
-- it, which is the reason this menu takes a name at all.
--
-- GUILD_PROMOTE stashes the name on the DIALOG (`dialog.data`), not on a global: the
-- popup engine hands `data` back to OnAccept, so the dialog acts on the name that was
-- right-clicked even if the selection moved while it was open. GUILD_LEAVE needs no
-- data — it is only ever offered on yourself — and formats the guild's own name into
-- "Really leave %s?". The `if dialog` is ours, and it is not decoration: the ref writes
-- `dialog.data` on the bare return because its engine runs FOUR dialog instances and
-- ours runs two (UiPanels.xml's header), so a refused Show returning nil is reachable
-- here and indexing it would raise inside a menu click.
CLICK_ROW["GUILD_PROMOTE"] = function(menu)
local dialog = StaticPopup_Show("CONFIRM_GUILD_PROMOTE", menu.name);
if ( dialog ) then
dialog.data = menu.name;
end
end
CLICK_ROW["GUILD_LEAVE"] = function()
StaticPopup_Show("CONFIRM_GUILD_LEAVE", GetGuildInfo("player"));
end
-- The four raid rank verbs (ref UnitPopup_OnClick l.624-631). Three go by NAME and one by
-- the row index — the reference's own split, and it is the wire's: `CMSG_GROUP_UNINVITE`
-- would take either, but the assistant and leader opcodes need a guid the engine resolves
-- from a name, and the menu never had one to hand for a raid row.
CLICK_ROW["RAID_LEADER"] = function(menu)
PromoteByName(menu.name);
end
CLICK_ROW["RAID_PROMOTE"] = function(menu)
PromoteToAssistant(menu.name);
end
CLICK_ROW["RAID_DEMOTE"] = function(menu)
DemoteAssistant(menu.name);
end
CLICK_ROW["RAID_REMOVE"] = function(menu)
UninviteFromRaid(menu.userData);
end
function UnitPopup_OnClick()
local clicked = this;
local key = clicked.value;
local menu = getglobal(UIDROPDOWNMENU_INIT_MENU);
local click = CLICK_ROW[key];
if ( click ) then
click(menu, key, clicked);
end
PlaySound("UChatScrollButton");
end
]]></Script>
<!-- The OnUpdate driver (ref UIParent.xml l.21 runs UnitPopup_OnUpdate from UIParent's own
OnUpdate; ours has none — the DeathFrame hidden-driver pattern instead). Awake only while
a unit menu of ours is open: UnitPopup_ShowMenu shows it, its own early-out hides it, and
a hidden frame is never ticked (tick.rs). It ships shown so the first tick runs the
early-out once and parks it. -->
<Frame name="BenillaUnitPopupDriver" parent="UIParent" hidden="false">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnUpdate>UnitPopup_OnUpdate(arg1)</OnUpdate>
</Scripts>
</Frame>
</Ui>
+42 -48
View File
@@ -162,9 +162,11 @@ MoneyFrame.xml
# `every_chain_xml_brings_its_own_lua` is now a gate so neither can recur.
Interface\FrameXML\MoneyInputFrame.lua
Interface\FrameXML\MoneyInputFrame.xml
# The UIPanel slot manager (decision 0084 §2): script-only, no frames of its own — must load
# before any panel frame (Gossip/Merchant today) so ShowUIPanel/HideUIPanel already exist
# when those frames' OnLoad/OnEvent reference them.
# The UIPanel slot manager (decision 0084 §2) and the fade/flash/pulse kits — the UIParent.lua
# halves this engine still carries itself: script-only but for the kits' tick drivers, loaded
# before any panel frame so ShowUIPanel/HideUIPanel already exist when those frames' OnLoad/OnEvent
# reference them. The StaticPopup engine and every dialog it used to carry are the reference's
# own file now, far below at the reference's own slot after UIPanelTemplates (decision 1960).
UiPanels.xml
# The tooltip: a hover window the bag/merchant/loot/unit frames drive. Loaded before them so
# `GameTooltip` + its verbs exist when their OnEnter handlers fire (runtime, so order
@@ -203,10 +205,13 @@ Interface\FrameXML\UIDropDownMenu.xml
# needs for the tooltip backdrop its menu wears). Far above ChatFrame.xml, which is the one thing
# that used it here — resolved at load, and now satisfied earlier rather than in the same file.
Interface\FrameXML\UIMenu.xml
# The unit right-click popups (0434 phase 5): UnitPopupMenus/Buttons + UnitPopup_ShowMenu
# — after the dropdown kit it drives, before the unit/party frames whose DropDown
# children's OnLoad initialize into it.
UnitPopup.xml
# ══ THE UNIT MENUS — the reference's own, off the player's chain (1751; decision 1958) ══
# UnitPopup.xml + UnitPopup.lua: UnitPopupButtons/Menus and UnitPopup_ShowMenu, the nine menus
# and every row's facts — after the dropdown kit it drives (its own position, FrameXML.toc
# l.16), before the unit/party frames whose DropDown children's OnLoad initialize into it. Its
# enable pass runs from UIParent's OnUpdate line, the reference's own hook. 0434's
# transcription is gone.
Interface\FrameXML\UnitPopup.xml
# ══ THE CHAT-LINK TOOLTIP — the reference's own, off the player's chain (1751, window 6) ══
# `SetItemRef` (the chat-link click router) and `ItemRefTooltip` / `ItemRefCloseButton`.
#
@@ -311,35 +316,6 @@ 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 death arc's dialogs + event driver (decision 0308): registry entries on the shared
# StaticPopup engine, so after UiPanels.xml.
DeathFrame.xml
# The two duel dialogs + their event driver (decision 0633): registry entries on the same
# StaticPopup engine, and DUEL_OUTOFBOUNDS leans on its per-tick countdown branch.
DuelFrame.xml
# The escort-quest confirm + its event driver (decision 1733): one more registry entry on the same
# StaticPopup engine. Not QuestFrame.xml's or QuestLogFrame.xml's — it belongs to no window, the
# way the duel dialogs above belong to none.
QuestShareFrame.xml
# The two enchant-apply confirms + their event driver (decision 0928): the same StaticPopup
# registry again. Not CraftFrame's — the gate that raises them is the generic item-target
# bind, reached by a poison or a sharpening stone with no profession window open.
EnchantConfirm.xml
# The innkeeper's bind confirm + its event driver (decision 1331): the same StaticPopup registry.
# Not GossipFrame's, and the wire is why — the server closes the gossip menu BEFORE it asks, so
# this dialog is free-standing by the time it shows. Needs only UiPanels.xml, far above.
BinderConfirm.xml
# The summon confirm + its event driver (decision 1747): the same StaticPopup registry once more.
# Free-standing like the two around it, and for a stronger reason — a warlock's ritual, a meeting
# stone and a GM `.summon` all raise it with nothing whatever on screen. The only entry here whose
# event carries no arguments: the dialog reads the summoner, the destination and the clock back out
# of the engine every tick, so it needs UiPanels.xml's countdown branch as well as its registry.
ConfirmSummon.xml
# The class trainer's respec confirm + its event driver (decision 1580): the binder confirm's twin
# one line up, free-standing for the identical reason — the server closes the gossip menu before it
# asks. The only entry in the registry that raises a money frame, so it needs MoneyFrame.xml (far
# above, ahead of UiPanels.xml) as well as the StaticPopup engine itself.
TalentWipeConfirm.xml
# ── MEASUREMENT (1938): the options window at the reference's slot (FrameXML.toc l.21, before
# MainMenuBar l.34) with the four rows it inherits from. ──
# The faux-scroll kit (decisions 0247/0250/0251): BenillaScrollBarTemplate (the draggable
@@ -371,6 +347,17 @@ ScrollTemplates.xml
Interface\FrameXML\UIPanelTemplates.lua
Interface\FrameXML\UIPanelTemplates.xml
# ══ THE DIALOG ENGINE — the reference's own, off the player's chain (1751; decision 1960) ══
# StaticPopup.xml + StaticPopup.lua: StaticPopupDialogs (all 76 of the reference's entries),
# StaticPopup_Show/Hide/Visible/Resize/OnUpdate, the four dialog instances, the edit boxes and the
# money frame. The reference's toc has it at l.23, right after UIPanelTemplates and UIOptionsFrame;
# here too it sits after UIPanelTemplates — the CloseButton it inherits (UIPanelCloseButton) is that
# file's, and UIPanelTemplates cannot rise above ScrollTemplates.xml (see above). Everything that
# registers a dialog of its own (Mail, Auction, the keybindings page) loads below this line. The
# event arms that raise these dialogs are UIParent.xml's, the reference's own home. 0308's engine,
# the 37 dialogs UiPanels.xml carried and the seven free-standing dialog files (Death, Duel,
# QuestShare, EnchantConfirm, BinderConfirm, ConfirmSummon, TalentWipeConfirm) are gone.
Interface\FrameXML\StaticPopup.xml
# The Keybindings page MODULE (decision 1008, superseding 0997's standalone window):
# the templates + script of the Options window's Keybindings category, over the
@@ -828,13 +815,15 @@ SpellBookAdapters.xml
# What we GAIN: `TalentFrameTab4` and `TalentFrameTab5`. The reference lays out five tab slots
# where our transcription stopped at the three a 1.12 class actually has.
Interface\AddOns\Blizzard_TalentUI\Blizzard_TalentUI.xml
# The social window (decision 0668): the O-key friends/ignore/who window over the social
# API (`benilla-ui/src/script/social.rs`) + `crate::ui_social`'s feed/drain. Needs
# UiPanels (the panel manager, `CharacterFrameTabButtonTemplate`, and the StaticPopup
# engine its two name-entry dialogs register into), ScrollTemplates (three faux lists),
# GameTooltip (the newbie tips) and UIDropDownMenu (the who list's variable column) —
# all loaded above.
FriendsFrame.xml
# ══ THE SOCIAL WINDOW — the reference's own, off the player's chain (1751; decision 1959) ══
# FriendsFrame.xml + FriendsFrame.lua: the O-key window's four tabs — friends/ignore, who, the
# guild roster with its control and info panes, and the raid tab whose pane is the stock
# RaidFrame.xml right below (the reference's own file split, its toc l.71-72). Over the social
# API (`benilla-ui/src/script/social.rs`) and `crate::ui_social`'s feed/drain, the guild seam,
# the party roster. Its twelve dialogs live with the other dialogs in UiPanels.xml; the
# GUILD_INVITE arms are UIParent.xml's, the reference's own home. 0668/1257/1549's transcription
# is gone.
Interface\FrameXML\FriendsFrame.xml
# ══ THE RAID PANE — the reference's own, off the player's chain (1751, scoped by 1839) ══
# The social window's fourth tab, in its own file because RaidFrame is a child of FriendsFrame and
# a child cannot be declared before its parent — the reference splits it the same way and for the
@@ -888,11 +877,16 @@ TradeFrame.xml
# rather than the `BENILLA_`-prefixed pair this file kept privately. Fonts.xml's own note said
# those two tables would arrive "the day something does" read them; the sourced file is that day.
Interface\FrameXML\ItemTextFrame.xml
# The trainer window (decision 0237 phase 3): the read-only class/profession trainer list.
# Loaded right after MerchantFrame per the director's brief (kept adjacent even though this
# v1 uses plain-text costs, not MerchantFrame's coin helpers) and before the other NPC
# session windows below.
TrainerFrame.xml
# ══ THE TRAINER WINDOW — the reference's own LoadOnDemand addon, off the chain (1751; decision 1957) ══
# No row: `Blizzard_TrainerUI` is the first Blizzard addon benilla loads the way the reference
# does — on demand, from UIParent.xml's TRAINER_SHOW arm through `ClassTrainerFrame_LoadUI` →
# `UIParentLoadAddOn` → `LoadAddOn`, off a registry row `ui_script::addons` reads from the
# chain (its toc, its `## SavedVariables`, its ADDON_LOADED at the end of ITS load). The timing
# is the mechanism: the list packet resets the engine's filter mask, and the addon's
# ADDON_LOADED arm re-pushes the saved filters after it — which eager loading cannot do. Its
# list inherits `ClassTrainerListScrollFrameTemplate` from
# `Interface\FrameXML\ClassTrainerFrameTemplates.xml`, far below, resolved at the addon's
# load. 0237/0247/0251's transcription is gone.
# ══ THE BANK WINDOW — the reference's own, off the player's chain (decision 1751, window 2) ══
# `BankFrame`, `BankFrameItem1..24`, `BankFrameBag1..6`, `BankFramePurchaseInfo` and the
# `BANK_CONTAINER`/`NUM_BAG_SLOTS`/`NUM_BANKGENERIC_SLOTS`/`NUM_BANKBAGSLOTS` constants — the real
+1 -1
View File
@@ -1178,7 +1178,7 @@ mod tests {
assert_eq!(
s.eval::<String>(&format!("return {ROW}9Description:GetText()"))
.unwrap(),
"JUMP"
crate::ui_script::keybindings_tests::label(&s, "BINDING_NAME_JUMP", "JUMP")
);
assert!(s.bind_capture_armed());
let mut app = vm_harness(s);
@@ -82,11 +82,14 @@ type DressQuery = (
);
/// One line per streamed player within [`DEFAULT_RADIUS`], contradictions first.
#[allow(clippy::too_many_arguments)]
fn fire_dress_census(
mut probe: ResMut<DressCensus>,
time: ProbeClock,
names: Res<NameCache>,
body: Query<&Transform, With<SelfPlayer>>,
body: Query<(Entity, &Transform), With<SelfPlayer>>,
// The self body's parts, one line each (`DRESS_PART`) — the draw-population inventory.
body_parts: crate::entities::BodyPartsDesc,
entities: Query<DressQuery>,
// The body's draw population (`parts=`/`mats=`): every mesh part hanging under the unit —
// body batches, each attach model's batches, cards — and how many DISTINCT materials
@@ -105,7 +108,7 @@ fn fire_dress_census(
} else {
-1.0
};
let Ok(body) = body.single() else {
let Ok((self_entity, body)) = body.single() else {
println!("DRESS_CENSUS t={now:.1} NO BODY — not in world, nothing measured");
return;
};
@@ -178,6 +181,9 @@ fn fire_dress_census(
for (_, _, line) in &rows {
println!("{line}");
}
for line in body_parts.describe(self_entity, &children) {
println!("{line}");
}
}
/// Mesh parts under `unit` (every depth) and the distinct materials they bind.
@@ -422,15 +422,19 @@ fn drive_live_fps(
let ld = |a: &std::sync::atomic::AtomicU64| {
a.load(std::sync::atomic::Ordering::Relaxed)
};
let fx = &benilla_world::particles::render::EFFECT_DRAW_STATS;
format!(
" wgpu_bufs={} wgpu_texs={} wgpu_bgs={} draws=o{}/t{}/s{}/ui{}",
" wgpu_bufs={} wgpu_texs={} wgpu_bgs={} draws=o{}/t{}/s{}/ui{} \
fx={}/{}",
ld(&c.0.buffers),
ld(&c.0.textures),
ld(&c.0.bind_groups),
ld(&c.0.draws_opaque),
ld(&c.0.draws_transparent),
ld(&c.0.draws_shadow),
ld(&c.0.draws_ui)
ld(&c.0.draws_ui),
fx[0].load(std::sync::atomic::Ordering::Relaxed),
fx[1].load(std::sync::atomic::Ordering::Relaxed)
)
})
.unwrap_or_default();
+1 -1
View File
@@ -120,7 +120,7 @@ use chain_beam::{simulate_chain_beams, spawn_chain_beams};
// The container feed reads the icon column off the same catalog resource (one DBC parse).
// The one InventoryType → equipment-slot table (`attach::preview`): the dressing-room feed places a
// tried-on item by the very same map the preview it feeds dresses by (decision 1060).
pub(crate) use attach::equip_slot;
pub(crate) use attach::{equip_slot, BodyPartsDesc};
pub(crate) use equipment::ItemDisplays;
pub(crate) use equipment::{BoneAttach, Equipment};
// The instruments' read of what a body is actually WEARING vs what it resolved (`WOW_DRESS_CENSUS`).
@@ -39,6 +39,50 @@ mod preview;
pub(crate) use preview::equip_slot;
pub(super) use preview::{build_dressup_preview, build_glue_pet, build_glue_preview};
mod redress;
/// The instruments' view of one body's draw population — what `WOW_DRESS_CENSUS` prints per
/// part (`DRESS_PARTS`): index, merge group, blend, and the material id, so "why nine draws
/// for one body" is a line of output and not a reading of the M2. Lives here because the
/// group and index components are this module's private bookkeeping (0026's dev seam).
#[derive(bevy::ecs::system::SystemParam)]
pub(crate) struct BodyPartsDesc<'w, 's> {
parts: Query<
'w,
's,
(
&'static dress::DressedPart,
Option<&'static merge::DressedGroup>,
&'static benilla_world::model_render::ModelPart,
&'static MeshMaterial3d<benilla_assets::materials::WowModelMaterial>,
),
>,
}
impl BodyPartsDesc<'_, '_> {
/// One line per drawn part under `unit`, sorted by part index.
pub(crate) fn describe(&self, unit: Entity, children: &Query<&Children>) -> Vec<String> {
let mut rows: Vec<(u32, String)> = children
.iter_descendants(unit)
.filter_map(|e| self.parts.get(e).ok())
.map(|(part, group, model, mat)| {
let group = group
.map(|g| format!("{:?}", &g.0[..]))
.unwrap_or_else(|| "-".into());
(
part.index,
format!(
"DRESS_PART #{:<3} group={group:<14} blend={:?} mat={:?}",
part.index,
model.blend,
mat.0.id()
),
)
})
.collect();
rows.sort_by_key(|(i, _)| *i);
rows.into_iter().map(|(_, l)| l).collect()
}
}
pub(super) use redress::redress_player_looks;
/// Set up the skinned instance shared by creatures/players (decision 0019) and animated
+18 -9
View File
@@ -233,6 +233,24 @@ pub(super) fn drain_chat_input(
if msg.is_empty() {
continue;
}
// A line with no slash is SPEECH — which is also how a `.gm`-style server command
// travels (the server reads the dot off a SAY). That is the seam's contract
// (`UiScript::push_chat_input`): a typed line takes the stock `ChatEdit_SendText` →
// `SendChatMessage` route since 1948, and a probe's line, which never touched the edit
// box, must reach the wire the same way. The migration dropped this branch with the
// old edit-state sender, and every `WOW_PROBE_CHAT` rig — the crowd raid's forty
// `.partybot add`s, the probe shield's `.cheat god on` — went silent on the server.
if !msg.starts_with('/') {
let cmd = ClientCommand::Chat {
kind: super::edit::SendType::Say.wire(),
target: None,
text: msg.to_string(),
};
if commands.0.send(cmd).is_err() {
warn!("chat: not connected; line dropped");
}
continue;
}
queue.push((msg.to_string(), parse_line(&table, msg)));
}
for (msg, parsed) in queue {
@@ -697,15 +715,6 @@ pub(super) fn drain_chat_input(
// over the same binding the popup row calls, so it enters the same intent queue
// (decision 0646 §3; the `/duel` reasoning above, verbatim).
ParsedChat::Pvp => script.queue_pvp_toggle(),
// Run the reference's own slash body (see the variant's doc). The argument is a Lua
// string literal, so it is escaped: a `/who` filter legitimately contains quotes
// (`z-"Elwynn Forest"`), and a newline would end the statement.
ParsedChat::Social { verb, arg } => {
let lua = format!("{}(\"{}\")", verb.lua_fn(), parse::escape_lua_string(&arg));
if let Err(e) = script.run(&lua) {
warn!("ui_chat(social): {e}");
}
}
ParsedChat::Help => {
// An honest benilla summary (the ref's HELP_TEXT_LINE pages are a settings-era
// nicety; this stays useful and never stale-quotes them).
+13 -55
View File
@@ -27,32 +27,10 @@ pub(in crate::ui_chat) fn escape_lua_string(s: &str) -> String {
.collect()
}
/// Which social slash command was typed — the selector for the Lua body it runs
/// ([`ParsedChat::Social`], decision 0668).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::ui_chat) enum SocialVerb {
/// `/who [filter]` — SLASH_WHO 1-2.
Who,
/// `/friends [name]` (aliases `/friend`) — SLASH_FRIENDS 1-4.
Friends,
/// `/removefriend <name>` (alias `/remfriend`) — SLASH_REMOVEFRIEND 1-4.
RemoveFriend,
/// `/ignore [name]` — SLASH_IGNORE 1-2. Toggles: an ignored name is un-ignored.
Ignore,
/// `/unignore [name]` — SLASH_UNIGNORE 1-2.
Unignore,
}
impl SocialVerb {
/// The Lua function in `FriendsFrame.xml` holding this verb's reference body.
pub(in crate::ui_chat) fn lua_fn(self) -> &'static str {
match self {
Self::Who => "BenillaSlashWho",
Self::Friends => "BenillaSlashFriends",
Self::RemoveFriend => "BenillaSlashRemoveFriend",
Self::Ignore => "BenillaSlashIgnore",
Self::Unignore => "BenillaSlashUnignore",
}
/// A social slash line as the reference runs it: `SlashCmdList["<KEY>"](<arg>)` (1959).
fn social_body(key: &str, args: &str) -> ParsedChat {
ParsedChat::Lua {
body: format!("SlashCmdList[\"{key}\"](\"{}\")", escape_lua_string(args)),
}
}
@@ -156,13 +134,6 @@ pub(in crate::ui_chat) enum ParsedChat {
/// `/pvp` — `TogglePVP()` (decision 0646 §3). Takes no argument: the binding has no state
/// form, and the server reads the toggle from our current preference.
Pvp,
/// The social verbs (decision 0668) — `/who`, `/friends`, `/removefriend`, `/ignore`,
/// `/unignore`. Each carries the raw argument (`/who`'s is a whole filter string, not a
/// name), and each runs the reference's OWN `SlashCmdList` body, transcribed into
/// `FriendsFrame.xml` as `BenillaSlash*`: those bodies do more than send (a bare `/who`
/// opens the panel and fills its edit box, a bare `/ignore` opens the ignore list), and
/// keeping them in the FrameXML is what stops that behaviour being re-derived here.
Social { verb: SocialVerb, arg: String },
/// `/partytest [lead|raid|invite|mark|ping|off]` — the party-frame dev instrument (decision 0434, the
/// `/chattest` pattern): a synthetic roster through the real apply path (`lead` = the same
/// roster with US leading, for the leader-only popup rows), a fake pending invite for the
@@ -341,28 +312,15 @@ fn slash_command(index: SlashIndex, args: &str) -> ParsedChat {
},
S::DuelCancel => ParsedChat::Forfeit,
S::Pvp => ParsedChat::Pvp,
// The social verbs (decision 0668). The argument is passed WHOLE: `/who`'s is a filter
// expression (`z-"Elwynn Forest" 1-10`), not a name.
S::Who => ParsedChat::Social {
verb: SocialVerb::Who,
arg: args.to_string(),
},
S::Friends => ParsedChat::Social {
verb: SocialVerb::Friends,
arg: args.to_string(),
},
S::RemoveFriend => ParsedChat::Social {
verb: SocialVerb::RemoveFriend,
arg: args.to_string(),
},
S::Ignore => ParsedChat::Social {
verb: SocialVerb::Ignore,
arg: args.to_string(),
},
S::Unignore => ParsedChat::Social {
verb: SocialVerb::Unignore,
arg: args.to_string(),
},
// The social verbs (decision 0668): the reference's own `SlashCmdList` bodies, which the
// stock ChatFrame.lua carries since 1948 — and whose parser claims these lines before
// they ever reach here, so these rows are the shape kept for the day the arm is pruned
// (1948's follow-on). The argument is passed WHOLE: `/who`'s is a filter expression.
S::Who => social_body("WHO", args),
S::Friends => social_body("FRIENDS", args),
S::RemoveFriend => social_body("REMOVEFRIEND", args),
S::Ignore => social_body("IGNORE", args),
S::Unignore => social_body("UNIGNORE", args),
// The one-line reference bodies over globals benilla already implements (decision 0881,
// the 0668 posture): `InitiateTrade("target")`, `InspectUnit("target")`,
// `SetLootMethod(...)`, `RunScript(msg)`. Running the reference's own call keeps the
+2
View File
@@ -451,6 +451,8 @@ fn chat_vm() -> benilla_ui::script::UiScript {
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
crate::ui_script::load_ui_for_test(&s, file);
+5
View File
@@ -1176,6 +1176,11 @@ mod bind_confirm_tests {
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
] {
crate::ui_script::load_ui_for_test(s, file);
}
+20 -5
View File
@@ -69,10 +69,16 @@ pub(super) fn drain_pet_actions(
return;
};
let pressed = script.take_pet_actions();
let orders = script.take_pet_orders();
let toggles = script.take_pet_autocast_toggles();
let stops = script.take_pet_stop_attacks();
let writes = script.take_pet_set_actions();
if pressed.is_empty() && toggles.is_empty() && stops == 0 && writes.is_empty() {
if pressed.is_empty()
&& orders.is_empty()
&& toggles.is_empty()
&& stops == 0
&& writes.is_empty()
{
return;
}
let pet_guid = bar.spells.pet_guid;
@@ -96,10 +102,19 @@ pub(super) fn drain_pet_actions(
// an ATTACK press latch (see [`possessing`]) — which for an ordinary pet is never.
let possessing = possessing(pet_store, pet.self_guid.0);
for slot in pressed {
let Some(entry) = slot_entry(&bar, slot) else {
continue;
};
// A pressed slot and a one-shot order (`PetAttack` & co., 1958) are the same press: the
// slot's own packed word, or the word the verb synthesized — both re-enter the reference's
// one dispatcher (`0x4bd1d0`), so they share one loop. The slot number only ever labelled logs.
let presses: Vec<(u32, PetActionEntry)> = pressed
.into_iter()
.filter_map(|slot| slot_entry(&bar, slot).map(|e| (slot, e)))
.chain(
orders
.into_iter()
.map(|packed| (0, PetActionEntry::from(packed))),
)
.collect();
for (slot, entry) in presses {
// The spell arm's early exit (wow-re §10.1, `0x4bd240`–`0x4bd2ad`): a press on a spell the
// pet is already running takes the aura OFF and **returns** — `CMSG_PET_ACTION` never
// leaves, so it is a cancel, not a re-cast. Nothing is latched locally either: the icon
@@ -207,6 +207,10 @@ fn load_action_bar(s: &UiScript) {
// reference declares them in UIOptionsFrame_Init (1938) — and the manifest loads it before
// the bars.
super::test_ui::load_ui(s, "UiPanels.xml");
// The dialog engine — the keybindings page registers its two confirms into its table (1960).
super::test_ui::load_ui(s, r"Interface\FrameXML\BasicControls.xml"); // `TEXT`
super::test_ui::load_ui(s, r"Interface\FrameXML\LocaleProperties.lua"); // `GetText`
super::test_ui::load_ui(s, r"Interface\FrameXML\StaticPopup.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\UIDropDownMenu.xml");
super::test_ui::load_ui(s, "ScrollTemplates.xml");
super::test_ui::load_ui(s, "KeyBindingsPage.xml");
@@ -1371,6 +1375,9 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
+121 -6
View File
@@ -370,7 +370,56 @@ fn root_from(home: Option<PathBuf>) -> Option<PathBuf> {
/// runs on one machine load the same interface. Real ordering constraints between addons are
/// expressed by `## Dependencies:` and honoured by [`load_third_party`]'s recursion, which is the
/// mechanism the reference actually uses.
/// The reference's own LoadOnDemand addons, as its `Interface\\AddOns\\` folder names them — the
/// twelve `Blizzard_*` folders on a 1.12 install, each a `.pub` decoy on disk with the real files
/// inside the archive (the InspectUI header in the manifest). Probed against the chain, so a
/// chain without one simply has no such addon.
const BLIZZARD_ADDONS: [&str; 12] = [
"Blizzard_AuctionUI",
"Blizzard_BattlefieldMinimap",
"Blizzard_BindingUI",
"Blizzard_CombatText",
"Blizzard_CraftUI",
"Blizzard_GMSurveyUI",
"Blizzard_InspectUI",
"Blizzard_MacroUI",
"Blizzard_RaidUI",
"Blizzard_TalentUI",
"Blizzard_TradeSkillUI",
"Blizzard_TrainerUI",
];
/// The Blizzard addons the chain carries as LoadOnDemand registry rows — the ones the reference
/// reaches through `LoadAddOn` from `UIParentLoadAddOn` (1957). Two exclusions, both by shape:
/// an addon `benilla.toc` lists with everything else (the eager trio the InspectUI header
/// describes) already loaded and is not a row here; an addon whose toc is not LoadOnDemand
/// would need the startup walk to read the chain, which it does not yet — that one stays with
/// its own file until its window migrates.
fn chain_addons() -> Vec<Addon> {
let eager = super::manifest::chain_addons(&Addon::builtin().toc.files);
BLIZZARD_ADDONS
.iter()
.filter(|name| !eager.iter().any(|e| e.eq_ignore_ascii_case(name)))
.filter_map(|name| {
let bytes = super::reference_ui::read(&format!("Interface/AddOns/{name}/{name}.toc"))?;
let toc = Toc::parse(&benilla_ui::source::decode(&bytes));
toc.load_on_demand().then(|| Addon {
name: (*name).to_string(),
toc,
source: Source::Chain,
})
})
.collect()
}
fn discover() -> Vec<Addon> {
let mut found = discover_folder();
found.extend(chain_addons());
found
}
/// The player's own addons — every folder under the AddOns root with a `<Name>.toc`.
fn discover_folder() -> Vec<Addon> {
let Some(root) = root() else {
return Vec::new();
};
@@ -424,7 +473,9 @@ fn manifest_in(dir: &Path, name: &str) -> Option<String> {
/// Build the AddOn API's registry row for one discovered addon — every `.toc` directive the
/// eleven verbs can be asked about, read once here rather than re-parsed per call.
fn info_for(addon: &Addon) -> benilla_ui::script::AddOnInfo {
info_from_toc(&addon.name, &addon.toc)
let mut info = info_from_toc(&addon.name, &addon.toc);
info.chain = matches!(addon.source, Source::Chain);
info
}
/// [`info_for`]'s body, over the two things it actually reads.
@@ -462,6 +513,7 @@ pub(crate) fn info_from_toc(name: &str, toc: &Toc) -> benilla_ui::script::AddOnI
// The version gate's dword (decision 1292) — the client's own parse: leading integer,
// 0 when the manifest is silent (and 0 is out of date, not "unknown").
interface: toc.interface_version(),
chain: false, // `info_for` stamps the chain rows; the harness's rows are folder addons
}
}
@@ -553,7 +605,9 @@ impl InstalledAddOn {
/// everything as enabled.
pub(crate) fn installed(identity: Option<&(String, String)>) -> Vec<InstalledAddOn> {
let disabled = disabled_set(identity);
discover()
// The player's own folder only: the glue's list is what a player can toggle, and the
// reference's Blizzard addons are not in it.
discover_folder()
.into_iter()
.map(|addon| InstalledAddOn {
enabled: !disabled.contains(&addon.name.to_ascii_lowercase()),
@@ -697,6 +751,8 @@ pub(super) fn load_third_party(
// rather than answer whatever a previous session left, and an addon that asks before any
// exist is asking a real question.
let mut infos: Vec<_> = addons.iter().map(info_for).collect();
// The chain-sourced rows read through the reference's own store (1957).
script.set_addon_chain_reader(Box::new(super::reference_ui::read));
if let Some(text) = enable_state_path(identity).and_then(|p| std::fs::read_to_string(p).ok()) {
for (name, enabled) in parse_enable_state(&text) {
if let Some(i) = infos
@@ -925,6 +981,44 @@ impl Walk {
#[cfg(test)]
mod tests {
/// The chain's Blizzard LoadOnDemand addons are registry rows (1957): every row is
/// LoadOnDemand and read off the chain, `Blizzard_TrainerUI` is among them, and an addon
/// the manifest lists eagerly is not a row twice.
#[test]
fn the_chain_carries_blizzards_load_on_demand_addons_as_registry_rows() {
let _data = benilla_formats::wow_data_or_skip!();
let rows = chain_addons();
assert!(
rows.iter().any(|a| a.name == "Blizzard_TrainerUI"),
"the trainer addon is a row: {:?}",
rows.iter().map(|a| &a.name).collect::<Vec<_>>()
);
for a in &rows {
assert!(a.toc.load_on_demand(), "{} is LoadOnDemand", a.name);
assert!(
matches!(a.source, Source::Chain),
"{} comes off the chain",
a.name
);
assert!(
info_for(a).chain,
"{}'s registry row is marked chain-sourced",
a.name
);
}
let eager = super::super::manifest::chain_addons(&Addon::builtin().toc.files);
for e in &eager {
assert!(
!rows.iter().any(|a| a.name.eq_ignore_ascii_case(e)),
"{e} is listed in benilla.toc and must not also be a row"
);
}
// The glue's list is the player's folder alone.
assert!(installed(None)
.iter()
.all(|a| !a.name.starts_with("Blizzard_")));
}
use super::*;
/// Build an addon over a temp AddOns root, so the walk can be tested without an install.
@@ -1607,7 +1701,12 @@ mod tests {
assert!(failures.is_empty(), "load errors: {failures:?}");
// Discovery saw it at all — the `.toc` decoded rather than read as absent.
assert_eq!(script.eval::<i64>("return GetNumAddOns()").ok(), Some(1));
// The one in the folder, plus the chain's Blizzard LoadOnDemand rows on a machine that
// has a chain (1957) — the registry is one list, as the reference's is.
assert_eq!(
script.eval::<i64>("return GetNumAddOns()").ok(),
Some(1 + chain_addons().len() as i64)
);
assert_eq!(
script
.eval::<String>("return GetAddOnMetadata('Umlaut', 'Title')")
@@ -1705,7 +1804,12 @@ mod tests {
let mut script = UiScript::new().unwrap();
let _ = load_third_party(&mut script, None, true);
assert_eq!(script.eval::<i64>("return GetNumAddOns()").ok(), Some(1));
// The one in the folder, plus the chain's Blizzard LoadOnDemand rows on a machine that
// has a chain (1957) — the registry is one list, as the reference's is.
assert_eq!(
script.eval::<i64>("return GetNumAddOns()").ok(),
Some(1 + chain_addons().len() as i64)
);
assert_eq!(
script
.eval::<Vec<String>>(
@@ -1861,7 +1965,12 @@ mod tests {
let _ = load_third_party(&mut script, None, true);
// Discovered and described, but NOT run.
assert_eq!(script.eval::<i64>("return GetNumAddOns()").ok(), Some(1));
// The one in the folder, plus the chain's Blizzard LoadOnDemand rows on a machine that
// has a chain (1957) — the registry is one list, as the reference's is.
assert_eq!(
script.eval::<i64>("return GetNumAddOns()").ok(),
Some(1 + chain_addons().len() as i64)
);
assert_eq!(
script
.eval::<bool>("return IsAddOnLoadOnDemand(1) == 1")
@@ -1986,8 +2095,14 @@ mod tests {
save_enable_state(&script, Some(&id));
let written = std::fs::read_to_string(home.join("addons/Realm-Char.txt")).unwrap();
// The folder's two in registry order, then the chain's Blizzard rows (1957) — every
// registry row has a line, the reference's own one-line-per-addon format.
let mut expected = String::from("Drop: disabled\nKeep: enabled\n");
for a in chain_addons() {
expected.push_str(&format!("{}: enabled\n", a.name));
}
assert_eq!(
written, "Drop: disabled\nKeep: enabled\n",
written, expected,
"the reference's own one-line-per-addon format"
);
@@ -16,7 +16,11 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "BinderConfirm.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s.set_binder_pending(true);
s
}
@@ -37,6 +37,9 @@ fn harness() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
@@ -398,6 +398,8 @@ fn managed_positions_track_the_bottom_bar_stack() {
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
// The loader's post-load bootstrap, replayed with no bars in existence: the bare bases.
@@ -1209,6 +1209,10 @@ fn an_addons_tab_click_selects_through_the_generic_entry_point() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
// A conforming row: tabs named `<frame>Tab1..N` (what `PanelTemplates_UpdateTabs` getglobals)
@@ -48,6 +48,10 @@ fn chat_with_menu() -> UiScript {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"Interface\\FrameXML\\GlobalStrings.lua",
@@ -26,6 +26,10 @@ fn chat_ui() -> UiScript {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"Interface\\FrameXML\\GlobalStrings.lua",
@@ -29,6 +29,8 @@ fn chat_frame() -> UiScript {
// bar and the chat files; `FCF_OnUpdate` — the dock's hover, fade and drag driver — runs
// from UIParent's OnUpdate, as in the reference.
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
// `FCF_ValidateChatFramePosition` (a tab-drag stop) reads `MainMenuBar:GetHeight()`; the bar's
// own load-time chain precedes it, as in the action-bar harness.
@@ -994,6 +996,10 @@ fn the_chat_menu_builds_its_rows_on_the_references_kit() {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
r"Interface\FrameXML\UIMenu.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
@@ -28,6 +28,10 @@ fn ui_with_the_cinematic_frame() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\CinematicFrame.xml");
s.resolve();
s
@@ -26,6 +26,10 @@ fn picker() -> UiScript {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\ColorPickerFrame.xml",
] {
load_xml(&s, file);
@@ -23,11 +23,15 @@ fn load_combo_frame() -> UiScript {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
@@ -15,7 +15,11 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "DeathFrame.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s
}
@@ -38,7 +42,7 @@ fn death_popup_counts_down_and_release_queues_repop() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"5 minutes until release",
"5 Minutes until release",
"the countdown renders through the engine's DEATH per-tick text"
);
// ESC must NOT close it (no hideOnEscape on DEATH — the ref's law).
@@ -249,7 +253,7 @@ fn resurrect_request_picks_variant_and_answers() {
fn xp_loss_two_step_confirm_then_range_hide() {
let mut s = setup();
s.set_death(DeathUiState {
sickness_duration: Some("8 minutes".into()),
sickness_duration: Some("8 Minutes".into()),
spirit_healer_in_range: true,
..Default::default()
});
@@ -262,7 +266,7 @@ fn xp_loss_two_step_confirm_then_range_hide() {
.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap();
assert!(
text.contains("afflicted by 8 minutes of Resurrection Sickness"),
text.contains("afflicted by 8 Minutes of Resurrection Sickness"),
"the sickness duration formats into CONFIRM_XP_LOSS: {text}"
);
// First Accept: the AGAIN text swaps in, the dialog stays, nothing queues. XP_LOSS's verbatim
@@ -318,7 +322,7 @@ fn xp_loss_two_step_confirm_then_range_hide() {
fn xp_loss_cancel_then_reconfirm_reshows_with_the_alert_dress() {
let mut s = setup();
s.set_death(DeathUiState {
sickness_duration: Some("8 minutes".into()),
sickness_duration: Some("8 Minutes".into()),
spirit_healer_in_range: true,
..Default::default()
});
@@ -414,7 +418,7 @@ fn corpse_range_events_drive_recover_corpse() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"2 seconds until resurrection"
"2 Seconds until resurrection"
);
s.tick(1.6);
assert_eq!(
@@ -61,6 +61,17 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
// The stock DELETE_GOOD_ITEM's OnHide hands focus back to `ChatFrameEditBox` (1960).
load_xml(&s, r"Interface\FrameXML\UIMenu.xml");
load_xml(&s, r"Interface\FrameXML\ChatFrame.xml");
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
load_xml(&s, r"Interface\FrameXML\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, r"Interface\FrameXML\FloatingChatFrame.xml"); // declares ChatFrameEditBox
load_xml(&s, "UIParent.xml");
s.set_money(0);
s
}
@@ -437,13 +448,14 @@ fn no_on_the_typed_confirm_clears_and_leaves_the_box_empty_for_next_time() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// ESC out of the focused box: 1743's named divergence. The reference's `DELETE_GOOD_ITEM` names
/// no `EditBoxOnEscapePressed`, so in 1.12 the focused box swallows the key and the destroy
/// confirm cannot be dismissed with ESC at all. benilla's engine falls back to the ordinary
/// hideOnEscape leg — the entry's `OnCancel` (`ClearCursor()`), then hide — so the item is
/// released rather than left held under a dialog that will not close.
/// ESC inside the typed confirm's focused box is SWALLOWED, the reference's own behaviour (1960):
/// the stock `DELETE_GOOD_ITEM` entry defines no `EditBoxOnEscapePressed`, so the box's
/// `StaticPopup_EditBoxOnEscapePressed` finds nothing to call and the dialog stays up with the
/// item still on the cursor — the player answers with No. (ADD_FRIEND and its kin hide on ESC
/// because their entries say so; this one does not.) Our engine used to fall through to a
/// cancel here, which the earlier form of this test pinned.
#[test]
fn escape_out_of_the_typed_confirm_cancels_the_way_every_other_popup_does() {
fn escape_in_the_typed_confirms_box_is_swallowed_as_the_reference_leaves_it() {
let mut s = setup();
drop_in_world(&mut s, 871, "Flurry Axe", 4);
assert!(s
@@ -452,10 +464,15 @@ fn escape_out_of_the_typed_confirm_cancels_the_way_every_other_popup_does() {
assert!(s.key_input("ESCAPE"), "the focused box consumes ESCAPE");
assert!(
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"ESC closes it"
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"the dialog stays: the entry names no escape handler"
);
assert!(s.cursor_item().is_none(), "and runs OnCancel's ClearCursor");
assert!(
s.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
.unwrap(),
"and the box keeps focus"
);
assert!(s.cursor_item().is_some(), "the item is still on the cursor");
assert!(s.take_container_destroys().is_empty(), "ESC never destroys");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -34,7 +34,9 @@ const JERKY_LINK: &str = "|cffffffff|Hitem:117|h[Tough Jerky]|h|r";
/// with these, in manifest order.
const ROOM_UI: &[&str] = &[
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\ItemRef.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\StackSplitFrame.xml",
@@ -46,6 +48,8 @@ const ROOM_UI: &[&str] = &[
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
];
@@ -60,6 +64,10 @@ fn load_room(s: &UiScript) {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"Cooldown.xml",
@@ -115,6 +123,8 @@ fn shown_paper_doll() -> UiScript {
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
super::test_ui::load_ui_strict(&s, file);
@@ -15,7 +15,11 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "DuelFrame.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s
}
@@ -77,7 +81,7 @@ fn out_of_bounds_counts_down_and_inbounds_dismisses_it() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"Exiting duel area, you will forfeit in 10 seconds.",
"Exiting duel area, you will forfeit in 10 Seconds.",
"the engine's countdown branch fills %d %s"
);
s.fire_event("DUEL_INBOUNDS", vec![]);
@@ -20,6 +20,10 @@ fn harness() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"MinimapCluster.xml",
@@ -393,6 +397,9 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"MinimapCluster.xml",
@@ -20,7 +20,11 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "EnchantConfirm.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s
}
@@ -116,6 +116,9 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
@@ -155,6 +155,8 @@ fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
s.set_money(0);
s.set_container(0, Some(one_item_backpack()));
@@ -191,6 +193,10 @@ fn escape_closes_the_options_window_before_opening_the_menu() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml"); // the Keybindings page's faux-scroll kit (1008)
@@ -251,6 +257,8 @@ fn escape_closes_an_open_stack_split_frame() {
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
load_xml(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
s.set_money(0);
@@ -499,6 +507,10 @@ fn an_addon_frame_registered_in_uispecialframes_closes_on_escape() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameMenuFrame.xml");
// The addon's three lines, verbatim in shape.
@@ -20,6 +20,10 @@ fn an_idle_fade_driver_parks_itself_off_the_tick() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
s.resolve();
s.run(
"BENILLA_TEST_FADE_TICKS = 0\n\
@@ -59,6 +63,10 @@ fn a_started_fade_still_ramps_and_the_driver_reparks_after() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
s.resolve();
s.run(r#"CreateFrame("Frame", "BenillaFadeProbe")"#)
.unwrap();
@@ -120,6 +128,10 @@ fn a_flash_alternates_then_stops_and_the_driver_reparks() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
s.resolve();
// Parked until something asks — the whole point of the driver over UIParent's own OnUpdate.
@@ -192,12 +192,6 @@ const KNOWN: &[Known] = &[
"the enchant slot: ours inherits BenillaTradeEnchantItemTemplate, the reference's the \
ordinary PlayerTradeItemTemplate. A structural difference in our own trade window.",
},
Known {
frame: "WhoFrameDropDown",
flag: Flag::Mouse,
why: "the reference's /who sort dropdown carries its own handlers over the shared \
UIDropDownMenuTemplate; ours takes the template alone.",
},
Known {
frame: "WorldStateAlwaysUpFrame",
flag: Flag::Mouse,
@@ -603,11 +597,11 @@ fn the_shipped_frames_carry_the_references_flags() {
}
assert!(
// 1948 retired our ChatFrame.xml, 1952 our SpellBookFrame.xml, 1953 our PetActionBar.xml
// and 1956 our SkillFrame.xml (their frames are the reference's own now), which took the
// paired count from the low 400s to the high 310s; the floor guards the pairing, not the
// census.
compared > 300,
// 1948 retired our ChatFrame.xml, 1952 our SpellBookFrame.xml, 1953 our PetActionBar.xml,
// 1956 our SkillFrame.xml, 1958 our UnitPopup.xml and 1959 our FriendsFrame.xml (their
// frames are the reference's own now), which took the paired count from the low 400s to
// the high 150s; the floor guards the pairing, not the census.
compared > 120,
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
);
assert!(
+40 -207
View File
@@ -15,50 +15,9 @@ use super::test_ui::load_ui_strict as load_xml;
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
// The row right-click menu is the shared UnitPopup "FRIEND" menu, so the window's slice of
// the manifest includes it (it loads well before FriendsFrame.xml in the real order).
load_xml(&s, "UIParent.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "ScrollTemplates.xml");
// The guild pane's frames inherit the reference's shared UIPanelButtonTemplate /
// UIPanelCloseButton / UIPanelScrollFrameTemplate rather than a private copy (decision 1257),
// so this file's slice needs the shared kit — and the strict `load_xml` above is what would
// otherwise let those frames load art-less and silent.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "FriendsFrame.xml");
// The social window's fourth tab lives in its own file, and it is part of THIS window's
// manifest slice now: `BENILLA_FRIENDS_SUBFRAMES` names "RaidFrame", and both
// `FriendsFrame_ShowSubFrame` and `FriendsFrame_OnHide` resolve every name in that list
// through `getglobal` and call `:Hide()` on it. The reference's list names it too and never
// guards, because there RaidFrame.xml is FrameXML and always loaded — so the guard belongs in
// the harness's load order, not in shipped Lua defending against a state the client cannot be
// in (decision 1549).
// Stock `RaidFrame_OnLoad` reconciles the party frames the moment it loads
// (`RaidOptionsFrame_UpdatePartyFrames` -> `HidePartyFrame`/`ShowPartyFrame`), so this pane
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
// The reference's own `BasicControls.xml`, which is manifest entry 3 — far above the pane
// (1874). It is here for the error/message pair and `TEXT`. NOTE: unlike `raid_tests`, this
// harness does not load `UIParent.xml`, so `UIParentLoadAddOn` — which 1881 moved there out
// of BasicControls — is absent. Nothing this file exercises reaches it; if a future
// assertion drives `RaidFrame_LoadUI`, that is the line to add.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\BuffFrame.xml");
load_xml(&s, r"Interface\FrameXML\UnitFrame.xml");
load_xml(&s, r"Interface\FrameXML\CombatFeedback.xml");
load_xml(&s, r"Interface\FrameXML\PartyFrame.xml");
load_xml(&s, r"Interface\FrameXML\RaidFrame.xml");
load_xml(&s, r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml");
for f in super::test_ui::SOCIAL_UI {
load_xml(&s, f);
}
s
}
@@ -171,17 +130,17 @@ fn friend_rows_render_the_online_and_offline_templates() {
);
assert_eq!(
s.eval::<String>("return FriendsFrameFriendButton1NameLocation:GetText()")
s.eval::<String>("return FriendsFrameFriendButton1ButtonTextNameLocation:GetText()")
.unwrap(),
"Onerogue |cffffffff- Elwynn Forest|r <AFK>"
);
assert_eq!(
s.eval::<String>("return FriendsFrameFriendButton1Info:GetText()")
s.eval::<String>("return FriendsFrameFriendButton1ButtonTextInfo:GetText()")
.unwrap(),
"Level 60 Rogue"
);
assert_eq!(
s.eval::<String>("return FriendsFrameFriendButton2NameLocation:GetText()")
s.eval::<String>("return FriendsFrameFriendButton2ButtonTextNameLocation:GetText()")
.unwrap(),
"|cff999999Twomage - Offline|r"
);
@@ -270,8 +229,19 @@ fn the_friend_buttons_queue_their_verbs() {
benilla_ui::script::PartyRequest::InviteName(n) if n == "Twomage"
)));
// Send Message is the reference's `ChatFrame_OpenChat("/w Twomage ")`: the box shows with
// the text PENDING (`editBox.setText = 1`), applied by its next OnUpdate, whose OnTextSet
// runs the parse that turns the line into a whisper to the friend (1959).
s.run("FriendsFrame_SendMessage()").unwrap();
assert_eq!(s.take_tell_requests(), vec!["Twomage".to_string()]);
s.tick(0.05);
assert!(s
.eval::<bool>("return ChatFrameEditBox:IsVisible()")
.unwrap());
assert_eq!(
s.eval::<(String, String)>("return ChatFrameEditBox.chatType, ChatFrameEditBox.tellTarget")
.unwrap(),
("WHISPER".to_string(), "Twomage".to_string())
);
}
/// The Ignore toggle-tab swaps tab 1's list without leaving the tab, and the ignore rows render
@@ -304,12 +274,12 @@ fn the_ignore_list_is_the_other_half_of_tab_one() {
"Ignore List"
);
assert_eq!(
s.eval::<String>("return FriendsFrameIgnoreButton1Name:GetText()")
s.eval::<String>("return FriendsFrameIgnoreButton1ButtonTextName:GetText()")
.unwrap(),
"Spammer"
);
assert_eq!(
s.eval::<String>("return FriendsFrameIgnoreButton2Name:GetText()")
s.eval::<String>("return FriendsFrameIgnoreButton2ButtonTextName:GetText()")
.unwrap(),
"Ninja"
);
@@ -328,9 +298,12 @@ fn the_ignore_list_is_the_other_half_of_tab_one() {
assert!(s
.eval::<bool>("return FriendsListFrame:IsVisible()")
.unwrap());
// The reference's `ShowIgnorePanel` shows the WINDOW (its tab switch is commented out in the
// stock file), so the list that was up stays up — the friends list here (1959).
s.run("ShowIgnorePanel()").unwrap();
assert!(s.eval::<bool>("return FriendsFrame:IsVisible()").unwrap());
assert!(s
.eval::<bool>("return IgnoreListFrame:IsVisible()")
.eval::<bool>("return FriendsListFrame:IsVisible()")
.unwrap());
}
@@ -483,10 +456,13 @@ fn the_who_buttons_need_a_selected_row() {
},
"WHO_LIST_UPDATE",
);
// The reference keeps the selection across answers: `WhoList_Update` enables the buttons
// whenever `WhoFrame.selectedWho` is set, and its WHO_LIST_UPDATE arm clears nothing. (Our
// transcription dropped it; 1959.)
assert!(
!s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled() ~= 0")
s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled() ~= 0")
.unwrap(),
"a fresh answer drops the old selection"
"a fresh answer keeps the selection"
);
}
@@ -527,57 +503,6 @@ fn the_who_edit_box_sends_its_filter() {
);
}
/// The slash bodies are the reference's: a NAMED `/friends` befriends, a bare one refreshes; a
/// bare `/who` opens the panel and fills the edit box with the default filter.
#[test]
fn the_slash_bodies_match_the_reference() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
// The WhoFrame's OnLoad routes results to chat until it is shown; drain that so each
// assertion below sees only what its own call queued.
let _ = s.take_social_requests();
s.run("BenillaSlashFriends(\"Onerogue\")").unwrap();
assert_eq!(
s.take_social_requests(),
vec![SocialRequest::AddFriend("Onerogue".to_string())]
);
s.run("BenillaSlashFriends(\"\")").unwrap();
assert_eq!(
s.take_social_requests(),
vec![SocialRequest::RefreshFriends]
);
// `/ignore <name>` toggles rather than adds — the ref's AddOrDelIgnore.
s.run("BenillaSlashIgnore(\"Spammer\")").unwrap();
assert_eq!(
s.take_social_requests(),
vec![SocialRequest::ToggleIgnore("Spammer".to_string())]
);
// A bare `/who` opens the Who panel and sends the default filter, which it also shows.
s.run("BenillaSlashWho(\"\")").unwrap();
assert!(s.eval::<bool>("return WhoFrame:IsVisible()").unwrap());
let requests = s.take_social_requests();
let sent = requests
.iter()
.find_map(|r| match r {
SocialRequest::Who(filter) => Some(filter.clone()),
_ => None,
})
.expect("a bare /who still sends a query");
assert!(
sent.starts_with("z-\""),
"the default filter is zone-scoped: {sent}"
);
assert_eq!(
s.eval::<String>("return WhoFrameEditBox:GetText()")
.unwrap(),
sent,
"and the edit box shows what was sent"
);
}
/// The Add Friend button with no friendly target opens the name-entry dialog — the first
/// customer of the popup engine's `hasEditBox` capability. Accepting sends what was typed.
#[test]
@@ -604,7 +529,9 @@ fn add_friend_without_a_target_opens_the_name_dialog() {
let _ = s.take_social_requests();
s.run("StaticPopup1EditBox:SetText(\"Onerogue\")").unwrap();
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
// Through the button: the stock dialog's OnAccept reads `this:GetParent()`, which only a
// real click seats.
s.run("StaticPopup1Button1:Click()").unwrap();
assert_eq!(
s.take_social_requests(),
vec![SocialRequest::AddFriend("Onerogue".to_string())]
@@ -684,7 +611,14 @@ fn right_clicking_a_who_row_opens_the_friend_menu() {
Some(1),
"the menu has a Whisper row"
);
assert_eq!(s.take_tell_requests(), vec!["Tigole".to_string()]);
// The stock WHISPER row is `ChatFrame_SendTell(name)`: the chat box opens on the tell, its
// pending text applied by the box's next OnUpdate (1959).
s.tick(0.05);
assert_eq!(
s.eval::<(String, String)>("return ChatFrameEditBox.chatType, ChatFrameEditBox.tellTarget")
.unwrap(),
("WHISPER".to_string(), "Tigole".to_string())
);
}
/// An OFFLINE friend's right-click opens nothing — there is no verb to offer them, which is the
@@ -742,104 +676,3 @@ fn selecting_a_row_reads_back_in_the_same_tick() {
.take_social_requests()
.contains(&SocialRequest::SelectFriend(2)));
}
/// **The window's geometry, diffed against the reference FrameXML itself.**
///
/// Every number in `FriendsFrame.xml` is a transcription of one in the reference's own file, and a
/// wrong one is invisible to every other test here: the frame loads, the clicks work, only the
/// *look* is wrong — so it surfaces as a screenshot from the director, one tab at a time. Two
/// rounds of that is what this replaces. Four of the five it caught on its first run were the
/// FriendsListFrame's values that had leaked into the IgnoreListFrame subtree, which is exactly
/// the failure mode of building a sibling pane by copy-adapting instead of transcribing.
///
/// It scrapes both files for `<AbsDimension>` pairs per named element and compares the elements
/// that exist in both (ours carry a `Benilla` prefix). Deliberately narrow — it does not try to
/// understand the XML, only to notice that a number moved. Known-benign differences are listed
/// explicitly rather than filtered by a pattern, so a NEW difference can never hide inside an
/// exemption. Skips without the extracted reference.
#[test]
fn the_window_geometry_matches_the_reference_framexml() {
let _data = benilla_formats::wow_data_or_skip!();
let Some(reference) = super::framexml_diff::reference("FriendsFrame.xml") else {
eprintln!("skipping: no extracted FrameXML");
return;
};
// Differences that are ours on purpose. Each is a *deliberate* deviation with a reason, not a
// tolerance: the list is short and every entry names why.
const EXPECTED: &[&str] = &[
// The ref omits an all-zero <Offset>; we write none at all. Same anchor, fewer bytes.
"FriendsFrameFriendButton2",
"FriendsFrameFriendButton3",
"FriendsFrameFriendButton4",
"FriendsFrameFriendButton5",
"FriendsFrameFriendButton6",
"FriendsFrameFriendButton7",
"FriendsFrameFriendButton8",
"FriendsFrameFriendButton9",
"FriendsFrameFriendButton10",
"FriendsFrameIgnoreButton2",
"FriendsFrameIgnoreButton3",
"FriendsFrameIgnoreButton4",
"FriendsFrameIgnoreButton5",
"FriendsFrameIgnoreButton6",
"FriendsFrameIgnoreButton7",
"FriendsFrameIgnoreButton8",
"FriendsFrameIgnoreButton9",
"FriendsFrameIgnoreButton10",
"FriendsFrameIgnoreButton11",
"FriendsFrameIgnoreButton12",
"FriendsFrameIgnoreButton13",
"FriendsFrameIgnoreButton14",
"FriendsFrameIgnoreButton15",
"FriendsFrameIgnoreButton16",
"FriendsFrameIgnoreButton17",
"FriendsFrameIgnoreButton18",
"FriendsFrameIgnoreButton19",
"FriendsFrameIgnoreButton20",
"WhoFrameButton2",
"WhoFrameButton3",
"WhoFrameButton4",
"WhoFrameButton5",
"WhoFrameButton6",
"WhoFrameButton7",
"WhoFrameButton8",
"WhoFrameButton9",
"WhoFrameButton10",
"WhoFrameButton11",
"WhoFrameButton12",
"WhoFrameButton13",
"WhoFrameButton14",
"WhoFrameButton15",
"WhoFrameButton16",
"WhoFrameButton17",
"FriendsFrameToggleTab2",
"IgnoreFrameToggleTab2",
"FriendsFrameTopLeft",
"FriendsFrameTopRight",
"FriendsFrameBottomLeft",
"FriendsFrameBottomRight",
"WhoFrameAddFriendButton",
"WhoFrameWhoButton",
// The ref writes an all-zero <Offset> on these two row columns; we write none.
"FriendsFrameButtonTemplate/Info",
"FriendsFrameWhoButtonTemplate/Variable",
// Our column header carries the highlight's two anchors inline (the ref's sit in a
// separate <HighlightTexture> block the scrape attributes to the same element).
"WhoFrameColumnHeaderTemplate/Right",
// The three faux-scroll frames: the ref decorates each with two UI-Character-ScrollBar
// trough textures. benilla's FauxScrollFrameTemplate draws its own complete bar
// (decisions 0247/0250/0251), so the ref's loose art would double it.
"FriendsFrameFriendsScrollFrame",
"FriendsFrameIgnoreScrollFrame",
"WhoListScrollFrame",
// …and the guild pane's, the fourth of the same kind (decision 1257).
"GuildListScrollFrame",
// The window's own close button predates UIPanelTemplates.xml shipping the reference's
// `UIPanelCloseButton`, so it states inline the 32x32 that template confers. The guild
// pane's two close buttons inherit it and therefore need no entry here.
"FriendsFrameCloseButton",
];
super::framexml_diff::assert_geometry_matches("FriendsFrame.xml", &reference, EXPECTED, 210);
}
@@ -45,6 +45,10 @@ fn harness_with(extra: &[&str]) -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
]
.into_iter()
.chain(extra.iter().copied())
@@ -270,7 +274,9 @@ fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() {
// 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.
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\UnitFrame.xml",
@@ -394,7 +400,7 @@ fn player_camping_opens_a_counting_dialog_whose_early_close_cancels() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"20 seconds until logout",
"20 Seconds until logout",
"the countdown text is the engine's, from the server's 20 s clock"
);
// Cancel is the only button (the ref's CAMP_NOW is commented out in 1.12).
@@ -457,7 +463,7 @@ fn player_quiting_offers_exit_now_and_logout_cancel_closes_it() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"20 seconds until exit"
"20 Seconds until exit"
);
assert_eq!(
s.eval::<String>("return StaticPopup1Button1:GetText()")
@@ -618,7 +624,9 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
"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",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\UnitFrame.xml",
@@ -78,6 +78,9 @@ fn setup() -> UiScript {
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
@@ -552,6 +555,10 @@ fn managed_positions_engage_for_the_bare_frame_name() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// `TOOLTIP_DEFAULT_COLOR`, which the chain's dropdown backdrops read in their OnLoad — the
// dropdown kit rides in with the loot window now (1838), so this bespoke setup needs it too.
load_xml(&s, "GameTooltip.xml");
@@ -618,6 +625,8 @@ fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
s.set_loot_rolls(rolls());
@@ -203,39 +203,9 @@ fn setup() -> UiScript {
..UnitState::default()
}),
);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "FriendsFrame.xml");
// The social window's fourth tab lives in its own file, and it is part of THIS window's
// manifest slice now: `BENILLA_FRIENDS_SUBFRAMES` names "RaidFrame", and both
// `FriendsFrame_ShowSubFrame` and `FriendsFrame_OnHide` resolve every name in that list
// through `getglobal` and call `:Hide()` on it. The reference's list names it too and never
// guards, because there RaidFrame.xml is FrameXML and always loaded — so the guard belongs in
// the harness's load order, not in shipped Lua defending against a state the client cannot be
// in (decision 1549).
// Stock `RaidFrame_OnLoad` reconciles the party frames the moment it loads
// (`RaidOptionsFrame_UpdatePartyFrames` -> `HidePartyFrame`/`ShowPartyFrame`), so this pane
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
load_xml(&s, r"Interface\FrameXML\TextStatusBar.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\BuffFrame.xml");
load_xml(&s, r"Interface\FrameXML\UnitFrame.xml");
load_xml(&s, r"Interface\FrameXML\CombatFeedback.xml");
load_xml(&s, r"Interface\FrameXML\PartyFrame.xml");
load_xml(&s, r"Interface\FrameXML\RaidFrame.xml");
load_xml(&s, r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml");
for f in super::test_ui::SOCIAL_UI {
load_xml(&s, f);
}
s
}
@@ -1436,11 +1406,13 @@ fn every_list_in_the_window_takes_the_mouse_wheel() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
open(&s);
// The reference's wheel lives on each list's FauxScrollFrameTemplate scroll frame, not on
// the pane (1959).
for frame in [
"FriendsListFrame",
"IgnoreListFrame",
"WhoFrame",
"GuildFrame",
"FriendsFrameFriendsScrollFrame",
"FriendsFrameIgnoreScrollFrame",
"WhoListScrollFrame",
"GuildListScrollFrame",
] {
assert!(
s.eval::<bool>(&format!(
@@ -32,6 +32,8 @@ fn setup() -> UiScript {
"MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// The stock file's `HelpFrame_OnShow` calls `UpdateMicroButtons()` before
// `GetGMStatus()`, so without the micro row the OnShow raises and the status ask never
// happens. Ours never called it. MicroMenu needs the bar's button kit under it.
@@ -373,6 +373,9 @@ fn shown_inspect_honor_page() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// `InspectUnit`'s home since 1832.
"UIParent.xml",
"GameTooltip.xml",
@@ -121,6 +121,8 @@ fn armed() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
load_xml(&s, "UIParent.xml");
@@ -168,6 +170,8 @@ fn shipped_inspect_frame_loads_clean() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
load_xml(&s, "UIParent.xml");
@@ -93,9 +93,13 @@ fn the_self_menu_row_gates_on_the_binding_and_confirms_before_sending() {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua", // `TEXT`, which StaticPopup.lua and UnitPopup.lua read at file scope
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\BuffFrame.xml",
@@ -16,10 +16,11 @@
//! Keybindings redirect head; and the action bar's abbreviation law (`GetBindingText`
//! transcribed in UIParent.xml) reads `s-2`, the ref's own Lua.
//!
//! Labels here are the RAW tokens (`BINDING_HEADER_MOVEMENT`, `BUTTON3`): the harness loads no
//! GlobalStrings, exercising the page's `getglobal(...) or raw` fallback — the app's VM
//! executes the real 1.12 GlobalStrings.lua at boot, which turns them into "Movement Keys" /
//! "Middle Mouse". One test seeds a handful of the real strings to pin that path too.
//! Labels are read the way the page reads them — `KeyBindings_String(token, raw)`, the
//! GlobalStrings global with the raw token as fallback ([`label`]). The harness carries the
//! real 1.12 GlobalStrings.lua since the dialog engine became the reference's file (1960; the
//! stock StaticPopup.lua reads the strings at load), so a label here is the app's own
//! "Movement Keys" / "Jump", never the raw token the strings-less harness used to show.
use benilla_ui::script::keybind::{KeybindCommand, KeybindRequest};
use benilla_ui::script::{QuadContent, UiScript};
@@ -48,6 +49,10 @@ pub(crate) fn harness() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
@@ -74,6 +79,12 @@ pub(crate) fn harness() -> UiScript {
s
}
/// A label as the page resolves it: the GlobalStrings global named `token`, else `raw`.
pub(crate) fn label(s: &UiScript, token: &str, raw: &str) -> String {
s.eval::<String>(&format!(r#"return KeyBindings_String("{token}", "{raw}")"#))
.unwrap()
}
/// Open the options window on the Keybindings page.
pub(crate) fn on_page(s: &mut UiScript) {
s.run(r#"ShowUIPanel(OptionsFrame); OptionsFrame_SelectCategory("Keybindings")"#)
@@ -115,7 +126,7 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() {
let text = s
.eval::<String>(&format!("return {ROW}{}HeaderText:GetText()", i + 1))
.unwrap();
assert_eq!(&text, token, "section {} (raw-token fallback)", i + 1);
assert_eq!(text, label(&s, token, token), "section {}", i + 1);
}
assert!(
!s.eval::<bool>(&format!("return {ROW}{}:IsVisible()", expected.len() + 1))
@@ -129,14 +140,14 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() {
assert_eq!(
s.eval::<String>(&format!("return {ROW}2Description:GetText()"))
.unwrap(),
"MOVEANDSTEER"
label(&s, "BINDING_NAME_MOVEANDSTEER", "MOVEANDSTEER")
);
assert_eq!(
s.eval::<String>(&format!("return {ROW}2Key1ButtonText:GetText()"))
.unwrap(),
"BUTTON3"
label(&s, "KEY_BUTTON3", "BUTTON3")
);
// With the real strings present (the app's GlobalStrings), the same rows read 1.12 text.
// The same rows with the strings pinned by hand, so the expectation is literal 1.12 text.
s.run(
r#"BINDING_HEADER_MOVEMENT = "Movement Keys"
BINDING_NAME_MOVEANDSTEER = "Move and Steer"
@@ -164,7 +175,7 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() {
assert_eq!(
s.eval::<String>(&format!("return {ROW}2HeaderText:GetText()"))
.unwrap(),
"BINDING_HEADER_CHAT"
label(&s, "BINDING_HEADER_CHAT", "BINDING_HEADER_CHAT")
);
// Leaving the page hides its body and the Unbind button.
s.run(r#"OptionsFrame_SelectCategory("Controls")"#).unwrap();
@@ -228,10 +239,11 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
s.eval::<String>(r#"return GetBindingAction("T")"#).unwrap(),
"MOVEFORWARD"
);
let victim = label(&s, "BINDING_NAME_ATTACKTARGET", "ATTACKTARGET");
assert!(
s.eval::<String>("return OptionsFrameContainerBodyKeybindingsOutput:GetText()")
.unwrap()
.contains("ATTACKTARGET"),
.contains(&victim),
"the newly-bare victim is named"
);
// **The wheel binds like any other key** (B265, decision 1295) — including onto MOVEFORWARD,
@@ -250,7 +262,10 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
assert_eq!(
s.eval::<String>("return OptionsFrameContainerBodyKeybindingsOutput:GetText()")
.unwrap(),
"|cffff0000CAMERAZOOMIN Function is Now Unbound!|r"
format!(
"|cffff0000{} Function is Now Unbound!|r",
label(&s, "BINDING_NAME_CAMERAZOOMIN", "CAMERAZOOMIN")
)
);
// The refusal that IS there is the key-string validator, and this is the page arm that
// reports it: a chord the engine will not take restores the slot's old key and says why
@@ -293,7 +308,7 @@ fn unbind_reset_and_the_live_commit_replace_okay_cancel() {
assert_eq!(
s.eval::<String>(&format!("return {ROW}9Description:GetText()"))
.unwrap(),
"JUMP"
label(&s, "BINDING_NAME_JUMP", "JUMP")
);
s.run(&format!("{ROW}9Key1Button:Click()")).unwrap();
s.run("OptionsFrameContainerUnbind:Click()").unwrap();
@@ -593,11 +608,12 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() {
s.resolve();
// A spin over a row NAME — the dead zone when the handler lived on the sibling.
let steer = label(&s, "BINDING_NAME_MOVEANDSTEER", "MOVEANDSTEER");
let quads = s.extract();
let (wx, wy) = quads
.iter()
.find_map(|q| match &q.content {
QuadContent::Text { text: Some(t), .. } if t == "MOVEANDSTEER" => q
QuadContent::Text { text: Some(t), .. } if *t == steer => q
.rect
.map(|r| ((r.left + r.right) * 0.5, (r.bottom + r.top) * 0.5)),
_ => None,
@@ -706,7 +722,10 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() {
);
// Page side: search finds it and paints a live row with the capsule filled.
s.run(r#"OptionsFrameSearchBox:SetText("bonusactionbutton1")"#)
// The search tags are the DISPLAY names uppercased (era AddSearchTags), so the query is the
// label the app's strings give the row, not its token.
let query = label(&s, "BINDING_NAME_BONUSACTIONBUTTON1", "BONUSACTIONBUTTON1").to_lowercase();
s.run(&format!(r#"OptionsFrameSearchBox:SetText("{query}")"#))
.unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
assert!(s
@@ -715,8 +734,8 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() {
assert_eq!(
s.eval::<String>("return OptionsFrameContainerBodyKeybindSearch1Description:GetText()")
.unwrap(),
"BONUSACTIONBUTTON1",
"the raw-token fallback — the app's GlobalStrings read 'Secondary Action Button 1'"
label(&s, "BINDING_NAME_BONUSACTIONBUTTON1", "BONUSACTIONBUTTON1"),
"the row wears the app's own string"
);
assert_eq!(
s.eval::<String>("return OptionsFrameContainerBodyKeybindSearch1Key1ButtonText:GetText()")
@@ -620,6 +620,8 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
load_xml(&s, file);
@@ -93,6 +93,8 @@ fn harness_with(player: &str) -> UiScript {
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`
"MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// The chain's `PanelTemplates_SelectTab` reaches for `GameTooltip` unguarded.
"GameTooltip.xml",
"UIParent.xml", // `ShowMacroFrame` lives here now
@@ -183,6 +183,26 @@ pub(crate) fn load_default_ui(script: &UiScript) -> Vec<String> {
/// chain document means `Interface\FrameXML\ContainerFrame.xml`'s `<Script
/// file="ContainerFrame.lua"/>` reaches `Interface\FrameXML\ContainerFrame.lua` without anything
/// here knowing about it.
/// The Blizzard addons the manifest lists off the chain (`Interface\AddOns\<Name>\…` rows), by
/// name, in first-appearance order — the eager trio the InspectUI header describes, which the
/// registry must not also carry as LoadOnDemand rows (`addons::chain_addons`, 1957).
pub(crate) fn chain_addons(files: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for file in files {
let norm = file.replace('/', "\\");
let Some(rest) = norm.strip_prefix("Interface\\AddOns\\") else {
continue;
};
let Some((name, _)) = rest.split_once('\\') else {
continue;
};
if !out.iter().any(|n| n == name) {
out.push(name.to_string());
}
}
out
}
fn load_manifest(script: &UiScript, files: &[String]) -> Vec<String> {
let builtin = Addon::builtin();
let reference = reference_ui::addon(
@@ -1147,6 +1147,8 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
load_xml(&s, file);
@@ -27,6 +27,10 @@ fn harness() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MirrorTimer.xml");
s
}
@@ -43,6 +43,9 @@ fn load_action_bar(s: &UiScript) {
// MultiActionBars.xml l.39), and the manifest's since 1938.
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UiPanels.xml", // StaticPopupDialogs, which the options window's pages register into
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
@@ -284,6 +287,10 @@ fn shipped_stance_bar_drives_end_to_end() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -483,6 +490,9 @@ fn multibar_hover_renders_the_buttons_own_action() {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
@@ -584,6 +594,10 @@ fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -646,6 +660,10 @@ fn every_extra_bar_stays_down_until_its_own_toggle_is_set() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -733,6 +751,10 @@ fn raising_a_bottom_bar_moves_the_managed_bottom_stack() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -797,6 +819,10 @@ fn viewable_action_bar_pages_follow_the_bar_toggles() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -863,6 +889,10 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -966,6 +996,10 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1109,6 +1143,10 @@ fn a_bar_toggle_sends_the_byte_its_globals_pack_to() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1196,6 +1234,10 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1248,6 +1290,10 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1320,6 +1366,10 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1441,6 +1491,10 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1558,6 +1612,10 @@ fn an_extra_bars_empty_well_keeps_its_bound_hotkey_label() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -45,6 +45,10 @@ fn harness_on(mut s: UiScript) -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml", // the Keybindings page's faux-scroll kit
@@ -722,6 +726,9 @@ fn interface_harness() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
// The target-of-target pair's definer (1576) and the three files ahead of it, all in
// their manifest seats. The chain is a real load-ORDER requirement rather than
@@ -732,7 +739,8 @@ fn interface_harness() -> UiScript {
// loader takes it.
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\BuffFrame.xml",
@@ -792,6 +800,10 @@ fn chat_harness() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
@@ -844,6 +856,9 @@ fn actionbars_harness() -> UiScript {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
@@ -51,6 +51,10 @@ fn harness() -> UiScript {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
"OptionsFrameTemplates.xml",
] {
@@ -56,6 +56,11 @@ fn shipped_gossip_frame_drives_end_to_end() {
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
// The window + its scroll frame (bar + child) + the 32-row shared pool (quest rows and option
// rows both draw from it, decision 0088 §3 — the reference's own NUMGOSSIPBUTTONS) + the close
// button + the GOODBYE button. The greeting and the NPC-name banner are FontString layers (the
@@ -256,6 +261,11 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
s.set_gossip(Some(GossipMenu {
@@ -414,6 +424,11 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// Three long options — the shape of a real judgement/roleplay menu, every one of them wrapping
@@ -527,6 +542,11 @@ fn gossip_show_hide_plays_open_and_close_kits() {
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// Hidden at load: no open sound (never transitions on startup).
@@ -570,6 +590,11 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
@@ -664,6 +689,11 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
@@ -718,6 +748,11 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
@@ -821,6 +856,11 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml"); // the reference bank's slot buttons inherit it
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
@@ -907,6 +947,11 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// Eight wrapping options: ~4 lines each, far past the 334 px scroll frame.
@@ -1063,6 +1108,11 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
// The addon's three lines, in the order an addon writes them.
@@ -1138,6 +1188,11 @@ fn the_1507_registry_rows_match_the_reference_bytes() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
for probe in [
// ItemTextFrame — UIParent.lua l.20 (the B288 row).
"UIPanelWindows['ItemTextFrame'].area == 'left'",
@@ -1177,6 +1232,11 @@ fn a_dead_player_opens_only_whiledead_windows() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s.set_unit(
"player",
Some(benilla_ui::script::UnitState {
@@ -1245,6 +1305,11 @@ fn a_frame_arriving_at_center_puts_the_child_windows_away() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
// The letter is open; two shipped-row stand-ins take the seats: MerchantFrame (pushable 0)
// holds left, TradeFrame (pushable 1) then ARRIVES at center (UIParent.lua l.734-741's
@@ -35,6 +35,8 @@ pub(super) fn load_pet_bar(s: &UiScript) {
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
@@ -31,7 +31,8 @@ fn load_pet_frame() -> UiScript {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
@@ -96,6 +96,8 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// `QuestTitleButtonTemplate`, which the reference's registrar inherits for its two service
// rows — 1.12 declares it in QuestFrameTemplates.xml, an `<Include>` of QuestFrame.xml, and
// ours declares it in QuestFrame.xml directly. An unknown template is a loader WARNING, so
@@ -18,7 +18,11 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "QuestShareFrame.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s
}
@@ -24,6 +24,9 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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, "Interface\\FrameXML\\MerchantFrame.xml");
@@ -91,6 +94,9 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -186,6 +192,9 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -285,6 +294,9 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -361,6 +373,9 @@ fn greeting_goodbye_button_closes_the_window() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -415,6 +430,9 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -540,6 +558,9 @@ fn write_on_still_fades_when_instant_text_is_off() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -614,6 +635,9 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -694,6 +718,9 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -791,6 +818,9 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -36,6 +36,9 @@ fn harness() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -91,6 +91,9 @@ fn shipped_questlog_frame_loads_clean() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -121,6 +124,9 @@ fn shipped_questlog_frame_drives_end_to_end() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -327,6 +333,9 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -471,6 +480,9 @@ fn watch_guards_no_op_without_erroring() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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
@@ -584,6 +596,9 @@ fn progress_auto_watches_for_five_minutes() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -665,6 +680,9 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// The flag is declared by the options window's init since 1944, as the reference declares it
// in UIOptionsFrame_Init — so the window that owns it loads first, as in the manifest.
load_xml(&s, "UIParent.xml");
@@ -727,6 +745,9 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -802,6 +823,9 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -863,6 +887,9 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1048,6 +1075,9 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1119,6 +1149,9 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1180,6 +1213,9 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1253,6 +1289,9 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// This used to load LootFrame.xml for `BENILLA_LOOT_QUALITY_COLORS`, an alias our own copy of
// that window set to `ITEM_QUALITY_COLORS`. 1751 retired the window and the alias with it, so
// Fonts.xml alone is the source now. Formerly: it
@@ -1351,6 +1390,10 @@ fn popup_children_inherit_the_dialog_stratum() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
s.run(
"StaticPopupDialogs[\"TEST_STRATUM\"] = { text = \"Abandon?\", button1 = \"Yes\", \
button2 = \"No\", timeout = 0 }\n\
@@ -1404,6 +1447,9 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1499,6 +1545,9 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1605,6 +1654,9 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1688,6 +1740,9 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
@@ -1725,6 +1780,9 @@ fn share_quest_click_queues_the_selected_quests_id() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.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 —
+3 -31
View File
@@ -24,37 +24,9 @@ use super::test_ui::load_ui_strict as load_xml;
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "FriendsFrame.xml");
// Stock `RaidFrame_OnLoad` reconciles the party frames the moment it loads
// (`RaidOptionsFrame_UpdatePartyFrames` -> `HidePartyFrame`/`ShowPartyFrame`), so this pane
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
// The reference's own `BasicControls.xml`, which is manifest entry 3 — far above the pane
// (1874). `UIParentLoadAddOn` used to be what this line was here for; 1881 moved that
// function to `UIParent.xml`, its home in the reference, and this harness already loads
// `UIParent.xml` above, so the `RaidFrame_LoadUI` arm on `RAID_ROSTER_UPDATE` still
// resolves. What is left here is the error/message pair and `TEXT`.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\BuffFrame.xml");
load_xml(&s, r"Interface\FrameXML\UnitFrame.xml");
load_xml(&s, r"Interface\FrameXML\CombatFeedback.xml");
load_xml(&s, r"Interface\FrameXML\PartyFrame.xml");
load_xml(&s, r"Interface\FrameXML\RaidFrame.xml");
load_xml(&s, r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml");
for f in super::test_ui::SOCIAL_UI {
load_xml(&s, f);
}
s
}
+106 -15
View File
@@ -1471,6 +1471,16 @@ mod tests {
// reads clean. Each belongs to whichever window's migration left it, and each is one
// binding or one sourced file away.
const KNOWN: &[(&str, &str, &str)] = &[
(
"ClassTrainerFrameTemplates.xml",
"ClassTrainerSkillButton_OnClick",
"defined by `Blizzard_TrainerUI`, which is a LoadOnDemand addon here as it is in \
the reference (1957): it loads on the first trainer, through UIParent's \
TRAINER_SHOW arm, and the skill-button template that carries this handler is \
instantiated only by that addon, so no click can reach the name before its \
definer exists. The reference is in exactly this state until its addon loads. \
Permanent by design, not a gap to close.",
),
(
"ContainerFrame.xml",
"KeyRingButtonIDToInvSlotID",
@@ -1506,16 +1516,6 @@ mod tests {
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
skill-point wire exists; not built (1956).",
),
(
"ClassTrainerFrameTemplates.xml",
"ClassTrainerSkillButton_OnClick",
"defined by `Blizzard_TrainerUI`, a LoadOnDemand addon we do not load yet. The \
file is on the manifest for ONE of its templates — the icon chooser's \
`ClassTrainerListScrollFrameTemplate` (1862) — and the skill-button template that \
carries this handler is never instantiated, so no click can reach it. The \
reference is in the same state until its trainer addon loads on demand. It \
resolves when that window migrates.",
),
(
"DurabilityFrame.xml",
"UpdateInventoryAlertStatus",
@@ -1524,11 +1524,70 @@ mod tests {
instead, so the recompute exists and only the Lua verb that forces one does not.",
),
(
"ChatFrame.xml",
"value",
"not a global: `for index, value in SlashCmdList do … value(msg)` \
(ChatFrame.lua l.2168-2171) calls the addon's handler through the table's loop \
variable. The scan has no scopes, so the name reads as a call.",
"StaticPopup.xml",
"AcceptAreaSpiritHeal",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by AREA_SPIRIT_HEAL's Accept, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"CancelAreaSpiritHeal",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by AREA_SPIRIT_HEAL's Cancel, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"GetAreaSpiritHealerTime",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by AREA_SPIRIT_HEAL's OnShow, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"AcceptBattlefieldPort",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_BATTLEFIELD_ENTRY / CONFIRM_LEAVE_QUEUE, raised only by BattlefieldFrame.lua, which is not on the chain yet.",
),
(
"StaticPopup.xml",
"BeginTrade",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by the TRADE dialog, which 1.12.1 can never show (wow-re `incoming-trade-request-law.md`: TRADE_REQUEST is signalled by nothing).",
),
(
"StaticPopup.xml",
"CancelTrade",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by the TRADE dialog, which 1.12.1 can never show (wow-re `incoming-trade-request-law.md`: TRADE_REQUEST is signalled by nothing).",
),
(
"StaticPopup.xml",
"ReplaceTradeEnchant",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by TRADE_REPLACE_ENCHANT's Accept, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"CancelMeetingStoneRequest",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_LEAVE_QUEUE's meeting-stone arm, raised only by BattlefieldFrame.lua / the meeting-stone gossip, neither on the chain yet.",
),
(
"StaticPopup.xml",
"CheckPetUntrainerDist",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_PET_UNLEARN's OnUpdate, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"ConfirmPetUnlearn",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_PET_UNLEARN's Accept, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"GetInstanceBootTimeRemaining",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by INSTANCE_BOOT's OnShow, an event this engine does not fire yet.",
),
];
@@ -1571,6 +1630,37 @@ mod tests {
// are not are the ones a stat tooltip is built out of: `strupper`, `strsub`, `abs`,
// `max`, `floor`, `format`, `getglobal`. `reference/1.12-globals.tsv` is the filter
// that actually belongs here, and it does not care about case.
// Names the file binds LOCALLY — `local X`, `local function X`, a `for` loop's
// variables — are not globals however they are called: the stock StaticPopup.lua
// reads a dialog's handlers into locals (`local OnAccept = …; OnAccept(…)`) and
// ChatFrame.lua walks `SlashCmdList` with `for index, value in …; value(msg)`.
let mut locals: std::collections::HashSet<String> = std::collections::HashSet::new();
for line in text.lines() {
let l = line.trim_start();
let rest = if let Some(r) = l.strip_prefix("local function ") {
Some(r)
} else if let Some(r) = l.strip_prefix("local ") {
Some(r)
} else {
l.strip_prefix("for ")
};
if let Some(rest) = rest {
for name in rest
.split(['=', ' ', '\t'])
.take_while(|w| *w != "in" && *w != "=" && !w.starts_with('('))
.flat_map(|w| w.split(','))
.map(|w| w.trim())
.filter(|w| !w.is_empty())
{
let name: String =
name.chars().take_while(|c| super::is_word(*c)).collect();
if !name.is_empty() {
locals.insert(name);
}
}
}
}
let b: Vec<char> = text.chars().collect();
let mut called: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut i = 0;
@@ -1599,6 +1689,7 @@ mod tests {
let mut gaps: Vec<&String> = called
.iter()
.filter(|n| !defines.contains(*n))
.filter(|n| !locals.contains(*n))
.filter(|n| reference.contains(n.as_str()))
.filter(|n| !have.contains(*n))
.collect();
@@ -19,6 +19,10 @@ fn harness() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"ScreenshotStatus.xml",
] {
@@ -106,6 +106,8 @@ pub(super) fn spellbook_ui(w: f32, h: f32) -> UiScript {
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
@@ -131,6 +133,10 @@ fn shipped_spellbook_loads_clean() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
// The reference's own file, off the chain, with the one adapter it needs from this engine.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
@@ -679,6 +685,8 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`
"MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// `ShowMacroFrame` lives here since 1848.
"UIParent.xml",
"GameTooltip.xml",
@@ -15,6 +15,10 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
s
}
@@ -78,7 +82,7 @@ fn start_delay_gates_button1_then_swaps_the_text_in() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"2 seconds until resurrection",
"2 Seconds until resurrection",
"the delayText countdown renders (ceil of 1.5s)"
);
s.tick(1.6);
@@ -181,14 +185,14 @@ fn the_death_countdown_text_rerenders_each_tick() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"2 minutes until release",
"2 Minutes until release",
"above 60s renders ceil-minutes"
);
s.tick(31.0);
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"59 seconds until release",
"59 Seconds until release",
"below 60s renders seconds"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -31,7 +31,11 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "ConfirmSummon.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s.set_summon_confirm(SummonConfirmUiState {
summoner: "Twomage".into(),
area: "Stormwind City".into(),
@@ -66,7 +70,7 @@ fn the_confirm_names_the_summoner_and_accept_queues_the_response() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"Twomage wants to summon you to Stormwind City. The spell will be cancelled in 2 minutes.",
"Twomage wants to summon you to Stormwind City. The spell will be cancelled in 2 Minutes.",
"the tick composes summoner + area + the count and its unit word"
);
@@ -93,7 +97,7 @@ fn the_countdown_line_switches_to_seconds_and_singularises() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"Twomage wants to summon you to Elwynn Forest. The spell will be cancelled in 45 seconds."
"Twomage wants to summon you to Elwynn Forest. The spell will be cancelled in 45 Seconds."
);
// OnShow seeded 45 and the first tick spent 0.1 of it, so 44.9 stands; spending 44.2 more
@@ -103,7 +107,7 @@ fn the_countdown_line_switches_to_seconds_and_singularises() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"Twomage wants to summon you to Elwynn Forest. The spell will be cancelled in 1 second."
"Twomage wants to summon you to Elwynn Forest. The spell will be cancelled in 1 Second."
);
}
@@ -124,7 +128,7 @@ fn a_summoner_whose_name_is_still_resolving_fills_in_on_a_later_tick() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
" wants to summon you to Stormwind City. The spell will be cancelled in 30 seconds.",
" wants to summon you to Stormwind City. The spell will be cancelled in 30 Seconds.",
"no name yet: a blank, not a raise and not a withheld dialog"
);
@@ -137,7 +141,7 @@ fn a_summoner_whose_name_is_still_resolving_fills_in_on_a_later_tick() {
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"Twomage wants to summon you to Stormwind City. The spell will be cancelled in 30 seconds.",
"Twomage wants to summon you to Stormwind City. The spell will be cancelled in 30 Seconds.",
"the name query landed; the same open dialog picks it up"
);
}
@@ -17,7 +17,11 @@ fn setup() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "TalentWipeConfirm.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
s.set_talent_master_pending(true);
s
}
@@ -34,7 +34,8 @@ fn harness() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE, and its
// declarer is UIParent (ref UIParent.lua:65) since 1888.
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
@@ -18,6 +18,9 @@ fn taxi_script() -> UiScript {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml"); // TaxiNodeOnButtonEnter's tooltip + SetTooltipMoney
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // BenillaErrorsFrame_AddMessage — DrawOneHopLines' refusal
// Three the reference's own TaxiFrame leans on that our transcription did not:
+58 -2
View File
@@ -149,6 +149,8 @@ pub(super) const MERCHANT_UI: &[&str] = &[
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml", // app load order: tooltip before merchant
];
@@ -160,6 +162,9 @@ pub(super) const LOOT_UI: &[&str] = &[
"UiPanels.xml", // StaticPopup, and the LOOT_BIND / CONFIRM_LOOT_DISTRIBUTION dialogs
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml", // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
"Interface\\FrameXML\\UIDropDownMenu.xml", // GroupLootDropDown's OnLoad calls UIDropDownMenu_Initialize
// `UnitPopup.xml` reads ITEM_QUALITY_COLORS at FILE SCOPE for its three loot-threshold rows,
@@ -167,7 +172,8 @@ pub(super) const LOOT_UI: &[&str] = &[
// That declarer is UIParent (ref UIParent.lua:65); 1888 moved the table there when Fonts.xml
// went on the chain, because the reference's Fonts.xml does not declare it.
"UIParent.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
// …and what its rows' OnLoad calls: every `PartyMemberFrame<N>` and its pet frame runs
// `UnitFrame_Initialize`, which lives in UnitFrame.lua and itself calls
// `SetTextStatusBarText` out of TextStatusBar.lua. Naming PartyFrame without these loads
@@ -238,12 +244,13 @@ pub(super) const CHARACTER_UI: &[&str] = &[
"Cooldown.xml", // CooldownFrameTemplate + CooldownFrame_SetTimer, per equipment slot
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\StaticPopup.xml",
// The unit frames' four right-click dropdowns call `UIDropDownMenu_Initialize` at LOAD, so the
// kit and the menu table it initialises from both precede them — the manifest's own order
// (175 → 189 → 193 → 263).
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\UIMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
// `UnitFrame_Initialize` and `CombatFeedback_Initialize`, which the two windows below call in
@@ -288,6 +295,52 @@ pub(super) const CHARACTER_UI: &[&str] = &[
"Interface\\FrameXML\\HonorFrame.xml",
];
/// The social window's slice of the manifest (1959): everything the reference's
/// `FriendsFrame.xml` + `RaidFrame.xml` reach at load or on show — `TEXT` and `GetText`, the
/// bar chain under `UpdateMicroButtons`, the chat window whose edit box `FriendsFrame_SendMessage`
/// opens, the unit menu, the party frames `RaidFrame_OnLoad` reconciles, and the raid tab's
/// LoadOnDemand addon — in the manifest's own order. Shared by the friends, guild and raid
/// harnesses.
pub(super) const SOCIAL_UI: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml",
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"Interface\\FrameXML\\OptionsFrameTemplates.xml",
"Interface\\FrameXML\\ReputationFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
"MicroMenu.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
"Interface\\FrameXML\\BuffFrame.xml",
"Interface\\FrameXML\\UnitFrame.xml",
"Interface\\FrameXML\\CombatFeedback.xml",
"Interface\\FrameXML\\PartyFrame.xml",
"Interface\\FrameXML\\FriendsFrame.xml",
"Interface\\FrameXML\\RaidFrame.xml",
"Interface\\AddOns\\Blizzard_RaidUI\\Blizzard_RaidUI.xml",
];
pub(super) const BAG_UI: &[&str] = &[
// The reference's own localized strings — `BACKPACK_TOOLTIP`, `EQUIP_CONTAINER`, `KEYRING`,
// the `*_FONT_COLOR_CODE` pair. The app loads this at VM setup, ahead of the manifest
@@ -311,6 +364,7 @@ pub(super) const BAG_UI: &[&str] = &[
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"GameTooltip.xml",
"Cooldown.xml",
// The bag BAR declares `parent="MainMenuBarArtFrame"`, resolved at LOAD — so without this the
@@ -327,6 +381,8 @@ pub(super) const BAG_UI: &[&str] = &[
"MicroMenu.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The dialog engine, after the UIPanelCloseButton it inherits (1960).
r"Interface\FrameXML\StaticPopup.xml",
"Interface\\FrameXML\\ContainerFrame.xml",
// `PaperDollItemSlotButtonTemplate` and the `PaperDollItemSlotButton_*` family behind it,
// which every bag button inherits and runs — resolved at load, so this has to precede the bar
@@ -99,7 +99,8 @@ fn unit_frame_hover_takes_the_default_corner_and_drops_on_leave() {
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\BuffFrame.xml",
"Interface\\FrameXML\\UnitFrame.xml",
"Interface\\FrameXML\\CombatFeedback.xml",
@@ -165,7 +166,8 @@ fn your_own_portrait_explains_the_menu_instead_of_showing_your_health() {
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\BuffFrame.xml",
"Interface\\FrameXML\\UnitFrame.xml",
"Interface\\FrameXML\\CombatFeedback.xml",
@@ -290,6 +292,9 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
@@ -65,7 +65,8 @@ fn load_tot() -> UiScript {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
+200 -114
View File
@@ -2,7 +2,7 @@
//! `assets/ui/TrainerFrame.xml` — a client-sorted, collapsible **skill-line tree** with a **dropdown**
//! state filter and a draggable **scroll bar** (decisions 0247/0251) — loaded behind its deps
//! (`UiPanels.xml` + `UIDropDownMenu.xml` + `ScrollTemplates.xml` + `MerchantFrame.xml` for the
//! `BenillaMoney_*` coin helpers) and fed a synthetic service list. Covers what only a runtime load
//! the stock money frames) and fed a synthetic service list. Covers what only a runtime load
//! exercises: the Lua parses and every referenced global resolves, the tree renders interleaved
//! header/service rows, a header click folds its group, the dropdown filter hides a state, the wheel
//! scrolls the list, the NPC name rides `arg1` into the title, the byte-exact GlobalStrings render, the
@@ -22,27 +22,83 @@ use super::test_ui::load_ui as load_xml;
/// pushes into the engine on every show — so a test that wants the full tree sets those, not the
/// engine's own `SetTrainerServiceTypeFilter`, which the next `TRAINER_SHOW` would overwrite.
fn trainer_script() -> UiScript {
let mut s = trainer_script_base();
// The reference's own LoadOnDemand addon's files, direct off the chain (what its `LoadAddOn`
// runs), and the FrameXML templates file its list inherits from (1957).
load_xml(
&s,
"Interface\\AddOns\\Blizzard_TrainerUI\\Blizzard_TrainerUI.xml",
);
finish_trainer_load(&mut s);
s
}
/// Everything the manifest loads before the trainer addon — the chain a LoadOnDemand load lands
/// on. The stock row's label is a width-0 `<ButtonText>` (fit the text), so a measurer is seated
/// the way the app's VM has one at world entry.
fn trainer_script_base() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the kit's MenuBackdrop), app order
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the filter dropdown's kit
load_xml(&s, "ScrollTemplates.xml"); // the faux-scroll bar kit
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // the merchant kit; the money frames are MoneyFrame.xml's (1937)
load_xml(&s, "TrainerFrame.xml");
s.run(
"TRAINER_FILTER_AVAILABLE = 1 TRAINER_FILTER_UNAVAILABLE = 1 TRAINER_FILTER_USED = 1 \
BenillaTrainerFrame_ApplyFilter()",
)
.unwrap();
s.set_text_measurer(Box::new(super::FixedWidthFont(7.0)));
// The reference's window calls `UpdateMicroButtons` on show and inherits the panel kit's
// templates, so the harness carries what the manifest loads before it, in the manifest's
// order (ScrollTemplates BEFORE UIPanelTemplates, 1846) — and the reference's own
// LoadOnDemand addon last, with the FrameXML templates file its list inherits from (1957).
for f in [
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
"MicroMenu.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\ClassTrainerFrameTemplates.xml",
] {
load_xml(&s, f);
}
s
}
/// What follows the addon's files in its LoadOnDemand load (1957): the saved chunk (none in a
/// bare harness) and then its ADDON_LOADED, whose arm pushes the three filter globals into the
/// engine. The harness wants "used" shown too. The stock title reads `UnitName("npc")`, so the
/// trainer NPC is seated here as the app seats it on TRAINER_SHOW.
fn finish_trainer_load(s: &mut UiScript) {
s.set_unit(
"npc",
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Sana Winterhoof".into()),
..Default::default()
}),
);
s.run("TRAINER_FILTER_USED = 1").unwrap();
s.fire_event(
"ADDON_LOADED",
vec![ScriptValue::Str("Blizzard_TrainerUI".into())],
);
}
/// Whether any rendered text quad carries `color` (within a small tolerance) — used to spot the
/// reddened cost coins, whose `(1.0, 0.1, 0.1)` is distinct from the unavailable row's `(0.9, 0, 0)`.
fn has_text_color(quads: &[ExtractedQuad], color: [f32; 3]) -> bool {
@@ -192,7 +248,7 @@ fn shipped_trainer_frame_drives_end_to_end() {
// Hidden by default.
assert!(!s
.eval::<bool>("return BenillaTrainerFrame:IsVisible()")
.eval::<bool>("return ClassTrainerFrame:IsVisible()")
.unwrap());
// The app's feed: 50 copper in the purse + the warrior menu, then the open event with the name.
@@ -206,22 +262,22 @@ fn shipped_trainer_frame_drives_end_to_end() {
// Shown, title took the NPC name off arg1.
assert!(s
.eval::<bool>("return BenillaTrainerFrame:IsVisible()")
.eval::<bool>("return ClassTrainerFrame:IsVisible()")
.unwrap());
assert_eq!(
s.eval::<String>("return BenillaTrainerTitleText:GetText()")
s.eval::<String>("return ClassTrainerNameText:GetText()")
.unwrap(),
"Sana Winterhoof"
);
// Row 1 renders the "Arms" header (its name, no indent); row 2 the first service.
assert_eq!(
s.eval::<String>("return BenillaTrainerService1Text:GetText()")
s.eval::<String>("return ClassTrainerSkill1Text:GetText()")
.unwrap(),
"Arms"
);
assert_eq!(
s.eval::<String>("return BenillaTrainerService2Text:GetText()")
s.eval::<String>("return ClassTrainerSkill2Text:GetText()")
.unwrap(),
" Heroic Strike"
);
@@ -233,12 +289,12 @@ fn shipped_trainer_frame_drives_end_to_end() {
2
);
assert_eq!(
s.eval::<String>("return BenillaTrainerCostLabel:GetText()")
s.eval::<String>("return ClassTrainerCostLabel:GetText()")
.unwrap(),
"Cost:"
);
assert!(s
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
.eval::<bool>("return ClassTrainerTrainButton:IsEnabled() ~= 0")
.unwrap());
// Train buys the selected service — the row's spell id reaches the app's drain.
@@ -248,11 +304,11 @@ fn shipped_trainer_frame_drives_end_to_end() {
assert!(s.take_trainer_buys().is_empty(), "drained");
// Select the available-but-unaffordable service (Thunder Clap, index 6, 500c > 50c): Train disables
// and the cost coins redden (BenillaMoney_SetColor 1.0, 0.1, 0.1).
s.run("SelectTrainerService(6); BenillaTrainerFrame_Update()")
// and the cost coins redden (SetMoneyFrameColor 1.0, 0.1, 0.1).
s.run("this = ClassTrainerSkill6; ClassTrainerSkillButton_OnClick('LeftButton')")
.unwrap();
assert!(!s
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
.eval::<bool>("return ClassTrainerTrainButton:IsEnabled() ~= 0")
.unwrap());
s.resolve();
assert!(
@@ -262,10 +318,10 @@ fn shipped_trainer_frame_drives_end_to_end() {
// Select the gated service (Cleave, index 3): the Requires: line is built from the level/skill/
// ability gates (byte-exact REQUIRES_LABEL), and Train stays disabled (unavailable).
s.run("SelectTrainerService(3); BenillaTrainerFrame_Update()")
s.run("this = ClassTrainerSkill3; ClassTrainerSkillButton_OnClick('LeftButton')")
.unwrap();
let reqs = s
.eval::<String>("return BenillaTrainerSkillRequirements:GetText()")
.eval::<String>("return ClassTrainerSkillRequirements:GetText()")
.unwrap();
assert!(reqs.starts_with("Requires: "), "reqs: {reqs}");
for term in ["Level", "Swords", "Charge"] {
@@ -279,14 +335,14 @@ fn shipped_trainer_frame_drives_end_to_end() {
"a known prerequisite shows white with its rank: {reqs}"
);
assert!(!s
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
.eval::<bool>("return ClassTrainerTrainButton:IsEnabled() ~= 0")
.unwrap());
// The app's client-side close: clear the snapshot + fire TRAINER_CLOSED → the window hides.
s.set_trainer(None);
s.fire_event("TRAINER_CLOSED", vec![]);
assert!(!s
.eval::<bool>("return BenillaTrainerFrame:IsVisible()")
.eval::<bool>("return ClassTrainerFrame:IsVisible()")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -304,25 +360,25 @@ fn clicking_a_header_row_collapses_its_group() {
// Full tree: 2 headers + 4 services = 6 rows; row 2 is Heroic Strike.
assert_eq!(s.eval::<i64>("return GetNumTrainerServices()").unwrap(), 6);
assert_eq!(
s.eval::<String>("return BenillaTrainerService2Text:GetText()")
s.eval::<String>("return ClassTrainerSkill2Text:GetText()")
.unwrap(),
" Heroic Strike"
);
// Click the Arms header (row 1): its two services fold → 4 rows (H:Arms, H:Fury, Rend, Thunder
// Clap). Row 2 is now the Fury header.
s.run("BenillaTrainerSkillButton_OnClick(BenillaTrainerService1, 'LeftButton')")
s.run("this = ClassTrainerSkill1; ClassTrainerSkillButton_OnClick('LeftButton')")
.unwrap();
assert_eq!(s.eval::<i64>("return GetNumTrainerServices()").unwrap(), 4);
assert_eq!(
s.eval::<String>("return BenillaTrainerService2Text:GetText()")
s.eval::<String>("return ClassTrainerSkill2Text:GetText()")
.unwrap(),
"Fury",
"Arms folded; its header (row 1) now abuts the Fury header (row 2)"
);
// Click it again → expands back to 6 rows.
s.run("BenillaTrainerSkillButton_OnClick(BenillaTrainerService1, 'LeftButton')")
s.run("this = ClassTrainerSkill1; ClassTrainerSkillButton_OnClick('LeftButton')")
.unwrap();
assert_eq!(s.eval::<i64>("return GetNumTrainerServices()").unwrap(), 6);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -342,7 +398,9 @@ fn filter_hides_a_state_keeping_headers() {
// Drive the dropdown's own click handler for the "used" row: the row button rides as `this` with
// its pre-click state (checked=on, value="used"), exactly as UIDropDownMenuButton_OnClick invokes
// the row func. It flips the engine filter off and repaints. Rend drops (6 → 5); headers remain.
s.run("this = { value = 'used', checked = 1 }; BenillaTrainerFilterDropDown_OnClick()")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap();
s.run("this = DropDownList1Button3; UIDropDownMenuButton_OnClick()")
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
@@ -373,7 +431,7 @@ fn filter_rows_toggle_through_the_dropdown_kit() {
// Open the menu the way the capsule's arrow does. Row 2 is "Unavailable" (Initialize's order),
// checked because trainer_script() turned every state on.
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
let row_checked = |s: &mut UiScript| {
@@ -416,9 +474,9 @@ fn filter_rows_toggle_through_the_dropdown_kit() {
// what the clicks left behind (the two states left on, "used" still on from trainer_script()).
s.run("this = DropDownList1Button2; UIDropDownMenuButton_OnClick()")
.unwrap();
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap(); // same owner → closes
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap(); // re-open → re-Initialize
assert!(
!row_checked(&mut s),
@@ -461,11 +519,11 @@ fn wheel_over_a_row_scrolls_the_list() {
s.fire_event("TRAINER_SHOW", vec![ScriptValue::Str("Sana".into())]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
let row1 = "return BenillaTrainerService1Text:GetText()";
let row1 = "return ClassTrainerSkill1Text:GetText()";
// At the top: row 1 is the "Arms" header, row 2 the first service.
assert_eq!(s.eval::<String>(row1).unwrap(), "Arms");
assert_eq!(
s.eval::<String>("return BenillaTrainerService2Text:GetText()")
s.eval::<String>("return ClassTrainerSkill2Text:GetText()")
.unwrap(),
" Service 01"
);
@@ -486,7 +544,7 @@ fn wheel_over_a_row_scrolls_the_list() {
// reference's and is asserted as "a page, not a row"; pinning the exact row count would pin a
// pixel arithmetic whose inputs resolve a frame apart.
let offset = s
.eval::<i64>("return BenillaTrainerListScrollFrame.offset or -1")
.eval::<i64>("return ClassTrainerListScrollFrame.offset or -1")
.unwrap();
assert!(
offset > 1,
@@ -503,7 +561,7 @@ fn wheel_over_a_row_scrolls_the_list() {
// there is the clamp working, not the wheel failing.
s.mouse_wheel(x, y, -1.0);
assert_eq!(
s.eval::<i64>("return BenillaTrainerListScrollFrame.offset or -1")
s.eval::<i64>("return ClassTrainerListScrollFrame.offset or -1")
.unwrap(),
offset,
"clamped at the bottom (numItems - numToDisplay), never past it"
@@ -513,7 +571,7 @@ fn wheel_over_a_row_scrolls_the_list() {
s.mouse_wheel(x, y, 1.0);
s.mouse_wheel(x, y, 1.0);
assert_eq!(
s.eval::<i64>("return BenillaTrainerListScrollFrame.offset or -1")
s.eval::<i64>("return ClassTrainerListScrollFrame.offset or -1")
.unwrap(),
0,
"back at the top, clamped"
@@ -545,13 +603,13 @@ fn a_selected_or_hovered_service_row_paints_its_name_white() {
// The name really is the button's own label now — a child FontString would leave
// GetFontString() nil and no per-state font could reach it.
assert!(
s.eval::<bool>("return BenillaTrainerService1:GetFontString() ~= nil")
s.eval::<bool>("return ClassTrainerSkill1:GetFontString() ~= nil")
.unwrap(),
"the row name is the Button's ButtonText, the only region per-state fonts reach"
);
// Cleave (index 3) is unavailable → red when not selected. Select it → its name goes white.
s.run("SelectTrainerService(3); BenillaTrainerFrame_Update()")
s.run("this = ClassTrainerSkill3; ClassTrainerSkillButton_OnClick('LeftButton')")
.unwrap();
s.resolve();
assert!(
@@ -560,7 +618,7 @@ fn a_selected_or_hovered_service_row_paints_its_name_white() {
);
// Select Heroic Strike (index 2): Cleave is no longer selected → back to unavailable red.
s.run("SelectTrainerService(2); BenillaTrainerFrame_Update()")
s.run("this = ClassTrainerSkill2; ClassTrainerSkillButton_OnClick('LeftButton')")
.unwrap();
s.resolve();
assert!(
@@ -617,7 +675,7 @@ fn wheel_scroll_is_silent_but_the_arrows_click() {
);
// The down arrow (enabled at the top), though — clicking it plays the ref's arrow click.
s.run("BenillaTrainerListScrollFrameScrollBarScrollDownButton:Click()")
s.run("ClassTrainerListScrollFrameScrollBarScrollDownButton:Click()")
.unwrap();
assert!(
s.take_sounds().contains(&click),
@@ -641,12 +699,12 @@ fn the_scrollbar_arrows_step_the_list_the_way_they_point() {
s.set_trainer(Some(long_menu())); // 16 rows > 11 visible → the bar shows
s.fire_event("TRAINER_SHOW", vec![ScriptValue::Str("Sana".into())]);
let offset = |s: &mut UiScript| {
s.eval::<i64>("return BenillaTrainerListScrollFrame.offset or -1")
s.eval::<i64>("return ClassTrainerListScrollFrame.offset or -1")
.unwrap()
};
let click = |s: &mut UiScript, which: &str| {
s.run(&format!(
"BenillaTrainerListScrollFrameScrollBarScroll{which}Button:Click()"
"ClassTrainerListScrollFrameScrollBarScroll{which}Button:Click()"
))
.unwrap();
};
@@ -668,42 +726,70 @@ fn the_scrollbar_arrows_step_the_list_the_way_they_point() {
}
/// **The filter is remembered across a restart** (decision 1128) — the whole persistence path, in
/// one test: the shipped XML declares its three globals for saving at file scope, a dropdown toggle
/// writes the *global* (not only the engine mask), the serialized file carries it, and a fresh VM
/// that loads the same XML and then executes that file comes up with the toggled filter applied.
///
/// It also pins the two halves that are easy to "simplify" apart and silently break: writing only
/// the engine mask in the click handler would lose the choice at the next list packet (the reference
/// rebuilds that mask on every one), and applying the globals only in `<OnShow>` would leave the
/// first painted frame under the reset.
/// The reference's own restart, through its own mechanism (1957): the addon is a LoadOnDemand
/// registry row read off the chain; `LoadAddOn` runs its files (file-scope defaults), then its
/// per-addon saved file over them, then fires ADDON_LOADED — whose arm pushes the three filter
/// globals into the engine. A dropdown toggle writes the GLOBAL; the addon's `## SavedVariables`
/// carry it into the file; a fresh VM loading the addon comes up with the toggle applied.
#[test]
fn the_state_filter_survives_a_restart_through_the_saved_variables_file() {
let mut s = trainer_script();
s.set_money(50);
s.set_trainer(Some(menu()));
s.fire_event("TRAINER_SHOW", vec![ScriptValue::Str("Sana".into())]);
assert_eq!(s.eval::<i64>("return GetNumTrainerServices()").unwrap(), 6);
let _data = benilla_formats::wow_data_or_skip!();
let toc =
super::reference_ui::read("Interface/AddOns/Blizzard_TrainerUI/Blizzard_TrainerUI.toc")
.map(|b| benilla_ui::toc::Toc::parse(&benilla_ui::source::decode(&b)))
.expect("the addon's toc off the chain");
assert!(toc.load_on_demand(), "the reference ships it LoadOnDemand");
let info = || {
let mut i = super::addons::info_from_toc("Blizzard_TrainerUI", &toc);
i.chain = true;
i
};
let saved_dir =
std::env::temp_dir().join(format!("benilla-trainer-saved-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&saved_dir);
std::fs::create_dir_all(&saved_dir).unwrap();
let boot = |s: &mut UiScript| {
s.set_addon_chain_reader(Box::new(super::reference_ui::read));
s.register_addons(vec![info()], None, Some(saved_dir.clone()), None);
s.set_unit(
"npc",
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Sana Winterhoof".into()),
..Default::default()
}),
);
s.set_money(50);
s.set_trainer(Some(menu()));
// The reference's arm: TRAINER_SHOW → ClassTrainerFrame_LoadUI → UIParentLoadAddOn.
s.fire_event("TRAINER_SHOW", vec![ScriptValue::Str("Sana".into())]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
s.eval::<bool>("return IsAddOnLoaded('Blizzard_TrainerUI') == 1")
.unwrap(),
"loaded on demand, off the chain"
);
};
// This XML declared exactly the reference's three globals, in its order. Filtered to the
// trainer's own prefix because the registry belongs to the whole UI: every file the harness
// loads adds its own (GameTooltip.xml's SHOW_NEWBIE_TIPS since 1136), and what the OTHER files
// register is not this test's business.
let declared: Vec<String> = s
.saved_variable_names()
.into_iter()
.filter(|n| n.starts_with("TRAINER_"))
.collect();
let mut s = trainer_script_base();
boot(&mut s);
// The reference's own file-scope default hides known spells (TRAINER_FILTER_USED = 0), so
// the one used service is out on the very first paint: 2 headers + 3 of the 4 services.
assert_eq!(s.eval::<i64>("return GetNumTrainerServices()").unwrap(), 5);
assert_eq!(
declared,
info().saved_variables,
vec![
"TRAINER_FILTER_AVAILABLE",
"TRAINER_FILTER_UNAVAILABLE",
"TRAINER_FILTER_USED",
]
],
"the toc's own three, in its order"
);
// Toggle "Unavailable" off through the dropdown row's own handler (pre-click state on `this`).
s.run("this = { value = 'unavailable', checked = 1 }; BenillaTrainerFilterDropDown_OnClick()")
// Toggle "Unavailable" off through the dropdown row's own handler.
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap();
s.run("this = DropDownList1Button2; UIDropDownMenuButton_OnClick()")
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
@@ -711,19 +797,17 @@ fn the_state_filter_survives_a_restart_through_the_saved_variables_file() {
0,
"the click must write the SAVED global, not just the engine mask"
);
let text = s.saved_variables_text();
let text = s.saved_variables_text_for(&info().saved_variables);
assert!(
text.contains("TRAINER_FILTER_UNAVAILABLE = 0"),
"the file carries the toggle: {text}"
);
std::fs::write(saved_dir.join("Blizzard_TrainerUI.lua"), &text).unwrap();
// The restart: a fresh VM with the same XML (file-scope defaults stand), then the saved file
// executed over them, then a trainer opens.
let mut fresh = trainer_script();
fresh.run(&text).unwrap();
fresh.set_money(50);
fresh.set_trainer(Some(menu()));
fresh.fire_event("TRAINER_SHOW", vec![ScriptValue::Str("Sana".into())]);
// The restart: a fresh VM loads the addon on demand — files, then the saved file, then
// ADDON_LOADED — and the first paint is already filtered.
let mut fresh = trainer_script_base();
boot(&mut fresh);
assert!(
!fresh
.eval::<bool>("return GetTrainerServiceTypeFilter('unavailable') == 1")
@@ -732,14 +816,10 @@ fn the_state_filter_survives_a_restart_through_the_saved_variables_file() {
);
assert_eq!(
fresh.eval::<i64>("return GetNumTrainerServices()").unwrap(),
5,
4,
"Cleave (the unavailable service) is hidden on the very first paint after the restart"
);
assert!(
fresh.errors().is_empty(),
"script errors: {:?}",
fresh.errors()
);
let _ = std::fs::remove_dir_all(&saved_dir);
}
/// A **new list packet resets the engine's filter mask** to the builder's own default — mask 3 at a
@@ -750,7 +830,10 @@ fn the_state_filter_survives_a_restart_through_the_saved_variables_file() {
fn a_new_list_packet_resets_the_filter_mask_and_the_collapse_set() {
let mut s = trainer_script();
s.set_trainer(Some(menu()));
s.run("BenillaTrainerFrame_ApplyFilter()").unwrap();
s.fire_event(
"ADDON_LOADED",
vec![ScriptValue::Str("Blizzard_TrainerUI".into())],
);
// Collapse a group, and turn a state off directly (no global) — both are engine-side state.
s.run("CollapseTrainerSkillLine(1) SetTrainerServiceTypeFilter('used', 1)")
.unwrap();
@@ -880,7 +963,7 @@ fn a_long_row_name_stays_on_one_line_and_carries_its_rank_along() {
// Find each name's row by what it painted (the group comparator owns the order, not this test).
let row_of = |s: &mut UiScript, needle: &str| -> i64 {
s.eval::<i64>(&format!(
"for i = 1, 11 do local b = getglobal('BenillaTrainerService' .. i) \
"for i = 1, 11 do local b = getglobal('ClassTrainerSkill' .. i) \
local t = b:GetText() if t and strfind(t, '{needle}', 1, 1) then return i end \
end return 0"
))
@@ -892,9 +975,9 @@ fn a_long_row_name_stays_on_one_line_and_carries_its_rank_along() {
let geom = |s: &mut UiScript, row: i64| -> (f32, f32, f32, f32) {
s.eval::<(f32, f32, f32, f32)>(&format!(
"local b = getglobal('BenillaTrainerService{row}') \
"local b = getglobal('ClassTrainerSkill{row}') \
local n = b:GetFontString() \
return n:GetHeight(), n:GetRight(), b.subtext:GetLeft(), b:GetHeight()"
return n:GetHeight(), n:GetRight(), getglobal(b:GetName() .. 'SubText'):GetLeft(), b:GetHeight()"
))
.unwrap()
};
@@ -941,11 +1024,12 @@ fn learning_a_spell_keeps_the_filter_and_the_collapse_a_re_open_still_resets() {
let mut s = trainer_script();
// The reference's own file-scope defaults (1128), then the player's choice below.
s.run(
"TRAINER_FILTER_AVAILABLE = 1 TRAINER_FILTER_UNAVAILABLE = 1 TRAINER_FILTER_USED = 0 \
BenillaTrainerFrame_ApplyFilter()",
)
.unwrap();
s.run("TRAINER_FILTER_AVAILABLE = 1 TRAINER_FILTER_UNAVAILABLE = 1 TRAINER_FILTER_USED = 0")
.unwrap();
s.fire_event(
"ADDON_LOADED",
vec![ScriptValue::Str("Blizzard_TrainerUI".into())],
);
s.set_money(5000);
// The trainer feed (`ui_trainer::feed_trainer`), reduced to the part this bug lives in.
@@ -969,11 +1053,11 @@ fn learning_a_spell_keeps_the_filter_and_the_collapse_a_re_open_still_resets() {
let mut open = TrainerOpen::default();
open.open(DAZALAR, 0, vec![], "Hello, hunter!".into());
feed(&mut s, &mut open, menu(), "TRAINER_SHOW");
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap();
s.run("this = DropDownList1Button2; UIDropDownMenuButton_OnClick()")
.unwrap();
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap();
assert!(
!filter_on(&mut s, "unavailable"),
@@ -1006,37 +1090,39 @@ fn learning_a_spell_keeps_the_filter_and_the_collapse_a_re_open_still_resets() {
"and the list is still his — the folded Arms group is empty of learnables now, so it goes \
with its header, leaving Fury over Thunder Clap"
);
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap();
assert!(
!s.eval::<bool>("return DropDownList1Button2Check:IsVisible() and true or false")
.unwrap(),
"the dropdown agrees with the list, instead of claiming a filter the list ignores"
);
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
s.run("ToggleDropDownMenu(1, nil, ClassTrainerFrameFilterDropDown)")
.unwrap();
// He walks away and comes back: THIS packet opens a window, so the reference's reset lands —
// and the window's own OnShow/TRAINER_SHOW puts the saved globals straight back over it. The
// filter survives (it is saved), the collapse does not (it is engine-side, and the reset clears
// it) — the reference's own split.
// He walks away and comes back: THIS packet opens a window, so the reference's reset lands
// (`0x4d75d9` writes the mask to 3 on every list) — and nothing puts the saved globals back
// over it: the stock addon pushes them once, on ITS ADDON_LOADED, which a LoadOnDemand
// addon fires on the session's first trainer. The collapse is engine-side and clears too
// (1957; the transcription used to re-push on show, which the reference never does).
open.clear();
open.open(DAZALAR, 0, vec![], "Hello, hunter!".into());
let mut learned = menu();
learned.services[0].category = TrainerServiceCategory::Used;
feed(&mut s, &mut open, learned, "TRAINER_SHOW");
assert!(
!filter_on(&mut s, "unavailable"),
"the saved filter is pushed back over the packet reset"
filter_on(&mut s, "unavailable"),
"a fresh list is the reference's reset: unavailable shows again"
);
assert!(filter_on(&mut s, "available"));
assert!(
filter_on(&mut s, "available"),
"and the states he never turned off are still on"
!filter_on(&mut s, "used"),
"and the reset's mask is 3 — used stays hidden"
);
assert_eq!(
rows(&mut s),
2,
"nothing collapsed now — but still filtered"
s.eval::<i64>("return TRAINER_FILTER_UNAVAILABLE").unwrap(),
0,
"his saved choice is still the global's, for the next session's first trainer"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -28,7 +28,8 @@ fn load_unit_frames(s: &UiScript) {
load_xml(s, "MoneyFrame.xml");
load_xml(s, "GameTooltip.xml");
load_xml(s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(s, "UnitPopup.xml");
load_xml(s, "Interface\\FrameXML\\BasicControls.xml"); // `TEXT`, which UnitPopup.lua reads at file scope
load_xml(s, "Interface\\FrameXML\\UnitPopup.xml");
load_xml(s, "Interface\\FrameXML\\BuffFrame.xml");
load_xml(s, "Interface\\FrameXML\\UnitFrame.xml");
load_xml(s, "Interface\\FrameXML\\CombatFeedback.xml");
@@ -650,6 +651,9 @@ fn shipped_target_frame_runs_the_level_law() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -1212,12 +1216,16 @@ fn the_party_art_paints_over_the_bars() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE and its
// declarer is UIParent (ref UIParent.lua:65) since 1888.
load_xml(&s, "UIParent.xml");
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
// The reference's own kit, in the manifest's order. `UIParent.xml` is not decoration here:
// `RaiseFrameLevel`/`LowerFrameLevel` live in it (ref UIParent.lua l.1890-1896) and stock
// `TargetofTargetTextureFrame`'s OnLoad calls one of them.
@@ -22,9 +22,17 @@ fn load_popup_frames(s: &UiScript) {
// `StaticPopupDialogs` and the `PanelTemplates_*` family, both of which FriendsFrame.xml
// reaches at LOAD (its tab row and its confirm dialogs).
"UiPanels.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua", // `TEXT`, read at file scope below
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\ItemRef.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
@@ -36,9 +44,9 @@ fn load_popup_frames(s: &UiScript) {
"Interface\\FrameXML\\TargetFrame.xml",
"Interface\\FrameXML\\PetFrame.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"FriendsFrame.xml",
"Interface\\FrameXML\\FriendsFrame.xml",
// Declares ChatFrameEditBox, which the rename dialog's OnHide refocuses (1960).
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
load_xml(s, file);
}
@@ -108,11 +116,13 @@ fn chat_name_right_click_opens_the_invite_menu() {
s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
"the chat-name right-click opens the FRIEND dropdown"
);
// title (Bob) + Whisper + Invite + Cancel — the rest of FRIEND is DEFERRED/hidden.
// title (Bob) + Whisper + Invite + Target + Cancel — the reference's FRIEND menu with its two
// guild rows hidden by their own predicates (no guild, no guild frame). Our transcription
// had deferred Target; the stock row is `TargetByName`, which exists (1958).
assert_eq!(
s.eval::<i64>("return DropDownList1.numButtons").unwrap(),
4,
"title + Whisper + Invite + Cancel"
5,
"title + Whisper + Invite + Target + Cancel"
);
assert_eq!(
s.eval::<String>("return DropDownList1Button1:GetText()")
@@ -138,14 +148,22 @@ fn chat_name_right_click_opens_the_invite_menu() {
"Invite on a chat name queues an invite-by-name"
);
// A plain LEFT-click on the name whispers instead (ref's else branch).
// A plain LEFT-click on the name whispers instead (ref's else branch): the stock
// `ChatFrame_SendTell` puts the box into WHISPER mode at the name and opens it (1960).
s.run(r#"SetItemRef("player:Carol", "|Hplayer:Carol|h[Carol]|h", "LeftButton")"#)
.unwrap();
s.tick(0.05);
assert_eq!(
s.take_tell_requests(),
vec!["Carol".to_string()],
s.eval::<(String, String)>("return ChatFrameEditBox.chatType, ChatFrameEditBox.tellTarget")
.unwrap(),
("WHISPER".to_string(), "Carol".to_string()),
"left-click a chat name opens a whisper"
);
assert!(
s.eval::<bool>("return ChatFrameEditBox:IsVisible()")
.unwrap(),
"and the box is open"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -471,9 +489,14 @@ fn load_pet_menu_frames(s: &UiScript) {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua", // `TEXT`, read at file scope below
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UnitPopup.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\BuffFrame.xml",
@@ -488,6 +511,8 @@ fn load_pet_menu_frames(s: &UiScript) {
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// Declares ChatFrameEditBox, which the rename dialog's OnHide refocuses (1960).
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
load_xml(s, file);
}
@@ -795,150 +820,6 @@ fn the_pet_details_row_opens_the_pet_paper_doll() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
// ── The enable-pass driver (BenillaUnitPopupDriver) ─────────────────────────────────────────────
/// **The idle popup driver parks itself** (decision 1396's class, the audit's driver-hide item):
/// with no unit menu of ours open, `UnitPopup_OnUpdate`'s early-out hides the driver's own frame —
/// and the engine dispatches OnUpdate only to visible frames (`tick.rs`), so the per-frame scan
/// stops entirely. `UnitPopup_ShowMenu` — the one opener every unit menu goes through (grep:
/// UnitFrames/PartyFrame/FriendsFrame/ItemRef all call it) — wakes it again.
///
/// The probe wraps `UnitPopup_OnUpdate` in a counter: after the first tick parks the shipped-shown
/// driver, ten idle ticks never run the pass. The control: an open target menu still runs the
/// per-frame enable pass — a disconnected target's Whisper row greys, and greys back live when the
/// unit reconnects mid-open — and closing the menu re-parks the driver.
#[test]
fn an_idle_unit_popup_driver_parks_itself_off_the_tick() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_strings(&s);
load_popup_frames(&s);
s.set_unit(
"player",
Some(UnitState {
exists: true,
name: Some("Me".into()),
is_player: true,
player_controlled: true,
..UnitState::default()
}),
);
// A friendly player target, DISCONNECTED (is_connected defaults to false): the Whisper row's
// grey predicate is live from the first enable pass.
s.set_unit(
"target",
Some(UnitState {
exists: true,
name: Some("Ally".into()),
health: 40,
max_health: 40,
is_player: true,
player_controlled: true,
reaction: 5,
..UnitState::default()
}),
);
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
s.resolve();
assert!(s.errors().is_empty(), "load errors: {:?}", s.errors());
s.run(
"BENILLA_TEST_POPUP_TICKS = 0\n\
local real = UnitPopup_OnUpdate\n\
function UnitPopup_OnUpdate(elapsed)\n\
BENILLA_TEST_POPUP_TICKS = BENILLA_TEST_POPUP_TICKS + 1\n\
real(elapsed)\n\
end",
)
.unwrap();
// The driver ships shown; the first tick's early-out parks it, and then nothing runs.
s.tick(0.016);
s.resolve();
assert!(
!s.eval::<bool>("return BenillaUnitPopupDriver:IsShown()")
.unwrap(),
"no menu open: the first tick parks the driver"
);
s.run("BENILLA_TEST_POPUP_TICKS = 0").unwrap();
for _ in 0..10 {
s.tick(0.016);
s.resolve();
}
assert_eq!(
s.eval::<i64>("return BENILLA_TEST_POPUP_TICKS").unwrap(),
0,
"a parked driver never runs the enable pass"
);
// Open the PLAYER menu through the real hit path: the driver wakes...
let (cx, cy) = s
.eval::<(f64, f64)>("return TargetFrame:GetCenter()")
.unwrap();
s.mouse_button(cx as f32, cy as f32, "RightButton", true);
s.mouse_button(cx as f32, cy as f32, "RightButton", false);
s.resolve();
assert!(
s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
"the menu opened"
);
assert!(
s.eval::<bool>("return BenillaUnitPopupDriver:IsShown()")
.unwrap(),
"UnitPopup_ShowMenu wakes the driver"
);
// ...and the pass runs per frame: the disconnected target's Whisper row greys...
s.tick(0.016);
s.resolve();
assert!(
s.eval::<i64>("return BENILLA_TEST_POPUP_TICKS").unwrap() > 0,
"an open menu ticks the enable pass"
);
assert!(
!s.eval::<bool>("return DropDownList1Button2:IsEnabled() ~= 0")
.unwrap(),
"Whisper greys against a disconnected target"
);
// ...and un-greys LIVE when the unit reconnects while the menu is up — the whole reason the
// pass is per-frame rather than baked at open.
s.set_unit(
"target",
Some(UnitState {
exists: true,
name: Some("Ally".into()),
health: 40,
max_health: 40,
is_player: true,
player_controlled: true,
reaction: 5,
is_connected: true,
..UnitState::default()
}),
);
s.tick(0.016);
s.resolve();
assert!(
s.eval::<bool>("return DropDownList1Button2:IsEnabled() ~= 0")
.unwrap(),
"Whisper enables the frame the target reconnects"
);
// Closing the menu re-parks the driver on its next tick.
s.run("CloseDropDownMenus()").unwrap();
s.tick(0.016);
s.resolve();
assert!(
!s.eval::<bool>("return BenillaUnitPopupDriver:IsShown()")
.unwrap(),
"a closed menu parks the driver again"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Click an open dropdown row through the real hit path.
fn click_row(s: &mut UiScript, row: u32) {
click_frame(s, &format!("DropDownList1Button{row}"));
@@ -18,6 +18,10 @@ fn harness() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad
"ScrollTemplates.xml",
@@ -20,6 +20,10 @@ pub(crate) fn harness() -> UiScript {
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"UIParent.xml",
"GameTooltip.xml",
"WorldStateFrame.xml",
@@ -47,6 +47,8 @@ fn harness() -> UiScript {
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
// The fading kit, its own manifest entry since window 8 — as it is in the reference's TOC.
load_xml(&s, "Interface\\FrameXML\\FadingFrame.xml");
+7 -1
View File
@@ -82,7 +82,12 @@ pub(super) fn feed_social(
fed.seeded = true;
if social.friends_dirty || first {
social.friends_dirty = false;
script.fire_event("FRIENDLIST_UPDATE", Vec::new());
let event = if std::mem::take(&mut social.friends_show_pending) {
"FRIENDLIST_SHOW"
} else {
"FRIENDLIST_UPDATE"
};
script.fire_event(event, Vec::new());
}
if social.ignores_dirty || first {
social.ignores_dirty = false;
@@ -314,6 +319,7 @@ pub(super) fn drain_social(
for request in requests {
match request {
SocialRequest::RefreshFriends => {
social.friends_show_pending = true;
let _ = commands.0.send(ClientCommand::FriendListRequest);
}
SocialRequest::AddFriend(name) => {
+3
View File
@@ -111,6 +111,9 @@ pub(crate) struct SocialState {
pending_lines: Vec<FriendStatusUpdate>,
/// Set whenever a list changed, so the feed knows to fire the Era update event.
friends_dirty: bool,
/// An explicit `ShowFriends()` is out: the next list answers it with `FRIENDLIST_SHOW`
/// rather than `FRIENDLIST_UPDATE` (the reference's two arms, 1959).
pub(crate) friends_show_pending: bool,
ignores_dirty: bool,
who_dirty: bool,
}
+18 -9
View File
@@ -749,6 +749,8 @@ fn feed_player_control(
let lost = lost.get(&script);
if *lost != player.control_lost {
*lost = player.control_lost;
// `HasFullControl`'s flag rides the same edge (1958).
script.set_player_control(!player.control_lost);
let event = if player.control_lost {
"PLAYER_CONTROL_LOST"
} else {
@@ -951,6 +953,12 @@ pub(crate) fn snapshot(
// sets it without being a player.
player_controlled: store.0.unit_flags() & 0x8 != 0,
flags: store.0.unit_flags(),
owner: store
.0
.unit_summoned_by()
.or_else(|| store.0.unit_charmed_by())
.or_else(|| store.0.unit_created_by())
.unwrap_or(0),
// `UnitAffectingCombat 0x517e10` — the SAME `UNIT_FIELD_FLAGS` word, bit 19
// (`shr ecx,0x13; test cl,1`). One flag for every token: wow-re's whole-image census of
// that idiom found the local-player readers reading this identical bit, so there is no
@@ -1227,10 +1235,13 @@ pub(crate) fn fire_transitions(
if changed(|u| u64::from(u.power_type)) {
script.fire_event("UNIT_DISPLAYPOWER", vec![tok()]);
}
// `UNIT_FLAGS` — on a change of the unit's `UNIT_FIELD_FLAGS`, the field the reference's
// per-unit reflex is registered on by byte offset (wow-re `pet-action-bar-api.md` §9's
// `0x5ff580`). The stock pet bar filters it for `"pet"` and re-derives its usability (1953).
if changed(|u| u64::from(u.flags)) {
// `UNIT_FLAGS` (id 40) — the per-field watch bridge (wow-re `unit-field-event-bridge.md`,
// VERIFIED): `0x51bbb0` registers one watch per named unit field, the notifier `0x465570`
// fires `0x51bd50` → `0x515e50` on any change of the dword's bytes, once per token mapping to
// the unit, `arg1` the token. The create leg runs no notify pass, so a unit's FIRST snapshot
// is not a transition here — unlike the fields above, whose first-appearance fire is this
// feed's own posture (1953, corrected in 1957). The stock pet bar filters it for `"pet"`.
if prev.is_some() && changed(|u| u64::from(u.flags)) {
script.fire_event("UNIT_FLAGS", vec![tok()]);
}
// The POWER pair is named per resource in 1.12, not once with the token as arg2: the reference's
@@ -1987,7 +1998,8 @@ mod tests {
/// instructions and 12 branches of the watcher. The negative half is asserted too, because
/// "fire on both, it's cheaper" is the obvious wrong simplification.
/// `UNIT_FLAGS` rides the raw field: any change of `UNIT_FIELD_FLAGS` fires it with the
/// token, an unchanged field does not, and a unit's first snapshot is a transition (1953).
/// token, an unchanged field does not, and a unit's first snapshot is NOT a transition — the
/// reference's watch bridge has no watch to fire on the create leg (1953, corrected 1957).
#[test]
fn a_flags_change_fires_unit_flags_with_the_token() {
let fired = |prev: Option<UnitState>, cur: UnitState| -> Vec<String> {
@@ -2024,10 +2036,7 @@ mod tests {
fired(Some(base.clone()), base.clone()),
Vec::<String>::new()
);
assert_eq!(
fired(None, base.clone()),
vec!["UNIT_FLAGS:pet".to_string()]
);
assert_eq!(fired(None, base.clone()), Vec::<String>::new());
}
/// The two edges on the player's own state (1953): the control flag's, which fires LOST on
+5 -1
View File
@@ -20,7 +20,7 @@ mod common;
/// the `MoneyTypeInfo` table this window registers `AUCTION_DEPOSIT` into, The `MoneyInputFrame_*` 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; 12] = [
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
// The four money-entry frames come off the chain since 1882 — `MoneyInputFrameTemplate` and
@@ -36,6 +36,10 @@ const FILES: [&str; 12] = [
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"AuctionFrame.xml",
];
+3 -1
View File
@@ -33,7 +33,7 @@ use benilla_ui::script::{UiScript, UnitCombatStats, UnitState};
/// `X = X or "…"` fallbacks, and every row here formats a real string — `SPELL_STAT0_NAME`..`4`
/// through `TEXT()` (`PaperDollFrame.lua:143`), `RESISTANCE<n>_NAME` and
/// `RESISTANCE_TOOLTIP_SUBTEXT` (`:184`/`:224`), `ARMOR` and `ARMOR_TOOLTIP` (`:236`/`:249`).
const FILES: [&str; 12] = [
const FILES: &[&str] = &[
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\BasicControls.xml",
@@ -47,6 +47,8 @@ const FILES: [&str; 12] = [
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
// `CooldownFrameTemplate` and `CooldownFrame_SetTimer`. Not scenery: every one of the 20 slot
// buttons runs `PaperDollItemSlotButton_Update` from its own OnLoad, which calls
@@ -26,7 +26,7 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// chain from `Interface\FrameXML\UIPanelTemplates.xml`, seated below `UiPanels.xml` exactly as
/// the manifest seats it. So this list carries the chain pair and the loader below has to be able
/// to READ a chain entry, which a disk-only provider under `assets/ui` cannot.
const FILES: [&str; 8] = [
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
// A REGRESSION GUARD, not a dependency (1923). FadingFrame.xml's entire body is a single
// `<Script file="FadingFrame.lua"/>`, so it loads correctly ONLY if that relative reference
@@ -45,6 +45,10 @@ const FILES: [&str; 8] = [
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which StaticPopup.lua reads at file scope
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"UIParent.xml",
"ScrollTemplates.xml",
];
+4 -1
View File
@@ -18,7 +18,7 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// The reader's dependency prefix, in the manifest's own order. `ScrollTemplates.xml` and
/// `UIPanelTemplates.xml` joined it with decisions 1337/1338: the page sits in a real ScrollFrame
/// now, whose template is in the second and whose `ScrollFrame_OnLoad` is in the first.
const FILES: [&str; 9] = [
const FILES: &[&str] = &[
// `ITEM_TEXT_FROM`, which the reference's READY arm concatenates into the creator tail — and
// `attempt to concatenate a nil value` kills the handler before it reaches its `ShowUIPanel`,
// so the window simply never opens. Our deleted copy carried the string as a local fallback.
@@ -32,6 +32,9 @@ const FILES: [&str; 9] = [
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"Interface\\FrameXML\\ItemTextFrame.xml",
];
+4 -1
View File
@@ -12,7 +12,7 @@ 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; 10] = [
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
// The send tab's money entry comes off the chain since 1882 — `MoneyInputFrameTemplate` and
@@ -23,6 +23,9 @@ const FILES: [&str; 10] = [
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"MailFrame.xml",
];
+3 -1
View File
@@ -20,7 +20,7 @@ use benilla_ui::script::{
/// `ItemButtonTemplate.xml` is the `SetItemButton*` family the talent buttons grey through (the
/// reference's own verb; see TalentFrame.xml's header) — it sits at .toc line 32, above every
/// other entry here bar `Fonts.xml`.
const FILES: [&str; 13] = [
const FILES: &[&str] = &[
// `PLAYER_LEVEL` and the rest of the strings the stock file formats through.
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
@@ -38,6 +38,8 @@ const FILES: [&str; 13] = [
// scrollbar and no wheel (decision 1833). `load_ui_strict` below is what makes that loud.
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"ScrollTemplates.xml",
// Stock `TalentFrame_OnShow` opens with `SetButtonPulse(TalentMicroButton, 0, 1)` and then
// `UpdateMicroButtons()` — both live here, and a nil `TalentMicroButton` throws out of OnShow
+5 -1
View File
@@ -13,7 +13,7 @@ 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; 9] = [
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
// The money entry comes off the chain since 1882 — `MoneyInputFrameTemplate` and the
@@ -24,6 +24,10 @@ const FILES: [&str; 9] = [
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"TradeFrame.xml",
];
+5 -1
View File
@@ -20,12 +20,16 @@ mod common;
/// The tradeskill window's load prefix — the app's own order (`ui_script/mod.rs`), members only.
/// CraftFrame.xml rides along (it loads right after TradeSkillFrame.xml in the app and shares
/// its guarded-global utilities) so a load error in EITHER window fails here.
const FILES: [&str; 10] = [
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
+6 -1
View File
@@ -356,7 +356,12 @@ fn is_instrument_consumer(rel: &str) -> bool {
/// is the whole of the crossing: one function, "make this handle's asset exist now", the
/// doorway's own rule about when a material is real. The alternative — eager materials for
/// the two lanes that read early — would put the store's law in the caller's hands.
const CEILING: usize = 173;
/// And 173 → 174: `particles::render::EFFECT_DRAW_STATS`, an instrument PUBLISH — the effect
/// lane's own per-frame draw census (items in the transparent phase, draws after the merge),
/// the number the `FPS_PROBE` line prints as `fx=`. It is the reading that refuted the
/// additive-window regroup on the crowd rig (decision 1955): a lane whose merge walk is the
/// only place the count exists has to publish it or stay unmeasurable.
const CEILING: usize = 174;
/// How far under [`CEILING`] the real count may sit before this test asks for the ceiling to be
/// lowered. Slack, not tolerance: it keeps a single closure from failing the gate, while making it

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