the unit frames are the reference's own, and the water has motes in it

- the player, party, target and pet frames are the reference's own files (`BuffFrame`, `UnitFrame`, `CombatFeedback`, `PlayerFrame`, `PartyFrame`, `TargetFrame`, `PetFrame`, in the reference's toc order) — our 2741-line `UnitFrames.xml` and our `PartyFrame.xml` are deleted; four engine verbs built for them (`PartialPlayTime`, `NoPlayTime`, `UnitPlayerControlled`, `UnitIsCivilian`, each answering 1/nil), the whole-UI load bracketed in the counted sound-suppression scope `0x48fbf0` self-brackets (the reference fires a `PlaySound` during load and throws it away), and `GetBillingTimeRested` read off `SMSG_AUTH_RESPONSE`'s third billing field rather than hardcoded; the differences the stock files bring are pinned rather than papered — `PlayerFrame` never hides, an unresolved name is BLANK not "Unknown", the target frame has no numerals at all (a bar hover pops the unit tooltip instead)
- what the swap surfaced, each a hard break under the stock files: `UnitBuff`/`UnitDebuff` returned the Classic Era ten-value tuple, name first, where 1.12 returns two and three values TEXTURE first (`0x519500`/`0x5198f0`) — a raise twice per `UNIT_AURA` on any debuffed target, and all 184 corpus call sites bind the 1.12 shape; the power events are named per resource (`UNIT_MANA`/`RAGE`/`FOCUS`/`ENERGY`/`HAPPINESS` and their MAX twins) and we fired the Era pair, so no mana, rage or energy bar could live-update; `SetRaidTarget` is the engine verb and `SetRaidTargetIcon` the FrameXML toggle wrapper — we had registered the wrapper's name on the engine's body, so every raid mark in the unit popup raised
- the underwater drift cloud: the reference draws a fixed 4000-mote field whenever the camera eye is inside a liquid — not a particle system, nothing spawns, ages or dies; a population scattered once into a 30 yd camera-relative cube, advected by the camera's own motion plus a gust and wrapped, so the field stands still in the world; benilla had the murk and nothing in it; a dry frame commits no draw, and the submerged one is bounded by the reference's own 666-quad cap (633–644 of 4000 measured live, which is the expected survivor count of a uniform cube through the 90° cone — an independent check on population, distribution and cone at once)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 17:04:32 +07:00
co-authored by Claude Fable 5.1
parent 6bafea0919
commit 7e3a67a02e
51 changed files with 2337 additions and 3467 deletions
@@ -2,17 +2,23 @@ use benilla_ui::script::{
QuadContent, ScriptValue, SelectionRequest, SoundRequest, UiScript, UnitState,
};
use super::test_ui::load_ui as load_xml;
use super::test_ui::{hover, load_ui as load_xml, unhover};
/// The unit frames' production load prefix (ui_script/mod.rs order): fonts + UIParent +
/// tooltip, then the dropdown kit + unit popups the frames' DropDown children initialize into.
fn load_unit_frames(s: &UiScript) {
// The app runs the real `Interface\FrameXML\GlobalStrings.lua` off the player's chain
// BEFORE any XML (`load_global_strings`); this stands in for it, with the one key these
// frames resolve at load time — `DEAD` l.898, which both dead-text FontStrings name in
// their `text=` attribute. Without it the loader's literal fallback would leave the KEY on
// screen, which is the bug this file guards below.
s.run(r#"DEAD = "Dead""#).unwrap();
// The app runs the real `Interface\FrameXML\GlobalStrings.lua` off the player's chain BEFORE
// any XML (`load_global_strings`), so the fixture names it too. This used to be a single
// hand-set `DEAD = "Dead"`, which was enough while the frames were OUR transcription: ours
// carried `X = X or "…"` fallbacks and hard literals for everything else. The reference's own
// files carry none, and they resolve GlobalStrings at LOAD in three separate places —
// `CombatFeedback.lua` l.7-17 builds the whole `CombatFeedbackText` table out of them
// (`TEXT(ABSORB)`, `TEXT(MISS)`, …), `UnitFrame.lua` l.1-6 builds `ManaBarColor`'s prefixes
// the same way, and `UnitFrame_OnEnter` l.60/63 passes `PARTY_OPTIONS_LABEL` /
// `PLAYER_OPTIONS_LABEL` straight into `GameTooltip:SetText`, which raises on nil rather than
// drawing an empty plate. Hand-setting the union of those is a second copy of the reference's
// own file; naming the file is the only version that cannot drift. (`DEAD` is l.898 of it.)
load_xml(s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(s, "Fonts.xml");
load_xml(s, "UIParent.xml");
// The bars' numerals machinery (decision 1082), which the manifest loads immediately ahead of
@@ -22,7 +28,13 @@ fn load_unit_frames(s: &UiScript) {
load_xml(s, "GameTooltip.xml");
load_xml(s, "UIDropDownMenu.xml");
load_xml(s, "UnitPopup.xml");
load_xml(s, "UnitFrames.xml");
load_xml(s, "Interface\\FrameXML\\BuffFrame.xml");
load_xml(s, "Interface\\FrameXML\\UnitFrame.xml");
load_xml(s, "Interface\\FrameXML\\CombatFeedback.xml");
load_xml(s, "Interface\\FrameXML\\PlayerFrame.xml");
load_xml(s, "Interface\\FrameXML\\PartyFrame.xml");
load_xml(s, "Interface\\FrameXML\\TargetFrame.xml");
load_xml(s, "Interface\\FrameXML\\PetFrame.xml");
}
/// Load the real `assets/ui/UnitFrames.xml` (the shipped default UI) into a bare engine and
@@ -36,13 +48,31 @@ fn shipped_unit_frames_drive_end_to_end() {
load_unit_frames(&s);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// No units yet: both frames hid themselves on their OnLoad's first update.
let hidden: bool = s
.eval("return not PlayerFrame:IsVisible() and not TargetFrame:IsVisible()")
// **Only the TARGET frame hides.** Our deleted transcription hid both; the reference hides
// exactly one. `PlayerFrame` is a plain top-level `<Button>` with no `hidden=` attribute
// (ref PlayerFrame.xml l.4) and `PlayerFrame_Update` (ref PlayerFrame.lua l.29-37) wraps its
// whole body in `if UnitExists("player")` without an else — nothing in the file ever calls
// `PlayerFrame:Hide()`. That is the real client's behaviour: the player plate is up from the
// moment the UI loads, empty until the player object arrives. `TargetFrame_Update`
// (ref TargetFrame.lua l.38-55) is the one with the `else this:Hide()`.
let shape: bool = s
.eval("return PlayerFrame:IsVisible() and not TargetFrame:IsVisible()")
.unwrap();
assert!(hidden, "frames hide while their units don't exist");
assert!(
shape,
"the player plate is always up; only the target frame hides while its unit is absent"
);
// The player appears (name still unresolved), at 72/100 health, 45/80 mana.
//
// `PLAYER_ENTERING_WORLD` is the event, not `UNIT_HEALTH`, and the reference is why: the
// stock bars each register their OWN events and repaint only themselves
// (`UnitFrameHealthBar_Initialize` takes UNIT_HEALTH/UNIT_MAXHEALTH, ref UnitFrame.lua
// l.150-151; `UnitFrameManaBar_Initialize` takes the ten UNIT_MANA/RAGE/… ones, l.189-199),
// so UNIT_HEALTH alone would leave the mana bar at its load-time 0/0. The one handler that
// repaints name + portrait + both bars together is `PlayerFrame_OnEvent`'s
// PLAYER_ENTERING_WORLD arm → `UnitFrame_Update()` (ref PlayerFrame.lua l.96-100), which is
// also what really fires when the player object arrives.
s.set_unit(
"player",
Some(UnitState {
@@ -56,10 +86,14 @@ fn shipped_unit_frames_drive_end_to_end() {
max_power: 80,
dead: false,
reaction: 0,
// Not decoration: `UnitFrameManaBar_Update`'s disconnect leg (ref UnitFrame.lua
// l.209-212) pins a disconnected unit's power bar to MAX and greys it, and
// `UnitState::default()` leaves this `false`.
is_connected: true,
..UnitState::default()
}),
);
s.fire_event("UNIT_HEALTH", vec![ScriptValue::Str("player".into())]);
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
assert!(s.eval::<bool>("return PlayerFrame:IsVisible()").unwrap());
let ok: bool = s
.eval(
@@ -71,12 +105,24 @@ fn shipped_unit_frames_drive_end_to_end() {
return hb:GetValue() == 72 and hmax == 100
and pb:GetValue() == 45 and pmax == 80 and pb:IsVisible()
and b == 1 and r == 0 -- mana blue
and PlayerName:GetText() == "Unknown"
"#,
)
.unwrap();
assert!(ok, "player frame painted from the snapshot");
// …and the name plate is BLANK while the name is unresolved, not the word "Unknown". That
// word was our transcription's, twice over — a `text="Unknown"` literal on the FontString
// (deleted UnitFrames.xml l.1364) and a `UnitName(unit) or "Unknown"` fallback (l.803). The
// reference's `PlayerName` carries no `text=` at all (ref PlayerFrame.xml l.58) and
// `GetUnitName` returns `UnitName`'s result unchanged (ref UnitFrame.lua l.226-236), so what
// shows is whatever the engine's `UnitName` returns for a nameless unit — nil here.
assert_eq!(
s.eval::<Option<String>>("return PlayerName:GetText()")
.unwrap(),
None,
"no name yet: the stock file has no \"Unknown\" literal to fall back on"
);
// The name-query answer lands: UNIT_NAME_UPDATE repaints the name.
s.set_unit(
"player",
@@ -91,6 +137,7 @@ fn shipped_unit_frames_drive_end_to_end() {
max_power: 80,
dead: false,
reaction: 0,
is_connected: true,
..UnitState::default()
}),
);
@@ -100,7 +147,21 @@ fn shipped_unit_frames_drive_end_to_end() {
"Benilla"
);
// A powerless wolf gets targeted: frame shows, power bar hides, health fills 30/50.
// A powerless wolf gets targeted: frame shows, power bar runs EMPTY, health fills 30/50.
//
// "Empty", not hidden — and the reason is a reference quirk worth knowing. The only thing in
// 1.12 that hides a StatusBar for having no track is `TextStatusBar_UpdateTextString`'s
// `else textStatusBar:Hide()` (ref TextStatusBar.lua l.55-57), and that whole body sits
// inside `if(string)` — `string` being `bar.TextString`. The reference **never declares**
// `TargetFrameHealthBarText` / `TargetFrameManaBarText`: TargetFrame.xml l.486-487 passes
// both names into `UnitFrame_Initialize` and neither exists anywhere in FrameXML (only the
// player's and the pet's do — PlayerFrame.xml l.79/88, PetFrame.xml l.87/96). So
// `SetTextStatusBarText` early-returns on the nil (ref TextStatusBar.lua l.7-10), the
// target's bars carry no `TextString`, and the hide can never fire. It doesn't show: these
// bars have a `<BarTexture>` and no background, so a 0/0 bar draws nothing either way.
//
// Our transcription hid it, because 1146 §3 *declared* the two text regions the reference
// leaves dangling. Those are gone with the file.
s.set_unit(
"target",
Some(UnitState {
@@ -114,6 +175,7 @@ fn shipped_unit_frames_drive_end_to_end() {
max_power: 0,
dead: false,
reaction: 4, // neutral
is_connected: true,
..UnitState::default()
}),
);
@@ -122,8 +184,10 @@ fn shipped_unit_frames_drive_end_to_end() {
let ok: bool = s
.eval(
r#"
local _, pmax = TargetFrameManaBar:GetMinMaxValues()
return TargetFrame:IsVisible()
and not TargetFrameManaBar:IsVisible()
and TargetFrameManaBar:GetValue() == 0 and pmax == 0
and TargetFrameManaBar.TextString == nil -- the reference declares no text region
and TargetName:GetText() == "Young Wolf"
and TargetLevelText:GetText() == "3"
and not TargetDeadText:IsShown() -- living target: no dead word
@@ -132,7 +196,7 @@ fn shipped_unit_frames_drive_end_to_end() {
.unwrap();
assert!(
ok,
"target frame painted; powerless unit hides its power bar"
"target frame painted; a powerless unit's power bar runs empty over an empty track"
);
// The select sound rides the frame's OnShow (ref TargetFrame_OnShow): a neutral (4) wolf is
// neither UnitIsEnemy (≤2) nor UnitIsFriend (≥5) → the neutral kit.
@@ -341,20 +405,9 @@ fn unit_combat_drives_the_player_hit_indicator() {
fn left_clicking_the_player_frame_targets_self() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The SELF-menu strings the popup rows bake at UnitPopup.xml load (no GlobalStrings in a
// bare engine — production reads the real file).
s.run(
r#"
LOOT_METHOD = "Loot Method"
LOOT_THRESHOLD = "Loot Threshold"
LOOT_GROUP_LOOT = "Group Loot"
PARTY_LEAVE = "Leave Party"
RAID_TARGET_ICON = "Raid Target Icon"
CANCEL = "Cancel"
ITEM_QUALITY2_DESC = "Uncommon"
"#,
)
.unwrap();
// The SELF-menu strings the popup rows bake at UnitPopup.xml load arrive with
// `load_unit_frames`' `GlobalStrings.lua`; they used to be hand-set here, and one of the
// hand-set values was wrong (see the row assertion below).
load_unit_frames(&s);
// The player must exist for the frame to be shown and mouse-hittable.
@@ -427,10 +480,14 @@ fn left_clicking_the_player_frame_targets_self() {
6,
"title + Loot Method + Loot Threshold + Leave Party + Raid Target Icon + Cancel"
);
// `PARTY_LEAVE`, verbatim. The reference's own GlobalStrings.lua l.2991 is
// `PARTY_LEAVE = "Leave party"` — lower-case "party". The hand-set fixture this test used to
// open with title-cased it, so the row read back the fixture's own typo rather than the
// client's string; naming the real file is what exposed it.
assert_eq!(
s.eval::<String>("return DropDownList1Button4:GetText()")
.unwrap(),
"Leave Party"
"Leave party"
);
// The nested rows carry the expand arrow (the level-2 gate for a leader).
assert!(
@@ -467,21 +524,9 @@ fn left_clicking_the_player_frame_targets_self() {
fn raid_mark_clicks_through_the_nested_level() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run(
r#"
LOOT_METHOD = "Loot Method"
LOOT_THRESHOLD = "Loot Threshold"
LOOT_GROUP_LOOT = "Group Loot"
PARTY_LEAVE = "Leave Party"
RAID_TARGET_ICON = "Raid Target Icon"
CANCEL = "Cancel"
ITEM_QUALITY2_DESC = "Uncommon"
RAID_TARGET_1 = "Star"; RAID_TARGET_2 = "Circle"; RAID_TARGET_3 = "Diamond";
RAID_TARGET_4 = "Triangle"; RAID_TARGET_5 = "Moon"; RAID_TARGET_6 = "Square";
RAID_TARGET_7 = "Cross"; RAID_TARGET_8 = "Skull"; NONE = "None"
"#,
)
.unwrap();
// The menu strings arrive with `load_unit_frames`' own `GlobalStrings.lua`
// (`RAID_TARGET_ICON` l.3288, `RAID_TARGET_1..8` l.3280-3287, `NONE` l.2795), which is where
// production reads them; they used to be hand-set here.
load_unit_frames(&s);
s.set_unit(
@@ -551,6 +596,13 @@ fn raid_mark_clicks_through_the_nested_level() {
.unwrap();
s.mouse_button(sx as f32, sy as f32, "LeftButton", true);
s.mouse_button(sx as f32, sy as f32, "LeftButton", false);
// RED while the `SetRaidTarget` engine binding is absent, and deliberately left that way.
// `UnitPopup.xml`'s row calls `SetRaidTargetIcon(menu.unit, mark)`; the definition of that
// used to be ours, and since the migration it is the reference's own — `TargetFrame.lua`
// l.486-492 — whose whole body is `SetRaidTarget(unit, 0 or index)`. `SetRaidTarget` is an
// engine verb this house does not have yet, so the click raises
// "attempt to call global 'SetRaidTarget' (a nil value)" and no intent is queued. 1203: it
// gets built, never stubbed.
assert_eq!(
s.take_party_requests(),
vec![benilla_ui::script::PartyRequest::SetRaidTarget {
@@ -608,6 +660,7 @@ fn shipped_target_frame_runs_the_level_law() {
exists: true,
level: 3,
is_player: true,
player_controlled: true,
health: 50,
max_health: 50,
..UnitState::default()
@@ -738,6 +791,7 @@ fn shipped_target_frame_runs_the_level_law() {
exists: true,
level: 30,
is_player: true,
player_controlled: true,
health: 50,
max_health: 50,
..UnitState::default()
@@ -789,6 +843,7 @@ fn pvp_icon_follows_the_three_branch_law() {
max_health: 50,
level: 10,
is_player: true,
player_controlled: true,
faction_group: Some("Alliance".into()),
pvp,
is_pvp_ffa: ffa,
@@ -896,6 +951,7 @@ fn flagged_friendly_player_plate_is_green() {
max_health: 50,
level: 20,
is_player: true,
player_controlled: true,
reaction: 5,
can_attack: false,
faction_group: Some("Alliance".into()),
@@ -1075,10 +1131,16 @@ fn the_ring_art_paints_over_the_bars() {
power_type: 0,
power: 60,
max_power: 100,
is_connected: true, // else the power bar takes the disconnect leg's grey max fill
..UnitState::default()
}),
);
s.fire_event("UNIT_HEALTH", vec![ScriptValue::Str("player".into())]);
// PLAYER_ENTERING_WORLD, not UNIT_HEALTH: the reference repaints the NAME only from
// `UnitFrame_Update` (ref UnitFrame.lua l.23-28) and on UNIT_NAME_UPDATE (l.31-34) — the
// health bar's own event touches its bar and nothing else. `PlayerFrame_OnEvent`'s
// PLAYER_ENTERING_WORLD arm (ref PlayerFrame.lua l.96-100) is the one that runs the full
// repaint, and the name quad is half of what this test measures.
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.resolve();
let quads = s.extract();
@@ -1131,16 +1193,28 @@ fn the_party_art_paints_over_the_bars() {
// The loot test's prefix (`loot_tests.rs`): PartyFrame's inline <Script> reads
// StaticPopupDialogs, which UiPanels.xml defines, and its per-member dropdown OnLoad walks the
// whole popup kit.
// GlobalStrings first, for the same reason `load_unit_frames` names it — the stock unit-frame
// files resolve it at LOAD (`CombatFeedback.lua` l.7-17, `UnitFrame.lua` l.1-6).
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "UIDropDownMenu.xml");
load_xml(&s, "UnitPopup.xml");
// BENILLA_POWER_COLORS (the shared power-colour table) lives in UnitFrames.xml; the party
// frames' Update reads it. The player/target frames it also defines stay hidden here — no unit.
load_xml(&s, "UnitFrames.xml");
load_xml(&s, "PartyFrame.xml");
// The reference's own kit, in the manifest's order. `UIParent.xml` is not decoration here:
// `RaiseFrameLevel`/`LowerFrameLevel` live in it (ref UIParent.lua l.1890-1896) and stock
// `TargetofTargetTextureFrame`'s OnLoad calls one of them.
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UnitFrame.xml");
load_xml(&s, "Interface\\FrameXML\\CombatFeedback.xml");
load_xml(&s, "Interface\\FrameXML\\PlayerFrame.xml");
load_xml(&s, "Interface\\FrameXML\\PartyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\TargetFrame.xml");
load_xml(&s, "Interface\\FrameXML\\PetFrame.xml");
s.set_unit(
"party1",
@@ -1153,9 +1227,27 @@ fn the_party_art_paints_over_the_bars() {
power_type: 0,
power: 60,
max_power: 100,
is_connected: true,
..UnitState::default()
}),
);
// **The ROSTER, not the unit snapshot, is what shows a party row.** Stock
// `PartyMemberFrame_UpdateMember` gates on `GetPartyMember(id)` (ref PartyMemberFrame.lua
// l.41-57) — `UnitExists("party1")` is never consulted — and its else arm is `this:Hide()`.
// Our deleted `PartyFrame.xml` keyed the row off the unit token, so setting `party1` alone
// used to be enough; against the reference's file it paints nothing at all.
s.set_party(benilla_ui::script::PartyState {
members: vec![benilla_ui::script::PartyMemberInfo {
name: "Onepriest".into(),
guid: 0x0_0B12,
}],
leader_index: 0,
leader_guid: 0,
raid: Vec::new(),
loot_method: "group".into(),
master_looter: None,
loot_threshold: 2,
});
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
s.resolve();
let quads = s.extract();
@@ -1166,14 +1258,39 @@ fn the_party_art_paints_over_the_bars() {
s.quad_owner_name(q.target)
.is_some_and(|n| n.starts_with("PartyMemberFrame1"))
};
let art = quads
// The ART cannot be scoped that way, and the reason is the reference's own declaration:
// `PartyMemberFrameTemplate` hangs `$parentTexture` two levels down inside a pair of
// **anonymous** `<Frame setAllPoints="true">` wrappers (ref PartyFrameTemplates.xml
// l.230-240). The region itself still resolves to `PartyMemberFrame1Texture` — `$parent`
// walks to the nearest named ancestor — but the frame that OWNS the quad has no name at all,
// so an owner-name filter can never see it. (Our deleted PartyFrame.xml gave that wrapper a
// name, `$parentTextureFrame`, which is why this used to work.) Scoped by geometry instead:
// rows 2-4 are hidden with no member, so exactly one UI-PartyFrame quad is drawn, and it is
// asserted to sit on member 1's rect.
let art_quads: Vec<_> = quads
.iter()
.find(|q| {
mine(q)
&& matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
.filter(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.ends_with("UI-PartyFrame"))
})
.expect("the party member frame art");
.collect();
assert_eq!(
art_quads.len(),
1,
"one member in the party, one row of art drawn"
);
let art = art_quads[0];
let row: Vec<f32> = s
.eval(
"return { PartyMemberFrame1:GetLeft(), PartyMemberFrame1:GetBottom(), \
PartyMemberFrame1:GetRight(), PartyMemberFrame1:GetTop() }",
)
.unwrap();
let r = art.rect.expect("the art quad has a resolved rect");
assert!(
r.left >= row[0] - 1.0 && r.right <= row[2] + 1.0 && r.top <= row[3] + 3.0,
"the art quad sits on PartyMemberFrame1 (art {r:?}, row {row:?})"
);
let fill = quads
.iter()
.filter(|q| {
@@ -1234,8 +1351,16 @@ fn a_feigning_target_paints_empty_bars_and_the_dead_text() {
assert!(alive, "the control: a live hunter reads live");
// He feigns. Only the flag moved on the wire — the snapshot turns it into these three.
//
// BOTH events, because the reference's bars are independent listeners: the health bar takes
// UNIT_HEALTH/UNIT_MAXHEALTH (ref UnitFrame.lua l.150-151) and the mana bar takes the ten
// UNIT_MANA/RAGE/FOCUS/… ones (l.189-199) — there is no shared "repaint the frame" event, so
// UNIT_HEALTH alone leaves the power bar showing the pre-feign number. Our deleted
// transcription drove both bars off one update, which is why this test used to fire one event.
// The server sends both when the dynflag lands.
s.set_unit("target", hunter(0, 0, true));
s.fire_event("UNIT_HEALTH", vec![ScriptValue::Str("target".into())]);
s.fire_event("UNIT_MANA", vec![ScriptValue::Str("target".into())]);
let (hp, hmax, mana, mmax): (f64, f64, f64, f64) = (
s.eval("return TargetFrameHealthBar:GetValue()").unwrap(),
s.eval("local _, m = TargetFrameHealthBar:GetMinMaxValues() return m")
@@ -1270,6 +1395,7 @@ fn a_feigning_target_paints_empty_bars_and_the_dead_text() {
// He stands back up: the flag clears, and nothing about the body needed restoring.
s.set_unit("target", hunter(1200, 300, false));
s.fire_event("UNIT_HEALTH", vec![ScriptValue::Str("target".into())]);
s.fire_event("UNIT_MANA", vec![ScriptValue::Str("target".into())]);
let up: bool = s
.eval(
r#"
@@ -1444,10 +1570,21 @@ fn the_rest_badge_covers_the_level_number() {
/// frames people actually watch (director report). Now the player's health and power bars carry
/// "value / max", and the switch pins them.
///
/// Which bars it pins is the REFERENCE's own split, transcribed rather than chosen: 1.12 sets
/// `textLockable` on the player's two bars, the pet's two and the XP bar, and on nothing else — so
/// the target frame's numerals are hover-only there and stay blank here. The power bar is whatever
/// the unit runs on; a rage unit reads "45 / 80" the same as a mana one.
/// Which bars it reaches is the REFERENCE's own split, and running the reference's own files made
/// it narrower than 1146 believed. `textLockable` is set on the player's two bars, the pet's two
/// and the XP bar and nothing else — but the TARGET frame goes further than "not lockable": it has
/// **no text regions at all**. `TargetFrame.xml` l.486-487 hands `TargetFrameHealthBarText` /
/// `TargetFrameManaBarText` to `UnitFrame_Initialize` and neither name is declared anywhere in
/// FrameXML (only `PlayerFrame.xml` l.79/88 and `PetFrame.xml` l.87/96 declare theirs), so
/// `SetTextStatusBarText` early-returns on the nil (ref TextStatusBar.lua l.7-10) and those bars
/// never get a `TextString`. 1146 §3 *added* the two regions to our transcription so a hover could
/// reveal them; they went with the file. What 1.12 actually gives you when you hover a target bar
/// is the unit TOOLTIP (ref TextStatusBar.xml l.16-26), asserted below.
///
/// The power bar's numerals also carry the resource's LABEL again — "Rage 45 / 80" — because
/// `UnitFrame_UpdateManaType` sets the prefix on every update (ref UnitFrame.lua l.129) and that
/// call is inside the stock file. 1147 §1 cut ours on the director's look call; the stock file has
/// no such cut.
#[test]
fn status_bar_text_paints_the_player_numerals_but_not_the_targets() {
let mut s = UiScript::new().unwrap();
@@ -1465,6 +1602,10 @@ fn status_bar_text_paints_the_player_numerals_but_not_the_targets() {
max_power: 80,
dead: false,
reaction: 4,
// Without this `UnitFrameManaBar_Update` takes its disconnect leg (ref UnitFrame.lua
// l.209-212) — the power bar pins to MAX and never reaches `UnitFrame_UpdateManaType`,
// so it would read "80 / 80" with no prefix.
is_connected: true,
..UnitState::default()
};
s.set_unit("player", Some(alive(72, 45, 1))); // power_type 1 = RAGE
@@ -1493,59 +1634,95 @@ fn status_bar_text_paints_the_player_numerals_but_not_the_targets() {
shown(&s, "PlayerFrameHealthBarText"),
"your own health numerals pin on"
);
// Bare here, and only because this fixture stops short of the character window: the
// reference's "Health" prefix is set by `CharacterFrame_OnLoad` (ref CharacterFrame.lua l.55),
// not by the unit frames, and the manifest loads `Interface\FrameXML\CharacterFrame.xml` far
// below these. In a full run the player's health bar reads "Health 72 / 100".
assert_eq!(
text(&s, "PlayerFrameHealthBarText"),
"72 / 100",
"bare numbers: the reference's \"Health\"/\"Rage\" labels were cut on the \
director's call (1147)"
"no prefix without the character window, which is what sets HEALTH"
);
// The power bar's LABEL is back, and it is the unit frames' own:
// `UnitFrame_UpdateManaType` re-sets the prefix from `ManaBarColor[UnitPowerType(unit)].prefix`
// on every mana-bar update (ref UnitFrame.lua l.122-129), and
// `TextStatusBar_UpdateTextString` renders `prefix .. " " .. value .. " / " .. max`
// (ref TextStatusBar.lua l.42-46). 1147 §1 cut the three prefix calls out of OUR files on the
// director's look call; this one lives inside the stock file, so it came back with it.
assert_eq!(
s.eval::<String>("return PlayerFrameManaBar.prefix")
.unwrap(),
"Rage",
"the prefix follows the resource — this player runs on rage"
);
// The STRING, though, is still the one rendered before the switch existed. That is the
// reference's own ordering, not a gap: `TextStatusBar_OnEvent`'s CVAR_UPDATE arm only calls
// `TextString:Show()` (ref TextStatusBar.lua l.14-24) — it never re-renders — and the last
// render ran from `SetValue`'s OnValueChanged during PLAYER_ENTERING_WORLD, before
// `UnitFrame_UpdateManaType` had set the prefix (ref UnitFrame.lua l.208-215 sets the value
// first). Its own re-render is gated on `GetCVar("statusBarText") == "1"` (l.130-132), which
// was not yet true. So the label lands on the next repaint — asserted at the end of this test.
assert_eq!(
text(&s, "PlayerFrameManaBarText"),
"45 / 80",
"the power bar shows the number alone, whatever resource it is"
"flipping the switch shows the string, it does not re-render it"
);
// The target's stay dark with the switch ON: 1.12 never makes its bars textLockable, so the
// option does not reach them.
// The target's bars carry NO text region, so there is nothing for the switch to pin and
// nothing for a hover to reveal. Asserted on the globals themselves: the reference leaves both
// names undeclared (ref TargetFrame.xml l.486-487 passes them anyway).
assert!(
!shown(&s, "TargetFrameHealthBarText"),
"the switch pins your own numbers, never the target's"
s.eval::<bool>(
"return TargetFrameHealthBarText == nil and TargetFrameManaBarText == nil \
and TargetFrameHealthBar.TextString == nil and TargetFrameManaBar.TextString == nil"
)
.unwrap(),
"the reference declares no numerals on the target frame, at any switch setting"
);
// ...but the HOVER reveals them, which is how you read a target's health in 1.12 (1146).
s.run("BenillaUnitFrameBar_OnEnter(TargetFrameHealthBar)")
.unwrap();
assert!(shown(&s, "TargetFrameHealthBarText"));
assert_eq!(text(&s, "TargetFrameHealthBarText"), "50 / 100");
// `this` bound, as the engine binds it for a bar's own `<OnLeave>`: the stock
// `HideTextStatusBarText` reads `this.isZero` on one line where every other reads `bar`
// (TextStatusBar.lua:95 — the reference's own slip; a nil index only from bare Lua).
s.run(
"this = TargetFrameHealthBar BenillaUnitFrameBar_OnLeave(TargetFrameHealthBar) this = nil",
)
.unwrap();
// Hovering a target bar pops the unit TOOLTIP instead — the `elseif this:GetParent() ==
// TargetFrame` arm of the template's own OnEnter (ref TextStatusBar.xml l.16-26). Driven
// through the real pointer path, because that arm reads `this`.
hover(&mut s, "TargetFrameHealthBar");
assert!(
!shown(&s, "TargetFrameHealthBarText"),
"and go away again — the lockShow refcount balances"
s.eval::<bool>(
"return GameTooltip:IsShown() and GameTooltip:IsOwned(TargetFrameHealthBar)"
)
.unwrap(),
"the target's bar hover is a tooltip, not numerals ({:?})",
s.errors()
);
assert!(
s.eval::<String>("return tostring(GameTooltipTextLeft1:GetText())")
.unwrap()
.contains("Somebody"),
"and it is the unit's own plate"
);
unhover(&mut s);
assert!(
s.eval::<bool>("return not GameTooltip:IsShown()").unwrap(),
"the template's OnLeave hides it (ref TextStatusBar.xml l.28-31)"
);
// The same hover works with the switch OFF, which is the point of it on YOUR frame too: the
// numbers are there when you go looking, without living on the bar.
// The hover on YOUR bar with the switch OFF is the reveal that still exists: the numbers are
// there when you go looking, without living on the bar. `ShowTextStatusBarText` /
// `HideTextStatusBarText` and their `lockShow` refcount (ref TextStatusBar.lua l.77-102) are
// the mechanism, reached through the template's OnEnter/OnLeave.
s.run("SetCVar(\"statusBarText\", \"0\", \"STATUS_BAR_TEXT\")")
.unwrap();
s.tick(0.0);
assert!(!shown(&s, "PlayerFrameHealthBarText"));
s.run("BenillaUnitFrameBar_OnEnter(PlayerFrameHealthBar)")
.unwrap();
hover(&mut s, "PlayerFrameHealthBar");
assert!(
shown(&s, "PlayerFrameHealthBarText"),
"hover shows them even with the option off"
"hover shows them even with the option off ({:?})",
s.errors()
);
unhover(&mut s);
assert!(
!shown(&s, "PlayerFrameHealthBarText"),
"and go away again — the lockShow refcount balances"
);
s.run(
"this = PlayerFrameHealthBar BenillaUnitFrameBar_OnLeave(PlayerFrameHealthBar) this = nil",
)
.unwrap();
assert!(!shown(&s, "PlayerFrameHealthBarText"));
s.run("SetCVar(\"statusBarText\", \"1\", \"STATUS_BAR_TEXT\")")
.unwrap();
s.tick(0.0);
@@ -1555,10 +1732,11 @@ fn status_bar_text_paints_the_player_numerals_but_not_the_targets() {
s.fire_event("UNIT_HEALTH", vec![ScriptValue::Str("player".into())]);
assert_eq!(text(&s, "PlayerFrameHealthBarText"), "31 / 100");
// A power-type change repaints the same way (it used to re-LABEL the bar too — 1147 cut that).
// A power-type change repaints AND re-labels — `UnitFrame_UpdateManaType` runs off
// UNIT_DISPLAYPOWER (ref UnitFrame.lua l.36-40) and the prefix follows the new resource.
s.set_unit("player", Some(alive(31, 60, 3)));
s.fire_event("UNIT_DISPLAYPOWER", vec![ScriptValue::Str("player".into())]);
assert_eq!(text(&s, "PlayerFrameManaBarText"), "60 / 80");
assert_eq!(text(&s, "PlayerFrameManaBarText"), "Energy 60 / 80");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}