//! **The bottom-of-screen clearance law, enforced** (decision 1499). //! //! The bottom band of the screen is shared: the main bar sits on it, the extra action bars, the //! stance/pet bar and the reputation watch bar stack above it, and the bag windows, the cast bar, //! the chat panes and the default tooltip anchor all have to keep clear of whatever is standing. //! `UIParent_ManageFramePositions()` (UIParent.xml) is the ONE thing that decides that — it seats //! the managed frames and writes the managed globals — and every one of the defects below was a //! frame that quietly opted out of it. //! //! Three guards, each pinning a different way to opt out: //! //! 1. [`no_shipped_file_declares_its_own_copy_of_a_managed_offset`] — the *shadow copy*. //! `BagFrame.xml` kept `local BENILLA_CONTAINER_OFFSET_X/Y = 0/70`. Locals, so the pass's //! writes could never reach them; the open-bag stack sat at the no-bars corner forever and a //! raised bottom multibar drew straight through the lowest bag window (director-caught — the //! screenshot that opened 1499). `GameTooltip.xml` carried the softer `X = X or 0` form of the //! same thing. A value with two written statements has no owner. //! //! 2. [`every_bottom_anchored_top_level_frame_is_accounted_for`] — the *new frame nobody wired //! up*. This is the guard that has to hold for features that do not exist yet: a top-level //! frame anchored to the screen's bottom edge is, by construction, in the contested band, so it //! must be a managed row, a listener consumer, or an exemption someone wrote a reason for. //! //! 3. [`no_bottom_band_frame_overlaps_a_raised_bar`] — the *symptom itself*, over the real shipped //! XML: for every combination of raised bars, no bottom-band frame's rect may intersect a //! visible bar's. This is the one that would have failed on the director's screenshot. use benilla_ui::script::UiScript; /// The globals `UIPARENT_MANAGED_FRAME_POSITIONS`'s `isVar` rows own. Only `UIParent.xml` may /// write these; everyone else reads them fresh at use (falling back inline, never by assigning). const MANAGED_GLOBALS: &[&str] = &[ "CONTAINER_OFFSET_X", "CONTAINER_OFFSET_Y", "PETACTIONBAR_XPOS", "PETACTIONBAR_YPOS", "BATTLEFIELD_TAB_OFFSET_Y", ]; fn ui_dir() -> std::path::PathBuf { std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui") } /// Blank out `` regions, keeping every newline so line numbers still point at the file. /// The scan below reads Lua, and an XML comment is prose: `BagFrame.xml`'s own explanation of the /// backpack anchor says "CONTAINER_OFFSET_X=0 / CONTAINER_OFFSET_Y=70" in English, which is a /// description of the law, not a violation of it. fn without_xml_comments(text: &str) -> String { let mut out = String::with_capacity(text.len()); let mut rest = text; while let Some(start) = rest.find("").map(|e| e + 3).unwrap_or(after.len()); for ch in after[..end].chars() { if ch == '\n' { out.push('\n'); } } rest = &after[end..]; } out.push_str(rest); out } fn shipped_xml() -> Vec<(String, String)> { let mut out: Vec<(String, String)> = std::fs::read_dir(ui_dir()) .expect("assets/ui") .flatten() .map(|e| e.path()) .filter(|p| p.extension().is_some_and(|e| e == "xml")) .map(|p| { ( p.file_name().unwrap().to_string_lossy().into_owned(), std::fs::read_to_string(&p).expect("read"), ) }) .collect(); out.sort(); // A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751), so // the floor sits well under the count rather than one step above it (1956). assert!(out.len() >= 6, "only {} xml files swept", out.len()); out } /// **Nothing outside `UIParent.xml` may ASSIGN a managed offset** — not as a local, not as a /// global, not as an `or`-guarded default. /// /// Reading one is fine and expected (`-CONTAINER_OFFSET_X` in the right stack, `CONTAINER_OFFSET_Y` /// in the bag stack and the tooltip anchor). Falling back at the point of use is fine /// (`local offsetY = CONTAINER_OFFSET_Y or 70`) — that is a read with a default, and it is what the /// per-window harnesses that ship no `UIParent.xml` need. What is forbidden is *storing* the value /// anywhere the pass cannot reach, because the pass recomputes these on every bar change and a /// stored copy is stale from the moment the next bar goes up. /// /// The rule is spelled as "an assignment statement whose left-hand side names a managed global, /// or a name containing one" — the second half is what catches `BENILLA_CONTAINER_OFFSET_Y`, which /// was not the global's name at all and is exactly why nothing noticed it for so long. #[test] fn no_shipped_file_declares_its_own_copy_of_a_managed_offset() { let mut offences = Vec::new(); for (name, text) in shipped_xml() { if name == r"Interface\FrameXML\UIParent.xml" { continue; // the owner: its var rows are where these numbers are defined } for (n, line) in without_xml_comments(&text).lines().enumerate() { let code = line.split("--").next().unwrap_or(""); let Some((lhs, _)) = code.split_once('=') else { continue; }; // `==`, `<=`, `>=`, `~=` are comparisons, not assignments. if code[lhs.len() + 1..].starts_with('=') || lhs.ends_with(['<', '>', '~', '=', '!']) || lhs.contains("--") { continue; } let lhs = lhs.trim().trim_start_matches("local ").trim(); if MANAGED_GLOBALS.iter().any(|g| lhs.contains(g)) { offences.push(format!("{name}:{}: {}", n + 1, line.trim())); } } } assert!( offences.is_empty(), "a managed offset is owned by UIParent.xml's manage pass and may only be READ elsewhere \ (fall back inline at the point of use — `local y = CONTAINER_OFFSET_Y or 70` — never by \ assigning a copy, which goes stale the moment a bar is raised; decision 1499):\n{}", offences.join("\n") ); } /// Frames whose seat the manage pass owns — the `UIPARENT_MANAGED_FRAME_POSITIONS` keys that name /// a frame, plus the two the pass's custom tail seats by hand. Kept here as literal text so this /// test and `UIParent.xml` cannot drift apart silently: the next guard asserts every one of these /// really appears in the shipped pass. const MANAGED_FRAMES: &[&str] = &[ "MultiBarBottomLeft", "GroupLootFrame1", "TutorialFrameParent", "FramerateLabel", "CastingBarFrame", "ChatFrame1", "ChatFrame2", "ShapeshiftBarFrame", "PetActionBarFrame", "QuestTimerFrame", "DurabilityFrame", "QuestWatchFrame", ]; /// Top-level frames that ARE anchored to the screen's bottom edge and are deliberately not managed. /// Every entry carries the reason, because "it was already like that" is how the bag stack got its /// shadow copy. Adding a frame here is a decision; adding one silently is what this test forbids. const BOTTOM_EXEMPT: &[(&str, &str)] = &[ ( "MainMenuBar", "the base itself — the bar everything else measures its clearance FROM, so it has no \ clearance of its own to compute", ), ("UIParent", "the full-screen root itself"), ( "ZoneTextFrame", "a mid-screen splash at BOTTOM +512 — nowhere near the contested band", ), ("SubZoneTextFrame", "as ZoneTextFrame"), ( "MultiBarRight", "a BAR, not a tenant of the band — it is one of the things the others clear. Its own seat \ is the reference's (MultiActionBars.xml: BOTTOMRIGHT -7,+98, with MultiBarLeft riding its \ TOPLEFT), and it sets the rightLeft/rightRight flags CONTAINER_OFFSET_X reads", ), ( "ItemRefTooltip", "the reference's own answer, matched exactly (ItemRef.xml l.4): frameStrata HIGH + \ toplevel + movable at BOTTOM +80. A linked-item window lands ABOVE the HIGH bars rather \ than clearing them (decision 1318), and the player drags it where they want", ), ( "ChatFrame3", "undocked chat panes are user-placed, and the reference manages only ChatFrame1/2 in \ UIPARENT_MANAGED_FRAME_POSITIONS. All five ship hidden at a placeholder BOTTOMLEFT", ), ("ChatFrame4", "as ChatFrame3"), ("ChatFrame5", "as ChatFrame3"), ("ChatFrame6", "as ChatFrame3"), ("ChatFrame7", "as ChatFrame3"), ( "BlackoutWorld", "the world map's full-screen cover (WorldMapFrame.xml) — TOPLEFT+BOTTOMRIGHT is how it \ fills the screen, not a seat in the contested band, and clearance is the opposite of \ what it wants: a blackout lifted over the action bars would leave a strip of world \ showing under the map. It joins this population at all only because decision 1757 took \ its parent=UIParent away, which is what lets it survive the UIParent:Hide() that \ showing the map performs", ), ]; /// **A top-level frame anchored to the screen's bottom edge is either managed, or exempt with a /// stated reason.** There is no third option, and this is the guard that covers features nobody /// has written yet. /// /// The band at the bottom of the screen is contested by construction: the main bar owns it, and /// every optional bar stacks upward into it. So the moment a new window anchors `BOTTOM`, /// `BOTTOMLEFT` or `BOTTOMRIGHT` to the screen root, it has taken on a clearance problem — and the /// only correct answers are to join `UIPARENT_MANAGED_FRAME_POSITIONS`, to seat itself from a /// managed global and register a listener (the bag stack's answer), or to say out loud why it does /// not need to. Failing here is not a bug report; it is a prompt to pick one. #[test] fn every_bottom_anchored_top_level_frame_is_accounted_for() { // The pass is the stock `UIParent.lua`'s since 1988 — read off the player's chain. let _data = benilla_formats::wow_data_or_skip!(); let pass = String::from_utf8_lossy( &super::reference_ui::read(r"Interface\FrameXML\UIParent.lua") .expect("the stock UIParent.lua off the chain"), ) .into_owned(); for f in MANAGED_FRAMES { assert!( pass.contains(f), "{f} is listed here as managed but does not appear in UIParent.lua's pass — this \ list has drifted from the file it mirrors" ); } let mut unaccounted = Vec::new(); for (file, text) in shipped_xml() { let doc = benilla_ui::framexml::parse(&text).unwrap_or_else(|e| panic!("{file}: {e}")); for (name, anchors) in bottom_anchored_top_level(&doc) { let known = MANAGED_FRAMES.contains(&name.as_str()) || BOTTOM_EXEMPT.iter().any(|(n, _)| *n == name); if !known { unaccounted.push(format!("{file}: {name} ({anchors})")); } } } assert!( unaccounted.is_empty(), "these top-level frames anchor to the screen's bottom edge but nothing decides their \ clearance over the action bars. Give each one a row in \ UIPARENT_MANAGED_FRAME_POSITIONS (the stock UIParent.lua's table, since 1988), or add \ it to BOTTOM_EXEMPT here with the reason it needs no row (decision 1499):\n{}", unaccounted.join("\n") ); } /// Every top-level INSTANCE (a ``/`