Files
benilla-octo/crates/benilla-app/src/ui_script/test_ui.rs
T
gromandClaude Opus 5 9dde1a80e5 The keyring grows in rows, the rank is art, and two fixtures I left behind
- **Keyring**: `GetKeyRingSize` has no level term at all — it scans for the highest filled slot
  and rounds up to a multiple of four (`ContainerFrame.lua:817-844`), expanding only when the key
  count has caught up with that slot. So one key is one row of four, four keys in four slots grow
  to eight rather than refusing a fifth, and full is all 32. The realm also **fixed** 1.12's silent
  refusal: it renamed the message to `NO_EMPTY_KEYRING_SLOTS_ERROR` and defined it
  ("Your keyring is full."), where stock printed a name that did not exist.
- **Bag tooltip**: the item-button `OnUpdate` accumulates toward `TOOLTIP_UPDATE_TIME` instead of
  counting down from it (`:694-704`), and reads the threshold before adding — so a landing template
  repaints an open plate two frames after it crosses a fifth of a second, not on the next frame.
- **Raid grid**: the rank is a `$parentRankIcon` Texture now (`Blizzard_RaidUI.xml:49`), set to
  `RAID_LEADER_ICON`/`RAID_ASSISTANT_ICON` (`:100`, `:155-161`). The `$parentRank` FontString that
  held "(L)"/"(A)" is still declared and never written.
- **Ready check**: the realm moved it into its own group UI (`Turtle_GroupUI.lua:259`) and
  `RaidFrameReadyCheckButton`'s OnClick calls `GroupFrame_InitiateReadyCheck` by name, so
  `load_social_ui` seats that file; without it the button clicks into a nil.

And two fixtures that were mine to fix and I missed: `quest_tests`' detail and reward panels still
carried `item_id: 0`, which `3f2982445` taught the counters to take-while out of existence. Same
correction as the `benilla-ui` fixture in that commit, with the reason written where it is read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 14:15:58 -06:00

936 lines
49 KiB
Rust

//! The tests' interface loader: a bare filename is a file we ship under `assets/ui`, a path one off
//! the player's patch chain ([`super::reference_ui::is_chain_entry`], the manifest's rule). It
//! reads the source tree, not the compiled-in copy.
use benilla_ui::script::{QuadContent, UiScript};
/// Load one interface file into `s`, panicking on any loader error; returns the frames it built
/// (0 for a `.lua`). A chain entry needs client data, so a test naming one opens with
/// `benilla_formats::wow_data_or_skip!()`.
pub(crate) fn load_ui(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, false, false)
}
/// The cooldown indicator's file, as the stock `CooldownFrameTemplate` names it.
pub(super) const COOLDOWN_MODEL: &str = r"Interface\Cooldown\UI-Cooldown-Indicator.mdx";
/// The cooldown indicator's file facts (`benilla-extract m2seq`), handed to the engine as the app
/// does: sequence 0 (id 0, 1000 ms, clamp) is the sweep `CooldownFrame_OnUpdateModel` scrubs,
/// sequence 1 (id 1, 1000 ms, clamp) the finish flash whose end hides the frame.
pub(super) fn cooldown_facts(s: &mut UiScript) {
use benilla_ui::widget::{ModelFileFacts, SequenceFacts};
let seq = |anim_id, duration_ms| SequenceFacts {
anim_id,
duration_ms,
looping: false,
};
s.set_model_facts(
COOLDOWN_MODEL,
ModelFileFacts {
sequences: vec![seq(0, 1000), seq(1, 1000)],
bbox: ([0.0; 3], [0.0; 3]),
cameras: 0,
},
);
}
/// The play head `(anim_id, cursor_ms)` of the shown cooldown pane `owner` names, as the tile
/// renderer samples it.
pub(super) fn cooldown_play(s: &UiScript, owner: &str) -> Option<(u16, u32)> {
let heads = s.visible_model_panes();
s.extract().into_iter().find_map(|q| match &q.content {
QuadContent::ModelPane {
handle,
model: Some(m),
..
} if m.eq_ignore_ascii_case(COOLDOWN_MODEL)
&& s.quad_owner_name(q.target).as_deref() == Some(owner) =>
{
heads
.iter()
.find(|p| p.handle == *handle)
.and_then(|p| p.play.map(|ph| (ph.anim_id, ph.cursor_ms)))
}
_ => None,
})
}
/// [`cooldown_play`] for whichever cooldown pane is shown: `ContainerFrame_GenerateFrame` numbers
/// a bag's slots from the far end, so a one-item test asks for the sweep, not a name.
pub(super) fn cooldown_play_any(s: &UiScript) -> Option<(u16, u32)> {
let heads = s.visible_model_panes();
s.extract().into_iter().find_map(|q| match &q.content {
QuadContent::ModelPane {
handle,
model: Some(m),
..
} if m.eq_ignore_ascii_case(COOLDOWN_MODEL) => heads
.iter()
.find(|p| p.handle == *handle)
.and_then(|p| p.play.map(|ph| (ph.anim_id, ph.cursor_ms))),
_ => None,
})
}
/// [`load_ui`], and a missing template fails too: the loader only warns, and the art is lost.
pub(super) fn load_ui_strict(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, true, false)
}
/// [`load_ui`], and any loader warning fails.
pub(super) fn load_ui_no_warnings(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, false, true)
}
/// What a file needs seated before it, as `FrameXML.toc`'s own order supplies it.
///
/// A harness loads the handful of files its subject needs, never the whole manifest, so anything
/// an earlier TOC entry defines is simply absent. That is not a loud failure — the loader *warns*
/// "unknown template" and builds the frame without its art — so a harness missing a prerequisite
/// quietly tests a different, artless frame than the one the client draws. This table is how a
/// harness gets the TOC's ordering without having to know the TOC.
///
/// Each row is `(file, what it needs first)`, with the reference's own line numbers as the proof
/// of order:
/// - `StaticPopup.xml` (TOC 30) inherits `MoneyInputFrameTemplate` from `MoneyInputFrame` (20-21).
/// - `PaperDollFrame.xml` calls `UIDropDownMenu_Initialize` at `OnLoad`, from `UIDropDownMenu`
/// (24) — a *global*, not a template, so the loader does not even warn: the OnLoad raises.
/// - Anything inheriting `ItemButtonTemplate` needs `ItemButtonTemplate.xml` (58).
const PREREQS: &[(&str, &[&str])] = &[
(
"staticpopup.xml",
&[
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
// Its item-confirm popups inherit this too (TOC 58).
r"Interface\FrameXML\ItemButtonTemplate.xml",
],
),
(
"paperdollframe.xml",
&[
r"Interface\FrameXML\UIDropDownMenu.lua",
r"Interface\FrameXML\UIDropDownMenu.xml",
],
),
(
"containerframe.xml",
&[r"Interface\FrameXML\ItemButtonTemplate.xml"],
),
(
"merchantframe.xml",
&[r"Interface\FrameXML\ItemButtonTemplate.xml"],
),
// `CONTINENTS_LENGTH = 2`, the realm's two-continent map (`Overrides.lua:1`, toc row 13 — 72
// rows above this file). `WorldMap_UpdateContinentDropDownText` and its zone twin compare the
// current continent against it unguarded (`WorldMapFrame.lua:440`, `:457`), so a harness that
// seats the map alone raises `attempt to compare nil with number` on the second open.
("worldmapframe.xml", &[r"Interface\FrameXML\Overrides.lua"]),
(
"lootframe.xml",
&[
r"Interface\FrameXML\ItemButtonTemplate.xml",
// The `.lua` alone, for `MAX_RAID_MEMBERS` (`RaidFrame.lua:1`): the limit the realm's
// rewritten master-loot submenu counts candidates to (`LootFrame.lua:273`). A runtime
// need, not a load one — the loot window is toc row 76 and `RaidFrame.xml` is 82, so
// the live client has the constant long before a menu can open, while a harness that
// seats the loot window alone raises `'for' limit must be a number` on the first
// class-row hover.
r"Interface\FrameXML\RaidFrame.lua",
],
),
(
"tradeframe.xml",
&[r"Interface\FrameXML\ItemButtonTemplate.xml"],
),
(
"bankframe.xml",
&[r"Interface\FrameXML\ItemButtonTemplate.xml"],
),
// The realm's toc puts `ChatFrame.xml` immediately above this one (rows 57 and 58), and the
// order is load-bearing rather than tidy: `ChatFrame.xml` builds **ChatFrame1**, the frame
// `DOCKED_CHAT_FRAMES` starts with, and this file docks itself onto that dock at load
// (`FloatingChatFrame.xml`'s inline `<Script>` → `FCF_DockFrame` → `FCF_SaveDock`). Seated
// alone, the dock's first entry is missing, `FCF_SaveDock` reaches `SetChatWindowDocked` with
// window 0, and `FloatingChatFrame_Update` hands `FCF_SetWindowName` a frame it cannot name —
// two loader errors, and **137 of this crate's tests** died on them, every one that seats any
// part of the chat UI.
// `GetDifficultyColor` is a **FrameXML** function (`reference/1.12-globals.tsv` types it
// `framexml`, not a client global) and `QuestLogFrame.lua` is what defines it — ten toc rows
// above this one. `FriendsFrame.lua:523` calls it for every roster row's level colour, from
// four different entry points (`FriendsFrame_Update`, `FriendsFrame_OnEvent`,
// `FriendsFrameGuildStatusButton_OnClick`, `GuildFrameGuildListToggleButton_OnClick`), and the
// raise takes the rest of the pane's update with it — including the
// `GuildPlayerStatusFrame:Show()` at the end of that branch (`:579`), which is how it reads as
// "the guild tab opens and the player pane is not visible".
//
// The `.lua` alone, not `QuestLogFrame.xml`: the functions are what is missing, and the window
// would drag its own prerequisites in for frames no guild test looks at.
(
"friendsframe.xml",
&[r"Interface\FrameXML\QuestLogFrame.lua"],
),
(
"floatingchatframe.xml",
&[
r"Interface\FrameXML\ChatFrame.xml",
// …and the stock video window, which the dock's hover pass indexes **unguarded**:
// `not OptionsFrame:IsShown()` at `FloatingChatFrame.lua:862`, inside the OnUpdate
// `UIParent` drives. The realm's toc loads it 29 rows above this one (row 29 against
// 58), so this is the toc's own order, and it gives the real frame rather than a
// stand-in that would shadow it.
r"Interface\FrameXML\OptionsFrame.xml",
],
),
];
/// The ledger of files [`load_entry`] has already seated in this VM, as a Lua table so it lives
/// exactly as long as the script does. Without it a prerequisite seated for one file would be
/// loaded again for the next — and these are not virtual-only files: `MoneyInputFrame.xml`,
/// `UIDropDownMenu.xml` and `ItemButtonTemplate.xml` each build concrete frames too, so a second
/// load would duplicate them.
const LOAD_LEDGER: &str = "__benilla_test_loaded";
fn already_seated(s: &UiScript, path: &str) -> bool {
let key = path.replace('\\', "/").to_ascii_lowercase();
s.eval::<bool>(&format!(
"return ({LOAD_LEDGER} and {LOAD_LEDGER}[\"{key}\"]) and true or false"
))
.unwrap_or(false)
}
fn mark_seated(s: &UiScript, path: &str) {
let key = path.replace('\\', "/").to_ascii_lowercase();
let _ = s.run(&format!(
"{LOAD_LEDGER} = {LOAD_LEDGER} or {{}} {LOAD_LEDGER}[\"{key}\"] = true"
));
}
fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bool) -> usize {
// The app registers its CVars before any file loads: stock `UIOptionsFrame.xml` reads two
// camera CVars at `OnLoad` and raises on a nil. A value a test already set stands.
s.register_cvars(crate::cvars::registered_pairs());
let path = entry.replace('\\', "/");
// The TOC's ordering, for a harness that loads a file on its own. Recursive by construction:
// each prerequisite goes through this same path, so a prerequisite's own prerequisites seat
// too, and the ledger keeps any of them from being built twice.
if let Some(leaf) = path.rsplit('/').next().map(str::to_ascii_lowercase) {
if let Some((_, needs)) = PREREQS.iter().find(|(f, _)| *f == leaf) {
for need in *needs {
if !already_seated(s, need) {
load_entry(s, need, false, false);
}
}
}
}
mark_seated(s, &path);
let bytes = read(&path).unwrap_or_else(|| panic!("{entry}: not found"));
if path.to_ascii_lowercase().ends_with(".lua") {
s.run_chunk_named(&bytes, &format!("@{entry}"))
.unwrap_or_else(|e| panic!("{entry}: {e}"));
return 0;
}
let doc = benilla_ui::framexml::parse(&benilla_ui::source::decode(&bytes))
.unwrap_or_else(|e| panic!("{entry}: {e}"));
let provider = |req: &str| -> Option<Vec<u8>> { read(req) };
// Seated before the load: `MultiActionBarFrame_OnLoad` indexes `UIOptionsFrameCheckButtons`
// during this load walk.
if path
.rsplit('/')
.next()
.is_some_and(|l| l.eq_ignore_ascii_case("MultiActionBars.xml"))
{
s.run(MULTI_ACTION_BAR_STAND_INS)
.expect("the multibar stand-ins");
}
// Our options window's Graphics rows read stock `OptionsFrameSliders` (`OptionsFrame.lua`) at
// OnLoad; the manifest loads it one seat above ours, so a kit seats it before our file.
if path
.rsplit('/')
.next()
.is_some_and(|l| l.eq_ignore_ascii_case("OptionsFrame.xml"))
&& !super::reference_ui::is_chain_entry(&path)
{
let bytes = read("Interface/FrameXML/OptionsFrame.lua")
.expect("the reference's own OptionsFrame.lua");
s.run_chunk_named(&bytes, "@Interface\\FrameXML\\OptionsFrame.lua")
.expect("the video window's slider table");
}
let report = benilla_ui::loader::load_in(s, &doc, &path, &provider);
assert!(
report.errors.is_empty(),
"{entry}: loader errors: {:?}",
report.errors
);
let leaf = path.rsplit('/').next().unwrap_or("");
if leaf.eq_ignore_ascii_case("MainMenuBarMicroButtons.xml") {
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) {
s.run(UIPARENT_STAND_INS).expect("the UIParent stand-ins");
}
if leaf.eq_ignore_ascii_case("UIOptionsFrame.xml") {
s.run(UIOPTIONS_STAND_INS)
.expect("the stock options window's stand-ins");
}
if no_warnings {
assert!(
report.warnings.is_empty(),
"{entry}: loader warnings: {:?}",
report.warnings
);
}
if strict_templates {
let missing: Vec<&String> = report
.warnings
.iter()
.filter(|w| w.contains("unknown template"))
.collect();
assert!(
missing.is_empty(),
"{entry}: inherits a template this house does not ship (the frame loads, its ART does \
not): {missing:?}"
);
}
report.frames
}
/// What a kit owes stock `UIParent.xml`: no-op stand-ins, seated when it loads, for what its
/// `<OnUpdate>`, `UIParent_OnEvent` and `ShowUIPanel` call from files a kit may stop short of. A
/// `function X()` is a plain global write, so the real definition replaces a stand-in in any order.
pub(super) const UIPARENT_STAND_INS: &str = r#"
-- Callees of the stock UIParent.xml's <OnUpdate> and of UIParent_OnEvent's arms that live in
-- files a kit may stop short of, plus the bag verbs the stock ShowUIPanel calls and the two
-- container constants its window walks read: no-op stand-ins, each overwritten by the real
-- definition when its file loads (a chunk's `function X()` is a plain global write, unlike a
-- frame's non-overwriting publish — which is why the FRAMES below are seated on first use).
FCF_OnUpdate = FCF_OnUpdate or function() end
FCF_DockUpdate = FCF_DockUpdate or function() end
UnitPopup_OnUpdate = UnitPopup_OnUpdate or function() end
BattlefieldFrame_OnUpdate = BattlefieldFrame_OnUpdate or function() end
MultiActionBar_Update = MultiActionBar_Update or function() end
RaidOptionsFrame_UpdatePartyFrames = RaidOptionsFrame_UpdatePartyFrames or function() end
LocalizeFrames = LocalizeFrames or function() end
updateContainerFrameAnchors = updateContainerFrameAnchors or function() end
-- 1.12 keeps UpdateNameplates in UIOptionsFrame.lua, which a kit reaches only at manifest
-- l.21; our own OptionsFrame.xml re-declares it below that. Both are plain
-- `function X()` writes, so a full kit ends on ours and a short one keeps this no-op.
UpdateNameplates = UpdateNameplates or function() end
CloseAllBags = CloseAllBags or function() end
OpenBackpack = OpenBackpack or function() end
CloseBackpack = CloseBackpack or function() end
NUM_CONTAINER_FRAMES = NUM_CONTAINER_FRAMES or 0
-- The reference's own initial values (`ContainerFrame.lua:11-12`), not zeroes: the tooltip's
-- default corner is `-CONTAINER_OFFSET_X - 13, CONTAINER_OFFSET_Y`, so a kit reading zero here
-- would seat every default-anchored plate 70 units low.
CONTAINER_OFFSET_X = CONTAINER_OFFSET_X or 0
CONTAINER_OFFSET_Y = CONTAINER_OFFSET_Y or 70
BATTLEFIELD_TAB_OFFSET_Y = BATTLEFIELD_TAB_OFFSET_Y or 210
-- The pass writes its `isVar` rows with `setglobal`, but only for a name that already reads
-- non-nil (`frame = getglobal(index); if frame then`), so an unseeded one is never written at
-- all. These four are the reference's own seeds, from the files that declare them
-- (ContainerFrame.lua, PetActionBarFrame.lua, WorldStateFrame.lua).
PETACTIONBAR_YPOS = PETACTIONBAR_YPOS or 98
PETACTIONBAR_XPOS = PETACTIONBAR_XPOS or 36
-- The frames these three read UNGUARDED, seated on the call rather than at load: a frame's
-- publish to _G is non-overwriting (0x701bd0), so a stand-in seated before the real file loads
-- would shadow the real window for good.
--
-- A stand-in is seated UNDER a name, so it is given that name and the id the name implies.
-- Neither is cosmetic and the publish is `setglobal` either way, so the naming costs nothing:
-- `CreateFrame("Frame")` with no name answers `GetName()` nil and `GetID()` 0, and the files a
-- kit loads AFTER the pass has run read both off whatever `_G` holds. That is how one missing
-- pair cost **137 of this crate's tests**: `ChatFrame1`/`ChatFrame2` are rows of the managed
-- table, so the first pass seated two anonymous frames over them, the publish of the real
-- windows is non-overwriting (`0x701bd0`) so the stand-ins stayed, and then
-- `FloatingChatFrame.lua` read them back — `FCF_SetWindowName` concatenating a nil
-- `GetName()` (l.693), and `FCF_SaveDock` handing `SetChatWindowDocked` window **0** (l.1284).
-- Trailing digits are the id because that is FrameXML's own convention for a numbered frame
-- (`<Frame name="ChatFrame2" id="2">`, the `$parentN`/`id=N` pairing throughout), and a name
-- without them keeps the reference's own id-less 0.
local function benilla_seat(names)
for _, name in ipairs(names) do
if not getglobal(name) then
local f = CreateFrame("Frame", name)
local _, _, digits = string.find(name, "(%d+)$")
if digits then f:SetID(tonumber(digits)) end
f:Hide()
setglobal(name, f)
end
end
end
local real_manage = UIParent_ManageFramePositions
function UIParent_ManageFramePositions()
benilla_seat({ "MainMenuBar", "MultiBarLeft", "MultiBarRight", "MultiBarBottomLeft",
"PetActionBarFrame", "ShapeshiftBarFrame", "ReputationWatchBar", "MainMenuExpBar",
"MainMenuBarMaxLevelBar", "CastingBarFrame", "QuestTimerFrame", "QuestWatchFrame",
"DurabilityFrame", "DurabilityShield", "DurabilityOffWeapon", "DurabilityRanged",
-- NOT ChatFrame1/ChatFrame2, though they are rows of the managed table and the pass
-- names them at UIParent.lua:2024: every row read goes through
-- `frame = _G[index]; if ( frame )` (l.1962), so a missing one is skipped, and the
-- l.2024 comparison is `frame == ChatFrame1`, which a nil global answers without
-- raising. Seating them was pure loss: this pass runs AT LOAD, from
-- ShapeshiftBar_OnLoad (BonusActionBarFrame.lua:109 -> :157), which every kit reaches
-- twelve toc rows BEFORE the chat files, so the stand-ins shadowed the real windows
-- for good (a frame's publish to _G is non-overwriting, 0x701bd0) and
-- FloatingChatFrame.lua read them back. That cost 137 of this crate's tests.
"MinimapCluster", "ShapeshiftBarLeft",
"ShapeshiftBarMiddle", "ShapeshiftBarRight", "BattlefieldMinimapTab" })
-- …and every FRAME the managed table itself names (`frame:IsObjectType` on a nil is what
-- a kit missing one raises) — read off the table, so a row added there needs nothing here.
-- The `isVar` rows are skipped: those keys are global NUMBERS the pass writes
-- (CONTAINER_OFFSET_X/Y, PETACTIONBAR_YPOS…), and a frame seated under one of those names
-- would be arithmetic's problem two files later.
-- …except the two chat windows, and the exception is the point rather than a special
-- case. The pass reads a managed row ONLY through `frame = _G[index]; if ( frame )`
-- (UIParent.lua:1962) and compares at l.2024 with `frame == ChatFrame1`, which a nil
-- global answers without raising -- so these two never need a stand-in. Every other row
-- can be reached unguarded by the custom-positioning tail (`MultiBarBottomLeft:IsShown()`
-- l.2040, `PetActionBarFrame:IsShown()` l.2065, `MultiBarBottomRight:IsShown()` l.2070),
-- which is why the rest of the table still seats.
--
-- And seating these two is not merely unnecessary, it is destructive: `FloatingChatFrame`
-- builds the real ChatFrame1..7 twelve toc rows AFTER this pass first runs (it runs at
-- load, from ShapeshiftBar_OnLoad, BonusActionBarFrame.lua:109 -> :157), a frame's publish
-- to _G is non-overwriting (0x701bd0), so a stand-in seated here shadows the real window
-- for the rest of the VM's life while the real one -- correctly built, correctly parented
-- -- is unreachable by name. FloatingChatFrame.lua then reads the stand-in back:
-- FCF_SetWindowName concatenating its nil GetName (l.693), FCF_SaveDock handing
-- SetChatWindowDocked window 0 (l.1284), and FCF_UpdateButtonSide doing arithmetic on its
-- unresolvable GetRight (l.1019). That cost 136 of this crate's tests.
-- A managed row's own KEY is never seated here, and the exception the chat windows used
-- to be is now the rule. The pass reads every row through `frame = _G[index]; if ( frame )`
-- (UIParent.lua:1962), so an absent one is skipped and costs nothing; what *is* reached
-- unguarded is the custom-positioning tail, and every name that tail touches
-- (`MultiBarBottomLeft` l.2041, `ShapeshiftBar*` l.2043-2057, `PetActionBarFrame` l.2065,
-- `BattlefieldMinimapTab` l.2036…) is in the explicit list above, where it can be read
-- and reasoned about.
--
-- Seating a key is not merely unnecessary, it is destructive, because a frame's publish to
-- `_G` is non-overwriting (`0x701bd0`): a stand-in seated at this pass — which runs AT
-- LOAD, from `ShapeshiftBar_OnLoad` (`BonusActionBarFrame.lua:109` -> `:157`) — shadows
-- the real frame for the VM's life if its own file loads later, and the real one, properly
-- built and parented, becomes unreachable by name. That cost `ChatFrame1`/`ChatFrame2`
-- **137** of this crate's tests, and then `GroupLootFrame1` (a row at `UIParent.lua:1909`,
-- declared in `LootFrame.xml`, loaded after) **five more**: the real roll popups got no
-- `<OnShow>`, so no name, no icon, no bind-on-pickup decoration, and `CANCEL_LOOT_ROLL`
-- reached no registered frame. `FramerateLabel` and `TutorialFrameParent` are the same
-- shape waiting to happen.
local named = {}
for _, row in pairs(UIPARENT_MANAGED_FRAME_POSITIONS) do
-- A row's ANCHOR TARGET still seats, and it is a different set: the keys are the
-- frames being MOVED, the targets are what they move relative to, and several targets
-- (`ActionButton1`, `MainMenuBarArtFrame`) are declared in files a one-window kit
-- never loads. An unresolvable target is a RAISE at l.1668, which aborts the pass
-- mid-way rather than anchoring to the parent and carrying on.
if row.anchorTo then table.insert(named, row.anchorTo) end
end
benilla_seat(named)
for _, name in ipairs({ "SlidingActionBarTexture0", "SlidingActionBarTexture1" }) do
if not getglobal(name) then setglobal(name, UIParent:CreateTexture()) end
end
return real_manage()
end
-- The four options/menu windows `IsOptionFrameOpen` (l.997) and `ToggleGameMenu` (l.1467)
-- index unguarded. `IsOptionFrameOpen` is on the path of every window close, so a kit that
-- loads no options window raised on the first bag click. In the shipped manifest all four
-- 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
-- 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()
benilla_seat({ "GameMenuFrame", "OptionsFrame", "UIOptionsFrame", "SoundOptionsFrame" })
if not OptionsFrameCancel then
OptionsFrameCancel = CreateFrame("Button")
OptionsFrameCancel:Hide()
end
end
local real_option_open = IsOptionFrameOpen
function IsOptionFrameOpen()
benilla_seat_options()
return real_option_open()
end
local real_toggle_menu = ToggleGameMenu
function ToggleGameMenu(clicked)
benilla_seat_options()
return real_toggle_menu(clicked)
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.
pub(super) const UIOPTIONS_STAND_INS: &str = r#"
BuffButtons_UpdatePositions = BuffButtons_UpdatePositions or function() end
FCF_Set_SimpleChat = FCF_Set_SimpleChat or function() end
FCF_Set_NormalChat = FCF_Set_NormalChat or function() end
FCF_Set_ChatLocked = FCF_Set_ChatLocked or function() end
SetChatMouseOverDelay = SetChatMouseOverDelay or function() end
MultiActionBar_ShowAllGrids = MultiActionBar_ShowAllGrids or function() end
RaidOptionsFrame_UpdatePartyFrames = RaidOptionsFrame_UpdatePartyFrames or function() end
-- …and the one its own CHECK BUTTONS reach: three of them register VARIABLES_LOADED for
-- themselves (xml l.373, 925, 993), and CheckButton43's arm calls `PartyFrame.lua`'s
-- `UpdatePartyMemberBackground`. The other two call the file's own dropdown loaders.
UpdatePartyMemberBackground = UpdatePartyMemberBackground or function() end
"#;
/// What a kit owes stock `MultiActionBars.xml`: its OnLoad (`MultiActionBars.lua:10`) writes five
/// rows into `UIOptionsFrameCheckButtons`, which `UIOptionsFrame.xml` declares eighteen toc rows
/// earlier; `or {}` keeps the real table when that file loaded first.
pub(super) const MULTI_ACTION_BAR_STAND_INS: &str = r#"
UIOptionsFrameCheckButtons = UIOptionsFrameCheckButtons or {}
-- The five ROWS it writes into, not just the table: the reference's hack assigns
-- `UIOptionsFrameCheckButtons["SHOW_MULTIBAR1_TEXT"].setFunc`, which needs the row to exist.
-- Empty rows on purpose — the real ones carry `index = 33..36, 40`, and a stand-in that
-- restated those numbers would be a transcription of the reference's table in a test helper,
-- which `OptionsFrame.xml` does not carry either. Any kit that reads an index has the
-- real window loaded and therefore the real table.
for _, key in ipairs({ "SHOW_MULTIBAR1_TEXT", "SHOW_MULTIBAR2_TEXT", "SHOW_MULTIBAR3_TEXT",
"SHOW_MULTIBAR4_TEXT", "ALWAYS_SHOW_MULTIBARS_TEXT" }) do
UIOptionsFrameCheckButtons[key] = UIOptionsFrameCheckButtons[key] or {}
end
"#;
/// What a kit owes the stock micro-button row: `UpdateMicroButtons`
/// (`MainMenuBarMicroButtons.lua:20-84`) reads ten panels, `KeyRingButton` and `IsBagOpen`
/// unguarded. Hidden, unnamed stand-ins are seated on its first call, not at load: a named frame's
/// publish never overwrites (`0x701bd0`), so an early one would shadow the real frame.
/// `tests/common/mod.rs` carries the same chunk.
pub(super) const MICRO_BUTTON_STAND_INS: &str = r#"
local real = UpdateMicroButtons
function UpdateMicroButtons()
for _, name in ipairs({ "CharacterFrame", "SpellBookFrame", "QuestLogFrame", "GameMenuFrame",
"OptionsFrame", "SoundOptionsFrame", "UIOptionsFrame", "FriendsFrame", "WorldMapFrame",
"HelpFrame" }) do
if not getglobal(name) then local f = CreateFrame("Frame") f:Hide() setglobal(name, f) end
end
if not KeyRingButton then KeyRingButton = CreateFrame("Button") KeyRingButton:Hide() end
if not KEYRING_CONTAINER then KEYRING_CONTAINER = -2 end
if not IsBagOpen then function IsBagOpen() return nil end end
return real()
end
"#;
/// One file's bytes, off the chain for a path or from this crate's `assets/ui` for a bare name;
/// also the loader's `<Include>`/`<Script file=>` provider.
pub(super) fn read(req: &str) -> Option<Vec<u8>> {
if super::reference_ui::is_chain_entry(req) {
return super::reference_ui::read(req);
}
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
std::fs::read(dir.join(req)).ok()
}
/// What stock `MerchantFrame.xml` needs to load and behave, including `BasicControls.xml` for the
/// `TEXT()` that `MerchantFrame.lua:70` calls on first show and `ItemButtonTemplate.xml`, whose
/// absence only warns. Needs client data: open with `benilla_formats::wow_data_or_skip!()`.
pub(super) const MERCHANT_UI: &[&str] = &[
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\BasicControls.xml", // TEXT()
"Interface\\FrameXML\\ItemButtonTemplate.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIParent.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The stock window tab, whose `<OnShow>` needs the `UIPanelTemplates` pair above it.
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml", // app load order: tooltip before merchant
];
/// Several callers' file lists, merged into the one order `benilla.toc` (both stores) actually
/// loads them in, deduplicated — for a test that needs more than one list's files together and
/// cannot just splice one list after another, because the two interleave in the real order.
/// Panics naming whatever it can't place, so a typo fails loud instead of silently dropping a
/// dependency.
pub(super) fn production_order(lists: &[&[&'static str]]) -> Vec<&'static str> {
let order = super::manifest::manifest_files();
let mut wanted: Vec<&'static str> = lists.iter().flat_map(|l| l.iter().copied()).collect();
wanted.sort_unstable();
wanted.dedup();
let placed: Vec<&'static str> = order
.iter()
.filter_map(|f| wanted.iter().copied().find(|w| *w == f.as_str()))
.collect();
let missing: Vec<&str> = wanted
.iter()
.copied()
.filter(|w| !placed.contains(w))
.collect();
assert!(
missing.is_empty(),
"production_order: not in benilla.toc: {missing:?}"
);
placed
}
/// Stock `GossipFrame.xml`'s dependencies in `benilla.toc` order, shared with `ui_gossip`'s feed
/// tests. The greeting pane inherits `UIPanelScrollFrameTemplate`, and a missing template only
/// warns, losing the scrollbar silently. Needs client data.
pub(crate) const GOSSIP_UI: &[&str] = &[
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\GlobalStrings.lua",
r"Interface\FrameXML\BasicControls.xml",
r"Interface\FrameXML\LocaleProperties.lua",
r"Interface\FrameXML\StaticPopup.xml",
];
/// What stock `LootFrame.xml` needs to load and behave. `GlobalStrings.lua` because a bare VM
/// lacks what the app loads at setup (the master-loot menu reads `GROUP` and `GIVE_LOOT`),
/// `ItemButtonTemplate.xml` because a missing template only warns, and `PartyFrame.xml` because
/// `LootFrame.lua:217` reads `MAX_PARTY_MEMBERS` at load. Needs client data.
pub(super) const LOOT_UI: &[&str] = &[
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIParent.xml", // UIParent.lua: the panel slot manager and the fades
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml", // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
"Interface\\FrameXML\\UIDropDownMenu.xml", // GroupLootDropDown's OnLoad calls UIDropDownMenu_Initialize
// `UnitPopup.lua` reads `ITEM_QUALITY_COLORS` at file scope (`UnitPopup.lua:47-49`), so its
// declarer, `UIParent.lua:65`, precedes it.
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
// Each `PartyMemberFrame<N>` and its pet frame run `UnitFrame_Initialize` (UnitFrame.lua) at
// OnLoad, which calls `SetTextStatusBarText` (TextStatusBar.lua).
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\UnitFrame.xml",
// `RefreshBuffs`, which each party row's OnLoad reaches (`PartyMemberFrame.lua:60`), is in
// BuffFrame.lua: the reference's toc has BuffFrame at 40 and PartyFrame at 45.
"Interface\\FrameXML\\BuffFrame.xml",
"Interface\\FrameXML\\PartyFrame.xml",
];
/// What stock `CharacterFrame.xml`, `PaperDollFrame.xml` and `PetPaperDollFrame.xml` need to load
/// and behave. `CharacterFrame_OnLoad` reaches `TextStatusBar.lua`, `PlayerFrame.xml`,
/// `MainMenuBar.xml` (`MainMenuExpBar`) and the `UIPanelTemplates` pair; two bite only on show:
/// `HonorFrame.xml`, whose labels `PaperDollFrame.lua:103` and `:122` write, and
/// `MainMenuBarMicroButtons.xml` (`MicroButtonTooltipText` on a tab hover). Needs client data.
pub(super) const CHARACTER_UI: &[&str] = &[
// The stock strings, which `PaperDollFrame_OnLoad` reads at load; there are no fallbacks.
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
// `GetText`, the gendered-string helper `ReputationFrame.lua:65` calls for each standing.
r"Interface\FrameXML\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml", // TEXT(), which those labels go through
"Interface\\FrameXML\\ItemButtonTemplate.xml", // PaperDollItemSlotButtonTemplate's base
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIParent.xml", // Model_OnLoad/_Rotate*/_OnUpdate: the model turntable
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\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' dropdowns call `UIDropDownMenu_Initialize` at load, so the kit and the menu
// table precede them.
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
// `UnitFrame_Initialize` and `CombatFeedback_Initialize`, called by the frames below at load.
"Interface\\FrameXML\\UnitFrame.xml",
"Interface\\FrameXML\\CombatFeedback.xml",
"Interface\\FrameXML\\PlayerFrame.xml",
// `PetFrame.xml`'s debuff buttons inherit `PartyBuffButtonTemplate`, declared here; the
// manifest reaches this file only through `PartyFrame.xml`'s `<Include>`.
"Interface\\FrameXML\\PartyFrameTemplates.xml",
"Interface\\FrameXML\\PetFrame.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
// Every page must exist before the window opens: `CharacterFrame_ShowSubFrame` hides each
// `CHARACTERFRAME_SUBFRAMES` page it is not showing, unguarded (`CharacterFrame.lua:25-32`).
"ScrollTemplates.xml", // our scroll kit
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The stock window tab, whose `<OnShow>` needs the `UIPanelTemplates` pair above it.
r"Interface\FrameXML\CharacterFrameTemplates.xml",
// ReputationFrame's detail check boxes inherit its `OptionsCheckButtonTemplate`.
"Interface\\FrameXML\\OptionsFrameTemplates.xml",
"Interface\\FrameXML\\CharacterFrame.xml",
"Interface\\FrameXML\\PaperDollFrame.xml",
"Interface\\FrameXML\\PetPaperDollFrame.xml",
// `updateContainerFrameAnchors`, which `ReputationWatchBar_Update` calls when the bar moves
// (`ReputationFrame.lua:248`): in the reference the bar's presence reflows the bag row.
"Interface\\FrameXML\\ContainerFrame.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"Interface\\FrameXML\\SkillFrame.xml",
"Interface\\FrameXML\\HonorFrame.xml",
// The sixth page TurtleWoW's `CharacterFrame.lua` lists and 1.12 never shipped. The realm
// ships it itself, in FrameXML (toc rows 106-110), and the subframe list is swept unguarded,
// so it must be seated before the window opens. The `.lua` leads: the window's `<OnLoad>`
// calls `ArenaFrame_OnLoad`, and the templates precede the frame that inherits them.
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUI.lua",
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUITemplates.xml",
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaFrame.xml",
];
/// The manifest slice stock `FriendsFrame.xml` and `RaidFrame.xml` reach at load or on show, in its
/// order; [`load_social_ui`] adds the raid tab's LoadOnDemand addon.
pub(super) const SOCIAL_UI: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
// The realm's own globals (toc row 2, right after `GlobalStrings.lua`). `TW_CLASS_TOKEN` is
// the localized-class-name → token map its `FriendsFrame.lua:187-192` and
// `GuildFrame.lua` colour rows through; without it those rows silently draw uncoloured, which
// is a different window than the one the client paints.
r"Interface\FrameXML\Globals.lua",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml",
r"Interface\FrameXML\UIParent.xml",
"Interface\\FrameXML\\Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
// The stock window tab, whose `<OnShow>` needs the `UIPanelTemplates` pair above it.
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\OptionsFrameTemplates.xml",
"Interface\\FrameXML\\ReputationFrame.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.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",
];
/// Load [`SOCIAL_UI`], then the raid tab's LoadOnDemand addon as the app reaches it: seated off the
/// chain and loaded by stock `RaidFrame_LoadUI`. Needs client data.
pub(super) fn load_social_ui(s: &mut UiScript) {
for f in SOCIAL_UI {
load_ui_strict(s, f);
}
// The `.lua` alone, for `GroupFrame_InitiateReadyCheck`: the realm moved the ready check out
// of `Blizzard_RaidUI` and into its own group UI (`Turtle_GroupUI.lua:259`, toc row 106-ish),
// and `RaidFrameReadyCheckButton`'s `<OnClick>` calls it by name
// (`Blizzard_RaidUI.xml:538-540`). Without it the button clicks into a nil.
load_ui(s, r"Interface\FrameXML\Turtle_GroupUI\Turtle_GroupUI.lua");
seat_chain_addon(s, "Blizzard_RaidUI");
s.run("RaidFrame_LoadUI()").unwrap();
}
/// The files a test needs before it can open a bag window, in `benilla.toc` order, trimmed to what
/// the bags reach for. Needs client data: open with `benilla_formats::wow_data_or_skip!()`.
pub(super) const BAG_UI: &[&str] = &[
// The stock strings: the bar's hovers pass them to `GameTooltip:SetText`, which raises on nil.
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
// `TEXT()`, which the backpack button's OnEnter and `BagSlotButton_OnEnter` call.
"Interface\\FrameXML\\BasicControls.xml",
// `UIParent`: the twelve `ContainerFrame`s are its children, `updateContainerFrameAnchors`
// anchors each open bag to its parent, and `OpenAllBags` returns unless `UIParent` is visible.
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml", // our scroll kit
"Interface\\FrameXML\\ItemButtonTemplate.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\Cooldown.xml",
// `MainMenuBar.xml` and its templates: the bag bar's `parent="MainMenuBarArtFrame"` resolves
// at load, and `MainMenuBar_UpdateKeyRing` puts the keyring on the bar.
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// `UpdateMicroButtons`, which the keyring's OnHide and OnShow call
// (`ContainerFrame.lua:117`, `:137`).
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The dialog engine, after the `UIPanelCloseButton` it inherits.
r"Interface\FrameXML\StaticPopup.xml",
"Interface\\FrameXML\\ContainerFrame.xml",
// `BagSlotButtonTemplate` inherits `PaperDollItemSlotButtonTemplate`, and its OnLoad
// (`PaperDollItemSlotButton_OnLoad`) gives each bag button its inventory-slot id, 20..23.
// `CharacterFrame.xml` stays out: a missing `parent=` only warns.
"Interface\\FrameXML\\PaperDollFrame.xml",
// The stock bag bar, with `BagSlotButtonTemplate` and `KEYRING_CONTAINER`.
"Interface\\FrameXML\\MainMenuBarBagButtons.xml",
// `ContainerFrameItemButton_OnClick` hides `StackSplitFrame` on every plain click
// (`ContainerFrame.lua:581`) and opens it on the shift fork.
"Interface\\FrameXML\\StackSplitFrame.xml",
// The chat edit box: the shift arm tests `ChatFrameEditBox:IsShown()`
// (`ContainerFrame.lua:568`) to choose between linking the item and splitting the stack.
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
// Our adapters over the stock container files, loaded after them and the bar so they win.
"ContainerFrameAdapters.xml",
// `updateContainerFrameAnchors` measures every open bag against `BankFrame:GetRight()`
// (`ContainerFrame.lua:505`) on every open and close, so the bank window is a hard dependency.
"Interface\\FrameXML\\BankFrame.xml",
];
/// The name of the `ContainerFrame` showing bag `id`, asked of `IsBagOpen`: the reference recycles
/// twelve windows across every container (`ContainerFrame_GetOpenFrame`).
pub(super) fn bag_window(s: &UiScript, id: i64) -> Option<String> {
s.eval::<Option<i64>>(&format!("return IsBagOpen({id})"))
.unwrap()
.map(|i| format!("ContainerFrame{i}"))
}
/// Is bag `id` open?
pub(super) fn bag_open(s: &UiScript, id: i64) -> bool {
bag_window(s, id).is_some()
}
/// The item button in bag `id`'s open window that holds game slot `slot`, found by `GetID`:
/// `ContainerFrame_GenerateFrame` numbers the buttons backwards (`index = size - j + 1`).
pub(super) fn bag_slot_button(s: &UiScript, id: i64, slot: u32) -> String {
let w = bag_window(s, id).unwrap_or_else(|| panic!("bag {id} is not open"));
s.eval::<String>(&format!(
"for j = 1, MAX_CONTAINER_ITEMS do \
local b = getglobal(\"{w}Item\"..j) \
if b and b:IsShown() and b:GetID() == {slot} then return \"{w}Item\"..j end \
end return \"\""
))
.inspect(|n| assert!(!n.is_empty(), "no {w}Item* is bag {id} slot {slot}"))
.expect("the item-button scan")
}
/// The centre of a named frame, in the y-up UI space `mouse_move`/`mouse_button` take.
pub(super) fn centre_of(s: &mut UiScript, name: &str) -> (f32, f32) {
s.resolve();
let r: Vec<f32> = s
.eval(&format!(
"local f = getglobal(\"{name}\") \
return {{ f:GetLeft(), f:GetBottom(), f:GetWidth(), f:GetHeight() }}"
))
.unwrap_or_else(|e| panic!("{name}: no resolved rect: {e}"));
assert_eq!(r.len(), 4, "{name}: unresolved rect {r:?}");
(r[0] + r[2] / 2.0, r[1] + r[3] / 2.0)
}
/// Move the mouse onto the centre of `name` through the engine's hit test and `OnEnter`: stock
/// handlers read `this`, which only the engine sets.
pub(super) fn hover(s: &mut UiScript, name: &str) {
let (x, y) = centre_of(s, name);
s.mouse_move(x, y);
}
/// Move the mouse well clear of everything: the `OnLeave` half of [`hover`].
pub(super) fn unhover(s: &mut UiScript) {
s.mouse_move(-500.0, -500.0);
}
/// Press and release `button` over the centre of `name`, the way a player's mouse does.
pub(super) fn click(s: &mut UiScript, name: &str, button: &str) {
let (x, y) = centre_of(s, name);
s.mouse_move(x, y);
s.mouse_button(x, y, button, true);
s.mouse_button(x, y, button, false);
}
/// Seat stock `WorldFrame.xml` and `MirrorTimer.xml`: `WorldFrame_OnUpdate` loops to
/// `MIRRORTIMER_NUMTIMERS`, which `MirrorTimer.lua` declares, and raises every tick without it.
/// Its other loop reads `STATICPOPUP_NUMDIALOGS`, which every caller already loads.
pub(super) fn load_world_frame(s: &UiScript) {
load_ui(s, r"Interface\FrameXML\WorldFrame.xml");
load_ui(s, r"Interface\FrameXML\MirrorTimer.xml");
}
/// A completed left click on the game world, at a point the loaded `WorldFrame` owns: searched on
/// a grid, since a harness's windows may cover any fixed point.
pub(super) fn world_click(s: &mut UiScript) {
let (x, y) = world_point(s);
s.mouse_move(x, y);
s.mouse_button(x, y, "LeftButton", true);
assert!(
s.mouse_button(x, y, "LeftButton", false),
"a world drop consumes the completed click"
);
}
/// [`world_click`]'s search.
fn world_point(s: &mut UiScript) -> (f32, f32) {
s.resolve();
let r: Vec<f32> = s
.eval(
"local f = WorldFrame \
return { f:GetLeft(), f:GetBottom(), f:GetWidth(), f:GetHeight() }",
)
.expect(
"the fixture must load Interface\\FrameXML\\WorldFrame.xml (load_world_frame): a \
world drop is a click on THAT frame, not on nothing",
);
assert_eq!(r.len(), 4, "WorldFrame: unresolved rect {r:?}");
let (steps, mut blockers) = (7, Vec::new());
for row in 0..steps {
for col in 0..steps {
let x = r[0] + r[2] * (col as f32 + 0.5) / steps as f32;
let y = r[1] + r[3] * (row as f32 + 0.5) / steps as f32;
match s.hit_test(x, y) {
Some(id) if s.is_world_frame(id) => return (x, y),
_ => blockers.push(
s.hit_test_name(x, y)
.unwrap_or_else(|| "<anonymous>".into()),
),
}
}
}
blockers.sort();
blockers.dedup();
panic!("no point on the world frame is clickable — the UI covers all {steps}x{steps} of them: {blockers:?}")
}
/// Seat one of the reference's LoadOnDemand Blizzard addons off the chain, so a harness's
/// `UIParentLoadAddOn(name)` loads it as the app does. Needs client data.
pub(super) fn seat_chain_addon(s: &mut UiScript, name: &str) {
let toc = super::reference_ui::read(&format!("Interface/AddOns/{name}/{name}.toc"))
.map(|b| benilla_ui::toc::Toc::parse(&benilla_ui::source::decode(&b)))
.unwrap_or_else(|| panic!("{name}: no toc off the chain"));
let mut info = super::addons::info_from_toc(name, &toc);
info.chain = true;
s.set_addon_chain_reader(Box::new(super::reference_ui::read));
s.register_addons(vec![info], None, None, None);
}