Files
benilla-octo/crates/benilla-app/src/ui_script/zone_text_tests.rs
T
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
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
2026-10-15 15:01:14 -06:00

378 lines
14 KiB
Rust

//! Stock `ZoneText.xml`, driven by hand as `crate::area::feed_zone_events` drives it: host globals
//! written, the event fired, the clock ticked. A plain `ZONE_CHANGED` re-caches the zone name
//! silently (`ZoneText.xml:92`), so a later `ZONE_CHANGED_NEW_AREA` on that name never splashes.
use benilla_ui::script::UiScript;
use super::test_ui::load_ui as load_xml;
fn visible(s: &UiScript, frame: &str) -> bool {
s.eval::<bool>(&format!("return {frame}:IsVisible() and true or false"))
.unwrap()
}
fn text_of(s: &UiScript, fontstring: &str) -> String {
s.eval::<String>(&format!("return {fontstring}:GetText() or ''"))
.unwrap()
}
/// The host globals the app writes on an area change, in long brackets so an apostrophe survives.
fn set_area(s: &UiScript, zone: &str, sub: &str, pvp: &str, faction: &str) {
s.run(&format!(
"__benilla_zone_name = [[{zone}]]; __benilla_subzone_name = [[{sub}]]; \
__benilla_pvp_type = [[{pvp}]]; __benilla_pvp_faction = [[{faction}]]; \
__benilla_pvp_arena = false"
))
.unwrap();
}
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The splash formats its territory and autofollow lines straight out of GlobalStrings.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// `TEXT()`, which AutoFollowStatus_OnEvent puts its message through.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// The fading kit, its own `FrameXML.toc` entry.
load_xml(&s, "Interface\\FrameXML\\FadingFrame.xml");
load_xml(&s, "Interface\\FrameXML\\ZoneText.xml");
s
}
#[test]
fn new_area_splashes_zone_pvp_and_subzone_then_fades_out() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
// OnLoad hides both fading frames.
assert!(!visible(&s, "ZoneTextFrame"));
assert!(!visible(&s, "SubZoneTextFrame"));
set_area(&s, "Westfall", "", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
assert!(visible(&s, "ZoneTextFrame"), "zone splash shows");
assert_eq!(text_of(&s, "ZoneTextString"), "Westfall");
assert_eq!(text_of(&s, "PVPInfoTextString"), "Alliance Territory");
assert_eq!(text_of(&s, "SubZoneTextString"), "");
// The stock timeline: in 0.5 s, hold 1.0 s, out 2.0 s (`ZoneText.xml:5`).
s.tick(0.25);
let alpha: f32 = s.eval("return ZoneTextFrame:GetAlpha()").unwrap();
assert!((alpha - 0.5).abs() < 0.05, "mid-fade-in alpha, got {alpha}");
s.tick(0.5);
let alpha: f32 = s.eval("return ZoneTextFrame:GetAlpha()").unwrap();
assert!((alpha - 1.0).abs() < 0.01, "hold alpha, got {alpha}");
s.tick(3.0);
assert!(!visible(&s, "ZoneTextFrame"), "fade-out ends in Hide()");
}
#[test]
fn subzone_hop_shows_only_the_small_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0); // let the login splash finish
assert!(!visible(&s, "ZoneTextFrame"));
set_area(&s, "Elwynn Forest", "Goldshire", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED", vec![]);
assert!(visible(&s, "SubZoneTextFrame"), "subzone splash shows");
assert!(
!visible(&s, "ZoneTextFrame"),
"the big zone name stays hidden on a subzone hop"
);
assert_eq!(text_of(&s, "SubZoneTextString"), "Goldshire");
}
#[test]
fn plain_zone_changed_recaches_silently_so_new_area_wont_resplash() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
assert!(!visible(&s, "ZoneTextFrame"));
set_area(&s, "Westfall", "The Jansen Stead", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED", vec![]);
assert!(
!visible(&s, "ZoneTextFrame"),
"plain ZONE_CHANGED never splashes the zone name"
);
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
assert!(
!visible(&s, "ZoneTextFrame"),
"NEW_AREA on the already-cached zone text must not re-splash"
);
set_area(&s, "Duskwood", "", "contested", "");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
assert!(visible(&s, "ZoneTextFrame"));
assert_eq!(text_of(&s, "PVPInfoTextString"), "Contested Territory");
}
/// Northshire Abbey is an outdoor subzone of Elwynn (`AreaTable` row 24, parent 12), so entering
/// it is a plain `ZONE_CHANGED`; the territory line is `ZoneTextFrame`'s child, which stays hidden.
#[test]
fn abbey_grounds_subzone_hop_shows_no_territory_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
"Elwynn Forest",
"Northshire Valley",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0); // both splashes fully faded
set_area(
&s,
"Elwynn Forest",
"Northshire Abbey",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED", vec![]);
assert!(visible(&s, "SubZoneTextFrame"), "the subzone line splashes");
assert_eq!(text_of(&s, "SubZoneTextString"), "Northshire Abbey");
assert!(
!visible(&s, "ZoneTextFrame"),
"ZoneTextFrame stays hidden on a plain subzone hop — no territory line can show"
);
}
/// Indoors, the zone-name override skips when the whole-WMO name equals the subzone (`0x67e670`),
/// so the zone stays and the group's name becomes the subzone: no big splash, no territory line.
#[test]
fn abbey_interior_shows_the_room_in_the_small_line_alone() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
"Elwynn Forest",
"Northshire Abbey",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
set_area(&s, "Elwynn Forest", "Main Hall", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
assert!(
!visible(&s, "ZoneTextFrame"),
"the zone text did not change — no big splash, so no territory line"
);
assert!(
visible(&s, "SubZoneTextFrame"),
"the room name splashes small"
);
assert_eq!(text_of(&s, "SubZoneTextString"), "Main Hall");
}
/// An inn's WMO name differs from the street's, so the override makes it the zone. Handlers run in
/// registration order (`0x702140` appends), so `SubZoneTextFrame`'s `SetZoneText(1)` writes last.
#[test]
fn inn_entry_splashes_the_inn_name_with_territory_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "Goldshire", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
set_area(&s, "Lion's Pride Inn", "", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
assert!(visible(&s, "ZoneTextFrame"), "the inn name splashes big");
assert_eq!(text_of(&s, "ZoneTextString"), "Lion's Pride Inn");
assert_eq!(
text_of(&s, "PVPInfoTextString"),
"Alliance Territory",
"SubZoneTextFrame fires last (FIFO) — its SetZoneText(1) leaves the territory line set"
);
}
#[test]
fn indoor_exit_returns_the_subzone_line_alone() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
"Elwynn Forest",
"Northshire Abbey",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.run("__benilla_subzone_name = 'Main Hall'").unwrap();
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
s.tick(4.0); // everything faded
set_area(
&s,
"Elwynn Forest",
"Northshire Abbey",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED", vec![]);
assert!(visible(&s, "SubZoneTextFrame"));
assert_eq!(text_of(&s, "SubZoneTextString"), "Northshire Abbey");
assert!(
!visible(&s, "ZoneTextFrame"),
"nothing changed the zone text — cache-only, no splash"
);
}
#[test]
fn room_to_room_hop_splashes_the_room_name_alone() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "Main Hall", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
s.tick(4.0);
set_area(&s, "Elwynn Forest", "Library Wing", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
assert!(
!visible(&s, "ZoneTextFrame"),
"no zone-text change, no big splash"
);
assert!(visible(&s, "SubZoneTextFrame"));
assert_eq!(text_of(&s, "SubZoneTextString"), "Library Wing");
}
/// An FFA pit: `GetZonePVPInfo`'s isArena, the leaf area's flag `0x80`.
#[test]
fn arena_pit_shows_the_ffa_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Stranglethorn Vale", "", "contested", "");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
s.run(
"__benilla_subzone_name = 'Gurubashi Arena'; __benilla_zone_text = 'Gurubashi Arena'; \
__benilla_pvp_arena = true",
)
.unwrap();
s.fire_event("ZONE_CHANGED", vec![]);
assert!(visible(&s, "SubZoneTextFrame"));
assert_eq!(text_of(&s, "SubZoneTextString"), "Gurubashi Arena");
assert_eq!(text_of(&s, "PVPArenaTextString"), "PvP Area");
}
/// With PvP info showing, `SubZoneTextString`'s `TOP` anchors to `PVPInfoTextString`'s `BOTTOM`.
#[test]
fn subzone_seat_hangs_under_the_territory_line_on_new_area() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
"Stormwind City",
"Valley of Heroes",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.resolve();
let ok: bool = s
.eval(
"return SubZoneTextString:GetTop() == PVPInfoTextString:GetBottom() \
and PVPInfoTextString:GetTop() == ZoneTextString:GetBottom()",
)
.unwrap();
assert!(
ok,
"zone → territory → subzone, each seated at the previous line's bottom"
);
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
}
/// `AutoFollowStatus`, all of `ZoneText.lua`: END reuses the name BEGIN latched, so the app fires
/// END with no argument.
#[test]
fn the_autofollow_status_line_names_the_followee_and_fades_on_end() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
assert!(
!visible(&s, "AutoFollowStatus"),
"hidden until a follow begins"
);
s.fire_event(
"AUTOFOLLOW_BEGIN",
vec![benilla_ui::script::ScriptValue::Str("Probeone".into())],
);
s.resolve();
assert!(visible(&s, "AutoFollowStatus"));
assert_eq!(text_of(&s, "AutoFollowStatusText"), "Following Probeone.");
let alpha: f32 = s.eval("return AutoFollowStatus:GetAlpha()").unwrap();
assert!((alpha - 1.0).abs() < 0.01, "full alpha while following");
// Only END arms the fade; there is no hold timer (`ZoneText.lua:19`).
s.tick(10.0);
assert!(visible(&s, "AutoFollowStatus"), "no fade while following");
let alpha: f32 = s.eval("return AutoFollowStatus:GetAlpha()").unwrap();
assert!((alpha - 1.0).abs() < 0.01, "still opaque after 10 s");
s.fire_event("AUTOFOLLOW_END", vec![]);
s.resolve();
assert_eq!(
text_of(&s, "AutoFollowStatusText"),
"You stop following Probeone.",
"END has no argument of its own — the name comes from what BEGIN latched"
);
// `AUTOFOLLOW_STATUS_FADETIME`: a linear 4 s fade.
s.tick(2.0);
let alpha: f32 = s.eval("return AutoFollowStatus:GetAlpha()").unwrap();
assert!((alpha - 0.5).abs() < 0.05, "mid-fade alpha, got {alpha}");
s.tick(2.5);
assert!(!visible(&s, "AutoFollowStatus"), "gone after the 4 s fade");
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
}
/// A switch of subject fires BEGIN alone, and the stock BEGIN arm clears `fadeTime` and restores
/// alpha (`ZoneText.lua:11`).
#[test]
fn a_begin_during_the_end_fade_resets_the_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event(
"AUTOFOLLOW_BEGIN",
vec![benilla_ui::script::ScriptValue::Str("Probeone".into())],
);
s.fire_event("AUTOFOLLOW_END", vec![]);
s.tick(3.0); // most of the way through the fade
let faded: f32 = s.eval("return AutoFollowStatus:GetAlpha()").unwrap();
assert!(faded < 0.4, "mid-fade, got {faded}");
s.fire_event(
"AUTOFOLLOW_BEGIN",
vec![benilla_ui::script::ScriptValue::Str("Probetwo".into())],
);
s.resolve();
assert_eq!(text_of(&s, "AutoFollowStatusText"), "Following Probetwo.");
let alpha: f32 = s.eval("return AutoFollowStatus:GetAlpha()").unwrap();
assert!(
(alpha - 1.0).abs() < 0.01,
"BEGIN restores alpha, got {alpha}"
);
s.tick(10.0);
assert!(
visible(&s, "AutoFollowStatus"),
"and clears the pending fade rather than letting it finish"
);
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
}