The core loads the stock GameMenuFrame.xml, and the layer reshapes it in place into benilla's menu, keeping every stock rung in the chain anchored TOP (#564)

The core now loads the stock `GameMenuFrame.xml` at its `FrameXML.toc` row, right after `UIPanelTemplates.xml`, and benilla's ESC menu look moves into the layer as `GameMenuAdapters.xml`, which reshapes the stock frame in place: a later file cannot take an occupied frame name (`0x701bd0`).

- Every stock rung stays in the chain with its name and a `TOP` anchor. The three the menu does not show are hidden and laid over the rung above, since a zero height leaves a rung's bottom edge unresolved (`combineEdge 0x767440`). So an addon that inserts under Interface Options and re-anchors Key Bindings (ShaguTweaks), or under Macros and re-anchors Logout (MCP, MoveAnything), lays out as on 1.12.1, with no error.
- The era rungs keep their look and behaviour: Options opens benilla's window; the `BenillaOptionsFrame`-aware wrappers of `ToggleGameMenu`, `IsOptionFrameOpen` and `UpdateMicroButtons` move into the layer; the non-stock names take the `Benilla` prefix. The core toc now names no file of ours.
- With the layer off (`WOW_STOCK_UI=1`), the menu is the stock menu and every rung opens its stock window.

Verified: a region dump of the open menu on main and here differs only in our own frames' names, their insertion order within one level (the rects do not overlap) and sub-1e-4 px float noise; new tests for ShaguTweaks' insert, MCP from the corpus, a pfUI-style skin and the stock-only menu, each failing on main; `scripts/gates.sh` and `scripts/smoke.sh` green; a live run opened the menu, opened Options and returned to the game, and MoveAnything's own insert landed under Macros with Logout below it. Built on macOS; no platform seam touched.

Fixes #553
Fixes #540

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-28 22:01:24 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 22634279bd
commit 6e869eea4e
17 changed files with 633 additions and 297 deletions
@@ -0,0 +1,132 @@
<Ui>
<!--
The ESC menu: the stock GameMenuFrame.xml, reshaped in place as a 1.12 addon reshapes it.
Deviation: the rungs are the 1.15 era client's ESC menu, because its settings screens
replace 1.12's, which are much worse to use; the era's AddOns rung is dropped, since the
character-select AddOns screen is the only addon UI.
-->
<!-- The era's header font, which 1.12's Fonts.xml lacks. -->
<Font name="BenillaGameFontNormalMed1" inherits="GameFontNormal" virtual="true">
<FontHeight><AbsValue val="13"/></FontHeight>
</Font>
<Script><![CDATA[
-- ══ The frame: the era layout, 28+144+28 wide, 32 top, 28 bottom, 7 rungs of 21 and
-- three gaps of 20 ═══════════════════════════════════════════════════════════════════════
GameMenuFrame:SetWidth(200)
GameMenuFrame:SetHeight(267)
-- The stock title (`GameMenuFrame.xml:36`) has no name: found as addons find it.
for _, region in ipairs({ GameMenuFrame:GetRegions() }) do
if region:GetObjectType() == "FontString" and region:GetText() == MAIN_MENU then
region:SetFontObject(BenillaGameFontNormalMed1)
end
end
-- ERA_WINDOW_SCALE (OptionsFrame.xml) stands in for the era's UIParent scale; read at
-- every show, then the stock OnShow.
local Ref_GameMenuFrame_OnShow = GameMenuFrame:GetScript("OnShow")
GameMenuFrame:SetScript("OnShow", function()
this:SetScale(ERA_WINDOW_SCALE or 1)
Ref_GameMenuFrame_OnShow()
end)
-- ══ The rungs ═══════════════════════════════════════════════════════════════════════════
-- Every stock rung stays, anchored TOP to a BOTTOM as in the stock file, so an addon's
-- `SetPoint("TOP", …)` replaces its one anchor. The three the era menu lacks are hidden
-- in place, with Sound and UI Options each laid over the rung above it, so they take no
-- space: a width or height of 0 is no size in 1.12's layout, not a flat rect.
-- Literal: 1.12 has no GAMEMENU_OPTIONS; its OPTIONS_MENU is "Options Menu".
GameMenuButtonOptions:SetText("Options")
GameMenuButtonOptions:ClearAllPoints()
GameMenuButtonOptions:SetPoint("TOP", GameMenuFrame, "TOP", 0, -32)
GameMenuButtonOptions:SetScript("OnClick", function()
PlaySound("igMainMenuOption")
HideUIPanel(GameMenuFrame)
ShowUIPanel(BenillaOptionsFrame)
end)
for _, fold in ipairs({
{ GameMenuButtonSoundOptions, GameMenuButtonOptions },
{ GameMenuButtonUIOptions, GameMenuButtonSoundOptions },
}) do
local button, above = fold[1], fold[2]
button:Hide()
button:ClearAllPoints()
button:SetPoint("TOP", above, "BOTTOM", 0, button:GetHeight())
end
-- Key Bindings keeps its stock anchor, 1 under UI Options: the era rungs hang from its
-- top, so an addon that inserts a button above it and re-anchors it under that button
-- (ShaguTweaks) moves them down as it moves the stock rungs.
GameMenuButtonKeybindings:Hide()
-- The era's Edit Mode and Support, with nothing behind them yet: disabled.
local editMode = CreateFrame("Button", "BenillaGameMenuButtonEditMode", GameMenuFrame,
"GameMenuButtonTemplate")
editMode:SetPoint("TOP", GameMenuButtonKeybindings, "TOP", 0, -19)
editMode:SetText("Edit Mode")
editMode:Disable()
local support = CreateFrame("Button", "BenillaGameMenuButtonSupport", GameMenuFrame,
"GameMenuButtonTemplate")
support:SetPoint("TOP", editMode, "BOTTOM", 0, 0)
support:SetText("Support")
support:Disable()
GameMenuButtonMacros:SetPoint("TOP", support, "BOTTOM", 0, 0)
GameMenuButtonLogout:SetPoint("TOP", GameMenuButtonMacros, "BOTTOM", 0, -20)
GameMenuButtonQuit:SetPoint("TOP", GameMenuButtonLogout, "BOTTOM", 0, 0)
GameMenuButtonContinue:SetPoint("TOP", GameMenuButtonQuit, "BOTTOM", 0, -20)
-- ══ Our options window in the stock "is an options window open?" checks ═══════════════
-- Each wraps the stock function to add `BenillaOptionsFrame`.
local Ref_IsOptionFrameOpen = IsOptionFrameOpen
local Ref_ToggleGameMenu = ToggleGameMenu
local Ref_UpdateMicroButtons = UpdateMicroButtons
-- Guarded for a test kit that loads the game menu without the options window.
local function BenillaOptions_IsOpen()
return BenillaOptionsFrame and BenillaOptionsFrame:IsVisible()
end
-- `UIParent.lua:996`: a yes keeps the ESC sweep off the centre panel, which our window is.
function IsOptionFrameOpen()
if BenillaOptions_IsOpen() then
return 1
end
return Ref_IsOptionFrameOpen()
end
-- `UIParent.lua:1465-1497`: static popup, then an open options window, then the stock
-- ladder. Our Close click is the discard-and-hide of the reference's `OptionsFrameCancel`.
function ToggleGameMenu(clicked)
if clicked then
-- As the reference's `clicked` arm: close an options window, then toggle.
if BenillaOptions_IsOpen() then
BenillaOptionsFrameCloseButton:Click()
end
return Ref_ToggleGameMenu(clicked)
end
if StaticPopup_EscapePressed() then
return
end
if BenillaOptions_IsOpen() then
BenillaOptionsFrameCloseButton:Click()
return
end
return Ref_ToggleGameMenu(clicked)
end
-- `MainMenuBarMicroButtons.lua:47-58`: only ever pushes the button, so the stock decision
-- stands and ours adds our window. Guarded for a test kit that loads no micro buttons.
function UpdateMicroButtons()
if Ref_UpdateMicroButtons then
Ref_UpdateMicroButtons()
end
if MainMenuMicroButton and BenillaOptions_IsOpen() then
MainMenuMicroButton:SetButtonState("PUSHED", 1)
end
end
]]></Script>
</Ui>
@@ -1,216 +0,0 @@
<Ui>
<!--
The game menu ESC opens, in the chrome of the reference's GameMenuFrame.xml.
Deviation: the rungs are the 1.15 era client's ESC menu, because its settings screens
replace 1.12's, which are much worse to use; the era's AddOns rung is dropped, since the
character-select AddOns screen is the only addon UI. Addons address `GameMenuFrame` by name.
-->
<Script><![CDATA[
-- Label strings as `X = X or "..."`, so a stock GlobalStrings value wins where one exists.
MAIN_MENU = MAIN_MENU or "Main Menu"
HUD_EDIT_MODE_MENU = HUD_EDIT_MODE_MENU or "Edit Mode"
GAMEMENU_SUPPORT = GAMEMENU_SUPPORT or "Support"
MACROS = MACROS or "Macros"
LOGOUT = LOGOUT or "Logout"
EXIT_GAME = EXIT_GAME or "Exit Game"
RETURN_TO_GAME = RETURN_TO_GAME or "Return to Game"
CANCEL = CANCEL or "Cancel"
-- A rung with nothing behind it yet (Edit Mode, Support), disabled.
function GameMenuButton_Pending(button, text)
button:SetText(text)
button:Disable()
end
-- ══ Our options window in the stock "is an options window open?" checks ══
-- Each wraps the stock body to add `BenillaOptionsFrame`; here because this file loads
-- last, after MainMenuBarMicroButtons.xml.
local Ref_IsOptionFrameOpen = IsOptionFrameOpen
local Ref_ToggleGameMenu = ToggleGameMenu
local Ref_UpdateMicroButtons = UpdateMicroButtons
-- Guarded for a test kit that loads the game menu without the options window.
local function BenillaOptions_IsOpen()
return BenillaOptionsFrame and BenillaOptionsFrame:IsVisible()
end
-- `UIParent.lua:996`: a yes keeps the ESC sweep off the centre panel, which our window is.
function IsOptionFrameOpen()
if BenillaOptions_IsOpen() then
return 1
end
return Ref_IsOptionFrameOpen()
end
-- `UIParent.lua:1465-1497`: static popup, then an open options window, then the stock
-- ladder. Our Close click is the discard-and-hide of the reference's `OptionsFrameCancel`.
function ToggleGameMenu(clicked)
if clicked then
-- As the reference's `clicked` arm: close an options window, then toggle.
if BenillaOptions_IsOpen() then
BenillaOptionsFrameCloseButton:Click()
end
return Ref_ToggleGameMenu(clicked)
end
if StaticPopup_EscapePressed() then
return
end
if BenillaOptions_IsOpen() then
BenillaOptionsFrameCloseButton:Click()
return
end
return Ref_ToggleGameMenu(clicked)
end
-- `MainMenuBarMicroButtons.lua:47-58`: only ever pushes the button, so the stock decision
-- stands and ours adds our window.
function UpdateMicroButtons()
if Ref_UpdateMicroButtons then
Ref_UpdateMicroButtons()
end
if MainMenuMicroButton and BenillaOptions_IsOpen() then
MainMenuMicroButton:SetButtonState("PUSHED", 1)
end
end
]]></Script>
<!-- The era's header font, which 1.12's Fonts.xml lacks. -->
<Font name="GameFontNormalMed1" inherits="GameFontNormal" virtual="true">
<FontHeight><AbsValue val="13"/></FontHeight>
</Font>
<!-- GameMenuFrame.xml:4's attributes but `movable`, inert there: it has no drag scripts. -->
<Frame name="GameMenuFrame" parent="UIParent" frameStrata="DIALOG" toplevel="true" enableMouse="true" hidden="true">
<!-- The era layout: 28+144+28 wide, 32 top, 28 bottom, 7 rungs of 21, 3 gaps of 20. -->
<Size><AbsDimension x="200" y="267"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<Texture name="GameMenuFrameHeader" file="Interface\DialogFrame\UI-DialogBox-Header">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="12"/></Offset></Anchor></Anchors>
</Texture>
<!-- The era's 13px font on the 1.12 seat, within half a unit of the era's. -->
<FontString name="GameMenuFrameTitle" inherits="GameFontNormalMed1" text="MAIN_MENU">
<Anchors>
<Anchor point="TOP" relativeTo="GameMenuFrameHeader"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Literal: 1.12 has no GAMEMENU_OPTIONS; its OPTIONS_MENU is "Options Menu". -->
<Button name="GameMenuButtonOptions" inherits="GameMenuButtonTemplate" text="Options">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="28" y="-32"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
PlaySound("igMainMenuOption")
HideUIPanel(GameMenuFrame)
ShowUIPanel(BenillaOptionsFrame)
</OnClick>
</Scripts>
</Button>
<Button name="GameMenuButtonEditMode" inherits="GameMenuButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonOptions" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>GameMenuButton_Pending(self, HUD_EDIT_MODE_MENU)</OnLoad></Scripts>
</Button>
<Button name="GameMenuButtonSupport" inherits="GameMenuButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonEditMode" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>GameMenuButton_Pending(self, GAMEMENU_SUPPORT)</OnLoad></Scripts>
</Button>
<Button name="GameMenuButtonMacros" inherits="GameMenuButtonTemplate" text="MACROS">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonSupport" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<!-- GameMenuFrame.xml:118-122. -->
<Scripts>
<OnClick>
PlaySound("igMainMenuOption")
HideUIPanel(getglobal("GameMenuFrame"))
ShowMacroFrame()
</OnClick>
</Scripts>
</Button>
<!-- As the reference: disabled while a CAMP or QUIT countdown is up. -->
<Button name="GameMenuButtonLogout" inherits="GameMenuButtonTemplate" text="LOGOUT">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonMacros" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnShow>
if not StaticPopup_Visible("CAMP") and not StaticPopup_Visible("QUIT") then
self:Enable()
else
self:Disable()
end
</OnShow>
<OnClick>
PlaySound("igMainMenuLogout")
Logout()
HideUIPanel(GameMenuFrame)
</OnClick>
</Scripts>
</Button>
<Button name="GameMenuButtonQuit" inherits="GameMenuButtonTemplate" text="EXIT_GAME">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonLogout" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnShow>
if not StaticPopup_Visible("CAMP") and not StaticPopup_Visible("QUIT") then
self:Enable()
else
self:Disable()
end
</OnShow>
<OnClick>
PlaySound("igMainMenuQuit")
Quit()
HideUIPanel(GameMenuFrame)
</OnClick>
</Scripts>
</Button>
<Button name="GameMenuButtonContinue" inherits="GameMenuButtonTemplate" text="RETURN_TO_GAME">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonQuit" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
PlaySound("igMainMenuContinue")
HideUIPanel(GameMenuFrame)
</OnClick>
</Scripts>
</Button>
</Frames>
<!-- The reference's OnShow/OnHide, guarded for a test that loads no action bar. -->
<Scripts>
<OnShow>
-- ERA_WINDOW_SCALE (OptionsFrame.xml) stands in for the era's UIParent scale. A
-- second OnShow on this frame would replace this one.
this:SetScale(ERA_WINDOW_SCALE or 1)
if UpdateMicroButtons then UpdateMicroButtons() end
if Disable_BagButtons then Disable_BagButtons() end
</OnShow>
<OnHide>
if UpdateMicroButtons then UpdateMicroButtons() end
if Enable_BagButtons then Enable_BagButtons() end
</OnHide>
</Scripts>
</Frame>
</Ui>
@@ -1256,7 +1256,7 @@
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- Not GameFontNormalMed1: GameMenuFrame.xml declares that after this file. -->
<!-- Not BenillaGameFontNormalMed1: GameMenuAdapters.xml declares it later. -->
<FontString name="$parentTitleText" inherits="GameFontNormal" text="Options">
<Anchors>
<Anchor point="TOP" relativeTo="$parentHeader"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor>
+5 -7
View File
@@ -1,7 +1,6 @@
# The core interface, in load order: the reference's own `FrameXML.toc` files, off the player's
# patch chain, and one bare filename of ours from `assets/ui`. Entry 0, Fonts.xml, loads alone
# into every boot VM, from Startup on; the rest loads on entering the world, then benilla's layer
# (`layer.toc`), then the third-party addons.
# patch chain. Entry 0, Fonts.xml, loads alone into every boot VM, from Startup on; the rest loads
# on entering the world, then benilla's layer (`layer.toc`), then the third-party addons.
# The font objects thus exist before the login screen; the reference loads Fonts.xml with the
# rest of FrameXML.
# `inherits=`, `parent=` and a named `relativeTo=` resolve only against names declared above; a
@@ -54,6 +53,9 @@ Interface\FrameXML\ComboFrame.xml
Interface\FrameXML\UIPanelTemplates.lua
Interface\FrameXML\UIPanelTemplates.xml
# Inherits `GameMenuButtonTemplate`. It sources UIParent.lua again, as at the reference's own row,
# which rebuilds UIParent.lua's tables; no row between here and UIParent.xml writes into them.
Interface\FrameXML\GameMenuFrame.xml
# The reference's three options windows, loaded hidden so addons find their frames by name.
# Deviation: the player opens the layer's own options window, because 1.12's settings screens are
# much worse to use; the stock one holds `OptionsFrame`.
@@ -148,7 +150,3 @@ Interface\FrameXML\HelpFrame.xml
Interface\FrameXML\CinematicFrame.xml
Interface\FrameXML\TabardFrame.xml
Interface\FrameXML\TutorialFrame.xml
# Ours, in place of the stock GameMenuFrame.xml until the layer reshapes the stock menu (#553).
# Last: it wraps `ToggleGameMenu`, `IsOptionFrameOpen` and `UpdateMicroButtons` to count
# `BenillaOptionsFrame`.
GameMenuFrame.xml
+2
View File
@@ -11,6 +11,8 @@ ScrollTemplates.xml
KeyBindingsPage.xml
# `BenillaOptionsFrame`; its rows read the option globals the stock options windows set.
OptionsFrame.xml
# The ESC menu's era look on the stock `GameMenuFrame`, whose Options rung opens ours.
GameMenuAdapters.xml
# Wraps the stock bag verbs; the keyring is a container (id -2).
ContainerFrameAdapters.xml
# The pet page's autocast widgets in the pet action button's proportions.
@@ -181,10 +181,11 @@ fn escape_closes_the_options_window_before_opening_the_menu() {
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\GameMenuFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the Keybindings page's faux-scroll kit
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "GameMenuFrame.xml");
load_xml(&s, "GameMenuAdapters.xml");
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
assert!(s
@@ -469,7 +470,8 @@ fn an_addon_frame_registered_in_uispecialframes_closes_on_escape() {
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");
load_xml(&s, r"Interface\FrameXML\GameMenuFrame.xml");
load_xml(&s, "GameMenuAdapters.xml");
s.run(
r#"
@@ -401,7 +401,7 @@ fn the_shipped_frames_carry_the_references_flags() {
assert!(
// The floor guards the pairing, not the census: the paired count falls as each of our
// files declaring a reference-named frame retires.
compared > 5,
compared >= 1,
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
);
assert!(
@@ -0,0 +1,389 @@
//! The stock ESC menu's rungs under the addons that reshape it (ShaguTweaks, MCP, pfUI), with the
//! layer on and without it: every stock rung stays in the chain, anchored `TOP`, so an addon's
//! insert lays out as on 1.12.1. Without the layer, the menu is the stock one.
use std::path::Path;
use benilla_ui::script::UiScript;
use crate::local_state::test_env::ENV_LOCK;
/// The production in-game load with the ESC menu open: the layer on unless `stock_ui`, and the
/// AddOns root filled by `addons`. Fails on any load error or raise.
fn open_menu_with(tag: &str, stock_ui: bool, addons: impl FnOnce(&Path)) -> UiScript {
let (mut s, failures) = super::layer_tests::production_load_with(tag, stock_ui, "", addons);
assert!(failures.is_empty(), "load failures: {failures:#?}");
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
s.resolve();
assert!(s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
}
/// An addon of `files`, each a name and its text, in the AddOns root.
fn write_addon(root: &Path, name: &str, files: &[(&str, &str)]) {
let dir = root.join(name);
std::fs::create_dir_all(&dir).unwrap();
for (file, body) in files {
std::fs::write(dir.join(file), body).unwrap();
}
}
/// The corpus addon `name`, copied whole into the AddOns root.
fn copy_addon(corpus: &Path, root: &Path, name: &str) {
let dir = root.join(name);
std::fs::create_dir_all(&dir).unwrap();
for entry in std::fs::read_dir(corpus.join(name)).unwrap().flatten() {
if entry.file_type().unwrap().is_file() {
std::fs::copy(entry.path(), dir.join(entry.file_name())).unwrap();
}
}
}
/// A shown rung's `(top, bottom)` down from the frame's top, and whether it is centred in it.
fn rung(s: &UiScript, name: &str) -> (f64, f64, bool) {
let (top, bottom, left, right, ftop, fleft, fright) = s
.eval::<(f64, f64, f64, f64, f64, f64, f64)>(&format!(
"local f = GameMenuFrame return {name}:GetTop(), {name}:GetBottom(), \
{name}:GetLeft(), {name}:GetRight(), f:GetTop(), f:GetLeft(), f:GetRight()"
))
.unwrap_or_else(|e| panic!("{name}: {e}"));
let centred = ((left - fleft) - (fright - right)).abs() < 1e-3;
(ftop - top, ftop - bottom, centred)
}
/// The shown rungs of the open menu, top to bottom: the children of `GameMenuFrame` that draw.
fn shown_rungs(s: &UiScript) -> Vec<String> {
let mut names: Vec<String> = s
.eval(
"local out = {} for _, c in ipairs({ GameMenuFrame:GetChildren() }) do \
if c:IsVisible() then table.insert(out, c:GetName()) end end return out",
)
.unwrap();
names.sort_by(|a, b| rung(s, a).0.total_cmp(&rung(s, b).0));
names
}
/// No two shown rungs overlap, each is centred, and each is 21 tall.
fn assert_a_clean_ladder(s: &UiScript) -> Vec<String> {
let names = shown_rungs(s);
for pair in names.windows(2) {
let (above, below) = (rung(s, &pair[0]), rung(s, &pair[1]));
assert!(
below.0 >= above.1 - 1e-3,
"{} (down {}..{}) overlaps {} (down {}..{})",
pair[1],
below.0,
below.1,
pair[0],
above.0,
above.1
);
}
for name in &names {
let (top, bottom, centred) = rung(s, name);
assert!(
(bottom - top - 21.0).abs() < 1e-3,
"{name} is {} tall",
bottom - top
);
assert!(centred, "{name} is off centre in the menu");
}
names
}
/// ShaguTweaks' insert (`config.lua:313-324`), restated in its own calls: narrow and deepen the
/// menu, hang a new rung under Interface Options, then hang Key Bindings under the new rung.
const SHAGU_ADVANCED_OPTIONS: &str = r#"
local panel = CreateFrame("Frame", nil, UIParent)
panel:Hide()
GameMenuFrame:SetWidth(GameMenuFrame:GetWidth() - 10)
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + 10)
local rung = CreateFrame("Button", "GameMenuButtonAdvancedOptions", GameMenuFrame, "GameMenuButtonTemplate")
rung:SetPoint("TOP", GameMenuButtonUIOptions, "BOTTOM", 0, -1)
rung:SetText("Advanced Options")
rung:SetScript("OnClick", function() HideUIPanel(GameMenuFrame); panel:Show() end)
GameMenuButtonKeybindings:ClearAllPoints()
GameMenuButtonKeybindings:SetPoint("TOP", rung, "BOTTOM", 0, -1)
"#;
fn shagu(root: &Path) {
write_addon(
root,
"ZZShagu",
&[
("ZZShagu.toc", "## Interface: 11200\nconfig.lua\n"),
("config.lua", SHAGU_ADVANCED_OPTIONS),
],
);
}
/// ShaguTweaks anchors its Advanced Options under `GameMenuButtonUIOptions` and re-anchors
/// `GameMenuButtonKeybindings` under it (`GameMenuFrame.xml:78`, `:93`): it loads without a raise,
/// the button lands 1 under the Options rung, and every rung below moves down by its height and
/// the gap, 22, as the stock rungs do on the stock menu.
#[test]
fn shagutweaks_advanced_options_lands_under_the_options_rung_and_pushes_the_rest_down() {
let _data = benilla_formats::wow_data_or_skip!();
let _l = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for stock_ui in [false, true] {
let tag = if stock_ui { "shagu-stock" } else { "shagu" };
let base = open_menu_with(&format!("{tag}-base"), stock_ui, |_| {});
let s = open_menu_with(tag, stock_ui, shagu);
let before = shown_rungs(&base);
let after = assert_a_clean_ladder(&s);
let options = rung(&s, "GameMenuButtonOptions");
let advanced = rung(&s, "GameMenuButtonAdvancedOptions");
// Under the Options rung on ours, as the era menu has no Sound or UI Options rung.
let above = if stock_ui {
"GameMenuButtonUIOptions"
} else {
"GameMenuButtonOptions"
};
assert!(
(advanced.0 - (rung(&s, above).1 + 1.0)).abs() < 1e-3,
"stock_ui {stock_ui}: Advanced Options at {advanced:?}, 1 under {above}"
);
let at = after
.iter()
.position(|n| n == "GameMenuButtonAdvancedOptions")
.expect("the button is shown");
assert_eq!(after[at - 1], above, "stock_ui {stock_ui}: {after:?}");
let mut expected = before.clone();
expected.insert(at, "GameMenuButtonAdvancedOptions".into());
assert_eq!(after, expected, "stock_ui {stock_ui}: the ladder");
for name in &after[at + 1..] {
let moved = (rung(&s, name).0 - options.0)
- (rung(&base, name).0 - rung(&base, "GameMenuButtonOptions").0);
assert!(
(moved - 22.0).abs() < 1e-3,
"stock_ui {stock_ui}: {name} moved {moved} down, not 22"
);
}
}
}
/// MCP's AddOns button (`MCP.xml:108-127`) anchors `TOP` under Macros, sets Logout's `TOP` under
/// itself without `ClearAllPoints` and grows the menu by 25: Logout's one `TOP` anchor is replaced,
/// so Logout, Exit Game and Return to Game move down under it, as on the stock menu.
#[test]
fn mcps_addons_button_lands_under_macros_and_pushes_logout_down() {
let _data = benilla_formats::wow_data_or_skip!();
let corpus = benilla_formats::addon_corpus_or_skip!();
let _l = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for stock_ui in [false, true] {
let tag = if stock_ui { "mcp-stock" } else { "mcp" };
let base = open_menu_with(&format!("{tag}-base"), stock_ui, |_| {});
let s = open_menu_with(tag, stock_ui, |root| copy_addon(&corpus, root, "MCP"));
let after = assert_a_clean_ladder(&s);
let macros = rung(&s, "GameMenuButtonMacros");
let addons = rung(&s, "GameMenuButtonAddOns");
let logout = rung(&s, "GameMenuButtonLogout");
assert!(
(addons.0 - (macros.1 + 1.0)).abs() < 1e-3,
"stock_ui {stock_ui}: AddOns {addons:?} 1 under Macros {macros:?}"
);
assert!(
(logout.0 - (addons.1 + 1.0)).abs() < 1e-3,
"stock_ui {stock_ui}: Logout {logout:?} 1 under AddOns {addons:?}"
);
let at = after
.iter()
.position(|n| n == "GameMenuButtonAddOns")
.unwrap();
assert_eq!(after[at - 1], "GameMenuButtonMacros");
assert_eq!(
after[at + 1..],
[
"GameMenuButtonLogout",
"GameMenuButtonQuit",
"GameMenuButtonContinue"
]
);
let height = |s: &UiScript| s.eval::<f64>("return GameMenuFrame:GetHeight()").unwrap();
assert!(
(height(&s) - height(&base) - 25.0).abs() < 1e-3,
"stock_ui {stock_ui}: the menu grows by MCP's 25"
);
let span = |s: &UiScript, a: &str, b: &str| rung(s, b).0 - rung(s, a).0;
for (a, b) in [
("GameMenuButtonLogout", "GameMenuButtonQuit"),
("GameMenuButtonQuit", "GameMenuButtonContinue"),
] {
assert!(
(span(&s, a, b) - span(&base, a, b)).abs() < 1e-3,
"stock_ui {stock_ui}: {b} still follows {a}"
);
}
}
}
/// pfUI's game-menu skin (`skins/blizzard/game_menu.lua`) in its shape: it narrows the menu by 30,
/// finds the unnamed MAIN_MENU title among the regions, adds a button at the top, shifts
/// `GameMenuButtonOptions`' own point down 22 through `GetPoint`, and skins every stock rung that
/// exists. The rungs stay centred in the narrower frame, as the stock ones do, with no raise.
const PFUI_SHAPED_SKIN: &str = r#"
GameMenuFrame:SetWidth(GameMenuFrame:GetWidth() - 30)
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + 6)
local title
for _, region in ipairs({ GameMenuFrame:GetRegions() }) do
if region:GetObjectType() == "FontString" and region:GetDrawLayer() == "ARTWORK"
and region:GetText() and string.find(region:GetText(), MAIN_MENU, 1) then
title = region
end
end
title:SetTextColor(1, 1, 1, 1)
title:ClearAllPoints()
title:SetPoint("TOP", GameMenuFrame, "TOP", 0, 16)
local button = CreateFrame("Button", "GameMenuButtonPFUI", GameMenuFrame, "GameMenuButtonTemplate")
button:SetPoint("TOP", 0, -10)
button:SetText("pfUI Config")
local point, relativeTo, relativePoint, xOffset, yOffset = GameMenuButtonOptions:GetPoint()
GameMenuButtonOptions:SetPoint(point, relativeTo, relativePoint, xOffset, yOffset - 22)
ZZPFUI_SKINNED = 0
for _, rung in pairs({ GameMenuButtonOptions, GameMenuButtonSoundOptions, GameMenuButtonUIOptions,
GameMenuButtonKeybindings, GameMenuButtonMacros, GameMenuButtonRatings, GameMenuButtonLogout,
GameMenuButtonQuit, GameMenuButtonContinue }) do
if rung then ZZPFUI_SKINNED = ZZPFUI_SKINNED + 1 end
end
"#;
#[test]
fn a_pfui_shaped_skin_keeps_every_rung_centred_in_the_narrowed_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let _l = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for stock_ui in [false, true] {
let tag = if stock_ui { "pfui-stock" } else { "pfui" };
let s = open_menu_with(tag, stock_ui, |root| {
write_addon(
root,
"ZZpfUI",
&[
("ZZpfUI.toc", "## Interface: 11200\nskin.lua\n"),
("skin.lua", PFUI_SHAPED_SKIN),
],
);
});
let after = assert_a_clean_ladder(&s);
assert_eq!(
after[0], "GameMenuButtonPFUI",
"stock_ui {stock_ui}: {after:?}"
);
assert_eq!(
after[1], "GameMenuButtonOptions",
"stock_ui {stock_ui}: {after:?}"
);
assert_eq!(
s.eval::<i64>("return ZZPFUI_SKINNED").unwrap(),
8,
"stock_ui {stock_ui}: every stock rung is there to skin"
);
}
}
/// Booted without the layer, as `WOW_STOCK_UI=1` does, the ESC menu is the stock one: its eight
/// rungs at the stock file's geometry (`GameMenuFrame.xml:5-185`), each opening its stock window.
#[test]
fn the_stock_ui_menu_is_the_stock_menu_and_every_rung_opens_its_stock_window() {
use benilla_ui::script::SessionRequest;
let _data = benilla_formats::wow_data_or_skip!();
let _l = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut s = open_menu_with("stock-menu", true, |_| {});
let rungs = assert_a_clean_ladder(&s);
assert_eq!(
rungs,
[
"GameMenuButtonOptions",
"GameMenuButtonSoundOptions",
"GameMenuButtonUIOptions",
"GameMenuButtonKeybindings",
"GameMenuButtonMacros",
"GameMenuButtonLogout",
"GameMenuButtonQuit",
"GameMenuButtonContinue",
]
);
assert_eq!(
s.eval::<(f64, f64)>("return GameMenuFrame:GetWidth(), GameMenuFrame:GetHeight()")
.unwrap(),
(195.0, 246.0)
);
// Options' centre 37 under the top, then 1-unit gaps, and 16 above Return to Game.
let tops: Vec<f64> = rungs.iter().map(|n| rung(&s, n).0).collect();
let want = [26.5, 48.5, 70.5, 92.5, 114.5, 136.5, 158.5, 195.5];
for ((name, got), want) in rungs.iter().zip(&tops).zip(want) {
assert!((got - want).abs() < 1e-3, "{name} at {got}, not {want}");
}
let labels: Vec<String> = s
.eval(
"local out = {} for _, n in ipairs({ 'Options', 'SoundOptions', 'UIOptions', \
'Keybindings', 'Macros', 'Logout', 'Quit', 'Continue' }) do \
table.insert(out, getglobal('GameMenuButton' .. n):GetText()) end return out",
)
.unwrap();
let globals: Vec<String> = s
.eval(
"return { VIDEOOPTIONS_MENU, SOUNDOPTIONS_MENU, UIOPTIONS_MENU, KEY_BINDINGS, MACROS, \
LOGOUT, EXIT_GAME, RETURN_TO_GAME }",
)
.unwrap();
assert_eq!(labels, globals);
assert!(!s
.eval::<bool>("return BenillaGameMenuButtonEditMode ~= nil")
.unwrap());
for (rung, window) in [
("Options", "OptionsFrame"),
("SoundOptions", "SoundOptionsFrame"),
("UIOptions", "UIOptionsFrame"),
("Keybindings", "KeyBindingFrame"),
("Macros", "MacroFrame"),
] {
s.run("HideUIPanel(GameMenuFrame) ShowUIPanel(GameMenuFrame)")
.unwrap();
s.run(&format!("GameMenuButton{rung}:Click()")).unwrap();
assert!(
s.eval::<bool>(&format!(
"return {window} ~= nil and {window}:IsVisible() and true or false"
))
.unwrap(),
"{rung} opens {window}"
);
s.run(&format!("HideUIPanel({window})")).unwrap();
// The stock video window's own show and hide read video CVars and verbs benilla does not
// register yet (`OptionsFrame.lua:146`, `:260`); what the menu does is the click above.
let errors = s.take_errors();
assert!(
errors
.iter()
.all(|e| rung == "Options" && e.contains("OptionsFrame.lua")),
"{rung}: {errors:?}"
);
}
for (rung, request) in [
("Logout", SessionRequest::Logout),
("Quit", SessionRequest::Quit),
] {
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
let _ = s.take_session_requests();
s.run(&format!("GameMenuButton{rung}:Click()")).unwrap();
assert_eq!(s.take_session_requests(), vec![request], "{rung}");
}
s.run("ShowUIPanel(GameMenuFrame) GameMenuButtonContinue:Click()")
.unwrap();
assert!(!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -1,4 +1,5 @@
//! Tests for our `GameMenuFrame.xml`, the frame ESC opens. The bags in its way are the stock
//! Tests for the frame ESC opens: the stock `GameMenuFrame.xml` as the layer's
//! `GameMenuAdapters.xml` reshapes it. The bags in its way are the stock
//! `ContainerFrame1..12`, recycled, so tests ask [`bag_open`] rather than naming a frame.
use benilla_ui::script::{
@@ -7,8 +8,8 @@ use benilla_ui::script::{
use super::test_ui::{bag_open, load_ui as load_xml, BAG_UI};
/// The menu over what it needs (`GameMenuButtonTemplate` is `UIPanelTemplates.xml:403`), with
/// `extra` in its way, deduped so no file loads twice.
/// The stock menu over what it needs (`GameMenuButtonTemplate` is `UIPanelTemplates.xml:403`),
/// with `extra` in its way, then the layer's reshaping, deduped so no file loads twice.
fn harness_with(extra: &[&str]) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
@@ -24,10 +25,12 @@ fn harness_with(extra: &[&str]) -> UiScript {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// Ahead of `extra`, as the core loads before the layer: it sources UIParent.lua again.
r"Interface\FrameXML\GameMenuFrame.xml",
]
.into_iter()
.chain(extra.iter().copied())
.chain(std::iter::once("GameMenuFrame.xml"))
.chain(std::iter::once("GameMenuAdapters.xml"))
.collect();
let mut loaded = std::collections::HashSet::new();
for file in files {
@@ -58,8 +61,8 @@ fn bag_harness_with(before: &[&str], after: &[&str]) -> UiScript {
/// replace 1.12's; without the AddOns rung, as the character-select screen is the only addon UI.
const LADDER: [&str; 7] = [
"GameMenuButtonOptions",
"GameMenuButtonEditMode",
"GameMenuButtonSupport",
"BenillaGameMenuButtonEditMode",
"BenillaGameMenuButtonSupport",
"GameMenuButtonMacros",
"GameMenuButtonLogout",
"GameMenuButtonQuit",
@@ -114,7 +117,10 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
let s = harness();
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
for name in ["GameMenuButtonEditMode", "GameMenuButtonSupport"] {
for name in [
"BenillaGameMenuButtonEditMode",
"BenillaGameMenuButtonSupport",
] {
assert!(
!s.eval::<bool>(&format!("return {name}:IsEnabled() ~= 0"))
.unwrap(),
@@ -134,10 +140,10 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
"{name} is live"
);
}
// Edit Mode reads the era's `HUD_EDIT_MODE_MENU` and Continue 1.12's `RETURN_TO_GAME`; Options
// is a literal, as 1.12's `OPTIONS_MENU` is "Options Menu".
// Edit Mode is the era's label and Continue 1.12's `RETURN_TO_GAME`; Options is a literal, as
// 1.12's `OPTIONS_MENU` is "Options Menu".
assert_eq!(
s.eval::<String>("return GameMenuButtonEditMode:GetText()")
s.eval::<String>("return BenillaGameMenuButtonEditMode:GetText()")
.unwrap(),
"Edit Mode"
);
@@ -35,10 +35,12 @@ pub(crate) fn harness() -> UiScript {
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
// Before our files, as the core loads before the layer: it sources UIParent.lua again.
r"Interface\FrameXML\GameMenuFrame.xml",
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"GameMenuFrame.xml",
"GameMenuAdapters.xml",
] {
// Strict for ours: an unknown template only warns, so a skinless window would pass.
if file == "KeyBindingsPage.xml" || file == "OptionsFrame.xml" {
+33 -17
View File
@@ -28,17 +28,34 @@ f:SetScript("OnEvent", function() table.insert(ADDON_LOADED_LOG, arg1) end)
/// A VM after the production in-game load, with one third-party addon, `ZZOrder`, in a hermetic
/// AddOns root; the layer on unless `stock_ui`. Returns the VM and the load's failures.
fn production_load(tag: &str, stock_ui: bool) -> (UiScript, Vec<String>) {
let before = format!("{DEFINE_LOG}\n{ADDON_LOADED_LOG}");
production_load_with(tag, stock_ui, &before, |root| {
let dir = root.join("ZZOrder");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("ZZOrder.toc"), "## Interface: 11200\nprobe.lua\n").unwrap();
std::fs::write(
dir.join("probe.lua"),
"ZZORDER_SAW_LAYER = BenillaOptionsFrame ~= nil\n",
)
.unwrap();
})
}
/// The production in-game load, the layer on unless `stock_ui`, over a hermetic AddOns root that
/// `addons` fills; `before` runs ahead of the load. The caller holds [`ENV_LOCK`]. Returns the VM
/// and the load's failures.
pub(super) fn production_load_with(
tag: &str,
stock_ui: bool,
before: &str,
addons: impl FnOnce(&std::path::Path),
) -> (UiScript, Vec<String>) {
let tmp = std::env::temp_dir().join(format!("benilla-layer-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let home = tmp.join("benilla-config");
let dir = home.join("AddOns").join("ZZOrder");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("ZZOrder.toc"), "## Interface: 11200\nprobe.lua\n").unwrap();
std::fs::write(
dir.join("probe.lua"),
"ZZORDER_SAW_LAYER = BenillaOptionsFrame ~= nil\n",
)
.unwrap();
let root = home.join("AddOns");
std::fs::create_dir_all(&root).unwrap();
addons(&root);
let _c = EnvGuard::unset("WOW_CAPTURE");
let _h = EnvGuard::set("BENILLA_HOME", home.to_str().unwrap());
@@ -50,6 +67,8 @@ fn production_load(tag: &str, stock_ui: bool) -> (UiScript, Vec<String>) {
exists: true,
name: Some("Probesix".into()),
level: 60,
class: Some("Warrior".into()),
class_file: Some("WARRIOR".into()),
..Default::default()
}),
);
@@ -57,8 +76,7 @@ fn production_load(tag: &str, stock_ui: bool) -> (UiScript, Vec<String>) {
s.note_addon_info_reply(&[]);
s.register_cvars(crate::cvars::registered_pairs());
let mut failures = super::load_font_registry(&s);
s.run(DEFINE_LOG).unwrap();
s.run(ADDON_LOADED_LOG).unwrap();
s.run(before).unwrap();
// The layer passed in, not set through `WOW_STOCK_UI`: every test in this process reads it.
failures.extend(super::manifest::load_ingame_ui_with(
&mut s,
@@ -109,12 +127,10 @@ fn the_layer_loads_after_every_stock_file_and_before_the_addons() {
assert!(failures.is_empty(), "load failures: {failures:#?}");
let log: Vec<String> = s.eval("return DEFINE_LOG").unwrap();
// The core's last stock row (TutorialFrame.lua), the core's own last row (GameMenuFrame.xml),
// the layer's first file (ScrollTemplates.xml) and last (FrameXMLFixes.xml is all
// redefinitions, so ScriptLogFrame.xml), then the addon.
// The core's last row (TutorialFrame.lua), the layer's first file (ScrollTemplates.xml) and
// last (FrameXMLFixes.xml is all redefinitions, so ScriptLogFrame.xml), then the addon.
let order = [
"TutorialFrame_OnHide",
"GameMenuButton_Pending",
"BenillaScrollBar_Step",
"BenillaScriptLog_Toggle",
"ZZORDER_SAW_LAYER",
@@ -139,7 +155,7 @@ fn the_layer_loads_after_every_stock_file_and_before_the_addons() {
|| n.strip_prefix("arg")
.is_some_and(|d| d.len() == 1 && d.as_bytes()[0].is_ascii_digit())
};
let late: Vec<&String> = log[at[2]..]
let late: Vec<&String> = log[at[1]..]
.iter()
.filter(|n| stock.contains(n.as_str()) && !dispatch(n))
.collect();
@@ -235,6 +251,7 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() {
"BenillaScrollBar_Step",
"KeyBindings_OnHostKey",
"BenillaOptionsFrame_SelectCategory",
"BenillaGameMenuButtonEditMode",
"BENILLA_BAG_WAS_OPEN",
"BenillaScriptLog_Toggle",
] {
@@ -245,12 +262,11 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() {
}
assert!(!s.eval::<bool>("return BenillaOptionsFrame ~= nil").unwrap());
assert!(!s.eval::<bool>("return ZZORDER_SAW_LAYER").unwrap());
// The host's calls into the layer, and the game menu's Options rung.
// The host's calls into the layer.
for body in [
super::ERRORS_TOGGLE.to_string(),
super::ERRORS_CLEAR.to_string(),
super::host_key_capture("CTRL-J"),
"GameMenuButtonOptions:Click()".to_string(),
] {
s.run(&body).unwrap_or_else(|e| panic!("{body}: {e}"));
}
+2
View File
@@ -817,6 +817,8 @@ mod tooltip_template_tests;
#[cfg(test)]
mod escape_tests;
#[cfg(test)]
mod game_menu_addon_tests;
#[cfg(test)]
mod game_menu_tests;
@@ -34,6 +34,8 @@ fn harness_on(mut s: UiScript) -> UiScript {
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
// Before our files, as the core loads before the layer: it sources UIParent.lua again.
r"Interface\FrameXML\GameMenuFrame.xml",
// The stock options kit, hidden: `UIOptionsFrame_Init` assigns the option globals our
// rows capture at OnLoad, and these declare the tables the Graphics rows and census read.
r"Interface\FrameXML\OptionsFrameTemplates.xml",
@@ -42,7 +44,7 @@ fn harness_on(mut s: UiScript) -> UiScript {
"ScrollTemplates.xml", // the page scroll and the Keybindings list
"KeyBindingsPage.xml", // the Keybindings page's templates and script
"OptionsFrame.xml",
"GameMenuFrame.xml",
"GameMenuAdapters.xml",
] {
// Our window loads strict: a missing template there fails instead of only warning.
if loaded(&s, file) {
@@ -1322,10 +1322,9 @@ mod tests {
);
}
/// The core's chain rows are the reference's own `FrameXML.toc` rows, every one, but the stock
/// `GameMenuFrame.xml`, which ours stands in for until the layer reshapes the stock menu. A
/// row the stock toc lacks runs a file the reference does not, or twice: `UIParent.xml:4`
/// sources `LocaleProperties.lua`, which has no row.
/// The core's chain rows are the reference's own `FrameXML.toc` rows, every one. A row the
/// stock toc lacks runs a file the reference does not, or twice: `UIParent.xml:4` sources
/// `LocaleProperties.lua`, which has no row.
#[test]
fn the_cores_chain_rows_are_the_stock_tocs_rows() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -1337,7 +1336,6 @@ mod tests {
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.filter(|l| !l.eq_ignore_ascii_case("GameMenuFrame.xml"))
.map(|l| format!("interface\\framexml\\{}", l.to_ascii_lowercase()))
.collect();
let mut core: Vec<String> = super::super::addons::Addon::core()
@@ -486,7 +486,7 @@ fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
};
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
let mut keys = 0;
let mut swept = 0;
for entry in std::fs::read_dir(&dir).expect("assets/ui").flatten() {
let path = entry.path();
if path.extension().is_none_or(|e| e != "xml") {
@@ -505,7 +505,6 @@ fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
"{file}: text=\"{value}\" is shaped like a GlobalStrings key but the real \
GlobalStrings.lua has no such string — it would render as its own key name"
);
keys += 1;
} else {
assert!(
resolved.is_none(),
@@ -513,10 +512,11 @@ fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
the loader would silently show that string's value instead of these words"
);
}
swept += 1;
}
}
// A floor, so the sweep cannot pass by matching nothing.
assert!(keys >= 1, "only {keys} key-shaped text= values swept");
assert!(swept >= 1, "only {swept} text= values swept");
}
/// No shipped script hands a GlobalStrings key to a text sink as its words: `SetText` cannot tell
@@ -1096,7 +1096,7 @@ fn every_declared_parent_really_attaches() {
let declared = shipped_frame_parents();
assert!(
// A sanity floor for the scan, not a census.
declared.len() >= 2,
!declared.is_empty(),
"only {} parent declarations found — the scan broke",
declared.len()
);
@@ -1445,7 +1445,7 @@ fn the_stock_sound_options_window_loads_hidden_and_the_alias_is_gone() {
);
// `IsOptionFrameOpen` names the stock options windows, all hidden; it sees ours through the
// wrapper `GameMenuFrame.xml` installs.
// wrapper `GameMenuAdapters.xml` installs.
assert!(
s.eval::<bool>("return IsOptionFrameOpen() and true or false")
.unwrap(),
@@ -1503,7 +1503,7 @@ fn the_stock_sound_window_turns_a_box_off_and_stores_a_slider_as_lua_text() {
/// The stock Video Options window loads hidden (`OptionsFrame.xml:5`) and owns the `OptionsFrame`
/// name; ours is `BenillaOptionsFrame`, which the ESC menu opens and the stock "options window
/// open?" consumers see only through the `GameMenuFrame.xml` wrappers.
/// open?" consumers see only through the `GameMenuAdapters.xml` wrappers.
#[test]
fn the_stock_video_options_window_loads_hidden_and_owns_its_own_name() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -1989,8 +1989,7 @@ fn the_stock_options_windows_load_and_save_are_reachable_for_addons() {
}
/// benilla's own interface is one layer, and it does not grow: `layer.toc` names exactly these
/// files, all ours (none off the chain), and `assets/ui` holds nothing else but the two manifests
/// and the core's one file. A window is migrated, not authored: point `benilla.toc` at
/// files, all ours (none off the chain), and `assets/ui` holds nothing else but the two manifests. A window is migrated, not authored: point `benilla.toc` at
/// `Interface\FrameXML\<Window>.xml`, delete ours, and build the engine verbs the stock file
/// calls; what changes the stock UI goes in one of these files.
#[test]
@@ -1999,6 +1998,7 @@ fn the_layer_does_not_grow() {
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"GameMenuAdapters.xml",
"ContainerFrameAdapters.xml",
"SpellBookAdapters.xml",
"ScriptLogFrame.xml",
@@ -2020,44 +2020,28 @@ fn the_layer_does_not_grow() {
found.sort();
let mut shipped: Vec<String> = LAYER
.iter()
.chain(&["benilla.toc", "layer.toc", "GameMenuFrame.xml"])
.chain(&["benilla.toc", "layer.toc"])
.map(|s| s.to_string())
.collect();
shipped.sort();
assert_eq!(
found, shipped,
"assets/ui holds the two manifests, the layer's files and the core's one file, nothing else"
"assets/ui holds the two manifests and the layer's files, nothing else"
);
}
/// The core is the stock UI: `benilla.toc` names the reference's own files off the chain and one
/// file of ours, `GameMenuFrame.xml`, which stands in for the stock one until the layer reshapes
/// the stock menu, and whose row says so.
/// The core is the stock UI: `benilla.toc` names the reference's own files off the chain and no
/// file of ours.
#[test]
fn the_core_toc_names_no_file_of_ours_but_the_game_menu() {
fn the_core_toc_names_no_file_of_ours() {
let core = super::addons::Addon::core().toc.files;
let ours: Vec<&String> = core
.iter()
.filter(|f| !super::reference_ui::is_chain_entry(f))
.collect();
assert_eq!(
ours,
["GameMenuFrame.xml"],
"benilla.toc names a file of ours: benilla's own interface is the layer (layer.toc), \
which loads after every stock file"
);
let toc = super::content::read(super::manifest::MANIFEST).expect("benilla.toc");
let lines: Vec<&str> = toc.lines().collect();
let at = lines
.iter()
.position(|l| l.trim() == "GameMenuFrame.xml")
.expect("the row");
assert!(
lines[..at]
.iter()
.rev()
.take_while(|l| l.starts_with('#'))
.any(|l| l.contains("#553")),
"GameMenuFrame.xml's row names the issue that retires it"
ours.is_empty(),
"benilla.toc names a file of ours, {ours:?}: benilla's own interface is the layer \
(layer.toc), which loads after every stock file"
);
}
+18 -2
View File
@@ -130,9 +130,16 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bo
s.run(MICRO_BUTTON_STAND_INS)
.expect("the micro-button stand-ins");
}
if leaf.eq_ignore_ascii_case("UIParent.xml") && super::reference_ui::is_chain_entry(&path) {
// The stock GameMenuFrame.xml sources UIParent.lua again, which drops the stand-ins' wrappers.
if (leaf.eq_ignore_ascii_case("UIParent.xml") || leaf.eq_ignore_ascii_case("GameMenuFrame.xml"))
&& super::reference_ui::is_chain_entry(&path)
{
s.run(UIPARENT_STAND_INS).expect("the UIParent stand-ins");
}
if leaf.eq_ignore_ascii_case("GameMenuFrame.xml") && super::reference_ui::is_chain_entry(&path)
{
s.run(GAME_MENU_STAND_INS).expect("the game menu stand-ins");
}
if leaf.eq_ignore_ascii_case("UIOptionsFrame.xml") {
s.run(UIOPTIONS_STAND_INS)
.expect("the stock options window's stand-ins");
@@ -241,7 +248,7 @@ pub(super) const UIPARENT_STAND_INS: &str = r#"
-- names are real, and all three options windows are the REFERENCE's own files,
-- loaded hidden — including `OptionsFrame`, the video window, which used to be our own
-- window's name. Ours is `BenillaOptionsFrame` now and is not in this list: it is not a name
-- the reference indexes, and the wrappers in `GameMenuFrame.xml` are what tell these two
-- the reference indexes, and the wrappers in `GameMenuAdapters.xml` are what tell these two
-- functions about it. A KIT is a prefix of the manifest and may load none of the four, which
-- is what these stand-ins are for.
local function benilla_seat_options()
@@ -263,6 +270,15 @@ pub(super) const UIPARENT_STAND_INS: &str = r#"
end
"#;
/// What a kit owes stock `GameMenuFrame.xml`: its OnShow and OnHide (`GameMenuFrame.xml:188-195`)
/// call the micro-button and bag-bar verbs unguarded, from files a kit may stop short of. Plain
/// global writes, so the real definitions replace them in any order.
pub(super) const GAME_MENU_STAND_INS: &str = r#"
UpdateMicroButtons = UpdateMicroButtons or function() end
Disable_BagButtons = Disable_BagButtons or function() end
Enable_BagButtons = Enable_BagButtons or function() end
"#;
/// What a kit owes stock `UIOptionsFrame.xml`: its `VARIABLES_LOADED` arm
/// (`UIOptionsFrame.lua:193-227`) calls six functions from files a kit may not load, and a kit
/// fires the event itself. Functions, so seating them at load is safe in any order.
+5 -2
View File
@@ -42,7 +42,10 @@ pub fn load_ui(script: &UiScript, entry: &str) {
.run(MICRO_BUTTON_STAND_INS)
.expect("the micro-button stand-ins");
}
if leaf.eq_ignore_ascii_case("UIParent.xml") && entry.contains('\\') {
// The stock GameMenuFrame.xml sources UIParent.lua again, which drops the stand-ins' wrappers.
if (leaf.eq_ignore_ascii_case("UIParent.xml") || leaf.eq_ignore_ascii_case("GameMenuFrame.xml"))
&& entry.contains('\\')
{
script
.run(UIPARENT_STAND_INS)
.expect("the UIParent stand-ins");
@@ -123,7 +126,7 @@ const UIPARENT_STAND_INS: &str = r#"
-- names are real, and all three options windows are the REFERENCE's own files,
-- loaded hidden — including `OptionsFrame`, the video window, which used to be our own
-- window's name. Ours is `BenillaOptionsFrame` now and is not in this list: it is not a name
-- the reference indexes, and the wrappers in `GameMenuFrame.xml` are what tell these two
-- the reference indexes, and the wrappers in `GameMenuAdapters.xml` are what tell these two
-- functions about it. A KIT is a prefix of the manifest and may load none of the four, which
-- is what these stand-ins are for.
local function benilla_seat_options()