- a panic writes benilla-config/Diagnostics/crash-<time>.txt with the build id, the location, a backtrace and the last 200 log lines, and names the file as its last line, so a bug report can attach it (2266) - every wire-count allocation hint is capped and the compressed update object reads through the cursor, so a malformed packet can no longer ask for gigabytes (2266) - the 23 in-world run conditions are one set, the 45 probe variables one registry, the install-gated tests say when they skip, and every gate is timed (2266) - one workspace-level lint allowance replaces 413 per-site attributes, and the decision corpus gets a lint in the gate (2266) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
610 lines
30 KiB
Rust
610 lines
30 KiB
Rust
//! **The reference's frame FLAGS are the test oracle** — the flag half of decision 0675's "the
|
|
//! reference file is the test oracle", and the gate for the class decision 1739 closed.
|
|
//!
|
|
//! `framexml_diff` guards the *numbers* a transcribed window carries. Nothing guarded its *flags*,
|
|
//! and they went quietly wrong at scale: on 2026-08-30 a sweep found **47 frames** the reference
|
|
//! marks `toplevel` and ours did not (so no window ever came to the front), **23** the reference
|
|
//! makes mouse-interactive and ours did not (so a click on a window's own background fell through
|
|
//! to the 3D world), and **81** carrying a reference `id=` ours dropped (so `GetID()` — the
|
|
//! contract every 1.12 addon reads a slot index out of — answered 0). Every gate was green
|
|
//! throughout, because a flag is invisible to a behavioural test that never presses on the frame.
|
|
//!
|
|
//! ## Two properties make this a guard rather than a comfort
|
|
//!
|
|
//! **Our side is read from the ENGINE, never from our XML.** An attribute-vs-attribute diff is
|
|
//! precisely what let the class hide, twice over: benilla renames templates (`ChatFrameTemplate` →
|
|
//! `BenillaChatFrameTemplate`), so a name-keyed text diff reports a gap of zero on a window that is
|
|
//! entirely missing the flag; and the `<Scripts>` **auto-enable** law (wow-re
|
|
//! `ui/scratch/scripts-auto-enable.md` §1, VERIFIED — an `<OnEnter>`/`<OnLeave>`/`<OnMouseDown>`/
|
|
//! `<OnMouseUp>`/`<OnDragStart>` reaches the same enable primitive `0x76af00(2,-1)` the attribute
|
|
//! does) makes `enableMouse=` a poor proxy for whether the frame actually takes the mouse. Asking
|
|
//! the loaded engine — `IsToplevel()`, `IsMouseEnabled()`, `GetID()`, `GetParent()` — is immune to
|
|
//! both, and to a third: a `parent=` can arrive from a template or from nesting, and `GetParent()`
|
|
//! answers the same whichever way it came.
|
|
//!
|
|
//! **Divergences are an explicit list with a reason each, never a pattern.** [`KNOWN`] carries
|
|
//! them, in both directions; a new one cannot hide inside a tolerance. **It is empty today** —
|
|
//! 1751's window migrations, 1795, 1970 and 1980 each retired a group, and the in-body notes are
|
|
//! the record of what each one was. The array stays because an empty list is the thing that
|
|
//! notices a divergence arriving.
|
|
//!
|
|
//! ## Four flags, and only four
|
|
//!
|
|
//! `toplevel`, the effective mouse enable, `id` and the **parent** are mechanical: the reference's
|
|
//! value is right for any frame we transcribe, and a difference is a defect. The neighbours are
|
|
//! **not**, and are deliberately out of scope rather than silently tolerated — `movable` is an
|
|
//! inert flag for all but two reference frames (nothing calls `StartMoving` on the rest, so
|
|
//! copying it would be cargo-cult), `frameStrata` decides what draws over what and is the
|
|
//! director's call, `setAllPoints` has an exact `<Size>`+`<Anchors>` equivalent that five of our
|
|
//! pages deliberately use, and `hidden` is equivalent whenever the reference hides in `OnLoad`
|
|
//! instead. Decision 1739 carries the reasoning per attribute.
|
|
//!
|
|
//! **The parent joined them at decision 1757, and it is the flag with teeth.** Until 1734 restored
|
|
//! `SetFullScreenFrame`'s `UIParent:Hide()`, a `parent=` was little more than a coordinate space;
|
|
//! after it, the seat decides whether a frame can be on screen *at all* while a fullscreen panel
|
|
//! is up. 1734 swept the direction it went looking for — 72 declarations the reference has and we
|
|
//! had dropped — and nothing asked the opposite question, so the frames we parent that the
|
|
//! reference leaves top-level went the other way in silence: opening the world map hid the map's
|
|
//! own blackout (the 3D world came back through the margins beside the 4:3 sheet — the director
|
|
//! reported it), the shared dropdown lists could not open over it, and the screenshot
|
|
//! confirmation could not appear during the fly-by whose SCREENSHOT key `CinematicFrame` hands
|
|
//! back by name. A gate that reads one direction of a two-directional property is a gate someone
|
|
//! has to remember to run backwards.
|
|
//!
|
|
//! Both ways a frame acquires a parent count, because the XML makes them look unrelated: the
|
|
//! `parent=` attribute — **on the element or on a template it inherits**, which is where the
|
|
//! reference keeps the chat frames' — and nesting inside another frame's `<Frames>`. Reading only
|
|
//! instances reports all fourteen chat frames as top-level; reading only attributes reports
|
|
//! `ScreenshotStatus` as parentless when the reference nests it in `WorldFrame` for the property
|
|
//! `WorldFrame.xml`'s header states outright: *"Children of the world frame are visible even when
|
|
//! the UI is turned off."*
|
|
//!
|
|
//! ## What it can and cannot see
|
|
//!
|
|
//! The population is the shipped tree's **named, published frames**, so a virtual template is not
|
|
//! compared directly — but every instance of one is, carrying the template's resolved flags, which
|
|
//! is the same coverage by a different route (a `KNOWN` entry naming a template therefore reads as
|
|
//! stale and is refused).
|
|
//!
|
|
//! The reference side is **XML only**: a `SetID` or `EnableMouse` the reference makes from Lua at
|
|
//! `OnLoad` is invisible here and reads as absent. That blind spot only ever under-reports — it
|
|
//! can hide a divergence, never invent one — so nothing it misses turns into a false failure. It
|
|
//! would be a `KNOWN` entry's reason if any frame still needed one.
|
|
//!
|
|
//! The whole module skips cleanly with no install — `_extracted_framexml/` is a gitignored
|
|
//! Blizzard asset, like every other client-data test here.
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use benilla_ui::framexml::{self, Element, TopLevel};
|
|
|
|
/// Element tags that are *frames* — the ones with flags to compare. Regions (`<Texture>`,
|
|
/// `<FontString>`) carry names too and must not shadow a frame of the same name.
|
|
const FRAME_TAGS: &[&str] = &[
|
|
"Frame",
|
|
"Button",
|
|
"CheckButton",
|
|
"EditBox",
|
|
"ScrollFrame",
|
|
"Slider",
|
|
"StatusBar",
|
|
"MessageFrame",
|
|
"ScrollingMessageFrame",
|
|
"Model",
|
|
"PlayerModel",
|
|
"DressUpModel",
|
|
"TabardModel",
|
|
"ColorSelect",
|
|
"SimpleHTML",
|
|
"GameTooltip",
|
|
"Minimap",
|
|
"MovieFrame",
|
|
"WorldFrame",
|
|
];
|
|
|
|
/// The five `<Scripts>` handler names that auto-enable the MOUSE kind, and only those five
|
|
/// (wow-re `ui/scratch/scripts-auto-enable.md` §1's kind-2 OR-chain, `0x769fb7`..`0x76a022`).
|
|
/// `OnDragStop`/`OnReceiveDrag` bind a slot and trip no enable — they are deliberately absent.
|
|
const MOUSE_HANDLERS: &[&str] = &[
|
|
"OnEnter",
|
|
"OnLeave",
|
|
"OnMouseDown",
|
|
"OnMouseUp",
|
|
"OnDragStart",
|
|
];
|
|
|
|
/// Whether an element's TAG is a widget kind the constructor mouse-enables — so it needs no
|
|
/// `enableMouse` and no handler to be clickable.
|
|
///
|
|
/// **Answered by the engine, not by a copy of its list.** This used to be a `MOUSE_BY_CTOR` array
|
|
/// here whose own comment said it was "deliberately the SAME list `WidgetArena::create` uses… so a
|
|
/// wrong entry is wrong in one place rather than two". It was a second list, and it drifted
|
|
/// silently the first time the engine's was edited — this sweep then reported six scroll frames as
|
|
/// divergences that were only the two models disagreeing with each other, which is precisely what
|
|
/// the comment claimed could not happen.
|
|
fn tag_mouse_enabled_by_ctor(tag: &str) -> bool {
|
|
benilla_ui::script::frame_kind_from_tag(tag)
|
|
.is_some_and(benilla_ui::widget::mouse_enabled_by_ctor)
|
|
}
|
|
|
|
/// One accepted difference between benilla and the reference, with the reason it is accepted.
|
|
///
|
|
/// `frame` is benilla's name for it; `flag` is which of the four; `why` is why the difference is
|
|
/// right (or, for the handler gaps, why the honest fix is not this flag).
|
|
struct Known {
|
|
frame: &'static str,
|
|
flag: Flag,
|
|
why: &'static str,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
enum Flag {
|
|
Toplevel,
|
|
Mouse,
|
|
Id,
|
|
Parent,
|
|
}
|
|
|
|
impl std::fmt::Display for Flag {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(match self {
|
|
Flag::Toplevel => "toplevel",
|
|
Flag::Mouse => "mouse",
|
|
Flag::Id => "id",
|
|
Flag::Parent => "parent",
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The accepted differences — **none, today**. The four groups this list once carried (the frames
|
|
/// the reference made mouse-interactive through handlers we did not carry, the merchant rows, the
|
|
/// faux scroll panes, the Lua-set ids) each retired with the window that owned them; the in-body
|
|
/// notes below name the record for each. An empty array is not an oversight: it is what fails the
|
|
/// gate the moment a new divergence appears, and every entry it ever holds is a judgement someone
|
|
/// made, never a tolerance.
|
|
const KNOWN: &[Known] = &[
|
|
// ── The reference had it and we did not: seven handler gaps, not flag gaps (all retired) ───
|
|
//
|
|
// In each of these the reference's mouse came from an `<OnEnter>`/`<OnLeave>` pair whose body
|
|
// we had not built, and the interaction the player would notice was the TOOLTIP those handlers
|
|
// show — not the click-blocking the flag gives. Declaring `enableMouse="true"` would have made
|
|
// the frame swallow the click and hand back nothing, which is worse than the gap.
|
|
// `PetPaperDollFrameExpBar` RETIRED here (decision 1751's character-sheet window). It read
|
|
// "the reference bar inherits TextStatusBar, whose OnEnter/OnLeave show the value text; ours is
|
|
// a plain StatusBar" — true of OUR `PetPaperDollFrame.xml`, which is deleted. The pet page is
|
|
// the reference's own file now, so its XP bar inherits `TextStatusBar` because it IS the
|
|
// reference's declaration, and the divergence has nothing left to describe.
|
|
//
|
|
// Seven `*ScrollFrame` mouse entries RETIRED here (1795). They read "our faux scroll pane is a
|
|
// Frame with an explicit `<OnMouseWheel>`, not a ScrollFrame" — a divergence that existed only
|
|
// because OUR `ScrollFrame` ctor took the mouse and the reference's does not. Correcting the
|
|
// ctor list to the client's made both sides agree, and an accepted divergence that has been
|
|
// fixed is documentation claiming a defect we do not have.
|
|
// The two `TargetofTarget*Bar` entries RETIRED here (the unit-frame migration), for the same
|
|
// reason their twin the pet page's XP bar went: they said our bars were plain StatusBars where
|
|
// the reference inherits `TextStatusBar` — true of our transcription, and no longer true of
|
|
// anything. `TargetFrame.xml` is the reference's own now and its ToT bars inherit
|
|
// `TextStatusBar` like every other unit bar. The gate found them itself, which is its job.
|
|
// ── We take the mouse where the reference does not ─────────────────────────────────────────
|
|
//
|
|
// The merchant rows' divergence RETIRED (1751): our `MerchantFrame.xml` is gone and the
|
|
// reference's own file is on the player's chain, so its rows are its rows. There were 13
|
|
// entries here — twelve `MerchantItem<N>` plus `MerchantBuyBackItem` — saying ours took the
|
|
// mouse on the row itself where the reference splits each row into an inert container plus a
|
|
// `$parentItemButton`. That is exactly what this gate exists to notice going away.
|
|
// The `WorldMapFrame` mouse entry RETIRED (1980): it said our map body took the mouse where
|
|
// the reference relies on `WorldMapButton` alone — true of our transcription, and the map is
|
|
// the reference's own file now.
|
|
// ── The faux scroll panes: a different WIDGET KIND, not a missing flag ─────────────────────
|
|
//
|
|
// The reference declares each of these `<ScrollFrame …inherits="FauxScrollFrameTemplate">` and
|
|
// takes the mouse from that kind's constructor, even though a faux pane never really scrolls —
|
|
// the kind is chosen for the wheel and the scrollbar plumbing. benilla's `FauxScrollFrameTemplate`
|
|
// is a plain `<Frame>` and wires the wheel explicitly, with an `<OnMouseWheel>` on the pane
|
|
// routed through `BenillaFauxScrollFrame_OnMouseWheel` (FriendsFrame.xml's own note at the
|
|
// friends pane). Same behaviour — the list scrolls under the wheel — reached a different way,
|
|
// and the reference's own faux panes use nothing else the mouse flag gives.
|
|
//
|
|
// The two mail panes are a further step out: ours are flat art with a FontString body, a
|
|
// render approximation MailFrame.xml names at the site, so neither side scrolls them.
|
|
// ── id ─────────────────────────────────────────────────────────────────────────────────────
|
|
//
|
|
// Empty, and it is worth saying why rather than leaving a bare header. The reference side of
|
|
// this comparison is XML only: the sweep cannot see a `SetID` the reference makes from Lua at
|
|
// `OnLoad`, so a frame the reference numbers *there* used to read as 0 here while ours
|
|
// declared the number in XML. The five entries that lived here were all one case — the bag
|
|
// bar's `CharacterBag0..3Slot` and `KeyRingButton`.
|
|
//
|
|
// 1751's third window deleted them by making the difference not exist: the bar IS
|
|
// `Interface\FrameXML\MainMenuBarBagButtons.xml` now, so those ids come from
|
|
// `PaperDollItemSlotButton_OnLoad`'s `GetInventorySlotInfo` and from `this:SetID(
|
|
// KEYRING_CONTAINER)` — the reference's own Lua, on both sides. This gate reported them the
|
|
// frame it was built to report (1751 §5: the drift instruments retire with the copies).
|
|
// ── parent ─────────────────────────────────────────────────────────────────────────────────
|
|
//
|
|
// Empty since 1970. The rows that lived here were seats inside the SAME tree the reference
|
|
// seats them in — the question this flag exists to ask (decision 1757): a frame whose seat
|
|
// crosses the boundary between UIParent's tree and the top level is a defect, because
|
|
// `SetFullScreenFrame` hides `UIParent` and everything below it; a frame seated one rung
|
|
// along inside that tree is not. The last two were our mail transcription's flat body panes,
|
|
// which hung their content off the scroll frame with no scroll child between; the mail
|
|
// window is the reference's own now, scroll children and all.
|
|
];
|
|
|
|
/// The extracted reference FrameXML directory, or `None` when the install isn't there.
|
|
fn reference_dir() -> Option<PathBuf> {
|
|
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../WoW/_extracted_framexml");
|
|
dir.is_dir().then_some(dir)
|
|
}
|
|
|
|
/// Every named *frame* element in the reference corpus, keyed by name — templates and instances
|
|
/// alike, nested `<Frames>` included, `$parent`-relative names excluded (they repeat across
|
|
/// templates and name nothing on their own).
|
|
fn reference_frames() -> Option<HashMap<String, Element>> {
|
|
let dir = reference_dir()?;
|
|
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
|
.ok()?
|
|
.flatten()
|
|
.map(|e| e.path())
|
|
.filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("xml")))
|
|
.collect();
|
|
paths.sort();
|
|
|
|
let mut out: HashMap<String, Element> = HashMap::new();
|
|
for path in paths {
|
|
// Blizzard ships a UTF-8 BOM on some of these and stray high bytes in comments; the parse
|
|
// is what matters, so read lossily rather than refusing the file.
|
|
let bytes = std::fs::read(&path).ok()?;
|
|
let text = String::from_utf8_lossy(&bytes);
|
|
let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else {
|
|
continue;
|
|
};
|
|
for item in &doc.items {
|
|
if let TopLevel::Template(el) | TopLevel::Instance(el) = item {
|
|
collect_named(el, &mut out);
|
|
}
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
/// Recurse an element tree, publishing every named frame. First name wins, matching the client's
|
|
/// auto-publish rule.
|
|
fn collect_named(el: &Element, out: &mut HashMap<String, Element>) {
|
|
if FRAME_TAGS.iter().any(|t| t.eq_ignore_ascii_case(&el.tag)) {
|
|
if let Some(name) = el.attr("name") {
|
|
if !name.contains("$parent") {
|
|
out.entry(name.to_string()).or_insert_with(|| el.clone());
|
|
}
|
|
}
|
|
}
|
|
for child in &el.children {
|
|
collect_named(child, out);
|
|
}
|
|
}
|
|
|
|
/// Every named reference frame's **enclosing frame**, if it is nested inside one.
|
|
///
|
|
/// A frame acquires its parent one of two ways, and the XML gives no hint that they are the same
|
|
/// question: `parent="X"` (on the element, or on a template it inherits — see [`resolved_attr`]),
|
|
/// or *nesting* inside another frame's `<Frames>`. The reference uses both heavily —
|
|
/// `DropDownList1` is top-level with neither, `ScreenshotStatus` carries no attribute at all and
|
|
/// is nested in `WorldFrame` — so reading only one of them is worse than reading neither. This
|
|
/// half is the nesting; `resolved_attr` is the other, and the attribute wins where both exist.
|
|
/// `enclosing` is the nearest named frame ancestor, threaded down the walk.
|
|
fn collect_nesting(
|
|
el: &Element,
|
|
enclosing: Option<&str>,
|
|
out: &mut HashMap<String, Option<String>>,
|
|
) {
|
|
let mut inner = enclosing;
|
|
if FRAME_TAGS.iter().any(|t| t.eq_ignore_ascii_case(&el.tag)) {
|
|
if let Some(name) = el.attr("name") {
|
|
if !name.contains("$parent") {
|
|
out.entry(name.to_string())
|
|
.or_insert_with(|| enclosing.map(str::to_string));
|
|
inner = Some(name);
|
|
}
|
|
}
|
|
}
|
|
for child in &el.children {
|
|
collect_nesting(child, inner, out);
|
|
}
|
|
}
|
|
|
|
/// The reference's nesting table, built over the same corpus [`reference_frames`] reads.
|
|
fn reference_nesting() -> Option<HashMap<String, Option<String>>> {
|
|
let dir = reference_dir()?;
|
|
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
|
.ok()?
|
|
.flatten()
|
|
.map(|e| e.path())
|
|
.filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("xml")))
|
|
.collect();
|
|
paths.sort();
|
|
|
|
let mut out: HashMap<String, Option<String>> = HashMap::new();
|
|
for path in paths {
|
|
let Ok(bytes) = std::fs::read(&path) else {
|
|
continue;
|
|
};
|
|
let text = String::from_utf8_lossy(&bytes);
|
|
let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else {
|
|
continue;
|
|
};
|
|
for item in &doc.items {
|
|
if let TopLevel::Template(el) | TopLevel::Instance(el) = item {
|
|
collect_nesting(el, None, &mut out);
|
|
}
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
/// The templates `el` inherits, **right to left** — a later name in the list overrides an earlier
|
|
/// one, so the search for an inherited value has to try the last one first.
|
|
fn inherits(el: &Element) -> impl Iterator<Item = &str> {
|
|
el.attr("inherits")
|
|
.unwrap_or("")
|
|
.split(',')
|
|
.map(str::trim)
|
|
.filter(|s| !s.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.into_iter()
|
|
.rev()
|
|
}
|
|
|
|
/// An attribute's value on `name`, following `inherits=`; the element's own value wins over any
|
|
/// template's. `depth` guards a malformed cycle (the shipped corpus has none).
|
|
fn resolved_attr<'a>(
|
|
name: &str,
|
|
frames: &'a HashMap<String, Element>,
|
|
attr: &str,
|
|
depth: u32,
|
|
) -> Option<&'a str> {
|
|
if depth > 16 {
|
|
return None;
|
|
}
|
|
let el = frames.get(name)?;
|
|
if let Some(v) = el.attr(attr) {
|
|
return Some(v);
|
|
}
|
|
inherits(el).find_map(|t| resolved_attr(t, frames, attr, depth + 1))
|
|
}
|
|
|
|
/// Whether any element in `name`'s inherits chain declares one of `handlers` inside `<Scripts>`.
|
|
fn declares_handler(
|
|
name: &str,
|
|
frames: &HashMap<String, Element>,
|
|
handlers: &[&str],
|
|
depth: u32,
|
|
) -> bool {
|
|
if depth > 16 {
|
|
return false;
|
|
}
|
|
let Some(el) = frames.get(name) else {
|
|
return false;
|
|
};
|
|
let own = el
|
|
.children
|
|
.iter()
|
|
.filter(|c| c.tag.eq_ignore_ascii_case("Scripts"))
|
|
.flat_map(|s| s.children.iter())
|
|
.any(|h| handlers.iter().any(|w| w.eq_ignore_ascii_case(&h.tag)));
|
|
own || inherits(el).any(|t| declares_handler(t, frames, handlers, depth + 1))
|
|
}
|
|
|
|
/// Whether the reference's frame of this name **takes the mouse** once loaded — the three ways
|
|
/// `0x76af00(2, -1)` is reached, per `scripts-auto-enable.md` §1.3: the widget's own ctor, the
|
|
/// `enableMouse` attribute, or an auto-enabling `<Scripts>` handler.
|
|
fn reference_takes_mouse(name: &str, frames: &HashMap<String, Element>) -> bool {
|
|
let Some(el) = frames.get(name) else {
|
|
return false;
|
|
};
|
|
if tag_mouse_enabled_by_ctor(&el.tag) {
|
|
return true;
|
|
}
|
|
if resolved_attr(name, frames, "enableMouse", 0).is_some_and(|v| v.eq_ignore_ascii_case("true"))
|
|
{
|
|
return true;
|
|
}
|
|
declares_handler(name, frames, MOUSE_HANDLERS, 0)
|
|
}
|
|
|
|
/// The reference name for one of ours: the same name, or the one behind our `Benilla` prefix
|
|
/// (`BenillaChatFrameTemplate` is the reference's `ChatFrameTemplate` — the rename that made a
|
|
/// text diff report a gap of zero on a window missing every flag).
|
|
fn reference_name<'a>(ours: &str, frames: &'a HashMap<String, Element>) -> Option<&'a str> {
|
|
if let Some((k, _)) = frames.get_key_value(ours) {
|
|
return Some(k);
|
|
}
|
|
let bare = ours.strip_prefix("Benilla")?;
|
|
frames.get_key_value(bare).map(|(k, _)| k.as_str())
|
|
}
|
|
|
|
/// One divergence found by the sweep, rendered for the failure message.
|
|
fn describe(frame: &str, flag: Flag, ours: &str, theirs: &str) -> String {
|
|
format!(" {frame} — {flag}: ours {ours}, reference {theirs}")
|
|
}
|
|
|
|
/// **Every frame both UIs name carries the reference's `toplevel`, mouse enable, `id` and
|
|
/// parent** — read off the loaded engine, with [`KNOWN`] the only accepted differences.
|
|
///
|
|
/// Verified to fail: delete `toplevel="true"` from `CharacterFrame.xml` and this names
|
|
/// `CharacterFrame`; delete `id="1"` from `ActionButton1` and it names that; put
|
|
/// `parent="UIParent"` back on `BlackoutWorld` and it names that.
|
|
#[test]
|
|
fn the_shipped_frames_carry_the_references_flags() {
|
|
// Two gates, because the corpus and the manifest are two assets: the extracted reference
|
|
// dir below, and the chain `load_default_ui` reads — under `WOW_DATA=` (1451) the first is
|
|
// still there and the second is not.
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let Some(reference) = reference_frames() else {
|
|
eprintln!("skipping: no extracted reference FrameXML (WoW/_extracted_framexml)");
|
|
return;
|
|
};
|
|
let nesting = reference_nesting().expect("the same corpus the frames came from");
|
|
assert!(
|
|
reference.len() > 500,
|
|
"only {} reference frames parsed — the corpus scan broke, and a sweep over nothing \
|
|
passes whatever we did",
|
|
reference.len()
|
|
);
|
|
|
|
let mut s = benilla_ui::script::UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
// The in-game UI materializes on world entry (1051), so a player always exists by the time the
|
|
// manifest loads — and the stock macro window's character tab formats `UnitName("player")`
|
|
// into its label inside its own OnLoad. A manifest load with no player is a state the client
|
|
// never reaches (decision 1848).
|
|
s.set_unit(
|
|
"player",
|
|
Some(benilla_ui::script::UnitState {
|
|
exists: true,
|
|
name: Some("Probefour".into()),
|
|
level: 60,
|
|
..Default::default()
|
|
}),
|
|
);
|
|
let failures = super::load_default_ui(&s);
|
|
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
|
|
s.resolve();
|
|
|
|
// The loaded frame's own parent, by name — `GetParent()`, not the `parent=` attribute, so a
|
|
// frame that acquires its parent by NESTING reads the same as one that declares it. An empty
|
|
// string is a top-level frame.
|
|
let parent_of = |n: &str| -> Option<String> {
|
|
s.eval::<String>(&format!(
|
|
"local f = getglobal(\"{n}\") if not f or not f.GetParent then return \"\" end \
|
|
local p = f:GetParent() return (p and p:GetName()) or \"\""
|
|
))
|
|
.ok()
|
|
.filter(|v| !v.is_empty())
|
|
};
|
|
|
|
// Read the flags off the ENGINE, not off our XML — the whole point of the module.
|
|
let flags = |n: &str| -> Option<(bool, bool, i64)> {
|
|
s.eval::<i64>(&format!(
|
|
"local f = getglobal(\"{n}\") \
|
|
if not f or not f.IsToplevel then return -1 end \
|
|
return (f:IsToplevel() and 1 or 0) + (f:IsMouseEnabled() and 2 or 0)"
|
|
))
|
|
.ok()
|
|
.filter(|v| *v >= 0)
|
|
.map(|v| {
|
|
let id = s
|
|
.eval::<i64>(&format!("return getglobal(\"{n}\"):GetID()"))
|
|
.unwrap_or(0);
|
|
(v & 1 == 1, v & 2 == 2, id)
|
|
})
|
|
};
|
|
|
|
let mut divergences: Vec<String> = Vec::new();
|
|
let mut compared = 0usize;
|
|
// Every KNOWN entry starts unclaimed; a real divergence claims it. What is left at the end
|
|
// is an entry describing a difference that no longer exists.
|
|
let mut unused: Vec<&Known> = KNOWN.iter().collect();
|
|
|
|
let mut ours: Vec<String> = super::shipped_xml_tests::shipped_frame_names();
|
|
ours.sort();
|
|
for name in &ours {
|
|
let Some(theirs) = reference_name(name, &reference) else {
|
|
continue;
|
|
};
|
|
let Some((toplevel, mouse, id)) = flags(name) else {
|
|
continue; // not a frame in the loaded tree (a template, or a region name)
|
|
};
|
|
compared += 1;
|
|
|
|
let want_toplevel = resolved_attr(theirs, &reference, "toplevel", 0)
|
|
.is_some_and(|v| v.eq_ignore_ascii_case("true"));
|
|
let want_mouse = reference_takes_mouse(theirs, &reference);
|
|
let want_id: i64 = resolved_attr(theirs, &reference, "id", 0)
|
|
.and_then(|v| v.trim().parse().ok())
|
|
.unwrap_or(0);
|
|
// The declared parent wins over the nesting, and `resolved_attr` follows `inherits=` —
|
|
// which is not a detail: the reference puts the chat frames' `parent="UIParent"` on
|
|
// `FloatingChatFrameTemplate`/`ChatTabTemplate`, and reading instances alone would report
|
|
// all fourteen as top-level. That template blind spot is the one 1734's own gap analysis
|
|
// fell into (commit "the chat templates carry parent=UIParent").
|
|
//
|
|
// Parent names are then compared through the same rename map the frames themselves are: a
|
|
// benilla-only prefix on the PARENT would otherwise report every child of it as diverged.
|
|
let want_parent = resolved_attr(theirs, &reference, "parent", 0)
|
|
.map(str::to_string)
|
|
.or_else(|| nesting.get(theirs).cloned().flatten());
|
|
let parent = parent_of(name);
|
|
let parent_matches = match (&parent, &want_parent) {
|
|
(None, None) => true,
|
|
(Some(p), Some(w)) => p == w || p.strip_prefix("Benilla") == Some(w.as_str()),
|
|
_ => false,
|
|
};
|
|
let show = |p: &Option<String>| p.clone().unwrap_or_else(|| "(top-level)".into());
|
|
|
|
for (flag, differs, ours_s, theirs_s) in [
|
|
(
|
|
Flag::Toplevel,
|
|
toplevel != want_toplevel,
|
|
toplevel.to_string(),
|
|
want_toplevel.to_string(),
|
|
),
|
|
(
|
|
Flag::Mouse,
|
|
mouse != want_mouse,
|
|
mouse.to_string(),
|
|
want_mouse.to_string(),
|
|
),
|
|
(Flag::Id, id != want_id, id.to_string(), want_id.to_string()),
|
|
(
|
|
Flag::Parent,
|
|
!parent_matches,
|
|
show(&parent),
|
|
show(&want_parent),
|
|
),
|
|
] {
|
|
if !differs {
|
|
continue;
|
|
}
|
|
if KNOWN.iter().any(|k| k.frame == name && k.flag == flag) {
|
|
unused.retain(|u| !(u.frame == name && u.flag == flag));
|
|
continue;
|
|
}
|
|
divergences.push(describe(name, flag, &ours_s, &theirs_s));
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
// 1948 retired our ChatFrame.xml, 1952 our SpellBookFrame.xml, 1953 our PetActionBar.xml,
|
|
// 1956 our SkillFrame.xml, 1958 our UnitPopup.xml and 1959 our FriendsFrame.xml (their
|
|
// frames are the reference's own now), which took the paired count from the low 400s to
|
|
// the high 150s; 1966 (TradeFrame), 1968 (GameTooltip), 1969 (DressUpFrame) and 1970
|
|
// (MailFrame) to the high 80s, and 1980 (WorldMapFrame, whose 50-odd blip frames were
|
|
// most of what was left) to the high teens, and 1987 (the micro row) to nine. The floor
|
|
// guards the pairing, not the census — it comes down with every window that migrates,
|
|
// and reaches zero with the last file of ours that declares a reference-named frame.
|
|
compared > 5,
|
|
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
|
|
);
|
|
assert!(
|
|
divergences.is_empty(),
|
|
"{} frame flag(s) diverge from the reference. Each is either a defect to fix or an entry \
|
|
to add to KNOWN with the reason it is right — never a tolerance:\n{}",
|
|
divergences.len(),
|
|
divergences.join("\n")
|
|
);
|
|
// A KNOWN entry that no longer describes a real difference is stale documentation claiming a
|
|
// divergence that has been fixed — the exact rot this module exists to prevent elsewhere.
|
|
let stale: Vec<String> = unused
|
|
.iter()
|
|
.map(|k| format!(" {} ({}) — claimed: {}", k.frame, k.flag, k.why))
|
|
.collect();
|
|
assert!(
|
|
stale.is_empty(),
|
|
"{} KNOWN entr(y/ies) no longer describe a real difference — delete them, an accepted \
|
|
divergence that has been fixed is documentation claiming a defect we do not have:\n{}",
|
|
stale.len(),
|
|
stale.join("\n")
|
|
);
|
|
}
|