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