diff --git a/crates/benilla-app/assets/ui/BinderConfirm.xml b/crates/benilla-app/assets/ui/BinderConfirm.xml
deleted file mode 100644
index f9f8dc9f..00000000
--- a/crates/benilla-app/assets/ui/BinderConfirm.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
- self:RegisterEvent("CONFIRM_BINDER")
-
- BenillaBinderConfirmDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/ConfirmSummon.xml b/crates/benilla-app/assets/ui/ConfirmSummon.xml
deleted file mode 100644
index 0232d3c2..00000000
--- a/crates/benilla-app/assets/ui/ConfirmSummon.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
-
- self:RegisterEvent("CONFIRM_SUMMON")
-
- BenillaConfirmSummonDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/DeathFrame.xml b/crates/benilla-app/assets/ui/DeathFrame.xml
deleted file mode 100644
index a00d638d..00000000
--- a/crates/benilla-app/assets/ui/DeathFrame.xml
+++ /dev/null
@@ -1,310 +0,0 @@
-
-
-
-
-
-
-
-
-
- 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")
-
- BenillaDeathDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/DuelFrame.xml b/crates/benilla-app/assets/ui/DuelFrame.xml
deleted file mode 100644
index 59177c3c..00000000
--- a/crates/benilla-app/assets/ui/DuelFrame.xml
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
-
-
-
-
- self:RegisterEvent("DUEL_REQUESTED")
- self:RegisterEvent("DUEL_OUTOFBOUNDS")
- self:RegisterEvent("DUEL_INBOUNDS")
- self:RegisterEvent("DUEL_FINISHED")
-
- BenillaDuelDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/EnchantConfirm.xml b/crates/benilla-app/assets/ui/EnchantConfirm.xml
deleted file mode 100644
index 0cdcfaa1..00000000
--- a/crates/benilla-app/assets/ui/EnchantConfirm.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
-
- self:RegisterEvent("BIND_ENCHANT")
- self:RegisterEvent("REPLACE_ENCHANT")
- self:RegisterEvent("CURRENT_SPELL_CAST_CHANGED")
-
- BenillaEnchantConfirmDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/FriendsFrame.xml b/crates/benilla-app/assets/ui/FriendsFrame.xml
deleted file mode 100644
index 92b2bb15..00000000
--- a/crates/benilla-app/assets/ui/FriendsFrame.xml
+++ /dev/null
@@ -1,3405 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaGuildRankCheckbox_OnClick(self)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ScrollFrame_OnLoad()
-
-
-
- FauxScrollFrame_OnVerticalScroll(FRIENDS_FRAME_FRIEND_HEIGHT, FriendsList_Update)
-
-
-
-
- BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameFriendsScrollFrame, arg1)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ScrollFrame_OnLoad()
-
-
-
- FauxScrollFrame_OnVerticalScroll(FRIENDS_FRAME_IGNORE_HEIGHT, IgnoreList_Update)
-
-
-
-
- BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameIgnoreScrollFrame, arg1)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- this:ClearFocus()
- WhoFrameEditBox_OnEnterPressed()
- this:ClearFocus()
-
-
-
-
-
-
-
-
-
- ScrollFrame_OnLoad()
-
-
-
- FauxScrollFrame_OnVerticalScroll(FRIENDS_FRAME_WHO_HEIGHT, WhoList_Update)
-
-
-
-
-
- SetWhoToUI(0)
- SetWhoToUI(1)
- SetWhoToUI(0)
- BenillaFauxScrollFrame_OnMouseWheel(WhoListScrollFrame, arg1)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- RegisterForSave("SHOW_OFFLINE_GUILD_MEMBERS")
- this:RegisterEvent("VARIABLES_LOADED")
-
-
- if event == "VARIABLES_LOADED" then
- SetGuildRosterShowOffline(SHOW_OFFLINE_GUILD_MEMBERS)
- this:SetChecked(GetGuildRosterShowOffline())
- end
-
-
- BenillaGuildOfflineToggle_OnClick(self:GetChecked())
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ScrollFrame_OnLoad()
-
-
-
- FauxScrollFrame_OnVerticalScroll(FRIENDS_FRAME_GUILD_HEIGHT, GuildStatus_Update)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- GuildRoster()
- BenillaGuild_HideSatellitesExcept("GuildInfoFrame")
- BenillaFauxScrollFrame_OnMouseWheel(GuildListScrollFrame, arg1)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- GuildControlPopupFrameDropDown_OnLoad()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- this:SetText(GuildControlGetRankName(1))
- this:ClearFocus()
-
- GuildControlPopup_MarkDirty()
-
- self:ClearFocus()
- GuildControlPopup_MarkDirty()
-
- this:ClearFocus()
-
-
-
-
-
- GuildControlPopupFrame_OnLoad()
- GuildControlPopupFrame_OnShow()
- GuildControlPopupFrame_OnHide()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- GuildInfoFrame_ClaimFocus()
- GuildInfoFrame_ClaimFocus()
-
-
-
-
-
- this:ClearFocus()
-
- GuildInfoEditBox_FollowCaret(self)
-
-
-
-
-
-
-
- BenillaGuildPane_TooltipBackdrop(self)
- GuildInfoFrame_Update()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaGuildPane_TooltipBackdrop(self, 0.5, 0.25)
- StaticPopup_Show("SET_GUILDPLAYERNOTE")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaGuildPane_TooltipBackdrop(self, 0.5, 0.25)
- StaticPopup_Show("SET_GUILDOFFICERNOTE")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- FriendsFrame_OnLoad()
- FriendsFrame_OnShow()
- FriendsFrame_OnHide()
- FriendsFrame_OnEvent()
-
-
-
-
-
-
-
-
-
- this:RegisterEvent("GUILD_INVITE_REQUEST")
- this:RegisterEvent("GUILD_INVITE_CANCEL")
-
- BenillaGuildInviteDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/QuestShareFrame.xml b/crates/benilla-app/assets/ui/QuestShareFrame.xml
deleted file mode 100644
index cc510130..00000000
--- a/crates/benilla-app/assets/ui/QuestShareFrame.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
- self:RegisterEvent("QUEST_ACCEPT_CONFIRM")
- BenillaQuestShareDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/TalentWipeConfirm.xml b/crates/benilla-app/assets/ui/TalentWipeConfirm.xml
deleted file mode 100644
index c906f822..00000000
--- a/crates/benilla-app/assets/ui/TalentWipeConfirm.xml
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
-
-
-
- self:RegisterEvent("CONFIRM_TALENT_WIPE")
-
- BenillaTalentWipeConfirmDriver_OnEvent()
-
-
-
diff --git a/crates/benilla-app/assets/ui/TrainerFrame.xml b/crates/benilla-app/assets/ui/TrainerFrame.xml
deleted file mode 100644
index dc414b2d..00000000
--- a/crates/benilla-app/assets/ui/TrainerFrame.xml
+++ /dev/null
@@ -1,892 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaTrainerFilterDropDown_OnLoad()
- BenillaTrainerFilterDropDown_OnLoad()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaTrainerList_OnVerticalScroll()
-
-
-
-
-
-
-
-
-
- SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("STATIC");
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaTrainerFrame_OnLoad(self)
- BenillaTrainerFrame_OnEvent(self)
- BenillaTrainerFrame_OnMouseWheel(arg1)
- BenillaTrainerFrame_ApplyFilter(); PlaySound("igCharacterInfoOpen")
- CloseTrainer(); PlaySound("igCharacterInfoClose")
-
-
-
diff --git a/crates/benilla-app/assets/ui/UIParent.xml b/crates/benilla-app/assets/ui/UIParent.xml
index bcf1bc76..124f45a5 100644
--- a/crates/benilla-app/assets/ui/UIParent.xml
+++ b/crates/benilla-app/assets/ui/UIParent.xml
@@ -33,11 +33,48 @@
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")
UIParent_OnEvent(event)
- if FCF_OnUpdate then FCF_OnUpdate(arg1) end
+ if FCF_OnUpdate then FCF_OnUpdate(arg1) end if ButtonPulse_OnUpdate then ButtonPulse_OnUpdate(arg1) end if UnitPopup_OnUpdate then UnitPopup_OnUpdate(arg1) end
@@ -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
diff --git a/crates/benilla-app/assets/ui/UiPanels.xml b/crates/benilla-app/assets/ui/UiPanels.xml
index af7b932b..f997b870 100644
--- a/crates/benilla-app/assets/ui/UiPanels.xml
+++ b/crates/benilla-app/assets/ui/UiPanels.xml
@@ -1001,1196 +1001,6 @@
end
end
- -- ── The shared dialog IS the ref's StaticPopup engine (decision 0308 §3) ─────────────────
- -- Re-expressed from the patch-chain StaticPopup.lua (mpqx extraction, decision 0246):
- -- the StaticPopupDialogs registry + StaticPopup_Show(which, arg1, arg2) + the OnUpdate
- -- countdown/StartDelay machinery — the death arc's dialogs exercise nearly all of it, and
- -- StaticPopup_Show/StaticPopupDialogs are 1.14 addon-API surface (0068). v1 scope:
- -- · FOUR dialog instances, the reference's own count. It was 2 for a long time, on the
- -- reasoning that the deepest nesting exercised was decline-res re-showing DEATH (Show
- -- precedes the click's hide, so a free second instance is required) and that it should
- -- "grow toward 4 when a third coexists". 1751 window 7 supplied that third caller from
- -- an unexpected direction: the reference's own `MirrorTimer_Show` bounds its free-bar
- -- search by this constant rather than by `MIRRORTIMER_NUMTIMERS` (MirrorTimer.lua
- -- l.32, a copy-paste from StaticPopup.lua), so at 2 the third mirror bar was
- -- unreachable for a new timer. The coupling is the reference's bug and this is not the
- -- file to fix it in; matching its count is.
- -- · info fields honored: text, button1, button2, DisplayButton2, OnShow(data),
- -- OnHide(data), OnAccept(data, data2) (truthy return = keep open — RECOVER_CORPSE
- -- waits for the server, XP_LOSS's first Accept re-asks), OnCancel(data, reason)
- -- (reason ∈ "clicked"|"timeout"|"override"), OnUpdate(elapsed, dialog), timeout,
- -- StartDelay, delayText, sound, whileDead, hideOnEscape, cancels, showAlert (the
- -- 420 widen + DialogAlertIcon, decision 1068).
- -- · hasEditBox + maxLetters + all three box hooks — EditBoxOnEnterPressed,
- -- EditBoxOnTextChanged, EditBoxOnEscapePressed — are live (decision 0668's
- -- ADD_FRIEND/ADD_IGNORE are the first customers; 1743's DELETE_GOOD_ITEM is the
- -- first to gate button1 on what is typed): the box shows, caps its length,
- -- opens empty, and re-anchors the buttons under itself.
- -- · hasWideEditBox is live too (the guild arc's SET_GUILDMOTD / SET_GUILDPLAYERNOTE /
- -- SET_GUILDOFFICERNOTE, registered in FriendsFrame.xml): the 350x64 centred box
- -- replaces the narrow one, and the dialog widens to 420. See the widen arm below for
- -- the reference's own oddity — the widen is keyed on `which`, not on hasWideEditBox.
- -- · hasMoneyFrame is live (decision 1580's CONFIRM_TALENT_WIPE is the first customer,
- -- and the reference's other five are all price confirms): a SmallMoneyFrameTemplate
- -- child under the text, shown per entry, with the buttons dropping to -24 to clear it
- -- and the frame growing 16px. The AMOUNT is not the engine's — the entry's own
- -- OnShow/the event arm calls MoneyFrame_Update, exactly as the reference does.
- -- · interruptCinematic is LIVE as of the cinematic arc: a dialog without it is
- -- refused while one plays, the ref's own l.1454 gate (it was inert only while
- -- 0196 acked every cinematic instantly, so nothing could be playing).
- -- · Accepted but v1-inert: exclusive, notClosableByLogout, preferredIndex.
- -- · text_arg1/2 stash on the DIALOG frame (the ref stashes them on the Text region;
- -- our regions don't carry Lua fields).
- -- · GetText("SECONDS"/"MINUTES") localization collapses to English singular/plural.
- -- · Button widths stay the template's fixed 120 (the ref widens past 120/110 via
- -- GetTextWidth; the widen law joins when a longer label first needs it).
- -- · Resize runs per tick while shown, not only on text swaps: GetHeight's measure has a
- -- frame of latency (the measured-width convention below), so the height self-corrects
- -- the frame after any text change.
- STATICPOPUP_NUMDIALOGS = 4
- StaticPopupDialogs = {}
-
- local function StaticPopupTimeUnit(n, minutes)
- if minutes then
- if n == 1 then return "minute" end
- return "minutes"
- end
- if n == 1 then return "second" end
- return "seconds"
- end
-
- -- ref StaticPopup.lua l.1414-1426, `data` included. Ours took `which` alone until 1793
- -- found the arity drift; the reference's second argument is what makes a `multiple` dialog
- -- addressable, and dropping it is silent in both directions (Lua discards a surplus
- -- argument without complaint).
- --
- -- The `info.multiple` gate is the reference's own and is why `data` is not simply an
- -- equality: a dialog kind declared `multiple` can have several instances up at once, so it
- -- is matched by data; every other kind has at most one, so its data is not consulted at
- -- all. An unknown `which` answers nil rather than scanning — the reference's own early out.
- function StaticPopup_FindVisible(which, data)
- local info = StaticPopupDialogs[which]
- if not info then
- return nil
- end
- for index = 1, STATICPOPUP_NUMDIALOGS do
- local frame = getglobal("StaticPopup" .. index)
- if frame:IsShown() and frame.which == which
- and (not info.multiple or frame.data == data) then
- return frame
- end
- end
- return nil
- end
-
- -- Ref l.1870-1877 (returns the frame NAME — the ref's contract).
- function StaticPopup_Visible(which)
- local frame = StaticPopup_FindVisible(which)
- if frame then
- return frame:GetName()
- end
- return nil
- end
-
- -- ref StaticPopup.lua l.1703-1710, `data` included (1793). With a `data` the reference
- -- hides only the instance carrying it; without one it hides every instance of the kind,
- -- which is what every one-argument caller means and gets.
- --
- -- Note the reference does NOT test `IsShown()` here, where `FindVisible` does — hiding an
- -- already-hidden dialog is a no-op, and the missing test is not an oversight to correct:
- -- `Hide()` on a hidden frame still runs nothing, and adding the guard would change which
- -- frames the loop touches for no visible difference. Transcribed as it is.
- function StaticPopup_Hide(which, data)
- for index = 1, STATICPOPUP_NUMDIALOGS do
- local dialog = getglobal("StaticPopup" .. index)
- if dialog.which == which and (not data or data == dialog.data) then
- dialog:Hide()
- end
- end
- end
-
- -- Ref l.1428-1438, all three legs.
- --
- -- Note the ref measures the NARROW `EditBox` for the height even when the shown box is the
- -- wide one — the wide box is 64 tall, anchored CENTER, and floats over a frame sized for a
- -- 32-tall box. Transcribed as written: the guild MOTD dialog's look is that arithmetic.
- --
- -- The money leg does NOT measure the coin row either: it is the plain height plus a flat
- -- 16, which is the room the -24 button drop below leaves for it. Same arithmetic, same
- -- reason — the ref sizes these from constants, not from what is in the frame.
- function StaticPopup_Resize(dialog, which)
- local text = getglobal(dialog:GetName() .. "Text")
- local button1 = getglobal(dialog:GetName() .. "Button1")
- local editBox = getglobal(dialog:GetName() .. "EditBox")
- local info = StaticPopupDialogs[which]
- if info and info.hasEditBox then
- dialog:SetHeight(16 + text:GetHeight() + 8 + editBox:GetHeight() + 8
- + button1:GetHeight() + 16)
- elseif info and info.hasMoneyFrame then
- dialog:SetHeight(16 + text:GetHeight() + 8 + button1:GetHeight() + 32)
- else
- dialog:SetHeight(16 + text:GetHeight() + 8 + button1:GetHeight() + 16)
- end
- end
-
- -- Ref l.1441-1700. Returns the dialog, or nil when refused (whileDead gate / no free
- -- instance) — calling info.OnCancel() on refusal exactly as the ref does.
- function StaticPopup_Show(which, text_arg1, text_arg2, data)
- local info = StaticPopupDialogs[which]
- if not info then
- return nil
- end
-
- -- The whileDead gate (ref l.1447-1452). UnitIsDeadOrGhost lands with the death arc's
- -- ghost predicate — guarded like CursorHasItem in ToggleGameMenu until then.
- if UnitIsDeadOrGhost and UnitIsDeadOrGhost("player") and not info.whileDead then
- if info.OnCancel then
- info.OnCancel()
- end
- return nil
- end
-
- -- The cinematic gate (ref l.1454-1459), the whileDead gate's twin: a fly-by owns the
- -- screen, so a dialog that has not declared itself worth interrupting one is cancelled
- -- rather than raised over it. `InCinematic` is guarded like the predicate above so a
- -- VM without the binding behaves as it did before.
- if InCinematic and InCinematic() and not info.interruptCinematic then
- if info.OnCancel then
- info.OnCancel()
- end
- return nil
- end
-
- -- Showing X hides a visible X.cancels with an "override" cancel (ref l.1470-1485).
- if info.cancels then
- local frame = StaticPopup_FindVisible(info.cancels)
- if frame then
- frame:Hide()
- local OnCancel = StaticPopupDialogs[frame.which].OnCancel
- if OnCancel then
- OnCancel(frame.data, "override")
- end
- end
- end
-
- -- Showing DEATH sweeps every visible non-whileDead dialog (ref l.1501-1512).
- if which == "DEATH" then
- for index = 1, STATICPOPUP_NUMDIALOGS do
- local frame = getglobal("StaticPopup" .. index)
- if frame:IsShown() and not StaticPopupDialogs[frame.which].whileDead then
- frame:Hide()
- local OnCancel = StaticPopupDialogs[frame.which].OnCancel
- if OnCancel then
- OnCancel(frame.data, "override")
- end
- end
- end
- end
-
- -- A visible same-type dialog is overridden in place; else take a free instance
- -- (ref l.1515-1553).
- local dialog = StaticPopup_FindVisible(which)
- if dialog then
- if info.OnCancel then
- info.OnCancel(dialog.data, "override")
- end
- dialog:Hide()
- end
- if not dialog then
- for i = 1, STATICPOPUP_NUMDIALOGS do
- local frame = getglobal("StaticPopup" .. i)
- if not frame:IsShown() then
- dialog = frame
- break
- end
- end
- end
- if not dialog then
- if info.OnCancel then
- info.OnCancel()
- end
- return nil
- end
-
- -- The text. Countdown/delay dialogs start blank — their OnUpdate writes it
- -- (ref l.1556-1574's which-list, verbatim).
- local text = getglobal(dialog:GetName() .. "Text")
- if (which == "DEATH") or (which == "CAMP") or (which == "QUIT")
- or (which == "DUEL_OUTOFBOUNDS") or (which == "RECOVER_CORPSE")
- or (which == "RESURRECT") or (which == "RESURRECT_NO_SICKNESS")
- or (which == "INSTANCE_BOOT") or (which == "CONFIRM_SUMMON")
- or (which == "AREA_SPIRIT_HEAL") then
- text:SetText(" ")
- dialog.text_arg1 = text_arg1
- dialog.text_arg2 = text_arg2
- else
- text:SetText(format(info.text, text_arg1, text_arg2))
- end
-
- -- The width/alert dress (ref l.1576-1589), an if/elseif in the ref's own order: every
- -- Show resets width 320 / icon hidden; a GUILD MESSAGE dialog widens to 420 with NO
- -- icon; otherwise a showAlert entry widens to 420 and shows the icon. HELP_TICKET's
- -- 350 arm waits for that dialog.
- --
- -- **The widen is keyed on `which`, not on `hasWideEditBox`** — that is the reference's
- -- own shape, transcribed rather than improved: a fourth wide-box dialog would get the
- -- 350x64 box inside an un-widened 320 frame until its name joins this list. (The ref's
- -- list also names SET_GUILDPLAYERNOTE twice, l.1582; the duplicate is inert.)
- local alertIcon = getglobal(dialog:GetName() .. "AlertIcon")
- alertIcon:Hide()
- dialog:SetWidth(320)
- if (which == "SET_GUILDMOTD") or (which == "SET_GUILDPLAYERNOTE")
- or (which == "SET_GUILDOFFICERNOTE") then
- dialog:SetWidth(420)
- elseif info.showAlert then
- dialog:SetWidth(420)
- alertIcon:Show()
- end
-
- -- The edit box (ref l.1599-1624): shown only for a hasEditBox entry, capped at the
- -- entry's maxLetters, and always empty on open — a name left over from the last time
- -- the dialog ran would be sent by a reflexive Enter. `hasWideEditBox` picks the 350x64
- -- centred box INSTEAD of the narrow one; exactly one of the two is ever shown, and
- -- neither is when the entry has no box at all (the ref's three-way, verbatim).
- local editBox = getglobal(dialog:GetName() .. "EditBox")
- local wideEditBox = getglobal(dialog:GetName() .. "WideEditBox")
- if info.hasEditBox then
- if info.hasWideEditBox then
- wideEditBox:SetMaxLetters(info.maxLetters or 0)
- wideEditBox:SetText("")
- wideEditBox:Show()
- editBox:Hide()
- else
- editBox:SetMaxLetters(info.maxLetters or 0)
- editBox:SetText("")
- editBox:Show()
- wideEditBox:Hide()
- end
- else
- editBox:Hide()
- wideEditBox:Hide()
- end
-
- -- The money frame (ref l.1627-1632): shown for a hasMoneyFrame entry, hidden
- -- otherwise. The engine never fills it — the AMOUNT belongs to whoever raised the
- -- dialog, who calls MoneyFrame_Update on it (the ref's own division: its
- -- CONFIRM_TALENT_WIPE arm in UIParent.lua does exactly that with the event's arg1).
- getglobal(dialog:GetName() .. "MoneyFrame"):Hide()
- if info.hasMoneyFrame then
- getglobal(dialog:GetName() .. "MoneyFrame"):Show()
- end
-
- -- The buttons (ref l.1634-1676; fixed 120 widths — header note). Button2 shows only
- -- when the entry names it AND DisplayButton2 (when present) allows it; a solo button1
- -- re-seats centered under the text. With an edit box the pair hangs off the BOX's
- -- bottom instead of the text's (ref l.1641-1643), which is what leaves room for it;
- -- with a money frame the pair stays on the TEXT and drops 24 instead of 8, which is
- -- what leaves room for the coins (ref l.1644-1646).
- local buttonAnchor = info.hasEditBox and (dialog:GetName() .. "EditBox")
- or (dialog:GetName() .. "Text")
- local buttonDrop = info.hasMoneyFrame and -24 or -8
- local button1 = getglobal(dialog:GetName() .. "Button1")
- local button2 = getglobal(dialog:GetName() .. "Button2")
- if info.button2 and (not info.DisplayButton2 or info.DisplayButton2()) then
- button1:ClearAllPoints()
- button2:ClearAllPoints()
- button1:SetPoint("TOPRIGHT", buttonAnchor, "BOTTOM", -6, buttonDrop)
- button2:SetPoint("LEFT", dialog:GetName() .. "Button1", "RIGHT", 13, 0)
- button2:SetText(info.button2)
- button2:Show()
- else
- button1:ClearAllPoints()
- button1:SetPoint("TOP", buttonAnchor, "BOTTOM", 0, -8)
- button2:Hide()
- end
- if info.button1 then
- button1:SetText(info.button1)
- button1:Show()
- else
- button1:Hide()
- end
-
- -- Misc vars (ref l.1677-1690). `which` is set before Show so OnShow reads it.
- dialog.which = which
- dialog.timeleft = info.timeout or 0
- dialog.hideOnEscape = info.hideOnEscape
- dialog.data = data
-
- if info.StartDelay then
- dialog.startDelay = info.StartDelay()
- button1:Disable()
- else
- dialog.startDelay = nil
- button1:Enable()
- end
-
- dialog:Show()
- StaticPopup_Resize(dialog, which)
-
- if info.sound then
- PlaySound(info.sound)
- end
- return dialog
- end
-
- -- Ref l.1850-1868: a truthy OnAccept return keeps the dialog open.
- function StaticPopup_OnClick(dialog, index)
- local which = dialog.which
- local info = StaticPopupDialogs[which]
- if not info then
- return
- end
- local dontHide = nil
- if index == 1 then
- if info.OnAccept then
- dontHide = info.OnAccept(dialog.data, dialog.data2)
- end
- else
- if info.OnCancel then
- info.OnCancel(dialog.data, "clicked")
- end
- end
- if not dontHide and (which == dialog.which) then
- dialog:Hide()
- end
- end
-
- -- The edit box's three script hooks (ref StaticPopup.xml's template scripts, ref
- -- l.1810-1829). All three are pure delegation to the entry's own hook of the same name;
- -- `this` is the EDIT BOX while they run, which is what lets an entry body say
- -- `this:GetParent()`. Enter runs the entry's EditBoxOnEnterPressed, which by convention
- -- does the accept work and hides the dialog itself; text-changed drives a live gate on
- -- button1 (DELETE_GOOD_ITEM's typed "DELETE" is the only 1.12 customer).
- function StaticPopup_EditBoxOnEnterPressed(editBox)
- local dialog = editBox:GetParent()
- local info = StaticPopupDialogs[dialog.which]
- if info and info.EditBoxOnEnterPressed then
- info.EditBoxOnEnterPressed(dialog.data)
- end
- end
-
- function StaticPopup_EditBoxOnTextChanged(editBox)
- local dialog = editBox:GetParent()
- local info = StaticPopupDialogs[dialog.which]
- if info and info.EditBoxOnTextChanged then
- info.EditBoxOnTextChanged(dialog.data)
- end
- end
-
- -- Escape delegates like its two siblings — and where the entry names no hook, falls back
- -- to what ESC would have done had the focused box not swallowed the key, i.e. the ordinary
- -- StaticPopup_EscapePressed leg: the entry's OnCancel, then hide, and only for a
- -- hideOnEscape entry. NAMED DIVERGENCE (1743): the reference has no fallback at all, so a
- -- 1.12 dialog whose entry omits the hook simply eats the key — which is what its own
- -- DELETE_GOOD_ITEM does, leaving a destroy confirm that ESC cannot dismiss. Every other
- -- benilla popup closes on ESC and this one now does too; without the OnCancel leg it would
- -- also have hidden while still holding the item, which is the half that made the old
- -- unconditional `dialog:Hide()` wrong rather than merely lenient.
- function StaticPopup_EditBoxOnEscapePressed(editBox)
- local dialog = editBox:GetParent()
- local info = StaticPopupDialogs[dialog.which]
- editBox:ClearFocus()
- if info and info.EditBoxOnEscapePressed then
- info.EditBoxOnEscapePressed(dialog.data)
- elseif dialog.hideOnEscape then
- if info and info.OnCancel then
- info.OnCancel(dialog.data, "clicked")
- end
- dialog:Hide()
- end
- end
-
- -- Ref l.1832-1848: the open/close pair sounds on EVERY path, then the entry's own hook.
- function StaticPopup_OnShow()
- PlaySound("igMainMenuOpen")
- local info = StaticPopupDialogs[this.which]
- if info and info.OnShow then
- info.OnShow(this.data)
- end
- end
-
- function StaticPopup_OnHide()
- PlaySound("igMainMenuClose")
- local info = StaticPopupDialogs[this.which]
- if info and info.OnHide then
- info.OnHide(this.data)
- end
- end
-
- -- Ref l.1712-1808: the timeout countdown (expiry → OnCancel "timeout" + hide), the
- -- per-tick countdown text for the timer dialogs, the StartDelay gate (button1 disabled,
- -- delayText counts down, then the real text swaps in and the button enables), then the
- -- entry's own OnUpdate. CONFIRM_SUMMON's special text format landed with its dialog
- -- (decision 1747); INSTANCE_BOOT's joins when that dialog does, and its which-list slot
- -- stays meanwhile so the gate is the ref's.
- function StaticPopup_OnUpdate(dialog, elapsed)
- local info = StaticPopupDialogs[dialog.which]
- if not info then
- return
- end
- if (dialog.timeleft or 0) > 0 then
- local which = dialog.which
- local timeleft = dialog.timeleft - elapsed
- if timeleft <= 0 then
- dialog.timeleft = 0
- if info.OnCancel then
- info.OnCancel(dialog.data, "timeout")
- end
- dialog:Hide()
- return
- end
- dialog.timeleft = timeleft
-
- if (which == "DEATH") or (which == "CAMP") or (which == "QUIT")
- or (which == "DUEL_OUTOFBOUNDS") or (which == "INSTANCE_BOOT")
- or (which == "CONFIRM_SUMMON") or (which == "AREA_SPIRIT_HEAL") then
- local text = getglobal(dialog:GetName() .. "Text")
- timeleft = ceil(timeleft)
- local n, unit = timeleft, StaticPopupTimeUnit(timeleft, false)
- if timeleft >= 60 then
- n = ceil(timeleft / 60)
- unit = StaticPopupTimeUnit(n, true)
- end
- -- The ref's own if/elseif over `which` (l.1746-1758), which exists because two
- -- of these dialogs take MORE than the count and its unit word. CONFIRM_SUMMON
- -- prefixes the summoner and the destination, both read live from the engine
- -- every tick — which is what lets a name still resolving through the name cache
- -- paint blank once and then fill itself in (decision 1747). INSTANCE_BOOT's arm
- -- (GetBindLocation() first) joins when that dialog does; until then it falls
- -- through here, which is wrong for it and harmless, since it cannot be shown.
- if which == "CONFIRM_SUMMON" then
- text:SetText(format(info.text, GetSummonConfirmSummoner(),
- GetSummonConfirmAreaName(), n, unit))
- else
- text:SetText(format(info.text, n, unit))
- end
- end
- end
- if dialog.startDelay then
- local which = dialog.which
- local timeleft = dialog.startDelay - elapsed
- if timeleft <= 0 then
- dialog.startDelay = nil
- local text = getglobal(dialog:GetName() .. "Text")
- text:SetText(format(info.text, dialog.text_arg1, dialog.text_arg2))
- getglobal(dialog:GetName() .. "Button1"):Enable()
- return
- end
- dialog.startDelay = timeleft
-
- if (which == "RECOVER_CORPSE") or (which == "RESURRECT")
- or (which == "RESURRECT_NO_SICKNESS") then
- local text = getglobal(dialog:GetName() .. "Text")
- timeleft = ceil(timeleft)
- if (which == "RESURRECT") or (which == "RESURRECT_NO_SICKNESS") then
- if timeleft < 60 then
- text:SetText(format(info.delayText, dialog.text_arg1, timeleft,
- StaticPopupTimeUnit(timeleft, false)))
- else
- local m = ceil(timeleft / 60)
- text:SetText(format(info.delayText, dialog.text_arg1, m,
- StaticPopupTimeUnit(m, true)))
- end
- else
- if timeleft < 60 then
- text:SetText(format(info.delayText, timeleft,
- StaticPopupTimeUnit(timeleft, false)))
- else
- local m = ceil(timeleft / 60)
- text:SetText(format(info.delayText, m,
- StaticPopupTimeUnit(m, true)))
- end
- end
- end
- end
- StaticPopup_Resize(dialog, dialog.which)
- if info.OnUpdate then
- info.OnUpdate(elapsed, dialog)
- end
- end
-
- -- Ref l.1879-1893: only hideOnEscape dialogs respond — a shown DEATH popup deliberately
- -- does NOT close on ESC. Returns truthy when something closed (the caller's chain stops).
- function StaticPopup_EscapePressed()
- local closed = nil
- for index = 1, STATICPOPUP_NUMDIALOGS do
- local frame = getglobal("StaticPopup" .. index)
- if frame:IsShown() and frame.hideOnEscape then
- local OnCancel = StaticPopupDialogs[frame.which].OnCancel
- if OnCancel then
- OnCancel(frame.data, "clicked")
- end
- frame:Hide()
- closed = 1
- end
- end
- return closed
- end
-
- -- Destroy-confirm strings — the real 1.12 GlobalStrings.lua (patch-2.MPQ,
- -- Interface\FrameXML\GlobalStrings.lua l.921-923; YES/NO are l.5463/l.2794, the same pair
- -- LootFrame.xml declares), quoted verbatim in the FALLBACK form:
- -- the player's own localized GlobalStrings.lua runs off the patch chain BEFORE this file
- -- (ui_script/mod.rs), so these only fill in for the harnesses that load no chain. They
- -- were plain locals until 1743; a local could never be the localized text, and
- -- DELETE_ITEM_CONFIRM_STRING in particular MUST be the player's own word — it is what the
- -- good-item dialog below makes them type.
- DELETE_ITEM = DELETE_ITEM or "Do you want to destroy %s?"
- DELETE_GOOD_ITEM = DELETE_GOOD_ITEM
- or "Do you want to destroy %s?\n\nType \"DELETE\" into the field to confirm."
- DELETE_ITEM_CONFIRM_STRING = DELETE_ITEM_CONFIRM_STRING or "DELETE"
- YES = YES or "Yes"
- NO = NO or "No"
-
- -- ref GlobalStrings.lua l.2612 / l.2818 / l.465, fallback form (the player's own localized
- -- file runs off the patch chain first).
- -- ── The group-invite popup (ref StaticPopup.lua l.526-548) ───────────────────────────
- -- Moved here from our `PartyFrame.xml` when 1751 took that window to the player's chain.
- -- This file is our `StaticPopup.lua`, which is where the reference declares it; the party
- -- frames were only where ours was first needed.
- -- The strings and the three flags are the reference's own (INVITATION = "%s invites you to
- -- a group.", GlobalStrings:2586-area; a 60 s silent timeout, answerable while dead,
- -- closable with ESC).
- --
- -- The whole of its behaviour turns on ONE latch, `inviteAccepted`, and the four hooks below
- -- are just the four moments that read or write it. It lives on the registry entry rather
- -- than in an upvalue here because that is where the reference keeps it and an addon
- -- inspecting the dialog expects to find it there — which is why each hook goes back through
- -- `StaticPopupDialogs["PARTY_INVITE"]` instead of closing over a local.
- --
- -- They are named rather than inlined so the entry below reads as the manifest it is: four
- -- strings, three flags, four hooks.
-
- -- Every showing starts unanswered — including a re-show of the same entry, which is what a
- -- second invite arriving while the first is up does.
- local function PartyInvite_OnShow()
- StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = nil
- end
-
- -- The only path that answers the server affirmatively, and the only one that latches.
- local function PartyInvite_OnAccept()
- AcceptGroup()
- StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = 1
- end
-
- local function PartyInvite_OnCancel()
- DeclineGroup()
- end
-
- -- The catch-all. EVERY way this dialog leaves the screen lands here — Decline, the 60 s
- -- timeout, ESC, and another dialog overriding it — and the latch is what tells the one
- -- that already said yes from the four that never answered at all. A clicked Decline
- -- therefore declines twice (OnCancel, then here); see DOUBLE-DECLINE in the header, which
- -- is the reference's behaviour and the wire tolerates it.
- local function PartyInvite_OnHide()
- if not StaticPopupDialogs["PARTY_INVITE"].inviteAccepted then
- DeclineGroup()
- end
- end
-
- -- ── Two dialogs rehomed from our retired RaidFrame.xml (1874) ──────────────────────────
- -- Their reference home is `StaticPopup.lua`, which we do not load — this file is our
- -- stand-in for that engine, so this is where they belong. They came here because the raid
- -- pane went to the chain and the reference's own `RaidFrame.lua` does not declare them:
- -- deleting our file took `CONFIRM_RESET_INSTANCES` with it, and `UnitPopup.xml`'s "Reset
- -- all instances" row calls `StaticPopup_Show` on exactly that name.
- -- ── Add Member's dialog (ref StaticPopup.lua l.871-895) ─────────────────────────────────
- StaticPopupDialogs["ADD_RAIDMEMBER"] = {
- text = ADD_RAIDMEMBER_LABEL,
- button1 = "Accept",
- button2 = "Cancel",
- hasEditBox = 1,
- maxLetters = 12,
- timeout = 0,
- hideOnEscape = 1,
- OnAccept = function()
- InviteByName(getglobal("StaticPopup1EditBox"):GetText())
- end,
- OnShow = function()
- getglobal("StaticPopup1EditBox"):SetFocus()
- end,
- EditBoxOnEnterPressed = function()
- InviteByName(getglobal("StaticPopup1EditBox"):GetText())
- StaticPopup_Hide("ADD_RAIDMEMBER")
- end,
- }
-
- -- ── "Reset all instances" (ref StaticPopup.lua l.23-33; decision 1748) ──────────────────
- -- Raised by UnitPopup's SELF menu, and registered HERE because this file is the instance
- -- house — the same rule that keeps the pet dialogs in PetActionBar.xml. `ResetInstances()`
- -- is the only call in the client that sends `CMSG_RESET_INSTANCES`; everything else in the
- -- lockout family is the server talking.
- --
- -- The answer is per instance rather than one verdict, and it arrives as CHAT_MSG_SYSTEM
- -- lines the engine composes: an `SMSG_INSTANCE_RESET` per dungeon that reset, and — on
- -- the group path only — an `SMSG_INSTANCE_RESET_FAILED` per dungeon that would not.
- -- Nothing back at all is the ordinary answer for a character with nothing resettable. So
- -- there is nothing to show here and no result to wait for.
- StaticPopupDialogs["CONFIRM_RESET_INSTANCES"] = {
- text = CONFIRM_RESET_INSTANCES,
- button1 = YES,
- button2 = NO,
- timeout = 0,
- whileDead = 1,
- hideOnEscape = 1,
- OnAccept = function()
- ResetInstances()
- end,
- }
-
-
- StaticPopupDialogs["PARTY_INVITE"] = {
- text = INVITATION,
- button1 = ACCEPT,
- button2 = DECLINE,
- sound = "igPlayerInvite",
- timeout = 60,
- whileDead = 1,
- hideOnEscape = 1,
- OnShow = PartyInvite_OnShow,
- OnAccept = PartyInvite_OnAccept,
- OnCancel = PartyInvite_OnCancel,
- OnHide = PartyInvite_OnHide,
- }
-
- -- ── The logout countdowns ────────────────────────────────────────────────────────────
- -- The CAMP and QUIT dialogs, moved here from our `GameMenuFrame.xml` when 1751 took that
- -- window to the player's chain. This file is our `StaticPopup.lua`, which is where the
- -- reference declares both (l.564 / l.586) — the ESC menu never owned them, it just
- -- happened to be where ours were first needed.
- CAMP_TIMER = CAMP_TIMER or "%d %s until logout"
- QUIT_TIMER = QUIT_TIMER or "%d %s until exit"
- QUIT_NOW = QUIT_NOW or "Exit now"
- -- CANCEL before the tables below, not after: a dialog entry captures its button labels BY
- -- VALUE at load, so a `button1 = CANCEL` built while CANCEL is still nil silently falls
- -- back to the popup engine's default ("Yes") rather than failing. It reads as a wrong
- -- label on a real dialog and as nothing at all in a test that does not check the text.
- CANCEL = CANCEL or "Cancel"
-
- -- Dismissing a running countdown means "I didn't mean it": call the logout off. Only while
- -- it is still RUNNING, though — the popup engine zeroes `timeleft` before hiding a dialog
- -- that expired, so reaching zero leaves the logout alone. (Backwards, and every logout in
- -- the field is cancelled by its own dialog at t=0 and the character never leaves.) One
- -- function because both dialogs mean exactly this; `this` is the popup frame, which the
- -- engine has bound around the handler.
- local function CancelLogoutIfStillCounting()
- if this.timeleft > 0 then
- CancelLogout()
- end
- end
-
- StaticPopupDialogs["CAMP"] = {
- text = CAMP_TIMER,
- button1 = CANCEL,
- timeout = 20,
- whileDead = 1,
- hideOnEscape = 1,
- OnAccept = function()
- CancelLogout()
- end,
- OnHide = CancelLogoutIfStillCounting,
- }
-
- StaticPopupDialogs["QUIT"] = {
- text = QUIT_TIMER,
- button1 = QUIT_NOW,
- button2 = CANCEL,
- timeout = 20,
- whileDead = 1,
- hideOnEscape = 1,
- -- A force-quit needs no server round trip, so the countdown it was narrating is over
- -- the moment the button is pressed: zero `timeleft` so the OnHide below stands down.
- OnAccept = function()
- ForceQuit()
- this.timeleft = 0
- end,
- OnHide = CancelLogoutIfStillCounting,
- }
-
- LOOT_NO_DROP = LOOT_NO_DROP or "Looting this item will bind it to you."
- OKAY = OKAY or "Okay"
-
- -- ref StaticPopup.lua l.601-611, verbatim. `OnAccept`'s argument is `dialog.data`, the
- -- event's row — and `LootSlot` is the CONTINUATION verb, so this line is the only thing in
- -- the client that ever completes a bind-deferred take (decision 1744).
- --
- -- It lives here rather than in the loot window because this file is our `StaticPopup.lua`
- -- and that is the reference's own home for it. Our transcription of `LootFrame.xml`
- -- declared it; the stock file does not, so 1751 moved it where it always belonged.
- --
- -- No OnCancel (declining leaves the row in the window, and the app drops the pending stash
- -- when the window turns over) and no OnUpdate poll: the loot window's own LOOT_CLOSED
- -- hides it (`LootFrame.lua:53-54`).
- StaticPopupDialogs["LOOT_BIND"] = {
- text = LOOT_NO_DROP,
- button1 = OKAY,
- button2 = CANCEL,
- OnAccept = function(slot)
- LootSlot(slot)
- end,
- timeout = 0,
- exclusive = 1,
- hideOnEscape = 1,
- }
-
- CONFIRM_LOOT_DISTRIBUTION = CONFIRM_LOOT_DISTRIBUTION
- or "You wish to assign %s to %s. Is this correct?" -- GlobalStrings.lua l.848
-
- -- The master-loot confirmation the group-loot dropdown raises for an at-or-above-threshold
- -- item (ref StaticPopup.lua l.85-94, verbatim). It lives HERE because that is the
- -- reference's own home for it — this file is our `StaticPopup.lua` — and because 1751 took
- -- `LootFrame.xml` to the player's chain: our transcription used to declare this dialog
- -- itself, and the stock file does not.
- --
- -- `data` is the candidate index the dropdown stashed; the SLOT is re-read live from
- -- `LootFrame.selectedSlot` at accept time, exactly as the reference does. That is not a
- -- shortcut — it is why the row click hides this popup first (`LootFrame.lua:156`), so an
- -- accept can never land on a row the player has since clicked away from.
- StaticPopupDialogs["CONFIRM_LOOT_DISTRIBUTION"] = {
- text = CONFIRM_LOOT_DISTRIBUTION,
- button1 = YES,
- button2 = NO,
- OnAccept = function(data)
- GiveMasterLoot(LootFrame.selectedSlot, data)
- end,
- timeout = 0,
- hideOnEscape = 1,
- }
-
- -- Ref StaticPopup.lua l.659-679, verbatim — its own OnUpdate poll (cursor emptied by any
- -- other path → hide) replaces the old bespoke `deleteItemFlow` CURSOR_UPDATE driver.
- -- exclusive is v1-inert (engine header note); showAlert is live since 1068 — the ref's
- -- own "display the error image" case is this very dialog.
- -- CONFIRM_LOOT_ROLL — the BoP roll's "this will bind to you" gate, keyed BARE like every
- -- other entry (the key is `StaticPopup_Show`'s argument, not a frame name).
- --
- -- It moved here from our own GroupLootFrame.xml when 1838 retired that file. The reference
- -- keeps it in `StaticPopup.lua`; we have no counterpart to that file, and our own
- -- convention is that a dialog whose window has migrated comes to sit beside DELETE_ITEM
- -- below — the same move `CloseMenus` made when the dropdown kit went to the chain.
- --
- -- The C `RollOnLoot` is what diverts a BoP Need/Greed here rather than rolling: the gate
- -- lives in the binding, not in Lua (decision 0594, verified in the 5875 binary), and only
- -- `OnAccept`'s `ConfirmLootRoll` re-enters past it. `id`/`rollType` arrive as data/data2,
- -- set by the `CONFIRM_LOOT_ROLL` arm in UIParent.xml's own OnEvent.
- --
- -- The three strings are quoted verbatim rather than read from GlobalStrings, which is this
- -- file's existing idiom for exactly one reason: a bare global is nil in a harness that has
- -- not loaded the chain's GlobalStrings, and a `format()` over nil aborts the whole inline
- -- chunk — taking every function defined after it down with it.
- StaticPopupDialogs["CONFIRM_LOOT_ROLL"] = {
- text = "Looting this item will bind it to you.", -- LOOT_NO_DROP
- button1 = "Okay", -- OKAY
- button2 = "Cancel", -- CANCEL
- OnAccept = function(id, rollType)
- ConfirmLootRoll(id, rollType)
- end,
- timeout = 0,
- whileDead = 1,
- exclusive = 1,
- hideOnEscape = 1
- }
-
- StaticPopupDialogs["DELETE_ITEM"] = {
- text = DELETE_ITEM,
- button1 = YES,
- button2 = NO,
- OnAccept = function()
- DeleteCursorItem()
- end,
- OnCancel = function()
- ClearCursor()
- end,
- OnUpdate = function()
- if not CursorHasItem() then
- StaticPopup_Hide("DELETE_ITEM")
- end
- end,
- timeout = 0,
- whileDead = 1,
- exclusive = 1,
- showAlert = 1,
- hideOnEscape = 1,
- }
-
- -- Ref StaticPopup.lua l.680-724, verbatim — the RARE-or-better destroy. Same accept,
- -- cancel and auto-hide poll as DELETE_ITEM above; what it adds is the typed confirmation:
- -- the edit box takes focus on open, button1 starts DISABLED, and only the exact
- -- DELETE_ITEM_CONFIRM_STRING (case-insensitively, via the ref's own strupper) enables it.
- --
- -- `this` is the dialog inside OnShow/OnHide and the EDIT BOX inside the two EditBox hooks —
- -- the reference's own convention, which is why those two reach the dialog through
- -- `this:GetParent()`. Our engine sets the same `this` on both paths (StaticPopup_OnShow /
- -- the template's /), so the bodies transcribe unchanged.
- --
- -- The one guard added: the ref's OnHide reads ChatFrameEditBox unguarded, which raises in a
- -- harness that loads no ChatFrame.xml. Same `and` the bag's shift-link arm carries.
- StaticPopupDialogs["DELETE_GOOD_ITEM"] = {
- text = DELETE_GOOD_ITEM,
- button1 = YES,
- button2 = NO,
- OnAccept = function()
- DeleteCursorItem()
- end,
- OnCancel = function()
- ClearCursor()
- end,
- OnUpdate = function()
- if not CursorHasItem() then
- StaticPopup_Hide("DELETE_GOOD_ITEM")
- end
- end,
- timeout = 0,
- whileDead = 1,
- exclusive = 1,
- showAlert = 1,
- hideOnEscape = 1,
- hasEditBox = 1,
- maxLetters = 32,
- OnShow = function()
- getglobal(this:GetName() .. "Button1"):Disable()
- getglobal(this:GetName() .. "EditBox"):SetFocus()
- end,
- OnHide = function()
- if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
- ChatFrameEditBox:SetFocus()
- end
- getglobal(this:GetName() .. "EditBox"):SetText("")
- end,
- EditBoxOnEnterPressed = function()
- if getglobal(this:GetParent():GetName() .. "Button1"):IsEnabled() == 1 then
- DeleteCursorItem()
- this:GetParent():Hide()
- end
- end,
- EditBoxOnTextChanged = function()
- local editBox = getglobal(this:GetParent():GetName() .. "EditBox")
- if strupper(editBox:GetText()) == DELETE_ITEM_CONFIRM_STRING then
- getglobal(this:GetParent():GetName() .. "Button1"):Enable()
- else
- getglobal(this:GetParent():GetName() .. "Button1"):Disable()
- end
- end,
- }
-
- -- ── The equip / auto-equip / use soulbind confirms (decision 1750) ────────────────────
- -- ref GlobalStrings.lua l.1459 / l.4453 / l.2818 / l.465, fallback form.
- EQUIP_NO_DROP = EQUIP_NO_DROP or "Equipping this item will bind it to you."
- USE_NO_DROP = USE_NO_DROP or "Using this item will bind it to you."
- OKAY = OKAY or "Okay"
- CANCEL = CANCEL or "Cancel"
-
- -- ref StaticPopup.lua l.612-647, verbatim, twice: EQUIP_BIND and AUTOEQUIP_BIND are the
- -- SAME entry down to the last field — same text, same three hooks — and differ only in
- -- which event raises them. Built from one factory rather than transcribed twice, because
- -- two copies of a nine-field table is exactly how the two drift apart.
- --
- -- `data` is a 0-based index into the client's pending-equip array, NOT a slot (the RE
- -- corrected benilla's assumption here). It is opaque: the event hands it out and these
- -- hooks hand it straight back.
- --
- -- **OnCancel AND OnHide both cancel, and that is deliberate, not redundant.** OnHide is
- -- what retires a record when `exclusive = 1` makes a second bind question hide this one;
- -- without it the superseded item's pending record leaks. The doubling it causes on the
- -- ordinary paths is harmless by construction — an Accept runs OnAccept then OnHide, so the
- -- app sees [accept, cancel] for one index and the cancel finds the record already taken.
- local function equip_bind_entry()
- return {
- text = EQUIP_NO_DROP,
- button1 = OKAY,
- button2 = CANCEL,
- OnAccept = function(index)
- EquipPendingItem(index)
- end,
- OnCancel = function(index)
- CancelPendingEquip(index)
- end,
- OnHide = function(index)
- CancelPendingEquip(index)
- end,
- timeout = 0,
- exclusive = 1,
- whileDead = 1,
- hideOnEscape = 1,
- }
- end
- StaticPopupDialogs["EQUIP_BIND"] = equip_bind_entry()
- StaticPopupDialogs["AUTOEQUIP_BIND"] = equip_bind_entry()
-
- -- ref StaticPopup.lua l.648-658. No index, no cancel hook: the use arm's pending state is
- -- a single cell in the client rather than an array element, and declining just drops it.
- StaticPopupDialogs["USE_BIND"] = {
- text = USE_NO_DROP,
- button1 = OKAY,
- button2 = CANCEL,
- OnAccept = function()
- ConfirmBindOnUse()
- end,
- timeout = 0,
- exclusive = 1,
- hideOnEscape = 1,
- }
-
- -- The bank-slot purchase confirm (ref StaticPopup.lua l.47-60), transcribed verbatim.
- --
- -- **It is HERE because its window is the reference's own file now** (1751). Every other
- -- dialog in this client is declared by the window that shows it; the bank window is
- -- `Interface\FrameXML\BankFrame.xml`, executed off the player's chain, and the reference
- -- keeps this entry in `StaticPopup.lua` — the one file benilla replaces with the engine
- -- above. So the house's StaticPopup home carries it, and `BankFramePurchaseButton`'s
- -- `StaticPopup_Show("CONFIRM_BUY_BANK_SLOT")` finds it. Left out, the Purchase button
- -- would be a silent no-op.
- --
- -- The coin row is real (1580 made `hasMoneyFrame` live): `BankFrame.nextSlotCost` is set
- -- by the reference's own `UpdateBagSlotStatus` off `GetBankSlotCost`, and this OnShow is
- -- the reference's own line for reading it.
- CONFIRM_BUY_BANK_SLOT = CONFIRM_BUY_BANK_SLOT
- or "Do you want to purchase a bank slot for:"
- StaticPopupDialogs["CONFIRM_BUY_BANK_SLOT"] = {
- text = CONFIRM_BUY_BANK_SLOT,
- button1 = YES,
- button2 = NO,
- OnAccept = function()
- PurchaseSlot()
- end,
- OnShow = function()
- MoneyFrame_Update(this:GetName() .. "MoneyFrame", BankFrame.nextSlotCost)
- end,
- hasMoneyFrame = 1,
- timeout = 0,
- hideOnEscape = 1,
- }
-
- -- The guild-charter rename box, ref StaticPopup.lua l.133-160 — **in the reference's own
- -- home, which is this file**. It lived in our PetitionFrame.xml on the reasoning that "a
- -- popup belongs to the window that raises it"; that is a fine rule and it is not 1.12's,
- -- which keeps every dialog in one registry. 1751's twelfth window made the charter the
- -- reference's own file, whose Rename button calls `StaticPopup_Show("RENAME_GUILD")` and
- -- registers nothing, so the entry had to come here.
- --
- -- 24 letters is the server's MAX_CHARTER_NAME, so the box cannot compose a name that will
- -- be refused for length. Accept and Enter run the same two lines; the reference spells
- -- them out twice (l.139-147), and its Enter arm hides the dialog while its Accept arm does
- -- not — the popup engine hides on a plain Accept anyway.
- StaticPopupDialogs["RENAME_GUILD"] = {
- text = RENAME_GUILD_LABEL,
- button1 = ACCEPT,
- button2 = CANCEL,
- hasEditBox = 1,
- maxLetters = 24,
- OnAccept = function()
- RenamePetition(getglobal(this:GetParent():GetName() .. "EditBox"):GetText())
- end,
- EditBoxOnEnterPressed = function()
- RenamePetition(getglobal(this:GetParent():GetName() .. "EditBox"):GetText())
- this:GetParent():Hide()
- end,
- OnShow = function()
- getglobal(this:GetName() .. "EditBox"):SetFocus()
- end,
- OnHide = function()
- if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
- ChatFrameEditBox:SetFocus()
- end
- getglobal(this:GetName() .. "EditBox"):SetText("")
- end,
- timeout = 0,
- exclusive = 1,
- whileDead = 1,
- hideOnEscape = 1,
- }
-
- -- The quest log's abandon confirm and its with-items twin — ref StaticPopup.lua l.749-775,
- -- in the reference's own home, which is this file (the same move RENAME_GUILD made). They
- -- lived in our QuestLogFrame.xml until 1944 took that window stock; stock's Abandon button
- -- calls `StaticPopup_Show("ABANDON_QUEST", GetAbandonQuestName())` and registers nothing.
- -- The wording is the GlobalStrings key by name, as this file spells its dialogs (the
- -- reference wraps the same globals in TEXT()); the with-items twin is declared as the
- -- reference declares it, whatever `GetAbandonQuestItems()` answers.
- StaticPopupDialogs["ABANDON_QUEST"] = {
- text = ABANDON_QUEST_CONFIRM,
- button1 = YES,
- button2 = NO,
- OnAccept = function()
- AbandonQuest()
- PlaySound("igQuestLogAbandonQuest")
- end,
- timeout = 0,
- whileDead = 1,
- exclusive = 1,
- hideOnEscape = 1,
- }
- StaticPopupDialogs["ABANDON_QUEST_WITH_ITEMS"] = {
- text = ABANDON_QUEST_CONFIRM_WITH_ITEMS,
- button1 = YES,
- button2 = NO,
- OnAccept = function()
- AbandonQuest()
- PlaySound("igQuestLogAbandonQuest")
- end,
- timeout = 0,
- whileDead = 1,
- exclusive = 1,
- hideOnEscape = 1,
- }
-
- -- The pet arc's three (decision 1066; ref StaticPopup.lua l.738, l.1069, l.365). ABANDON_PET
- -- is the plain confirm. RENAME_PET is a CHAIN of two, which is the reference's own shape and
- -- not a flourish: the first collects the name, the second reads it back ("Name your pet
- -- 'Bruce'?") before anything is sent — a rename is one-shot, so a typo is permanent the way
- -- the abandon is. That second Show happens while the first dialog is still up, which is
- -- why the engine's two instances are both needed. Registered beside the other dialogs since
- -- the pet bar became the reference's file (1953).
- StaticPopupDialogs["ABANDON_PET"] = {
- text = ABANDON_PET,
- button1 = OKAY,
- button2 = CANCEL,
- OnAccept = function()
- PetAbandon();
- end,
- timeout = 0,
- exclusive = 1,
- hideOnEscape = 1,
- }
-
- StaticPopupDialogs["RENAME_PET"] = {
- text = PET_RENAME_LABEL,
- button1 = ACCEPT,
- button2 = CANCEL,
- hasEditBox = 1,
- -- The 12-letter cap is the reference's, and it is the client's only name check: the
- -- server re-validates with CheckPetName and refuses with SMSG_PET_NAME_INVALID.
- maxLetters = 12,
- OnAccept = function()
- -- `this` is the clicked button, so its parent is THIS dialog — never
- -- StaticPopup1 by name, which would read the wrong box the moment this dialog
- -- lands in the second instance (the chain below is exactly that case).
- local text = getglobal(this:GetParent():GetName() .. "EditBox"):GetText()
- local dialog = StaticPopup_Show("PETRENAMECONFIRM", text)
- if dialog then
- dialog.data = text
- end
- end,
- EditBoxOnEnterPressed = function()
- local text = getglobal(this:GetParent():GetName() .. "EditBox"):GetText()
- local dialog = StaticPopup_Show("PETRENAMECONFIRM", text)
- if dialog then
- dialog.data = text
- end
- this:GetParent():Hide()
- end,
- OnShow = function()
- getglobal(this:GetName() .. "EditBox"):SetFocus()
- end,
- timeout = 0,
- exclusive = 1,
- hideOnEscape = 1,
- }
-
- StaticPopupDialogs["PETRENAMECONFIRM"] = {
- -- "Name your pet '%s'?" — the name arrives as text_arg1 AND as `data`; the first
- -- fills the question, the second is what actually gets sent.
- text = PET_RENAME_CONFIRMATION,
- button1 = YES,
- button2 = NO,
- OnAccept = function(data)
- PetRename(data);
- end,
- timeout = 0,
- hideOnEscape = 1,
- }
-
- -- The skills page's unlearn confirm (ref StaticPopup.lua l.1121-1133), registered beside
- -- the other dialogs since the page became the reference's file (1956).
- StaticPopupDialogs["UNLEARN_SKILL"] = {
- text = UNLEARN_SKILL,
- button1 = UNLEARN,
- button2 = CANCEL,
- OnAccept = function(index)
- AbandonSkill(index)
- end,
- timeout = 60,
- exclusive = 1,
- whileDead = 1,
- showAlert = 1,
- hideOnEscape = 1
- }
-
- -- ref UIParent.lua l.324-343 + l.356-360, the four arms on one driver. Each equip arm
- -- hides the OTHER dialog first — the two questions are mutually exclusive by construction
- -- (one item, one destination), and the hide is what cancels the record the new question
- -- replaces.
- function BenillaBindConfirmDriver_OnEvent(frame)
- if event == "EQUIP_BIND_CONFIRM" then
- StaticPopup_Hide("AUTOEQUIP_BIND")
- local dialog = StaticPopup_Show("EQUIP_BIND")
- if dialog then
- dialog.data = arg1
- end
- elseif event == "AUTOEQUIP_BIND_CONFIRM" then
- StaticPopup_Hide("EQUIP_BIND")
- local dialog = StaticPopup_Show("AUTOEQUIP_BIND")
- if dialog then
- dialog.data = arg1
- end
- elseif event == "PARTY_INVITE_REQUEST" then
- -- ref UIParent.lua's own arms. Same move as the logout three and the loot bind
- -- confirm: the popup-raising event belongs beside the popup, not in whichever
- -- window happened to need it first.
- StaticPopup_Show("PARTY_INVITE", arg1)
- elseif event == "PARTY_INVITE_CANCEL" then
- StaticPopup_Hide("PARTY_INVITE")
- elseif event == "PLAYER_CAMPING" then
- -- ref UIParent.lua l.304-315. These three lived on our GameMenuFrame.xml's own
- -- hidden listener, on the reasoning that benilla has no UIParent frame; they are
- -- here now for the same reason the loot bind confirm is — 1751 took that window
- -- to the chain, and the reference's placement (UIParent, beside every other
- -- popup-raising event) is the one that survives the window being someone else's.
- StaticPopup_Show("CAMP")
- elseif event == "PLAYER_QUITING" then
- StaticPopup_Show("QUIT")
- elseif event == "LOGOUT_CANCEL" then
- StaticPopup_Hide("CAMP")
- StaticPopup_Hide("QUIT")
- elseif event == "LOOT_BIND_CONFIRM" then
- -- ref UIParent.lua l.317-323. The row rides on `dialog.data` rather than as a
- -- text arg — LOOT_NO_DROP has no format specifier, so the sentence is the same
- -- whatever binds; the number is only there to hand back to `LootSlot`.
- --
- -- This arm used to live in our own `LootFrame.xml`, on the window's event
- -- listener, on the reasoning that LOOT_CLOSED already had to hide the dialog and
- -- the arc stayed in one file. 1751 took that window to the chain, and the
- -- reference's placement — UIParent, beside every other bind confirm — turned out
- -- to be the one that survives the window being someone else's.
- local dialog = StaticPopup_Show("LOOT_BIND")
- if dialog then
- dialog.data = arg1
- end
- elseif event == "USE_BIND_CONFIRM" then
- StaticPopup_Show("USE_BIND")
- elseif event == "CURSOR_UPDATE" then
- -- ref l.356-360: the player picked something else up, so the standing question is
- -- stale. Both hides run their OnHide, which cancels the record.
- StaticPopup_Hide("EQUIP_BIND")
- StaticPopup_Hide("AUTOEQUIP_BIND")
- end
- end
-
- -- BenillaDeleteItemConfirmDriver's OnEvent — DELETE_ITEM_CONFIRM(name, quality) is the
- -- engine's world-drop of an item payload (decision 0216 §3, cursor.rs::world_drop) →
- -- ref UIParent.lua:344-352, now whole: the ref's `arg2 >= 3` fork routes RARE-or-better
- -- (quality 3..6 — blue, epic, legendary, artifact) to the typed-confirmation variant and
- -- everything below it to the plain one. 0216 §3's deferral is closed (1743).
- function BenillaDeleteItemConfirmDriver_OnEvent(frame)
- if event == "DELETE_ITEM_CONFIRM" then
- if arg2 >= 3 then
- StaticPopup_Show("DELETE_GOOD_ITEM", arg1)
- else
- StaticPopup_Show("DELETE_ITEM", arg1)
- end
- end
- end
-
-- ── The shared frame-fade kit — ref UIParent.lua:1103-1219 is the specification (the
-- function names are its published API and the arithmetic is the same lerp); the code is
-- ours. FADEFRAMES is a plain ARRAY walked by index rather than by `pairs`, because
@@ -2731,212 +1541,4 @@
only consumer and loads long after that entry, so it has been inheriting the reference's
since the swap. -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- StaticPopup_EditBoxOnEnterPressed(self)
- StaticPopup_EditBoxOnEscapePressed(self)
- StaticPopup_EditBoxOnTextChanged(self)
-
-
-
-
-
-
-
-
-
- SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("STATIC")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- StaticPopup_EditBoxOnEnterPressed(self)
- StaticPopup_EditBoxOnEscapePressed(self)
-
-
-
-
-
-
- StaticPopup_OnShow()
- StaticPopup_OnHide()
- StaticPopup_OnUpdate(this, arg1)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- self:RegisterEvent("DELETE_ITEM_CONFIRM")
-
- BenillaDeleteItemConfirmDriver_OnEvent(self)
-
-
-
-
-
-
-
-
-
- self:RegisterEvent("EQUIP_BIND_CONFIRM")
- self:RegisterEvent("AUTOEQUIP_BIND_CONFIRM")
- self:RegisterEvent("USE_BIND_CONFIRM")
- self:RegisterEvent("LOOT_BIND_CONFIRM")
- self:RegisterEvent("PLAYER_CAMPING")
- self:RegisterEvent("PLAYER_QUITING")
- self:RegisterEvent("LOGOUT_CANCEL")
- self:RegisterEvent("PARTY_INVITE_REQUEST")
- self:RegisterEvent("PARTY_INVITE_CANCEL")
- self:RegisterEvent("CURSOR_UPDATE")
-
- BenillaBindConfirmDriver_OnEvent(self)
-
-
diff --git a/crates/benilla-app/assets/ui/UnitPopup.xml b/crates/benilla-app/assets/ui/UnitPopup.xml
deleted file mode 100644
index ebc205f2..00000000
--- a/crates/benilla-app/assets/ui/UnitPopup.xml
+++ /dev/null
@@ -1,885 +0,0 @@
-
-
-
-
-
-
-
-
-
- UnitPopup_OnUpdate(arg1)
-
-
-
diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc
index a9a1c19a..f4c5410d 100644
--- a/crates/benilla-app/assets/ui/benilla.toc
+++ b/crates/benilla-app/assets/ui/benilla.toc
@@ -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
diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs
index 4e88eb31..a189f10d 100644
--- a/crates/benilla-app/src/bindings.rs
+++ b/crates/benilla-app/src/bindings.rs
@@ -1178,7 +1178,7 @@ mod tests {
assert_eq!(
s.eval::(&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);
diff --git a/crates/benilla-app/src/capture/probes/dress_census.rs b/crates/benilla-app/src/capture/probes/dress_census.rs
index b6e3d4b6..1c369ae1 100644
--- a/crates/benilla-app/src/capture/probes/dress_census.rs
+++ b/crates/benilla-app/src/capture/probes/dress_census.rs
@@ -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,
time: ProbeClock,
names: Res,
- body: Query<&Transform, With>,
+ body: Query<(Entity, &Transform), With>,
+ // The self body's parts, one line each (`DRESS_PART`) — the draw-population inventory.
+ body_parts: crate::entities::BodyPartsDesc,
entities: Query,
// 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.
diff --git a/crates/benilla-app/src/capture/probes/live_fps.rs b/crates/benilla-app/src/capture/probes/live_fps.rs
index 4529b6ca..62850b8b 100644
--- a/crates/benilla-app/src/capture/probes/live_fps.rs
+++ b/crates/benilla-app/src/capture/probes/live_fps.rs
@@ -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();
diff --git a/crates/benilla-app/src/entities.rs b/crates/benilla-app/src/entities.rs
index d9c3fb21..a224b395 100644
--- a/crates/benilla-app/src/entities.rs
+++ b/crates/benilla-app/src/entities.rs
@@ -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`).
diff --git a/crates/benilla-app/src/entities/attach/mod.rs b/crates/benilla-app/src/entities/attach/mod.rs
index 4537343c..e0c0a53d 100644
--- a/crates/benilla-app/src/entities/attach/mod.rs
+++ b/crates/benilla-app/src/entities/attach/mod.rs
@@ -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,
+ ),
+ >,
+}
+
+impl BodyPartsDesc<'_, '_> {
+ /// One line per drawn part under `unit`, sorted by part index.
+ pub(crate) fn describe(&self, unit: Entity, children: &Query<&Children>) -> Vec {
+ 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
diff --git a/crates/benilla-app/src/ui_chat/input/mod.rs b/crates/benilla-app/src/ui_chat/input/mod.rs
index a6f16287..43a4f860 100644
--- a/crates/benilla-app/src/ui_chat/input/mod.rs
+++ b/crates/benilla-app/src/ui_chat/input/mod.rs
@@ -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).
diff --git a/crates/benilla-app/src/ui_chat/input/parse.rs b/crates/benilla-app/src/ui_chat/input/parse.rs
index 40cc1e5f..3848fd8b 100644
--- a/crates/benilla-app/src/ui_chat/input/parse.rs
+++ b/crates/benilla-app/src/ui_chat/input/parse.rs
@@ -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 ` (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[""]()` (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
diff --git a/crates/benilla-app/src/ui_chat/tests.rs b/crates/benilla-app/src/ui_chat/tests.rs
index dc256c75..afc30f90 100644
--- a/crates/benilla-app/src/ui_chat/tests.rs
+++ b/crates/benilla-app/src/ui_chat/tests.rs
@@ -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);
diff --git a/crates/benilla-app/src/ui_items/drain.rs b/crates/benilla-app/src/ui_items/drain.rs
index 8b0984b0..489dfd42 100644
--- a/crates/benilla-app/src/ui_items/drain.rs
+++ b/crates/benilla-app/src/ui_items/drain.rs
@@ -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);
}
diff --git a/crates/benilla-app/src/ui_pet/drain.rs b/crates/benilla-app/src/ui_pet/drain.rs
index 964db50a..bfcedb40 100644
--- a/crates/benilla-app/src/ui_pet/drain.rs
+++ b/crates/benilla-app/src/ui_pet/drain.rs
@@ -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
diff --git a/crates/benilla-app/src/ui_script/action_bar_tests.rs b/crates/benilla-app/src/ui_script/action_bar_tests.rs
index cebdf175..7a77b8b3 100644
--- a/crates/benilla-app/src/ui_script/action_bar_tests.rs
+++ b/crates/benilla-app/src/ui_script/action_bar_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/addons.rs b/crates/benilla-app/src/ui_script/addons.rs
index dd643450..b3598d35 100644
--- a/crates/benilla-app/src/ui_script/addons.rs
+++ b/crates/benilla-app/src/ui_script/addons.rs
@@ -370,7 +370,56 @@ fn root_from(home: Option) -> Option {
/// 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 {
+ 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 {
+ let mut found = discover_folder();
+ found.extend(chain_addons());
+ found
+}
+
+/// The player's own addons — every folder under the AddOns root with a `.toc`.
+fn discover_folder() -> Vec {
let Some(root) = root() else {
return Vec::new();
};
@@ -424,7 +473,9 @@ fn manifest_in(dir: &Path, name: &str) -> Option {
/// 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 {
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::>()
+ );
+ 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::("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::("return GetNumAddOns()").ok(),
+ Some(1 + chain_addons().len() as i64)
+ );
assert_eq!(
script
.eval::("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::("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::("return GetNumAddOns()").ok(),
+ Some(1 + chain_addons().len() as i64)
+ );
assert_eq!(
script
.eval::>(
@@ -1861,7 +1965,12 @@ mod tests {
let _ = load_third_party(&mut script, None, true);
// Discovered and described, but NOT run.
- assert_eq!(script.eval::("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::("return GetNumAddOns()").ok(),
+ Some(1 + chain_addons().len() as i64)
+ );
assert_eq!(
script
.eval::("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"
);
diff --git a/crates/benilla-app/src/ui_script/binder_tests.rs b/crates/benilla-app/src/ui_script/binder_tests.rs
index c84d646d..dcabdbeb 100644
--- a/crates/benilla-app/src/ui_script/binder_tests.rs
+++ b/crates/benilla-app/src/ui_script/binder_tests.rs
@@ -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
}
diff --git a/crates/benilla-app/src/ui_script/buff_tests.rs b/crates/benilla-app/src/ui_script/buff_tests.rs
index aa3a8aec..92edb8ef 100644
--- a/crates/benilla-app/src/ui_script/buff_tests.rs
+++ b/crates/benilla-app/src/ui_script/buff_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/cast_tests.rs b/crates/benilla-app/src/ui_script/cast_tests.rs
index 9404d969..d5feb42a 100644
--- a/crates/benilla-app/src/ui_script/cast_tests.rs
+++ b/crates/benilla-app/src/ui_script/cast_tests.rs
@@ -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.
diff --git a/crates/benilla-app/src/ui_script/character_tests.rs b/crates/benilla-app/src/ui_script/character_tests.rs
index 83205115..c6c8fef1 100644
--- a/crates/benilla-app/src/ui_script/character_tests.rs
+++ b/crates/benilla-app/src/ui_script/character_tests.rs
@@ -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 `Tab1..N` (what `PanelTemplates_UpdateTabs` getglobals)
diff --git a/crates/benilla-app/src/ui_script/chat_options_tests.rs b/crates/benilla-app/src/ui_script/chat_options_tests.rs
index d6f6d89a..1f0f4aab 100644
--- a/crates/benilla-app/src/ui_script/chat_options_tests.rs
+++ b/crates/benilla-app/src/ui_script/chat_options_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/chat_resize_tests.rs b/crates/benilla-app/src/ui_script/chat_resize_tests.rs
index e54b3dfb..80540903 100644
--- a/crates/benilla-app/src/ui_script/chat_resize_tests.rs
+++ b/crates/benilla-app/src/ui_script/chat_resize_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/chat_tests.rs b/crates/benilla-app/src/ui_script/chat_tests.rs
index 5e498554..fd08bcaa 100644
--- a/crates/benilla-app/src/ui_script/chat_tests.rs
+++ b/crates/benilla-app/src/ui_script/chat_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/cinematic_tests.rs b/crates/benilla-app/src/ui_script/cinematic_tests.rs
index 1c6e8ad2..cf0fd768 100644
--- a/crates/benilla-app/src/ui_script/cinematic_tests.rs
+++ b/crates/benilla-app/src/ui_script/cinematic_tests.rs
@@ -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
diff --git a/crates/benilla-app/src/ui_script/color_picker_tests.rs b/crates/benilla-app/src/ui_script/color_picker_tests.rs
index 6bebc046..b1eff707 100644
--- a/crates/benilla-app/src/ui_script/color_picker_tests.rs
+++ b/crates/benilla-app/src/ui_script/color_picker_tests.rs
@@ -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);
diff --git a/crates/benilla-app/src/ui_script/combo_frame_tests.rs b/crates/benilla-app/src/ui_script/combo_frame_tests.rs
index f8b48742..44da3c04 100644
--- a/crates/benilla-app/src/ui_script/combo_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/combo_frame_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/death_tests.rs b/crates/benilla-app/src/ui_script/death_tests.rs
index a47de347..a6cac816 100644
--- a/crates/benilla-app/src/ui_script/death_tests.rs
+++ b/crates/benilla-app/src/ui_script/death_tests.rs
@@ -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::("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::("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::("return StaticPopup1Text:GetText()")
.unwrap(),
- "2 seconds until resurrection"
+ "2 Seconds until resurrection"
);
s.tick(1.6);
assert_eq!(
diff --git a/crates/benilla-app/src/ui_script/delete_item_tests.rs b/crates/benilla-app/src/ui_script/delete_item_tests.rs
index 1dab4349..c7fa3caf 100644
--- a/crates/benilla-app/src/ui_script/delete_item_tests.rs
+++ b/crates/benilla-app/src/ui_script/delete_item_tests.rs
@@ -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::("return StaticPopup1:IsVisible()").unwrap(),
- "ESC closes it"
+ s.eval::("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::("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());
}
diff --git a/crates/benilla-app/src/ui_script/dressup_tests.rs b/crates/benilla-app/src/ui_script/dressup_tests.rs
index ff554ac6..e6a5422d 100644
--- a/crates/benilla-app/src/ui_script/dressup_tests.rs
+++ b/crates/benilla-app/src/ui_script/dressup_tests.rs
@@ -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);
diff --git a/crates/benilla-app/src/ui_script/duel_tests.rs b/crates/benilla-app/src/ui_script/duel_tests.rs
index e6e8fac7..2f458503 100644
--- a/crates/benilla-app/src/ui_script/duel_tests.rs
+++ b/crates/benilla-app/src/ui_script/duel_tests.rs
@@ -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::("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![]);
diff --git a/crates/benilla-app/src/ui_script/durability_tests.rs b/crates/benilla-app/src/ui_script/durability_tests.rs
index 29216d43..22dd7b01 100644
--- a/crates/benilla-app/src/ui_script/durability_tests.rs
+++ b/crates/benilla-app/src/ui_script/durability_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/enchant_confirm_tests.rs b/crates/benilla-app/src/ui_script/enchant_confirm_tests.rs
index fbb7031d..98e86be5 100644
--- a/crates/benilla-app/src/ui_script/enchant_confirm_tests.rs
+++ b/crates/benilla-app/src/ui_script/enchant_confirm_tests.rs
@@ -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
}
diff --git a/crates/benilla-app/src/ui_script/errors_tests.rs b/crates/benilla-app/src/ui_script/errors_tests.rs
index 09ff622f..b26d5f66 100644
--- a/crates/benilla-app/src/ui_script/errors_tests.rs
+++ b/crates/benilla-app/src/ui_script/errors_tests.rs
@@ -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
diff --git a/crates/benilla-app/src/ui_script/escape_tests.rs b/crates/benilla-app/src/ui_script/escape_tests.rs
index 6a5fab8a..efd70e0f 100644
--- a/crates/benilla-app/src/ui_script/escape_tests.rs
+++ b/crates/benilla-app/src/ui_script/escape_tests.rs
@@ -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.
diff --git a/crates/benilla-app/src/ui_script/fade_tests.rs b/crates/benilla-app/src/ui_script/fade_tests.rs
index 2776ef2f..4a551302 100644
--- a/crates/benilla-app/src/ui_script/fade_tests.rs
+++ b/crates/benilla-app/src/ui_script/fade_tests.rs
@@ -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.
diff --git a/crates/benilla-app/src/ui_script/frame_flag_gate.rs b/crates/benilla-app/src/ui_script/frame_flag_gate.rs
index 682e5563..5cabbad1 100644
--- a/crates/benilla-app/src/ui_script/frame_flag_gate.rs
+++ b/crates/benilla-app/src/ui_script/frame_flag_gate.rs
@@ -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!(
diff --git a/crates/benilla-app/src/ui_script/friends_tests.rs b/crates/benilla-app/src/ui_script/friends_tests.rs
index 1056c9ca..b2b4a223 100644
--- a/crates/benilla-app/src/ui_script/friends_tests.rs
+++ b/crates/benilla-app/src/ui_script/friends_tests.rs
@@ -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::("return FriendsFrameFriendButton1NameLocation:GetText()")
+ s.eval::("return FriendsFrameFriendButton1ButtonTextNameLocation:GetText()")
.unwrap(),
"Onerogue |cffffffff- Elwynn Forest|r "
);
assert_eq!(
- s.eval::("return FriendsFrameFriendButton1Info:GetText()")
+ s.eval::("return FriendsFrameFriendButton1ButtonTextInfo:GetText()")
.unwrap(),
"Level 60 Rogue"
);
assert_eq!(
- s.eval::("return FriendsFrameFriendButton2NameLocation:GetText()")
+ s.eval::("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::("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::("return FriendsFrameIgnoreButton1Name:GetText()")
+ s.eval::("return FriendsFrameIgnoreButton1ButtonTextName:GetText()")
.unwrap(),
"Spammer"
);
assert_eq!(
- s.eval::("return FriendsFrameIgnoreButton2Name:GetText()")
+ s.eval::("return FriendsFrameIgnoreButton2ButtonTextName:GetText()")
.unwrap(),
"Ninja"
);
@@ -328,9 +298,12 @@ fn the_ignore_list_is_the_other_half_of_tab_one() {
assert!(s
.eval::("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::("return FriendsFrame:IsVisible()").unwrap());
assert!(s
- .eval::("return IgnoreListFrame:IsVisible()")
+ .eval::("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::("return WhoFrameAddFriendButton:IsEnabled() ~= 0")
+ s.eval::("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 ` 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::("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::("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 `` 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 ; 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 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 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);
-}
diff --git a/crates/benilla-app/src/ui_script/game_menu_tests.rs b/crates/benilla-app/src/ui_script/game_menu_tests.rs
index f669714f..c255a0ac 100644
--- a/crates/benilla-app/src/ui_script/game_menu_tests.rs
+++ b/crates/benilla-app/src/ui_script/game_menu_tests.rs
@@ -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::("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::("return StaticPopup1Text:GetText()")
.unwrap(),
- "20 seconds until exit"
+ "20 Seconds until exit"
);
assert_eq!(
s.eval::("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",
diff --git a/crates/benilla-app/src/ui_script/group_loot_tests.rs b/crates/benilla-app/src/ui_script/group_loot_tests.rs
index f7e62cea..85f78504 100644
--- a/crates/benilla-app/src/ui_script/group_loot_tests.rs
+++ b/crates/benilla-app/src/ui_script/group_loot_tests.rs
@@ -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());
diff --git a/crates/benilla-app/src/ui_script/guild_tests.rs b/crates/benilla-app/src/ui_script/guild_tests.rs
index e9f4e52d..ed6c74e4 100644
--- a/crates/benilla-app/src/ui_script/guild_tests.rs
+++ b/crates/benilla-app/src/ui_script/guild_tests.rs
@@ -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::(&format!(
diff --git a/crates/benilla-app/src/ui_script/help_frame_tests.rs b/crates/benilla-app/src/ui_script/help_frame_tests.rs
index 9c591cc2..f44bacb8 100644
--- a/crates/benilla-app/src/ui_script/help_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/help_frame_tests.rs
@@ -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.
diff --git a/crates/benilla-app/src/ui_script/honor_frame_tests.rs b/crates/benilla-app/src/ui_script/honor_frame_tests.rs
index 914cb7ed..897e16f8 100644
--- a/crates/benilla-app/src/ui_script/honor_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/honor_frame_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/inspect_tests.rs b/crates/benilla-app/src/ui_script/inspect_tests.rs
index 67a3ecaa..b66d0e5d 100644
--- a/crates/benilla-app/src/ui_script/inspect_tests.rs
+++ b/crates/benilla-app/src/ui_script/inspect_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/instance_tests.rs b/crates/benilla-app/src/ui_script/instance_tests.rs
index 249ca62c..232bcf41 100644
--- a/crates/benilla-app/src/ui_script/instance_tests.rs
+++ b/crates/benilla-app/src/ui_script/instance_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/keybindings_tests.rs b/crates/benilla-app/src/ui_script/keybindings_tests.rs
index fcf08487..b22a2915 100644
--- a/crates/benilla-app/src/ui_script/keybindings_tests.rs
+++ b/crates/benilla-app/src/ui_script/keybindings_tests.rs
@@ -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::(&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::(&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::(&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::(&format!("return {ROW}2Description:GetText()"))
.unwrap(),
- "MOVEANDSTEER"
+ label(&s, "BINDING_NAME_MOVEANDSTEER", "MOVEANDSTEER")
);
assert_eq!(
s.eval::(&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::(&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::(r#"return GetBindingAction("T")"#).unwrap(),
"MOVEFORWARD"
);
+ let victim = label(&s, "BINDING_NAME_ATTACKTARGET", "ATTACKTARGET");
assert!(
s.eval::("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::("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::(&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::("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::("return OptionsFrameContainerBodyKeybindSearch1Key1ButtonText:GetText()")
diff --git a/crates/benilla-app/src/ui_script/loot_tests.rs b/crates/benilla-app/src/ui_script/loot_tests.rs
index 3ebc088a..ccffe73f 100644
--- a/crates/benilla-app/src/ui_script/loot_tests.rs
+++ b/crates/benilla-app/src/ui_script/loot_tests.rs
@@ -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);
diff --git a/crates/benilla-app/src/ui_script/macro_tests.rs b/crates/benilla-app/src/ui_script/macro_tests.rs
index 24513744..f03b0fbb 100644
--- a/crates/benilla-app/src/ui_script/macro_tests.rs
+++ b/crates/benilla-app/src/ui_script/macro_tests.rs
@@ -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
diff --git a/crates/benilla-app/src/ui_script/manifest.rs b/crates/benilla-app/src/ui_script/manifest.rs
index 698cf121..768c0f9d 100644
--- a/crates/benilla-app/src/ui_script/manifest.rs
+++ b/crates/benilla-app/src/ui_script/manifest.rs
@@ -183,6 +183,26 @@ pub(crate) fn load_default_ui(script: &UiScript) -> Vec {
/// chain document means `Interface\FrameXML\ContainerFrame.xml`'s `` reaches `Interface\FrameXML\ContainerFrame.lua` without anything
/// here knowing about it.
+/// The Blizzard addons the manifest lists off the chain (`Interface\AddOns\\…` 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 {
+ let mut out: Vec = 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 {
let builtin = Addon::builtin();
let reference = reference_ui::addon(
diff --git a/crates/benilla-app/src/ui_script/merchant_tests.rs b/crates/benilla-app/src/ui_script/merchant_tests.rs
index ea4c8f4d..2bb555d1 100644
--- a/crates/benilla-app/src/ui_script/merchant_tests.rs
+++ b/crates/benilla-app/src/ui_script/merchant_tests.rs
@@ -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);
diff --git a/crates/benilla-app/src/ui_script/mirror_timer_tests.rs b/crates/benilla-app/src/ui_script/mirror_timer_tests.rs
index d90c8ac3..290ff052 100644
--- a/crates/benilla-app/src/ui_script/mirror_timer_tests.rs
+++ b/crates/benilla-app/src/ui_script/mirror_timer_tests.rs
@@ -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
}
diff --git a/crates/benilla-app/src/ui_script/multibar_stance_tests.rs b/crates/benilla-app/src/ui_script/multibar_stance_tests.rs
index 623928b5..696d6b68 100644
--- a/crates/benilla-app/src/ui_script/multibar_stance_tests.rs
+++ b/crates/benilla-app/src/ui_script/multibar_stance_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs
index e04b2c93..1aaab210 100644
--- a/crates/benilla-app/src/ui_script/options_tests.rs
+++ b/crates/benilla-app/src/ui_script/options_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/panel_template_tests.rs b/crates/benilla-app/src/ui_script/panel_template_tests.rs
index 8eaa4c13..aeb85c8e 100644
--- a/crates/benilla-app/src/ui_script/panel_template_tests.rs
+++ b/crates/benilla-app/src/ui_script/panel_template_tests.rs
@@ -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",
] {
diff --git a/crates/benilla-app/src/ui_script/panel_tests.rs b/crates/benilla-app/src/ui_script/panel_tests.rs
index dee9206f..42d6e984 100644
--- a/crates/benilla-app/src/ui_script/panel_tests.rs
+++ b/crates/benilla-app/src/ui_script/panel_tests.rs
@@ -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
diff --git a/crates/benilla-app/src/ui_script/pet_bar_tests.rs b/crates/benilla-app/src/ui_script/pet_bar_tests.rs
index 022a2c5a..edd57436 100644
--- a/crates/benilla-app/src/ui_script/pet_bar_tests.rs
+++ b/crates/benilla-app/src/ui_script/pet_bar_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/pet_frame_tests.rs b/crates/benilla-app/src/ui_script/pet_frame_tests.rs
index 90ad9276..b8fc6dd5 100644
--- a/crates/benilla-app/src/ui_script/pet_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/pet_frame_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/petition_tests.rs b/crates/benilla-app/src/ui_script/petition_tests.rs
index a479bcff..c52c6924 100644
--- a/crates/benilla-app/src/ui_script/petition_tests.rs
+++ b/crates/benilla-app/src/ui_script/petition_tests.rs
@@ -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 `` of QuestFrame.xml, and
// ours declares it in QuestFrame.xml directly. An unknown template is a loader WARNING, so
diff --git a/crates/benilla-app/src/ui_script/quest_share_tests.rs b/crates/benilla-app/src/ui_script/quest_share_tests.rs
index dfda8093..c249b190 100644
--- a/crates/benilla-app/src/ui_script/quest_share_tests.rs
+++ b/crates/benilla-app/src/ui_script/quest_share_tests.rs
@@ -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
}
diff --git a/crates/benilla-app/src/ui_script/quest_tests.rs b/crates/benilla-app/src/ui_script/quest_tests.rs
index 79e2be04..85d430a1 100644
--- a/crates/benilla-app/src/ui_script/quest_tests.rs
+++ b/crates/benilla-app/src/ui_script/quest_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/quest_timer_tests.rs b/crates/benilla-app/src/ui_script/quest_timer_tests.rs
index 6f18c5d7..41ee308e 100644
--- a/crates/benilla-app/src/ui_script/quest_timer_tests.rs
+++ b/crates/benilla-app/src/ui_script/quest_timer_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/questlog_tests.rs b/crates/benilla-app/src/ui_script/questlog_tests.rs
index 9d7c2f58..f65d9f4c 100644
--- a/crates/benilla-app/src/ui_script/questlog_tests.rs
+++ b/crates/benilla-app/src/ui_script/questlog_tests.rs
@@ -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 —
diff --git a/crates/benilla-app/src/ui_script/raid_tests.rs b/crates/benilla-app/src/ui_script/raid_tests.rs
index 2e2e3882..7c79e2d4 100644
--- a/crates/benilla-app/src/ui_script/raid_tests.rs
+++ b/crates/benilla-app/src/ui_script/raid_tests.rs
@@ -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
}
diff --git a/crates/benilla-app/src/ui_script/reference_ui.rs b/crates/benilla-app/src/ui_script/reference_ui.rs
index 00258458..c6318f36 100644
--- a/crates/benilla-app/src/ui_script/reference_ui.rs
+++ b/crates/benilla-app/src/ui_script/reference_ui.rs
@@ -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 = 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 = text.chars().collect();
let mut called: std::collections::HashSet = 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();
diff --git a/crates/benilla-app/src/ui_script/screenshot_tests.rs b/crates/benilla-app/src/ui_script/screenshot_tests.rs
index 45185088..08556f68 100644
--- a/crates/benilla-app/src/ui_script/screenshot_tests.rs
+++ b/crates/benilla-app/src/ui_script/screenshot_tests.rs
@@ -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",
] {
diff --git a/crates/benilla-app/src/ui_script/spellbook_tests.rs b/crates/benilla-app/src/ui_script/spellbook_tests.rs
index 00eb1f7e..c35d19b3 100644
--- a/crates/benilla-app/src/ui_script/spellbook_tests.rs
+++ b/crates/benilla-app/src/ui_script/spellbook_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/static_popup_tests.rs b/crates/benilla-app/src/ui_script/static_popup_tests.rs
index 4db527dd..448dd91e 100644
--- a/crates/benilla-app/src/ui_script/static_popup_tests.rs
+++ b/crates/benilla-app/src/ui_script/static_popup_tests.rs
@@ -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::("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::("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::("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());
diff --git a/crates/benilla-app/src/ui_script/summon_tests.rs b/crates/benilla-app/src/ui_script/summon_tests.rs
index 273a54bd..a2d766cf 100644
--- a/crates/benilla-app/src/ui_script/summon_tests.rs
+++ b/crates/benilla-app/src/ui_script/summon_tests.rs
@@ -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::("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::("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::("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::("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::("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"
);
}
diff --git a/crates/benilla-app/src/ui_script/talent_wipe_tests.rs b/crates/benilla-app/src/ui_script/talent_wipe_tests.rs
index 58f77b78..e8c4c9c7 100644
--- a/crates/benilla-app/src/ui_script/talent_wipe_tests.rs
+++ b/crates/benilla-app/src/ui_script/talent_wipe_tests.rs
@@ -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
}
diff --git a/crates/benilla-app/src/ui_script/target_aura_tests.rs b/crates/benilla-app/src/ui_script/target_aura_tests.rs
index b8ea088e..1a67a702 100644
--- a/crates/benilla-app/src/ui_script/target_aura_tests.rs
+++ b/crates/benilla-app/src/ui_script/target_aura_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/taxi_tests.rs b/crates/benilla-app/src/ui_script/taxi_tests.rs
index eb5debf1..9b0c77bd 100644
--- a/crates/benilla-app/src/ui_script/taxi_tests.rs
+++ b/crates/benilla-app/src/ui_script/taxi_tests.rs
@@ -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:
diff --git a/crates/benilla-app/src/ui_script/test_ui.rs b/crates/benilla-app/src/ui_script/test_ui.rs
index 9775cec5..2df0c4cf 100644
--- a/crates/benilla-app/src/ui_script/test_ui.rs
+++ b/crates/benilla-app/src/ui_script/test_ui.rs
@@ -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` 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
diff --git a/crates/benilla-app/src/ui_script/tooltip_anchor_tests.rs b/crates/benilla-app/src/ui_script/tooltip_anchor_tests.rs
index 6714c50c..b4c6fbb2 100644
--- a/crates/benilla-app/src/ui_script/tooltip_anchor_tests.rs
+++ b/crates/benilla-app/src/ui_script/tooltip_anchor_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/tot_frame_tests.rs b/crates/benilla-app/src/ui_script/tot_frame_tests.rs
index 91da0303..d91f091f 100644
--- a/crates/benilla-app/src/ui_script/tot_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/tot_frame_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_script/trainer_tests.rs b/crates/benilla-app/src/ui_script/trainer_tests.rs
index 9c9558e2..9c557b8c 100644
--- a/crates/benilla-app/src/ui_script/trainer_tests.rs
+++ b/crates/benilla-app/src/ui_script/trainer_tests.rs
@@ -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 `` (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::("return BenillaTrainerFrame:IsVisible()")
+ .eval::("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::("return BenillaTrainerFrame:IsVisible()")
+ .eval::("return ClassTrainerFrame:IsVisible()")
.unwrap());
assert_eq!(
- s.eval::("return BenillaTrainerTitleText:GetText()")
+ s.eval::("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::("return BenillaTrainerService1Text:GetText()")
+ s.eval::("return ClassTrainerSkill1Text:GetText()")
.unwrap(),
"Arms"
);
assert_eq!(
- s.eval::("return BenillaTrainerService2Text:GetText()")
+ s.eval::("return ClassTrainerSkill2Text:GetText()")
.unwrap(),
" Heroic Strike"
);
@@ -233,12 +289,12 @@ fn shipped_trainer_frame_drives_end_to_end() {
2
);
assert_eq!(
- s.eval::("return BenillaTrainerCostLabel:GetText()")
+ s.eval::("return ClassTrainerCostLabel:GetText()")
.unwrap(),
"Cost:"
);
assert!(s
- .eval::("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
+ .eval::("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::("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
+ .eval::("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::("return BenillaTrainerSkillRequirements:GetText()")
+ .eval::("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::("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
+ .eval::("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::("return BenillaTrainerFrame:IsVisible()")
+ .eval::("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::("return GetNumTrainerServices()").unwrap(), 6);
assert_eq!(
- s.eval::("return BenillaTrainerService2Text:GetText()")
+ s.eval::("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::("return GetNumTrainerServices()").unwrap(), 4);
assert_eq!(
- s.eval::("return BenillaTrainerService2Text:GetText()")
+ s.eval::("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::("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::(row1).unwrap(), "Arms");
assert_eq!(
- s.eval::("return BenillaTrainerService2Text:GetText()")
+ s.eval::("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::("return BenillaTrainerListScrollFrame.offset or -1")
+ .eval::("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::("return BenillaTrainerListScrollFrame.offset or -1")
+ s.eval::("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::("return BenillaTrainerListScrollFrame.offset or -1")
+ s.eval::("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::("return BenillaTrainerService1:GetFontString() ~= nil")
+ s.eval::("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::("return BenillaTrainerListScrollFrame.offset or -1")
+ s.eval::("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 `` 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::("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::("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 = 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::("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::("return GetTrainerServiceTypeFilter('unavailable') == 1")
@@ -732,14 +816,10 @@ fn the_state_filter_survives_a_restart_through_the_saved_variables_file() {
);
assert_eq!(
fresh.eval::("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::(&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::("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::("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());
}
diff --git a/crates/benilla-app/src/ui_script/unit_frame_tests.rs b/crates/benilla-app/src/ui_script/unit_frame_tests.rs
index 08e086ae..c1c428b9 100644
--- a/crates/benilla-app/src/ui_script/unit_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/unit_frame_tests.rs
@@ -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.
diff --git a/crates/benilla-app/src/ui_script/unit_popup_tests.rs b/crates/benilla-app/src/ui_script/unit_popup_tests.rs
index b407e88b..896e0102 100644
--- a/crates/benilla-app/src/ui_script/unit_popup_tests.rs
+++ b/crates/benilla-app/src/ui_script/unit_popup_tests.rs
@@ -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::("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::("return DropDownList1.numButtons").unwrap(),
- 4,
- "title + Whisper + Invite + Cancel"
+ 5,
+ "title + Whisper + Invite + Target + Cancel"
);
assert_eq!(
s.eval::("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::("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::("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::("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::("return DropDownList1:IsVisible()").unwrap(),
- "the menu opened"
- );
- assert!(
- s.eval::("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::("return BENILLA_TEST_POPUP_TICKS").unwrap() > 0,
- "an open menu ticks the enable pass"
- );
- assert!(
- !s.eval::("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::("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::("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}"));
diff --git a/crates/benilla-app/src/ui_script/world_map_tests.rs b/crates/benilla-app/src/ui_script/world_map_tests.rs
index 46e35047..73022bf7 100644
--- a/crates/benilla-app/src/ui_script/world_map_tests.rs
+++ b/crates/benilla-app/src/ui_script/world_map_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/world_state_tests.rs b/crates/benilla-app/src/ui_script/world_state_tests.rs
index 3329d28a..c6ffddc5 100644
--- a/crates/benilla-app/src/ui_script/world_state_tests.rs
+++ b/crates/benilla-app/src/ui_script/world_state_tests.rs
@@ -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",
diff --git a/crates/benilla-app/src/ui_script/zone_text_tests.rs b/crates/benilla-app/src/ui_script/zone_text_tests.rs
index ae3f82dd..969f9ea8 100644
--- a/crates/benilla-app/src/ui_script/zone_text_tests.rs
+++ b/crates/benilla-app/src/ui_script/zone_text_tests.rs
@@ -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");
diff --git a/crates/benilla-app/src/ui_social/feed.rs b/crates/benilla-app/src/ui_social/feed.rs
index fc09b9b4..813ff0b7 100644
--- a/crates/benilla-app/src/ui_social/feed.rs
+++ b/crates/benilla-app/src/ui_social/feed.rs
@@ -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) => {
diff --git a/crates/benilla-app/src/ui_social/mod.rs b/crates/benilla-app/src/ui_social/mod.rs
index e0140f3f..ebdd9596 100644
--- a/crates/benilla-app/src/ui_social/mod.rs
+++ b/crates/benilla-app/src/ui_social/mod.rs
@@ -111,6 +111,9 @@ pub(crate) struct SocialState {
pending_lines: Vec,
/// 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,
}
diff --git a/crates/benilla-app/src/ui_unit.rs b/crates/benilla-app/src/ui_unit.rs
index 5c245a26..66ac069a 100644
--- a/crates/benilla-app/src/ui_unit.rs
+++ b/crates/benilla-app/src/ui_unit.rs
@@ -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, cur: UnitState| -> Vec {
@@ -2024,10 +2036,7 @@ mod tests {
fired(Some(base.clone()), base.clone()),
Vec::::new()
);
- assert_eq!(
- fired(None, base.clone()),
- vec!["UNIT_FLAGS:pet".to_string()]
- );
+ assert_eq!(fired(None, base.clone()), Vec::::new());
}
/// The two edges on the player's own state (1953): the control flag's, which fires LOST on
diff --git a/crates/benilla-app/tests/auction_frame.rs b/crates/benilla-app/tests/auction_frame.rs
index 27660ec5..ed8aacb8 100644
--- a/crates/benilla-app/tests/auction_frame.rs
+++ b/crates/benilla-app/tests/auction_frame.rs
@@ -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",
];
diff --git a/crates/benilla-app/tests/character_stats.rs b/crates/benilla-app/tests/character_stats.rs
index 652224c1..0f93d995 100644
--- a/crates/benilla-app/tests/character_stats.rs
+++ b/crates/benilla-app/tests/character_stats.rs
@@ -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_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
diff --git a/crates/benilla-app/tests/create_frame_template.rs b/crates/benilla-app/tests/create_frame_template.rs
index 7c0b0ddb..3f231b3c 100644
--- a/crates/benilla-app/tests/create_frame_template.rs
+++ b/crates/benilla-app/tests/create_frame_template.rs
@@ -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
// ``, 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",
];
diff --git a/crates/benilla-app/tests/item_text_frame.rs b/crates/benilla-app/tests/item_text_frame.rs
index f1f2c012..3044fd49 100644
--- a/crates/benilla-app/tests/item_text_frame.rs
+++ b/crates/benilla-app/tests/item_text_frame.rs
@@ -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",
];
diff --git a/crates/benilla-app/tests/mail_frame.rs b/crates/benilla-app/tests/mail_frame.rs
index a07b2a3b..6956fc5c 100644
--- a/crates/benilla-app/tests/mail_frame.rs
+++ b/crates/benilla-app/tests/mail_frame.rs
@@ -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",
];
diff --git a/crates/benilla-app/tests/talent_frame.rs b/crates/benilla-app/tests/talent_frame.rs
index 5f91a9e8..e3e96b26 100644
--- a/crates/benilla-app/tests/talent_frame.rs
+++ b/crates/benilla-app/tests/talent_frame.rs
@@ -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
diff --git a/crates/benilla-app/tests/trade_frame.rs b/crates/benilla-app/tests/trade_frame.rs
index 6a24e36b..92550c90 100644
--- a/crates/benilla-app/tests/trade_frame.rs
+++ b/crates/benilla-app/tests/trade_frame.rs
@@ -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",
];
diff --git a/crates/benilla-app/tests/tradeskill_frame.rs b/crates/benilla-app/tests/tradeskill_frame.rs
index 9fda1b86..eafa624b 100644
--- a/crates/benilla-app/tests/tradeskill_frame.rs
+++ b/crates/benilla-app/tests/tradeskill_frame.rs
@@ -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",
diff --git a/crates/benilla-app/tests/world_api_wall.rs b/crates/benilla-app/tests/world_api_wall.rs
index c6f426e4..e9aaea8f 100644
--- a/crates/benilla-app/tests/world_api_wall.rs
+++ b/crates/benilla-app/tests/world_api_wall.rs
@@ -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
diff --git a/crates/benilla-ui/src/loader/geometry.rs b/crates/benilla-ui/src/loader/geometry.rs
index b25e4d8c..33526bc9 100644
--- a/crates/benilla-ui/src/loader/geometry.rs
+++ b/crates/benilla-ui/src/loader/geometry.rs
@@ -162,18 +162,28 @@ impl Loader<'_> {
.next()
.map(abs_dim)
.unwrap_or((None, None));
- self.call(
- wrapper,
- "SetPoint",
- (
- point.to_string(),
- rel_to,
- rel_point,
- x.unwrap_or(0.0),
- y.unwrap_or(0.0),
- ),
- dbg,
+ let args = (
+ point.to_string(),
+ rel_to,
+ rel_point,
+ x.unwrap_or(0.0),
+ y.unwrap_or(0.0),
);
+ // A target that is not built yet waits for the frame's subtree (`Loader::deferred_anchors`).
+ if args
+ .1
+ .as_deref()
+ .is_some_and(|n| !self.anchor_target_exists(n))
+ {
+ self.deferred_anchors.push(super::DeferredAnchor {
+ wrapper: wrapper.clone(),
+ region: false,
+ args,
+ dbg: dbg.to_string(),
+ });
+ continue;
+ }
+ self.call(wrapper, "SetPoint", args, dbg);
}
}
}
diff --git a/crates/benilla-ui/src/loader/mod.rs b/crates/benilla-ui/src/loader/mod.rs
index e97c3d01..f58e3ae6 100644
--- a/crates/benilla-ui/src/loader/mod.rs
+++ b/crates/benilla-ui/src/loader/mod.rs
@@ -139,6 +139,7 @@ pub fn load_into(
path: path.to_string(),
report: LoadReport::default(),
warned: HashSet::new(),
+ deferred_anchors: Vec::new(),
};
// Fold the document layer's own parse warnings in (decision 0068: reuse framexml's warnings).
loader.report.warnings.extend(doc.warnings.iter().cloned());
@@ -192,6 +193,7 @@ pub fn apply_template(lua: &mlua::Lua, wrapper: &Table, kind: &str, template: &s
path: String::new(),
report: LoadReport::default(),
warned: HashSet::new(),
+ deferred_anchors: Vec::new(),
};
let (own_name, parent_name) = loader.frame_names(wrapper);
@@ -368,6 +370,24 @@ struct Loader<'a> {
pub(super) report: LoadReport,
/// Warn-once keys (so a document with 200 `OnClick` handlers doesn't emit 200 identical warnings).
pub(super) warned: HashSet,
+ /// Anchors whose named `relativeTo` did not resolve when its element was read, applied once
+ /// the enclosing frame's subtree exists ([`Loader::drain_deferred_anchors`]). The real
+ /// client is order-free here because it resolves anchors at layout, after every child of
+ /// the frame is built; this loader resolves a name at `SetPoint` time, so a ``
+ /// hung off a `` declared after it (the stock trainer row) — or a state
+ /// texture hung off the label declared after IT (the sort-header arrow) — needs the same
+ /// grace. Each entry belongs to the `decorate` that pushed it and drains there, so a nested
+ /// frame's anchors never wait for its parent (1957).
+ pub(super) deferred_anchors: Vec,
+}
+
+/// One `SetPoint` the loader holds until its target can exist — see `Loader::deferred_anchors`.
+pub(super) struct DeferredAnchor {
+ pub(super) wrapper: Table,
+ /// A region wrapper (`call_region`) rather than a frame's (`call`).
+ pub(super) region: bool,
+ pub(super) args: (String, Option, String, f32, f32),
+ pub(super) dbg: String,
}
impl Loader<'_> {
@@ -813,6 +833,9 @@ impl Loader<'_> {
parent_name: &str,
dbg_name: &str,
) {
+ // Everything this frame defers drains before its OnLoad (step 8 below); a nested frame's
+ // own deferrals drain inside ITS decorate, so the mark is this frame's alone.
+ let deferred_mark = self.deferred_anchors.len();
// 2 · LoadXML attributes (rf24 `0x769820`).
self.apply_attrs(el, wrapper, dbg_name);
// 3 · and 4 · (the CLayoutFrame geometry base, rf24 `0x767800`).
@@ -883,6 +906,10 @@ impl Loader<'_> {
}
}
+ // 7c · the anchors this frame's own elements could not resolve while their targets were
+ // still unbuilt — every child exists now, which is when the client's own layout
+ // pass would have read them.
+ self.drain_deferred_anchors(deferred_mark);
// 8 · this frame's OnLoad, now that its subtree is complete (bottom-up).
if let Some(func) = onload {
self.fire_onload(wrapper, &func, dbg_name);
@@ -897,6 +924,25 @@ impl Loader<'_> {
/// Returns the frame's own name (`None` if it is anonymous) and the name of its nearest
/// **named** ancestor — rf27 rule 3's walk, with [`framexml::DEFAULT_PARENT_NAME`] when there
/// is none.
+ /// Can a `relativeTo` name resolve right now — a frame in the arena or a named region?
+ pub(super) fn anchor_target_exists(&self, name: &str) -> bool {
+ let model = self.model();
+ model.arena.lookup(name).is_some() || model.region_names.contains_key(name)
+ }
+
+ /// Apply the anchors deferred since `mark` (the enclosing `decorate`'s entry), in order. A
+ /// target still missing now is a real miss and warns through the resolver like any other.
+ pub(super) fn drain_deferred_anchors(&mut self, mark: usize) {
+ let pending: Vec = self.deferred_anchors.drain(mark..).collect();
+ for d in pending {
+ if d.region {
+ self.call_region(&d.wrapper, "SetPoint", d.args, &d.dbg);
+ } else {
+ self.call(&d.wrapper, "SetPoint", d.args, &d.dbg);
+ }
+ }
+ }
+
fn frame_names(&self, wrapper: &Table) -> (Option, String) {
let own = wrapper
.call_method::