This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
402 lines
14 KiB
Rust
402 lines
14 KiB
Rust
//! The eight stock micro buttons, loaded behind `MainMenuBarArtFrame`, which they anchor to.
|
||
|
||
use benilla_ui::script::{QuadContent, ScriptValue, TexCoords, UiScript, UnitState};
|
||
|
||
const ROW: [&str; 8] = [
|
||
"CharacterMicroButton",
|
||
"SpellbookMicroButton",
|
||
"TalentMicroButton",
|
||
"QuestLogMicroButton",
|
||
"SocialsMicroButton",
|
||
"WorldMapMicroButton",
|
||
"MainMenuMicroButton",
|
||
"HelpMicroButton",
|
||
];
|
||
|
||
/// At 1024 wide the 1024-wide bar sits at x 0, so every stock offset is a screen coordinate.
|
||
fn harness() -> UiScript {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
for file in [
|
||
"Interface\\FrameXML\\Fonts.xml",
|
||
"Interface\\FrameXML\\Cooldown.xml",
|
||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||
r"Interface\FrameXML\UIParent.xml",
|
||
// The labels each button's OnLoad reads, through `TEXT()`.
|
||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||
"Interface\\FrameXML\\BasicControls.xml",
|
||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||
r"Interface\FrameXML\MoneyFrame.lua",
|
||
r"Interface\FrameXML\MoneyFrame.xml",
|
||
"Interface\\FrameXML\\GameTooltip.xml",
|
||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
|
||
] {
|
||
super::test_ui::load_ui(&s, file);
|
||
}
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
s
|
||
}
|
||
|
||
fn set_player_level(s: &mut UiScript, level: u32) {
|
||
s.set_unit(
|
||
"player",
|
||
Some(UnitState {
|
||
exists: true,
|
||
level,
|
||
..Default::default()
|
||
}),
|
||
);
|
||
s.fire_event("UNIT_LEVEL", vec![ScriptValue::Str("player".into())]);
|
||
}
|
||
|
||
fn left_of(s: &UiScript, name: &str) -> f64 {
|
||
s.eval::<f64>(&format!("return {name}:GetLeft()"))
|
||
.unwrap_or_else(|e| panic!("{name}: {e}"))
|
||
}
|
||
|
||
/// `CharacterMicroButton` sits at the art frame's `BOTTOMLEFT` +(552, 2), each button 29x58,
|
||
/// chained at -3 for a 26 px stride; `UpdateTalentButton` re-seats the quest log at -2
|
||
/// (`MainMenuBarMicroButtons.lua:139`), so past its first pass at 10+ the tail sits 1 px right.
|
||
#[test]
|
||
fn the_micro_row_sits_where_the_reference_puts_it() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.resolve();
|
||
|
||
for (i, name) in ROW.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, as declared"
|
||
);
|
||
assert_eq!(bottom, 2.0, "{name} sits 2 above the bar's bottom");
|
||
}
|
||
|
||
set_player_level(&mut s, 60);
|
||
s.resolve();
|
||
for (i, name) in ROW.iter().enumerate().skip(3) {
|
||
assert_eq!(
|
||
left_of(&s, name),
|
||
553.0 + 26.0 * i as f64,
|
||
"{name} after the talent gate's own −2"
|
||
);
|
||
}
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// `HitRectInsets top="18"` (`MainMenuBarMicroButtons.xml:8`): the art fills the lower 40 of 58.
|
||
#[test]
|
||
fn the_transparent_top_of_a_micro_button_does_not_capture_the_mouse() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.resolve();
|
||
|
||
// Mid-button (552 + 14); the art band is y 2..42, the inset header y 42..60.
|
||
assert_eq!(
|
||
s.hit_test_name(566.0, 20.0).as_deref(),
|
||
Some("CharacterMicroButton"),
|
||
"the art band takes the mouse"
|
||
);
|
||
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());
|
||
}
|
||
|
||
/// `MicroButtonPortrait` samples the `"player"` portrait through a crop window, so the 18x25
|
||
/// region shows a face; pushing swaps the window and dims it (`MainMenuBarMicroButtons.lua:109`).
|
||
#[test]
|
||
fn the_character_button_carries_the_player_portrait_through_the_reference_crop() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
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 reference'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 (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 (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());
|
||
}
|
||
|
||
/// Under level 10 `UpdateTalentButton` hides the talent button and seats the quest log on its
|
||
/// anchor (`MainMenuBarMicroButtons.lua:133`).
|
||
#[test]
|
||
fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
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 takes the talent button's seat: slot 3, not 4.
|
||
assert_eq!(left_of(&s, "QuestLogMicroButton"), 552.0 + 26.0 * 2.0);
|
||
assert_eq!(
|
||
left_of(&s, "HelpMicroButton"),
|
||
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!(
|
||
left_of(&s, "QuestLogMicroButton"),
|
||
553.0 + 26.0 * 3.0,
|
||
"…and the tail goes back out, at the gate's own −2"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The template's `OnEnter` (`MainMenuBarMicroButtons.xml:12`) reads `this`, so the hover goes
|
||
/// through the engine; a bound label gains its key on `UPDATE_BINDINGS`.
|
||
#[test]
|
||
fn every_micro_button_hovers_with_its_reference_explanation() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
|
||
s.register_bindings(&crate::bindings::registry_commands());
|
||
assert_eq!(
|
||
s.eval::<Option<i64>>("return SetBinding(\"C\", \"TOGGLECHARACTER0\")")
|
||
.unwrap(),
|
||
Some(1)
|
||
);
|
||
s.fire_event("UPDATE_BINDINGS", vec![]);
|
||
s.resolve();
|
||
|
||
for (button, label, newbie) in [
|
||
(
|
||
"CharacterMicroButton",
|
||
"CHARACTER_BUTTON",
|
||
"NEWBIE_TOOLTIP_CHARACTER",
|
||
),
|
||
// The spellbook's own `OnEnter` (`MainMenuBarMicroButtons.xml:79`) picks its label.
|
||
(
|
||
"SpellbookMicroButton",
|
||
"PlayerHasSpells() and SPELLBOOK_ABILITIES_BUTTON or ABILITYBOOK_BUTTON",
|
||
"NEWBIE_TOOLTIP_SPELLBOOK",
|
||
),
|
||
(
|
||
"TalentMicroButton",
|
||
"TALENTS_BUTTON",
|
||
"NEWBIE_TOOLTIP_TALENTS",
|
||
),
|
||
(
|
||
"QuestLogMicroButton",
|
||
"QUESTLOG_BUTTON",
|
||
"NEWBIE_TOOLTIP_QUESTLOG",
|
||
),
|
||
(
|
||
"SocialsMicroButton",
|
||
"SOCIAL_BUTTON",
|
||
"NEWBIE_TOOLTIP_SOCIAL",
|
||
),
|
||
(
|
||
"WorldMapMicroButton",
|
||
"WORLDMAP_BUTTON",
|
||
"NEWBIE_TOOLTIP_WORLDMAP",
|
||
),
|
||
(
|
||
"MainMenuMicroButton",
|
||
"MAINMENU_BUTTON",
|
||
"NEWBIE_TOOLTIP_MAINMENU",
|
||
),
|
||
("HelpMicroButton", "HELP_BUTTON", "NEWBIE_TOOLTIP_HELP"),
|
||
] {
|
||
super::test_ui::hover(&mut s, button);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
|
||
2,
|
||
"{button}: the label, then the explanation"
|
||
);
|
||
let label = s.eval::<String>(&format!("return {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 reference's {newbie}, verbatim"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GameTooltip.default").unwrap(),
|
||
1,
|
||
"{button}: the default-corner anchor, not ANCHOR_RIGHT off the button"
|
||
);
|
||
super::test_ui::unhover(&mut s);
|
||
}
|
||
|
||
super::test_ui::hover(&mut s, "CharacterMicroButton");
|
||
let line1 = s
|
||
.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||
.unwrap();
|
||
assert_eq!(
|
||
line1,
|
||
s.eval::<String>(
|
||
"return CHARACTER_BUTTON .. \" \" .. NORMAL_FONT_COLOR_CODE .. \"(C)\" .. FONT_COLOR_CODE_CLOSE"
|
||
)
|
||
.unwrap(),
|
||
"the key read on UPDATE_BINDINGS"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The whole manifest, since `UpdateMicroButtons` reads every panel. A ding pulses the talent
|
||
/// button for 60 s unless the character sheet is open (`MainMenuBarMicroButtons.lua:120`).
|
||
#[test]
|
||
fn a_micro_button_pushes_while_its_panel_is_open_and_the_talent_button_pulses_on_a_ding() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
// A player exists before the manifest loads; the sheet formats its race and class.
|
||
s.set_unit(
|
||
"player",
|
||
Some(UnitState {
|
||
exists: true,
|
||
name: Some("Probefour".into()),
|
||
level: 60,
|
||
race: Some("Night Elf".into()),
|
||
race_file: Some("NightElf".into()),
|
||
class: Some("Warrior".into()),
|
||
class_file: Some("WARRIOR".into()),
|
||
sex: 2,
|
||
is_player: true,
|
||
player_controlled: true,
|
||
..Default::default()
|
||
}),
|
||
);
|
||
// The app runs GlobalStrings ahead of the manifest; the ding's chat lines format through it.
|
||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
assert!(super::load_default_ui(&s).is_empty());
|
||
s.resolve();
|
||
|
||
let state = |s: &UiScript, button: &str| {
|
||
s.eval::<String>(&format!("return {button}:GetButtonState()"))
|
||
.unwrap()
|
||
};
|
||
let face = |s: &UiScript| {
|
||
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
|
||
.unwrap()
|
||
};
|
||
|
||
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
|
||
assert_eq!(face(&s), 1.0);
|
||
// The button's `OnClick` (`MainMenuBarMicroButtons.xml:49`); the sheet's `OnShow` calls back.
|
||
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
|
||
assert_eq!(
|
||
state(&s, "CharacterMicroButton"),
|
||
"PUSHED",
|
||
"open sheet ⇒ button held down"
|
||
);
|
||
assert_eq!(
|
||
face(&s),
|
||
0.5,
|
||
"…and the face dims (CharacterMicroButton_SetPushed)"
|
||
);
|
||
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
|
||
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
|
||
assert_eq!(face(&s), 1.0);
|
||
|
||
s.run("ToggleWorldMap()").unwrap();
|
||
assert_eq!(state(&s, "WorldMapMicroButton"), "PUSHED");
|
||
s.run("ToggleWorldMap()").unwrap();
|
||
assert_eq!(state(&s, "WorldMapMicroButton"), "NORMAL");
|
||
|
||
// The nine args `ui_unit` fires: level, health, power, talent points, five stats.
|
||
s.fire_event(
|
||
"PLAYER_LEVEL_UP",
|
||
vec![
|
||
ScriptValue::Int(61),
|
||
ScriptValue::Int(12),
|
||
ScriptValue::Int(0),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(0),
|
||
ScriptValue::Int(0),
|
||
],
|
||
);
|
||
assert_eq!(
|
||
s.eval::<f64>("return TalentMicroButton.pulseTimeLeft")
|
||
.unwrap(),
|
||
60.0,
|
||
"the sixty-second pulse"
|
||
);
|
||
assert!(
|
||
s.eval::<bool>(
|
||
"for _, b in ipairs(PULSEBUTTONS) do if b == TalentMicroButton then return true end end \
|
||
return false"
|
||
)
|
||
.unwrap(),
|
||
"on the pulse list"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|