the action bars are the reference's own, and a gift can be wrapped

- the action-bar cluster is the reference's own: `MainMenuBar.xml`, `ActionBarFrame.xml`, `MultiActionBars.xml` and `BonusActionBarFrame.xml` run off the player's own chain, and our `ActionBar.xml` (2047 lines), `MultiBars.xml` and `StanceBar.xml` are deleted — three, since the stock bonus-bar file declares the shapeshift bar too; one engine verb, `ChangeActionBarPage`, fires `ACTIONBAR_PAGE_CHANGED` and nothing else; one load blocker: stock `MultiActionBars.lua` writes the options window's table from its OnLoad, the reference's own load-order hack, so our options window moves to the reference's toc slot; and ten things the reference does that ours did not: an empty button is HIDDEN while showgrid is 0, the bonus overlay sits one pixel right of the main buttons, every slide takes one frame more than its time, and `ActionButton_ShowGrid` ghosts EVERY ring, occupied included (1938)
- a gift can be wrapped: right-clicking wrapping paper fell through to plain use and sent `CMSG_USE_ITEM`, so nothing happened — you could receive a gift and never give one; the wrap is a displayed-cursor MODE plus a stashed slot, never a payload, so `CursorHasItem()` stays false and every stock gate on it stays shut: right-click arms it, a left-click on a bag slot sends `CMSG_WRAP_ITEM`, and the client applies no eligibility test — the server's `ERR_CANT_WRAP_*` reasons are the error line (1934); a duration-limited item counts down: `SMSG_ITEM_TIME_UPDATE` parks a deadline and the tooltip's `ITEM_DURATION` line runs the enchant countdown's bucket ladder, so "Duration: 1 days" is what the reference renders; an update for an item we do not hold is DROPPED, no queue (1933); and a renamed crowd-control refusal names its mechanic — "Can't do that while stunned" — off `SpellMechanic.dbc` (1943)
- a creature paths on a boat: `SMSG_MONSTER_MOVE_TRANSPORT` carries the transport's guid and deck-local coordinates, and is the attach/detach edge; a deck sample lands on the rider or nowhere, never as a world write a few yards from the map origin; and "there is no terrain under a boat" turned out wrong: the ground probe admits GameObject meshes — a boat's `.wmo` is a GameObject, so the probe hits the deck (1936); `MSG_MOVE_TIME_SKIPPED` is read, not dropped — the reference adds the lag to the observed unit's last-wire stamp, so dropping it scheduled their next packet `lag` ms late, a hitch for every player who stalled zoning in near us; and our own settle hold reports once (1935); and the tradeskill tool order was backwards — focus FIRST, so every two-tool recipe had read "Blacksmith Hammer, Anvil" for the reference's "Anvil, Blacksmith Hammer"; the focus's `hasTool` is 1.0 in the reference too (1942)
- the crowd's second sitting, instruments first: the probe line carries the tail — cpu p95/p99 and the six worst frames — because a mean under budget hides the one frame in twenty over it (raid legs railed at 16.67 with p95 at 18–20 ms) and whether that frame was work or a wait is the first question about a stutter, and the visible meshes are counted by lane; then two cuts — a terrain tile furnishes as 4×4 cells of 4×4 chunks, one mesh, one entity, one draw per 133-yd cell instead of per 33-yd chunk (a city horizon is over a thousand chunks a frame, and a cell changes nothing a pixel can see; Stormwind 10.55 → 10.32 `cpu_ms`), and an authored point light is our own `WorldPointLight`, not Bevy's `PointLight`, which was only a data carrier while Bevy ran its whole light lane over every lamp, ~2 % of the alone frame; the sitting's record is 1945, and the code says so (1945)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:54:27 +07:00
co-authored by Claude Fable 5.1
parent 72e78ebc92
commit 2f7cfcabf5
107 changed files with 3407 additions and 3490 deletions
@@ -18,11 +18,25 @@ fn shipped_action_bar_drives_end_to_end() {
// `load_ui` returns the same `report.frames` the disk reader asserted on, so this
// count is the one that always stood here — moved, not re-derived.
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
let frames = super::test_ui::load_ui(&s, "ActionBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
let frames = super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml")
+ super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml")
+ super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
assert_eq!(
frames, 59,
"bar + XP StatusBar (+ its numerals overlay) + exhaustion tick + max-level rail + art frame + 12 buttons (each with a Cooldown child) + 2 page buttons + the performance meter and its hover button, \
+ BonusActionBarFrame and its 12 buttons with their Cooldown children (25 — hidden, as the reference's is; decision 1223) — and NO ReputationWatchBar: its three frames went home to the reference's own ReputationFrame.xml with 1875"
frames, 80,
"what the three stock files declare (1938): MainMenuBar.xml's 8 — the bar, the XP StatusBar, \
the overlay frame, the max-level rail, the art frame, the performance bar and its button, \
the exhaustion tick; ActionBarFrame.xml's 14 — 12 ActionButtons and the 2 page arrows; \
BonusActionBarFrame.xml's 24 — the bonus frame with its 12 buttons and the shapeshift frame \
with its 10; plus one $parentCooldown per action, bonus and shapeshift button (12 + 12 + 10). \
Ours built 59 for the same seats: no shapeshift bar in the file (StanceBar.xml's), no \
overlay frame, no performance-bar button"
);
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
@@ -60,13 +74,20 @@ fn shipped_action_bar_drives_end_to_end() {
}),
);
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
// The app fires this on the offset's edge (ui_action/feed.rs); the stock bonus frame shows on
// it and slides up over the main bar for BONUSACTIONBAR_SLIDETIME (0.15 s) — one OnUpdate
// paints the start, the next lands it.
s.fire_event("UPDATE_BONUS_ACTIONBAR", vec![]);
s.tick(10.0);
s.tick(0.2);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// Button 1 paints action 73's icon seated in the art frame's first well: the 1024-wide bar
// centers at BOTTOM of the 1024-wide screen ⇒ bar/art-frame left edge = (1024-1024)/2 = 0.
// Button1 anchors the art frame's BOTTOMLEFT +(8,4), 36×36 ⇒ x[8,44] y[4,40]; the icon fills
// the button (owner-sized) ⇒ the same rect. The chain stride is 36 + 6 = 42, so button i's left
// edge is 8 + (i-1)*42.
// The stance page is painted by BonusActionButton1/2, not by the main bar (1897/1938). The
// 1024-wide bar centers at BOTTOM of the 1024-wide screen ⇒ bar left edge = 0; the bonus
// frame lands at the bar's BOTTOMLEFT + (BONUSACTIONBAR_XPOS 4, BONUSACTIONBAR_YPOS 43), is
// 43 high, and its button 1 sits at its own BOTTOMLEFT + (5, 4), 36×36 ⇒ x[9,45] y[4,40] —
// one pixel right of the main bar's (8, 4). The reference's own geometry, off
// BonusActionBarFrame.xml:54-96; the chain stride is 36 + 6 = 42.
s.resolve();
let quads = s.extract();
let icon = |path: &str| {
@@ -75,11 +96,12 @@ fn shipped_action_bar_drives_end_to_end() {
.find(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == path))
.and_then(|q| q.rect)
};
let r = icon("Interface\\Icons\\Ability_SteelMelee").expect("button 1 icon");
assert_eq!((r.left, r.bottom, r.right, r.top), (8.0, 4.0, 44.0, 40.0));
let r2 = icon("Interface\\Icons\\Ability_Rogue_Ambush").expect("button 2 icon");
assert_eq!(r2.left, 8.0 + 42.0); // button 2 left = 50
// Twelve quickslot rings (every button draws its NormalTexture), two icons only.
let r = icon("Interface\\Icons\\Ability_SteelMelee").expect("bonus button 1 icon");
assert_eq!((r.left, r.bottom, r.right, r.top), (9.0, 4.0, 45.0, 40.0));
let r2 = icon("Interface\\Icons\\Ability_Rogue_Ambush").expect("bonus button 2 icon");
assert_eq!(r2.left, 9.0 + 42.0); // bonus button 2 left = 51
// Only an OCCUPIED button is shown — an empty one hides while showgrid == 0
// (ActionButton.lua:69-70) — so two rings, two icons.
let rings = quads
.iter()
.filter(|q| {
@@ -87,7 +109,10 @@ fn shipped_action_bar_drives_end_to_end() {
if p.contains("UI-Quickslot2"))
})
.count();
assert_eq!(rings, 12);
assert_eq!(
rings, 2,
"the two occupied bonus buttons; every empty main and bonus slot is hidden"
);
let icons = quads
.iter()
.filter(|q| {
@@ -97,8 +122,8 @@ fn shipped_action_bar_drives_end_to_end() {
.count();
assert_eq!(icons, 2, "empty slots draw no icon quad");
// A physical click on button 1 queues UseAction(73) — the stance page's id, not 1. Button 1's
// center is (8+18, 4+18) = (26, 22).
// A physical click at (26, 22) lands on BonusActionButton1 (9..45 × 4..40, two frame levels
// above the hidden main slot) and queues UseAction(73) — the stance page's id, not 1.
s.mouse_button(26.0, 22.0, "LeftButton", true);
s.mouse_button(26.0, 22.0, "LeftButton", false);
assert_eq!(action_ids(&mut s), vec![73]);
@@ -124,9 +149,10 @@ fn shipped_action_bar_drives_end_to_end() {
s.run("ActionButtonDown(2)").unwrap();
assert_eq!(depressed(&s), 1, "key DOWN shows the pushed texture");
assert_eq!(
s.eval::<String>("return ActionButton2:GetButtonState()")
s.eval::<String>("return BonusActionButton2:GetButtonState()")
.unwrap(),
"PUSHED"
"PUSHED",
"with the overlay up a key drives the BONUS button (ActionButton.lua:15-22)"
);
s.run("ActionButtonUp(2)").unwrap();
assert_eq!(action_ids(&mut s), vec![74], "key '2' fires action 74");
@@ -135,6 +161,10 @@ fn shipped_action_bar_drives_end_to_end() {
// Stance drops (offset 0): the bar re-pages to actions 1..12 — all empty here, icons clear.
s.set_bonus_bar_offset(0);
s.fire_event("UPDATE_BONUS_ACTIONBAR", vec![]);
// The overlay descends carrying the old form's page (`lastBonusBar`) and hides at the end
// of its slide — one OnUpdate more than BONUSACTIONBAR_SLIDETIME (paint, then advance).
s.tick(0.2);
s.tick(0.01);
s.resolve();
let icons_after = s
.extract()
@@ -151,7 +181,16 @@ fn shipped_action_bar_drives_end_to_end() {
fn load_action_bar(s: &UiScript) {
super::test_ui::load_ui(s, "Cooldown.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
super::test_ui::load_ui(s, "ActionBar.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\TextStatusBar.lua");
super::test_ui::load_ui(s, "Interface\\FrameXML\\TextStatusBar.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\Fonts.xml");
super::test_ui::load_ui(s, "UIParent.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(s, "MoneyFrame.xml");
super::test_ui::load_ui(s, "GameTooltip.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
@@ -164,6 +203,14 @@ fn load_action_bar(s: &UiScript) {
super::test_ui::load_ui(s, r"Interface\FrameXML\UIPanelTemplates.xml");
super::test_ui::load_ui(s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
super::test_ui::load_ui(s, r"Interface\FrameXML\ReputationFrame.xml");
// The options window: `LOCK_ACTIONBAR` and `ALWAYS_SHOW_MULTIBARS` are declared there, as the
// reference declares them in UIOptionsFrame_Init (1938) — and the manifest loads it before
// the bars.
super::test_ui::load_ui(s, "UiPanels.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\UIDropDownMenu.xml");
super::test_ui::load_ui(s, "ScrollTemplates.xml");
super::test_ui::load_ui(s, "KeyBindingsPage.xml");
super::test_ui::load_ui(s, "OptionsFrame.xml");
}
/// The state/feedback layer (decision 0137 phase 4) through the REAL shipped XML: a pushed
@@ -454,7 +501,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
s.resolve();
s.run(r#"LOCK_ACTIONBAR = "1""#).unwrap();
s.run("BenillaActionButton_OnDragStart(ActionButton1)")
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnDragStart\")()")
.unwrap();
assert!(
s.cursor_payload().is_none(),
@@ -480,7 +527,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
);
// The receiving end is guarded too: the held action cannot be dropped back by a drag…
s.run("BenillaActionButton_OnReceiveDrag(ActionButton1)")
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnReceiveDrag\")()")
.unwrap();
assert!(
s.cursor_payload().is_some(),
@@ -488,7 +535,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
);
// …and unlocking makes both ends live again.
s.run(r#"LOCK_ACTIONBAR = "0""#).unwrap();
s.run("BenillaActionButton_OnReceiveDrag(ActionButton1)")
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnReceiveDrag\")()")
.unwrap();
assert!(s.cursor_payload().is_none(), "unlocked, the drop lands");
assert_eq!(
@@ -779,7 +826,16 @@ fn shipped_bag_frame_drives_end_to_end() {
let frames = load_ui(&s, file);
if *file == "Cooldown.xml" {
load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_ui(&s, "ActionBar.xml");
load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
load_ui(&s, "UIParent.xml");
load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_ui(&s, "MoneyFrame.xml");
load_ui(&s, "GameTooltip.xml");
load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
}
if *file == "Interface\\FrameXML\\MainMenuBarBagButtons.xml" {
bar_frames = frames;
@@ -803,6 +859,7 @@ fn shipped_bag_frame_drives_end_to_end() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -1112,7 +1169,20 @@ fn the_bonus_action_bar_exists_hidden_and_takes_layout_calls() {
.unwrap();
}
// A hidden bar changes nothing about the visible one.
// A hidden bar changes nothing about the visible one. (An EMPTY main-bar button is itself
// hidden under the reference — `ActionButton_Update` hides a slot with no action while
// `showgrid == 0`, ActionButton.lua:69-70 — so slot 1 is occupied first.)
s.set_action(
1,
Some(ActionSlot {
texture: Some("Interface\\Icons\\Ability_SteelMelee".into()),
kind: 0x00,
action: 100,
count: 0,
consumable: false,
}),
);
s.fire_event("ACTIONBAR_SLOT_CHANGED", vec![ScriptValue::Int(1)]);
assert!(
s.eval::<bool>("return ActionButton1:IsShown()").unwrap(),
"the main bar is untouched"
@@ -1193,7 +1263,7 @@ fn both_reference_action_button_templates_are_inheritable() {
// zBar's exact shape: inherit the bar template, supply your own OnLoad.
let doc = benilla_ui::framexml::parse(
r#"<Ui>
<CheckButton name="ZLikeButton" inherits="ActionBarButtonTemplate" id="1">
<CheckButton name="ZLikeButton" inherits="ActionBarButtonTemplate" parent="UIParent" id="1">
<Anchors><Anchor point="CENTER"/></Anchors>
</CheckButton>
<CheckButton name="BareLikeButton" inherits="ActionButtonTemplate" id="1">
@@ -1279,7 +1349,14 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -1288,7 +1365,15 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"MultiBars.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
] {
// `test_ui::load_ui`, not a disk read: this list names chain entries now (the reputation
// pane and the templates it inherits through), and `assets/ui` cannot answer for those.
@@ -1378,14 +1463,31 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
reference's own asymmetry with page-down, which rescans instead"
);
// A bonus page outranks the paged one entirely: with an offset up, the page is ignored.
// The bonus branch is GUARDED by the page (ActionButton.lua:447: `button.isBonus and
// CURRENT_ACTIONBAR_PAGE == 1`) — and a MAIN-bar button has no isBonus at all, so it never
// takes that branch: the bonus offset is BonusActionBarFrame's own twelve buttons' business
// (1897, adopted with 1938). Main-bar slot 1 on page 3 is action 25 whatever the offset.
s.run("CURRENT_ACTIONBAR_PAGE = 3").unwrap();
s.set_bonus_bar_offset(1);
assert_eq!(
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
.unwrap(),
25,
"a main-bar button follows its page, never the bonus offset (ActionButton.lua:447-456)"
);
s.run("CURRENT_ACTIONBAR_PAGE = 1").unwrap();
assert_eq!(
s.eval::<i64>("return ActionButton_GetPagedID(BonusActionButton1)")
.unwrap(),
73,
"bonus offset 1 is action 73, whatever page the main bar is on"
"the bonus frame's own button, on page 1 with offset 1, is 72 + 1"
);
s.run("CURRENT_ACTIONBAR_PAGE = 3").unwrap();
assert_eq!(
s.eval::<i64>("return ActionButton_GetPagedID(BonusActionButton1)")
.unwrap(),
25,
"and off page 1 even the bonus button follows the page: the conjunct 1897 found missing"
);
s.set_bonus_bar_offset(0);
assert_eq!(
@@ -1410,7 +1512,16 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
s.set_screen_size(1024.0, 768.0);
super::test_ui::load_ui(&s, "Cooldown.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
super::test_ui::load_ui(&s, "ActionBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
@@ -1474,12 +1585,34 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
.into_iter()
.map(|u| (u.action, u.on_self))
.collect::<Vec<_>>(),
vec![(73, true)],
"ActionButtonUp's onSelf reaches UseAction's third argument"
vec![(73, false)],
"the BONUS branch of stock ActionButtonUp passes a literal 0 for onSelf (ActionButton.lua:38) \
— in a stance, ALT-1 drives the bonus page without self-cast; the reference's own rule, \
which 1745's expectation of (73, true) had smoothed over"
);
s.run("BonusActionBarFrame:Hide() ActionButtonDown(1) ActionButtonUp(1, 1)")
.unwrap();
assert_eq!(
s.take_action_uses()
.into_iter()
.map(|u| (u.action, u.on_self))
.collect::<Vec<_>>(),
vec![(1, true)],
"on the main-bar branch onSelf reaches UseAction's third argument (ActionButton.lua:51)"
);
s.run("BonusActionBarFrame:Show()").unwrap();
// Half the slide: the replica is half-risen (top = 0.5 * 43 over the bar's bottom edge at
// y=0), the paint suppression still holds, still silent.
// The slide, frame by frame. Stock `BonusActionBar_OnUpdate` paints the position the timer
// has REACHED and then advances it (BonusActionBarFrame.lua:37-49), so the first OnUpdate
// after a Show paints the start (top = 0 over the bar's bottom edge at y=0), the second the
// half-risen replica (0.5 * 43), and the one that finds the timer past BONUSACTIONBAR_SLIDETIME
// lands it. Silent until then.
s.tick(0.075);
let top = s
.eval::<f64>("return BonusActionBarFrame:GetTop()")
.unwrap();
assert!(top.abs() < 0.01, "first frame top = {top}, want 0");
assert!(s.take_sounds().is_empty());
s.tick(0.075);
let top = s
.eval::<f64>("return BonusActionBarFrame:GetTop()")
@@ -1495,8 +1628,10 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
);
assert!(s.take_sounds().is_empty());
// The landing edge: snap to 43, THE sound, and the main bar adopts the bonus page.
s.tick(0.08);
// The landing edge: snap to 43 and THE sound. The main bar does NOT adopt the bonus page:
// a main-bar button has no `isBonus`, so `ActionButton_GetPagedID` keeps it on its page and the
// overlay IS the stance page (ActionButton.lua:447-456; 1897, adopted with 1938).
s.tick(0.01);
assert_eq!(
s.take_sounds(),
vec![benilla_ui::script::SoundRequest::KitName(
@@ -1519,8 +1654,8 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
assert_eq!(
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
.unwrap(),
73,
"the main bar adopts the bonus page at landing"
1,
"the main bar keeps its page under the landed overlay"
);
s.tick(0.5);
assert!(s.take_sounds().is_empty(), "a landed bar never re-sounds");
@@ -1540,7 +1675,8 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
assert_eq!(
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
.unwrap(),
97
1,
"the main bar stays on its page through a form swap"
);
s.tick(0.2);
assert!(
@@ -1571,7 +1707,11 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
// stand-in while the frame is still shown.
s.run("ActionButtonDown(1) ActionButtonUp(1)").unwrap();
assert_eq!(action_ids(&mut s), vec![97]);
// Paint-then-advance again: the frame that finds the timer past the slide time is the one
// that hides (BonusActionBarFrame.lua:51-66), so the descent takes one OnUpdate more than
// the time itself.
s.tick(0.2);
s.tick(0.01);
assert!(
!s.eval::<bool>("return BonusActionBarFrame:IsShown()")
.unwrap(),
@@ -1593,7 +1733,16 @@ fn bonus_bar_turnaround_continues_from_position() {
s.set_screen_size(1024.0, 768.0);
super::test_ui::load_ui(&s, "Cooldown.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
super::test_ui::load_ui(&s, "ActionBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
@@ -1620,15 +1769,28 @@ fn bonus_bar_turnaround_continues_from_position() {
"hide"
);
// A third of the way back down from the turnaround point: 43 * (0.5 - 0.03/0.15).
// `HideBonusActionBar` keeps a running timer (it resets it only when `completed`,
// BonusActionBarFrame.lua:86-88), so the descent starts where the rise had got to: the rise's
// one OnUpdate advanced the timer to 0.075 (half), and the hide arm paints (1 − 0.075/0.15) · 43
// = 21.5 on its first frame, then 12.9 on the next — paint, then advance.
s.tick(0.03);
let top = s
.eval::<f64>("return BonusActionBarFrame:GetTop()")
.unwrap();
assert!(
(top - 21.5).abs() < 0.6,
"turnaround descends from 21.5, top = {top}"
);
s.tick(0.03);
let top = s
.eval::<f64>("return BonusActionBarFrame:GetTop()")
.unwrap();
assert!(
(top - 12.9).abs() < 0.6,
"turnaround descends from 21.5, top = {top}"
"next frame top = {top}, want ~12.9"
);
s.tick(0.2);
s.tick(0.01);
assert!(!s
.eval::<bool>("return BonusActionBarFrame:IsShown()")
.unwrap());
@@ -1652,7 +1814,16 @@ fn the_page_arrows_do_not_steal_each_other_s_clicks() {
s.set_screen_size(1024.0, 768.0);
super::test_ui::load_ui(&s, "Cooldown.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
super::test_ui::load_ui(&s, "ActionBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
+14 -4
View File
@@ -415,6 +415,7 @@ fn bag_tooltip_hangs_left_when_the_slot_sits_in_the_right_half() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -491,6 +492,7 @@ fn hovered_bag_tooltip_fills_itself_when_the_stats_land() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -594,6 +596,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -615,6 +618,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
slots.insert(
2,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -798,6 +802,7 @@ fn drag_across_two_slots_queues_the_same_move_a_click_pickup_would() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -881,6 +886,7 @@ fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -1013,6 +1019,7 @@ fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -1454,6 +1461,7 @@ fn the_bar_bag_buttons_name_themselves_on_hover() {
// SetInventoryItem arm. Bar slot 1 is inventory slot 20 (Bag0Slot).
let mut inv: benilla_ui::script::InventorySlots = Default::default();
inv[20] = Some(benilla_ui::script::InvSlotView {
duration_ms: None,
already_bound: false,
bar_placeable: true,
durability: None,
@@ -1635,13 +1643,15 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
bar_right - perf_right
);
// And it reverts: destroying the last key takes the keyring back off the bar (our divergence
// from the ref's one-way saved-variable latch — see MainMenuBar_UpdateKeyRing).
// And it does NOT revert: the reference's latch is one-way — `MainMenuBar_UpdateKeyRing`
// only ever Shows, and SHOW_KEYRING is a saved variable (stock MainMenuBar.lua:174-183,
// MainMenuBar.xml:323-336). Our file used to take the button away with the last key; that
// divergence went with the file (1938).
s.set_has_key(false);
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(-2)]);
assert!(
!s.eval::<bool>("return KeyRingButton:IsShown()").unwrap(),
"losing the last key takes the button away again"
s.eval::<bool>("return KeyRingButton:IsShown()").unwrap(),
"losing the last key leaves the button on the bar — the reference's one-way latch"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
+13 -9
View File
@@ -40,15 +40,19 @@ fn harness() -> UiScript {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON (the unknown-icon fallback)
// The timer switch, PLANTED ON — not the shipped value. 1.12 declares it in
// UIOptionsFrame.lua (default "0"); we have no counterpart to that file, so it lives with the
// row that drives it (OptionsFrame.xml), where it shipped "1" from 0255/1139 until 1804 put
// it back on the reference's "0". These tests are about the timer text and the geometry it
// buys, so the harness turns it on the way the Interface page's row does. Order matters: the
// reference's `BuffFrame_OnLoad` calls `BuffButtons_UpdatePositions`, which seats the debuff
// row 20px differently depending on this value, so setting it afterwards leaves the bar laid
// out for the wrong one.
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON (the unknown-icon fallback)
// The timer switch, PLANTED ON — not the shipped value. 1.12 declares it in
// UIOptionsFrame.lua (default "0"); we have no counterpart to that file, so it lives with the
// row that drives it (OptionsFrame.xml), where it shipped "1" from 0255/1139 until 1804 put
// it back on the reference's "0". These tests are about the timer text and the geometry it
// buys, so the harness turns it on the way the Interface page's row does. Order matters: the
// reference's `BuffFrame_OnLoad` calls `BuffButtons_UpdatePositions`, which seats the debuff
// row 20px differently depending on this value, so setting it afterwards leaves the bar laid
// out for the wrong one.
s.run("SHOW_BUFF_DURATIONS = \"1\"").unwrap();
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
// …and APPLIED, the way the app applies it (`manifest::apply_buff_durations`).
@@ -412,6 +412,14 @@ fn managed_positions_track_the_bottom_bar_stack() {
assert_eq!(bottom(&s, "ChatFrame1"), 102.0, "85 + bottomLeft 17");
// The stance bar shows (the warrior at login): the pet term, plus chat's both-flags extra.
// The pass's shapeshift-appearance arm (the reference's own, UIParent.lua:1705-1732 — in ours
// since 1938) touches the bar's three shelf textures by name, unguarded as the reference has
// it; a stand-in frame needs stand-in textures.
s.run(
"local t = { Show = function() end, Hide = function() end } \
ShapeshiftBarLeft, ShapeshiftBarMiddle, ShapeshiftBarRight = t, t, t",
)
.unwrap();
s.run("ShapeshiftBarFrame = { IsShown = function() return true end, SetPoint = function() end, ClearAllPoints = function() end }; UIParent_ManageFramePositions()")
.unwrap();
s.resolve();
@@ -68,6 +68,7 @@ fn combat_stats() -> UnitCombatStats {
fn inventory_with_head_item() -> InventorySlots {
let mut slots: InventorySlots = Default::default();
slots[1] = Some(InvSlotView {
duration_ms: None,
already_bound: false,
bar_placeable: true,
durability: None,
@@ -94,6 +95,7 @@ fn backpack_with_fitting_helm() -> benilla_ui::script::ContainerState {
slots.insert(
1,
benilla_ui::script::ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -315,6 +315,7 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -22,6 +22,7 @@ fn one_item_backpack(item_id: u32, name: &str, quality: u32) -> ContainerState {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -24,6 +24,7 @@ fn one_item_backpack() -> ContainerState {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -244,6 +245,7 @@ fn escape_closes_an_open_stack_split_frame() {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -23,7 +23,10 @@ fn exp_bar_harness() -> UiScript {
"Interface\\FrameXML\\TextStatusBar.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -63,7 +66,8 @@ fn the_xp_bar_takes_the_mouse_and_explains_itself() {
"the XP strip must be mouse-enabled or the hover never fires"
);
s.run("BenillaExpBar_OnEnter(MainMenuExpBar)").unwrap();
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnEnter\")()")
.unwrap();
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap(),
@@ -81,7 +85,8 @@ fn the_xp_bar_takes_the_mouse_and_explains_itself() {
"the default-corner anchor"
);
s.run("BenillaExpBar_OnLeave()").unwrap();
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnLeave\")()")
.unwrap();
assert!(
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"leaving hides the plate"
@@ -301,7 +306,8 @@ fn the_xp_bar_numerals_show_on_hover() {
s.set_player_xp(1234, 5678);
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.run("BenillaExpBar_OnEnter(MainMenuExpBar)").unwrap();
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnEnter\")()")
.unwrap();
assert_eq!(
s.eval::<String>("return MainMenuBarExpText:GetText()")
.unwrap(),
@@ -315,7 +321,7 @@ fn the_xp_bar_numerals_show_on_hover() {
// `this.isZero` on one line where every other line reads `bar` (TextStatusBar.lua:95 — the
// reference's own slip, benign there because `this` is always SOME frame during dispatch, and
// a nil index only from bare Lua). Called from the bar's own `<OnLeave>` in play.
s.run("this = MainMenuExpBar BenillaExpBar_OnLeave(MainMenuExpBar) this = nil")
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnLeave\")() this = nil")
.unwrap();
assert!(
!s.eval::<bool>("return MainMenuBarExpText:IsShown()")
@@ -338,7 +344,8 @@ fn the_rest_state_line_joins_the_held_open_tooltip() {
// 1.12's default posture (0661): detailed newbie tips ON.
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
s.run("BenillaExpBar_OnEnter(MainMenuExpBar)").unwrap();
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnEnter\")()")
.unwrap();
assert_eq!(
s.eval::<f64>("return ExhaustionTick.timer").unwrap(),
1.0,
@@ -1509,7 +1509,7 @@ fn convert_entry(
// A slice that runs PAST the texture is the reference's tiling idiom, not a crop:
// `SetTexCoord(0, n, 0, 1)` on an n-slots-wide strip repeats the art n times — the
// stance shelf's middle carries one slot per extra form exactly that way
// (`StanceBar.xml`, `ShapeshiftBar_Update`). Clamp-sampled it smears the last column
// (stock `ShapeshiftBar_Update`). Clamp-sampled it smears the last column
// across the extra width instead. Clamp/repeat bake into the `Image`, so this picks
// the tiled GPU image + cache entry, as the `Backdrop` arm's `tile` does below.
let tiled = uv
@@ -667,7 +667,16 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
&[
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
],
&["Interface\\FrameXML\\MerchantFrame.xml"],
);
@@ -758,6 +767,7 @@ fn backpack() -> ContainerState {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -49,7 +49,7 @@ fn text_color(quads: &[ExtractedQuad], t: &str) -> Option<[f32; 4]> {
/// `UIParent.xml` comes with it because the `START_LOOT_ROLL` router lives there now — the
/// reference's own slot for it — where our file used to carry a dedicated hidden driver frame.
fn load_group_loot(s: &UiScript) {
load_xml(s, "UIParent.xml");
// `UIParent.xml` is loaded by `setup()` above — the stock main bar needs it first (1938).
// `LootFrame.xml` brings the whole loot window, and its `GroupLootDropDown` calls
// `UIDropDownMenu_Initialize` from its own OnLoad — the dropdown kit is on the chain too.
load_xml(s, r"Interface\FrameXML\UIDropDownMenu.xml");
@@ -81,8 +81,14 @@ fn setup() -> UiScript {
load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON (the in-flight icon fallback) —
// buff_tests.rs's own load-order precedent for this same global.
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON (the in-flight icon fallback) —
// buff_tests.rs's own load-order precedent for this same global.
s
}
@@ -569,6 +575,14 @@ fn managed_positions_engage_for_the_bare_frame_name() {
assert_eq!(bottom(&s), 102.0, "60 + bottomEither 42 — the row engaged");
// The stance bar shows on top of them.
// The pass's shapeshift-appearance arm (the reference's own, UIParent.lua:1705-1732 — in ours
// since 1938) touches the bar's three shelf textures by name, unguarded as the reference has
// it; a stand-in frame needs stand-in textures.
s.run(
"local t = { Show = function() end, Hide = function() end } \
ShapeshiftBarLeft, ShapeshiftBarMiddle, ShapeshiftBarRight = t, t, t",
)
.unwrap();
s.run("ShapeshiftBarFrame = { IsShown = function() return true end, SetPoint = function() end, ClearAllPoints = function() end }; UIParent_ManageFramePositions()")
.unwrap();
s.resolve();
@@ -36,7 +36,13 @@ fn setup() -> UiScript {
// happens. Ours never called it. MicroMenu needs the bar's button kit under it.
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
// `TicketStatusFrame_OnEvent` re-anchors TemporaryEnchantFrame before it arms the repoll
// timer, so without BuffFrame the handler raises and `refreshTime` is never set. This is
@@ -25,7 +25,15 @@ fn harness_with(extra: &[&str]) -> UiScript {
"Interface\\FrameXML\\Fonts.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
]
.into_iter()
+2 -2
View File
@@ -651,7 +651,7 @@ fn demo_unit_feed(script: Option<NonSendMut<UiScript>>, mut fired: Local<VmMemo<
),
(80, "Interface\\Icons\\INV_Misc_Food_16", 0x80, 117, 5),
(84, "Interface\\Icons\\Spell_Holy_SealOfMight", 0x00, 103, 0),
// The always-on multibars (MultiBars.xml): BottomLeft = actions 61..72, BottomRight
// The always-on multibars (stock MultiActionBars.xml): BottomLeft = actions 61..72, BottomRight
// = 49..60. A few occupied wells on each so the capture shows both rows seated
// (empty multibar wells hide — the ref's own default — so without these the rows
// would be invisible).
@@ -696,7 +696,7 @@ fn demo_unit_feed(script: Option<NonSendMut<UiScript>>, mut fired: Local<VmMemo<
}),
);
}
// The stance bar (StanceBar.xml): the synthetic warrior's three stances, battle active —
// The stance bar (stock BonusActionBarFrame.xml): the synthetic warrior's three stances, battle active —
// matches the bonus offset 1 above (battle stance page). Defensive shows the not-castable
// grey; berserker a running cooldown swipe.
script.set_shapeshift_forms(vec![
@@ -24,7 +24,12 @@ fn load_action_bar(s: &UiScript) {
"GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -33,6 +38,14 @@ fn load_action_bar(s: &UiScript) {
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
// The stock multibar file's OnLoad writes `UIOptionsFrameCheckButtons`, which our options
// window publishes — the reference's own load order (UIOptionsFrame.xml l.21 before
// MultiActionBars.xml l.39), and the manifest's since 1938.
"Interface\\FrameXML\\UIDropDownMenu.xml",
"UiPanels.xml", // StaticPopupDialogs, which the options window's pages register into
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
] {
load_xml(s, file);
}
@@ -47,7 +60,9 @@ fn show_bars(s: &UiScript, bars: &[u32]) {
if bars.contains(&bar) { "1" } else { "nil" }
));
}
lua.push_str("MultiActionBar_Update()");
// …then the manage pass, which the options window runs after a toggle (stock
// UIOptionsFrame.xml:1192, and our rows the same); stock MultiActionBar_Update does not.
lua.push_str("MultiActionBar_Update() UIParent_ManageFramePositions()");
s.run(&lua).unwrap();
}
@@ -65,21 +80,11 @@ fn shipped_multibars_drive_end_to_end() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/MultiBars.xml"),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(
report.errors.is_empty(),
"loader errors: {:?}",
report.errors
);
let frames = super::test_ui::load_ui(&s, "Interface\\FrameXML\\MultiActionBars.xml");
assert_eq!(
report.frames, 100,
"4 bar frames + 48 buttons, each with a Cooldown child — the two VERTICAL bars joined \
(hidden, as the reference's VerticalMultiBar3/4 are), which doubled this count"
frames, 100,
"what stock MultiActionBars.xml declares (1938): the four bar frames and their 48 buttons, \
each with a $parentCooldown — the same 100 ours built for the same seats"
);
// Occupy main slot 1, BottomLeft slot 1 (action 61), BottomRight slot 1 (action 49).
@@ -152,8 +157,9 @@ fn shipped_multibars_drive_end_to_end() {
};
assert_eq!(
rings(&quads, "Interface\\Buttons\\UI-Quickslot2"),
14,
"12 main wells + 2 occupied multibar buttons; 22 empty multibar wells hidden"
3,
"the occupied main slot + the 2 occupied multibar buttons; every empty well is hidden, \
the main bar's included (ActionButton.lua:69-70)"
);
// A click on BottomLeft button 1 (center (26,75)) queues the FIXED id 61 — and stays 61 when
@@ -183,8 +189,10 @@ fn shipped_multibars_drive_end_to_end() {
s.fire_event("UPDATE_BONUS_ACTIONBAR", vec![]);
// The offset edges above showed the bonus overlay (1524) and its descent takes 0.15s of
// frame time this event-only test never used to pass; without it the 12 overlay wells are
// still on screen and correctly answer the SHOWGRID below as 12 extra drop targets.
// still on screen and correctly answer the SHOWGRID below as 12 extra drop targets. The
// stock slide paints, THEN advances, so the frame that hides it is one OnUpdate past the time.
s.tick(0.2);
s.tick(0.01);
// While a payload is held (SHOWGRID), the hidden empty wells appear as drop-target rings
// (UI-Quickslot, the "no action" ring): 11 empty main wells swap texture + 22 multibar wells
@@ -199,10 +207,19 @@ fn shipped_multibars_drive_end_to_end() {
s.fire_event("ACTIONBAR_HIDEGRID", vec![]);
s.resolve();
assert_eq!(rings(&s.extract(), "Interface\\Buttons\\UI-Quickslot"), 0);
assert_eq!(rings(&s.extract(), "Interface\\Buttons\\UI-Quickslot2"), 14);
assert_eq!(
rings(&s.extract(), "Interface\\Buttons\\UI-Quickslot2"),
3,
"letting go hides every empty well again, the main bar's included; the three occupied \
buttons keep their rings"
);
// The unbound multibar hotkey corner carries the ref's RANGE_INDICATOR dot: out of range paints
// the red dot, back in range clears it (the main bar's labels tint instead).
// the red dot, back in range clears it (the main bar's labels tint instead). Two stock
// mechanisms, in order: `ActionButton_UpdateHotkeys` decides whether the corner's TEXT is the
// dot at all — only while `IsActionInRange` answers non-nil, i.e. with a target — and it runs
// on PLAYER_TARGET_CHANGED (ActionButton.lua:121-145, 333-334); every `ActionButton_OnUpdate`
// then shows or hides that text by range (l.389-395) and tints it red past the 0.2 s recheck.
use benilla_ui::script::ActionState;
s.set_action_state(
61,
@@ -213,6 +230,7 @@ fn shipped_multibars_drive_end_to_end() {
..Default::default()
}),
);
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
s.tick(0.5); // past the 0.2 s range recheck
s.resolve();
let dot_shown = |quads: &[benilla_ui::script::ExtractedQuad]| {
@@ -255,22 +273,27 @@ fn shipped_stance_bar_drives_end_to_end() {
// bottom multibar is UP to raise the flag. Since 1500 that is a player option rather than a
// given, so this test raises it; `the_stance_bar_sits_where_the_pass_puts_it` below owns the
// other state.
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
show_bars(&s, &[1]);
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/StanceBar.xml"),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
// The shapeshift bar is declared by stock BonusActionBarFrame.xml (ShapeshiftBarFrame and
// ShapeshiftButton1..10 beside the bonus bar), which load_action_bar loaded above (1938).
assert!(
report.errors.is_empty(),
"loader errors: {:?}",
report.errors
);
assert_eq!(
report.frames, 21,
"the bar frame + 10 buttons, each with a Cooldown child"
s.eval::<bool>("return ShapeshiftBarFrame ~= nil and ShapeshiftButton10 ~= nil")
.unwrap(),
"the stance bar's frames come with the stock bonus-bar file"
);
// No forms pushed (a mage): the frame is hidden.
@@ -440,7 +463,12 @@ fn multibar_hover_renders_the_buttons_own_action() {
"GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -449,7 +477,15 @@ fn multibar_hover_renders_the_buttons_own_action() {
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"MultiBars.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
] {
load_xml(&s, file);
}
@@ -483,7 +519,7 @@ fn multibar_hover_renders_the_buttons_own_action() {
let hover_name = |s: &UiScript, button: &str| -> Option<String> {
s.run(&format!(
"GameTooltip:Hide() BenillaActionButton_OnEnter({button})"
"GameTooltip:Hide() this = {button} ActionButton_SetTooltip()"
))
.unwrap();
s.eval::<Option<String>>(
@@ -537,7 +573,20 @@ fn multibar_hover_renders_the_buttons_own_action() {
fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
let s = UiScript::new().unwrap();
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
for bar in ["MultiBarRight", "MultiBarLeft"] {
assert!(
@@ -556,13 +605,13 @@ fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
// ref ActionButton.lua:8-9 — RIGHT_ACTIONBAR_PAGE = 3 (actions 25..36), LEFT = 4 (37..48).
for (bar, first, last) in [("MultiBarRight", 25, 36), ("MultiBarLeft", 37, 48)] {
assert_eq!(
s.eval::<i64>(&format!("return {bar}Button1.base + {bar}Button1.index"))
s.eval::<i64>(&format!("return ActionButton_GetPagedID({bar}Button1)"))
.unwrap(),
first,
"{bar}'s first slot"
);
assert_eq!(
s.eval::<i64>(&format!("return {bar}Button12.base + {bar}Button12.index"))
s.eval::<i64>(&format!("return ActionButton_GetPagedID({bar}Button12)"))
.unwrap(),
last,
"{bar}'s last slot"
@@ -586,7 +635,20 @@ fn every_extra_bar_stays_down_until_its_own_toggle_is_set() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
const BARS: [&str; 4] = [
"MultiBarBottomLeft",
@@ -660,7 +722,20 @@ fn raising_a_bottom_bar_moves_the_managed_bottom_stack() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Interface\\FrameXML\\CastingBarFrame.xml");
// The pass writes the y of a frame row into its anchor; read it back off the anchor rather
@@ -711,7 +786,20 @@ fn viewable_action_bar_pages_follow_the_bar_toggles() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
let viewable = |s: &UiScript| {
s.eval::<String>(
@@ -764,7 +852,20 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
show_bars(&s, &[1]);
let well = |s: &UiScript| {
@@ -798,12 +899,36 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
// Re-applying an "off" that is already off must not owe anything: with a payload in hand the
// wells stay open through it (the reference's own idempotence bug, closed by the latch).
// The reference's hide is a COUNTED decrement on every well (`ActionButton_HideGrid`), never
// a no-op: with the option off, one more apply closes a payload-held well. That is why our
// row runs `MultiActionBar_UpdateGridVisibility` only on a CLICK (stock UIOptionsFrame.xml:683)
// and its load arm only ever SHOWS (UIOptionsFrame.lua:218-220) — a load-time hide would push
// counts nobody raised below zero. (Ours used to guard the no-op; the guard went with the
// file, 1938.)
// The load arm, both ways: off touches no count, on opens the wells.
s.run("ALWAYS_SHOW_MULTIBARS = \"0\" OptionsFrame_ApplySavedSettings()")
.unwrap();
s.fire_event("ACTIONBAR_SHOWGRID", vec![]);
s.run("MultiActionBar_UpdateGridVisibility()").unwrap();
s.run("MultiActionBar_UpdateGridVisibility()").unwrap();
assert!(well(&s), "a no-op apply may not decrement");
assert!(well(&s));
s.fire_event("ACTIONBAR_HIDEGRID", vec![]);
assert!(!well(&s), "a load with the option off left the count alone");
s.run("ALWAYS_SHOW_MULTIBARS = \"1\" OptionsFrame_ApplySavedSettings()")
.unwrap();
assert!(well(&s), "a load with the option on opens the wells");
s.run("ALWAYS_SHOW_MULTIBARS = \"0\" MultiActionBar_UpdateGridVisibility()")
.unwrap();
assert!(!well(&s));
// …and the click arm against a held payload: one apply closes the well the payload opened.
// (The payload's own HIDEGRID that follows takes the count to −1 in the reference too — its
// quirk, not modelled here.)
s.fire_event("ACTIONBAR_SHOWGRID", vec![]);
assert!(well(&s));
s.run("MultiActionBar_UpdateGridVisibility()").unwrap();
assert!(
!well(&s),
"the option's hide counts against the payload's open"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -830,7 +955,20 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
s.set_action(
1,
Some(ActionSlot {
@@ -869,8 +1007,9 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
let resting = ring_alphas(&s, quickslot2);
assert_eq!(
resting.len(),
12,
"the 12 main wells; the multibar's are hidden"
1,
"only the occupied main slot draws a ring: every empty well, main bar included, is \
hidden while nothing is held (ActionButton.lua:69-70)"
);
assert!(
resting.iter().all(|a| *a == 1.0),
@@ -891,9 +1030,14 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
"every grid ring is the ref's half-alpha ghost, not an opaque plate: {ghosts:?}"
);
// …and the one OCCUPIED button is untouched by it.
// …and the one OCCUPIED button ghosts with it: `ActionButton_ShowGrid` sets the half alpha
// (ActionButton.lua:246) before anything asks whether the slot holds an action, and
// `HideGrid` touches no colour at all. What restores an occupied ring is its next
// `ActionButton_UpdateUsable` (l.271-283), whose `SetVertexColor(1, 1, 1)` carries the default
// alpha — from Update, and on SPELL_UPDATE_USABLE. Ours skipped occupied rings; the stock
// file does not (1938).
let occupied = ring_alphas(&s, quickslot2);
assert_eq!(occupied, vec![1.0], "the occupied ring does not ghost");
assert_eq!(occupied, vec![0.5], "the occupied ring ghosts too");
// A well that was ghosted and is then FILLED comes back opaque. This is the assertion that
// outlives the engine's own `SetVertexColor` divergence (1782): the day an absent alpha stops
@@ -914,21 +1058,28 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
s.resolve();
assert_eq!(
ring_alphas(&s, quickslot2),
vec![1.0, 1.0],
"a ghosted well that gets an action is opaque again, payload still held"
vec![0.5, 1.0],
"the well that got an action is opaque again (its Update ran UpdateUsable); the other \
occupied ring keeps its ghost, payload still held"
);
// Letting go restores both halves — art and tint, which is why they share one setter.
// Letting go takes the empty wells away; the colour it leaves alone.
s.fire_event("ACTIONBAR_HIDEGRID", vec![]);
s.resolve();
assert_eq!(ring_alphas(&s, quickslot), Vec::<f32>::new());
let after = ring_alphas(&s, quickslot2);
assert_eq!(after.len(), 12);
// (10 empty main wells back to the hollow ring + the 2 occupied ones.)
assert!(
after.iter().all(|a| *a == 1.0),
"the dim does not outlive the payload (the ref's own stuck-dim bug): {after:?}"
assert_eq!(
after,
vec![0.5, 1.0],
"HideGrid touches no colour: a ring the grid ghosted stays dim past the payload — the \
reference's own rule (ours used to clear it here)"
);
// What happens to that ring NEXT is decision 1782's open question, deliberately not asserted:
// the reference's `SetVertexColor(1, 1, 1)` in `ActionButton_UpdateUsable` keeps the alpha
// already on the region (byte-pinned, wow-re button-state-texture-path-setter.md §7 — the
// "stuck dim" a 1.12 player sees after a drag), while this engine's three-argument call still
// resets it to 1.0 until the director calls 1782. A test that pinned either outcome would be
// enshrining a divergence as fidelity.
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -947,25 +1098,42 @@ fn a_bar_toggle_sends_the_byte_its_globals_pack_to() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
let _ = s.take_action_bar_toggle_sends();
// The row's own setter, with the row's own "1"/"0" strings.
s.run("BenillaMultiBar_SetShown(1, \"1\")").unwrap();
s.run("SHOW_MULTI_ACTIONBAR_1 = 1 MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
.unwrap();
assert_eq!(s.take_action_bar_toggle_sends(), vec![0x01]);
s.run("BenillaMultiBar_SetShown(3, \"1\")").unwrap();
s.run("SHOW_MULTI_ACTIONBAR_3 = 1 MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
.unwrap();
assert_eq!(
s.take_action_bar_toggle_sends(),
vec![0x05],
"the WHOLE byte re-sent, not a delta"
);
s.run("BenillaMultiBar_SetShown(4, \"1\")").unwrap();
s.run("SHOW_MULTI_ACTIONBAR_4 = 1 MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
.unwrap();
assert_eq!(s.take_action_bar_toggle_sends(), vec![0x0d]);
s.run("BenillaMultiBar_SetShown(1, \"0\")").unwrap();
s.run("SHOW_MULTI_ACTIONBAR_1 = nil MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
.unwrap();
assert_eq!(
s.take_action_bar_toggle_sends(),
vec![0x0c],
"the row hands the setter a STRING, and \"0\" is off"
"the row turns its \"0\" into nil before the send, and nil is off"
);
// What is stored is 1 or nil and nothing else — GetActionBarToggles' own shape, and what the
@@ -1017,7 +1185,20 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
// `r##`: the Lua contains `select("#", ...)`, and `"#` would close a single-hash raw string.
s.run(
@@ -1029,7 +1210,11 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
"##,
)
.unwrap();
s.run("BenillaMultiBar_SetShown(2, \"1\")").unwrap();
// The shipped setter is the Action Bars row's own closure (OptionsFrame.xml), which is
// stock's three lines — assign the global, MultiActionBar_Update(), SetActionBarToggles(…) —
// plus the manage pass.
s.run("OptionsFrameContainerBodyActionBarsRowMultiBar2Check:Click()")
.unwrap();
assert_eq!(
s.eval::<i64>("return BENILLA_TEST_TOGGLE_ARGC").unwrap(),
4,
@@ -1052,8 +1237,22 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "StanceBar.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
s.set_shapeshift_forms(vec![ShapeshiftFormView {
spell_id: 2457,
texture: Some("Interface\\Icons\\Stance_A".into()),
@@ -1110,8 +1309,22 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "StanceBar.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
let form = |id: u32| ShapeshiftFormView {
spell_id: id,
@@ -1175,8 +1388,17 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
// Even at three forms, raised keeps the middle strip down — the fork is on the BAR, not the
// form count; the count only decides the middle strip within the unraised branch.
// A third form learned while the bar is up: the reference's `ShapeshiftBar_Update` shows the
// middle strip for three forms regardless of the bar (BonusActionBarFrame.lua:198-206), and
// nothing runs the pass on that event — the strip draws across the row until something
// moves. The pass, not Update, owns the raised look (UIParent.lua:1705-1720).
s.set_shapeshift_forms(vec![form(2457), form(71), form(2458)]);
s.fire_event("UPDATE_SHAPESHIFT_FORMS", vec![]);
assert!(
shown(&s, "ShapeshiftBarMiddle"),
"Update shows the strip; only the pass takes it down"
);
s.run("UIParent_ManageFramePositions()").unwrap();
assert!(!shown(&s, "ShapeshiftBarMiddle"));
assert_eq!(ring(&s), 50.0);
@@ -1208,8 +1430,22 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "StanceBar.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
show_bars(&s, &[]); // unraised: the shelf art is the state under test
let form = |id: u32| ShapeshiftFormView {
@@ -1311,7 +1547,20 @@ fn an_extra_bars_empty_well_keeps_its_bound_hotkey_label() {
// the player binds it, as the reporter had).
s.register_bindings(&crate::bindings::registry_commands());
load_action_bar(&s);
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
show_bars(&s, &[1]);
s.set_action(
61,
@@ -750,7 +750,10 @@ fn interface_harness() -> UiScript {
"Interface\\FrameXML\\ColorPickerFrame.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -811,7 +814,14 @@ fn actionbars_harness() -> UiScript {
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -820,7 +830,15 @@ fn actionbars_harness() -> UiScript {
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"MultiBars.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
],
);
harness_on(s)
@@ -2695,7 +2713,7 @@ fn the_action_bars_page_locks_the_real_bar() {
);
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.resolve();
s.run("BenillaActionButton_OnDragStart(ActionButton1)")
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnDragStart\")()")
.unwrap();
assert!(
s.cursor_payload().is_none(),
@@ -2705,7 +2723,7 @@ fn the_action_bars_page_locks_the_real_bar() {
// Defaults walks it back to ActionBar.xml's own assignment, and the bar drags again.
s.run("OptionsFrameContainerDefaults:Click()").unwrap();
assert_eq!(s.eval::<String>("return LOCK_ACTIONBAR").unwrap(), "0");
s.run("BenillaActionButton_OnDragStart(ActionButton1)")
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnDragStart\")()")
.unwrap();
assert!(s.cursor_payload().is_some(), "unlocked again");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -2846,9 +2864,9 @@ fn the_target_of_target_rows_gate_each_other_and_write_their_globals() {
assert!(
s.eval::<bool>(
"return OptionsFrameContainerBodyInterfaceRowTargetOfTarget.applyFunc \
== TargetofTarget_Update \
== \"TargetofTarget_Update\" \
and OptionsFrameContainerBodyInterfaceRowTargetOfTargetMode.applyFunc \
== TargetofTarget_Update"
== \"TargetofTarget_Update\""
)
.unwrap(),
"both rows re-decide the frame when they are written"
@@ -17,7 +17,15 @@ fn harness(extra: &[&str]) -> UiScript {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "ActionBar.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
for f in extra {
load_xml(&s, f);
}
@@ -230,7 +238,7 @@ fn hovering_the_meter_shows_the_live_latency() {
"the button over the meter takes the mouse"
);
s.run("BenillaPerformanceBar_OnEnter(MainMenuBarPerformanceBarFrameButton)")
s.run("this = MainMenuBarPerformanceBarFrameButton MainMenuBarPerformanceBarFrameButton:GetScript(\"OnEnter\")()")
.unwrap();
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
@@ -277,7 +285,7 @@ fn hovering_the_meter_shows_the_live_latency() {
"the refresh rebuilds BOTH lines — a held-open plate never decays to one"
);
s.run("BenillaPerformanceBar_OnLeave()").unwrap();
s.run("this = MainMenuBarPerformanceBarFrameButton MainMenuBarPerformanceBarFrameButton:GetScript(\"OnLeave\")()").unwrap();
assert!(
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"leaving hides the plate"
@@ -19,7 +19,13 @@ fn load_pet_bar(s: &UiScript) {
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -370,7 +376,13 @@ fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -383,7 +395,14 @@ fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
] {
load_xml(&s, file);
}
load_xml(&s, "MultiBars.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
if with_multibar {
s.run("SHOW_MULTI_ACTIONBAR_1 = 1 MultiActionBar_Update()")
.unwrap();
@@ -665,7 +684,12 @@ fn load_pet_bar_with_tooltip(s: &UiScript) {
"GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -1075,8 +1075,7 @@ mod tests {
/// do not ship under its own name usually has a counterpart of ours under a different one, and
/// both would declare the same frames.
///
/// That half doubles as the map nothing else holds: `ActionBarFrame.xml` is our
/// `ActionBar.xml`, `FloatingChatFrame.xml` is our `ChatFrame.xml`,
/// That half doubles as the map nothing else holds: `FloatingChatFrame.xml` is our `ChatFrame.xml`,
/// `MainMenuBarMicroButtons.xml` is our `MicroMenu.xml`, `StaticPopup.xml` is our
/// `UiPanels.xml`. (The row this map used to lead with — `PlayerFrame.xml`/`TargetFrame.xml`/
/// `PetFrame.xml` all being our one `UnitFrames.xml` — is retired: those four are the
@@ -2037,11 +2036,6 @@ mod tests {
flow works without it",
),
("ITEM_TEXT_TRANSLATION", "ItemTextFrame.lua"),
(
"PARTY_MEMBER_DISABLE",
"PartyMemberFrame.lua — the offline grey-out",
),
("PARTY_MEMBER_ENABLE", "PartyMemberFrame.lua"),
("PET_UI_CLOSE", "PetPaperDollFrame.lua"),
("PET_UI_UPDATE", "PetPaperDollFrame.lua"),
("PLAYER_DAMAGE_DONE_MODS", "PaperDollFrame.lua"),
@@ -2049,10 +2043,6 @@ mod tests {
"PLAYER_FLAGS_CHANGED",
"TargetFrame.lua — the AFK/DND badge",
),
(
"PLAYER_REGEN_ENABLED",
"PlayerFrame.lua — and _DISABLED is unfired too, so the pair is whole",
),
("PLAYTIME_CHANGED", "PlayerFrame.lua"),
("SHOW_COMPARE_TOOLTIP", "PaperDollFrame.lua"),
("SYSMSG", "UIErrorsFrame.lua"),
@@ -2080,47 +2070,59 @@ mod tests {
} else if p.extension().is_some_and(|x| x == "rs") {
let text = std::fs::read_to_string(&p).unwrap_or_default();
// Split so this file cannot match its own walker (see the sibling gate).
const CALL: &str = concat!("fire_event", "(");
let mut from = 0;
// Two producer shapes: `UiScript::fire_event(` and its `&Lua` twin
// `tick::fire_event_into(` (1924), which an engine verb fires from inside a
// binding — `UpdateSpells` → SPELLS_CHANGED, `ChangeActionBarPage` →
// ACTIONBAR_PAGE_CHANGED (1938).
const CALLS: [&str; 2] =
[concat!("fire_event", "("), concat!("fire_event_into", "(")];
let mut fires_indirectly = false;
while let Some(i) = text[from..].find(CALL) {
let at = from + i + CALL.len();
from = at;
let rest = text[at..].trim_start();
if !rest.starts_with('"') {
// `fire_event(event, args)` — the name reached the call through a
// variable, so adjacency cannot see it. `ui_cast.rs` does exactly
// this: a `match edge` yields `("SPELLCAST_INTERRUPTED", vec![])` and
// one call site fires whatever it produced. Treated below.
fires_indirectly = true;
}
if let Some(body) = rest.strip_prefix('"') {
if let Some(end) = body.find('"') {
fired.insert(body[..end].to_string());
for call in CALLS {
let mut from = 0;
while let Some(i) = text[from..].find(call) {
let at = from + i + call.len();
from = at;
let rest = text[at..].trim_start();
let rest = rest.strip_prefix("lua,").map_or(rest, str::trim_start);
if !rest.starts_with('"') {
fires_indirectly = true;
}
if let Some(body) = rest.strip_prefix('"') {
if let Some(end) = body.find('"') {
fired.insert(body[..end].to_string());
}
}
}
}
if fires_indirectly {
// The name reached `fire_event` through a variable. Rather than guess, we
// match the SHAPE that produced it: `("EVENT_NAME", vec!` — a tuple whose
// second element is the argument vector, which is how `ui_cast.rs`'s
// `match edge` hands a name and its args to one shared call site.
//
// The first attempt at this counted EVERY SCREAMING_CASE literal in such a
// file, and that swept up eighteen names off the UNPRODUCED list — a gate
// made vacuous by its own conservatism, which is the failure mode this
// file has now hit twice. The shape is the discipline: it admits the
// literals that are actually being fired and nothing else.
// An indirect fire names its event either as `"NAME", vec![…]` or as one
// arm of an `if … { "A" } else { "B" }` chosen before the call — a literal
// standing alone on its line (`ui_action/state.rs`'s START/STOP_AUTOREPEAT).
const SHAPE: &str = "\", vec!";
let caps = |lit: &str| {
!lit.is_empty()
&& lit.chars().all(|c| {
c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit()
})
};
for (i, _) in text.match_indices(SHAPE) {
let before = &text[..i];
let Some(q) = before.rfind('"') else { continue };
let lit = &before[q + 1..];
if !lit.is_empty()
&& lit.chars().all(|c| {
c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit()
})
{
if caps(lit) {
fired.insert(lit.to_string());
}
}
let lines: Vec<&str> = text.lines().map(str::trim).collect();
for (i, t) in lines.iter().enumerate() {
let Some(lit) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"'))
else {
continue;
};
let arm = i > 0
&& lines[i - 1].ends_with('{')
&& lines.get(i + 1).is_some_and(|n| n.starts_with('}'));
if arm && caps(lit) {
fired.insert(lit.to_string());
}
}
@@ -662,7 +662,7 @@ fn a_hover_sweep_across_owners_costs_no_graph_derivation() {
///
/// The two guards above drive `SetOwner(button, "ANCHOR_RIGHT")` — the bag slot's idiom. The bars
/// do not use it. With `UberTooltips` at its shipped default of `"1"`, every action button routes
/// through `GameTooltip_SetDefaultAnchor` (`ActionBar.xml:510`, and the stance/pet/bonus bars the
/// through `GameTooltip_SetDefaultAnchor` (stock `ActionButton_SetTooltip`, and the stance/pet/bonus bars the
/// same), which is `SetOwner(owner, "ANCHOR_NONE")` followed by an explicit `SetPoint` — a
/// completely different arm of the same verb, and the one that DROPS the tooltip's anchors.
///
@@ -1280,7 +1280,7 @@ fn a_cinematic_leaves_nothing_of_the_interface_on_screen() {
// The sweep is only worth anything if there was something to hide in the first place.
let before = names.iter().filter(|n| visible(&s, n)).count();
assert!(
before > 50,
before > 20, // 50 until 1938 took the three bar files (and their visible frames) stock
"only {before} frames visible before the cinematic — the sweep found no interface to \
hide, so it would pass no matter what the cascade did"
);
@@ -1350,7 +1350,7 @@ fn every_declared_parent_really_attaches() {
let declared = shipped_frame_parents();
assert!(
declared.len() > 60,
declared.len() > 30, // 67 before 1938 took the three bar files (25 of them) stock
"only {} parent declarations found — the scan broke",
declared.len()
);
@@ -127,7 +127,13 @@ fn shipped_spellbook_drives_end_to_end() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "ActionBar.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -203,6 +209,14 @@ fn shipped_spellbook_drives_end_to_end() {
// (`cursor::bar::place_action`) a bar-to-bar drag uses: a plain click on an action button
// routes through UseAction's checkCursor=1 fork to a place. Packs kind 0x00 (SPELL, decision
// 0216 §1) with the spell id — `action_sets` is the app's own CMSG_SET_ACTION_BUTTON queue.
// An EMPTY main-bar slot is hidden under the reference (ActionButton.lua:69-70) until a held
// payload opens the grid: the engine derives ACTIONBAR_SHOWGRID from the cursor's edge and
// fires it on the next tick, as the app ticks between any two mouse events.
s.tick(0.016);
assert!(
s.eval::<bool>("return ActionButton1:IsVisible()").unwrap(),
"the held spell opened the empty well"
);
let (ax, ay) = center(&s, "ActionButton1");
s.mouse_button(ax, ay, "LeftButton", true);
s.mouse_button(ax, ay, "LeftButton", false);
@@ -684,7 +698,12 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
r"Interface\FrameXML\ClassTrainerFrameTemplates.xml",
"MicroMenu.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
@@ -25,10 +25,15 @@ fn harness() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the unit popups' kit (TargetFrameDropDown's template)
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE, and its
// declarer is UIParent (ref UIParent.lua:65) since 1888.
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE, and its
// declarer is UIParent (ref UIParent.lua:65) since 1888.
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
+9 -3
View File
@@ -202,7 +202,7 @@ pub(super) const LOOT_UI: &[&str] = &[
/// 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.)
/// * **`ActionBar.xml`** for `MainMenuExpBar` — the third bar of that prefix call — and for
/// * **`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
@@ -259,7 +259,9 @@ pub(super) const CHARACTER_UI: &[&str] = &[
"Interface\\FrameXML\\PartyFrameTemplates.xml",
"Interface\\FrameXML\\PetFrame.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.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
@@ -315,7 +317,11 @@ pub(super) const BAG_UI: &[&str] = &[
// 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",
"ActionBar.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.
"MicroMenu.xml",
@@ -231,10 +231,20 @@ fn action_button_hover_takes_the_default_corner() {
let mut s = harness(&[
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
]);
s.register_cvars(crate::cvars::registered_pairs());
s.run("BenillaActionButton_OnEnter(ActionButton3)").unwrap();
s.run("this = ActionButton3 ActionButton_SetTooltip()")
.unwrap();
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
let ok: bool = s
.eval(
@@ -264,8 +274,25 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
let mut s = harness(&[
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"MultiBars.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
]);
s.register_cvars(crate::cvars::registered_pairs());
s.set_cvar_engine("UberTooltips", "0");
@@ -278,7 +305,8 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
.unwrap()
};
s.run("BenillaActionButton_OnEnter(ActionButton3)").unwrap();
s.run("this = ActionButton3 ActionButton_SetTooltip()")
.unwrap();
assert!(
s.eval::<bool>("return GameTooltip.default == nil").unwrap(),
"off: the main bar's plate is owner-anchored, not the default corner"
@@ -297,7 +325,7 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
// All three members of the ref's LEFT set, by frame — membership is not gated on visibility,
// and the two vertical bars are hidden until their option is ticked.
for bar in ["MultiBarBottomRight", "MultiBarRight", "MultiBarLeft"] {
s.run(&format!("BenillaActionButton_OnEnter({bar}Button1)"))
s.run(&format!("this = {bar}Button1 ActionButton_SetTooltip()"))
.unwrap();
assert_eq!(
seat(&s),
@@ -308,7 +336,7 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
// And the CVar back on restores the corner — the fork is a fork, not a one-way door.
s.set_cvar_engine("UberTooltips", "1");
s.run("BenillaActionButton_OnEnter(MultiBarBottomRightButton1)")
s.run("this = MultiBarBottomRightButton1 ActionButton_SetTooltip()")
.unwrap();
assert!(
s.eval::<bool>("return GameTooltip.default ~= nil").unwrap(),
@@ -326,8 +354,18 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
let mut s = harness(&[
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"StanceBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
]);
s.register_cvars(crate::cvars::registered_pairs());
s.set_shapeshift_forms(vec![benilla_ui::script::ShapeshiftFormView {
@@ -341,7 +379,7 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
s.fire_event("UPDATE_SHAPESHIFT_FORMS", vec![]);
s.resolve();
s.run("BenillaShapeshiftButton_OnEnter(ShapeshiftButton1)")
s.run("this = ShapeshiftButton1 ShapeshiftButton1:GetScript(\"OnEnter\")()")
.unwrap();
assert!(
s.eval::<bool>("return GameTooltip.default ~= nil").unwrap(),
@@ -349,7 +387,7 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
);
s.set_cvar_engine("UberTooltips", "0");
s.run("BenillaShapeshiftButton_OnEnter(ShapeshiftButton1)")
s.run("this = ShapeshiftButton1 ShapeshiftButton1:GetScript(\"OnEnter\")()")
.unwrap();
assert_eq!(
s.eval::<String>(
@@ -371,7 +409,16 @@ fn buff_hover_hangs_below_left_of_the_button() {
let mut s = harness(&[
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\BuffFrame.xml",
@@ -102,6 +102,7 @@ fn harness_with_bags() -> UiScript {
fn seed_items(s: &mut UiScript) {
let mut inv: InventorySlots = Default::default();
inv[1] = Some(InvSlotView {
duration_ms: None,
already_bound: false,
bar_placeable: true,
durability: None,
@@ -148,6 +149,7 @@ fn seed_items(s: &mut UiScript) {
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
@@ -308,6 +310,7 @@ fn doll_hover_renders_the_live_instance_and_never_self_compares() {
// Break the equipped helm: instance pair (0, 40); the template stays authored-full.
let mut inv: InventorySlots = Default::default();
inv[1] = Some(InvSlotView {
duration_ms: None,
already_bound: false,
bar_placeable: true,
durability: Some((0, 40)),
@@ -68,7 +68,12 @@ fn load_tot() -> UiScript {
load_xml(&s, "UnitPopup.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "ActionBar.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
@@ -483,7 +483,10 @@ fn load_pet_menu_frames(s: &UiScript) {
"Interface\\FrameXML\\PetFrame.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"ActionBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"PetActionBar.xml",
] {
load_xml(s, file);