This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
812 lines
40 KiB
Rust
812 lines
40 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"],
|
|
),
|
|
(
|
|
"lootframe.xml",
|
|
&[r"Interface\FrameXML\ItemButtonTemplate.xml"],
|
|
),
|
|
(
|
|
"tradeframe.xml",
|
|
&[r"Interface\FrameXML\ItemButtonTemplate.xml"],
|
|
),
|
|
(
|
|
"bankframe.xml",
|
|
&[r"Interface\FrameXML\ItemButtonTemplate.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.
|
|
local function benilla_seat(names)
|
|
for _, name in ipairs(names) do
|
|
if not getglobal(name) then local f = CreateFrame("Frame") 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",
|
|
"MinimapCluster", "ChatFrame1", "ChatFrame2", "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.
|
|
local named = {}
|
|
for name, row in pairs(UIPARENT_MANAGED_FRAME_POSITIONS) do
|
|
if not row.isVar then table.insert(named, name) end
|
|
-- …and every row's ANCHOR TARGET (`anchorTo`, l.1668), which 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 name is a RAISE, so a target the kit is
|
|
-- missing aborts `UIParent_ManageFramePositions` mid-pass 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",
|
|
// Ours: the sixth page TurtleWoW's `CharacterFrame.lua` lists and 1.12 never shipped. It loads
|
|
// here for the reason it does in `benilla.toc`: the subframe list is swept unguarded.
|
|
"CharacterFrameAdapters.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",
|
|
"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);
|
|
}
|
|
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);
|
|
}
|