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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user