Files
benilla-octo/crates/benilla-app/src/ui_script/micro_menu_tests.rs
T
SamandClaude Fable 5.1 4c61e6b7c6 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>
2026-09-04 20:54:27 +07:00

315 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The shipped `assets/ui/MicroMenu.xml` — the eight micro buttons in the main bar's right-hand
//! recess — loaded behind `ActionBar.xml` (their anchor target `MainMenuBarArtFrame` and the
//! `BenillaActionBarArt_SeatAbove` seating helper) into a bare engine.
//!
//! What these guard, in order: the row's ref geometry (29×58 at art-frame BOTTOMLEFT +(552,2), a
//! 26 px stride) — which is the whole point of the slice, since the gap the row fills is exactly
//! that panel of empty bar art; the `HitRectInsets` header that keeps a button's transparent top
//! from eating the mouse; `UpdateMicroButtons` tracking a panel's visibility; and
//! `UpdateTalentButton`'s level-10 gate closing the row up behind the hidden talent button.
use benilla_ui::script::{QuadContent, ScriptValue, TexCoords, UiScript};
/// Load `ActionBar.xml` then `MicroMenu.xml` into a 1024×768 engine, asserting both load clean.
/// 1024 wide is deliberate: the 1024-wide bar then centers at x=0, so every ref offset below is
/// also an absolute screen coordinate.
fn harness() -> UiScript {
harness_with(&[])
}
/// …with more shipped files behind the row — the tooltip test needs the real plate.
fn harness_with(extra: &[&str]) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
let files: Vec<&str> = [
"Interface\\FrameXML\\Fonts.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.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()
.chain(extra.iter().copied())
.collect();
// Through `test_ui::load_ui`, not off disk: the list above now names a chain file, and a
// reader that joins `assets/ui` cannot resolve one (the wall 1838 hit, and 1887 again).
for file in files {
super::test_ui::load_ui(&s, file);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
}
/// The row's geometry, quoted from ref-MainMenuBarMicroButtons.xml: CharacterMicroButton at the
/// art frame's BOTTOMLEFT +(552,2), each button 29×58, the rest chained BOTTOMLEFT-to-previous-
/// BOTTOMRIGHT +(-3,0) ⇒ a 26 px stride, so the eight run x 552..757 — inside the 1024-wide bar,
/// between the page arrows (x≈522) and the bag cluster at the far right.
#[test]
fn the_micro_row_sits_where_the_reference_puts_it() {
let mut s = harness();
// Past level 10 so all eight are shown and the row is unbroken.
set_player_level(&mut s, 10);
s.resolve();
let names = [
"CharacterMicroButton",
"SpellbookMicroButton",
"TalentMicroButton",
"QuestLogMicroButton",
"SocialsMicroButton",
"WorldMapMicroButton",
"MainMenuMicroButton",
"HelpMicroButton",
];
for (i, name) in names.iter().enumerate() {
let (left, bottom, w, h) = s
.eval::<(f64, f64, f64, f64)>(&format!(
"return {name}:GetLeft(), {name}:GetBottom(), {name}:GetWidth(), {name}:GetHeight()"
))
.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!((w, h), (29.0, 58.0), "{name} size");
assert_eq!(left, 552.0 + 26.0 * i as f64, "{name} left edge");
assert_eq!(bottom, 2.0, "{name} sits 2 above the bar's bottom");
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The `HitRectInsets top="18"` header: the micro-button art fills only the lower ~40 of the 58,
/// and the empty top must stay transparent to the mouse — otherwise the row eats hover over the
/// bar's XP strip and the sliver of world above it.
#[test]
fn the_transparent_top_of_a_micro_button_does_not_capture_the_mouse() {
let mut s = harness();
set_player_level(&mut s, 10);
s.resolve();
// Mid-button horizontally (552 + 14), inside the art band (y 2..42) ⇒ captures.
assert_eq!(
s.hit_test_name(566.0, 20.0).as_deref(),
Some("CharacterMicroButton"),
"the art band takes the mouse"
);
// Same column, in the dead 18-unit header (y 42..60) ⇒ nothing of ours captures it.
assert_ne!(
s.hit_test_name(566.0, 50.0).as_deref(),
Some("CharacterMicroButton"),
"the inset header must be transparent to the mouse"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The character button's face: the region binds the SAME `"player"` portrait slot the unit frame
/// samples, and carries the ref's crop window out to the renderer. Both halves matter — the crop is
/// what makes an 18×25 rectangle show a face instead of the whole square bake squashed into it, and
/// the extract path used to pin portrait quads to full UVs and drop it (fixed with this slice).
/// Pushing the button swaps the window and dims it (ref CharacterMicroButton_SetPushed).
#[test]
fn the_character_button_carries_the_player_portrait_through_the_reference_crop() {
let mut s = harness();
s.resolve();
let window = |s: &mut UiScript| {
s.extract()
.into_iter()
.find_map(|q| match q.content {
QuadContent::Texture {
portrait_unit: Some(unit),
tex_coords,
circular,
..
} => Some((unit, tex_coords, circular)),
_ => None,
})
.expect("the micro button's portrait quad")
};
let (unit, coords, circular) = window(&mut s);
assert_eq!(unit, "player");
assert!(
circular,
"the ref's round stencil lives in the bake's own UV space, so the crop below yields a \
rectangular slice OF a masked face — not an ellipse fitted to this 18x25 region"
);
let round4 = |c: TexCoords| match c {
TexCoords::Rect(e) => e.map(|v| (v * 10_000.0).round() / 10_000.0),
TexCoords::Corners(_) => panic!("the 4-edge form"),
};
assert_eq!(
round4(coords.expect("a crop window")),
[0.2, 0.8, 0.0666, 0.9],
"the normal window (ref CharacterMicroButton_SetNormal)"
);
s.run("CharacterMicroButton_SetPushed()").unwrap();
s.resolve();
let (_, coords, _) = window(&mut s);
assert_eq!(
round4(coords.expect("a crop window")),
[0.2666, 0.8666, 0.0, 0.8333],
"the held-down window (ref CharacterMicroButton_SetPushed)"
);
assert_eq!(
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
.unwrap(),
0.5,
"…and the face dims while the button is down"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UpdateMicroButtons` (ref MainMenuBarMicroButtons.lua l.20-84): a button is PUSHED exactly
/// while its panel is up. Driven here with a stand-in frame under the real panel's name rather
/// than the whole character window — this is the visibility contract, not that window's.
#[test]
fn a_micro_button_pushes_while_its_panel_is_open() {
let s = harness();
s.run(
r#"
local f = CreateFrame("Frame", "CharacterFrame")
f:SetPoint("CENTER", 0, 0); f:SetSize(100, 100); f:Hide()
"#,
)
.unwrap();
let state = |s: &UiScript| {
s.eval::<String>("return CharacterMicroButton:GetButtonState()")
.unwrap()
};
s.run("UpdateMicroButtons()").unwrap();
assert_eq!(state(&s), "NORMAL", "closed panel ⇒ button up");
s.run("CharacterFrame:Show(); UpdateMicroButtons()")
.unwrap();
assert_eq!(state(&s), "PUSHED", "open panel ⇒ button held down");
s.run("CharacterFrame:Hide(); UpdateMicroButtons()")
.unwrap();
assert_eq!(state(&s), "NORMAL", "closed again ⇒ button pops back");
// A button whose panel doesn't exist at all (the three unwired ones, and any panel a test
// harness didn't load) never pushes — and never errors.
assert_eq!(
s.eval::<String>("return SocialsMicroButton:GetButtonState()")
.unwrap(),
"NORMAL"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UpdateTalentButton` (ref l.133-141): under level 10 the talent button is hidden and the quest
/// log button slides onto its seat, so the row has no hole in it; at 10 the button returns and the
/// tail shifts back out by one stride.
#[test]
fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
let mut s = harness();
set_player_level(&mut s, 9);
s.resolve();
assert!(
!s.eval::<bool>("return TalentMicroButton:IsVisible()")
.unwrap(),
"no talents before 10"
);
// The quest log button takes the talent button's own seat — slot 3, not slot 4.
assert_eq!(
s.eval::<f64>("return QuestLogMicroButton:GetLeft()")
.unwrap(),
552.0 + 26.0 * 2.0
);
assert_eq!(
s.eval::<f64>("return HelpMicroButton:GetLeft()").unwrap(),
552.0 + 26.0 * 6.0,
"the whole tail moves up one slot with it"
);
set_player_level(&mut s, 10);
s.resolve();
assert!(
s.eval::<bool>("return TalentMicroButton:IsVisible()")
.unwrap(),
"the button returns at 10"
);
assert_eq!(
s.eval::<f64>("return QuestLogMicroButton:GetLeft()")
.unwrap(),
552.0 + 26.0 * 3.0
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The hover (ref MainMenuBarMicroButton's OnEnter, l.13): every button's plate is the ref's
/// TWO-line `GameTooltip_AddNewbieTip` — the label, then that button's own `NEWBIE_TOOLTIP_*`
/// explanation in gold. 1.12 ships detailed tips ON (`SHOW_NEWBIE_TIPS = "1"`, ref
/// UIOptionsFrame.lua l.100), so the paragraph is the DEFAULT hover; this file used to open-code
/// the tips-off branch and show a bare label. Decision 0661.
///
/// Checked across all eight, because the explanation is threaded per button through OnLoad and a
/// single missing argument is invisible until someone hovers that one button.
#[test]
fn every_micro_button_hovers_with_its_reference_explanation() {
let mut s = harness_with(&["MoneyFrame.xml", "UIParent.xml", "GameTooltip.xml"]);
set_player_level(&mut s, 10);
s.resolve();
for (button, newbie) in [
("CharacterMicroButton", "NEWBIE_TOOLTIP_CHARACTER"),
("SpellbookMicroButton", "NEWBIE_TOOLTIP_SPELLBOOK"),
("TalentMicroButton", "NEWBIE_TOOLTIP_TALENTS"),
("QuestLogMicroButton", "NEWBIE_TOOLTIP_QUESTLOG"),
("SocialsMicroButton", "NEWBIE_TOOLTIP_SOCIAL"),
("WorldMapMicroButton", "NEWBIE_TOOLTIP_WORLDMAP"),
("MainMenuMicroButton", "NEWBIE_TOOLTIP_MAINMENU"),
("HelpMicroButton", "NEWBIE_TOOLTIP_HELP"),
] {
s.run(&format!("BenillaMicroButton_OnEnter({button})"))
.unwrap();
assert_eq!(
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
2,
"{button}: the label, then the explanation"
);
// Line 1 is the button's own label (plus a `|cff…(key)|r` suffix on the five bound ones).
let label = s.eval::<String>(&format!("return {button}.label")).unwrap();
let line1 = s
.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap();
assert!(
line1.starts_with(&label),
"{button}: line 1 is {line1:?}, expected it to open with {label:?}"
);
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft2:GetText()")
.unwrap(),
s.eval::<String>(&format!("return {newbie}")).unwrap(),
"{button}: line 2 is the ref's {newbie}, verbatim"
);
assert_eq!(
s.eval::<i64>("return GameTooltip.default").unwrap(),
1,
"{button}: the default-corner anchor, not ANCHOR_RIGHT off the button"
);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Set the player's level and fire the `UNIT_LEVEL` the talent gate listens for.
fn set_player_level(s: &mut UiScript, level: u32) {
s.set_unit(
"player",
Some(benilla_ui::script::UnitState {
exists: true,
level,
..Default::default()
}),
);
s.fire_event("UNIT_LEVEL", vec![ScriptValue::Str("player".into())]);
}