Files
benilla-octo/crates/benilla-app/src/ui_script/zone_text_tests.rs
T
Sam SungandClaude Opus 5.5 ea7e0d087a Zone names stay uncoloured on PvE realms outside capitals, as in 1.12.1 (#825)
On a PvE realm, zone names were still coloured friendly, hostile or contested.

In 1.12.1, `GetZonePVPInfo` (`0x48d540`) answers a type only when its display gate is open (`0x48d5c2`/`0x48d5cc`). The gate opens on the realm's PvP flag or in a zone with Flags `0x10`, which is the six capitals. The realm flag is read from `Cfg_Configs.dbc` for the selected realm's type and stored once per world entry (`0x4015de`). A type with no row skips the store. Otherwise the type is nil, so the stock zone splash shows cream and the minimap zone text gold.

benilla now reads `Cfg_Configs.dbc` instead of a transcription that stopped at type 8, so realm type 10 lists as PvP. It latches the realm flag at world entry and applies the gate.

Tests cover the gate, the six capitals and every shipped Cfg_Configs row; each failed before the fix.

Fixes #816

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:53:16 +07:00

419 lines
15 KiB
Rust

//! Stock `ZoneText.xml`, driven by hand as `crate::area::feed_zone_events` drives it: zone caches
//! pushed, 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, ZoneTexts};
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 zone caches the app pushes on an area change: the minimap line is the subzone, else the
/// zone; an empty PvP word or faction is nil.
fn area(zone: &str, sub: &str, pvp: &str, faction: &str) -> ZoneTexts {
let some = |t: &str| (!t.is_empty()).then(|| t.to_string());
ZoneTexts {
zone: zone.into(),
real_zone: zone.into(),
subzone: sub.into(),
minimap: if sub.is_empty() { zone } else { sub }.into(),
pvp_type: some(pvp),
pvp_faction: some(faction),
is_arena: false,
}
}
fn set_area(s: &mut UiScript, zone: &str, sub: &str, pvp: &str, faction: &str) {
s.set_zone_texts(area(zone, sub, pvp, faction));
}
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\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
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(&mut 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(&mut 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(&mut 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(&mut s, "Elwynn Forest", "", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
assert!(!visible(&s, "ZoneTextFrame"));
set_area(
&mut 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(&mut 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(
&mut 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(
&mut 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(
&mut s,
"Elwynn Forest",
"Northshire Abbey",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
set_area(&mut 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(&mut s, "Elwynn Forest", "Goldshire", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
set_area(&mut 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(
&mut s,
"Elwynn Forest",
"Northshire Abbey",
"friendly",
"Alliance",
);
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
set_area(&mut s, "Elwynn Forest", "Main Hall", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
s.tick(4.0); // everything faded
set_area(
&mut 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(&mut s, "Elwynn Forest", "Main Hall", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
s.tick(4.0);
set_area(
&mut 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");
}
/// A nil pvpType, a PvE realm outside a capital: no territory line, and the zone and subzone in
/// the stock else-arm's cream (`ZoneText.xml:32-33`), not a faction colour.
#[test]
fn a_nil_pvp_type_splashes_uncoloured_with_no_territory_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&mut s, "Elwynn Forest", "Goldshire", "", "");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
assert!(visible(&s, "ZoneTextFrame"));
assert_eq!(text_of(&s, "PVPInfoTextString"), "");
for region in ["ZoneTextString", "SubZoneTextString"] {
let (r, g, b) = s
.eval::<(f32, f32, f32)>(&format!("return {region}:GetTextColor()"))
.unwrap();
assert!(
(r - 1.0).abs() < 1e-3 && (g - 0.9294).abs() < 1e-3 && (b - 0.7607).abs() < 1e-3,
"{region} is ({r}, {g}, {b})"
);
}
}
/// 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(&mut s, "Stranglethorn Vale", "", "contested", "");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
s.tick(4.0);
s.set_zone_texts(ZoneTexts {
is_arena: true,
..area("Stranglethorn Vale", "Gurubashi Arena", "contested", "")
});
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(
&mut 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());
}