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
378 lines
14 KiB
Rust
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());
|
|
}
|