skill-ups announce again, and the stack split fills its window

- a real skill-up prints its line; the lines the client keeps silent — class specs, racials, mounts — stay silent, off the game's own flag
- the stack-split dialog draws whole: its plate fills the window
- the four bag-bar buttons speak the reference's names all the way down, so Bagnon's overrides catch them and the backpack button lights while its window is open
- regions anchor at creation — the client's implicit-anchor law replaces a pile of draw-time fallbacks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-14 19:34:09 +07:00
co-authored by Claude Fable 5
parent b990a5a048
commit 606a72bde5
23 changed files with 1146 additions and 408 deletions
+126 -1
View File
@@ -73,7 +73,8 @@ fn bag_bar_icons_draw_above_the_action_bar_art() {
/// The backpack open/close kits (ContainerFrame.lua ContainerFrame_OnShow/OnHide, l.140 / l.120):
/// showing the window queues igBackPackOpen, hiding it queues igBackPackClose — and nothing queues
/// at load (the frame is authored hidden="true", so it never transitions on startup). Driven through
/// `BenillaBagToggle_OnClick`, the exact path both the bag button and the 'B' binding use.
/// `BenillaBagToggle_OnClick` — the toggle body the bag button's click wrapper calls (the 'B'
/// binding runs the bare `ToggleBackpack()`, the same one hop deeper).
#[test]
fn backpack_toggle_plays_open_and_close_kits() {
let mut s = UiScript::new().unwrap();
@@ -1002,6 +1003,60 @@ fn shift_click_on_a_stack_opens_the_split_frame() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// B180 — the split dialog's parchment plate fills the whole 172×96 frame. The reference authors
/// the plate 256×32 with NO anchors, a vestigial size the real client never renders (the TexCoords
/// crop exactly 172×96 out of the 256×128 art — the frame's own size, a complete panel drawn 1:1
/// over it). Our sized-anchor-less fallback draws CENTERED at the authored size instead, which
/// squashed the dialog to a 32-tall bar: the count and arrows floated above it and Okay/Cancel
/// straddled its bottom edge. The plate's authored size is dropped (StackSplit.xml says why); this
/// pins the render so the vestigial size cannot come back.
#[test]
fn the_split_frame_plate_fills_the_dialog() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
let (x, y) = open_backpack_with_a_five_stack(&mut s);
s.set_modifiers(true, false, false);
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
s.set_modifiers(false, false, false);
s.resolve();
let (left, right, top, bottom) = s
.eval::<(f64, f64, f64, f64)>(
"return StackSplitFrame:GetLeft(), StackSplitFrame:GetRight(), \
StackSplitFrame:GetTop(), StackSplitFrame:GetBottom()",
)
.unwrap();
assert!(
(right - left - 172.0).abs() < 0.5 && (top - bottom - 96.0).abs() < 0.5,
"the dialog frame is 172×96, got {}×{}",
right - left,
top - bottom
);
let plate = s
.extract()
.into_iter()
.find(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.contains("UI-MoneyFrame"))
})
.expect("the plate is on screen");
let r = plate.rect.expect("the plate has a rect");
for (edge, got, want) in [
("left", r.left, left as f32),
("right", r.right, right as f32),
("top", r.top, top as f32),
("bottom", r.bottom, bottom as f32),
] {
assert!(
(got - want).abs() < 0.5,
"plate {edge} = {got}, frame {edge} = {want} — the plate must fill the dialog"
);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Okay in the split spinner only picks the split carry up (ref/cursor.rs `SplitContainerItem` —
/// a pickup, not a self-contained move); a SUBSEQUENT placement is what actually queues the
/// `ContainerMove` with `count: Some(n)`, drained the same way any other container move is.
@@ -1834,3 +1889,73 @@ fn the_bag_slots_carry_the_references_names_and_icon_names() {
"Bagnon captures this OnClick to replace the bag behaviour"
);
}
/// The backpack button's checked law after the stated divergence (BenillaBagToggle_OnClick's
/// comment): checked belongs to the WINDOWS' OnShow/OnHide writes, and the button's XML click
/// wrapper only undoes the CheckButton widget's pre-handler flip. The killer case is the click
/// that never touches the backpack window — backpack shut, another bag's window open →
/// ToggleBackpack sees an open window and CLOSES ALL; the backpack never transitions, nothing
/// writes its button, and only the wrapper's undo keeps the widget flip from leaving it
/// lit-while-shut (the job the ref's native-window scan tail used to do).
#[test]
fn a_close_others_click_leaves_the_backpack_button_dark() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"Fonts.xml",
"UiPanels.xml",
"MerchantFrame.xml",
"Cooldown.xml",
"ActionBar.xml",
"BagFrame.xml",
] {
load_xml(&s, file);
}
s.set_money(0);
s.set_container(
1,
Some(benilla_ui::script::ContainerState {
name: Some("Pouch".into()),
num_slots: 6,
slots: std::collections::HashMap::new(),
}),
);
let checked = |s: &mut UiScript| {
s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
.unwrap()
};
// Bag 1's window alone is open; the backpack window and its button are dark.
s.run("ToggleBag(1)").unwrap();
assert!(!s.eval::<bool>("return BenillaBagFrame:IsShown()").unwrap());
assert!(!checked(&mut s));
// A REAL click on the backpack button (the widget flip fires only on real input): close-all.
s.resolve();
let r: Vec<f32> = s
.eval(
"local f = MainMenuBarBackpackButton \
return { f:GetLeft() + f:GetWidth() / 2, f:GetBottom() + f:GetHeight() / 2 }",
)
.unwrap();
s.mouse_move(r[0], r[1]);
s.mouse_button(r[0], r[1], "LeftButton", true);
s.mouse_button(r[0], r[1], "LeftButton", false);
assert!(
!s.eval::<bool>("return BenillaBagFrame1:IsShown()").unwrap(),
"the click closes the other bag's window (ToggleBackpack's close-all arm)"
);
assert!(
!checked(&mut s),
"no backpack transition happened, so the button must stay dark — the widget flip undone"
);
// The second real click opens the backpack (plus the equipped bag): its OnShow writes the light.
s.mouse_button(r[0], r[1], "LeftButton", true);
s.mouse_button(r[0], r[1], "LeftButton", false);
assert!(s.eval::<bool>("return BenillaBagFrame:IsShown()").unwrap());
assert!(
checked(&mut s),
"open ⇒ lit, written by the window's OnShow"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -1391,3 +1391,158 @@ fn bagnon_forevers_records_survive_the_logout_boundary() {
);
assert_eq!(item, "4496", "the logout boundary must not erase the slot");
}
/// **Bug 6 — the director's "smaller bags open their own little bags".** Bagnon's whole
/// interception model is the global override (`Bagnon_Core/core/Overrides.lua`: `ToggleBag = …`)
/// plus a `SetScript("OnClick")` wrap on our bar buttons whose replacement calls the ORIGINAL
/// handler back for a plain click. Both layers end at whatever the original OnClick does — and
/// ours toggled `BenillaBagFrame<N>` directly instead of calling `ToggleBag`, so every slot-button
/// click walked straight past both of Bagnon's hooks and opened the native window (the backpack
/// button was fixed in an earlier pass; the four slots were not). The ref's own
/// `BagSlotButton_OnClick` (MainMenuBarBagButtons.lua l.3-21) calls the GLOBAL `ToggleBag`, and
/// its checked tail scans only the NATIVE ContainerFrames — so with Bagnon holding the bags a
/// slot click toggles Bagnon's one window and the slot button stays unlit. Both halves pinned
/// here with a real mouse click, an equipped bag in slot 2, and Bagnon fully hooked.
#[test]
fn a_bag_slot_click_toggles_bagnon_not_the_native_window() {
let root = corpus_or_skip!();
let mut s = open_bagnon(&root);
// An equipped bag behind CharacterBag1Slot (bagId 2): the inventory surface (the bag ITEM in
// inv slot 21) and its container — both fed before any click, the schedule's own order.
let mut inv: benilla_ui::script::InventorySlots = Default::default();
inv[21] = Some(benilla_ui::script::InvSlotView {
item_id: 4497,
count: 1,
quality: 1,
name: Some("Small Green Pouch".into()),
link: Some("|cffffffff|Hitem:4497:0:0:0|h[Small Green Pouch]|h|r".into()),
icon: Some("Interface\\Icons\\INV_Misc_Bag_10_Green".into()),
..Default::default()
});
s.set_inventory_slots(inv);
s.set_container(
2,
Some(ContainerState {
name: Some("Small Green Pouch".into()),
num_slots: 6,
slots: Default::default(),
}),
);
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(2)]);
s.resolve();
// Bagnon is open (the fixture); a REAL click on the slot button must CLOSE Bagnon — the
// toggle routed through the override — and must never show the native window.
// Both interception layers must be live, and the click exercises both: the SetScript wrap
// (Bagnon.lua's Bagnon_AddBagHooks — its replacement calls our CAPTURED original back with
// no args, so `self` inside the XML wrapper must still resolve on the re-entrant call) and
// the global ToggleBag override the original then reaches.
assert!(
s.eval::<bool>("return CharacterBag1Slot:GetScript('OnClick') == BagnonBlizBag_OnClick")
.unwrap_or(false),
"Bagnon's SetScript OnClick wrap must be in place on the slot button"
);
let (x, y) = centre_of(&mut s, "CharacterBag1Slot");
s.mouse_move(x, y);
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
assert!(
!s.eval::<bool>("return Bagnon:IsVisible() or false")
.unwrap(),
"the slot click must reach Bagnon's ToggleBag override and close its window"
);
assert!(
!s.eval::<bool>("return BenillaBagFrame2:IsVisible() or false")
.unwrap(),
"the native bag window must NOT open — the click belongs to the addon's override"
);
// Click again: Bagnon back open, native still shut, and the slot button unlit — the ref's
// checked tail reads only the native window (its ContainerFrame scan), which Bagnon never
// shows.
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
for _ in 0..2 {
s.tick(0.1);
}
assert!(
s.eval::<bool>("return Bagnon:IsVisible() or false")
.unwrap(),
"the second click re-opens Bagnon through the same override"
);
assert!(
!s.eval::<bool>("return BenillaBagFrame2:IsVisible() or false")
.unwrap(),
"the native window stays shut on the re-open too"
);
assert!(
!s.eval::<bool>("return CharacterBag1Slot:GetChecked() and true or false")
.unwrap(),
"the slot button stays unlit with an addon holding the bags (the ref scan is native-only)"
);
let raised = s.errors();
let unexplained: Vec<&String> = raised
.iter()
.filter(|e| !e.contains("PutItemIn") && !e.contains("PickupBagFromSlot"))
.collect();
assert!(
unexplained.is_empty(),
"the click round-trip raised: {unexplained:#?}"
);
}
/// **Bug 7 — the director's unlit backpack button.** Bagnon lights `MainMenuBarBackpackButton`
/// from its window's OnShow/OnHide (`Bagnon.lua:33/39` — `SetChecked(1/0)`), and OnShow dispatch
/// is synchronous (wow-re `onshow-onhide-dispatch-order.md`), so whoever writes checked AFTER the
/// toggle wins. Two of our seats overwrote it: the button's old click tail re-derived checked
/// from the NATIVE backpack window (the ref's own scan — which leaves the button unlit on the
/// real client's click path too, a ref wart our stated divergence closes), and the `B` binding
/// ran the BUTTON handler instead of the ref's bare `ToggleBackpack()` body, dragging that tail
/// onto the one path the real client lights. Now: a real click on the button ends lit while
/// Bagnon's window is open and unlit when it closes, and the bare-global path (the binding's
/// body) does the same.
#[test]
fn the_backpack_button_lights_while_bagnon_holds_the_bags() {
let root = corpus_or_skip!();
let mut s = open_bagnon(&root);
// The fixture opened Bagnon through the bare global (the binding's own body): OnShow's
// SetChecked(1) must be standing — nothing runs after it on that path.
assert!(
s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
.unwrap(),
"the bare ToggleBackpack() path (the B binding) must leave the button lit"
);
// A REAL click on the button: auto-toggle flips, the wrapper's undo repairs it, Bagnon's
// OnHide writes the truth — closed and unlit.
let (x, y) = centre_of(&mut s, "MainMenuBarBackpackButton");
s.mouse_move(x, y);
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
assert!(
!s.eval::<bool>("return Bagnon:IsVisible() or false")
.unwrap(),
"the click must toggle Bagnon closed through its override"
);
assert!(
!s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
.unwrap(),
"closed ⇒ unlit (Bagnon's OnHide write is the last word)"
);
// And the second click: open again, lit again — the director's report, closed.
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
assert!(
s.eval::<bool>("return Bagnon:IsVisible() or false")
.unwrap(),
"the second click re-opens Bagnon"
);
assert!(
s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
.unwrap(),
"open ⇒ lit (Bagnon's OnShow write is the last word)"
);
}
@@ -826,6 +826,7 @@ mod clip_plumb_tests {
frame:SetScrollChild(child)
local marker = child:CreateTexture(nil, "ARTWORK")
marker:SetTexture(1, 0, 0) -- pathless colored quad: no BLP asset needed
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
"#,
)
.unwrap();
@@ -911,6 +912,7 @@ mod clip_plumb_tests {
plain:SetSize(50, 50)
local m = plain:CreateTexture(nil, "ARTWORK")
m:SetTexture(0, 1, 0)
m:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
"#,
)
.unwrap();
@@ -970,6 +972,7 @@ mod extract_gate_tests {
plain:SetSize(50, 50)
marker = plain:CreateTexture(nil, "ARTWORK")
marker:SetTexture(1, 0, 0)
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
"#,
)
.unwrap();