- a size-less `<Model>` pane's rect is its file's box in layout units: the reference's geometry overrides (`0x76d080`/`0x76d0d0`) answer the bbox extent whenever no size is authored, `bboxExtent · 768·√(a²+1) · layoutScale` FrameXML units (`1280 · extent` at 4:3), written into the pane's layout input when the file's facts land, re-derived on an aspect change, and ended for good by any authored size; `SetPosition` is the same unit, which is how `0x4a7b20` centres the arrow at `½·GetWidth, ½·GetHeight` through the override — so the map arrow's sprite arm and its hand-sized 33.6 px footprint go (its rect is `0.0262 × 0.0263` layout units, 33.5 × 33.7 FrameXML units at 4:3, 41.0 × 41.2 at 16:9), and the stock `ItemAnim` pane plays the item push unadapted (`ReplaceIconTexture` on the type-14 batch, the 1000 ms clamp, the stock `OnAnimFinished`), `ContainerFrameAdapters.xml` losing 0887's Lua card and its hand-measured 42.41 × 124.72 rect — the implicit one at 16:9 — and keeping the keyring fork and the three bag verbs (2015); and the cooldown is the reference's own model: the texture-transform law `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`, the raw quaternion component-lerped and never normalised, is one function (`uv_transform`) with the shader's UV fold as its twin — rotation and scale tracks bake per file sequence slot beside the translation, the mat-anim table gains an affine row `[cos − 1, sin, sx − 1, sy − 1]` on `anim_slots.z` whose identity is the zero row so every static batch reads exactly what it did, and a UI tile clones the twin material per animated batch and writes both rows every frame off the pane's play head, never the world clock; the stock `Cooldown.xml` runs off the chain on 2007's two handlers, the file's four quadrant quads turned by their own rotation tracks (`0 → −90°` over each quadrant's 250 ms window, clockwise from 12 o'clock) and its alpha tracks revealing the star for the flash; retired: the native `Cooldown` widget and `SetCooldown` (an Era API 1.12 never had — `CreateFrame("Cooldown")` is the reference's unknown-type refusal now), the loader's `Model → Cooldown` substitution, the app's hand-built pie and star, and our `Cooldown.xml`; `assets/ui` is eight files (2019)
- `UpdateMapHighlight` gets its zone-level exits (B360): at zone zoom ours returned the nil tail for every cursor position, so hovering Deadwind Pass from Duskwood showed no label where 1.12 shows "Deadwind Pass"; the reference's `0x4a7fa0` runs two lookups at zone level and answers a NAME only — first the current zone's exploration-gated overlay list (the builder at `0x4a6ad9` gating on the explored byte), each row's HitRect scaled by 1/1002 and 1/668 against the normalized cursor, edges inclusive, the first hit naming its Area1, then the continent grid re-windowed through the displayed zone's own rect, naming the neighbouring zone or city and nothing for the displayed zone itself — and only the name-only tail is reachable from there, fileName nil and the six coords zero, so the stock frame hides the highlight quad, as the director's own 1.12 shot shows; and `SetVerticalScroll` stores the offset as given (B370, B363): the reference's `0x786db0` compares the new offset against the old and writes it to `[+0x328]` verbatim, `0x787100` re-anchors the scroll child off it, and the range at `[+0x320]` is never read on that path — every clamp the reference exhibits is FrameXML's, through the scroll bar's `[min, max]`; ours clamped into `[0, GetVerticalScrollRange()]`, the child's overflow past the FRAME, which is 40 px short of where `FauxScrollFrame_Update` sends the bar on the 220-tall skills list (three rows lost) and 15 px short on the 287-tall who list (one); the setter fires `OnVerticalScroll` only on a change, as the bytes do, `BenillaScroll_Step` bounds its wheel in Lua where the reference's handler does, and the ScrollFrame kind's prose stops calling its offset unpinned (2017)
- overdraw's cheap half, the sky first: since 0588 every sky element wrote `frag_depth = 0.0`, which costs a pipeline its early-Z, so on an outdoor frame every full-screen dome fragment under the ground and the buildings was shaded in full (the gradient's `asin`, `atan2` and eight table lookups at dawn and dusk) and thrown away; one shared `sky_vertex.wgsl` — bevy's mesh vertex verbatim plus `out.position.z = 0.0` — is now the vertex stage of the gradient dome, the cloud dome, the star patches and the celestial discs and glares, and the model lane's `WOW_SKY_DEPTH` branch moves from its fragment to its vertex, so no sky fragment declares `frag_depth` and the rasterizer rejects the covered screen before the shader runs; every sky `specialize` zeroes the rasterizer bias constant the `sky_order` rung had been leaking into `DepthBiasState::constant`, harmless only while the fragment overwrote its depth, and the prose beside the pin follows it; then the retained `static_gx` pass draws BEFORE bevy's opaque pass (`StartMainPass → static_gx → MainOpaquePass`) — buildings and doodads are the frame's best occluders and a terrain fragment its dearest, six samples a pixel — so walls reject the terrain behind them instead of the reverse, the pass taking the first-use clear with it and the dev depth probe ordered after it; measured on 16 capture scenarios: 14 bit-identical, `northshire-dusk` 1 px and `water-night` 11 px, attributed to exact depth ties on static/terrain silhouettes and last-bit interpolation noise; a depth prepass and near-to-far bins are declined until a journal says where the time goes (2016)
- the grass cut goes per pixel because of a quarter-mip: `0x6813d0` sets `D3DSAMP_MIPMAPLODBIAS = +0.25` on stage 0 (`0x6813f4`) once per frame before the detail-doodad pass — capture-confirmed on all 29 doodad batches, and outside the `[0x6b1000, 0x6b3200)` scan 2004 had called complete; it decides the whole look because the `clutter_cut` probe finds 6 of 31 shipped detail atlases carry a BINARY alpha pyramid (mip 0 has DXT3's 16 levels, every level below exactly two, both starting zones among them), so `a × ramp` degenerates to a global on/off and every texel of a leaf crosses at the same depth — Elwynn's survivor coverage is flat at 47.6 % across the whole 52.5–61.1 yd band, then 0.0 %, while the probe's box-filtered control erodes 47.7 → 20.9 over the same band, so the art has the gradient the shipped pyramid threw away, and `clutter_size` puts the median tuft at 0.92 yd tall, 1.03 yd deep, a full yard for the boundary plane to sweep; the escape is bilinear: full alpha needs a 2×2 all-opaque texel neighbourhood, and those run out between mip 2 and 3 (179/56/10/0/0/0 on the dominant Elwynn cell), so the reference's λ ≈ 2.9–3.3 sits past the cliff and the tuft erodes continuously — 2.2 % of a tuft's pixels leave in the last half-yard with the bias, 11.6 % without, 24.2 % one mip sharper; wgpu has no sampler LOD bias, so the clutter lane re-samples base colour at `view.mip_bias + 0.25`, keeping the sampler every other model batch shares at the reference's 0; ruled out on the way: our BLP decoder (all nine uploaded levels byte-identical against the archive's 52 793 BLP2s), mip regeneration, the `has_mipmaps` byte's value 2 (a plain boolean, an authoring marker), anisotropy and geometry; what it does not fix is resolution-dependent, and so is the reference's own look — λ with the bias is 3.22 at the capture's 648p but 2.48 at 1080p and 1.48 at 2160p, so at a modern resolution the reference itself pops leaf by leaf, and scaling the bias with the viewport is one reversible line left for a look call (2021)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
286 lines
12 KiB
Rust
286 lines
12 KiB
Rust
//! The target frame's aura rows — the **reference's own** `Interface\FrameXML\TargetFrame.xml`
|
|
//! off the player's patch chain (decision 1751, which retired our `assets/ui/UnitFrames.xml`
|
|
//! transcription) — against its `TargetDebuffButton_Update` law (ref TargetFrame.lua l.263-387).
|
|
//! The stock XML/Lua is the unit under test; the app-side feed (`crate::ui_aura`'s target half) is
|
|
//! stubbed by pushing an [`AuraState`] list through [`UiScript::set_auras`] and firing the events
|
|
//! the feed fires — `PLAYER_TARGET_CHANGED` on a switch, `UNIT_AURA "target"` on a list change.
|
|
//!
|
|
//! Under test: the friend/hostile row swap (buffs first vs debuffs first), the 21→17 shrink when
|
|
//! the debuff count reaches the wrap (6, no target-of-target frame), the dispel-tinted border, the
|
|
//! stack count, and the hide-on-empty lifecycle. Files load in `benilla.toc`'s order, which is the
|
|
//! reference's own (BuffFrame 40 → CombatFeedback 41 → UnitFrame 43 → PlayerFrame 44 → PartyFrame
|
|
//! 45 → TargetFrame 46 → PetFrame 47), so `DebuffTypeColor` is defined ahead of the row that
|
|
//! indexes it and `RefreshBuffs` ahead of the party rows that call it from their own OnLoad.
|
|
|
|
use benilla_ui::script::{AuraState, QuadContent, ScriptValue, UiScript, UnitState};
|
|
|
|
use super::test_ui::load_ui as load_xml;
|
|
|
|
fn harness() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the dropdown kit's MenuBackdrop)
|
|
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the unit popups' kit (TargetFrameDropDown's template)
|
|
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
|
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON
|
|
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE, and its
|
|
// declarer is UIParent (ref UIParent.lua:65) since 1888.
|
|
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\UnitPopup.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");
|
|
|
|
// **Settle the target-of-target frame before any of this measures a row.** Stock
|
|
// `TargetofTargetFrame` carries no `hidden=` (ref TargetFrame.xml l.515), so it loads SHOWN,
|
|
// and the only thing that takes it down is `TargetofTarget_Update` — which
|
|
// `TargetFrame_OnEvent` runs *after* `TargetDebuffButton_Update` (ref TargetFrame.lua l.63-66,
|
|
// the update sitting inside `TargetFrame_Update` at l.51). So the very first target a freshly
|
|
// loaded client acquires lays its aura rows out for the 5-wide wrap, and only the next
|
|
// `TargetDebuffButton_Update` corrects them. One no-target `PLAYER_TARGET_CHANGED` runs that
|
|
// ladder to its end — exactly what deselecting once does — and leaves the frame in the state
|
|
// it holds for every target after the first, which is the state these tests are about.
|
|
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
|
s
|
|
}
|
|
|
|
/// Target a unit of the given reaction (2 = hostile, 5 = friendly) carrying `auras`.
|
|
fn target(s: &mut UiScript, reaction: u8, auras: Vec<AuraState>) {
|
|
s.set_unit(
|
|
"target",
|
|
Some(UnitState {
|
|
exists: true,
|
|
name: Some("Subject".into()),
|
|
health: 40,
|
|
max_health: 40,
|
|
level: 5,
|
|
reaction,
|
|
..UnitState::default()
|
|
}),
|
|
);
|
|
s.set_auras("target", Some(auras));
|
|
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
|
}
|
|
|
|
fn debuff(spell_id: u32, name: &str, count: u8, debuff_type: Option<&str>) -> AuraState {
|
|
AuraState {
|
|
spell_id,
|
|
name: Some(name.into()),
|
|
icon: Some(format!("Interface\\Icons\\Spell_{spell_id}")),
|
|
count,
|
|
debuff_type: debuff_type.map(Into::into),
|
|
// The 1.12 wire carries no duration for another unit (decision 0257 B6).
|
|
duration: 0.0,
|
|
expiration_time: 0.0,
|
|
helpful: false,
|
|
cancelable: false,
|
|
// Only the PLAYER cache carries `untilCancelled`, and only `GetPlayerBuff` reads it
|
|
// (decision 0257 / `benilla::ui_aura`); a target's rows have no such record.
|
|
until_cancelled: false,
|
|
channeled: false,
|
|
}
|
|
}
|
|
|
|
fn buff(spell_id: u32, name: &str) -> AuraState {
|
|
AuraState {
|
|
spell_id,
|
|
name: Some(name.into()),
|
|
icon: Some(format!("Interface\\Icons\\Spell_{spell_id}")),
|
|
count: 1,
|
|
debuff_type: None,
|
|
duration: 0.0,
|
|
expiration_time: 0.0,
|
|
helpful: true,
|
|
cancelable: false,
|
|
until_cancelled: false,
|
|
channeled: false,
|
|
}
|
|
}
|
|
|
|
fn shown(s: &UiScript, name: &str) -> bool {
|
|
s.eval::<bool>(&format!("return {name}:IsVisible()"))
|
|
.unwrap()
|
|
}
|
|
|
|
/// The first anchor of `name`: (point, relative frame's name, relativePoint, x, y).
|
|
fn anchor(s: &UiScript, name: &str) -> (String, String, String, f64, f64) {
|
|
s.eval(&format!(
|
|
r#"local p, rel, rp, x, y = {name}:GetPoint()
|
|
return p, (rel and rel:GetName()) or "<screen>", rp, x, y"#
|
|
))
|
|
.unwrap()
|
|
}
|
|
|
|
fn size(s: &UiScript, name: &str) -> (f64, f64) {
|
|
s.eval(&format!("return {name}:GetWidth(), {name}:GetHeight()"))
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn a_hostile_target_draws_debuffs_first_with_tint_and_count() {
|
|
let mut s = harness();
|
|
target(
|
|
&mut s,
|
|
2,
|
|
vec![
|
|
debuff(589, "Shadow Word: Pain", 1, Some("Magic")),
|
|
debuff(772, "Rend", 3, None),
|
|
buff(1126, "Mark of the Wild"),
|
|
],
|
|
);
|
|
|
|
assert!(shown(&s, "TargetFrameDebuff1"), "first debuff shows");
|
|
assert!(shown(&s, "TargetFrameDebuff2"), "second debuff shows");
|
|
assert!(
|
|
!shown(&s, "TargetFrameDebuff3"),
|
|
"no third debuff — button hides"
|
|
);
|
|
assert!(shown(&s, "TargetFrameBuff1"), "the buff shows");
|
|
assert!(!shown(&s, "TargetFrameBuff2"), "no second buff");
|
|
|
|
// Hostile: the debuff row opens at the frame's BOTTOMLEFT (5,32); buffs seat under Debuff7
|
|
// (the not-shown target-of-target leg, ref l.329).
|
|
let (p, rel, rp, x, y) = anchor(&s, "TargetFrameDebuff1");
|
|
assert_eq!(
|
|
(p.as_str(), rel.as_str(), rp.as_str(), x, y),
|
|
("TOPLEFT", "TargetFrame", "BOTTOMLEFT", 5.0, 32.0),
|
|
"hostile: debuffs first"
|
|
);
|
|
let (_, rel, _, _, _) = anchor(&s, "TargetFrameBuff1");
|
|
assert_eq!(rel, "TargetFrameDebuff7", "hostile: buffs below row 2");
|
|
|
|
// Under the wrap (2 < 6): full 21px icons, 23px borders.
|
|
assert_eq!(size(&s, "TargetFrameDebuff1"), (21.0, 21.0));
|
|
assert_eq!(size(&s, "TargetFrameDebuff1Border"), (23.0, 23.0));
|
|
assert_eq!(size(&s, "TargetFrameBuff1"), (21.0, 21.0));
|
|
|
|
// Stack count shows only above 1.
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return tostring((TargetFrameDebuff2Count:GetText()) or "")"#)
|
|
.unwrap(),
|
|
"3"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return tostring((TargetFrameDebuff1Count:GetText()) or "")"#)
|
|
.unwrap(),
|
|
""
|
|
);
|
|
|
|
// The Magic-tinted border drew (DebuffTypeColor["Magic"] = 0.20, 0.60, 1.00); Rend's untyped
|
|
// border wears the "none" red (0.80, 0, 0).
|
|
s.resolve();
|
|
let tints: Vec<[f32; 4]> = s
|
|
.extract()
|
|
.into_iter()
|
|
.filter_map(|q| match q.content {
|
|
QuadContent::Texture {
|
|
path: Some(p),
|
|
color: Some(c),
|
|
..
|
|
} if p.contains("UI-Debuff-Overlays") => Some(c),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert!(
|
|
tints
|
|
.iter()
|
|
.any(|c| (c[0] - 0.20).abs() < 1e-3 && (c[1] - 0.60).abs() < 1e-3),
|
|
"a Magic-tinted border drew, got {tints:?}"
|
|
);
|
|
assert!(
|
|
tints
|
|
.iter()
|
|
.any(|c| (c[0] - 0.80).abs() < 1e-3 && c[1].abs() < 1e-3),
|
|
"an untyped border wears the none-red, got {tints:?}"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
#[test]
|
|
fn a_friendly_target_puts_the_buff_row_first() {
|
|
let mut s = harness();
|
|
target(
|
|
&mut s,
|
|
5,
|
|
vec![
|
|
buff(1126, "Mark of the Wild"),
|
|
debuff(589, "Shadow Word: Pain", 1, Some("Magic")),
|
|
],
|
|
);
|
|
|
|
let (p, rel, rp, x, y) = anchor(&s, "TargetFrameBuff1");
|
|
assert_eq!(
|
|
(p.as_str(), rel.as_str(), rp.as_str(), x, y),
|
|
("TOPLEFT", "TargetFrame", "BOTTOMLEFT", 5.0, 32.0),
|
|
"friendly: buffs first"
|
|
);
|
|
let (p, rel, rp, x, y) = anchor(&s, "TargetFrameDebuff1");
|
|
assert_eq!(
|
|
(p.as_str(), rel.as_str(), rp.as_str(), x, y),
|
|
("TOPLEFT", "TargetFrameBuff1", "BOTTOMLEFT", 0.0, -2.0),
|
|
"friendly: debuffs under the buff row"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
#[test]
|
|
fn reaching_the_wrap_shrinks_the_first_row_to_17px() {
|
|
let mut s = harness();
|
|
let debuffs: Vec<AuraState> = (0..6)
|
|
.map(|i| debuff(1000 + i, &format!("D{i}"), 1, None))
|
|
.collect();
|
|
target(&mut s, 2, debuffs);
|
|
|
|
// 6 debuffs ≥ wrap (6): 17px icons, 19px borders — the reference resizes only the FIRST row.
|
|
assert_eq!(size(&s, "TargetFrameDebuff1"), (17.0, 17.0));
|
|
assert_eq!(size(&s, "TargetFrameDebuff1Border"), (19.0, 19.0));
|
|
assert_eq!(size(&s, "TargetFrameBuff1"), (17.0, 17.0));
|
|
|
|
// Dropping below the wrap grows them back — the feed re-fires UNIT_AURA on the change.
|
|
s.set_auras("target", Some(vec![debuff(1000, "D0", 1, None)]));
|
|
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
|
|
assert_eq!(size(&s, "TargetFrameDebuff1"), (21.0, 21.0));
|
|
assert_eq!(size(&s, "TargetFrameDebuff1Border"), (23.0, 23.0));
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
#[test]
|
|
fn clearing_the_list_or_the_target_hides_the_buttons() {
|
|
let mut s = harness();
|
|
target(&mut s, 2, vec![debuff(589, "Pain", 1, Some("Magic"))]);
|
|
assert!(shown(&s, "TargetFrameDebuff1"));
|
|
|
|
// The last debuff expires: the feed pushes the emptied list and re-fires.
|
|
s.set_auras("target", Some(vec![]));
|
|
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
|
|
assert!(
|
|
!shown(&s, "TargetFrameDebuff1"),
|
|
"an emptied list hides the button"
|
|
);
|
|
|
|
// Deselect: the frame (and every child button) hides; the token clears without a UNIT_AURA.
|
|
target(&mut s, 2, vec![debuff(589, "Pain", 1, Some("Magic"))]);
|
|
assert!(shown(&s, "TargetFrameDebuff1"));
|
|
s.set_unit("target", None);
|
|
s.set_auras("target", None);
|
|
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
|
assert!(!shown(&s, "TargetFrameDebuff1"), "no target, no buttons");
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|