channel lines get their warm color, and a /wave gets its sentence
- a channel line renders in the channel family's color, not the grey notice row the client consults and throws away - a text emote composes its sentence — gender and target picked from the game's own tables — so a /wave is a line in chat, not just an animation - a FontString's `font=` resolves a font object by name and gates the whole block, `<Gradient>` tints the art it sits on, and buttons' state fonts read `font=` too — Bagnon's white boxes and bare stack counts are fixed - the money frame sizes its coins from the digits themselves, so Bagnon's gold stops opening cramped Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -966,3 +966,275 @@ fn the_whole_bagnon_window_survives_being_used() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bagnon's window with a **stacked** item in the backpack — the count FontString only paints when
|
||||
/// the stack is >1 (`SetItemButtonCount`), so the plain [`open_bagnon`] fixture's single Small
|
||||
/// Brown Pouch can never exercise it.
|
||||
fn open_bagnon_stacked(root: &Path) -> UiScript {
|
||||
let mut s = seat(root, Seat::BeforeAddons);
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
texture: Some("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
count: 200,
|
||||
item_id: 4496,
|
||||
quality: Some(1),
|
||||
link: Some("|cffffffff|Hitem:4496:0:0:0|h[Small Brown Pouch]|h|r".into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.set_container(
|
||||
0,
|
||||
Some(ContainerState {
|
||||
name: Some("Backpack".into()),
|
||||
num_slots: 16,
|
||||
slots,
|
||||
}),
|
||||
);
|
||||
s.run("ToggleBackpack()").expect("ToggleBackpack");
|
||||
for _ in 0..3 {
|
||||
s.tick(0.1);
|
||||
}
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
/// The resolved `QuadContent::Text` for the one named FontString, or a panic naming what was found.
|
||||
fn text_quad(s: &mut UiScript, owner: &str) -> QuadContent {
|
||||
s.resolve();
|
||||
s.extract()
|
||||
.iter()
|
||||
.find(|q| {
|
||||
matches!(&q.content, QuadContent::Text { text: Some(_), .. })
|
||||
&& s.target_owner_name(q.target).as_deref() == Some(owner)
|
||||
})
|
||||
.map(|q| q.content.clone())
|
||||
.unwrap_or_else(|| panic!("no text quad owned by {owner}"))
|
||||
}
|
||||
|
||||
/// **The director's report: "item stack size text is missing the black bg."**
|
||||
///
|
||||
/// Bagnon writes `<FontString name="$parentCount" font="NumberFontNormal">`
|
||||
/// (`Bagnon_Core/core/Item.xml:14`) — the `font=` attribute, which the reference resolves against
|
||||
/// the font-object registry BEFORE the filesystem (`0x783d15 call 0x783870(value, create = 0)`;
|
||||
/// see [`super::super::loader`]'s `apply_fontstring_font`). We read it as a path only, so the
|
||||
/// literal string `"NumberFontNormal"` became the face: no height, no colour, and no
|
||||
/// `outline="NORMAL"` — and the atlas fell back silently to Friz 12.
|
||||
///
|
||||
/// **The black is the OUTLINE, not a shadow.** `NumberFontNormal` carries no `<Shadow>` in 1.12
|
||||
/// (`Fonts.xml:226`, matching the install byte for byte), so `shadow: None` here is correct and is
|
||||
/// asserted as such — a future "fix" that adds one would be a divergence, not an improvement.
|
||||
#[test]
|
||||
fn a_stack_count_wears_the_font_object_its_font_attr_names() {
|
||||
let root = corpus_or_skip!();
|
||||
let mut s = open_bagnon_stacked(&root);
|
||||
|
||||
let QuadContent::Text {
|
||||
text,
|
||||
font,
|
||||
font_height,
|
||||
outline,
|
||||
shadow,
|
||||
color,
|
||||
..
|
||||
} = text_quad(&mut s, "BagnonItem1")
|
||||
else {
|
||||
unreachable!("filtered to Text above")
|
||||
};
|
||||
|
||||
assert_eq!(text.as_deref(), Some("200"), "the stack count is on screen");
|
||||
// Every one of these was lost while `font=` was read as a path. The face is the assertion that
|
||||
// the registry was consulted at all; the outline is the one the director can SEE.
|
||||
assert_eq!(
|
||||
font.as_deref(),
|
||||
Some("Fonts\\ARIALN.TTF"),
|
||||
"`font=\"NumberFontNormal\"` must resolve the font OBJECT, not become a font path"
|
||||
);
|
||||
assert_eq!(font_height, Some(14.0), "NumberFontNormal's height");
|
||||
assert_eq!(
|
||||
outline,
|
||||
benilla_ui::script::Outline::Normal,
|
||||
"the black ring around a stack count IS `outline=\"NORMAL\"` — this is the reported symptom"
|
||||
);
|
||||
assert_eq!(
|
||||
color,
|
||||
Some([1.0, 1.0, 1.0, 1.0]),
|
||||
"NumberFontNormal's white"
|
||||
);
|
||||
assert_eq!(
|
||||
shadow, None,
|
||||
"and NO drop shadow: NumberFontNormal has no <Shadow> in 1.12 either (Fonts.xml:226). \
|
||||
The readability is the outline; adding a shadow here would be a divergence"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The director's report: the character dropdown draws as a solid WHITE box.**
|
||||
///
|
||||
/// `BagnonPopupFrame` (`Bagnon_Core/core/Frame.xml:23-36`) backs itself with
|
||||
/// `Interface\ChatFrame\ChatFrameBackground` — art that is white by design — and tints it
|
||||
/// black→dark-grey with a `<Gradient>`. The loader parsed `<Color>` and `<TexCoords>` but never
|
||||
/// `<Gradient>`, and dropped it in silence, so the white art rendered untinted.
|
||||
///
|
||||
/// `<Gradient>` survives beside a `file=` where `<Color>` does not, and that asymmetry is the
|
||||
/// point: they land in different fields (vertex colours `+0xb8` vs the texture `+0xcc` —
|
||||
/// wow-re `texture-color-composition.md` §1-2).
|
||||
#[test]
|
||||
fn a_texture_gradient_tints_the_art_it_sits_on() {
|
||||
let root = corpus_or_skip!();
|
||||
let mut s = open_bagnon_stacked(&root);
|
||||
s.run("BagnonDBUI_ShowCharacterList(Bagnon)")
|
||||
.expect("BagnonDBUI_ShowCharacterList");
|
||||
for _ in 0..3 {
|
||||
s.tick(0.1);
|
||||
}
|
||||
s.resolve();
|
||||
|
||||
let bg = s
|
||||
.extract()
|
||||
.iter()
|
||||
.find(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("ChatFrameBackground"))
|
||||
&& s.target_owner_name(q.target).as_deref() == Some("BagnonDBUICharacterList")
|
||||
})
|
||||
.map(|q| q.content.clone())
|
||||
.expect("the popup's background art reaches the render list");
|
||||
|
||||
let QuadContent::Texture { color, .. } = bg else {
|
||||
unreachable!("filtered to Texture above")
|
||||
};
|
||||
// The two stops are (0,0,0,0.9) and (0.2,0.2,0.2,0.9); `RegionData::gradient` is folded to its
|
||||
// midpoint by the paint, so this is 0.1 grey at the stops' shared alpha. `None` — or white —
|
||||
// is the bug: untinted ChatFrameBackground is the white slab on the director's screen.
|
||||
assert_eq!(
|
||||
color,
|
||||
Some([0.1, 0.1, 0.1, 0.9]),
|
||||
"the <Gradient> must tint the art; untinted this is the reported white box"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The director's report: "when I first open the bags with bagnon the gold numbers are all
|
||||
/// cramped up; if I close and open again it looks good."**
|
||||
///
|
||||
/// Bagnon's money display is `SmallMoneyFrameTemplate` — OUR `MoneyFrame.xml`. Its `ShowCoin` used
|
||||
/// to size each coin with `label:GetStringWidth()`, which is served from the measure round-trip and
|
||||
/// therefore reads **0 in the tick that set the text**: every coin came out at exactly one icon
|
||||
/// width and the digits overlapped. The second open looked right because the first open's measure
|
||||
/// had landed in the cache by then — the reopen was reading the previous open's numbers.
|
||||
///
|
||||
/// The fix sums `BENILLA_DIGIT_W`, the app's per-digit advance feed
|
||||
/// ([`benilla_ui::script::UiScript::set_digit_advances`]) — data pushed ahead, so the answer exists
|
||||
/// *in* the tick. That feed's own doc names this frame as what it was built for; only the merchant
|
||||
/// price had ever used it.
|
||||
///
|
||||
/// The assertion is the FIRST open, which is the half that was broken.
|
||||
#[test]
|
||||
fn a_money_frame_is_the_right_width_on_the_first_open() {
|
||||
let root = corpus_or_skip!();
|
||||
let mut s = seat(&root, Seat::BeforeAddons);
|
||||
|
||||
// 12345g 67s 89c — three denominations with different digit counts, so a width that ignored
|
||||
// the digits (the bug) cannot coincide with one that counts them.
|
||||
s.set_money(123_456_789);
|
||||
// The app feeds this once per atlas scale, before any window opens. A flat 8px advance makes
|
||||
// the expected widths exact arithmetic rather than a font-dependent number.
|
||||
s.set_digit_advances(&[8.0; 10]);
|
||||
|
||||
s.run("ToggleBackpack()").expect("ToggleBackpack");
|
||||
for _ in 0..3 {
|
||||
s.tick(0.1);
|
||||
}
|
||||
s.resolve();
|
||||
|
||||
let widths: Vec<f32> = s
|
||||
.eval(
|
||||
"local o = {} \
|
||||
for _, n in ipairs({\"Gold\", \"Silver\", \"Copper\"}) do \
|
||||
local b = getglobal(\"BagnonMoneyFrame\" .. n .. \"Button\") \
|
||||
table.insert(o, b and b:GetWidth() or -1) \
|
||||
end \
|
||||
return o",
|
||||
)
|
||||
.expect("the three coin buttons");
|
||||
|
||||
// MONEY_ICON_WIDTH_SMALL is 13; the digits are 8px each. 12345 → 5, 67 → 2, 89 → 2.
|
||||
assert_eq!(
|
||||
widths,
|
||||
vec![5.0 * 8.0 + 13.0, 2.0 * 8.0 + 13.0, 2.0 * 8.0 + 13.0],
|
||||
"each coin must be its digits PLUS its icon on the first open. All three coming back at \
|
||||
13.0 — the bare icon width — is the reported cramping: it means the width was taken from \
|
||||
a text measure that had not landed yet"
|
||||
);
|
||||
}
|
||||
|
||||
/// The font a FontString/Button label actually resolved: `(path, height)`.
|
||||
fn resolved_font(s: &UiScript, lua_expr: &str) -> (String, String) {
|
||||
let r: Vec<String> = s
|
||||
.eval(&format!(
|
||||
"local fs = {lua_expr} \
|
||||
if not fs then return {{ \"MISSING\", \"MISSING\" }} end \
|
||||
local p, h = fs:GetFont() \
|
||||
return {{ tostring(p), tostring(h) }}"
|
||||
))
|
||||
.expect("GetFont");
|
||||
(r[0].clone(), r[1].clone())
|
||||
}
|
||||
|
||||
/// **A Button's `<NormalFont>`/`<HighlightFont>`/`<DisabledFont>` take `font=` as well as
|
||||
/// `inherits=`** — they are `<Font>`-TYPED elements, routed by `CSimpleButton::LoadXML 0x7788c0`
|
||||
/// at `0x778bf4` into the same `0x783c30` a top-level `<Font>` uses, so `font=` gets the identical
|
||||
/// registry-first treatment (`0x783d15` → `0x783d22 call 0x770c60`).
|
||||
///
|
||||
/// We read only `inherits=`, so every corpus button declaring `font=` silently kept **no font
|
||||
/// object at all**. Bagnon does it at seven sites; `BagnonDBUINameBox` asks for
|
||||
/// `GameFontNormalLarge` (16px) and got nothing. Real FrameXML always writes `inherits=`, which is
|
||||
/// why nothing we ship ever noticed.
|
||||
///
|
||||
/// `style=` is deliberately not read — it does not exist in 1.12.1 (an isolated-token scan returns
|
||||
/// zero against nine controls each returning one); it is a later-client idiom.
|
||||
#[test]
|
||||
fn a_button_state_font_takes_the_font_attribute_not_just_inherits() {
|
||||
let root = corpus_or_skip!();
|
||||
let mut s = open_bagnon_stacked(&root);
|
||||
s.run("BagnonDBUI_ShowCharacterList(Bagnon)")
|
||||
.expect("BagnonDBUI_ShowCharacterList");
|
||||
for _ in 0..3 {
|
||||
s.tick(0.1);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
resolved_font(
|
||||
&s,
|
||||
"getglobal(\"BagnonDBUICharacterList1\"):GetFontString()"
|
||||
),
|
||||
("Fonts\\FRIZQT__.TTF".into(), "16".into()),
|
||||
"`<NormalFont font=\"GameFontNormalLarge\"/>` must link the font object — 16px, not the \
|
||||
12px default, and emphatically not the nil this returned while only `inherits=` was read"
|
||||
);
|
||||
}
|
||||
|
||||
/// **A `<FontHeight>` with no `font=` beside it is dead XML**, and so is a bare `outline=`.
|
||||
///
|
||||
/// `CSimpleFontString::LoadXML`'s height/outline/monochrome block sits at `[0x77111e, 0x771254)`
|
||||
/// and its ONLY predecessor is the `font=`-names-a-file miss leg. With no `font=` at all,
|
||||
/// `0x7710f1`/`0x7710fa je 0x771254` jump straight past it: the attributes are never parsed. They
|
||||
/// have no independent existence — they are companions of a font FILE path, nothing more.
|
||||
///
|
||||
/// The reference's own FrameXML contains exactly one such site, and it is the proof: `ZoneText.xml`
|
||||
/// gives `AutoFollowStatusText` `inherits="GameFontNormal"` (12px) **and** a
|
||||
/// `<FontHeight val="20">`. The real client draws that line at 12 and Blizzard's 20 never does
|
||||
/// anything. We honoured it — one of the few places benilla was drawing text at a size the
|
||||
/// reference does not.
|
||||
#[test]
|
||||
fn a_fontheight_with_no_font_attr_beside_it_is_never_read() {
|
||||
let root = corpus_or_skip!();
|
||||
let s = open_bagnon_stacked(&root);
|
||||
assert_eq!(
|
||||
resolved_font(&s, "AutoFollowStatusText"),
|
||||
("Fonts\\FRIZQT__.TTF".into(), "12".into()),
|
||||
"AutoFollowStatusText inherits GameFontNormal (12) and declares <FontHeight val=20> with \
|
||||
no font= — the 20 is dead XML in the reference, so 20 here means we are reading an \
|
||||
attribute the client never reaches"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ fn harness(money: u64) -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.set_money(money);
|
||||
// The digit advances the app feeds once per atlas scale. Fed here so the width arithmetic
|
||||
// below is deliberate rather than an artefact of an unfed VM — a flat 8px per digit makes
|
||||
// every expected number readable as `digits x 8 + icon`.
|
||||
s.set_digit_advances(&[8.0; 10]);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
|
||||
@@ -206,20 +210,25 @@ fn update_paints_a_static_frame_by_name_and_resizes_it() {
|
||||
);
|
||||
|
||||
// Every coin is its digits + a 13px icon (MONEY_ICON_WIDTH_SMALL, since this frame is `small`),
|
||||
// and the frame is the surviving coins packed with the -4px MONEY_BUTTON_SPACING gap. Headless
|
||||
// there is no font atlas, so the digit width is 0 and the arithmetic is exactly the icons:
|
||||
// 13 (base) + 13 + 13 + 13 - (-4) - (-4) = 60.
|
||||
// and the frame is the surviving coins packed with the -4px MONEY_BUTTON_SPACING gap:
|
||||
// 13 (base) + 21 + 21 + 21 - (-4) - (-4) = 84, each 21 being one 8px digit plus the icon
|
||||
// (2g 3s 4c is a single digit per denomination).
|
||||
//
|
||||
// **The digits used to contribute 0 here and this test asserted the bare 13s that came of it.**
|
||||
// That was the director's cramped gold on a first open: `ShowCoin` sized each coin from a text
|
||||
// measure that lands a frame later, so it read 0 every first time. It sums the engine's
|
||||
// `BenillaNumberWidth` feed now, which answers in the same tick.
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return TestPurseGoldButton:GetWidth()")
|
||||
.unwrap(),
|
||||
13.0
|
||||
21.0
|
||||
);
|
||||
assert_eq!(s.eval::<f64>("return TestPurse:GetWidth()").unwrap(), 60.0);
|
||||
assert_eq!(s.eval::<f64>("return TestPurse:GetWidth()").unwrap(), 84.0);
|
||||
|
||||
// Collapse away two denominations and the frame shrinks to match: 13 + 13 = 26.
|
||||
// Collapse away two denominations and the frame shrinks to match: 13 + 21 = 34.
|
||||
s.run("MoneyFrame_Update(\"TestPurse\", 9)").unwrap();
|
||||
assert_eq!(shown(&s, "TestPurse"), (false, false, true));
|
||||
assert_eq!(s.eval::<f64>("return TestPurse:GetWidth()").unwrap(), 26.0);
|
||||
assert_eq!(s.eval::<f64>("return TestPurse:GetWidth()").unwrap(), 34.0);
|
||||
}
|
||||
|
||||
/// The large template uses the 19px icon and its own spacing — the same painter, the other size
|
||||
@@ -227,15 +236,17 @@ fn update_paints_a_static_frame_by_name_and_resizes_it() {
|
||||
#[test]
|
||||
fn the_large_template_measures_with_the_nineteen_pixel_icon() {
|
||||
let s = harness(20_304);
|
||||
// One 8px digit + the 19px icon — the same digit sum as the small template, over the other
|
||||
// icon constant. That the two differ ONLY by the icon is what this test is for.
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return TestBigPurseGoldButton:GetWidth()")
|
||||
.unwrap(),
|
||||
19.0
|
||||
8.0 + 19.0
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return TestBigPurse:GetWidth()").unwrap(),
|
||||
19.0 * 4.0 + 8.0,
|
||||
"19 base + three 19px coins, packed with two -4px gaps"
|
||||
19.0 + 27.0 * 3.0 + 8.0,
|
||||
"19 base + three 27px coins, packed with two -4px gaps"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return MONEY_ICON_WIDTH").unwrap(),
|
||||
|
||||
Reference in New Issue
Block a user