the window tab goes stock, and the ferry keeps its rider at the seam

- the window tab is the reference's own: `CharacterFrameTemplates.xml` loads off the chain at the reference's toc l.21 — the reference never gives it a toc line, `UIOptionsFrame.xml` includes it, and ours is not the reference's options window, so the row is explicit — and our `CharacterFrameTabButtonTemplate` with its `BenillaTab_Room`/`BenillaTabButton_OnUpdate` settle is deleted, 192 lines; the stock template fits a tab ONCE, in `<OnShow>`, through `PanelTemplates_TabResize(0)`, which needs a `GetStringWidth` that answers inside the Lua call that asked — the engine's synchronous measurer is that, so the settle that re-fit from `OnUpdate` until a frame-late round-trip landed is dead weight; retired with it, the director's call: 1002's structural clamp (`benillaTabRightInset`/`benillaTabPadding`/`benillaTabMaxWidth`, a tab never past its window's drawn right edge) — the reference's only caps are the numbers each window passes `TabResize`, and nothing had set those fields since the macro window went stock; 1004's highlight law is unchanged and now the reference's own text (`LEFT +10`/`RIGHT −10`, two opposing anchors that never consult the width, so both `SetWidth` lines are dead code there and here); measured live on the frame the window opens, main's 115·115·115·115 becomes 89.8·99.3·70.8·75.6 on the character row, 77.9·66.1·70.8·66.1 on the social, 89.8·85.0 on the merchant — each its label's `GetStringWidth()` + 40 — and one frame later the two builds agree exactly, so what goes is a one-frame 115 px flash on every open of every one of those windows (1993)
- the ferry seam: `tick_transports` leaves an off-map transport's transform unwritten — the reference's own freeze, `0x5f5248`'s leg map read against the current map and the return at `0x5f5279` past the pose stores — but 0455's riding worldport composes the rider's world pose as `boat.translation + boat.rotation * local_pos` from exactly that transform, so a client whose path clock had not crossed put the body at Booty Bay's coordinates on Kalimdor, which want ZERO tiles of Kalimdor's WDT: the reported `0/0 resident`, which cannot clear (`is_ready` needs `total > 0`) and cannot re-raise (`focus_resident` takes its vacuous off-grid arm); the fall is a second bug in the same window: `SelfMoveMessage` dropped the wire's `ON_TRANSPORT` tail and `apply_self_move` re-derived the deck-local pose as `world − boat.translation`, which against a frozen source-continent boat is the distance between two continents — consistent while the boat stays frozen, a ~13,000 yd fling off the deck the instant it writes an honest pose, `update_attachment` keeping the ride because the body is airborne and no settle hold to catch it; the rider's deck-local pose now comes off the wire, and `first_cycle_on_map` + `reseek_ridden_transport_at_worldport` seek our clock to the head of the destination map's leg and write the pose in the worldport frame itself, a clock that already agreed left untouched; cleared with numbers: the timetable matches vmangos's to 18 ms over path 241's 350 818 ms cycle, the period is bit-identical to the DB row, the live skew is 32 and 144 ms on the Orgrimmar zeppelin; two divergences kept knowingly — we spare the ridden transport where `SMSG_NEW_WORLD` (`0x401bc0`) destroys the whole object manager, and we hide an off-map boat the reference draws; an off-grid view focus names its map and tile once, a ridden transport whose map disagrees with `CurrentMap` for three frames warns, and the sea-crossing probe sends itself to a dock (`WOW_PROBE_DOCK=x,y,z`) because the fleet's seams are not interchangeable — one path crosses mid-cycle, another at the wrap; why the director saw 38.5 s and not a few frames is still open (N08, 2026)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-06 19:36:48 +07:00
co-authored by Claude Fable 5.1
parent 6cab3e9380
commit c94c45115f
39 changed files with 1026 additions and 361 deletions
@@ -400,206 +400,17 @@
</Frame>
<!-- ══ Carried here from UiPanels.xml when that file went stock (decision 1988) ══
Three things our UIParent.lua counterpart held that are not the reference's, seated in
the earliest file of ours every consumer loads after: the placeholder icon, the tab
settle, and the window tab template the settle drives. -->
The placeholder icon our UIParent.lua counterpart held and the reference's does not,
seated in the earliest file of ours every consumer loads after. The window tab template
and its settle stood here too until 1993 put them on the chain. -->
<Script><![CDATA[
-- The placeholder icon for an item/spell whose texture has not arrived yet (Craft, Quest,
-- QuestLog, Trainer and TradeSkill read it). Was ActionBar.xml's (stock since 1938), then
-- UiPanels.xml's (stock since 1988); this file is the earliest of ours every reader loads
-- after. A benilla name, so nothing stock reads it.
BENILLA_FALLBACK_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
-- The tab kit — `PanelTemplates_*` — is the REFERENCE's, off the player's chain
-- (`Interface\FrameXML\UIPanelTemplates.lua`, seated below this file in the manifest).
-- Moved here from our UIParent.lua counterpart with 1988, beside the template that calls it.
-- Our eleven transcribed copies stood here dead from the day that entry landed, and one
-- of them was carrying a benilla-only return the settle below read (1855/1860). What
-- stays is that settle, which is ours because our text measure is a frame late and the
-- reference's is not.
-- The tab template's per-frame settle: the ref does TabResize(0) once, in OnShow
-- (CharacterFrameTemplates.xml l.77-80), trusting a synchronous text measure. Our
-- GetStringWidth has a frame of latency, so the template's OnUpdate calls this until the
-- measured width settles (then it's a no-op compare per frame). The ref sizes the hover
-- highlight alongside it; this kit does not — both tab templates anchor their highlight to
-- their own side edges, so it is the tab by construction and no settle can desync it
-- (decision 1004).
--
-- The fit PARAMETERS live on the tab (`benillaTabPadding`/`benillaTabMaxWidth`, defaulting
-- to the +0 fit every window tab has always used) rather than at the call site, because
-- there is exactly one call site and it is this settle loop: an instance that ran its own
-- TabResize in OnLoad — as the reference's macro tabs do — would just be overwritten on the
-- next frame. Setting the two fields IS how an instance asks for the reference's own fit.
-- THE STRUCTURAL CLAMP — a tab may never grow past its window's drawn right edge, whatever
-- the instance asked for (decision 1002; the director's "fix this properly to prevent it
-- ever again"). The reference's own caps are numbers hand-tuned to ITS font: the macro
-- window's 150 is right for the enUS FRIZQT__ the 1.12 client rasterizes and for nothing
-- else in particular, and the label it guards carries a CHARACTER NAME — the one string
-- whose width no window author can know. So the instance's cap is a preference and the
-- window's edge is the law.
--
-- `benillaTabRightInset` is how a window declares how far its DRAWN plate stops short of
-- its frame rect, because no kit can introspect an art margin. The macro window's is 40:
-- its 384-unit frame is painted by a 256-wide slice plus a 128-wide one that carries a
-- transparent tail. Clamping to the frame rect instead — the first attempt — clamped to a
-- boundary the player cannot see, and the tab still walked out over the world.
--
-- The clamp is applied to the RESULT, through TabResize's own `absoluteSize` arm, rather
-- than folded into `maxWidth`: that parameter is compared against `text + padding` but
-- assigned as `maxWidth + padding`, so a room budget expressed through it is off by the
-- padding and silently fails to bind. `absoluteSize` sets the tab width outright, which is
-- exactly what a room budget is.
function BenillaTab_Room(tab)
local parent = tab:GetParent()
if not parent then return nil end
local left, right = tab:GetLeft(), parent:GetRight()
if not left or not right then return nil end
local room = (right - (tab.benillaTabRightInset or 0)) - left
if room < 1 then room = 1 end
return room
end
function BenillaTabButton_OnUpdate(tab)
local name = tab:GetName()
local text = getglobal(name .. "Text")
local w = (text and text:GetStringWidth()) or 0
if w <= 0 then return end
local pad = tab.benillaTabPadding or 0
local room = BenillaTab_Room(tab)
if tab.benillaFitWidth == w and tab.benillaFitRoom == room then return end
tab.benillaFitWidth, tab.benillaFitRoom = w, room
PanelTemplates_TabResize(pad, tab, nil, tab.benillaTabMaxWidth)
-- THE CLAMP READS THE FIT BACK OFF THE ART, not off a return value and not off the
-- button. `PanelTemplates_TabResize` is the REFERENCE's now (1837 put
-- `UIPanelTemplates.lua` on the chain below this file, so its definition wins) and it
-- returns nothing; our own copy's benilla-only `return tabWidth` had already stopped
-- running, which is what took 1002's clamp off the air silently. `tab:GetWidth()`
-- cannot serve either — it reads the last RESOLVED rect, this frame's stale input
-- inside a settle, which is the whole reason 1002 added that return.
--
-- `$parentMiddle` can. `PanelTemplates_TabResize` sets it on its way out, a Texture's
-- width is its AUTHORED size, and a region's GetWidth reports the authored value
-- without consulting a resolved rect (region::virtual_span) — so it is the number the
-- verb just computed, same tick. The tab is that plus its two end caps, which is the
-- reference's own `tabWidth = width + sideWidths`.
if room then
local middle = getglobal(name .. "Middle")
local left = getglobal(name .. "Left")
local fitted = (middle and middle:GetWidth() or 0)
+ 2 * (left and left:GetWidth() or 0)
if fitted > room then
PanelTemplates_TabResize(pad, tab, room)
end
end
-- No highlight sizing here on purpose — BOTH tab templates anchor their highlight to
-- their own two side edges, so it IS the tab by construction (decision 1004).
end
]]></Script>
<!--
The window tab button (ref CharacterFrameTabButtonTemplate — Interface\FrameXML\
CharacterFrameTemplates.xml in patch.MPQ, NOT the base FrameXML.toc set; extracted via
mpqx, transcribed VERBATIM: 115x32, the Active/InActive three-slice pair with the ref's
exact TexCoords, text CENTER +2, the ADD highlight inset 10px). "Disabled" IS "selected"
here — PanelTemplates_SelectTab (script kit above) disables the button and shows the
*Disabled (= Active) slices. The ref's OnShow text-fit (PanelTemplates_TabResize(0)) runs
from OnUpdate here instead — BenillaTabButton_OnUpdate settles once the async text measure
lands, then no-ops (the ref's C measure is synchronous; the 115px XML width is just the
pre-fit state). Its OnShow *also* sets the highlight width, which is dead code in the
reference and dropped here: this highlight is anchored LEFT/RIGHT, and two opposing anchors
resolve the size from the span without consulting the width at all (wow-re layout.md,
VERIFIED). The small tab now says the same thing the same way (1004).
**The OnLoad raise and the OnClick are the reference's own, and both were dropped here
until decision 1810.** The note that stood in their place said the raise was unnecessary —
"the tabs sit below the window art's hit-rect so nothing overlaps them" — and that "OnClick
is the instance's to define". Both were true of our own CharacterFrame.xml, and both are
false of the reference's, which is the file that runs now:
- the reference's `PaperDollFrame` is `setAllPoints enableMouse="true"` with a
`bottom=45` hit inset, and the reference's tabs are anchored INSIDE the window (CENTER
to BOTTOMLEFT +60,+62), so the page's hit rect covers the whole row. Frame levels come
out CharacterFrame 1 / PaperDollFrame 2 / tabs 2, later-created winning — the page eats
every tab click. `SetFrameLevel(+4)` is what lifts the row out, and is why the
reference has it.
- stock `CharacterFrame.xml` gives its five tabs only `<OnEnter>`/`<OnLeave>`; the
dispatch lives HERE. Without it `CharacterFrameTab4:GetScript("OnClick")` is nil and
the character window's tabs do nothing at all.
The body hardcodes `CharacterFrame`, which reads odd on a shared template until you check
the other consumers: the reference's `MerchantFrameTab1/2` and every one of ours
(Auction/Friends/Inspect/Mail/Talent) declares its own `<OnClick>`, and an instance's
handler REPLACES the inherited one rather than adding to it. So this reaches exactly the
window it names, which is the reference's own arrangement.
Still ours (moved here from UiPanels.xml with 1988 — that file is the reference's
UIParent.lua now), and still why this template is not simply sourced off the chain:
`Interface\FrameXML\CharacterFrameTemplates.xml` fits its text in `<OnShow>` through
`PanelTemplates_TabResize(0)`, which needs the reference's SYNCHRONOUS string measure. Ours
lands a frame late, so the fit runs from `BenillaTabButton_OnUpdate` instead and an
OnShow-only fit would leave every tab at its pre-fit 115px. That divergence is 1004's, it
spans six windows, and it is what this template's own migration waits on.
-->
<Button name="CharacterFrameTabButtonTemplate" virtual="true">
<Size><AbsDimension x="115" y="32"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeftDisabled" file="Interface\PaperDollInfoFrame\UI-Character-ActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="5"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.15625" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddleDisabled" file="Interface\PaperDollInfoFrame\UI-Character-ActiveTab">
<Size><AbsDimension x="88" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeftDisabled" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.15625" right="0.84375" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentRightDisabled" file="Interface\PaperDollInfoFrame\UI-Character-ActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddleDisabled" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.84375" right="1.0" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentLeft" file="Interface\PaperDollInfoFrame\UI-Character-InActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0" right="0.15625" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\PaperDollInfoFrame\UI-Character-InActiveTab">
<Size><AbsDimension x="88" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.15625" right="0.84375" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentRight" file="Interface\PaperDollInfoFrame\UI-Character-InActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.84375" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<ButtonText name="$parentText">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="2"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormalSmall"/>
<HighlightFont inherits="GameFontHighlightSmall"/>
<DisabledFont inherits="GameFontHighlightSmall"/>
<HighlightTexture name="$parentHighlightTexture" file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="10" y="2"/></Offset></Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="2"/></Offset></Anchor>
</Anchors>
</HighlightTexture>
<Scripts>
<!-- ref CharacterFrameTemplates.xml:69-81, verbatim. -->
<OnLoad>this:SetFrameLevel(this:GetFrameLevel() + 4)</OnLoad>
<OnClick>
PanelTemplates_Tab_OnClick(CharacterFrame)
CharacterFrameTab_OnClick()
</OnClick>
<OnUpdate>BenillaTabButton_OnUpdate(self)</OnUpdate>
</Scripts>
</Button>
<!-- `FauxScrollFrameTemplate` and the `FauxScrollFrame_*`/`ScrollFrame_*`/`ScrollingEdit_*`
API are the REFERENCE's, off the player's chain (`Interface\FrameXML\UIPanelTemplates`,
seated below this file — so its declarations win). Our transcriptions stood here dead
+22 -4
View File
@@ -137,8 +137,8 @@ Interface\FrameXML\WorldFrame.xml
# created hidden and never shown: the one function that would flash them, PlayerStatus_OnUpdate,
# has no caller in the image or the chain (wow-re, decision 1999). Dead code in 5875, kept as
# the reference keeps it.
# What stayed ours moved out: the window tab template and its settle + the placeholder icon
# (ScrollTemplates.xml), 0561's bag verbs (ContainerFrameAdapters.xml), the script log's row
# What stayed ours moved out: the placeholder icon (ScrollTemplates.xml; the window tab template
# and its settle went with it and are the chain's since 1993), 0561's bag verbs (ContainerFrameAdapters.xml), the script log's row
# (ScriptLogFrame.xml), and three aliases in OptionsFrame.xml that answer the reference's three
# options windows and its Cancel button.
Interface\FrameXML\UIParent.xml
@@ -374,6 +374,23 @@ ScrollTemplates.xml
Interface\FrameXML\UIPanelTemplates.lua
Interface\FrameXML\UIPanelTemplates.xml
# ══ THE WINDOW TAB — the reference's own, off the player's chain (1751; decision 1993) ══
# `CharacterFrameTabButtonTemplate`, the big tab along a panel's bottom edge — the character,
# vendor and social windows' rows, and nothing else in the client (the small `TabButtonTemplate`
# is UIPanelTemplates.xml's, above).
#
# THE REFERENCE NEVER GIVES THIS FILE A TOC LINE. Neither `FrameXML.toc` lists it (base or
# patch); `UIOptionsFrame.xml` pulls it in with `<Include file="CharacterFrameTemplates.xml"/>`
# above its own `<Script file=>` — "Need the character tabs here" — and that file is the
# reference's toc l.21. Our options window is ours, so the row is explicit; l.21 is exactly here,
# right after the UIPanelTemplates pair (l.16/17) and before StaticPopup (l.23).
#
# Its `<OnShow>` fits each tab to its text through `PanelTemplates_TabResize(0)` — which needs
# the line above it and a SYNCHRONOUS `GetStringWidth`, the reason our own transcription carried
# an OnUpdate settle instead until the engine gained one (1993 retires it, and 1002's clamp with
# it: a tab's cap is the reference's own number now, so a long label overflows the plate exactly
# as it does in the real client).
Interface\FrameXML\CharacterFrameTemplates.xml
# ══ THE DIALOG ENGINE — the reference's own, off the player's chain (1751; decision 1960) ══
# StaticPopup.xml + StaticPopup.lua: StaticPopupDialogs (all 76 of the reference's entries),
# StaticPopup_Show/Hide/Visible/Resize/OnUpdate, the four dialog instances, the edit boxes and the
@@ -684,8 +701,9 @@ Interface\FrameXML\ContainerFrame.xml
#
# What the block needs from above, all far up: Fonts.xml (every `inherits=` font object),
# ItemButtonTemplate.xml (the slot template's base, and the `SetItemButton*` family),
# Cooldown.xml (`CooldownFrameTemplate` + `CooldownFrame_SetTimer`), GameTooltip.xml, UiPanels.xml
# (`CharacterFrameTabButtonTemplate`, `UIPanelWindows`), UIPanelTemplates.xml (`UIPanelCloseButton`,
# Cooldown.xml (`CooldownFrameTemplate` + `CooldownFrame_SetTimer`), GameTooltip.xml,
# CharacterFrameTemplates.xml (`CharacterFrameTabButtonTemplate`), UIParent.xml (`UIPanelWindows`),
# UIPanelTemplates.xml (`UIPanelCloseButton`,
# `UIPanelButtonTemplate`, and `PanelTemplates_SetNumTabs`, which `CharacterFrame_OnLoad` calls at
# LOAD), TextStatusBar.xml (`SetTextStatusBarTextPrefix`, called at load too, plus the
# `TextStatusBar` the pet page's XP bar inherits), UnitFrames.xml (`PlayerFrameHealthBar` /
@@ -32,6 +32,9 @@ impl Plugin for ProbeCrossingPlugin {
#[derive(Resource, Default)]
struct CrossingProbe {
phase: Phase,
/// The one-shot [`BOOTSTRAP_DOCK`] send — so a body that still cannot see a ferry after the
/// hop (wrong continent, a `.go` the server refused) doesn't re-teleport every frame.
bootstrapped: bool,
}
#[derive(Default, PartialEq)]
@@ -60,6 +63,36 @@ const DROP_HEIGHT: f32 = 10.0;
/// Seconds after the deck drop before conceding the boarding failed (settle 6 s + fall + attach,
/// with slack) and re-arming for the next docked window.
const BOARD_DEADLINE: f64 = 15.0;
/// Solid ground on the Booty Bay pier, beside the Ratchet ferry's berth (WoW coords) — where the
/// probe sends itself when no cross-continent transport is in range at all.
///
/// The probe used to *assume* it was standing at a dock: its `Wait` arm only reacts to a transport
/// the server has already put in visibility range, so a probe body parked anywhere else waited
/// forever, printing nothing. That made the instrument unrunnable from a cold login, which is the
/// only way an unattended session ever starts it.
const BOOTSTRAP_DOCK: [f32; 3] = [-14297.2, 531.0, 8.8];
/// `WOW_PROBE_DOCK=x,y,z` — go to *this* dock, unconditionally, before waiting for anything.
///
/// Without it the probe rides whichever cross-continent ferry the login happens to be standing
/// next to, which is not a choice at all: the 1.12 fleet's seams are not interchangeable (one
/// path crosses mid-cycle, another crosses at the cycle wrap), so "it worked" on the ferry that
/// answered says nothing about the one a report names. Overriding the destination is how a
/// specific seam gets measured.
fn dock_override() -> Option<[f32; 3]> {
let raw = std::env::var("WOW_PROBE_DOCK").ok()?;
let mut it = raw.split(',').map(|p| p.trim().parse::<f32>());
match (it.next(), it.next(), it.next(), it.next()) {
(Some(Ok(x)), Some(Ok(y)), Some(Ok(z)), None) => Some([x, y, z]),
_ => {
warn!("WOW_PROBE_DOCK={raw:?} is not `x,y,z` — ignored");
None
}
}
}
/// Seconds the probe watches for a cross-continent transport before sending itself to
/// [`BOOTSTRAP_DOCK`] — long enough for a login's object stream to deliver one if we are already
/// somewhere it sails from.
const BOOTSTRAP_AFTER: f64 = 20.0;
fn crossing_probe(
time: ProbeClock,
@@ -79,6 +112,33 @@ fn crossing_probe(
let now = time.elapsed_secs_f64();
match probe.phase {
Phase::Wait => {
// Nothing that crosses the sea is in range at all — we are not at a ferry dock. Send
// the body to one (once; `bootstrapped` latches) rather than waiting out the run.
let forced = dock_override();
let adrift = !transports
.iter()
.any(|(_, t, _)| t.touches_map(0) && t.touches_map(1));
if (forced.is_some() || adrift) && !probe.bootstrapped && now > BOOTSTRAP_AFTER {
probe.bootstrapped = true;
let [x, y, z] = forced.unwrap_or(BOOTSTRAP_DOCK);
info!(
"PROBE crossing: going to the dock ({x:.1}, {y:.1}, {z:.1}) — {}",
if forced.is_some() {
"WOW_PROBE_DOCK"
} else {
"no cross-continent transport in range"
}
);
let _ = net.0.send(ClientCommand::Chat {
kind: crate::net::ChatKind::Say,
target: None,
text: format!(".go xyz {x} {y} {z} "),
});
return;
}
if adrift {
return;
}
// A cross-continent transport (the 1.12 fleet crosses EK↔Kalimdor, maps 0↔1),
// currently docked on OUR map: drop onto its deck. `sample.pos` is WoW coords —
// exactly what `.go xyz` takes.
+11
View File
@@ -2486,6 +2486,17 @@ pub(crate) struct SelfMoveMessage {
pub(crate) pitch: f32,
pub(crate) fall_time: u32,
pub(crate) jump: Option<benilla_protocol::JumpInfo>,
/// The `ON_TRANSPORT` tail — **the server's own deck-local pose for us**, when it sent one.
///
/// This used to be dropped here, and the consumer re-derived the local pose as
/// `world − boat.translation` against whatever the client's transport tick had last written.
/// That is only equal to the server's answer while the two agree about where the boat *is* —
/// and the one moment they provably do not is a cross-map seam, where our path clock has not
/// crossed yet and [`crate::transport::tick_transports`] is holding the transform frozen at the
/// far continent's pose. Re-deriving there yields a local offset the size of the gap between
/// two continents, which the next honest boat pose then multiplies into a fling off the deck.
/// The authoritative answer was on the wire the whole time (decision 2026).
pub(crate) transport: Option<benilla_protocol::TransportPose>,
}
/// The server granted or revoked control of a unit (`SMSG_CLIENT_CONTROL_UPDATE`). Written by
@@ -339,6 +339,7 @@ pub(super) fn unit_move(
pitch: mv.pitch,
fall_time: mv.fall_time,
jump: mv.jump,
transport: mv.transport,
});
return;
}
+26 -4
View File
@@ -608,12 +608,34 @@ fn apply_self_move(
player.mover_pitch = m.pitch;
// Riding a deck: `ON_TRANSPORT` is outside the merge mask, so this packet did not deboard us —
// it moved us **within** the platform frame. Re-anchor the local pose from the boat's live
// transform, or next frame's carry recomposes the stale one and undoes the snap.
// it moved us **within** the platform frame. Re-anchor the local pose, then let the carry
// recompose from it; without the re-anchor the next frame's carry reinstates the stale one and
// undoes the snap.
//
// **Take the wire's own deck-local pose when the packet carried one** (decision 2026). The
// subtraction below is only the server's answer while client and server agree about where the
// boat is, and at a cross-map seam they provably do not: our path clock has not crossed yet, so
// [`crate::transport::tick_transports`] is holding the boat's transform frozen at the *source*
// continent's pose while this packet's `position` is a world position on the *destination*. The
// difference is then not a deck offset but the distance between two continents — stable while
// the boat stays frozen (the carry just reproduces the server's own position, so nothing looks
// wrong), and a ~13 000 yd fling off the deck the instant the boat writes an honest pose again.
// The server sent us the right number in the same packet; use it.
if let Some(ride) = player.ride.as_mut() {
if let Ok((boat, _, _)) = transports.get(ride.entity) {
ride.local_pos = boat.rotation.inverse() * (player.pos - boat.translation);
ride.boat_yaw = boat.rotation.to_euler(EulerRot::YXZ).0;
match m.transport.filter(|t| t.guid == ride.guid) {
Some(t) => {
ride.local_pos = wow_to_bevy([t.pos.x, t.pos.y, t.pos.z]);
// World facing = local + boat yaw (the GetAbsoluteFacing law), so the carried
// aim stays right even when the boat's own heading is the frozen one.
ride.boat_yaw = boat.rotation.to_euler(EulerRot::YXZ).0;
player.pos = boat.translation + boat.rotation * ride.local_pos;
}
None => {
ride.local_pos = boat.rotation.inverse() * (player.pos - boat.translation);
ride.boat_yaw = boat.rotation.to_euler(EulerRot::YXZ).0;
}
}
}
}
+339 -19
View File
@@ -91,7 +91,9 @@ impl Plugin for TransportPlugin {
// measured per-frame cost of that sweep in a streamed outdoor scene.
(
arm_transports,
reseek_ridden_transport_at_worldport,
tick_transports,
watch_ridden_transport_map,
republish_moved_collider_aabbs::<Collider>,
compose_riders,
ground_deck_riders,
@@ -202,6 +204,34 @@ impl Transport {
(progress % u64::from(self.period_ms().max(1))) as u32
}
/// Re-anchor this transport's clock so its cycle sits at the head of `map_id`'s leg, and
/// return the sample there. `None` if the drive has no leg on that map (a lift never does).
///
/// The anchor is stored as `progress_ms` + the instant it was taken, so seeking is simply
/// "restamp both": the clock runs on from the new cycle position exactly as it would have.
pub(crate) fn reseek_to_map(
&self,
anchor: &mut TransportAnchor,
map_id: u32,
) -> Option<TransportSample> {
let Drive::Taxi(t) = &self.drive else {
return None;
};
let cycle = self.cycle_ms(anchor);
// **Already there — leave the clock alone.** The healthy crossing is the common case, and
// seeking anyway would rewind the cycle to the head of the frame we are mid-way through,
// throwing away up to a leg of progress and jerking the deck backwards under the rider.
// Only a clock that actually disagrees with the server gets corrected.
let here = t.sample(cycle);
if here.map == map_id {
return Some(here);
}
let target = t.first_cycle_on_map(cycle, map_id)?;
anchor.progress_ms = target;
anchor.at = Instant::now();
Some(t.sample(target))
}
/// Whether any leg of this transport's cycle lies on `map_id` — the cross-map worldport's
/// spare predicate (decision 0455). **A lift always answers no** (1654): the spare exists so a
/// *ridden boat* survives the seam mid-cycle, and a lift never spans one — the server re-sends
@@ -463,27 +493,217 @@ fn tick_transports(
continue;
}
visibility.set_if_neq(Visibility::Inherited);
let translation = benilla_assets::coords::wow_to_bevy(sample.pos);
let heading = Quat::from_rotation_y(sample.heading);
if transform.translation != translation || transform.rotation != heading {
transform.translation = translation;
transform.rotation = heading;
}
// Mirror the pose into avian's own components: its Transform→Position sync runs in the
// physics schedule (before Update), so without this the mover's same-frame casts would
// see the deck one frame behind — ~0.5 yd astern at cruise. The broad-phase AABB still
// lags a step, which is harmless (a rider stands deep inside the boat's AABB).
if let Some(mut p) = position {
if p.0 != transform.translation {
p.0 = transform.translation;
}
}
if let Some(mut r) = rotation {
if r.0 != transform.rotation {
r.0 = transform.rotation;
}
write_deck_pose(&sample, &mut transform, position, rotation);
}
}
/// Write one sampled transport pose into the render transform **and** avian's own components.
///
/// The avian mirror is not optional: its `Transform`→`Position` sync runs in the physics schedule
/// (before `Update`), so without it the mover's same-frame casts see the deck one frame behind —
/// ~0.5 yd astern at cruise. Since 1663 the chain republishes the moved collider's AABB inline
/// too, so a pose written here is fully visible to this frame's queries however far it jumped.
///
/// Write-on-change on every component (1473): a docked leg samples the identical pose each frame,
/// and unconditional writes re-dirty the deck's whole subtree — transform propagation over every
/// submesh plus a `Visibility` re-propagation — for every docked elevator on the map, every frame.
///
/// **Takes `&mut Mut<Transform>`, not `&mut Transform`, and that is the whole point of the
/// signature.** Reaching through `Mut` with `DerefMut` sets the change flag unconditionally, so a
/// helper that took a plain `&mut Transform` would re-dirty the deck's subtree on every call —
/// exactly the per-frame propagation 1473 removed, reintroduced by the act of factoring the write
/// out of its loop. The existing `a_docked_lift_stops_dirtying_its_transform_and_visibility` test
/// catches it; this comment is here so the next factoring does not have to rediscover why.
fn write_deck_pose(
sample: &TransportSample,
transform: &mut Mut<Transform>,
position: Option<Mut<Position>>,
rotation: Option<Mut<Rotation>>,
) {
let translation = benilla_assets::coords::wow_to_bevy(sample.pos);
let heading = Quat::from_rotation_y(sample.heading);
// Read through `Deref` (no flag), write through `DerefMut` (flag) — only when it changed.
if transform.translation != translation || transform.rotation != heading {
transform.translation = translation;
transform.rotation = heading;
}
if let Some(mut p) = position {
if p.0 != transform.translation {
p.0 = transform.translation;
}
}
if let Some(mut r) = rotation {
if r.0 != transform.rotation {
r.0 = transform.rotation;
}
}
}
/// **Put the ridden transport on the destination map in the worldport frame itself** (decision
/// 2026), so the rider's world pose is not composed through the source continent's coordinates.
///
/// **This exists because of a divergence of ours, not a gap in the server.** The server does send
/// a fresh create for the boat we are standing on: `Map::SendInitSelf` builds it *first*
/// (`Map.cpp:1698-1702`), which is the only reason `SendInitTransports` then skips
/// `player->GetTransport()` (`Map.cpp:1719`) — a skip that reads like an omission and is not one.
/// That create re-anchors the clock exactly. But it lands a round trip *after* the worldport ack,
/// and decision 0455's spared transport is asked for a pose in the worldport frame itself, before
/// it. This fills that window; the server's create then supersedes it.
///
/// **The reference has no such window, because it keeps nothing.** `SMSG_NEW_WORLD` (`0x401bc0`)
/// destroys every object in the manager (`0x467700` → `0x467800`) and builds a fresh one already
/// set to the destination map (`0x464ff0`), then rebuilds both rider and transport from the
/// post-ack CREATE blocks; the worldport composes nothing at all, and streams around the raw wire
/// pose. Our spare — and therefore this — is the price of not doing that. Whether to adopt the
/// reference's purge-and-rebuild wholesale is the recorded follow-on (2026).
///
/// **Why this system writes the pose itself instead of leaving it to [`tick_transports`].**
/// `CurrentMap` is still the *source* map this frame — `player::wire_in` inserts the new one, and
/// a command insert lands at the next flush — so `tick_transports` would compare the freshly
/// sought sample against the old map, take its off-map arm, and hide the boat with its transform
/// unwritten. This system knows the destination map explicitly from the message, so it needs no
/// resource at all. From the next frame `tick_transports` agrees and takes over.
///
/// **Order matters and is enforced by the chain:** this runs before `tick_transports` (which must
/// not undo it) and both run `.before(WorldStage::Input)`, so `wire_in` composes the rider's world
/// pose through a transform that is already the destination continent's. Without that, the rider
/// is composed through the source continent's coordinates — off the destination's WDT grid, where
/// nothing streams, the loading cover cannot clear and there is no ground under the body.
#[allow(clippy::type_complexity)] // one Bevy system's full input set — as `tick_transports`
fn reseek_ridden_transport_at_worldport(
mut worldports: MessageReader<crate::net::WorldportMessage>,
player: Res<crate::player::Player>,
mut transports: Query<(
&Guid,
&Transport,
&mut TransportAnchor,
&mut Transform,
&mut Visibility,
Option<&mut Position>,
Option<&mut Rotation>,
)>,
) {
for w in worldports.read() {
// Only a transfer the server carried us through on a transport, and only while we are in
// fact still on one — the same predicate `wire_in`'s riding branch uses.
if w.transport_entry.is_none() {
continue;
}
let Some(deck) = player.ride_entity() else {
continue;
};
let Ok((guid, transport, mut anchor, mut transform, mut visibility, position, rotation)) =
transports.get_mut(deck)
else {
continue;
};
let before = transport.cycle_ms(&anchor);
let Some(sample) = transport.reseek_to_map(&mut anchor, w.map_id) else {
warn!(
"transport {:#x}: rode a transfer to map {} its own path never visits — cannot \
re-anchor; the rider's pose will be composed through a stale one",
guid.0, w.map_id
);
continue;
};
let after = anchor.progress_ms;
// Silent when we were already there (the healthy crossing — the two clocks agreed and the
// seek was a no-op). Loud when it moved, carrying **how far behind the server our own
// path clock was**: this is the only measurement of client-vs-server transport drift the
// client can take, and it is worth a line every time it is not zero.
//
// Measured **forward around the cycle**, because a crossing is very often the cycle wrap
// (on taxi path 241 the Kalimdor legs are the *first* frames, so "cross to Kalimdor" IS
// the wrap): a plain `after - before` reports a 144 ms nudge as −356 140 ms and reads as
// the clock running half a cycle ahead, which is the opposite of what happened.
if before != after {
let period = transport.period_ms().max(1);
let behind =
(u64::from(after) + u64::from(period) - u64::from(before)) % u64::from(period);
info!(
"transport {:#x}: re-anchored across the seam to map {} — our path clock was \
{behind} ms behind the server's (cycle {before} → {after} ms of {period} ms). \
Deck now at [{:.1}, {:.1}, {:.1}].",
guid.0, w.map_id, sample.pos[0], sample.pos[1], sample.pos[2],
);
}
visibility.set_if_neq(Visibility::Inherited);
write_deck_pose(&sample, &mut transform, position, rotation);
}
}
/// Frames the ridden transport's map may disagree with [`CurrentMap`] before it is a fault rather
/// than the one-frame seam artefact — see [`watch_ridden_transport_map`]. Three, matching the
/// loading screen's own `CLEAR_AFTER_READY_FRAMES` debounce: long enough to outlast any single
/// deferred resource write, short enough that a real stall is named while it is still on screen.
const DISAGREEMENT_GRACE_FRAMES: u32 = 3;
/// **Tripwire: the boat we are STANDING ON says it is on another map.**
///
/// [`tick_transports`] leaves an off-map transport's `Transform` unwritten (and hides it) — for a
/// boat sailing the other continent's leg that is right, and for a boat we are *riding* it is the
/// one state that must never happen. Its pose is what [`crate::player::wire_in`] composes our own
/// world position from at a cross-map worldport (decision 0455), so a frozen source-map pose puts
/// the body at *that continent's* coordinates on *this* map: off the destination's WDT grid, where
/// nothing streams, the loading cover cannot clear (`total == 0` never reads ready) and there is no
/// ground to stand on. That is the 2026-09-05 report — a minute of loading screen on the
/// Booty Bay ferry, then a fall through the world.
///
/// It arises from our own path clock disagreeing with the server's about *when* the crossing
/// happens — measured live at 32 ms and 144 ms on the Orgrimmar zeppelin, in the two directions.
/// One line per sustained entry into the state, with the numbers needed to size it.
///
/// **A single frame of it is normal and must not warn.** `CurrentMap` is written by
/// `player::wire_in` with a command insert, so it lands one frame after the worldport that
/// [`reseek_ridden_transport_at_worldport`] handled — for that one frame the freshly sought
/// transport legitimately reads the *new* map while the resource still reads the old, which is
/// the inverse of the pathology and fired on every healthy crossing before this grace existed.
/// Hence a consecutive-frame count: only a state that *persists* is the one worth a line.
fn watch_ridden_transport_map(
current_map: Option<Res<CurrentMap>>,
player: Res<crate::player::Player>,
transports: Query<(&Guid, &Transport, &TransportAnchor)>,
mut reported: Local<Option<u64>>,
mut streak: Local<u32>,
) {
let Some(current_map) = current_map else {
return;
};
let Some((guid, transport, anchor)) = player
.ride_entity()
.and_then(|deck| transports.get(deck).ok())
else {
*reported = None;
*streak = 0;
return;
};
let cycle = transport.cycle_ms(anchor);
let sample = transport.sample(cycle, current_map.0);
if sample.map == current_map.0 {
*reported = None;
*streak = 0;
return;
}
*streak += 1;
if *streak < DISAGREEMENT_GRACE_FRAMES {
return;
}
if reported.replace(guid.0) == Some(guid.0) {
return;
}
warn!(
"transport {:#x}: RIDDEN but its own timetable says map {} while we are on map {} — \
cycle {cycle} ms of {} ms, sampled pose [{:.1}, {:.1}, {:.1}]. Its transform is frozen \
at that off-map pose, so anything composed through it (our own body at a worldport) \
lands in the wrong continent's coordinates.",
guid.0,
sample.map,
current_map.0,
transport.period_ms(),
sample.pos[0],
sample.pos[1],
sample.pos[2],
);
}
/// An observed mover's pose in a transport's local frame — the `MOVEFLAG_ONTRANSPORT` tail of
@@ -668,6 +888,106 @@ mod tests {
use super::*;
use benilla_formats::ElevatorKeyframe;
/// The Ratchet–Booty Bay ferry's real timetable (taxi path 241) — the seam the 2026-09-05
/// report rode. Real data on purpose: a hand-built path is not a substitute here, because the
/// builder's map-change skip dance plus the client period pin can silently truncate a short
/// synthetic cycle so the far continent's legs are never reachable, and a test written against
/// that proves nothing about the code under test.
fn ferry_241(chain: &mut benilla_formats::Chain) -> Transport {
let cat = benilla_formats::load_taxi_path_nodes(chain).expect("TaxiPathNode.dbc");
let nodes = cat.path(241).expect("taxi path 241 (Ratchet–Booty Bay)");
let tt = TransportTimetable::build(nodes, 30.0, 1.0).expect("path 241 builds");
Transport {
drive: Drive::Taxi(Arc::new(tt)),
was_moving: false,
}
}
/// **The seam re-anchor moves the clock onto the destination map — and only when it has to**
/// (decision 2026). Composing the rider through a transport still sampling the *source*
/// continent is what put a body at Booty Bay's coordinates on Kalimdor, off the destination's
/// tile grid, where nothing streams and the loading cover cannot clear.
///
/// Path 241 is the case that matters and the reason this is not a synthetic test: its Kalimdor
/// legs are the *first* frames of the cycle, so "cross to Kalimdor" is the **cycle wrap**, not
/// an interior map change. A seek that only handled the interior case would leave exactly the
/// reported ferry broken.
#[test]
fn the_seam_reanchor_lands_on_the_new_map_and_leaves_an_agreeing_clock_alone() {
let data = benilla_formats::wow_data_or_skip!();
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
let transport = ferry_241(&mut chain);
let period = transport.period_ms();
// Ask the built timetable where each continent's legs actually fall rather than assuming.
let on = |map: u32| {
(0..period)
.step_by(500)
.find(|&ms| transport.sample(ms, 0).map == map)
.unwrap_or_else(|| panic!("path 241 has an instant on map {map}"))
};
let (on_azeroth, on_kalimdor) = (on(0), on(1));
// Standing on Azeroth's leg, told we are now on Kalimdor: the clock moves, and it moves
// onto Kalimdor — the stored anchor, not merely the returned sample.
let mut anchor = TransportAnchor {
progress_ms: on_azeroth,
at: Instant::now(),
};
let sample = transport
.reseek_to_map(&mut anchor, 1)
.expect("path 241 visits Kalimdor");
assert_eq!(sample.map, 1, "the re-anchored sample must be on Kalimdor");
assert_eq!(
transport.sample(anchor.progress_ms, 0).map,
1,
"the stored anchor must itself sit on Kalimdor, not just the returned sample"
);
// Already there and told so: the clock is left exactly as it was. Seeking anyway would
// rewind to the head of the frame we are mid-way through and jerk the deck backwards
// under the rider — the healthy crossing is the common case and must cost nothing.
let mut anchor = TransportAnchor {
progress_ms: on_kalimdor,
at: Instant::now(),
};
let sample = transport
.reseek_to_map(&mut anchor, 1)
.expect("path 241 visits Kalimdor");
assert_eq!(sample.map, 1);
assert_eq!(
anchor.progress_ms, on_kalimdor,
"an agreeing clock must not be touched"
);
// A map the path never visits has no answer — the caller warns rather than inventing one.
let mut anchor = TransportAnchor {
progress_ms: on_azeroth,
at: Instant::now(),
};
assert!(transport.reseek_to_map(&mut anchor, 4242).is_none());
assert_eq!(
anchor.progress_ms, on_azeroth,
"a failed seek must not move the clock"
);
}
/// A **lift** is never re-anchored across a seam (1654): a type-11 exists only for players on
/// its own map, it is despawned and re-created like any other object, and it has no taxi path
/// to seek along at all.
#[test]
fn a_lift_has_no_seam_to_reanchor() {
let lift = Transport {
drive: Drive::Lift(parked_lift()),
was_moving: false,
};
let mut anchor = TransportAnchor {
progress_ms: 0,
at: Instant::now(),
};
assert!(lift.reseek_to_map(&mut anchor, 1).is_none());
}
/// A two-frame pause path at `base_pos` — the same lift both tests below drive.
fn parked_lift() -> Lift {
Lift {
@@ -840,7 +840,7 @@ fn shipped_bag_frame_drives_end_to_end() {
load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
load_ui(&s, r"Interface\FrameXML\UIParent.xml");
load_ui(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_ui(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
@@ -851,6 +851,7 @@ fn shipped_bag_frame_drives_end_to_end() {
}
}
load_ui(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
load_ui(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_ui(&s, "Interface\\FrameXML\\MerchantFrame.xml");
assert_eq!(
+36 -18
View File
@@ -133,7 +133,8 @@ fn b_opens_the_backpack_alone_and_closes_every_bag() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -215,7 +216,8 @@ fn shift_b_toggles_every_bag_at_once() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s.set_container(
@@ -297,7 +299,8 @@ fn bag_bar_buttons_light_while_their_bag_is_open() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s.set_container(
@@ -415,7 +418,8 @@ fn bag_tooltip_hangs_left_when_the_slot_sits_in_the_right_half() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -493,7 +497,8 @@ fn hovered_bag_tooltip_fills_itself_when_the_stats_land() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -598,7 +603,8 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -735,7 +741,8 @@ fn readable_letter_hover_shows_the_inspect_magnifier() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -806,7 +813,8 @@ fn drag_across_two_slots_queues_the_same_move_a_click_pickup_would() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -891,7 +899,8 @@ fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -965,7 +974,8 @@ fn equipped_bag_window_snug_fits_its_row_count() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -1025,7 +1035,8 @@ fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
for file in BAG_UI {
load_xml(s, file);
}
load_xml(s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(s, "Interface\\FrameXML\\StackSplitFrame.xml");
s.set_money(0);
@@ -1367,7 +1378,8 @@ fn bag_slot_cooldown_sweeps_through_the_xml() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s.tick(100.0); // a nonzero clock epoch, like the engine cooldown tests
@@ -1437,7 +1449,8 @@ fn the_bar_bag_buttons_name_themselves_on_hover() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.resolve();
@@ -1572,7 +1585,8 @@ fn keyring_surface(s: &UiScript) {
for file in BAG_UI {
load_xml(s, file);
}
load_xml(s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(s, "Interface\\FrameXML\\UIErrorsFrame.xml");
}
@@ -1847,7 +1861,8 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// `ForcedBackpackItem.m2` (`m2seq`/`m2batch`): one sequence, id 0, 1000 ms, clamp; the
// header box spans the card's whole travel band — 0.02707 × 0.07962 model units, which at
@@ -2023,7 +2038,8 @@ fn an_addon_that_hooks_toggle_backpack_receives_the_click() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
@@ -2080,7 +2096,8 @@ fn the_bag_slots_carry_the_references_names_and_icon_names() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
for i in 0..4 {
@@ -2141,7 +2158,8 @@ fn the_backpack_buttons_ring_follows_its_own_window_through_real_clicks() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
// The reference's `ToggleBag` opens nothing for a container of size 0, so the backpack has to
@@ -1181,6 +1181,102 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
assert!(s.errors().is_empty(), "no handler errors: {:?}", s.errors());
}
/// **The five tabs fit their labels on the first show, from the reference's own `<OnShow>`** —
/// the whole point of retiring benilla's own copy of `CharacterFrameTabButtonTemplate`
/// (decision 1993).
///
/// The reference fits a tab once, in the template's `<OnShow>`: `PanelTemplates_TabResize(0)`, so
/// `tab = tabText:GetWidth() + 2 * $parentLeft:GetWidth()`. That needs a `GetStringWidth` that
/// answers inside the Lua call that asked; ours landed a frame late when our template was written,
/// which is the only reason that template ever carried an `OnUpdate` settle. The engine has a
/// synchronous measurer now, so the falsifier is a row of tabs still wearing the template's
/// authored 115 after the window opens — and a second one, a width that keeps moving per frame.
#[test]
fn the_five_tabs_fit_their_labels_on_the_first_show() {
let _data = benilla_formats::wow_data_or_skip!();
/// `2 * CharacterFrameTab1Left:GetWidth()` — the template's two 20-unit end slices.
const SIDES: f64 = 40.0;
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The app installs `AtlasMeasurer`; a bare VM installs none, and an OnShow fit against a
// pending measure is a configuration the app does not have (the same correction 1848 made to
// the macro harness).
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
for f in super::test_ui::CHARACTER_UI {
super::test_ui::load_ui_strict(&s, f);
}
s.set_unit("player", Some(player_unit()));
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
s.resolve();
let widths = |s: &mut UiScript| -> Vec<(f64, f64)> {
(1..=5)
.map(|i| {
s.eval::<(f64, f64)>(&format!(
"return CharacterFrameTab{i}Text:GetStringWidth(), CharacterFrameTab{i}:GetWidth()"
))
.unwrap()
})
.collect()
};
// Tab 2 is the PET tab, and `PetTab_Update` keeps it hidden while the player has no pet
// (`CharacterFrame.lua`) — a hidden tab never gets an OnShow, so it is still at its authored
// width until a pet turns it on. That is the reference's own arrangement, and it is asserted
// below rather than skipped.
assert!(
!s.eval::<bool>("return CharacterFrameTab2:IsVisible()")
.unwrap(),
"the pet tab is off with no pet — the rest of this test rests on it"
);
let first = widths(&mut s);
let fitted = |label: f64| label + SIDES;
for i in [0, 2, 3, 4] {
let (label, width) = first[i];
assert!(label > 0.0, "tab {} measured its label", i + 1);
assert_eq!(
width,
fitted(label),
"tab {} is its text plus the two end slices, from OnShow alone",
i + 1
);
assert_ne!(
width,
115.0,
"tab {} is still at the template's authored pre-fit",
i + 1
);
}
assert_eq!(
first[1].1, 115.0,
"…and the hidden pet tab is not yet fitted"
);
// Turn it on the way a pet does, and it fits on ITS first show — same handler, no settle.
s.run("CharacterFrameTab2:Show()").unwrap();
s.resolve();
let (pet_label, pet_width) = widths(&mut s)[1];
assert_eq!(
pet_width,
fitted(pet_label),
"the pet tab fits when it is first shown"
);
// …and it does not move afterwards: the reference fits once, and nothing re-fits per frame.
for _ in 0..3 {
s.tick(0.016);
}
s.resolve();
let settled = widths(&mut s);
assert_eq!(
settled[0], first[0],
"the fit is once, in OnShow — nothing re-fits per frame"
);
assert_eq!(&settled[2..], &first[2..], "…for the whole row");
assert!(s.errors().is_empty(), "no handler errors: {:?}", s.errors());
}
/// **The tab kit's XML-facing entry point, driven the way an addon drives it.**
///
/// Four corpus addons (Enchantrix, Outfitter, SimpleActionSets, TheoryCraft) put
@@ -1211,10 +1307,11 @@ fn an_addons_tab_click_selects_through_the_generic_entry_point() {
// The reference's `PanelTemplates_SelectTab` ends with `if GameTooltip:IsOwned(tab)` —
// an arm our deleted copy omitted ("our tabs set no tooltip"), so selecting a tab now needs
// the tooltip to exist (1860).
load_xml(&s, "ScrollTemplates.xml"); // our window tab template (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\CharacterFrameTemplates.xml"); // the window tab (1993)
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
@@ -87,7 +87,8 @@ fn bag_setup() -> UiScript {
}
// `ContainerFrame_Update` reads `MerchantFrame:IsShown()` on any slot the tooltip owns, and
// `BankFrame` (BAG_UI's last entry) fills its purse through `BenillaMoney_Set`.
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s
@@ -38,7 +38,8 @@ const ROOM_UI: &[&str] = &[
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\ItemRef.xml",
"ScrollTemplates.xml", // our window tab template, before the window that inherits it (1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\StackSplitFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
@@ -122,7 +122,8 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
@@ -77,7 +77,8 @@ fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
load_xml(&s, file);
}
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_Set, BankFrame's purse helper
s.set_money(0);
s.set_container(0, Some(one_item_backpack()));
@@ -159,7 +160,8 @@ fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
@@ -263,7 +265,8 @@ fn escape_closes_an_open_stack_split_frame() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -352,7 +355,8 @@ fn escape_ladder_cast_then_windows_then_target_one_eater_per_press() {
// GameTooltip.xml (BAG_UI's, for the bag slots' tooltips) also carries TOOLTIP_DEFAULT_COLOR,
// which the dropdown backdrop's OnLoad reads — so the kit can load straight after it.
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s.set_container(0, Some(one_item_backpack()));
@@ -454,7 +458,8 @@ fn escape_ladder_targeting_rung_after_cast_before_windows() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s.set_container(0, Some(one_item_backpack()));
@@ -101,6 +101,45 @@ fn the_window_opens_on_friends_with_the_guild_tab_disabled() {
assert!(!s.eval::<bool>("return FriendsFrame:IsVisible()").unwrap());
}
/// **The social window's tabs fit their labels on the first show — through TWO inheritance hops.**
///
/// `FriendsFrameTab1..4` inherit `FriendsFrameTabTemplate`, which inherits
/// `CharacterFrameTabButtonTemplate` (the reference's own file, on the chain since 1993). The
/// middle template declares an `<OnClick>` and nothing else, and handler replacement is **per
/// handler name** (wow-re `template-onload-replacement-law.md`) — so the base template's
/// `<OnShow>` fit still runs, two hops down. That is the arrangement this pins: a row of tabs
/// still wearing the base template's authored 115 would mean the OnShow was lost on the way.
#[test]
fn the_social_tabs_fit_their_labels_on_the_first_show() {
let _data = benilla_formats::wow_data_or_skip!();
/// `2 * FriendsFrameTab1Left:GetWidth()` — the big tab's two 20-unit end slices.
const SIDES: f64 = 40.0;
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The app installs `AtlasMeasurer`; the reference's fit is inline, so a harness that models
// the async round trip models a configuration the app does not have (1848's correction).
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
super::test_ui::load_social_ui(&mut s);
s.run("ToggleFriendsFrame(1)").unwrap();
s.resolve();
for i in 1..=4 {
let (label, width): (f64, f64) = s
.eval(&format!(
"return FriendsFrameTab{i}Text:GetStringWidth(), FriendsFrameTab{i}:GetWidth()"
))
.unwrap();
assert!(label > 0.0, "tab {i} measured its label");
assert_eq!(
width,
label + SIDES,
"tab {i} is its text plus the two end slices, from the base template's OnShow"
);
assert_ne!(width, 115.0, "tab {i} is still at the authored pre-fit");
}
assert!(s.errors().is_empty(), "no handler errors: {:?}", s.errors());
}
/// A friend row shows name/zone/status on its top line and "Level N Class" underneath; an
/// OFFLINE friend takes the greyed offline template instead. This is the test that fails if
/// `GetFriendInfo`'s six returns ever come back in the wrong order.
@@ -210,6 +210,7 @@ fn escape_opens_the_menu_only_when_nothing_else_wants_the_press_and_then_closes_
&[],
&[
"ScrollTemplates.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
],
);
@@ -253,6 +254,7 @@ fn the_clicked_form_closes_everything_and_opens_the_menu_in_one_go() {
&[],
&[
"ScrollTemplates.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
],
);
@@ -282,7 +284,8 @@ fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() {
let mut s = bag_harness_with(
&[],
&[
"ScrollTemplates.xml", // our window tab template, before the window that inherits it (1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
// The loot window is the reference's own since 1751 — `test_ui::LOOT_UI` carries
// what it needs and why, and PartyFrame's `MAX_PARTY_MEMBERS` is needed at LOAD.
@@ -652,6 +655,7 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
"Interface\\FrameXML\\BonusActionBarFrame.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
r"Interface\FrameXML\WorldMapFrame.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
// The loot window is the reference's own since 1751 — see `test_ui::LOOT_UI` for what
// each of these buys; `PartyFrame`'s MAX_PARTY_MEMBERS is needed at LOAD time.
@@ -710,14 +714,17 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml", // the window tab template, ours (1004/1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
],
&["Interface\\FrameXML\\MerchantFrame.xml"],
&[
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
],
);
s.set_money(0);
s.set_container(0, Some(backpack()));
@@ -372,9 +372,11 @@ fn shown_inspect_honor_page() -> UiScript {
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml", // the window tab template, ours (1004/1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The inspect window's four tabs (`Blizzard_InspectUI`) inherit it (1993).
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
@@ -120,9 +120,11 @@ fn armed() -> UiScript {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
// The inspect window's four tabs (`Blizzard_InspectUI`) inherit it (1993).
load_xml(&s, r"Interface\FrameXML\CharacterFrameTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
@@ -163,9 +165,11 @@ fn shipped_inspect_frame_loads_clean() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
// The inspect window's four tabs (`Blizzard_InspectUI`) inherit it (1993).
load_xml(&s, r"Interface\FrameXML\CharacterFrameTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
@@ -478,7 +478,8 @@ fn shipped_loot_pushed_to_center_by_merchant() {
load_xml(&s, f);
}
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// Loot opens onto the empty left slot.
+18 -10
View File
@@ -554,8 +554,11 @@ fn the_tab_row_settles_and_stays_inside_the_window() {
settled.iter().all(|w| *w == last),
"the tab fit never settles — it changes every frame: {widths:?}"
);
// 344, not 384: this window's art stops 40 units short of its frame rect (MacroFrame.xml's
// `benillaTabRightInset`), and the frame rect is not what the player sees.
// 344, not 384: this window's art stops 40 units short of its frame rect, and the frame
// rect is not what the player sees. It holds off the reference's own numbers — the −15
// padding and the 150 cap the two tabs pass `TabResize` — with no clamp of ours in the way;
// 1002's was retired with our tab template (1993) and never reached these tabs anyway, which
// are `TabButtonTemplate`'s.
assert!(
last.2 <= 344.0,
"tab 2 ends at {} of a window whose plate stops at 344: {widths:?}",
@@ -564,15 +567,20 @@ fn the_tab_row_settles_and_stays_inside_the_window() {
no_errors(&s, "tab settle");
}
/// **The structural cap: no character name can push the tab row off the window** — the guarantee
/// the reference does not have (its 150 is a number hand-tuned to its own font) and the one the
/// director asked for by name. A tab clamps at its parent's right edge whatever it was asked for,
/// so this holds for a name no window author could have anticipated.
/// **No character name can push the tab row off the window** — checked against a 64-character
/// one, because `CHARACTER_SPECIFIC_MACROS` formats the player's name into tab 2's label and no
/// window author can know its width.
///
/// What holds it is the reference's own `150` cap, passed to `TabResize` by tab 2's own OnLoad.
/// 1002 added a structural clamp on top of that — a tab may never grow past its window's drawn
/// right edge — and 1993 retired it with our tab template; it never reached these tabs anyway
/// (they are `TabButtonTemplate`'s, and the clamp rode our `CharacterFrameTabButtonTemplate`).
/// So this is the reference's guarantee, asserted at the drawn edge.
#[test]
fn no_character_name_can_push_the_tab_row_off_the_window() {
let _data = benilla_formats::wow_data_or_skip!();
// The `capped = false` arm is gone with decision 1848: it stripped a `benillaTabMaxWidth`
// field of our own, and the reference has no such switch — its cap is the literal `150` passed
// The `capped = false` arm is gone with decision 1848: it stripped a benilla-only max-width
// field, and the reference has no such switch — its cap is the literal `150` passed
// to `PanelTemplates_TabResize` in the tab's OnLoad, which nothing can turn off. So the
// guarantee is now just the guarantee, checked against four names including a 64-character one.
for name in ["Ai", "Onehunter", "Bartholomewthethird", &"W".repeat(64)] {
@@ -610,8 +618,8 @@ fn no_character_name_can_push_the_tab_row_off_the_window() {
#[test]
fn the_tab_highlight_is_exactly_its_tab() {
let _data = benilla_formats::wow_data_or_skip!();
// A name under the reference's cap, one over it, and one long enough that the structural
// drawn-edge clamp (1002) is what sets the width — all three must hold the same property.
// A name under the reference's cap, one over it, and a 40-character one — all three must
// hold the same property.
for name in ["Ai", "Onehunter", &"W".repeat(40)] {
// The name is chosen at construction (the stock tab labels itself in its own OnLoad) and
// there is no measure round trip to pump — decision 1848. The frame loop stays: the
@@ -343,7 +343,7 @@ fn merchant_show_hide_plays_open_and_close_kits() {
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// Hidden at load: no open sound (never transitions on startup).
@@ -393,7 +393,8 @@ fn vendor_open_opens_the_backpack_and_layers_the_sound() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
equip_bag(&mut s, 0, "Backpack", 16);
@@ -444,7 +445,8 @@ fn vendor_leaves_an_already_open_backpack_alone() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
equip_bag(&mut s, 0, "Backpack", 16);
@@ -493,7 +495,8 @@ fn vendor_opens_and_closes_all_equipped_bags() {
for file in BAG_UI {
load_xml(&s, file);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
equip_bag(&mut s, 0, "Backpack", 16);
@@ -530,7 +533,7 @@ fn merchant_switch_plays_close_then_open_and_queues_the_consumable_close() {
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// Vendor A open.
@@ -584,7 +587,7 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
// every FrameXML file loads before any hover fires, so load it here too — with the dropdown
// kit its GroupLootDropDown initializes against at load (benilla.toc l.64 vs 383).
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_merchant(Some(MerchantState {
@@ -824,7 +827,7 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(500);
@@ -935,51 +938,49 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The tabs fit their labels — the ref's OnShow text-fit (PanelTemplates_TabResize(0): tab width
/// = text + the two 20px end slices, middle slices stretched to the text), run from the
/// template's OnUpdate once the async text measure lands. Fixed 115px tabs looked wrong against
/// the ref (director pass 2026-07-05): too wide, and the −16 overlap lost its nestle gap.
/// The two tabs fit their labels, from the reference's own `<OnShow>` — `PanelTemplates_TabResize(0)`
/// on `CharacterFrameTabButtonTemplate`: tab width = text + the two 20px end slices, middle slices
/// stretched to the text. Fixed 115px tabs looked wrong against the ref (director pass 2026-07-05):
/// too wide, and the −16 overlap lost its nestle gap.
///
/// **Driven through a SYNCHRONOUS measurer, with no round trip to pump** — the same correction 1848
/// made to the macro harness, arriving here with the template (decision 1993). This used to feed
/// two widths through `set_measured_text_unwrapped` and then tick, because our own template re-fit
/// from `OnUpdate` until the measure settled. The reference's file fits once, in `OnShow`; a client
/// whose measure is still pending at that moment could never size its tabs at all, and the app is
/// not such a client — it installs `AtlasMeasurer`. Modelling the async path here was modelling a
/// configuration the app does not have.
#[test]
fn merchant_tabs_fit_their_labels() {
/// `2 * $parentLeft:GetWidth()` — the template's two 20-unit end slices.
const SIDES: f64 = 40.0;
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s.set_merchant(Some(MerchantState::default()));
s.fire_event("MERCHANT_SHOW", vec![ScriptValue::Str("Vendor".into())]);
s.resolve();
// Answer the measure round-trip for the two labels (the app's font-atlas job in-game).
let measures: Vec<(u32, f32, f32, u64)> = s
.fontstrings_needing_measure()
.into_iter()
.filter(|r| r.text == "Merchant" || r.text == "Buyback")
.map(|r| {
let w = if r.text == "Merchant" { 58.0 } else { 52.0 };
(r.id, w, 10.0, r.key)
})
.collect();
assert!(measures.len() >= 2, "both tab labels request a measure");
s.set_measured_text_unwrapped(&measures);
s.tick(0.016); // the template OnUpdate sees the settled width and runs the fit
s.resolve();
// Tab width = text + 2×20 end slices; the middle slices carry exactly the text width.
let (w1, w2): (f64, f64) = s
.eval("return MerchantFrameTab1:GetWidth(), MerchantFrameTab2:GetWidth()")
.unwrap();
assert_eq!((w1, w2), (98.0, 92.0), "text + 40, not the fixed 115");
let (m1, m2): (f64, f64) = s
let (l1, l2, w1, w2, m1, m2): (f64, f64, f64, f64, f64, f64) = s
.eval(
"return MerchantFrameTab1MiddleDisabled:GetWidth(), \
MerchantFrameTab2Middle:GetWidth()",
"return MerchantFrameTab1Text:GetStringWidth(), MerchantFrameTab2Text:GetStringWidth(), \
MerchantFrameTab1:GetWidth(), MerchantFrameTab2:GetWidth(), \
MerchantFrameTab1MiddleDisabled:GetWidth(), MerchantFrameTab2Middle:GetWidth()",
)
.unwrap();
assert_eq!((m1, m2), (58.0, 52.0), "middle slices stretch to the text");
assert!(l1 > 0.0 && l2 > 0.0, "both labels measured synchronously");
assert_eq!(
(w1, w2),
(l1 + SIDES, l2 + SIDES),
"text + 40, not the fixed 115"
);
assert_eq!((m1, m2), (l1, l2), "middle slices stretch to the text");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -996,7 +997,7 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// A purse of 50c: row 1 (25c) is affordable, row 2 (100c) is not.
@@ -1090,7 +1091,7 @@ fn trade_recipient_money_renders_the_digit_not_ellipsis() {
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // the BenillaMoney_* helpers
// TradeFrame's money entry is the chain's own since 1882 — its OnLoad calls MoneyInputFrame_*.
load_xml(&s, "Interface\\FrameXML\\MoneyInputFrame.lua");
@@ -1151,7 +1152,7 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
}
for file in [
r"Interface\FrameXML\UIParent.xml", // UIParent + UIParent.lua, the reference's own (1988)
"ScrollTemplates.xml", // the window tab template, ours (1004/1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\DressUpFrame.xml",
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
@@ -789,6 +789,7 @@ fn interface_harness() -> UiScript {
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
@@ -51,6 +51,7 @@ fn harness() -> UiScript {
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
@@ -525,28 +526,42 @@ fn tab_resize_falls_back_to_this_when_no_tab_is_passed() {
assert!(width > 0.0, "the tab got a width, got {width}");
}
/// **The window tab's structural clamp, driven the way the client drives it — from `OnUpdate`.**
/// **The reference's own window tab fits its text in `<OnShow>`, on the first show, with no
/// settle** — the property that let benilla's own copy of this template retire (decision 1993).
///
/// 1002's law is that a tab may never grow past its window's drawn right edge, whatever fit the
/// instance asked for. `BenillaTabButton_OnUpdate` enforces it by comparing the fitted width
/// against the room, and it used to read that width from `PanelTemplates_TabResize`'s return —
/// a benilla-only line on OUR copy of that verb.
/// The reference fits a tab exactly once, from the template's `<OnShow>`
/// (`CharacterFrameTemplates.xml:77-80`): `PanelTemplates_TabResize(0)`, whose `width` is
/// `tabText:GetWidth() + padding`. That needs a `GetStringWidth` that answers during the Lua call
/// that asked. Ours landed a frame late when our template was written, so our copy re-fit from
/// `OnUpdate` until the measure settled; the engine has a synchronous measurer now
/// (`script::measure`), so the stock handler is the whole fit. The falsifier is a tab still wearing
/// the template's authored 115 after one show — which is exactly what an OnShow-only fit on the old
/// engine produced.
///
/// 1837 put `Interface\FrameXML\UIPanelTemplates.lua` on the chain BELOW `UiPanels.xml`, so the
/// reference's `PanelTemplates_TabResize` is the one that runs, and it returns nothing. The
/// comparison became `nil > number`: a raise, once per tab per width change, with the clamp
/// silently off the air and the tab already sized by the unclamped first call.
/// It also pins **1004's structural half, now on the reference's own file**: the highlight anchors
/// `LEFT +10` / `RIGHT −10`, so it spans the tab at every width and `TabResize`'s own
/// `highlightTexture:SetWidth(tabWidth)` — and the `<OnShow>`'s second line, `SetWidth(
/// GetTextWidth() + 30)` — are dead against two opposing anchors, here as in the reference.
///
/// The existing tab test drives `OnLoad` and never ticks, which is why nothing caught it.
/// **What went with our template is 1002's clamp** (superseded by 1993): a tab may now grow past
/// its window's drawn edge exactly as it does in the real client, whose only caps are the numbers
/// each window passes `TabResize` itself. That is asserted here by its absence — the long label
/// below overflows the 160-wide probe window and nothing stops it.
#[test]
fn a_tab_never_grows_past_its_windows_right_edge() {
fn the_stock_tab_fits_its_text_on_the_first_show() {
let _data = benilla_formats::wow_data_or_skip!();
/// `2 * $parentLeft:GetWidth()` — the template's two 20-unit end slices.
const SIDES: f64 = 40.0;
let mut s = harness();
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
// A deliberately narrow window and a long label: the natural fit does not fit.
// The highlight authors NO `<Size>`, so its height is content-derived off the art's texel
// extent (decisions 1349/1664) — `UI-Character-Tab-Highlight.blp` is 128x32. Without a probe
// the Y axis never resolves and the region has no rect at all, which is a property of an
// engine-less VM, not of the template.
s.set_texture_size_probe(Box::new(|_| Some((128, 32))));
let doc = benilla_ui::framexml::parse(
r#"<Ui>
<Frame name="ProbeWindow">
<Frame name="ProbeWindow" parent="UIParent" hidden="true">
<Size><AbsDimension x="160" y="200"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Frames>
@@ -564,21 +579,58 @@ fn a_tab_never_grows_past_its_windows_right_edge() {
let report = benilla_ui::loader::load_in(&s, &doc, "test", &|_: &str| None);
assert!(report.errors.is_empty(), "load: {:?}", report.errors);
// Two ticks: the first lands the measure, the second settles against it.
s.tick(0.1);
s.tick(0.1);
// The first show, and nothing else: no tick, no measure round trip to pump.
s.run("ProbeWindow:Show()").unwrap();
assert!(
s.errors().is_empty(),
"the settle must not raise — the clamp reads the fit back, not a return value: {:?}",
"the stock OnShow must not raise: {:?}",
s.errors()
);
s.resolve();
let (left, width) = s
.eval::<(f64, f64)>("return ProbeTab:GetLeft(), ProbeTab:GetWidth()")
let (label, width) = s
.eval::<(f64, f64)>("return ProbeTabText:GetStringWidth(), ProbeTab:GetWidth()")
.unwrap();
assert!(label > 0.0, "the label measured synchronously, got {label}");
assert_eq!(
width,
label + SIDES,
"the tab is its text plus the two end slices, from OnShow alone"
);
assert_ne!(width, 115.0, "…and not the template's authored pre-fit");
// No settle: a frame later it is the same number, because nothing re-fits it.
for _ in 0..3 {
s.tick(0.016);
}
s.resolve();
assert_eq!(
s.eval::<f64>("return ProbeTab:GetWidth()").unwrap(),
width,
"the fit is once, in OnShow — nothing may move it per frame"
);
// 1004, structurally: the highlight is the tab inset 10 on each side, at this width.
// Lit, so it joins the resolved tree — the way the dropdown kit's checked rows are lit.
s.run("ProbeTab:LockHighlight()").unwrap();
s.resolve();
let (tl, tr, hl, hr) = s
.eval::<(f64, f64, f64, f64)>(
"return ProbeTab:GetLeft(), ProbeTab:GetRight(), \
ProbeTabHighlightTexture:GetLeft(), ProbeTabHighlightTexture:GetRight()",
)
.unwrap();
assert_eq!(
(hl - tl, tr - hr),
(10.0, 10.0),
"the highlight spans the tab's own edges — the OnShow SetWidth is inert against them"
);
// 1002's clamp is gone with our template: the reference has no such guarantee, and the long
// label really does run past the window's right edge now.
let right = s.eval::<f64>("return ProbeWindow:GetRight()").unwrap();
assert!(
left + width <= right + 0.5,
"the tab ran past its window: left {left} + width {width} > right {right}"
tl + width > right,
"the probe label was meant to overflow: left {tl} + width {width} <= right {right}"
);
}
@@ -737,7 +737,7 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
@@ -1176,7 +1176,7 @@ fn the_1507_registry_rows_match_the_reference_bytes() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
+20 -10
View File
@@ -30,7 +30,8 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// The BenillaMoney_* purse helpers the quest reward/progress panels repaint through live in
// MerchantFrame.xml (the same documented cross-window dep the bag tests load).
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -98,7 +99,8 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -196,7 +198,8 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -298,7 +301,8 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -377,7 +381,8 @@ fn greeting_goodbye_button_closes_the_window() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -434,7 +439,8 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -562,7 +568,8 @@ fn write_on_still_fades_when_instant_text_is_off() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -639,7 +646,8 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -722,7 +730,8 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -822,7 +831,8 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -95,7 +95,8 @@ fn shipped_questlog_frame_loads_clean() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -127,7 +128,8 @@ fn shipped_questlog_frame_drives_end_to_end() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -335,7 +337,8 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -480,7 +483,8 @@ fn watch_guards_no_op_without_erroring() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // the guards' red-line surface
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
@@ -594,7 +598,8 @@ fn progress_auto_watches_for_five_minutes() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -683,6 +688,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
@@ -742,7 +748,8 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
@@ -820,7 +827,8 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -883,7 +891,8 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1070,7 +1079,8 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1143,7 +1153,8 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1206,7 +1217,8 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1286,7 +1298,8 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
// Fonts.xml alone is the source now. Formerly: it
// needs the dropdown kit its GroupLootDropDown initializes against (benilla.toc l.64 vs 383).
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1376,7 +1389,7 @@ fn popup_children_inherit_the_dialog_stratum() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
@@ -1440,7 +1453,8 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1536,7 +1550,8 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1643,7 +1658,8 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1728,7 +1744,8 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -1767,7 +1784,8 @@ fn share_quest_click_queues_the_selected_quests_id() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
@@ -132,7 +132,7 @@ fn shipped_spellbook_loads_clean() {
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
@@ -38,7 +38,7 @@ fn harness(rank: u32) -> UiScript {
for file in [
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml", // the window tab template, ours (1004/1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
+11 -2
View File
@@ -351,7 +351,10 @@ pub(super) const MERCHANT_UI: &[&str] = &[
r"Interface\FrameXML\UIParent.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"ScrollTemplates.xml", // our window tab template, before the window that inherits it (1988)
// The reference's own window tab (`CharacterFrameTabButtonTemplate`), whose `<OnShow>`
// fits each tab to its text — it needs the `UIPanelTemplates` pair above it (1993).
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml", // app load order: tooltip before merchant
@@ -506,6 +509,9 @@ pub(super) const CHARACTER_UI: &[&str] = &[
"ScrollTemplates.xml", // SkillFrame's faux list + trough
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The reference's own window tab (`CharacterFrameTabButtonTemplate`), whose `<OnShow>`
// fits each tab to its text — it needs the `UIPanelTemplates` pair above it (1993).
r"Interface\FrameXML\CharacterFrameTemplates.xml",
// The four options templates off the chain, then ours for the one it does not carry
// (`UIOptionsCheckButtonTemplate` — decision 1841).
"Interface\\FrameXML\\OptionsFrameTemplates.xml",
@@ -547,6 +553,9 @@ pub(super) const SOCIAL_UI: &[&str] = &[
"ScrollTemplates.xml",
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
// The reference's own window tab (`CharacterFrameTabButtonTemplate`), whose `<OnShow>`
// fits each tab to its text — it needs the `UIPanelTemplates` pair above it (1993).
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\OptionsFrameTemplates.xml",
"Interface\\FrameXML\\ReputationFrame.xml",
"Interface\\FrameXML\\StaticPopup.xml",
@@ -598,7 +607,7 @@ pub(super) const BAG_UI: &[&str] = &[
// `OpenAllBags` opens with `if not UIParent:IsVisible() then return end`. Without it the
// windows fall out of the cascade and the reference's own layout pass has nothing to measure.
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml", // the window tab template, ours (1004/1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
"Interface\\FrameXML\\ItemButtonTemplate.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
@@ -58,8 +58,9 @@ fn harness() -> UiScript {
/// the other window the compare flow crosses, and `ItemRef.xml`, which declares the chat-link
/// router's own `ItemRefTooltip`. Both were in this harness's hand-copied list before 1751; the
/// manifest's order is the one kept (`FrameXML.toc` 63 → 77).
const ROUTER_UI: [&str; 3] = [
"ScrollTemplates.xml", // our window tab template, before the window that inherits it (1988)
const ROUTER_UI: [&str; 4] = [
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\ItemRef.xml",
];
@@ -71,6 +71,7 @@ fn trainer_script_base() -> UiScript {
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\ClassTrainerFrameTemplates.xml",
] {
@@ -653,9 +653,10 @@ fn shipped_target_frame_runs_the_level_law() {
// GetDifficultyColor's own load chain (the quest log window, its ref home).
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\CharacterFrameTemplates.xml"); // the window tab (1993)
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
@@ -44,6 +44,7 @@ fn load_popup_frames(s: &UiScript) {
"Interface\\FrameXML\\TargetFrame.xml",
"Interface\\FrameXML\\PetFrame.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\FriendsFrame.xml",
// Declares ChatFrameEditBox, which the rename dialog's OnHide refocuses (1960).
"Interface\\FrameXML\\FloatingChatFrame.xml",
@@ -36,6 +36,9 @@ const FILES: &[&str] = &[
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// `AuctionTabTemplate` inherits `CharacterFrameTabButtonTemplate`, and `inherits=`
// resolves at LOAD (1993).
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
+2 -1
View File
@@ -22,7 +22,7 @@ const FILES: &[&str] = &[
r"Interface\FrameXML\MoneyInputFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml", // the window tab template, ours (1004/1988)
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
@@ -33,6 +33,7 @@ const FILES: &[&str] = &[
// The stock tabs inherit `FriendsFrameTabTemplate`, and `inherits=` resolves at load — so the
// social window and the kit it needs come first, as the reference's toc has them (1970).
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\FriendsFrame.xml",
"Interface\\FrameXML\\MailFrame.xml",
];
+3
View File
@@ -40,6 +40,9 @@ const FILES: &[&str] = &[
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
// `TalentTabTemplate` inherits `CharacterFrameTabButtonTemplate`, and `inherits=`
// resolves at LOAD (1993).
r"Interface\FrameXML\CharacterFrameTemplates.xml",
"ScrollTemplates.xml",
// Stock `TalentFrame_OnShow` opens with `SetButtonPulse(TalentMicroButton, 0, 1)` and then
// `UpdateMicroButtons()` — both live here, and a nil `TalentMicroButton` throws out of OnShow
@@ -506,6 +506,43 @@ impl TransportTimetable {
Some(tt)
}
/// **The cycle instant this path first stands on `map_id`, searching forward from
/// `from_cycle_ms` and wrapping** — the client-side answer to "the server says this transport
/// has just crossed onto that continent; where in my own timetable is that?"
///
/// A client computes a transport's position from its own path clock, anchored whenever the
/// object's CREATE block last arrived, so that clock is free to drift from the server's about
/// *when* a crossing happens (measured live at 32–144 ms on the 1.12 fleet). The server does
/// re-anchor it on arrival — `Map::SendInitSelf` sends the ridden transport's create first,
/// which is why `SendInitTransports` then skips it — but that lands a round trip after the
/// worldport ack, and a consumer that must place a rider *in the worldport frame* has nothing
/// to place them through until it does. This is that answer: the instant our own timetable
/// puts the transport on the map the server just named.
///
/// Returns the frame's **arrival** instant (the start of its window), so the sample lands at
/// the head of the destination leg rather than mid-travel. `None` if no keyframe lies on
/// `map_id` at all — the caller has misidentified the transport.
pub fn first_cycle_on_map(&self, from_cycle_ms: u32, map_id: u32) -> Option<u32> {
let n = self.frames.len();
if n == 0 {
return None;
}
// The frame `from_cycle_ms` currently sits in — the scan starts there so a clock already
// on the destination leg answers "right here" instead of a whole cycle away.
let start = self
.frames
.iter()
.position(|f| from_cycle_ms < f.next_arrive_time)
.unwrap_or(n - 1);
(0..n)
.map(|k| (start + k) % n)
.find(|&i| self.frames[i].map_id == map_id)
.map(|i| match i {
0 => 0,
_ => self.frames[i - 1].next_arrive_time,
})
}
/// Whether any of the cycle's keyframes lies on `map_id` — i.e. this transport exists on
/// that map for part of its loop. The cross-map worldport (decision 0455) keeps such a
/// transport alive through the map switch (its clock is one continuous domain over the
@@ -259,3 +259,67 @@ fn touches_map_matches_the_paths_map_set() {
"no golden path crosses continents — the 0455 spare predicate has nothing to spare"
);
}
/// [`TransportTimetable::first_cycle_on_map`] — the seam's re-anchor target (decision 2026).
///
/// Two properties, on every cross-continent path in the fleet, because the fleet's seams are not
/// alike: one path changes map mid-cycle, another only at the cycle wrap (path 241's map-1 legs are
/// frames 0..22 and its map-0 legs are the tail, so "cross to Kalimdor" *is* the wrap), and a
/// re-anchor that only handled the interior case would leave exactly the reported ferry broken.
///
/// 1. From anywhere in the cycle, the answer is an instant that really does sample on the asked-for
/// map — that is the whole contract, and it is what the rider's world pose is composed through.
/// 2. Asking from an instant already on that map returns an instant on it too (the caller's
/// already-there short-circuit rests on the map agreeing, not on the number).
#[test]
fn first_cycle_on_map_lands_on_that_map() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let cat = load_taxi_path_nodes(&mut chain).expect("load TaxiPathNode");
let mut crossed = 0usize;
for g in GOLDENS {
let nodes = cat
.path(g.path_id)
.unwrap_or_else(|| panic!("path {} exists in TaxiPathNode.dbc", g.path_id));
let tt = TransportTimetable::build(nodes, g.move_speed, g.accel_rate)
.unwrap_or_else(|| panic!("path {} builds", g.path_id));
let maps: std::collections::HashSet<u32> = nodes.iter().map(|n| n.map_id).collect();
if maps.len() < 2 {
continue;
}
crossed += 1;
// Probe from 64 instants spread across the whole cycle — every leg, both continents, and
// the frames either side of the wrap.
for k in 0..64u32 {
let from = (u64::from(tt.period_ms) * u64::from(k) / 64) as u32;
for &m in &maps {
let at = tt.first_cycle_on_map(from, m).unwrap_or_else(|| {
panic!(
"path {} touches map {m} but found no cycle on it",
g.path_id
)
});
assert!(
at < tt.period_ms,
"path {} map {m}: {at} ms is outside the {} ms cycle",
g.path_id,
tt.period_ms
);
assert_eq!(
tt.sample(at).map,
m,
"path {} from {from} ms: first_cycle_on_map({m}) = {at} ms samples map {}",
g.path_id,
tt.sample(at).map
);
}
}
// A map the path never visits has no answer at all.
assert_eq!(tt.first_cycle_on_map(0, 9999), None);
}
assert!(
crossed > 0,
"no golden path crosses continents — this test proves nothing"
);
}
@@ -115,6 +115,10 @@ pub struct TerrainStreamer {
/// nothing to do and skips the window walk (three window-sized allocations and a scan of
/// every resident tile, on every still frame — decision 1979's floor).
settled: bool,
/// The focus tile the off-grid tripwire below last reported, so a focus parked outside the
/// map's WDT grid says so **once** rather than every frame. `None` while the focus is on the
/// grid (so re-entering the state re-reports).
off_grid_reported: Option<(i32, i32)>,
}
/// The placement id the map-global WMO is registered under. A WMO-only map authors **no** ADT tiles
@@ -795,6 +799,36 @@ fn stream_terrain(
desired.retain(|&(tx, ty)| w.has_tile(tx as u32, ty as u32));
}
// **The focus is off this map's tile grid** — the window touches no tile the WDT authors, so
// there is nothing to stream and nothing to wait for. On an ADT map that is never a place a
// body can legitimately stand: it means the focus was written in some *other* map's
// coordinates (the 2026-09-05 report — a rider composed through a cross-map transport whose
// own pose was still the source continent's) or has left the world entirely.
//
// It earns a tripwire because of what it does DOWNSTREAM and silently: `desired` empty makes
// `total == 0`, which [`WorldLoadProgress::is_ready`] reads as *not ready* — so the loading
// screen can never clear — while the vacuous arm below reports `focus_resident = true`, so the
// backstop that would have re-raised a cover cannot fire either. The player is left staring at
// a loading screen whose wait line reads `0/0 resident` with every other term nominal. Naming
// the map and the tile turns that into one greppable line. Once per focus tile, never a frame
// loop; `global_wmo` maps legitimately author no tiles and are excluded.
if wdt_index.is_some() && !state.global_wmo && desired.is_empty() {
let focus_tile = window.focus_tile();
if state.off_grid_reported != Some(focus_tile) {
state.off_grid_reported = Some(focus_tile);
let map_name = state.map_dir.clone().unwrap_or_default();
warn!(
"terrain: view focus [{:.1}, {:.1}] is OFF map {map_name}'s tile grid (tile \
{focus_tile:?}) — the window wants no tile the WDT authors, so nothing streams \
and a loading cover cannot clear. A focus written in another map's coordinates \
is the usual cause.",
center[0], center[1],
);
}
} else if !desired.is_empty() {
state.off_grid_reported = None;
}
// Unload tiles no longer desired: despawn the terrain entity, release the placements, drop the
// handle. **Budgeted per frame** (B181): dropping the whole trailing row on the crossing frame
// freed ~1300 mesh assets — each tile's 256 chunk cells, CPU copies included — plus the tiles'