- the window tab is the reference's own: `CharacterFrameTemplates.xml` loads off the chain at the reference's toc l.21 — the reference never gives it a toc line, `UIOptionsFrame.xml` includes it, and ours is not the reference's options window, so the row is explicit — and our `CharacterFrameTabButtonTemplate` with its `BenillaTab_Room`/`BenillaTabButton_OnUpdate` settle is deleted, 192 lines; the stock template fits a tab ONCE, in `<OnShow>`, through `PanelTemplates_TabResize(0)`, which needs a `GetStringWidth` that answers inside the Lua call that asked — the engine's synchronous measurer is that, so the settle that re-fit from `OnUpdate` until a frame-late round-trip landed is dead weight; retired with it, the director's call: 1002's structural clamp (`benillaTabRightInset`/`benillaTabPadding`/`benillaTabMaxWidth`, a tab never past its window's drawn right edge) — the reference's only caps are the numbers each window passes `TabResize`, and nothing had set those fields since the macro window went stock; 1004's highlight law is unchanged and now the reference's own text (`LEFT +10`/`RIGHT −10`, two opposing anchors that never consult the width, so both `SetWidth` lines are dead code there and here); measured live on the frame the window opens, main's 115·115·115·115 becomes 89.8·99.3·70.8·75.6 on the character row, 77.9·66.1·70.8·66.1 on the social, 89.8·85.0 on the merchant — each its label's `GetStringWidth()` + 40 — and one frame later the two builds agree exactly, so what goes is a one-frame 115 px flash on every open of every one of those windows (1993) - the ferry seam: `tick_transports` leaves an off-map transport's transform unwritten — the reference's own freeze, `0x5f5248`'s leg map read against the current map and the return at `0x5f5279` past the pose stores — but 0455's riding worldport composes the rider's world pose as `boat.translation + boat.rotation * local_pos` from exactly that transform, so a client whose path clock had not crossed put the body at Booty Bay's coordinates on Kalimdor, which want ZERO tiles of Kalimdor's WDT: the reported `0/0 resident`, which cannot clear (`is_ready` needs `total > 0`) and cannot re-raise (`focus_resident` takes its vacuous off-grid arm); the fall is a second bug in the same window: `SelfMoveMessage` dropped the wire's `ON_TRANSPORT` tail and `apply_self_move` re-derived the deck-local pose as `world − boat.translation`, which against a frozen source-continent boat is the distance between two continents — consistent while the boat stays frozen, a ~13,000 yd fling off the deck the instant it writes an honest pose, `update_attachment` keeping the ride because the body is airborne and no settle hold to catch it; the rider's deck-local pose now comes off the wire, and `first_cycle_on_map` + `reseek_ridden_transport_at_worldport` seek our clock to the head of the destination map's leg and write the pose in the worldport frame itself, a clock that already agreed left untouched; cleared with numbers: the timetable matches vmangos's to 18 ms over path 241's 350 818 ms cycle, the period is bit-identical to the DB row, the live skew is 32 and 144 ms on the Orgrimmar zeppelin; two divergences kept knowingly — we spare the ridden transport where `SMSG_NEW_WORLD` (`0x401bc0`) destroys the whole object manager, and we hide an off-map boat the reference draws; an off-grid view focus names its map and tile once, a ridden transport whose map disagrees with `CurrentMap` for three frames warns, and the sea-crossing probe sends itself to a dock (`WOW_PROBE_DOCK=x,y,z`) because the fleet's seams are not interchangeable — one path crosses mid-cycle, another at the wrap; why the director saw 38.5 s and not a few frames is still open (N08, 2026) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
762 lines
43 KiB
Rust
762 lines
43 KiB
Rust
//! **The tests' interface loader — one copy, both stores** (decision 1751).
|
||
//!
|
||
//! Half a dozen test files carried an identical private `load_xml` that read
|
||
//! `assets/ui/<name>` off disk, parsed it, and asserted the loader reported no errors — each with
|
||
//! the comment "duplicated so this file is self-contained". That was fine while `assets/ui` was
|
||
//! the only place an interface file could come from. It is not any more: a migrated window's file
|
||
//! lives in the player's own patch chain, so a test that wants bag windows has to name
|
||
//! `Interface\FrameXML\ContainerFrame.xml`, and six copies of a disk-only reader cannot say that.
|
||
//!
|
||
//! [`load_ui`] is that reader, generalised the same way [`super::manifest::load_manifest`] is:
|
||
//! **a bare filename is a file we ship, a path is one off the player's install.** The two are told
|
||
//! apart by [`super::reference_ui::is_chain_entry`], the manifest's own rule, so a test loads the
|
||
//! entries in the order `benilla.toc` names them and gets what the client gets.
|
||
//!
|
||
//! It reads the SOURCE TREE rather than the compiled-in copy, deliberately: these tests exist to
|
||
//! catch a mistake in a file somebody just edited, and `content::read`'s dev-build probe already
|
||
//! prefers disk for the same reason.
|
||
|
||
use benilla_ui::script::{QuadContent, UiScript};
|
||
|
||
/// Load one interface file into `s`, panicking on any loader error, and return how many frames it
|
||
/// materialized (`0` for a `.lua` entry, which materializes none).
|
||
///
|
||
/// `entry` is a manifest entry: `"BagFrame.xml"` for one of ours, or
|
||
/// `"Interface\\FrameXML\\ContainerFrame.xml"` for the reference's own — which also brings its
|
||
/// `<Script file="ContainerFrame.lua"/>` off the chain, exactly as it does in a real load.
|
||
///
|
||
/// **A chain entry needs client data**, so a test that names one has to open with
|
||
/// `benilla_formats::wow_data_or_skip!()`; [`BAG_UI`] is a list that always does.
|
||
pub(crate) fn load_ui(s: &UiScript, entry: &str) -> usize {
|
||
load_entry(s, entry, false, false)
|
||
}
|
||
|
||
/// The cooldown indicator's file: `Interface\Cooldown\UI-Cooldown-Indicator.mdx`, 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`): sequence 0 = id 0, 1000 ms,
|
||
/// clamp — the sweep `CooldownFrame_OnUpdateModel` scrubs; sequence 1 = id 1, 1000 ms, clamp —
|
||
/// the finish flash, whose completion hides the frame. Handed to the engine the way the app does
|
||
/// once the asset lands (decisions 2007/2019); every pane holding the file arms its Stand.
|
||
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]),
|
||
},
|
||
);
|
||
}
|
||
|
||
/// The play head `(anim_id, cursor_ms)` of the SHOWN cooldown pane `owner` names (`None` while
|
||
/// it is hidden, or has nothing armed) — what the tile renderer samples the file at. Read the way
|
||
/// the renderer reads it: the pane's quad in the extract joined to the engine's paint list.
|
||
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 — the bag windows number their slots
|
||
/// from the far end (`ContainerFrame_GenerateFrame`: `Item{j}` carries id `size − j + 1`), so a
|
||
/// test that seats one item asks for "the" sweep rather than 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 is a failure too**.
|
||
///
|
||
/// A frame that inherits a template nothing declares is a loader *warning*, not an error: the frame
|
||
/// is built and simply has none of the template's art. So an under-loaded dependency list passes
|
||
/// [`load_ui`] and then loses a window's whole skin silently — which is why four of the social
|
||
/// windows' test modules grew this check by hand. It is one function now rather than four copies,
|
||
/// and any test may ask for it.
|
||
pub(super) fn load_ui_strict(s: &UiScript, entry: &str) -> usize {
|
||
load_entry(s, entry, true, false)
|
||
}
|
||
|
||
/// [`load_ui`], and **no loader warning of any kind is tolerated**.
|
||
///
|
||
/// Stricter than [`load_ui_strict`], which only fails a missing template. A file whose own
|
||
/// assignment is "this loads perfectly clean" wants the whole warnings channel asserted empty — a
|
||
/// stale unknown-attribute or dropped-script warning is exactly the drift that check exists to
|
||
/// catch. It was a fourth private disk-only reader in `group_loot_tests.rs` until 1838; a
|
||
/// disk-only reader cannot name a chain file, which is what that test now loads.
|
||
pub(super) fn load_ui_no_warnings(s: &UiScript, entry: &str) -> usize {
|
||
load_entry(s, entry, false, true)
|
||
}
|
||
|
||
fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bool) -> usize {
|
||
let path = entry.replace('\\', "/");
|
||
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) };
|
||
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 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 the stock micro-button row.** `UpdateMicroButtons`
|
||
/// (`MainMenuBarMicroButtons.lua:20-84`) reads ten panels, `KeyRingButton` and `IsBagOpen`
|
||
/// UNGUARDED — the reference's own contract, because in the client every one of them is up
|
||
/// before anything can be shown. A kit is a prefix of the manifest and stops wherever its window
|
||
/// does, so [`load_entry`] wraps the function the moment the row loads: on its FIRST call — a
|
||
/// panel's OnShow, by which time the kit is complete — the wrapper seats a hidden, unnamed
|
||
/// stand-in under each name the kit never declared (a hidden frame is what a closed panel
|
||
/// answers), then runs the reference's own body. First use rather than load time, and unnamed,
|
||
/// because both registries keep the first writer: the arena's name map
|
||
/// (`named_registry_is_non_overwriting`) and `_G` itself — a named frame is published
|
||
/// non-overwriting (RF-0023: a `lua_gettable` nil check on GLOBALSINDEX, which is why a
|
||
/// metatable fallback would block the real publish just the same). A stand-in seated before the
|
||
/// real declaration shadows the real frame for good; the first cut did exactly that, 126
|
||
/// failures, every tabbed panel's `numTabs` read off the stand-in. `fire_chat_login`'s
|
||
/// `UIOptionsFrame` stand-in is the same move for the chat files. Seated here rather than at the
|
||
/// kits' seventy-odd consumers because the dependency is the row's, not any one window's — and
|
||
/// the shipped manifest never needs it. `tests/common/mod.rs` carries the same chunk for the
|
||
/// integration tests, which cannot reach this module (decision 1987).
|
||
/// **What a kit owes the stock `UIParent.xml`.** Its `<OnUpdate>` calls `FCF_OnUpdate`,
|
||
/// `UnitPopup_OnUpdate` and `BattlefieldFrame_OnUpdate` unguarded (the chat, unit-menu and
|
||
/// battlefield files, far below it in the manifest), `UIParent_OnEvent`'s `PLAYER_ENTERING_WORLD`
|
||
/// arm calls `MultiActionBar_Update`, and `ShowUIPanel` calls `CloseAllBags` (the container file's).
|
||
/// A kit that stops short of those files would raise on its first tick or first shown panel, so
|
||
/// [`load_entry`] seats a no-op stand-in under each name the moment the stock file loads. Unlike
|
||
/// the micro row's frames these are FUNCTIONS: a later chunk's `function X()` overwrites a global
|
||
/// outright, so seating at load is safe and the kit's order does not matter (decision 1988).
|
||
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
|
||
-- Ours, not the reference's file: 1.12 keeps UpdateNameplates in UIOptionsFrame.lua and
|
||
-- benilla's own options window carries it (the options family stays ours).
|
||
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 (RF-0023), 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
|
||
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; ours answers all three of the
|
||
-- reference's options windows (1987) and a kit may load none of them.
|
||
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
|
||
"#;
|
||
|
||
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, from whichever store its path names — the chain for a path, this crate's
|
||
/// `assets/ui` for a bare name. Also the `<Include>` / `<Script file=>` provider, which is why it
|
||
/// takes an already-resolved path in either space.
|
||
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()
|
||
}
|
||
|
||
/// The files a test needs before it can open a bag window, in manifest order: the four templates
|
||
/// stock `ContainerFrame.xml` inherits, then the reference's own file, then our bag bar.
|
||
///
|
||
/// Named as a list rather than folded into a `setup()` because the callers all want to add their
|
||
/// own files around it (a merchant, the bank, the action bar art), and the ORDER is the thing
|
||
/// being reused — it is `benilla.toc`'s, trimmed to what the bags actually reach for.
|
||
/// What the **loot window** needs before `Interface\FrameXML\LootFrame.xml` will load and behave —
|
||
/// the same shape as [`BAG_UI`], for the same reason, and it grew for 1751 exactly as that one did.
|
||
///
|
||
/// Three of these are load-bearing in a way that is invisible if you leave them out:
|
||
///
|
||
/// * **`GlobalStrings.lua`** — the stock file concatenates `GROUP` and `GIVE_LOOT` while building
|
||
/// the master-loot menu. The app loads the player's own copy ahead of the whole manifest at VM
|
||
/// setup; a bare `UiScript::new()` does not. Our deleted `LootFrame.xml` carried
|
||
/// `X = X or "…"` fallbacks for precisely these; the stock file carries none.
|
||
/// * **`ItemButtonTemplate.xml`** — stock `LootButtonTemplate` inherits it, and a missing template
|
||
/// is a loader *warning*, not an error. Leave it out and every row loads clean with no art and
|
||
/// no `$parentIconTexture`, which reads as a pass until an assertion looks for an icon.
|
||
/// * **`PartyFrame.xml`** — for `MAX_PARTY_MEMBERS`, which stock `LootFrame.lua:217` does
|
||
/// arithmetic on at LOAD time, not at click time. Its home is the reference's own
|
||
/// (`PartyMemberFrame.lua:1`), and it wants the dropdown kit and `UnitPopup` ahead of it — the
|
||
/// manifest's order (167 → 185 → 262), reproduced rather than short-circuited. Setting the
|
||
/// constant by hand would pass and teach nothing about the real load.
|
||
///
|
||
/// Needs client data, like [`BAG_UI`]: open with `benilla_formats::wow_data_or_skip!()`.
|
||
/// What the **vendor window** needs before `Interface\FrameXML\MerchantFrame.xml` will load and
|
||
/// behave — the same shape as [`BAG_UI`] and [`LOOT_UI`], and it grew for 1751 the same way.
|
||
///
|
||
/// The two that fail in the ways worth naming:
|
||
///
|
||
/// * **`BasicControls.xml`** — for `TEXT()`, the reference's identity-function wrapper, which
|
||
/// stock `MerchantFrame.lua:70` calls while building every row. Absent, it raises on the first
|
||
/// `MerchantFrame_UpdateMerchantInfo`, which is the first thing the window does when it shows.
|
||
/// * **`Interface\FrameXML\ItemButtonTemplate.xml`** — the stock rows' `$parentItemButton`
|
||
/// inherits it, and a missing template is a loader *warning*: the rows load clean with no art.
|
||
///
|
||
/// 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 reference's own window tab (`CharacterFrameTabButtonTemplate`), whose `<OnShow>`
|
||
// fits each tab to its text — it needs the `UIPanelTemplates` pair above it (1993).
|
||
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
|
||
];
|
||
|
||
/// The stock gossip window's dependencies in `benilla.toc` order — the list
|
||
/// `panel_tests::shipped_gossip_frame_drives_end_to_end` and its siblings load by hand, shared so
|
||
/// a feed-level test in `ui_gossip` can drive the reference's own `GossipFrame.xml` (1751).
|
||
/// `UIParent.xml` brings the slot manager (`ShowUIPanel`/`HideUIPanel`) the window's OnEvent
|
||
/// calls; `ScrollTemplates.xml` is our scroll kit the greeting pane inherits — a missing template
|
||
/// is a loader *warning*, so an under-loaded list passes and silently loses the scrollbar.
|
||
///
|
||
/// Needs client data: open with `benilla_formats::wow_data_or_skip!()`.
|
||
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",
|
||
];
|
||
|
||
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 + UIParent.lua (the slot manager, the fades; 1988)
|
||
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.xml` reads ITEM_QUALITY_COLORS at FILE SCOPE for its three loot-threshold rows,
|
||
// exactly as the reference's own UnitPopup.lua:47-49 does — so its declarer has to precede it.
|
||
// That declarer is UIParent (ref UIParent.lua:65); 1888 moved the table there when Fonts.xml
|
||
// went on the chain, because the reference's Fonts.xml does not declare it.
|
||
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
|
||
"Interface\\FrameXML\\UnitPopup.xml",
|
||
// …and what its rows' OnLoad calls: every `PartyMemberFrame<N>` and its pet frame runs
|
||
// `UnitFrame_Initialize`, which lives in UnitFrame.lua and itself calls
|
||
// `SetTextStatusBarText` out of TextStatusBar.lua. Naming PartyFrame without these loads
|
||
// four rows that each raise on their own OnLoad — the loader reports it, but only because
|
||
// `load_ui_strict` looks; a plain load would have gone quiet.
|
||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||
"Interface\\FrameXML\\UnitFrame.xml",
|
||
// …and `RefreshBuffs`, which `PartyMemberFrame.lua:60` calls from each row's OnLoad. Its home
|
||
// is BuffFrame.lua, which the chain's BuffFrame.xml sources — the reference's own toc has
|
||
// BuffFrame at 40 and PartyFrame at 45 for exactly this reason.
|
||
"Interface\\FrameXML\\BuffFrame.xml",
|
||
"Interface\\FrameXML\\PartyFrame.xml",
|
||
];
|
||
|
||
/// What the **character window** needs before `Interface\FrameXML\CharacterFrame.xml`,
|
||
/// `PaperDollFrame.xml` and `PetPaperDollFrame.xml` will load and behave — the same shape as
|
||
/// [`BAG_UI`] / [`LOOT_UI`] / [`MERCHANT_UI`], and grown the same way (decision 1751).
|
||
///
|
||
/// This one is the longest of the four, and the reason is `CharacterFrame_OnLoad`: it is the only
|
||
/// migrated window whose LOAD-time body reaches outside its own file, and it reaches into four
|
||
/// others at once —
|
||
///
|
||
/// * **`Interface\FrameXML\TextStatusBar.lua`** for `SetTextStatusBarTextPrefix`, which it calls
|
||
/// three times before doing anything else. Its `.xml` twin comes too, because the pet page's XP
|
||
/// bar inherits the `TextStatusBar` template it declares and a missing template is a loader
|
||
/// *warning* — the bar would load with no art and no text region and read as a pass.
|
||
/// * **`Interface\FrameXML\PlayerFrame.xml`** for `PlayerFrameHealthBar` / `PlayerFrameManaBar`,
|
||
/// the frames those three calls name, and **`PetFrame.xml`** for `PetFrameHealthBar` /
|
||
/// `PetFrameManaBar`, which `CharacterFrame_OnShow` shows the text on. Both call
|
||
/// `UnitFrame_Initialize` in their OnLoad and `PlayerFrame` also calls `CombatFeedback_Initialize`,
|
||
/// so `UnitFrame.xml` and `CombatFeedback.xml` come first — the reference's own toc order.
|
||
/// (This entry used to be our one `UnitFrames.xml`, and this note used to say "when they are
|
||
/// migrated, this becomes the stock pair". They are; it did.)
|
||
/// * **`Interface\FrameXML\MainMenuBar.xml`** for `MainMenuExpBar` — the third bar of that prefix call — and for
|
||
/// `ShowWatchedReputationBarText` / `HideWatchedReputationBarText`, which the window's
|
||
/// show/hide pair calls.
|
||
/// * **`UIPanelTemplates.xml`** for `PanelTemplates_SetNumTabs`/`_SetTab`, the last two lines of
|
||
/// that same OnLoad.
|
||
///
|
||
/// And two more that only bite later, which is exactly why they are written down:
|
||
///
|
||
/// * **`Interface\FrameXML\HonorFrame.xml`** — `PaperDollFrame_SetLevel` and `_SetGuild` write
|
||
/// `HonorLevelText` and `HonorGuildText` "while we're at it" (`PaperDollFrame.lua:100`/`:120`).
|
||
/// Nothing touches them at load, so leaving this out loads clean and then raises the first time
|
||
/// the window is SHOWN. It sits below the character block in the manifest for the same reason.
|
||
/// * **`MainMenuBarMicroButtons.xml`** — `UpdateMicroButtons` (called by both
|
||
/// `CharacterFrame_OnShow` and `_OnHide`) and `MicroButtonTooltipText` (all five tab hovers).
|
||
/// A tab hover is the only thing that reaches the second one, so its absence is invisible
|
||
/// until a test hovers a tab. The reference's own file since 1987; the panels its
|
||
/// `UpdateMicroButtons` reads unguarded and this kit stops short of are
|
||
/// [`MICRO_BUTTON_STAND_INS`]'s.
|
||
///
|
||
/// Needs client data, like its three siblings: open with `benilla_formats::wow_data_or_skip!()`.
|
||
pub(super) const CHARACTER_UI: &[&str] = &[
|
||
// The reference's own localized strings. The stock file has no `X = X or "…"` fallbacks —
|
||
// `PaperDollFrame_OnLoad` sets seven labels from them at LOAD, and `PaperDollFrame_SetStats`
|
||
// concatenates `SPELL_STAT0_NAME`..`4` on every repaint.
|
||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||
"Interface\\FrameXML\\Fonts.xml",
|
||
// `GetText` — the reference's gendered-string helper, which stock
|
||
// `ReputationFrame.lua:65` calls for every row's standing label. Manifest line 94
|
||
// (1875).
|
||
r"Interface\FrameXML\LocaleProperties.lua",
|
||
"Interface\\FrameXML\\BasicControls.xml", // TEXT(), which every one of those label sets goes 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 panes' 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' four right-click dropdowns call `UIDropDownMenu_Initialize` at LOAD, so the
|
||
// kit and the menu table it initialises from both precede them — the manifest's own order
|
||
// (175 → 189 → 193 → 263).
|
||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||
"Interface\\FrameXML\\UIMenu.xml",
|
||
"Interface\\FrameXML\\UnitPopup.xml",
|
||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||
// `UnitFrame_Initialize` and `CombatFeedback_Initialize`, which the two windows below call in
|
||
// their OnLoad. Neither file declares a frame; both are pure script.
|
||
"Interface\\FrameXML\\UnitFrame.xml",
|
||
"Interface\\FrameXML\\CombatFeedback.xml",
|
||
"Interface\\FrameXML\\PlayerFrame.xml",
|
||
// `PetFrame.xml`'s four `PetFrameBuff*` inherit `PartyBuffButtonTemplate`, which lives here.
|
||
// The manifest never names this file: it arrives through `PartyFrame.xml`'s
|
||
// `<Include file="PartyFrameTemplates.xml"/>`, and the reference's own toc puts PartyFrame (45)
|
||
// ahead of TargetFrame (46) and PetFrame (47) for exactly that reason. This kit wants the
|
||
// template and not four party member frames, so it takes the included file directly.
|
||
"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",
|
||
// The two page files these three need before the window can be OPENED, which is not the same
|
||
// as before it can load: `CHARACTERFRAME_SUBFRAMES` lists all five pages by name and
|
||
// `CharacterFrame_ShowSubFrame` calls `getglobal(value):Hide()` on each one it is not showing
|
||
// (`CharacterFrame.lua:25-32`), unguarded. A missing page is `attempt to index a nil value` on
|
||
// the very first `ToggleCharacter` — load-clean, then dead on the first click. Their own
|
||
// template dependencies come with them.
|
||
"ScrollTemplates.xml", // SkillFrame's faux list + trough
|
||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||
// The reference's own window tab (`CharacterFrameTabButtonTemplate`), whose `<OnShow>`
|
||
// fits each tab to its text — it needs the `UIPanelTemplates` pair above it (1993).
|
||
r"Interface\FrameXML\CharacterFrameTemplates.xml",
|
||
// The four options templates off the chain, then ours for the one it does not carry
|
||
// (`UIOptionsCheckButtonTemplate` — decision 1841).
|
||
"Interface\\FrameXML\\OptionsFrameTemplates.xml",
|
||
"OptionsFrameTemplates.xml", // ReputationFrame's detail check boxes
|
||
"Interface\\FrameXML\\CharacterFrame.xml",
|
||
"Interface\\FrameXML\\PaperDollFrame.xml",
|
||
"Interface\\FrameXML\\PetPaperDollFrame.xml",
|
||
// `updateContainerFrameAnchors` — `ReputationWatchBar_Update` hard-calls it when the bar
|
||
// moves (ReputationFrame.lua:248), because in the reference the bar's presence reflows the
|
||
// bag row. It comes with `ContainerFrame.xml`, manifest 585 against the pane's 693 (1875).
|
||
"Interface\\FrameXML\\ContainerFrame.xml",
|
||
r"Interface\FrameXML\ReputationFrame.xml",
|
||
"Interface\\FrameXML\\SkillFrame.xml",
|
||
"Interface\\FrameXML\\HonorFrame.xml",
|
||
];
|
||
|
||
/// The social window's slice of the manifest (1959): everything the reference's
|
||
/// `FriendsFrame.xml` + `RaidFrame.xml` reach at load or on show — `TEXT` and `GetText`, the
|
||
/// bar chain under `UpdateMicroButtons`, the chat window whose edit box `FriendsFrame_SendMessage`
|
||
/// opens, the unit menu, the party frames `RaidFrame_OnLoad` reconciles — in the manifest's own
|
||
/// order; the raid tab's LoadOnDemand addon follows through [`load_social_ui`]. Shared by the
|
||
/// friends, guild and raid harnesses.
|
||
pub(super) const SOCIAL_UI: &[&str] = &[
|
||
"Interface\\FrameXML\\Fonts.xml",
|
||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||
"Interface\\FrameXML\\LocaleProperties.lua",
|
||
"Interface\\FrameXML\\BasicControls.xml",
|
||
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 reference's own window tab (`CharacterFrameTabButtonTemplate`), whose `<OnShow>`
|
||
// fits each tab to its text — it needs the `UIPanelTemplates` pair above it (1993).
|
||
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 the way the app reaches it: seated
|
||
/// off the chain as a registry row (1957) and loaded by the reference's own `RaidFrame_LoadUI`
|
||
/// (UIParent.xml; 1967). Needs client data — the caller has checked with `wow_data_or_skip!`.
|
||
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();
|
||
}
|
||
|
||
pub(super) const BAG_UI: &[&str] = &[
|
||
// The reference's own localized strings — `BACKPACK_TOOLTIP`, `EQUIP_CONTAINER`, `KEYRING`,
|
||
// the `*_FONT_COLOR_CODE` pair. The app loads this at VM setup, ahead of the manifest
|
||
// (`ui_script/mod.rs`, `setup_script`); a test VM has to say so itself. Not optional since
|
||
// 1751's third window: stock `MainMenuBarBagButtons.lua`'s hovers pass these straight into
|
||
// `GameTooltip:SetText`, and `SetText(nil)` raises rather than showing an empty plate. Our
|
||
// deleted `BagFrame.xml` carried `X = X or "…"` fallbacks for exactly this gap; the real file
|
||
// is the better answer, and these tests already gate on the install.
|
||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||
"Interface\\FrameXML\\Fonts.xml",
|
||
// `TEXT()` — the reference's own identity-function wrapper, which stock
|
||
// `MainMenuBarBackpackButton`'s OnEnter calls (`GameTooltip:SetText(TEXT(BACKPACK_TOOLTIP)…)`)
|
||
// and `BagSlotButton_OnEnter` calls for `EQUIP_CONTAINER`. Manifest entry 3, and not optional
|
||
// for the bag bar since 1751's third window made that bar the reference's own.
|
||
"Interface\\FrameXML\\BasicControls.xml",
|
||
// `UIParent` itself: the twelve `ContainerFrame`s declare `parent="UIParent"`, and
|
||
// `updateContainerFrameAnchors` anchors each open bag to `frame:GetParent()` while
|
||
// `OpenAllBags` opens with `if not UIParent:IsVisible() then return end`. Without it the
|
||
// windows fall out of the cascade and the reference's own layout pass has nothing to measure.
|
||
r"Interface\FrameXML\UIParent.xml",
|
||
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
|
||
"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",
|
||
// The bag BAR declares `parent="MainMenuBarArtFrame"`, resolved at LOAD — so without this the
|
||
// six buttons fall back to UIParent and sit at a level no production run ever puts them at.
|
||
// It also carries `MainMenuBar_UpdateKeyRing`, which is what 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` — the KEYRING's own OnShow/OnHide calls it (ContainerFrame.lua l.117,
|
||
// l.137), because in the reference the keyring's existence moves the micro-button row.
|
||
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
|
||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||
// The dialog engine, after the UIPanelCloseButton it inherits (1960).
|
||
r"Interface\FrameXML\StaticPopup.xml",
|
||
"Interface\\FrameXML\\ContainerFrame.xml",
|
||
// `PaperDollItemSlotButtonTemplate` and the `PaperDollItemSlotButton_*` family behind it,
|
||
// which every bag button inherits and runs — resolved at load, so this has to precede the bar
|
||
// exactly as it does in the manifest. Stock `BagSlotButtonTemplate`'s OnLoad *is*
|
||
// `PaperDollItemSlotButton_OnLoad()`, which gives each bag button its inventory-slot id
|
||
// (20..23 via `GetInventorySlotInfo`), its six event registrations and its first paint.
|
||
//
|
||
// The whole paper-doll file, because that is where the reference declares both — our
|
||
// `ItemSlotButtonTemplates.xml` held a transcribed copy of the template only because our own
|
||
// character window loaded too late to declare it, and it is deleted (decision 1751). Its
|
||
// companion `Interface\\FrameXML\\CharacterFrame.xml` is deliberately NOT here: this list is
|
||
// the bags, `PaperDollFrame` only names `CharacterFrame` in `parent=` (a missing parent is a
|
||
// loader warning, not an error), and `CharacterFrame_OnLoad` would drag in the unit frames,
|
||
// the XP bar and the text-status-bar file for a window no bag test opens.
|
||
"Interface\\FrameXML\\PaperDollFrame.xml",
|
||
// The bag BAR itself, the reference's own since 1751's third window: MainMenuBarBackpackButton,
|
||
// CharacterBag0..3Slot, KeyRingButton, `BagSlotButtonTemplate`, and `KEYRING_CONTAINER`.
|
||
"Interface\\FrameXML\\MainMenuBarBagButtons.xml",
|
||
// `StackSplitFrame` is not optional either: the reference's own
|
||
// `ContainerFrameItemButton_OnClick` calls `StackSplitFrame:Hide()` on EVERY plain click
|
||
// (ContainerFrame.lua l.581) before the pickup, and opens it on the shift fork.
|
||
"Interface\\FrameXML\\StackSplitFrame.xml",
|
||
// …nor is the chat edit box. The reference's SHIFT arm opens with
|
||
// `if ( ChatFrameEditBox:IsShown() )` (ContainerFrame.lua l.569) to decide between posting the
|
||
// item's link and splitting the stack, so a VM without it raises before either.
|
||
"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 reference's container files — the keyring tooltip wrapper and the
|
||
// three bag verbs 0561 shadows (`OpenBackpack`/`CloseBackpack`/`CloseAllBags`). It has to be
|
||
// AFTER `ContainerFrame.xml` and after the bar, which is why it is here and not up with
|
||
// UiPanels.xml.
|
||
"ContainerFrameAdapters.xml",
|
||
// `updateContainerFrameAnchors` measures every open bag against `BankFrame:GetRight()`
|
||
// (ContainerFrame.lua l.505) on EVERY open and close, so the bank window is not optional
|
||
// scenery for a bag test — it is a hard dependency of the reference's own layout pass. It is
|
||
// one in the real client too; the manifest just satisfies it far below the bags. The
|
||
// reference's own file since 1751's second window.
|
||
"Interface\\FrameXML\\BankFrame.xml",
|
||
];
|
||
|
||
/// The name of the `ContainerFrame` currently showing bag `id`, or `None` if it is not open.
|
||
///
|
||
/// **Ask, never assume.** The reference recycles twelve windows across every container
|
||
/// (`ContainerFrame_GetOpenFrame`), so which one a bag lands in depends on what else is open —
|
||
/// there is no `BenillaBagFrame2` to name any more, and pinning `ContainerFrame3` in a test would
|
||
/// pin a coincidence. `IsBagOpen` is the reference's own published scan.
|
||
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? [`bag_window`]'s predicate half.
|
||
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`.
|
||
///
|
||
/// Asked of the buttons' own `GetID`, never derived: `ContainerFrame_GenerateFrame` numbers them
|
||
/// backwards (`index = size - j + 1`, so `…Item1` is the bag's LAST slot, bottom-right), and a
|
||
/// window generated for a different bag size numbers them differently.
|
||
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` — the whole engine path (hit test → `OnEnter`), never
|
||
/// `s.run("Handler(button)")`.
|
||
///
|
||
/// **This is not a style preference any more.** The reference's own handlers read `this`
|
||
/// (`ContainerFrameItemButton_OnClick(button, ignoreModifiers)` takes the MOUSE button as its first
|
||
/// argument and gets the frame from `this`), and only the engine sets `this`. A migrated window's
|
||
/// tests therefore drive the mouse, which is also the stronger test — it puts the
|
||
/// `RegisterForClicks` gate and the template's own script wiring under test.
|
||
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 one of the reference's LoadOnDemand Blizzard addons off the chain, so a harness's
|
||
/// `UIParentLoadAddOn(name)` can load it the way the app does (1957; the combat text since
|
||
/// 1964). Needs client data — the caller has already checked with `wow_data_or_skip!`.
|
||
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);
|
||
}
|