- the audio crackle was our own stall sampler: `/usr/bin/sample` suspends every thread of the task for each of its snapshots, CoreAudio's realtime IO thread included, and coreaudiod times the client out — every HAL overload of the session sat inside a sample of our own process (three for three by timestamp), and the watchdog fires on exactly the stalls the crackle lands on (a desktop switch's `nextDrawable` block, a world entry's compile burst); in-frame sampling now waits for a run nobody can hear; and the output stream is ours — kira's renderer runs 256-frame chunks on a realtime render thread (time-constraint policy, joined to the device's IO workgroup) into a lock-free ring, the IOProc copies one device buffer and touches nothing else (≤ 0.01 ms), the device buffer goes 2048 → 512, a lost or changed default device rebuilds the stream (cpal's no-op error callback on macOS had paused it forever), a debug allocator aborts on any allocation inside either realtime path, and per-cycle meters name a skipped cycle's layer; `SoundBufferSize` is registered at the reference's own FMOD dial, and `audiowatch.py` puts the OS side — thread bands, coreaudiod's overload verdicts — on the same timeline - right-clicking a trainer, questgiver, innkeeper, spirit healer, petitioner, tabard designer or battlemaster sent `CMSG_GOSSIP_HELLO` — eight of the reference's fourteen service arms collapsing into one packet, because the dispatch keyed on the CURSOR kind and Speak covers eight bits; the reference runs two structurally identical ladders over `UNIT_NPC_FLAGS` (one for the cursor at `0x482336`, one for the send at `0x5f0289`), so the ladder is transcribed, first match wins low to high: questgiver → `CMSG_QUESTGIVER_HELLO`, trainer → `CMSG_TRAINER_LIST`, petitioner → `CMSG_PETITION_SHOWLIST`, and the spirit healer and innkeeper send NOTHING on the click — they cache the guid and raise the confirm, whose Lua accept sends with a zero guid — both ghost-gated at entry, so a living player clicking a menuless spirit healer now gets exactly what the reference gives: nothing; the stable-master conjunct and the banker/auctioneer fork fall out of the bit order for free - the -1 blood id is unfilled, not none, and the tests now name the creatures: -1 splits families that share a directory (QuillBoar beside QuillBoarWarrior, GnollMelee beside GnollCaster) and splits single creatures shipped under two rows (a Baby Murloc is both), so a "bloodless" reading would put non-bleeding quilboar beside bleeding quilboar warriors in one Razorfen room; `violenceLevel` 2 IS the reference's default — formatted out of a per-region cap table at `0x86c3f8`, and the one capped locale is koKR, whose green-blood row the table predicts from bytes alone; a spurt whose victim authors no attachment point is dropped, not relocated, and no creature-keyed suppression exists anywhere on the spurt path (ten stop conditions, none keyed on the creature) - twenty dead copies of ours (`PanelTemplates_*`, `TabButtonTemplate`, `FauxScrollFrame_*`, `ScrollFrame_OnLoad`, `ScrollingEdit_*`) that a later chain entry had been shadowing are deleted under a gate that fails any such shadow, and deleting them turned 141 tests red on four bugs live since the kit swap: lists populated before showing (the reference gates every scroll-bar write on `IsVisible()`, so both arrows were dead on every faux list), every faux window wired to a `frame.updateFunc` the reference does not have (the bar moved, the list did not follow), a trainer pitch declared `local` where an `<OnVerticalScroll>` chunk cannot see it, and the armed-wheel guard bypassed by the template's own `<OnMouseWheel>`; nine faux windows (Skill, Craft, TradeSkill, Auction, QuestLog, Friends, ScriptLog) are still on `updateFunc` and their lists do not scroll until each is rewired Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
421 lines
17 KiB
Rust
421 lines
17 KiB
Rust
//! The reference's own `ZoneText.xml` — executed off the player's chain since 1751's eighth
|
|
//! window — driven engine-only (decision 0287): the zone splash shows on the
|
|
//! right events with the right strings/colors, fades on the reference 0.5/1.0/2.0 timeline, and
|
|
//! honors the subtle law — a plain `ZONE_CHANGED` re-caches the zone name *silently*, so a later
|
|
//! `ZONE_CHANGED_NEW_AREA` that lands on the already-cached name never re-splashes.
|
|
//!
|
|
//! The test plays the app's role by hand: write the zone host globals (what
|
|
//! `crate::area::feed_zone_events` pushes), fire the event, tick the clock.
|
|
|
|
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()
|
|
}
|
|
|
|
/// Push the host globals the app writes for an area transition. Long-bracket Lua strings, so a
|
|
/// name with an apostrophe ("Lion's Pride Inn") 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 player's own strings: the splash formats CONTESTED_TERRITORY,
|
|
// FACTION_CONTROLLED_TERRITORY, FREE_FOR_ALL_TERRITORY and the AUTOFOLLOW pair straight out of
|
|
// GlobalStrings. Our deleted copy carried `X = X or "…"` fallbacks for a harness with no
|
|
// chain; the reference's file has none, and it should not.
|
|
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
|
load_xml(&s, "Fonts.xml");
|
|
// `TEXT()`, which AutoFollowStatus_OnEvent puts its message through.
|
|
load_xml(&s, "BasicControls.xml");
|
|
load_xml(&s, "MoneyFrame.xml");
|
|
load_xml(&s, "UiPanels.xml");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
|
load_xml(&s, "UIParent.xml");
|
|
// The fading kit, its own manifest entry since window 8 — as it is in the reference's TOC.
|
|
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"));
|
|
|
|
// Cross into Westfall proper (Alliance-owned): the big name + the territory line show.
|
|
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 fade timeline (ref: in 0.5, hold 1.0, out 2.0). Mid-fade-in: alpha ≈ elapsed/0.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}");
|
|
// Into the hold plateau.
|
|
s.tick(0.5);
|
|
let alpha: f32 = s.eval("return ZoneTextFrame:GetAlpha()").unwrap();
|
|
assert!((alpha - 1.0).abs() < 0.01, "hold alpha, got {alpha}");
|
|
// Past in+hold+out: hidden again.
|
|
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"));
|
|
|
|
// Goldshire: same zone, new subzone → the subzone frame alone.
|
|
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"));
|
|
|
|
// The reference law: a plain ZONE_CHANGED updates ZoneTextFrame.zoneText and returns —
|
|
// no splash even though the zone text changed…
|
|
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"
|
|
);
|
|
// …and a NEW_AREA landing on the now-cached name stays silent too.
|
|
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
|
|
assert!(
|
|
!visible(&s, "ZoneTextFrame"),
|
|
"NEW_AREA on the already-cached zone text must not re-splash"
|
|
);
|
|
|
|
// A genuinely new zone splashes again.
|
|
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");
|
|
}
|
|
|
|
/// The director's abbey repro, outdoor half: "Northshire Abbey" is a real outdoor SUBZONE
|
|
/// (AreaTable row 24, parent Elwynn 12) — walking onto the abbey grounds is a plain subzone hop.
|
|
/// The reference shows ONLY the small subzone line: ZoneTextFrame never shows on a plain
|
|
/// ZONE_CHANGED, so the territory line (its child region) cannot render — even though
|
|
/// SubZoneTextFrame's handler calls SetZoneText(1), which SETS the (hidden) PVP string.
|
|
#[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"
|
|
);
|
|
}
|
|
|
|
/// The abbey repro, indoor half — the byte-corrected feed (wow-re `zonetext-indoor-bit.md` (d)):
|
|
/// crossing the abbey threshold, the zone-name override SKIPS (the whole-WMO name
|
|
/// "Northshire Abbey" equals the yard subzone), so the ZONE slot stays "Elwynn Forest" and the
|
|
/// group row's name re-populates the SUBZONE ("Main Hall"). ZoneTextFrame's text never changes ⇒
|
|
/// it stays hidden ⇒ no territory line (its child region) can render — the director's reference
|
|
/// truth, produced by the engine feed rather than by handler-order luck.
|
|
#[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);
|
|
|
|
// Step inside: zone slot UNCHANGED, subzone = the group row's name.
|
|
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");
|
|
}
|
|
|
|
/// The INN case — the override FIRES (the whole-WMO name differs from the street subzone):
|
|
/// zone slot = the inn's name, subzone nulls (unnamed group rows). The big splash shows the inn
|
|
/// name, and — the FIFO dispatch law (wow-re `event-dispatch-order.md`: SubZoneTextFrame
|
|
/// registered second fires LAST, its SetZoneText(1) is the last writer) — the territory line
|
|
/// shows under it, exactly as on a NEW_AREA splash.
|
|
#[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"
|
|
);
|
|
}
|
|
|
|
/// Leaving the interior: the subzone reverts to the leaf name (a TEXT change, outdoors ⇒ plain
|
|
/// ZONE_CHANGED ⇒ ZoneTextFrame only re-caches, silently) — the small line splashes alone,
|
|
/// exactly like the entry hop.
|
|
#[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"
|
|
);
|
|
}
|
|
|
|
/// Room→room inside one building (the corrected feed: the SUBZONE slot hops "Main Hall" →
|
|
/// "Library Wing", the zone slot stays the DBC zone): the room name splashes in the small line;
|
|
/// the big frame never shows, so no territory line.
|
|
#[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 — leaf Flags bit 0x80): the subzone line splashes with
|
|
/// the "PvP Area" arena string under it, red per the quote's PVPArenaTextString color.
|
|
#[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");
|
|
}
|
|
|
|
/// The subzone seat law: with PvP info showing (a friendly NEW_AREA), SubZoneTextString's TOP
|
|
/// anchors to PVPInfoTextString's BOTTOM — the three-line stack the director's screenshot shows.
|
|
#[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` — the third frame in `ZoneText.xml`, and the only thing `ref-ZoneText.lua`
|
|
/// actually contains (decision 0893). The app fires `AUTOFOLLOW_BEGIN` with the followee's name and
|
|
/// `AUTOFOLLOW_END` with nothing; the frame does the rest.
|
|
///
|
|
/// The load-bearing assertion is that **END reuses the name BEGIN latched**. That is why the app
|
|
/// gets to fire END with no argument at all, and why [`crate::player::FollowState`] latches the
|
|
/// name at start rather than re-reading it — by the time a follow ends, the followee is often gone.
|
|
#[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");
|
|
|
|
// A follow that runs for a while must NOT fade — the fade is armed only by END, and there is
|
|
// no hold timer to expire (this is not the FadingFrame kit).
|
|
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");
|
|
|
|
// END carries no argument: the line reuses the latched name.
|
|
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"
|
|
);
|
|
// A linear 4 s fade, no hold: half gone at 2 s, hidden past 4.
|
|
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 follow that switches subject fires `BEGIN` alone (see `crate::ui_follow`'s header). The
|
|
/// reference's own handler treats BEGIN as a full reset — it clears `fadeTime`, restores alpha and
|
|
/// re-shows — so a switch made *during* the end-fade must come back to a clean, opaque line rather
|
|
/// than inheriting the dying one's alpha.
|
|
#[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());
|
|
}
|