druids get their relic slot, and the loot window is the reference's own
- `UnitHasRelicSlot` is real in 1.12 and benilla had no binding for it: `0x519e50` reads `ChrClasses.dbc` field 16 — set for Paladin, Shaman and Druid in `patch.MPQ`'s 17-column copy (the base `dbc.MPQ` copy has 16 columns and no such field, which is how the "always false in vanilla" belief survived twice in decisions and seven times in code) — and stock `PaperDollFrame.lua` calls it unconditionally, so the character sheet would have raised for EVERY class; a druid, paladin or shaman's sheet now hides the ammo slot, shows the relic plate where the bow was, tooltips "Relic", and reports ranged Attack/Damage/Power as N/A, all four branches transcribed from the reference - the loot window is the reference's own `LootFrame.xml`: `LootButton` is built as a real widget type rather than aliased (a Button plus one slot dword and one overridden OnClick — right-click loots exactly like left, no shift/ctrl/alt takes, a row whose Lua handler errored still loots, a fast double-click takes two slots), `LOOT_SLOT_CLEARED` fires per row again because the stock file hangs hide-in-place AND the emptied-page auto-advance off it, `EditBox:Insert(nil)` is a no-op as `lua_tostring` makes it, and an alias `GameTooltip.xml` had quietly become a reader of would have turned every item tooltip white, silently, had it just gone nil - the vendor cursor: `PickupMerchantItem` is two verbs behind one name, decided by what the cursor holds and never by the caller — a held bag item SELLS (index ignored, no merchant-open gate), otherwise the vendor row is grabbed as cursor mode 5, which is invisible to `CursorHasItem` (modes 1 and 9 only) so all thirteen stock gates stay closed while it is held; the drop is the buy, `CMSG_BUY_ITEM_IN_SLOT`, count 1, into a bag or an equipment slot, and every refusal is silent and clears the cursor; the four `Model_*` rotation handlers land in UIParent where the reference keeps them - the gap report reads `:Method(` sites now — it found `SetMerchantCompareItem`, which would have raised on the first vendor-row hover with every check green, and `LootButton:SetSlot`, and 28 "free" windows became 25; the shared coin kit moved out of the merchant window into `MoneyFrame.xml` so the eight windows that draw coins through it can reach it without loading a vendor Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -65,11 +65,14 @@ fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
// LootFrame.xml owns GroupLootDropDown, whose OnLoad calls UIDropDownMenu_Initialize — the
|
||||
// shipped manifest loads the dropdown kit far ahead of it (benilla.toc l.110 vs 478), and the
|
||||
// kit's backdrop reads TOOLTIP_DEFAULT_COLOR out of GameTooltip.xml (already in BAG_UI).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
// The loot window is the reference's own since 1751, and its dependency list grew with the
|
||||
// swap — `LOOT_UI` carries the why for each entry. Loaded whole rather than cherry-picked:
|
||||
// `PartyFrame.xml` is needed at LOAD (MAX_PARTY_MEMBERS) and skipping it fails loudly, but
|
||||
// `ItemButtonTemplate` is only a warning and skipping it fails silently.
|
||||
for file in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_Set, BankFrame's purse helper
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
|
||||
@@ -1161,6 +1161,7 @@ pub(super) fn drive_script(
|
||||
CursorPayload::Macro(m) => m.texture,
|
||||
CursorPayload::PetAction(p) => p.texture,
|
||||
CursorPayload::StablePet(p) => Some(p.texture),
|
||||
CursorPayload::Merchant(m) => m.texture,
|
||||
});
|
||||
if let (Some(texture), Some(pos)) = (texture, window.cursor_position()) {
|
||||
if let Some(handle) = assets
|
||||
|
||||
@@ -264,10 +264,13 @@ fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() {
|
||||
&[],
|
||||
&[
|
||||
"MerchantFrame.xml",
|
||||
// LootFrame.xml owns GroupLootDropDown, whose OnLoad needs the dropdown kit — which
|
||||
// in turn reads TOOLTIP_DEFAULT_COLOR from GameTooltip.xml (BAG_UI's, ahead of this).
|
||||
// The loot window is the reference's own since 1751 — `test_ui::LOOT_UI` carries
|
||||
// what it needs and why, and PartyFrame's `MAX_PARTY_MEMBERS` is needed at LOAD.
|
||||
"UIDropDownMenu.xml",
|
||||
"LootFrame.xml",
|
||||
"UnitPopup.xml",
|
||||
"PartyFrame.xml",
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\LootFrame.xml",
|
||||
],
|
||||
);
|
||||
s.set_money(0);
|
||||
@@ -592,7 +595,12 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
|
||||
"ScrollTemplates.xml",
|
||||
"WorldMapFrame.xml",
|
||||
"MerchantFrame.xml",
|
||||
"LootFrame.xml",
|
||||
// The loot window is the reference's own since 1751 — see `test_ui::LOOT_UI` for what
|
||||
// each of these buys; `PartyFrame`'s MAX_PARTY_MEMBERS is needed at LOAD time.
|
||||
"UnitPopup.xml",
|
||||
"PartyFrame.xml",
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\LootFrame.xml",
|
||||
]);
|
||||
s.run("ToggleWorldMap()").unwrap();
|
||||
|
||||
|
||||
@@ -92,17 +92,17 @@ fn coin_and_two_items() -> LootState {
|
||||
fn shipped_loot_frame_drives_end_to_end() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml"); // ITEM_QUALITY_COLORS (LootFrame's palette), app load order
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// LootFrame.xml owns GroupLootDropDown, whose OnLoad calls UIDropDownMenu_Initialize —
|
||||
// the shipped manifest loads the dropdown kit far ahead of it (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, which the dropdown backdrop reads
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
assert_eq!(
|
||||
load_xml(&s, "LootFrame.xml"),
|
||||
10,
|
||||
"window + 4 rows + up + down + close + GroupLootDropDown and its template's own $parentButton"
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml"),
|
||||
34,
|
||||
"the STOCK file's own shape (1751): the window, its portrait overlay and Next/Prev art, \
|
||||
four LootButton rows each carrying ItemButtonTemplate's sub-frames, the two pagers, the \
|
||||
close button, and GroupLootDropDown with the dropdown template's own children. Our \
|
||||
transcription materialized 10 — a count is a fingerprint of a file, not a property of a \
|
||||
loot window, and this one is now the reference's"
|
||||
);
|
||||
|
||||
// Hidden by default: no coin icon on screen, left slot empty.
|
||||
@@ -133,7 +133,9 @@ fn shipped_loot_frame_drives_end_to_end() {
|
||||
.unwrap();
|
||||
assert_eq!(vis, (true, true, true, false), "coin + 2 items, 4th hidden");
|
||||
// Only 3 items ⇒ no pager.
|
||||
assert!(!s.eval::<bool>("return LootDownButton:IsVisible()").unwrap());
|
||||
assert!(!s
|
||||
.eval::<bool>("return LootFrameDownButton:IsVisible()")
|
||||
.unwrap());
|
||||
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
@@ -240,6 +242,14 @@ fn shipped_loot_frame_drives_end_to_end() {
|
||||
let mut coin_looted = coin_and_two_items();
|
||||
coin_looted.rows[0] = None;
|
||||
s.set_loot(Some(coin_looted));
|
||||
// The app's own pair, in the app's own order (`ui_loot`): the per-slot event first — the
|
||||
// stock file hangs "hide the button in place" off it and only off it — then the summary
|
||||
// repaint. Firing LOOT_UPDATE alone leaves the looted row drawn, which is what a client that
|
||||
// sent only the summary would actually show.
|
||||
s.fire_event(
|
||||
"LOOT_SLOT_CLEARED",
|
||||
vec![benilla_ui::script::ScriptValue::Int(1)],
|
||||
);
|
||||
s.fire_event("LOOT_UPDATE", vec![]);
|
||||
let vis2: (bool, bool, bool) = s
|
||||
.eval(
|
||||
@@ -254,7 +264,7 @@ fn shipped_loot_frame_drives_end_to_end() {
|
||||
);
|
||||
|
||||
// The close button hides the window → OnHide → CloseLoot() queues the release intent.
|
||||
s.run("BenillaLootCloseButton_OnClick()").unwrap();
|
||||
s.run("LootCloseButton:Click()").unwrap();
|
||||
assert!(
|
||||
s.take_loot_close(),
|
||||
"closing the window releases the loot (OnHide → CloseLoot)"
|
||||
@@ -275,14 +285,10 @@ fn shipped_loot_frame_drives_end_to_end() {
|
||||
fn loot_empty_roll_plays_the_empty_open_kit() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml"); // ITEM_QUALITY_COLORS (LootFrame's palette), app load order
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// LootFrame.xml owns GroupLootDropDown, whose OnLoad calls UIDropDownMenu_Initialize —
|
||||
// the shipped manifest loads the dropdown kit far ahead of it (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, which the dropdown backdrop reads
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
|
||||
// A normal, non-empty loot open queues no sound (the normal open kit is C-side).
|
||||
s.set_loot(Some(coin_and_two_items()));
|
||||
@@ -320,14 +326,10 @@ fn loot_empty_roll_plays_the_empty_open_kit() {
|
||||
fn fishing_loot_open_plays_the_reel_and_swaps_the_portrait() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml"); // ITEM_QUALITY_COLORS (LootFrame's palette), app load order
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// LootFrame.xml owns GroupLootDropDown, whose OnLoad calls UIDropDownMenu_Initialize —
|
||||
// the shipped manifest loads the dropdown kit far ahead of it (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, which the dropdown backdrop reads
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
let has_icon = |quads: &[ExtractedQuad], needle: &str| {
|
||||
quads.iter().any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p.contains(needle))
|
||||
@@ -375,14 +377,10 @@ fn fishing_loot_open_plays_the_reel_and_swaps_the_portrait() {
|
||||
fn shipped_loot_frame_pages_five_items() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml"); // ITEM_QUALITY_COLORS (LootFrame's palette), app load order
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// LootFrame.xml owns GroupLootDropDown, whose OnLoad calls UIDropDownMenu_Initialize —
|
||||
// the shipped manifest loads the dropdown kit far ahead of it (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, which the dropdown backdrop reads
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
|
||||
let rows: Vec<Option<LootRow>> = (0..5)
|
||||
.map(|i| {
|
||||
@@ -415,7 +413,7 @@ fn shipped_loot_frame_pages_five_items() {
|
||||
.unwrap();
|
||||
assert_eq!(page1, (true, true, true, false), "page 1 shows 3 rows");
|
||||
let pager1: (bool, bool) = s
|
||||
.eval("return LootUpButton:IsVisible(), LootDownButton:IsVisible()")
|
||||
.eval("return LootFrameUpButton:IsVisible(), LootFrameDownButton:IsVisible()")
|
||||
.unwrap();
|
||||
assert_eq!(pager1, (false, true), "page 1: Up hidden, Down shown");
|
||||
|
||||
@@ -429,13 +427,17 @@ fn shipped_loot_frame_pages_five_items() {
|
||||
.unwrap();
|
||||
assert_eq!(page2, (true, true, false), "page 2 shows the last 2 rows");
|
||||
let pager2: (bool, bool) = s
|
||||
.eval("return LootUpButton:IsVisible(), LootDownButton:IsVisible()")
|
||||
.eval("return LootFrameUpButton:IsVisible(), LootFrameDownButton:IsVisible()")
|
||||
.unwrap();
|
||||
assert_eq!(pager2, (true, false), "page 2: Up shown, Down hidden");
|
||||
|
||||
// Back on page 1, loot all three of its rows: their buttons hide in place, and the emptied
|
||||
// page advances to page 2 on its own — the ref's LOOT_SLOT_CLEARED tail (LootFrame.lua
|
||||
// l.38-50: every button hidden + Down visible → PageDown).
|
||||
//
|
||||
// The events are the app's own, in the app's own order: one LOOT_SLOT_CLEARED per row that
|
||||
// went away (`ui_loot`'s emitter), then the summary LOOT_UPDATE. The auto-advance hangs off
|
||||
// the per-slot event and ONLY off it, which is why the app fires both.
|
||||
s.run("LootFrame_PageUp()").unwrap();
|
||||
let mut cleared = rows;
|
||||
cleared[0] = None;
|
||||
@@ -446,6 +448,12 @@ fn shipped_loot_frame_pages_five_items() {
|
||||
master_candidates: Vec::new(),
|
||||
rows: cleared,
|
||||
}));
|
||||
for row in 1..=3 {
|
||||
s.fire_event(
|
||||
"LOOT_SLOT_CLEARED",
|
||||
vec![benilla_ui::script::ScriptValue::Int(row)],
|
||||
);
|
||||
}
|
||||
s.fire_event("LOOT_UPDATE", vec![]);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return LootFrame.page").unwrap(),
|
||||
@@ -466,14 +474,10 @@ fn shipped_loot_frame_pages_five_items() {
|
||||
fn shipped_loot_pushed_to_center_by_merchant() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml"); // ITEM_QUALITY_COLORS (LootFrame's palette), app load order
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// LootFrame.xml owns GroupLootDropDown, whose OnLoad calls UIDropDownMenu_Initialize —
|
||||
// the shipped manifest loads the dropdown kit far ahead of it (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, which the dropdown backdrop reads
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
|
||||
@@ -546,15 +550,10 @@ fn shipped_loot_pushed_to_center_by_merchant() {
|
||||
fn the_loot_window_draws_over_the_party_frames() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
// PartyFrame's per-member dropdown OnLoad walks the whole popup kit (app manifest order).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "UnitPopup.xml");
|
||||
load_xml(&s, "PartyFrame.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
|
||||
// The party frame up FIRST, the window second — the order that cannot be what saves it.
|
||||
s.eval::<()>("PartyMemberFrame1:Show()").unwrap();
|
||||
@@ -605,14 +604,12 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
|
||||
const WOOL_LINK: &str = "|cffffffff|Hitem:2589:0:0:0|h[Wool Cloth]|h|r";
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml", // BenillaChatEdit_InsertLink, the shared shift-insert helper
|
||||
"GameTooltip.xml", // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
|
||||
"UIDropDownMenu.xml", // LootFrame owns GroupLootDropDown, initialized at its OnLoad
|
||||
"LootFrame.xml",
|
||||
"Interface\\FrameXML\\LootFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
@@ -747,12 +744,10 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
|
||||
fn shipped_loot_frame_hands_a_master_row_to_a_candidate() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
|
||||
let row = |name: &str, quality: u32| {
|
||||
Some(LootRow {
|
||||
@@ -891,12 +886,10 @@ fn the_master_loot_menu_groups_raid_candidates_by_subgroup() {
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
|
||||
// A raid, so the dropdown takes its nested arm (GetNumRaidMembers() > 0 is the whole gate).
|
||||
s.set_party(PartyState {
|
||||
@@ -974,12 +967,10 @@ fn the_master_loot_menu_groups_raid_candidates_by_subgroup() {
|
||||
fn the_loot_bind_confirm_raises_the_dialog_and_okay_calls_loot_slot() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
s.set_loot(Some(coin_and_two_items()));
|
||||
s.fire_event("LOOT_OPENED", vec![]);
|
||||
let _ = s.take_loot_picks();
|
||||
@@ -1063,12 +1054,10 @@ fn the_loot_bind_confirm_raises_the_dialog_and_okay_calls_loot_slot() {
|
||||
fn a_row_click_takes_rather_than_continues() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
s.set_loot(Some(coin_and_two_items()));
|
||||
s.fire_event("LOOT_OPENED", vec![]);
|
||||
let quads = s.extract();
|
||||
|
||||
@@ -566,11 +566,13 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
// The quality→colour table (BENILLA_LOOT_QUALITY_COLORS) ships in LootFrame.xml; at runtime
|
||||
// The quality→colour table is `ITEM_QUALITY_COLORS`, and Fonts.xml is its home — this used
|
||||
// to load the whole loot window for `BENILLA_LOOT_QUALITY_COLORS`, an alias our own
|
||||
// LootFrame.xml set to that same table. 1751 took that window to the chain and the alias went
|
||||
// with it (GameTooltip.xml reads the real name now), so the dependency is gone. At runtime
|
||||
// every FrameXML file loads before any hover fires, so load it here too — with the dropdown
|
||||
// kit its GroupLootDropDown initializes against at load (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
|
||||
s.set_merchant(Some(MerchantState {
|
||||
|
||||
@@ -977,10 +977,11 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
// LootFrame.xml is here for BENILLA_LOOT_QUALITY_COLORS (the quality→colour table); it
|
||||
// This used to load LootFrame.xml for `BENILLA_LOOT_QUALITY_COLORS`, an alias our own copy of
|
||||
// that window set to `ITEM_QUALITY_COLORS`. 1751 retired the window and the alias with it, so
|
||||
// Fonts.xml alone is the source now. Formerly: it
|
||||
// needs the dropdown kit its GroupLootDropDown initializes against (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "LootFrame.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
|
||||
@@ -389,11 +389,28 @@ mod tests {
|
||||
/// mostly noise. Here the manifest is loaded first and `_G` is read after, so the answer is
|
||||
/// what this client *actually* answers to.
|
||||
///
|
||||
/// Two crudenesses, stated because they decide how to read the output. The `Name(` scan counts
|
||||
/// **Three columns, and the third one exists because its absence cost a swap.** `engine=` and
|
||||
/// `fx=` read bare `Name(` globals. `method=` reads `:Name(` calls against the method surface
|
||||
/// this engine actually exposes — built by walking `getmetatable(w).__index` over one widget
|
||||
/// of every type, plus the tooltip.
|
||||
///
|
||||
/// Until 1787 the method sites were **silently dropped**: a name in neither the engine nor the
|
||||
/// FrameXML half of `1.12-globals.tsv` was assumed to be a widget method and skipped, on the
|
||||
/// reasoning that widget methods are not in `_G`. True, and it meant the report could not see
|
||||
/// a widget method we had *not built*. `MerchantFrame.xml` read `0 engine` and
|
||||
/// [`chain_readiness_report`] read CLEAN while the stock row's `<OnEnter>` called
|
||||
/// `ShoppingTooltip1:SetMerchantCompareItem(...)`, which this engine does not have — so the
|
||||
/// file loaded, every check passed, and hovering a vendor row would have raised in play. Both
|
||||
/// instruments were right about what they measure. Neither measured the window.
|
||||
///
|
||||
/// Two crudenesses remain, stated because they decide how to read the output. Both scans count
|
||||
/// a call inside an XML comment (1.12 comments out whole blocks — `GuildRegistrarFrame`'s
|
||||
/// tabard button is one), so it can over-report; and it cannot see a name reached through
|
||||
/// `getglobal`, so it can under-report. It is for ranking work, not for proving a window done —
|
||||
/// [`chain_readiness_report`] and the window's own tests are that.
|
||||
/// tabard button is one), so they can over-report; and neither can see a name reached through
|
||||
/// `getglobal`, so they can under-report. The `method=` column is also receiver-blind: it asks
|
||||
/// "does ANY widget type answer to this name", not "does *this* receiver", so a method that
|
||||
/// exists on the wrong type still reads as present. It is for ranking work, not for proving a
|
||||
/// window done — [`chain_readiness_report`] and the window's own tests are that, and the
|
||||
/// paragraph above is what those two are worth on their own.
|
||||
#[test]
|
||||
#[ignore = "instrument: run by hand when choosing what to build next"]
|
||||
fn chain_gap_report() {
|
||||
@@ -427,6 +444,94 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// `:Name(` — a method call, receiver unknown.
|
||||
let called_methods = |text: &str| -> std::collections::HashSet<String> {
|
||||
let b: Vec<char> = text.chars().collect();
|
||||
let mut out = std::collections::HashSet::new();
|
||||
let mut i = 1;
|
||||
while i < b.len() {
|
||||
// `::` is not a method call, and neither is a `:` inside a word.
|
||||
if b[i - 1] == ':' && (i < 2 || b[i - 2] != ':') && b[i].is_ascii_alphabetic() {
|
||||
let mut j = i;
|
||||
while j < b.len() && super::is_word(b[j]) {
|
||||
j += 1;
|
||||
}
|
||||
let mut k = j;
|
||||
while k < b.len() && b[k] == ' ' {
|
||||
k += 1;
|
||||
}
|
||||
if k < b.len() && b[k] == '(' {
|
||||
out.insert(b[i..j].iter().collect::<String>());
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
// Whether this engine's widgets answer to a method name — **asked**, not enumerated. A
|
||||
// widget's methods come through an `__index` FUNCTION, so there is no table to walk; the
|
||||
// only way to know is to look the name up on a real widget. Receiver-blind by design (a
|
||||
// `:Name(` site does not say what it is called on), so this answers "does ANY widget type
|
||||
// answer to this name", which is the question that catches a method we never built.
|
||||
let answers = |s: &UiScript, names: &[String]| -> std::collections::HashSet<String> {
|
||||
let list = names
|
||||
.iter()
|
||||
.map(|n| format!("{n:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
s.eval::<Vec<String>>(&format!(
|
||||
r#"
|
||||
local want = {{{list}}}
|
||||
local probes = {{ GameTooltip }}
|
||||
local types = {{
|
||||
"Frame", "Button", "CheckButton", "LootButton", "StatusBar", "EditBox",
|
||||
"ScrollFrame",
|
||||
"Slider", "ColorSelect", "MessageFrame", "ScrollingMessageFrame",
|
||||
"SimpleHTML", "Model", "PlayerModel", "DressUpModel", "TabardModel",
|
||||
"Minimap", "MovieFrame", "Cooldown",
|
||||
}}
|
||||
for i = 1, table.getn(types) do
|
||||
local ok, w = pcall(function()
|
||||
return CreateFrame(types[i], "BenillaGapProbe" .. i, UIParent)
|
||||
end)
|
||||
if ok and w then table.insert(probes, w) end
|
||||
end
|
||||
local host = CreateFrame("Frame", "BenillaGapProbeHost", UIParent)
|
||||
table.insert(probes, host:CreateTexture())
|
||||
table.insert(probes, host:CreateFontString())
|
||||
local out = {{}}
|
||||
for i = 1, table.getn(want) do
|
||||
local name, found = want[i], false
|
||||
for j = 1, table.getn(probes) do
|
||||
local ok, v = pcall(function() return probes[j][name] end)
|
||||
if ok and type(v) == "function" then found = true break end
|
||||
end
|
||||
if found then table.insert(out, name) end
|
||||
end
|
||||
return out
|
||||
"#
|
||||
))
|
||||
.expect("probe the widget method surface")
|
||||
.into_iter()
|
||||
.collect()
|
||||
};
|
||||
// The instrument's own tripwire: if the probe stops working, the `method=` column goes
|
||||
// silently empty — which is precisely the failure mode 1787 exists to end.
|
||||
let control: Vec<String> = ["SetPoint", "SetMerchantItem", "BenillaNotAMethod"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_string())
|
||||
.collect();
|
||||
let got = answers(&s, &control);
|
||||
assert!(
|
||||
got.contains("SetPoint")
|
||||
&& got.contains("SetMerchantItem")
|
||||
&& !got.contains("BenillaNotAMethod"),
|
||||
"the method probe is not working — it answered {got:?} for {control:?}"
|
||||
);
|
||||
|
||||
let migrated: std::collections::HashSet<String> = super::super::addons::Addon::builtin()
|
||||
.toc
|
||||
.files
|
||||
@@ -493,7 +598,8 @@ mod tests {
|
||||
};
|
||||
|
||||
println!("\n=== 1751 gap report — what each unmigrated window would cost ===");
|
||||
let mut rows: Vec<(usize, String, Vec<String>, Vec<String>)> = Vec::new();
|
||||
#[allow(clippy::type_complexity)] // (engine count, file, engine, fx, method)
|
||||
let mut rows: Vec<(usize, String, Vec<String>, Vec<String>, Vec<String>)> = Vec::new();
|
||||
for f in &stock {
|
||||
if migrated.contains(f) {
|
||||
continue;
|
||||
@@ -526,24 +632,38 @@ mod tests {
|
||||
let h = home.get(&c).cloned().unwrap_or_else(|| "?".into());
|
||||
fx.push(format!("{c}<{h}>"));
|
||||
}
|
||||
None => {} // a widget method, not a global — the reference's _G would have it
|
||||
None => {} // not a global at all — the `:Name(` scan below is what sees it
|
||||
}
|
||||
}
|
||||
rows.push((eng.len(), f.clone(), eng, fx));
|
||||
// The method half. `own`/`have` do not apply: a method is never a global, so the only
|
||||
// question is whether this engine's widgets answer to the name.
|
||||
let mut asked: Vec<String> = called_methods(&text).into_iter().collect();
|
||||
asked.sort();
|
||||
let known = answers(&s, &asked);
|
||||
let mut meth: Vec<String> = asked.into_iter().filter(|m| !known.contains(m)).collect();
|
||||
meth.sort();
|
||||
rows.push((eng.len() + meth.len(), f.clone(), eng, fx, meth));
|
||||
}
|
||||
rows.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
|
||||
for (n, f, eng, fx) in &rows {
|
||||
for (n, f, eng, fx, meth) in &rows {
|
||||
println!("{n:>3} engine {f}");
|
||||
if !eng.is_empty() {
|
||||
println!(" engine: {}", eng.join(" "));
|
||||
}
|
||||
if !meth.is_empty() {
|
||||
println!(" method: {}", meth.join(" "));
|
||||
}
|
||||
if !fx.is_empty() {
|
||||
println!(" fx: {}", fx.join(" "));
|
||||
}
|
||||
}
|
||||
// "Free" now means free of BOTH — the count above is engine names plus missing methods,
|
||||
// because a window blocked on a widget method is exactly as blocked as one missing a
|
||||
// global, and reading it as free is the mistake 1787 is about.
|
||||
let free: Vec<&String> = rows.iter().filter(|r| r.0 == 0).map(|r| &r.1).collect();
|
||||
println!(
|
||||
"\n=== {} windows need NO engine work at all ===",
|
||||
"\n=== {} windows need NO engine work at all (no missing global AND no missing \
|
||||
method) ===",
|
||||
free.len()
|
||||
);
|
||||
for f in free {
|
||||
|
||||
@@ -91,6 +91,37 @@ fn read(req: &str) -> Option<Vec<u8>> {
|
||||
/// 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!()`.
|
||||
pub(super) const LOOT_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml", // ITEM_QUALITY_COLORS — the row-name palette
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml", // StaticPopup, and the LOOT_BIND / CONFIRM_LOOT_DISTRIBUTION dialogs
|
||||
"GameTooltip.xml", // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
|
||||
"UIDropDownMenu.xml", // GroupLootDropDown's OnLoad calls UIDropDownMenu_Initialize
|
||||
"UnitPopup.xml",
|
||||
"PartyFrame.xml",
|
||||
];
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user