From 22634279bd6ef86ee87249f01a5f0f8593f6914b Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:01:28 +0700 Subject: [PATCH] benilla's own interface loads as one layer after the stock FrameXML and before the addons; the core loads StatsFrame.xml and runs LocaleProperties.lua once, as FrameXML.toc does (#562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit benilla's own interface now loads as one layer on top of the stock UI, as `docs/METHOD.md` now states: the core loads the stock FrameXML rows, the layer (`assets/ui/layer.toc`) loads after the last of them and before the third-party addons, and like FrameXML it has no AddOn API row and fires no `ADDON_LOADED` (the reference loads `FrameXML.toc` outside `AddOn_Load`, `UI_Init` `0x48fbf0`). - Moved into the layer unchanged: `ScrollTemplates.xml`, `KeyBindingsPage.xml`, `OptionsFrame.xml`, `ContainerFrameAdapters.xml`, `SpellBookAdapters.xml`, `ScriptLogFrame.xml`. `GameMenuFrame.xml` stays the core's last row until #553. - The host's Lua fixes to stock code (`FCF_ChatTabFadeFinished`, the `DurabilityFrame` re-seat) move into the layer's `FrameXMLFixes.xml`; the resize re-seat stays host-side as an engine adaptation. - The core now loads `StatsFrame.xml` and runs `LocaleProperties.lua` once, from `UIParent.xml`, so its chain rows are exactly the stock `FrameXML.toc` rows but the menu; the order is #555. - A dev build boots without the layer under `WOW_STOCK_UI=1`; the host's calls into the layer are guarded. Verified: new tests for the layer's file list, the core list against the stock toc, the load position (a `_G` definition log), absence from the AddOn API and the stock-only boot, each failing with the change undone; a before/after dump of every global, frame, draw order and saved variable differs only by `StatsFrame` and the moved fixes' source; `scripts/gates.sh` and `scripts/smoke.sh` green, and a live logout/re-login under `WOW_STOCK_UI=1` clean. Built on macOS; no platform seam touched. Part of #552 Fixes #542 Fixes #543 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../benilla-app/assets/ui/FrameXMLFixes.xml | 33 +++ crates/benilla-app/assets/ui/benilla.toc | 35 +-- crates/benilla-app/assets/ui/layer.toc | 21 ++ crates/benilla-app/src/bindings.rs | 2 +- crates/benilla-app/src/bindings/commands.rs | 3 +- crates/benilla-app/src/ui_chat/input/parse.rs | 8 +- crates/benilla-app/src/ui_script/addons.rs | 55 ++-- .../benilla-app/src/ui_script/chat_tests.rs | 4 +- crates/benilla-app/src/ui_script/content.rs | 34 ++- .../src/ui_script/durability_tests.rs | 8 +- .../benilla-app/src/ui_script/layer_tests.rs | 258 ++++++++++++++++++ crates/benilla-app/src/ui_script/manifest.rs | 154 ++++++----- crates/benilla-app/src/ui_script/mod.rs | 3 + .../benilla-app/src/ui_script/reference_ui.rs | 66 ++++- .../src/ui_script/shipped_xml_tests.rs | 70 ++++- crates/benilla-app/src/ui_script/test_ui.rs | 2 +- docs/CONTRIBUTING.md | 7 +- docs/MAP.md | 3 +- docs/METHOD.md | 19 +- scripts/api-coverage.sh | 2 +- 20 files changed, 602 insertions(+), 185 deletions(-) create mode 100644 crates/benilla-app/assets/ui/FrameXMLFixes.xml create mode 100644 crates/benilla-app/assets/ui/layer.toc create mode 100644 crates/benilla-app/src/ui_script/layer_tests.rs diff --git a/crates/benilla-app/assets/ui/FrameXMLFixes.xml b/crates/benilla-app/assets/ui/FrameXMLFixes.xml new file mode 100644 index 00000000..26cfff83 --- /dev/null +++ b/crates/benilla-app/assets/ui/FrameXMLFixes.xml @@ -0,0 +1,33 @@ + + + + diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc index 9170c73a..6b27b073 100644 --- a/crates/benilla-app/assets/ui/benilla.toc +++ b/crates/benilla-app/assets/ui/benilla.toc @@ -1,6 +1,7 @@ -# The built-in interface, in load order: a bare filename is ours, from `assets/ui`; a path is -# the reference's own file, off the player's patch chain. Entry 0, Fonts.xml, loads alone into -# every boot VM, from Startup on; the rest loads on entering the world, then third-party addons. +# 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. # 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 @@ -19,8 +20,6 @@ Interface\FrameXML\Fonts.xml Interface\FrameXML\GlobalStrings.lua # `Localize()` runs at load and overwrites GlobalStrings keys, so this follows them. Interface\FrameXML\Localization.xml -# `GetText` is a Lua global, not an engine binding: the client's `0x703bf0` calls it. -Interface\FrameXML\LocaleProperties.lua Interface\FrameXML\BasicControls.xml # Before UIParent.xml, whose `LowHealthFrame` has `parent="WorldFrame"`. Interface\FrameXML\WorldFrame.xml @@ -52,15 +51,12 @@ Interface\FrameXML\TargetFrame.xml Interface\FrameXML\PetFrame.xml # Anchors `relativeTo="TargetFrame"`. Interface\FrameXML\ComboFrame.xml -# Ours: the scroll parts of our options window and script log. -ScrollTemplates.xml - Interface\FrameXML\UIPanelTemplates.lua Interface\FrameXML\UIPanelTemplates.xml # The reference's three options windows, loaded hidden so addons find their frames by name. -# Deviation: the player opens our own options window, `OptionsFrame.xml` below, because 1.12's -# settings screens are much worse to use; the stock one holds `OptionsFrame`. +# 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`. Interface\FrameXML\OptionsFrame.xml Interface\FrameXML\SoundOptionsFrame.xml # Inherits `OptionsCheckButtonTemplate`, and declares `UIOptionsFrameCheckButtons`, which @@ -70,12 +66,6 @@ Interface\FrameXML\UIOptionsFrame.xml Interface\FrameXML\StaticPopup.xml Interface\FrameXML\CoinPickupFrame.xml -# Ours: the Keybindings page's templates and script, which OptionsFrame.xml uses at load. -KeyBindingsPage.xml - -# Ours: `BenillaOptionsFrame`; its rows read the option globals the stock windows above set. -OptionsFrame.xml - # `MainMenuBar`, `MainMenuBarArtFrame` and `MainMenuExpBar`, which later files read at load. Interface\FrameXML\MainMenuBar.xml @@ -115,16 +105,14 @@ Interface\FrameXML\PaperDollFrame.xml Interface\FrameXML\PetPaperDollFrame.xml Interface\FrameXML\MainMenuBarBagButtons.xml Interface\FrameXML\StackSplitFrame.xml -# Ours: wraps the stock bag verbs; the keyring is a container (id -2). -ContainerFrameAdapters.xml Interface\FrameXML\ReputationFrame.xml Interface\FrameXML\SkillFrame.xml # Brings HonorFrameTemplates.xml, whose row templates `Blizzard_InspectUI` inherits. Interface\FrameXML\HonorFrame.xml Interface\FrameXML\DressUpFrame.xml +# `FrameXML.toc` has it between ItemButtonTemplate.xml and SpellBookFrame.xml. +Interface\FrameXML\StatsFrame.xml Interface\FrameXML\SpellBookFrame.xml -# Ours: the pet page's autocast widgets in the pet action button's proportions. -SpellBookAdapters.xml # RaidFrame.xml names `parent="FriendsFrame"` and MailFrame.xml inherits `FriendsFrameTabTemplate`. Interface\FrameXML\FriendsFrame.xml Interface\FrameXML\RaidFrame.xml @@ -156,12 +144,11 @@ Interface\FrameXML\BonusActionBarFrame.xml Interface\FrameXML\WorldStateFrame.xml # `ClassTrainerListScrollFrameTemplate`, which on-demand addons inherit. Interface\FrameXML\ClassTrainerFrameTemplates.xml -# Ours: the `/errors` script error log and our slash commands. -ScriptLogFrame.xml Interface\FrameXML\HelpFrame.xml Interface\FrameXML\CinematicFrame.xml Interface\FrameXML\TabardFrame.xml Interface\FrameXML\TutorialFrame.xml -# Ours: the ESC game menu. Last: it wraps `ToggleGameMenu`, `IsOptionFrameOpen` and -# `UpdateMicroButtons` to count `BenillaOptionsFrame`. +# 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 diff --git a/crates/benilla-app/assets/ui/layer.toc b/crates/benilla-app/assets/ui/layer.toc new file mode 100644 index 00000000..17843450 --- /dev/null +++ b/crates/benilla-app/assets/ui/layer.toc @@ -0,0 +1,21 @@ +# benilla's own interface, one layer on the stock one (docs/METHOD.md): every file is ours, from +# `assets/ui`, and loads in this order after every `benilla.toc` row and before the third-party +# addons. It is listed in no addon list, fires no `ADDON_LOADED` and cannot be switched off; a dev +# build boots without it under `WOW_STOCK_UI=1`. A test names its files, so it does not grow. + +## Interface: 11200 + +# The scroll parts of our options window and script log. +ScrollTemplates.xml +# The Keybindings page's templates and script, which OptionsFrame.xml uses at load. +KeyBindingsPage.xml +# `BenillaOptionsFrame`; its rows read the option globals the stock options windows set. +OptionsFrame.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. +SpellBookAdapters.xml +# The `/errors` script error log and our slash commands. +ScriptLogFrame.xml +# Our fixes to the stock Lua. +FrameXMLFixes.xml diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs index f2743f6d..7981c3c1 100644 --- a/crates/benilla-app/src/bindings.rs +++ b/crates/benilla-app/src/bindings.rs @@ -484,7 +484,7 @@ fn latch_and_dispatch( if let Some(chord_str) = captured { // The Keybindings page's own handler, not a binding body. if let Some(s) = script.as_mut() { - if let Err(e) = s.run(&format!("KeyBindings_OnHostKey(\"{chord_str}\")")) { + if let Err(e) = s.run(&crate::ui_script::host_key_capture(&chord_str)) { warn!("bindings(capture): {e}"); } } diff --git a/crates/benilla-app/src/bindings/commands.rs b/crates/benilla-app/src/bindings/commands.rs index 76c2459b..37f4222e 100644 --- a/crates/benilla-app/src/bindings/commands.rs +++ b/crates/benilla-app/src/bindings/commands.rs @@ -1731,9 +1731,10 @@ pub(crate) static ABSENT: &[Absent] = &[ // ── Misc ──────────────────────────────────────────────────────────────────────────── // The nine `hidden="true" debug="true"` rows: benilla's instruments for these answer to the // dev plane's chords, not to a binding. + // Stock `StatsFrame.lua` defines `ToggleStats`; the readout it shows is `GetDebugStats`. absent!( "TOGGLESTATS", - ["ToggleStats"], + ["GetDebugStats"], "a dev-plane instrument on a dev chord" ), absent!( diff --git a/crates/benilla-app/src/ui_chat/input/parse.rs b/crates/benilla-app/src/ui_chat/input/parse.rs index 6094d78d..f000aa5a 100644 --- a/crates/benilla-app/src/ui_chat/input/parse.rs +++ b/crates/benilla-app/src/ui_chat/input/parse.rs @@ -291,13 +291,13 @@ fn slash_command(index: SlashIndex, args: &str) -> ParsedChat { // `/reload` is the rebuild `/console reloadUI` runs (`0x4035f0`, which reads no arguments). S::ReloadUi => ParsedChat::ReloadUi, S::ConvertRaid => ParsedChat::ConvertRaid, - // `/errors`, benilla's own: toggles the script error log, a FrameXML window; - // `/errors clear` empties it. + // `/errors`, benilla's own: toggles the layer's script error log; `/errors clear` empties + // it. A no-op when the run boots without the layer. S::ScriptErrors => ParsedChat::Lua { body: if args.trim().eq_ignore_ascii_case("clear") { - "BenillaScriptLog_Clear()".into() + crate::ui_script::ERRORS_CLEAR.into() } else { - "BenillaScriptLog_Toggle()".into() + crate::ui_script::ERRORS_TOGGLE.into() }, }, // `SlashCmdList["CONSOLE"]` (`ChatFrame.lua:671`) is `ConsoleExec(msg)`. A typed line runs diff --git a/crates/benilla-app/src/ui_script/addons.rs b/crates/benilla-app/src/ui_script/addons.rs index bacb360d..38ed6680 100644 --- a/crates/benilla-app/src/ui_script/addons.rs +++ b/crates/benilla-app/src/ui_script/addons.rs @@ -1,8 +1,8 @@ //! Where interfaces come from: addon discovery under [`root`] and the load walk ([`Walk::load`]). -//! An [`Addon`] is a name, a parsed `.toc` and the [`Source`] its files come from. benilla's own -//! interface is one too, with the compiled-in tree as its source, but not for the lifecycle: like -//! FrameXML it loads outside `AddOn_Load` ([`super::manifest::load_ingame_ui`]) and gets no -//! `ADDON_LOADED`. +//! An [`Addon`] is a name, a parsed `.toc` and the [`Source`] its files come from. The core +//! interface and benilla's layer are two more, with the compiled-in tree as their source, but not +//! for the lifecycle: like FrameXML they load outside `AddOn_Load` +//! ([`super::manifest::load_ingame_ui`]), get no `ADDON_LOADED` and have no registry row. use std::collections::HashSet; use std::path::{Path, PathBuf}; @@ -23,7 +23,8 @@ pub(crate) const LOAD_INSTRUCTION_BUDGET: u64 = 200_000_000; /// Where one interface's files come from. pub(super) enum Source { - /// benilla's own interface: the compiled-in tree, shadowed by `assets/ui` in a dev build. + /// The core's and the layer's own files: the compiled-in tree, shadowed by `assets/ui` in a + /// dev build. Builtin, /// The AddOns root on disk, not this addon's own folder: a request under `Interface/AddOns/` /// maps onto it, so a dependent can reach a shared library addon beside it. @@ -34,8 +35,8 @@ pub(super) enum Source { /// One loadable interface: a name, its parsed manifest, and where its files come from. pub(super) struct Addon { - /// The folder name (`"benilla"` for the builtin): what the AddOn API keys on and what - /// `ADDON_LOADED` carries. + /// The folder name (`"benilla"` for the core and the layer, which have no registry row): what + /// the AddOn API keys on and what `ADDON_LOADED` carries. pub(super) name: String, /// The parsed `.toc`: the ordered file list plus every directive. pub(super) toc: Toc, @@ -48,15 +49,23 @@ impl Addon { Addon { name, toc, source } } - /// benilla's own interface, which is in the binary and so cannot be missing. - pub(super) fn builtin() -> Self { - let toc = content::read(super::manifest::MANIFEST) + /// The core interface, [`super::manifest::MANIFEST`], which is in the binary and so cannot + /// be missing. + pub(super) fn core() -> Self { + Self::shipped(super::manifest::MANIFEST) + } + + /// benilla's layer, [`super::manifest::LAYER_MANIFEST`], in the binary as the core is. + pub(super) fn layer() -> Self { + Self::shipped(super::manifest::LAYER_MANIFEST) + } + + /// The interface one shipped `.toc` names. + fn shipped(manifest: &str) -> Self { + let toc = content::read(manifest) .map(|t| Toc::parse(&t)) .unwrap_or_else(|| { - error!( - "ui_script: {} is not in the shipped UI — no interface will load", - super::manifest::MANIFEST - ); + error!("ui_script: {manifest} is not in the shipped UI — it will not load"); Toc::default() }); Addon { @@ -67,7 +76,7 @@ impl Addon { } /// One file's bytes, by a path already resolved into the source's path space. A `Dir` reads - /// under the AddOns root, then the chain ([`read_addon_file`]); only the flat builtin falls + /// under the AddOns root, then the chain ([`read_addon_file`]); only the flat shipped tree falls /// back to the basename, which for a `Dir` would rescue an escaping path. Bytes, not text: a /// `.lua` reaches Lua as it is on disk. fn read(&self, req: &str) -> Option> { @@ -85,7 +94,7 @@ impl Addon { } /// The base this addon's manifest entries resolve against: `Interface/AddOns/` for a - /// `Dir`, empty for the builtin's flat tree and the chain's full paths. + /// `Dir`, empty for the shipped flat tree and the chain's full paths. fn prefix(&self) -> String { match &self.source { Source::Builtin | Source::Chain => String::new(), @@ -95,7 +104,7 @@ impl Addon { /// The chunk name, which addons parse: `"@%s"` (`0x8716e0`) over the resolved install path, /// built by `0x704bc0` for every `.lua`, as for `