a hundred stale claims retire, and text= was never a divergence

- a comment that justifies code by an engine difference is a claim with an expiry date, and the tree had ~96 expired ones across `assets/ui`, `ui_script/` and `benilla-ui` — a hygiene pass, no behaviour change: dangling `Benilla*` names with zero definitions (`BenillaMoney_*` at thirteen sites after 1937/1962 converged the coin cluster onto the stock `SmallMoneyFrameTemplate`; `BenillaGossipRow_Resize`, `BenillaErrorsFrame_AddMessage`, the digit feed 1285 deleted, `BenillaMerchantItem_OnClick` and three more), "the shipped `assets/ui/<X>.xml`" for thirty-odd files the directory no longer holds — it holds nine — each site now naming the chain file that actually loads, `UiPanels.xml` in the present tense after 1988 took the panel manager and the dialog engine stock, and expired self-deprecation: SimpleHTML "a widget kind we do not have" (1337 built it), "benilla has no console" (1948 bound `ConsoleExec`), `Model`'s "seven verbs not published yet" and "`ClearFog` is a guess" (all 23 are published, the carve sits 500 lines below, `UNBUILT` has been `[&str; 0]` since 2027), `frame_flag_gate`'s doc describing four groups of accepted mouse divergences over a `KNOWN` list that is empty (the empty array and its enforcement stay), five comments saying key delivery is not gated on `keyboard_enabled` where `script::keyboard`'s walk filters on exactly that field (1319), `SetRaidTarget` called a verb this house lacks beside an assert that its intent IS queued (1820), two questlog tests deferring to a screenshot for `GetPoint`/`IsVisible` the regions already answer, `benilla.toc`'s "trade still needs four engine verbs" (all four bound, 1966) and its ping `Model`s "this engine draws no pixels for" (2013 renders `MinimapPing.mdx` itself), and `rig_anim`'s `set_paused` naming a booth caller it does not have; and the code the prose was propping up goes with it: `GameMenuFrame.xml`'s six `<OnLoad>` label setters, because `text=` on a `<Button>` is a global-string lookup at the loader's attribute step (0995, built 61 seconds before the file was written) — the buttons carry `text="LOGOUT"`/`"EXIT_GAME"`/`"MACROS"`/`"RETURN_TO_GAME"`/`"MAIN_MENU"` as the reference's own do; `ScriptLogFrame`'s five authored close-button lines, now `inherits="UIPanelCloseButton"` off the chain's `UIPanelTemplates.xml` seated 757 lines above it (1846); and the editbox's 22-line "gap" doc claiming only the self-acquire `autoFocus` path is built and its `false` default unverified — path 1 is 1686's `visibility_focus`, and the default is `true`, byte-read off `flags = 1` at `0x779a29`/`0x779a2e`, so the test named `autofocus_does_not_focus_on_show…` is renamed for what it actually pins (2044)
- the `text=` divergence never existed: `resolve_text`'s doc had claimed since 0068 that falling back to the raw attribute on a GlobalStrings miss was "one deliberate divergence" from a reference that draws the empty string, and read at the byte it is the reference's own behaviour — `FrameScript_GetText 0x703bf0` does return a pre-seeded empty string (`0x882748`) when the global is not a string, but that return never reaches a label, because all three `text=` readers image-wide test it and substitute the raw attribute: `Button::LoadXML` at `0x778c07` (`0x778c31 mov eax,esi`), `CSimpleFontString::LoadXML` at `0x771006` byte-identically, and `0x7292a6`, which pushes `edi` on the empty arm; so the loader changes nothing, the two test comments that had called `PetLevelText`'s "Level level race class" placeholder and `CLOSE` our divergence now name the reference's arm (a real 1.12 client draws them too), and what IS ours is the extra warning on a key-shaped miss (0983 → 0991), a strict addition; beside it, `GetEffectiveScale`'s root factor is 1 not because "benilla has no `uiScale` CVar" — 1804 registered one, default 0.9 — but because the dial is applied at the raster seam, `768/uiScale` UI units of virtual-screen height rather than a scale on `UIParent`, so every coordinate the VM hands Lua is already in those units and the reference's `GetCursorPosition()/GetEffectiveScale()` transcriptions convert correctly with a root of 1 (2044)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-06 19:39:30 +07:00
co-authored by Claude Fable 5.1
parent 28489a0b1f
commit 7bb52f7808
80 changed files with 425 additions and 409 deletions
@@ -45,12 +45,6 @@
on — hovering an ordinary bag slot — runs the real client's code, unedited.
── Named, not hidden: what still does not work through the sourced files ────────────────────
· `MerchantItemButton_OnClick`. The reference's `ContainerFrameItemButton_OnClick` closes its
shift-right-click-at-a-vendor arm with a `SplitStack` closure that calls that global, and
benilla's merchant window publishes `BenillaMerchantItem_OnClick` instead. Confirming a stack
split with a vendor open therefore raises. That is the same "a Benilla name shadows the
reference's" class this whole seam exists to close, one window over, and it is left loud
rather than guarded (1203): the fix is to publish the merchant global, not to hide the call.
· `REPAIR_COST` is read by the reference's repair-mode arm. It is a `GlobalStrings.lua` key and
this client sources that file too, so it resolves at runtime — and the arm is unreachable
today regardless, since `GameTooltip:SetBagItem`'s repairCost is an INTERIM 0.
+15 -22
View File
@@ -30,9 +30,9 @@
Options", SOUNDOPTIONS_MENU "Sound Options", UIOPTIONS_MENU "Interface Options",
MACROS "Macros", LOGOUT "Logout", EXIT_GAME "Exit Game",
RETURN_TO_GAME "Return to Game", MAIN_MENU "Main Menu". Declared as the real globals
(they are addon-visible API, decision 0068) and read back into each button's label in its
OnLoad — the ref's `text="LOGOUT"` attribute IS a global lookup, which this engine's
literal `text=` is not.
(they are addon-visible API, decision 0068) and carried on each button's own
`text=` attribute — which this engine resolves as a global-string lookup exactly as
the reference does (decision 0995, `loader/widgets.rs`'s `resolve_text`).
· **The sounds** — the ref's own OnClick lines: igMainMenuOption (the five option buttons),
igMainMenuLogout, igMainMenuQuit, igMainMenuContinue. All four kits verified present in
SoundEntries.dbc (patch-2.MPQ) alongside the igMainMenuOpen/Close pair already in use.
@@ -94,10 +94,9 @@
scripts, so it never actually moves).
`frameStrata="DIALOG"` is what puts it over the world UI, and that is kept.
**The "no frame-level raise" half of that reason is gone**: the raise law is built
(`script::object::toplevel`, wow-re `ui/scratch/toplevel-raise.md`) and the loader honours
`toplevel="true"`. Re-adding it here is a one-word change and would make the Esc menu come
to the front over any other DIALOG-strata frame it overlaps; left off pending the
director's call on the look, not on the mechanism.
(`script::object::toplevel`, wow-re `ui/scratch/toplevel-raise.md`), the loader honours
`toplevel="true"`, and the frame below carries it — so the Esc menu does come to the
front over any other DIALOG-strata frame it overlaps.
-->
<Script><![CDATA[
-- The reference GlobalStrings (patch-2.MPQ Interface\FrameXML\GlobalStrings.lua), quoted
@@ -164,7 +163,7 @@
<!-- Era swaps the header text to 13px (MainMenuFrameMixin:OnLoad →
SetHeaderFont(GameFontNormalMed1)); the seat is the 1.12 one (within half a
unit of era's — header SCOPE note). -->
<FontString name="GameMenuFrameTitle" inherits="GameFontNormalMed1">
<FontString name="GameMenuFrameTitle" inherits="GameFontNormalMed1" text="MAIN_MENU">
<Anchors>
<Anchor point="TOP" relativeTo="GameMenuFrameHeader"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor>
</Anchors>
@@ -184,12 +183,11 @@
the center-replacement rule for the swap (ShowUIPanel l.697-702: native-center
frames replace each other); hiding the menu explicitly says the same thing
without relying on the eviction path. -->
<Button name="GameMenuButtonOptions" inherits="GameMenuButtonTemplate">
<Button name="GameMenuButtonOptions" inherits="GameMenuButtonTemplate" text="Options">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="28" y="-32"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>self:SetText("Options")</OnLoad>
<OnClick>
PlaySound("igMainMenuOption")
HideUIPanel(GameMenuFrame)
@@ -210,14 +208,13 @@
</Anchors>
<Scripts><OnLoad>GameMenuButton_Pending(self, GAMEMENU_SUPPORT)</OnLoad></Scripts>
</Button>
<Button name="GameMenuButtonMacros" inherits="GameMenuButtonTemplate">
<Button name="GameMenuButtonMacros" inherits="GameMenuButtonTemplate" text="MACROS">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonSupport" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<!-- LIVE since decision 0983 (MacroFrame.xml). The ref's own body
(GameMenuFrame.xml l.118-122): hide the menu, then ShowMacroFrame(). -->
<Scripts>
<OnLoad>self:SetText(MACROS)</OnLoad>
<OnClick>
PlaySound("igMainMenuOption")
HideUIPanel(getglobal("GameMenuFrame"))
@@ -230,12 +227,11 @@
button while either logout dialog is already up (you cannot re-arm a running
countdown); it runs on the menu's own show, which is when a re-opened menu has to
re-read that state. -->
<Button name="GameMenuButtonLogout" inherits="GameMenuButtonTemplate">
<Button name="GameMenuButtonLogout" inherits="GameMenuButtonTemplate" text="LOGOUT">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonMacros" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>self:SetText(LOGOUT)</OnLoad>
<OnShow>
if not StaticPopup_Visible("CAMP") and not StaticPopup_Visible("QUIT") then
self:Enable()
@@ -250,12 +246,11 @@
</OnClick>
</Scripts>
</Button>
<Button name="GameMenuButtonQuit" inherits="GameMenuButtonTemplate">
<Button name="GameMenuButtonQuit" inherits="GameMenuButtonTemplate" text="EXIT_GAME">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonLogout" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>self:SetText(EXIT_GAME)</OnLoad>
<OnShow>
if not StaticPopup_Visible("CAMP") and not StaticPopup_Visible("QUIT") then
self:Enable()
@@ -270,12 +265,11 @@
</OnClick>
</Scripts>
</Button>
<Button name="GameMenuButtonContinue" inherits="GameMenuButtonTemplate">
<Button name="GameMenuButtonContinue" inherits="GameMenuButtonTemplate" text="RETURN_TO_GAME">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonQuit" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>self:SetText(RETURN_TO_GAME)</OnLoad>
<OnClick>
PlaySound("igMainMenuContinue")
HideUIPanel(GameMenuFrame)
@@ -286,11 +280,10 @@
<!-- ref GameMenuFrame OnShow/OnHide, verbatim: the micro row's Main Menu button pushes in
while the menu is up, and the bag bar greys out under it (you cannot open bags from
behind the menu — ShowUIPanel's center gate would refuse the window anyway, so the
greying is the visible half of a rule the panel manager already enforces). Guarded, the
the old UiPanels.xml `CloseMenus and CloseMenus()` posture: a headless test loads this file
without the action bar behind it. -->
greying is the visible half of a rule the panel manager already enforces). Guarded, in
the `CloseMenus and CloseMenus()` posture our retired panel file used: a headless test
loads this file without the action bar behind it. -->
<Scripts>
<OnLoad>GameMenuFrameTitle:SetText(MAIN_MENU)</OnLoad>
<OnShow>
-- The shared era-window scale first (ERA_WINDOW_SCALE, OptionsFrame.xml — loaded
-- before this file): the era menu has no scale of its own (GameMenuFrame.xml +
+11 -7
View File
@@ -108,7 +108,8 @@
permanently, AddOns the unselected gold — the era swaps these in code
(MinimalTabMixin), and with nothing behind AddOns the selection can never change, so
the two states are authored rather than driven. The mixin port comes with the addon
list. Width is era's text+40 law via an OnUpdate measure poll; the era's
list. Width is era's text+40 law, fitted once from each tab's own `<OnShow>` (2028 —
the engine's measurer answers in the call that asks); the era's
measure-at-the-medium-font quirk is deliberately dropped (~6 units of padding).
- **Search WORKS (0984)** — the era SettingsPanel mechanism re-expressed (the Script
block's search section): label-tag substring match scored by matched length,
@@ -1186,10 +1187,12 @@
-- numbering our page does not use, so each row names the child it drives instead.
--
-- NOT bound, and named rather than faked: pfUI's module goes on to hook `SetWorldDetail`
-- and drive `ConsoleExec("frillDensity ...")`, `lodDist`, `nearClip`, `maxLOD`. benilla has
-- no console and none of those CVars. Neither verb is called at load, so the module loads
-- and its slider write lands; its EXTENDED arm cannot work here and a no-op ConsoleExec
-- would be exactly the silent pretence 1203 exists to forbid.
-- (unbound) and drive `ConsoleExec("frillDensity ...")`, `lodDist`, `nearClip`, `maxLOD`.
-- `ConsoleExec` itself IS bound (1948 — it writes a registered CVar or pushes a console
-- command the app dispatches), but none of those four CVars is registered, so those
-- writes reach nothing. Neither verb is called at load, so the module loads and its
-- slider write lands; its EXTENDED arm cannot work here, and registering a CVar nothing
-- reads would be exactly the silent pretence 1203 exists to forbid.
OptionsFrameSliders = {
{ text = "UI Scale", func = "uiscale",
minValue = 0.64, maxValue = 1.0, valueStep = 0.01, benillaRow = "RowUiScale" },
@@ -1546,8 +1549,9 @@
-- Grey or wake one row, whichever control it wears (the Audio rule above predates this and
-- reaches its one checkbox by name). A dropdown row greys the way 1.12's own
-- OptionsFrame_DisableDropDown does — the capsule's toggle BUTTON disabled so the list
-- cannot open, and its text in GRAY_FONT_COLOR — since our kit's
-- UIDropDownMenu_DisableButton addresses a LIST row (level, id), not the capsule.
-- cannot open, and its text in GRAY_FONT_COLOR — since the chain's own
-- UIDropDownMenu_DisableButton addresses a LIST row (level, id), not the capsule, and
-- 1.12 has no `UIDropDownMenu_DisableDropDown` at all.
function OptionsRow_SetEnabled(row, on)
local name = row:GetName()
local box = getglobal(name .. "Check")
@@ -216,9 +216,9 @@
end
]]></Script>
<!-- One list row: the `#N xCount` index column, then the message head. Same shape as
BenillaCraftSkillButtonTemplate (fixed columns — this engine has no FontString-width-based
dynamic anchor). -->
<!-- One list row: the `#N xCount` index column, then the message head. Fixed columns, 1.12's
own list idiom — not a width limitation: a FontString with a zero-authored axis takes its
measured box into the resolve (`benilla-ui` `script/layout.rs`, decision 1664). -->
<Button name="BenillaScriptLogRowTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Layers>
@@ -275,15 +275,12 @@
</Layer>
</Layers>
<Frames>
<!-- The top-right X. This engine ships no UIPanelCloseButton; the minimize-button art
stands in, MacroFrame/TrainerFrame's precedent. -->
<Button name="BenillaScriptLogCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<!-- The top-right X: the chain's own `UIPanelCloseButton` (seated at benilla.toc's
`Interface\FrameXML\UIPanelTemplates.xml`, 757 lines above this file). Its
32x32, its three minimize-button state textures and its
`HideUIPanel(this:GetParent())` are byte-for-byte what stood here authored. -->
<Button name="BenillaScriptLogCloseButton" inherits="UIPanelCloseButton">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-29" y="-8"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts><OnClick>HideUIPanel(getglobal("BenillaScriptLogFrame"))</OnClick></Scripts>
</Button>
<!-- Declared BEFORE the rows so row text paints over the glow (the house z-tiebreak). -->
+13 -12
View File
@@ -174,9 +174,10 @@ Interface\FrameXML\MoneyFrame.xml
# It is groundwork rather than a window, and the windows it is groundwork FOR are named so the
# next session does not have to re-derive it: stock `TradeFrame.xml` and `MailFrame.xml` both build
# their amount entry from this template and call `MoneyInputFrame_SetMode`/`_GetCopper`/
# `_SetCopper`/`_ClearFocus`/`_SetTextColor`/`_SetOnvalueChangedFunc` — six of the names below —
# and neither can be sourced until they exist. (Trade still needs four ENGINE verbs beyond this:
# `AddTradeMoney`, `GetCursorMoney`, `GetTradePlayerItemLink`, `GetTradeTargetItemLink`.)
# `_SetCopper`/`_ClearFocus`/`_SetTextColor`/`_SetOnvalueChangedFunc` — six of the names below.
# Both windows are migrated now: `MailFrame.xml` at l.897 and `TradeFrame.xml` at l.906, the
# latter once 1966 bound the four engine verbs it also needed (`AddTradeMoney`,
# `GetCursorMoney`, `GetTradePlayerItemLink`, `GetTradeTargetItemLink`).
#
# **TWO entries, and the `.lua` was missing.** This `.xml` declares only the template and does not
# source its `.lua`; the reference's toc lists both (l.11-12). Listing the `.xml` alone brought the
@@ -489,8 +490,8 @@ Interface\FrameXML\MainMenuBarMicroButtons.xml
Interface\FrameXML\BattlefieldFrame.xml
# Minimap.xml + Minimap.lua: the cluster around the engine-rendered <Minimap> widget hole
# (`crate::minimap` fills it), the zone text, the zoom buttons, the tracking / mail / meeting
# stone / battlefield icons, and `MiniMapPing` — a <Model> this engine draws no pixels for; the
# app's ping sprite (`minimap/ping.rs`) follows that frame's own show/hide and alpha, so the
# stone / battlefield icons, and `MiniMapPing` — a <Model> that renders its own MinimapPing.mdx
# since 2013; `minimap/ping.rs` keeps the pin, the click seat and the wire half, so the
# 5 s lifetime is this file's, as the reference's is. After GameTooltip (the zone-text hover).
# 0203's transcription is gone.
Interface\FrameXML\Minimap.xml
@@ -579,8 +580,8 @@ Interface\FrameXML\ColorPickerFrame.xml
# <Include>s its own blip templates (WorldMapFrameTemplates.xml) before the frames that inherit
# them. Its player arrow is the Create/Position/Show/UpdateWorldMapArrowFrames family
# (`script/worldmap_arrow.rs`), an anonymous Model pane the app draws as the minimap's arrow
# sprite; its WorldMapPing is a MinimapPing.mdx Model pane this engine holds and draws nothing
# into (named gap, 1980).
# sprite; its WorldMapPing is the same MinimapPing.mdx Model pane, which draws its own file on
# the map sheet since 2013 (closing the gap 1980 named).
Interface\FrameXML\WorldMapFrame.xml
# ══ THE CAST BAR — the reference's own, off the player's chain (1751) ══
# `CastingBarFrame` + its Border/Flash/Spark/Text children, driven by the SPELLCAST_* events
@@ -964,7 +965,7 @@ Interface\FrameXML\BankFrame.xml
# a `CSimpleFrame` and nothing else, so it is `FrameKind::Frame` and the route lines stay what the
# reference makes them: plain `<Texture>` children rotated by an 8-argument `SetTexCoord`.
#
# Pulls only `TaxiFrame.lua`, and still needs only Fonts/UiPanels/GameTooltip (SetTooltipMoney),
# Pulls only `TaxiFrame.lua`, and still needs only Fonts/UIParent/GameTooltip (SetTooltipMoney),
# all loaded above — so it keeps its seat in the "NPC session windows grouped" cluster.
Interface\FrameXML\TaxiFrame.xml
# The stable-master window (decision 1676): the hunter's three-slot pet stable over the stable
@@ -1186,8 +1187,8 @@ Interface\FrameXML\TutorialFrame.xml
# options family, the same chain `MainMenuBarMicroButtons` is deferred behind. All three columns
# read clean because the file LOADS clean; what it needs is a sibling window, not a verb.
#
# Its CAMP/QUIT dialogs and their event driver HAVE moved out, to UiPanels.xml — the reference's
# own homes (StaticPopup.lua l.564/586 for the dialogs, UIParent.lua l.304-315 for the three
# events). The ESC menu never owned them; it was just where ours were first needed. That is one
# less thing to move when the options family lands.
# Its CAMP/QUIT dialogs and their event driver HAVE moved out — the dialogs to `StaticPopup.xml`,
# the driver to `UIParent.xml`, the reference's own homes (StaticPopup.lua l.564/586 for the
# dialogs, UIParent.lua l.304-315 for the three events), both stock since 1988. The ESC menu never
# owned them; it was just where ours were first needed. That is one less thing to move when the options family lands.
GameMenuFrame.xml
+5 -5
View File
@@ -1121,7 +1121,7 @@ fn load_dependencies(
///
/// **A string literal in the fourth argument, OR a local bound to one in the same file.** The
/// literal-only form was the honest under-report this doc used to name — and then a decision was
/// made on the number it produced. `assets/ui/ItemButtonTemplate.xml` declined to build
/// made on the number it produced. `Interface\FrameXML\ItemButtonTemplate.xml` declined to build
/// `ItemButtonTemplate` citing "the harness's template demand ranks it at zero on both axes"; the
/// zero was real, and pfUI wanted `ContainerFrameItemButtonTemplate` (which inherits it) the whole
/// time, through
@@ -3575,10 +3575,10 @@ mod dependency_tests {
/// **A template named through a local resolves; the shapes that still cannot are asserted too.**
///
/// The literal-only scan was an honest under-report — stated at `missing_templates` — and a
/// decision was then made on the number it produced: `assets/ui/ItemButtonTemplate.xml`
/// declined to build `ItemButtonTemplate` citing a demand of zero "on both axes". The zero was
/// real and the demand was not: pfUI binds `local tpl = "ContainerFrameItemButtonTemplate"`
/// and passes the variable, so the one addon that wanted it was invisible to the ranking.
/// decision was then made on the number it produced:
/// `Interface\FrameXML\ItemButtonTemplate.xml` declined to build `ItemButtonTemplate` citing a
/// demand of zero "on both axes". The zero was real and the demand was not: pfUI binds `local
/// tpl = "ContainerFrameItemButtonTemplate"` and passes the variable, so the one addon that wanted it was invisible to the ranking.
///
/// Both halves are asserted, because a scanner that quietly widened would be the worse fix:
/// the shape it now sees, AND the shapes it still does not, so the next decision quoting this
+2 -2
View File
@@ -252,8 +252,8 @@ pub(super) fn seed_ui_fixture(
gossip.quests = vec![(783, 0, 5, "Eagan Peltskinner".into())];
// One short option AND four that WRAP — the live shape both gossip bugs came in as
// (the director's screenshots), and a menu deliberately TALLER than the parchment so
// the capture covers the whole chain: the per-row auto-height
// (`BenillaGossipRow_Resize`), the scroll frame that contains the overflow, and the
// the capture covers the whole chain: the stock `GossipResize`'s per-row auto-height,
// the scroll frame that contains the overflow, and the
// scrollbar that appears with it. A fixture of one-line labels showed none of this —
// every row fit the template's 16 px and nothing ever overflowed.
let judgement = [
+3 -3
View File
@@ -35,9 +35,9 @@
//! rather than being skipped (`0x48efe0`).
//! - **ESC is not an engine binding.** `StopCinematic` has zero native callers in the reference:
//! the only skip path is `CinematicFrame.xml`'s own `OnKeyDown`, which is why benilla's copy of
//! that frame (`assets/ui/CinematicFrame.xml`) carries the same handler and why the Lua binding
//! queues [`SessionRequest::StopCinematic`](benilla_ui::script::SessionRequest) rather than
//! reaching in here.
//! that frame (stock `Interface\FrameXML\CinematicFrame.xml`) carries the same handler and why
//! the Lua binding queues [`SessionRequest::StopCinematic`](benilla_ui::script::SessionRequest)
//! rather than reaching in here.
//! - **The ack ends the run, once.** `CMSG_COMPLETE_CINEMATIC` goes out on a natural end and on an
//! ESC skip alike (`0x48f080`). Decision 0196 is why it can never be dropped: unacked, vmangos
//! re-anchors object visibility to its own copy of the flying camera and everything around the
+1 -1
View File
@@ -37,7 +37,7 @@
//! `MiniMapPing` `<Model>` it shows on `MINIMAP_PING`, re-seats every frame from
//! `GetPingPosition()`, holds 5 s (`MINIMAPPING_TIMER`), "fades" 0.5 s through a `SetAlpha(255·t)`
//! that clamps to full until the last ~2 ms, and hides. Since 1751's swap that file runs here
//! verbatim (1974), and since decision 2008 the `<Model>` it shows **renders its own file**
//! verbatim (1974), and since decision 2013 the `<Model>` it shows **renders its own file**
//! (`Interface\MiniMap\Ping\MinimapPing.mdx`, through `crate::ui_models`): the spinner on its
//! global-sequence clock, the static centre, the ring on the looping Stand — the model's own
//! bones, weight tracks and additive quads, on the pane's private clock that runs only while the
+5 -4
View File
@@ -186,8 +186,9 @@ pub(super) fn chat_wrong_faction(chat_log: &mut ChatLog) {
/// (wow-re `system/ui/ui.md` l.2459). Nothing on this path touches the chat composer.
///
/// **This used to push into the chat feed**, as a stand-in from before benilla had an errors
/// frame. It has had a real one for a long time (`assets/ui/ErrorsFrame.xml`, the ref
/// `UIErrorsFrame` as a genuine `MessageFrame`), and the stand-in outlived its reason: vmangos
/// frame. It has had a real one for a long time — the ref `UIErrorsFrame`, a genuine
/// `MessageFrame` (ours until 1751's fourteenth window, the chain's own
/// `Interface\FrameXML\UIErrorsFrame.xml` since) — and the stand-in outlived its reason: vmangos
/// `Player::SetGameMaster` answers `.gm on|off` with **both** `SendSysMessage` and
/// `SendNotification` (`Objects/Player.cpp:2676-2677`/`2701-2702`), so a client that sinks the
/// notification into chat prints "GM mode is ON" **twice** where the reference prints it once.
@@ -382,8 +383,8 @@ mod tests {
/// handler `0x401800` is "read the cstring, `mov edx,1`, `call 0x4945b0`" — event `0xe0`
/// `UI_ERROR_MESSAGE`, the UIErrorsFrame toast — plus a console log, and it never reaches the
/// chat composer. Benilla used to park the notice in the chat feed for want of an errors
/// frame; it has had one since `assets/ui/ErrorsFrame.xml`, and the stand-in was what printed
/// "GM mode is ON" twice.
/// frame; it has had one since our own `ErrorsFrame.xml` (the chain's `UIErrorsFrame.xml`
/// since 1751), and the stand-in was what printed "GM mode is ON" twice.
///
/// The pair is the test: BOTH halves of one toggle, one chat line, one toast.
#[test]
+1 -1
View File
@@ -3,7 +3,7 @@
//! The net bridge queues [`CastBarEdge`]s (self-casts only — the producers filter on the self
//! guid; the channel pair is self-only *on the wire*), and the drain fires the reference
//! client's FrameScript events into the script VM — `SPELLCAST_START` and family, the exact
//! contract `assets/ui/CastingBar.xml` (the extracted 1.12 `CastingBarFrame`) registers for.
//! contract stock `Interface\FrameXML\CastingBarFrame.xml` registers for.
//! The spell name rides the event (resolved here from the `Spell.dbc` catalog — the script VM
//! has no spell-catalog binding, deliberately: one lookup face, decision 0107).
+1 -1
View File
@@ -3,7 +3,7 @@
//!
//! The net bridge queues [`MirrorTimerEdge`]s and the drain fires the reference client's
//! FrameScript events into the script VM — `MIRROR_TIMER_START` / `_PAUSE` / `_STOP`, the exact
//! contract `assets/ui/MirrorTimer.xml` (the transcribed 1.12 `MirrorTimer1/2/3`) registers for.
//! contract stock `Interface\FrameXML\MirrorTimer.xml` (`MirrorTimer1/2/3`) registers for.
//! The bars themselves are the reference's: the frame stores the value and integrates
//! `value + scale * elapsed` every OnUpdate, so a packet every few seconds is enough to paint a
//! smooth countdown.
@@ -6,7 +6,7 @@ fn action_ids(s: &mut UiScript) -> Vec<u32> {
s.take_action_uses().into_iter().map(|u| u.action).collect()
}
/// Load the real `assets/ui/ActionBar.xml` (the shipped default bar) into a bare engine and
/// Load the stock `Interface\FrameXML\ActionBarFrame.xml` into a bare engine and
/// drive it with a synthetic action snapshot — the slice-1 chain minus Bevy: template
/// expansion over 12 instances, the vanilla bonus-page formula, icon paint on events, empty
/// slots drawing no icon, and a physical click queuing the right UseAction id.
@@ -798,19 +798,20 @@ fn macro_name_line_follows_get_action_text_through_the_xml() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The shipped **bag BAR** — `assets/ui/BagFrame.xml` — materialized frame for frame, and then
/// driven end to end: the bar's own backpack toggle opens the backpack window, the fed stack
/// paints in its slot's well, the slot's clicks queue the right intents, and the toggle shuts it
/// again. It lives in this file because the bar seats on `MainMenuBarArtFrame` (ActionBar.xml) —
/// the toggle's anchor arithmetic below is the reason.
/// The **bag BAR** — stock `Interface\FrameXML\MainMenuBarBagButtons.xml` — materialized frame
/// for frame, and then driven end to end: the bar's own backpack toggle opens the backpack
/// window, the fed stack paints in its slot's well, the slot's clicks queue the right intents,
/// and the toggle shuts it again. It lives in this file because the bar seats on
/// `MainMenuBarArtFrame` (`ActionBarFrame.xml`) — the toggle's anchor arithmetic below is the
/// reason.
///
/// **What decision 1751 changed.** This asserted `report.frames == 259` over a breakdown that
/// counted five bag WINDOWS and a keyring window (37 + 4×42 + 42, plus the bar's own handful).
/// Those windows are gone from this file: the live ones are the reference's `ContainerFrame1..12`,
/// executed off the player's own patch chain, and `BagFrame.xml` is the BAR and nothing else —
/// this client's stand-in for the reference's `MainMenuBarBagButtons.xml`. So the count is
/// recounted from what the file declares today, and the drive reaches the reference's window
/// through the bar's own button rather than showing one of ours by name.
/// Those windows are gone: the live ones are the reference's `ContainerFrame1..12`, executed off
/// the player's own patch chain, and the bar is the reference's own
/// `MainMenuBarBagButtons.xml` (1783). So the count is recounted from what that file declares,
/// and the drive reaches the reference's window through the bar's own button rather than showing
/// one of ours by name.
#[test]
fn shipped_bag_frame_drives_end_to_end() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -829,8 +830,7 @@ fn shipped_bag_frame_drives_end_to_end() {
// * StackSplit.xml and MerchantFrame.xml after the bags — the reference's
// `ContainerFrameItemButton_OnClick` reads `StackSplitFrame` on both arms and
// `MerchantFrame:IsShown()` on the right one, so a slot click raises without them. That
// dependency is the reference's, not ours; MerchantFrame.xml is additionally the home of
// the `BenillaMoney_*` coin rig this chain's windows call.
// dependency is the reference's, not ours.
let mut bar_frames = 0;
for file in BAG_UI {
let frames = load_ui(&s, file);
@@ -912,9 +912,9 @@ fn shipped_bag_frame_drives_end_to_end() {
// Click the toggle → the backpack window opens and slot 1 paints the jerky. The toggle seats
// on the bar's art frame BOTTOMRIGHT +(-6,2), 37×37: art frame BOTTOMRIGHT is the bar's
// (full-width, bottom-anchored) corner (1024,0) ⇒ toggle x[981,1018] y[2,39], center
// (999.5,20.5). That arithmetic is THIS file's — the button is `BagFrame.xml`'s and its seat
// is `ActionBar.xml`'s — so the click stays at literal coordinates rather than going through
// `centre_of`: hitting them is part of what is being tested.
// (999.5,20.5). That arithmetic is THIS file's — the button is `MainMenuBarBagButtons.xml`'s
// and its seat is `ActionBarFrame.xml`'s — so the click stays at literal coordinates rather
// than going through `centre_of`: hitting them is part of what is being tested.
s.mouse_button(999.0, 20.0, "LeftButton", true);
s.mouse_button(999.0, 20.0, "LeftButton", false);
s.resolve();
@@ -937,7 +937,8 @@ fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
);
assert!(!open(&s, 1), "hidden by default");
// CharacterBag0Slot == bag id 1 (BenillaBagBarSlot_OnLoad(self, 1) in BagFrame.xml).
// CharacterBag0Slot == bag id 1: the stock bar's handlers carry no `bagId` field and
// recompute `this:GetID() - CharacterBag0Slot:GetID() + 1` (see `:2119` below).
s.run("CharacterBag0Slot:Click()").unwrap();
let _ = s.take_sounds();
assert!(open(&s, 1), "the bag-bar click opened bag 1's window");
@@ -438,7 +438,7 @@ fn without_a_player_at_addon_load_bagnon_draws_an_empty_window() {
}
/// The **other** fault, pinned on its own so a later regression names itself: the reference's
/// `SetItemButton*` family (`assets/ui/ItemButtonTemplate.xml`).
/// `SetItemButton*` family (`Interface\FrameXML\ItemButtonTemplate.xml`).
///
/// Bagnon ends every slot update with `SetItemButtonDesaturated` / `SetItemButtonTexture` /
/// `SetItemButtonCount`. While those were nil the raise landed inside `BagnonFrame_AddBag`'s
@@ -1133,16 +1133,16 @@ fn a_texture_gradient_tints_the_art_it_sits_on() {
/// **The director's report: "when I first open the bags with bagnon the gold numbers are all
/// cramped up; if I close and open again it looks good."**
///
/// Bagnon's money display is `SmallMoneyFrameTemplate` — OUR `MoneyFrame.xml`. Its `ShowCoin` used
/// to size each coin with `label:GetStringWidth()`, which is served from the measure round-trip and
/// therefore reads **0 in the tick that set the text**: every coin came out at exactly one icon
/// width and the digits overlapped. The second open looked right because the first open's measure
/// had landed in the cache by then — the reopen was reading the previous open's numbers.
/// Bagnon's money display is `SmallMoneyFrameTemplate`, off the chain's own `MoneyFrame.xml`. Its
/// `ShowCoin` used to size each coin with `label:GetStringWidth()`, which is served from the
/// measure round-trip and therefore reads **0 in the tick that set the text**: every coin came out
/// at exactly one icon width and the digits overlapped. The second open looked right because the
/// first open's measure had landed in the cache by then — the reopen was reading the previous open's numbers.
///
/// The fix sums `BENILLA_DIGIT_W`, the app's per-digit advance feed
/// ([`benilla_ui::script::UiScript::set_digit_advances`]) — data pushed ahead, so the answer exists
/// *in* the tick. That feed's own doc names this frame as what it was built for; only the merchant
/// price had ever used it.
/// The fix is the engine's font measurer answering inside the Lua call that asked
/// (`benilla_ui`'s `script::measure`): `GetStringWidth` returns a real number in the tick that set
/// the text. A digits-only stand-in — a per-digit advance feed pushed ahead from the app — shipped
/// first and was retired by the general answer (decision 1285).
///
/// The assertion is the FIRST open, which is the half that was broken.
#[test]
@@ -1,5 +1,5 @@
//! The shipped `assets/ui/BasicControls.xml` — the reference's second file, driven the way an
//! addon drives it.
//! The stock `Interface\FrameXML\BasicControls.xml` — the reference's second file, driven the way
//! an addon drives it.
//!
//! Nothing benilla ships calls `message`, `TEXT` or `_ERRORMESSAGE`; their only consumers are
//! third-party addons (26 call `TEXT`, ~10 genuinely call `message`, two replace `_ERRORMESSAGE`).
@@ -1,6 +1,6 @@
//! The player buff bar (`assets/ui/BuffFrame.xml`, decisions 0255/0257) against its reference
//! behaviour. The XML/Lua is the unit under test; the app-side feed (`crate::ui_aura`) is stubbed by
//! pushing an [`AuraState`] list straight through [`UiScript::set_auras`] and firing
//! The player buff bar (stock `Interface\FrameXML\BuffFrame.xml`, decisions 0255/0257) against its
//! reference behaviour. The XML/Lua is the unit under test; the app-side feed (`crate::ui_aura`) is
//! stubbed by pushing an [`AuraState`] list straight through [`UiScript::set_auras`] and firing
//! `PLAYER_AURAS_CHANGED`, so these exercise the *button* handlers — the row/filter wiring, the
//! dispel-tinted border, the stack count, the countdown, the warning flash, and the right-click
//! cancel — the way the reference's own `BuffButton_*` do.
+10 -10
View File
@@ -1,9 +1,9 @@
//! Shipped end-to-end tests for the chat window + input (decision 0084, the chat arc): the real
//! `assets/ui/ChatFrame.xml` loaded through the engine loader, driven exactly as the app drives it —
//! `add_chat_message` → `resolve`/`extract` (lines render in the pinned colors), the wheel → the Lua
//! `OnMouseWheel` handler → `ScrollUp` (which freezes the fade), and the input EditBox's ENTER →
//! `OnEnterPressed` → `SubmitChatInput` → `take_chat_input` (the app then parses it — see
//! `crate::ui_chat`'s `parse_line` tests for the `/say`//`/yell`//`/w` mapping).
//! `Interface\FrameXML\ChatFrame.xml` loaded through the engine loader, driven exactly as the app
//! drives it — `add_chat_message` → `resolve`/`extract` (lines render in the pinned colors), the
//! wheel → the Lua `OnMouseWheel` handler → `ScrollUp` (which freezes the fade), and the input
//! EditBox's ENTER → `OnEnterPressed` → `SubmitChatInput` → `take_chat_input` (the app then parses
//! it — see `crate::ui_chat`'s `parse_line` tests for the `/say`//`/yell`//`/w` mapping).
use benilla_ui::script::{ExtractedQuad, QuadContent, UiScript};
@@ -378,11 +378,11 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
// ── ChatTypeInfo: the addon-facing color table ────────────────────────────────────────────────
/// `ChatTypeInfo` carries the shipped default chat colors twice: once in `assets/ui/ChatFrame.xml`
/// for addons to read, and once in [`crate::ui_chat::default_color`] for our own feed to render.
/// Both are the same wow-re byte table (`chat-color-table.md`, the static registry at
/// `.rdata 0x804710`) — so this is the gate that makes the duplication safe: every kind we model
/// must agree to the byte, and the table's shape (`sticky`/`id`) must be the reference's.
/// `ChatTypeInfo` carries the shipped default chat colors twice: once in
/// `Interface\FrameXML\ChatFrame.xml` for addons to read, and once in
/// [`crate::ui_chat::default_color`] for our own feed to render. Both are the same wow-re byte
/// table (`chat-color-table.md`, the static registry at `.rdata 0x804710`) — so this is the gate
/// that makes the duplication safe: every kind we model must agree to the byte, and the table's shape (`sticky`/`id`) must be the reference's.
#[test]
fn chat_type_info_matches_the_host_color_table() {
use crate::ui_chat::{default_color, ChatEventKind as K};
@@ -1,5 +1,5 @@
//! The shipped `assets/ui/ColorPickerFrame.xml` + the dropdown's colour-swatch row, driven the way
//! the corpus drives them.
//! The stock `Interface\FrameXML\ColorPickerFrame.xml` + the dropdown's colour-swatch row, driven
//! the way the corpus drives them.
//!
//! Nothing benilla ships opens this window. Its consumers are third-party addons — 86 of the 218
//! reach `ColorPickerFrame`, and 64 of those files are copies of `Dewdrop-2.0.lua`, THE Ace2 menu
@@ -85,8 +85,9 @@ fn bag_setup() -> UiScript {
for file in BAG_UI {
load_xml(&s, file);
}
// `ContainerFrame_Update` reads `MerchantFrame:IsShown()` on any slot the tooltip owns, and
// `BankFrame` (BAG_UI's last entry) fills its purse through `BenillaMoney_Set`.
// `ContainerFrame_Update` reads `MerchantFrame:IsShown()` on any slot the tooltip owns.
// `BankFrame`'s purse is an ordinary `SmallMoneyFrameTemplate` off the chain's own
// `MoneyFrame.xml` (1937/1962), so it is not a merchant dependency.
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");
@@ -1,5 +1,5 @@
//! The shipped errors/info frame (`assets/ui/ErrorsFrame.xml` — the ref UIErrorsFrame, a real
//! `<MessageFrame>`) driven engine-only: the yellow `UI_INFO_MESSAGE` toast (the quest
//! The errors/info frame — stock `Interface\FrameXML\UIErrorsFrame.xml`, a real
//! `<MessageFrame>` — driven engine-only: the yellow `UI_INFO_MESSAGE` toast (the quest
//! objective-progress popup's surface), the red `UI_ERROR_MESSAGE` line, insertMode-TOP stacking,
//! and the hold+fade expiry.
//!
@@ -124,7 +124,7 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
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
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // QuestLogDetail's reward money row (1928)
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -79,7 +79,9 @@ fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
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
// The reference's own `ContainerFrameItemButton_OnClick` reads `MerchantFrame:IsShown()`
// on the bag-slot click below; the coin rig this line used to name is gone (1937/1962).
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
s.set_money(0);
s.set_container(0, Some(one_item_backpack()));
@@ -23,10 +23,10 @@
//! answers the same whichever way it came.
//!
//! **Divergences are an explicit list with a reason each, never a pattern.** [`KNOWN`] carries
//! them, in both directions; a new one cannot hide inside a tolerance. The list is where the
//! seven frames the reference makes interactive through *handlers we do not carry* are recorded —
//! those want the handler (and its tooltip), never a bare `enableMouse="true"` that would swallow
//! the click and give nothing back.
//! them, in both directions; a new one cannot hide inside a tolerance. **It is empty today** —
//! 1751's window migrations, 1795, 1970 and 1980 each retired a group, and the in-body notes are
//! the record of what each one was. The array stays because an empty list is the thing that
//! notices a divergence arriving.
//!
//! ## Four flags, and only four
//!
@@ -68,8 +68,8 @@
//!
//! The reference side is **XML only**: a `SetID` or `EnableMouse` the reference makes from Lua at
//! `OnLoad` is invisible here and reads as absent. That blind spot only ever under-reports — it
//! can hide a divergence, never invent one — so nothing it misses turns into a false failure; the
//! handful of frames it does hide are in `KNOWN` with that reason.
//! can hide a divergence, never invent one — so nothing it misses turns into a false failure. It
//! would be a `KNOWN` entry's reason if any frame still needed one.
//!
//! The whole module skips cleanly with no install — `_extracted_framexml/` is a gitignored
//! Blizzard asset, like every other client-data test here.
@@ -157,16 +157,18 @@ impl std::fmt::Display for Flag {
}
}
/// The accepted differences, in four groups: **seven frames the reference makes mouse-interactive
/// through HANDLERS we do not carry**, the merchant rows whose mouse we take where it does not,
/// the faux scroll panes we build from a different widget kind, and the ids the reference sets
/// from Lua. Every entry is a judgement someone made; none of them is a tolerance.
/// The accepted differences — **none, today**. The four groups this list once carried (the frames
/// the reference made mouse-interactive through handlers we did not carry, the merchant rows, the
/// faux scroll panes, the Lua-set ids) each retired with the window that owned them; the in-body
/// notes below name the record for each. An empty array is not an oversight: it is what fails the
/// gate the moment a new divergence appears, and every entry it ever holds is a judgement someone
/// made, never a tolerance.
const KNOWN: &[Known] = &[
// ── The reference has it, we do not: all seven are handler gaps, not flag gaps ─────────────
// ── The reference had it and we did not: seven handler gaps, not flag gaps (all retired) ───
//
// In each of these the reference's mouse comes from an `<OnEnter>`/`<OnLeave>` pair whose body
// we have not built, and the interaction the player would notice is the TOOLTIP those handlers
// show — not the click-blocking the flag gives. Declaring `enableMouse="true"` here would make
// In each of these the reference's mouse came from an `<OnEnter>`/`<OnLeave>` pair whose body
// we had not built, and the interaction the player would notice was the TOOLTIP those handlers
// show — not the click-blocking the flag gives. Declaring `enableMouse="true"` would have made
// the frame swallow the click and hand back nothing, which is worse than the gap.
// `PetPaperDollFrameExpBar` RETIRED here (decision 1751's character-sheet window). It read
// "the reference bar inherits TextStatusBar, whose OnEnter/OnLeave show the value text; ours is
@@ -1,5 +1,5 @@
//! The group-loot roll popups (decision 0591, `assets/ui/GroupLootFrame.xml`): the four stacked
//! `GroupLootFrame`s that answer `START_LOOT_ROLL`/`CANCEL_LOOT_ROLL` off a pushed
//! The group-loot roll popups (decision 0591, stock `Interface\FrameXML\GroupLootFrame.xml`): the
//! four stacked `GroupLootFrame`s that answer `START_LOOT_ROLL`/`CANCEL_LOOT_ROLL` off a pushed
//! [`LootRollsState`] snapshot (the `loot_roll.rs` seam's own harness idiom, mirrored here the way
//! `loot_tests.rs` mirrors it for `set_loot`/`LootState`).
@@ -1,7 +1,7 @@
//! The shipped **inspect window** driven end-to-end, engine-only (no Bevy): the real
//! `assets/ui/InspectFrame.xml` loaded behind `Fonts.xml`/`UiPanels.xml`/`GameTooltip.xml` and fed a
//! synthetic target snapshot + a foreign equipment view (decision 0631). `character_tests.rs`'s
//! harness, turned onto the other paper doll.
//! The reference's own `Blizzard_InspectUI` addon loaded behind
//! `Fonts.xml`/`UIParent.xml`/`GameTooltip.xml` and fed a synthetic target snapshot + a foreign
//! equipment view (decision 0631). `character_tests.rs`'s harness, turned onto the other paper doll.
//!
//! What these are here to falsify, in order of how quietly it could have shipped broken:
//!
@@ -595,11 +595,11 @@ fn the_loot_window_draws_over_the_party_frames() {
/// previews the row's item in the dressing room (decision 1060), SHIFT posts its link into an open
/// chat edit box (decision 1059) — and **neither loots**.
///
/// That last clause is the whole point of the test, and it is ours to get right rather than the
/// reference's: in the real client the loot itself is the C `LootButton` widget's click behaviour
/// (l.94's `button:SetSlot(slot)`), so its Lua arms fall through harmlessly; ours owns the
/// `LootSlot` call, so the arms had to grow a `return` the reference does not have. The unmodified
/// click still loots — the regression that would otherwise ship silently.
/// That last clause is the whole point of the test, and it is the WIDGET's to get right: the loot
/// itself is the `LootButton` kind's click behaviour (`benilla-ui` `script/button.rs`, decision
/// 1799 — `l.94`'s `button:SetSlot(slot)` is what arms it), gated on no shift/ctrl/alt, which is
/// why the stock `LootFrameItem_OnClick` never calls a take itself and needs no `return`. The
/// unmodified click still loots — the regression that would otherwise ship silently.
#[test]
fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
const WOOL_LINK: &str = "|cffffffff|Hitem:2589:0:0:0|h[Wool Cloth]|h|r";
@@ -1,4 +1,4 @@
//! The shipped `assets/ui/MacroFrame.xml` — the macro editor + its name/icon popup (decision
//! The reference's own `Blizzard_MacroUI` addon — the macro editor + its name/icon popup (decision
//! 0983), driven against the engine's own macro table.
//!
//! What these guard, end to end through the real file: the window loads clean in its real
@@ -1,6 +1,6 @@
//! The shipped **merchant window** driven end-to-end, engine-only (no Bevy): the real
//! `assets/ui/MerchantFrame.xml` (+ `GameTooltip.xml` for the hover chain) loaded behind
//! `UiPanels.xml` and fed synthetic stock — the phase-4 vendor arc's machine checks (decision
//! The **merchant window** driven end-to-end, engine-only (no Bevy): the stock
//! `Interface\FrameXML\MerchantFrame.xml` (+ `GameTooltip.xml` for the hover chain) loaded behind
//! `UIParent.xml` and fed synthetic stock — the phase-4 vendor arc's machine checks (decision
//! 0081/0084). Split from `panel_tests` (which keeps gossip + the slot manager itself) along the
//! folder's one-file-per-window convention.
@@ -40,7 +40,7 @@ fn frame_rect(quads: &[ExtractedQuad], w: f32, h: f32) -> benilla_ui::layout::Re
.unwrap_or_else(|| panic!("no bare-frame quad sized {w}x{h}"))
}
/// Load the real `assets/ui/MerchantFrame.xml` (the shipped vendor window) behind `UiPanels.xml`
/// Load the stock `Interface\FrameXML\MerchantFrame.xml` behind `UIParent.xml`
/// into a bare engine and drive it with a synthetic 2-item stock + a purse — the whole phase-4
/// chain minus Bevy (decision 0081), now over the UIPanel slot manager (decision 0084): the
/// hidden→shown lifecycle on MERCHANT_SHOW goes through ShowUIPanel (landing at the left slot),
@@ -1092,7 +1092,7 @@ fn trade_recipient_money_renders_the_digit_not_ellipsis() {
load_xml(&s, f);
}
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // the BenillaMoney_* helpers
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // MerchantFrame:IsShown(), read on the slot click
// TradeFrame's money entry is the chain's own since 1882 — its OnLoad calls MoneyInputFrame_*.
load_xml(&s, "Interface\\FrameXML\\MoneyInputFrame.lua");
load_xml(&s, "Interface\\FrameXML\\MoneyInputFrame.xml");
@@ -1,12 +1,11 @@
//! The shipped `assets/ui/MoneyFrame.xml` — the reference's `MoneyFrameTemplate` /
//! The stock `Interface\FrameXML\MoneyFrame.xml` — the reference's `MoneyFrameTemplate` /
//! `SmallMoneyFrameTemplate` kit and the `MoneyTypeInfo` table behind it (decision 1190: a name the
//! shipped 1.12 UI defines is a name we publish too — under our own implementation of it, 1260).
//!
//! Nothing benilla ships consumes this file yet — our own eight coin displays run on
//! MerchantFrame.xml's `BenillaMoney_*` slot kit, and MoneyFrame.xml's header is where the two are
//! reconciled — so these tests ARE its only driver. That makes them the contract: they exercise it
//! the way an addon does (declare a frame on the template, point it at a type, change the type,
//! read the coins back), because that is who calls it.
//! Eight shipped windows consume this file (1937 converged the coin cluster onto it; 1962 put the
//! file itself on the chain). These tests exercise it the way an addon does — declare a frame on
//! the template, point it at a type, change the type, read the coins back — because that is who
//! calls it.
//!
//! What they guard: the type switch actually rewiring where the number comes from and whether the
//! coins take the mouse; the collapse rule per type (`collapse`, `showSmallerCoins`, `fixedWidth`);
@@ -203,8 +202,9 @@ fn update_paints_a_static_frame_by_name_and_resizes_it() {
//
// **The digits used to contribute 0 here and this test asserted the bare 13s that came of it.**
// That was the director's cramped gold on a first open: `ShowCoin` sized each coin from a text
// measure that lands a frame later, so it read 0 every first time. It sums the engine's
// `BenillaNumberWidth` feed now, which answers in the same tick.
// measure that lands a frame later, so it read 0 every first time. The engine's font
// measurer answers inside the Lua call that asked now (`script/measure.rs`), so the width
// is real on the first pass.
assert_eq!(
s.eval::<f64>("return TestPurseGoldButton:GetWidth()")
.unwrap(),
@@ -323,8 +323,8 @@ fn set_money_frame_color_recolours_the_digits_and_not_the_icons() {
}
/// `MoneyTypeInfo` is read directly by addons, so its shape is part of the contract — all seven
/// reference types, each with an `UpdateFunc`, and the two we cannot source yet answering 0 rather
/// than erroring (the header's `GetSendMailMoney`/`GetSendMailCOD` gap).
/// reference types, each with an `UpdateFunc`, and the two send-mail rows reading the amounts
/// their engine getters hold (`GetSendMailMoney`/`GetSendMailCOD`, built by 1962).
#[test]
fn the_money_type_table_carries_all_seven_reference_types() {
let s = harness(12_345);
@@ -345,7 +345,7 @@ fn the_money_type_table_carries_all_seven_reference_types() {
"MoneyTypeInfo[\"{t}\"] with an UpdateFunc"
);
}
// The two whose engine getters we have not built answer 0 instead of nil-calling.
// The two send-mail rows: with no send-mail amount set, their getters answer 0.
for t in ["SEND_MAIL", "SEND_MAIL_COD"] {
s.run(&format!(
"this = TestPurse MoneyFrame_SetType(\"{t}\") this = nil"
@@ -359,7 +359,7 @@ fn the_money_type_table_carries_all_seven_reference_types() {
assert_eq!(
s.eval::<i64>("return TestPurse.staticMoney").unwrap(),
0,
"{t} reads 0 while its engine getter is missing"
"{t} reads its engine getter, which is 0 with no send-mail amount set"
);
}
}
@@ -724,14 +724,15 @@ fn load_definers(s: &UiScript, files: &[&str]) {
/// The Interface page's harness (decision 1136), the same posture as `combat_harness` above: the
/// **real** definers ahead of the window, in the manifest's own order, so each row captures the
/// same file-scope value it captures in the client. `SHOW_NEWBIE_TIPS` rides in on `GameTooltip.xml`
/// which `harness_on` already loads; the two quest globals need their own windows —
/// `MerchantFrame.xml` for the coin helpers both quest files reuse and `ScrollTemplates.xml` for
/// the kit `QuestFrame.xml` inherits from (the same chain `quest_tests`/`questlog_tests` load).
/// `SHOW_BUFF_DURATIONS` arrives with the bar it re-anchors (1139), behind the two files
/// `buff_tests` loads ahead of it — `Cooldown.xml` (every button's child) and `ActionBar.xml`
/// (`BENILLA_FALLBACK_ICON`), themselves behind `UIParent.xml`. `TextStatusBar.xml` rides in
/// ahead of them for the Status Bar Text row's consumer — the XP bar's numerals (1140).
/// same file-scope value it captures in the client. `SHOW_NEWBIE_TIPS` is set by our own
/// `assets/ui/OptionsFrame.xml` (it lived in our `GameTooltip.xml` until the tooltip went stock,
/// 1968; the reference keeps it in `UIOptionsFrame.lua`), which `harness_on` already loads; the two
/// quest globals need their own windows — `MerchantFrame.xml` for the coin helpers both quest files
/// reuse and `ScrollTemplates.xml` for the kit `QuestFrame.xml` inherits from (the same chain
/// `quest_tests`/`questlog_tests` load). `SHOW_BUFF_DURATIONS` arrives with the bar it re-anchors
/// (1139), behind the two files `buff_tests` loads ahead of it — `Cooldown.xml` (every button's
/// child) and `ActionBar.xml` (`BENILLA_FALLBACK_ICON`), themselves behind `UIParent.xml`.
/// `TextStatusBar.xml` rides in ahead of them for the Status Bar Text row's consumer — the XP bar's numerals (1140).
fn interface_harness() -> UiScript {
let mut s = audio_harness();
s.set_screen_size(1024.0, 768.0);
@@ -1847,8 +1848,9 @@ fn defaults_resets_the_controls_page_to_registered_defaults() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Drive the window a few frames so the frame-late fits (the scroll body, the tab widths)
/// converge: rects resolve after Lua runs, so OptionsScroll_Fit answers one frame behind.
/// Drive the window a few frames so the scroll body's frame-late fit converges: rects resolve
/// after Lua runs, so `OptionsScroll_Fit` answers one frame behind. (The tab widths are not in
/// that set — 2028 put them on a one-shot `<OnShow>OptionsTab_FitWidth`.)
fn settle(s: &mut UiScript) {
for _ in 0..4 {
s.resolve();
@@ -1,5 +1,5 @@
//! The shipped `assets/ui/UIPanelTemplates.xml` + `assets/ui/OptionsFrameTemplates.xml` — the
//! reference's SHARED widget kit, driven the way an addon drives it.
//! The stock `Interface\FrameXML\UIPanelTemplates.xml` + our `assets/ui/OptionsFrameTemplates.xml`
//! — the reference's SHARED widget kit, driven the way an addon drives it.
//!
//! **These are not tests of a window.** Nothing benilla ships instantiates a single template in
//! either file: their only consumer is a third-party addon writing
@@ -238,7 +238,7 @@ fn a_panel_button_from_the_template_labels_and_paints() {
/// `UIPanelCloseButton` — 8 call sites across 4 corpus addons, and the one template here whose
/// whole point is its script: `HideUIPanel(this:GetParent())`, resolved at click time against
/// `UiPanels.xml`.
/// the chain's `UIParent.xml`.
#[test]
fn the_templated_close_button_hides_the_frame_it_sits_on() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -438,7 +438,7 @@ fn the_options_check_button_resolves_its_whole_inheritance_chain() {
/// real 1.12.1 client has, read from `reference/1.12-globals.tsv`.
///
/// Scoped to these two files on purpose: the rest of `assets/ui` is full of deliberately
/// benilla-shaped template names (`BenillaScrollBarTemplate`, `BenillaMacroPanelButtonTemplate`,
/// benilla-shaped template names (`BenillaScrollBarTemplate`, `BenillaScriptLogRowTemplate`,
/// `OptionsRedButtonTemplate`), and a whole-tree sweep would be asserting something else. These
/// two files make the opposite claim — *these are the reference's own names, which is why an addon
/// can find them* — so that claim is the one worth gating. A template renamed to something
@@ -4,9 +4,9 @@ use benilla_ui::script::{
};
/// Load one manifest entry into `s`, panicking on any loader error and returning the frame count
/// it materialized — the panel tests all load `UiPanels.xml` (decision 0084 §2's slot manager)
/// before the panel frame(s) under test, exactly as `benilla.toc`'s own order does, so
/// `ShowUIPanel`/`HideUIPanel` already exist when a frame's OnLoad/OnEvent references them.
/// it materialized — the panel tests all load `Interface\FrameXML\UIParent.xml` (decision 0084 §2's
/// slot manager) before the panel frame(s) under test, exactly as `benilla.toc`'s own order does,
/// so `ShowUIPanel`/`HideUIPanel` already exist when a frame's OnLoad/OnEvent references them.
///
/// This was a private disk-only reader until 1751's second window: `BankFrame.xml` is the
/// reference's own file off the player's chain now, and a reader that only knows `assets/ui`
@@ -30,7 +30,7 @@ fn frame_rect(quads: &[ExtractedQuad], w: f32, h: f32) -> benilla_ui::layout::Re
.unwrap_or_else(|| panic!("no bare-frame quad sized {w}x{h}"))
}
/// Load the real `assets/ui/GossipFrame.xml` (the shipped gossip window) behind `UiPanels.xml`
/// Load the stock `Interface\FrameXML\GossipFrame.xml` behind `UIParent.xml`
/// into a bare engine and drive it with a synthetic gossip menu — the whole phase-3 chain minus
/// Bevy (decision 0081), now over the UIPanel slot manager (decision 0084): the hidden→shown
/// lifecycle on GOSSIP_SHOW goes through ShowUIPanel (landing the window at the left slot,
@@ -738,7 +738,7 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
}
/// Pin §4's pushable path: a higher-`pushable` occupant (loot's future pushable=7 row, already
/// registered in `UiPanels.xml`) gets pushed to the center slot rather than replaced when a
/// registered in `UIParent.xml`) gets pushed to the center slot rather than replaced when a
/// pushable=0 frame (merchant) wants the left spot (UIParent.lua l.734-741) — the synthetic
/// registrant the pin calls for, since no loot window ships yet. A bare `CreateFrame` with a
/// distinctive 50×50 size stands in for it: it has no visual layers, but `extract` still emits
@@ -1,5 +1,5 @@
//! The shipped `assets/ui/ActionBar.xml`'s performance ("ping") meter — the tinted bar in the
//! main bar's last empty recess — over the real files, never a stub.
//! The stock `Interface\FrameXML\ActionBarFrame.xml`'s performance ("ping") meter — the tinted bar
//! in the main bar's last empty recess — over the real files, never a stub.
//!
//! What these guard, in order: the ref geometry that puts the bar IN the recess (the whole point of
//! the slice); the LOW-strata draw order that makes it show *through* the bar art instead of over
@@ -398,8 +398,10 @@ fn the_level_line_names_the_family_and_is_untouched_without_one() {
// level line at all", because our deleted `assets/ui/PetPaperDollFrame.xml` declared
// `PetLevelText` with no `text=` and it started empty. The reference declares it
// `text="Level level race class"` (stock `PetPaperDollFrame.xml:70`) — a design-time
// placeholder, and not a GlobalStrings key, so the loader's `text=` lookup falls through to the
// literal and that literal is what a player sees in this state, un-replaced. Both spellings say
// placeholder, and not a GlobalStrings key. **A real 1.12 client draws that literal too**:
// `CSimpleFontString::LoadXML` substitutes the raw attribute when `FrameScript_GetText` comes
// back empty (`771029`-`771032 mov eax,esi`), exactly as `Button::LoadXML 0x778c31` does — so
// this is fidelity, not the loader divergence this comment used to call it. Both spellings say
// the same thing about the guard; this one says it about the file that ships.
let drawn = with_family(None);
assert!(
@@ -1,7 +1,7 @@
//! The shipped **questgiver window** open/close sound, driven engine-only (no Bevy): the real
//! `assets/ui/QuestFrame.xml` loaded behind `UiPanels.xml` and shown/hidden through the wire events.
//! The window-sound convention's machine check for the quest arc (decision 0090), the sibling of the
//! merchant/gossip/bag/loot sound tests.
//! `Interface\FrameXML\QuestFrame.xml` loaded behind `UIParent.xml` and shown/hidden through the
//! wire events. The window-sound convention's machine check for the quest arc (decision 0090), the
//! sibling of the merchant/gossip/bag/loot sound tests.
use benilla_ui::script::{QuestPanel, QuestState, ScriptValue, SoundRequest, UiScript};
@@ -1,7 +1,7 @@
//! The shipped **quest log window** driven end-to-end, engine-only (no Bevy): the real
//! `assets/ui/QuestLogFrame.xml` loaded behind `UiPanels.xml`/`MerchantFrame.xml` (the money helpers
//! its reward rows reuse) and fed a synthetic 8-entry log + a resolved detail — mirroring
//! `quest_tests.rs`/`bag_tests.rs`'s engine-only harness for the quest-log slice (decision 0088 arc).
//! `Interface\FrameXML\QuestLogFrame.xml` loaded behind `UIParent.xml`/`MerchantFrame.xml` (the
//! money helpers its reward rows reuse) and fed a synthetic 8-entry log + a resolved detail —
//! mirroring `quest_tests.rs`/`bag_tests.rs`'s engine-only harness for the quest-log slice (decision 0088 arc).
use benilla_ui::script::{
ExtractedQuad, PartyMemberInfo, PartyState, QuadContent, QuestItemView, QuestLogDetail,
@@ -843,15 +843,19 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
s.run("ToggleQuestLog()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// A bare FontString region has no Show/Hide/IsVisible in this engine (only Frames/Buttons do —
// `region.rs`'s method table) — the empty-state message is a toggled SetText, read back here.
// The empty log says the reference's QUESTLOG_NO_QUESTS_TEXT (GlobalStrings.lua:3225) through
// stock QuestLogNoQuestsText; "Your quest log is empty." was ours (1944).
// stock QuestLogNoQuestsText; "Your quest log is empty." was ours (1944). Regions carry the
// real `Show`/`Hide`/`IsShown`/`IsVisible` (0138), so the label's visibility is assertable
// too — and nothing ever hides this one: `QuestLogFrame.xml:427` is its only mention in the
// whole chain, so it stands behind the rows whether or not the log is empty.
assert_eq!(
s.eval::<String>("return QuestLogNoQuestsText:GetText()")
.unwrap(),
"No Active Quests"
);
assert!(s
.eval::<bool>("return QuestLogNoQuestsText:IsVisible()")
.unwrap());
assert!(!s.eval::<bool>("return QuestLogTitle1:IsVisible()").unwrap());
assert!(!s
.eval::<bool>("return QuestLogFrameAbandonButton:IsEnabled() ~= 0")
@@ -968,14 +972,28 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
// With choices present, the receive text reads "...also..." (QuestFrame.lua:461-462) — it
// anchors under choice row 1 (the left/odd column: index=2 is even -> anchorIndex-1 -> 1, this
// function's own logic). GetPoint() isn't wired for FontString regions in this engine (only
// Frame/Button — `region.rs` has no GetPoint), so the anchor CHAIN itself is verified via the
// mandatory capture-loop screenshot (this task's report), not introspected here.
// function's own logic). Regions carry `GetPoint` (1244), so the anchor CHAIN is introspected
// here rather than deferred to a screenshot: TOPLEFT of QuestLogItem1's BOTTOMLEFT, +3/-5.
assert_eq!(
s.eval::<String>("return QuestLogItemReceiveText:GetText()")
.unwrap(),
"You will also receive:"
);
assert_eq!(
s.eval::<String>(
"local p, rel, rp = QuestLogItemReceiveText:GetPoint() \
return p .. '|' .. rel:GetName() .. '|' .. rp"
)
.unwrap(),
"TOPLEFT|QuestLogItem1|BOTTOMLEFT"
);
assert_eq!(
s.eval::<(f64, f64)>(
"local _, _, _, x, y = QuestLogItemReceiveText:GetPoint() return x, y"
)
.unwrap(),
(3.0, -5.0)
);
// One fixed reward shows, chained under the receive text (its own SetPoint target).
assert!(s.eval::<bool>("return QuestLogItem3:IsShown()").unwrap());
@@ -648,9 +648,10 @@ mod tests {
/// reasoning that widget methods are not in `_G`. True, and it meant the report could not see
/// a widget method we had *not built*. `MerchantFrame.xml` read `0 engine` and
/// [`chain_readiness_report`] read CLEAN while the stock row's `<OnEnter>` called
/// `ShoppingTooltip1:SetMerchantCompareItem(...)`, which this engine does not have — so the
/// file loaded, every check passed, and hovering a vendor row would have raised in play. Both
/// instruments were right about what they measure. Neither measured the window.
/// `ShoppingTooltip1:SetMerchantCompareItem(...)`, which this engine did not have then (1802
/// built it) — so the file loaded, every check passed, and hovering a vendor row would have
/// raised in play. Both instruments were right about what they measure. Neither measured the
/// window.
///
/// **A remaining `<?>` in the `fx=` column is usually a LoadOnDemand addon**, not something to
/// build. `ClassTrainerFrame_Show`, `CraftFrame_Show`, `MacroFrame_SaveMacro`,
@@ -1881,8 +1882,9 @@ mod tests {
/// read by the next session as if it were live.
///
/// That is not a tidiness problem. Our copies DIVERGE from the reference deliberately, and the
/// divergence is what dies: `UiPanels.xml`'s `PanelTemplates_TabResize` carried a benilla-only
/// `return tabWidth` that the tab settle reads, and the reference's returns nothing.
/// divergence is what dies: our retired `UiPanels.xml` carried a `PanelTemplates_TabResize`
/// with a benilla-only `return tabWidth` that our tab settle read, and the reference's
/// returns nothing (both are gone — 1988, 1993).
///
/// The reverse direction — ours seated BELOW the chain's, so we silently override the
/// reference — is a real category too, and a wider audit than this gate.
@@ -1,6 +1,6 @@
//! The shipped **Reputation tab** (`assets/ui/ReputationFrame.xml`) and the **reputation watch
//! bar** (`assets/ui/ActionBar.xml`) driven end-to-end, engine-only (no Bevy) — the per-window test
//! module the skills/spellbook/bank files already establish.
//! The stock **Reputation tab** (`Interface\FrameXML\ReputationFrame.xml`) and the **reputation
//! watch bar** (`Interface\FrameXML\ActionBarFrame.xml`) driven end-to-end, engine-only (no Bevy) —
//! the per-window test module the skills/spellbook/bank files already establish.
//!
//! What it pins is the PAINT law, which is the half `benilla-ui`'s own
//! `script::reputation::tests` structurally cannot reach: that module drives the twelve globals and
@@ -376,10 +376,11 @@ fn the_pages_close_button_sits_where_the_reference_seats_it_and_closes_the_windo
// **"Close", not "CLOSE".** An XML `text=` attribute is a GlobalStrings LOOKUP, not a literal
// (`loader::Loader::resolve_text`, wow-re `rf28-typed-widget-loadxml.md` l.36 —
// `FrameScript_GetText 0x703bf0`), and `CLOSE = "Close"` (`GlobalStrings.lua:760`). This test
// read "CLOSE" while the page's list carried no `GlobalStrings.lua`: it was asserting our
// loader's miss-fallback (the LITERAL, a deliberate divergence for benilla-authored files),
// not the label the reference client draws. [`super::test_ui::CHARACTER_UI`] loads the
// player's own strings first, as the app does, so this is the real label now.
// read "CLOSE" while the page's list carried no `GlobalStrings.lua`: it was asserting the
// miss-fallback (the raw attribute), which is the REFERENCE's own arm too (`0x778c31` /
// `0x771032`), not the label a client with the strings loaded draws.
// [`super::test_ui::CHARACTER_UI`] loads the player's own strings first, as the app does, so
// this is the real label now.
let label = page_quads(&s)
.iter()
.find_map(|q| match &q.content {
@@ -1,7 +1,7 @@
//! The shipped **spellbook window** driven end-to-end, engine-only (no Bevy): the real
//! `assets/ui/SpellBookFrame.xml` loaded behind `Fonts.xml`/`UiPanels.xml`/`GameTooltip.xml`
//! (plus `ActionBar.xml` for the cross-window place test) and fed a small synthetic book —
//! mirroring `character_tests.rs`'s/`action_bar_tests.rs`'s harness (decision 0216 §8, slice 5).
//! `Interface\FrameXML\SpellBookFrame.xml` loaded behind
//! `Fonts.xml`/`UIParent.xml`/`GameTooltip.xml` (plus `ActionBar.xml` for the cross-window place
//! test) and fed a small synthetic book — mirroring `character_tests.rs`'s/`action_bar_tests.rs`'s harness (decision 0216 §8, slice 5).
use benilla_ui::script::{SpellBookState, SpellSlotView, SpellTabView, UiScript};
@@ -1,7 +1,7 @@
//! The shared StaticPopup engine (decision 0308 §3 — the ref's registry + Show + OnUpdate
//! machinery, transcribed in UiPanels.xml): the countdown/StartDelay/cancels/ESC laws the death
//! arc's dialogs ride. Entries here are inline test dialogs — the real entries (DELETE_ITEM,
//! ABANDON_QUEST, the death family) are covered by their features' own tests.
//! machinery, stock `Interface\FrameXML\StaticPopup.xml` since 1988): the
//! countdown/StartDelay/cancels/ESC laws the death arc's dialogs ride. Entries here are inline test
//! dialogs — the real entries (DELETE_ITEM, ABANDON_QUEST, the death family) are covered by their features' own tests.
use benilla_ui::script::UiScript;
+11 -12
View File
@@ -1,10 +1,10 @@
//! The shipped taxi-map window driven end-to-end, engine-only (no Bevy): the real
//! `assets/ui/TaxiFrame.xml` fed a synthetic two-node snapshot (the [`crate::ui_taxi`] feed's exact
//! output shape). Covers what only a runtime load exercises: the Lua parses and every referenced
//! global resolves (including the static node-button pool and the runtime-created route-line
//! textures — TaxiFrame.xml's own header note on both), `TAXIMAP_OPENED` shows the window with the
//! flight master's name and paints the node buttons at their pushed positions, a click on a node
//! drains through `TakeTaxiNode`, and `TAXIMAP_CLOSED` hides it.
//! `Interface\FrameXML\TaxiFrame.xml` fed a synthetic two-node snapshot (the [`crate::ui_taxi`]
//! feed's exact output shape). Covers what only a runtime load exercises: the Lua parses and every
//! referenced global resolves (including the static node-button pool and the runtime-created
//! route-line textures — TaxiFrame.xml's own header note on both), `TAXIMAP_OPENED` shows the
//! window with the flight master's name and paints the node buttons at their pushed positions, a
//! click on a node drains through `TakeTaxiNode`, and `TAXIMAP_CLOSED` hides it.
use benilla_ui::script::{ScriptValue, TaxiNodeType, TaxiUiNode, TaxiUiState, UiScript};
@@ -23,12 +23,12 @@ fn taxi_script() -> UiScript {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TaxiNodeOnButtonEnter's tooltip + SetTooltipMoney
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // BenillaErrorsFrame_AddMessage — DrawOneHopLines' refusal
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // `UIErrorsFrame:AddMessage` — DrawOneHopLines' refusal
// Three the reference's own TaxiFrame leans on that our transcription did not:
// · GlobalStrings — `ERR_TAXINOPATHS` is a GlobalString, and `AddMessage(nil)` draws an
// empty line rather than raising, so its absence is silent.
// · UIPanelTemplates (.lua then .xml) — `TaxiCloseButton` inherits `UIPanelCloseButton`,
// which lives there and NOT in our UiPanels.xml. Without it the close button loads as a
// which the chain declares there. Without it the close button loads as a
// bare Button with no handler and a click does nothing at all.
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
@@ -128,10 +128,9 @@ fn shipped_taxi_frame_drives_end_to_end() {
/// The "no single-hop destination" refusal (ref `DrawOneHopLines`, fired from `OnShow`): a map
/// with only the `Current` node (no `Reachable` neighbor at all) hits `numSingleHops == 0` and
/// calls `BenillaErrorsFrame_AddMessage` (TaxiFrame.xml's deviation 3 from the reference's
/// `UIErrorsFrame:AddMessage`) then hides the window — verified end-to-end since this is the one
/// call this engine has no precedent for outside this window (`ErrorsFrame.xml`'s own Lua seam,
/// not yet exercised from another shipped window's script).
/// calls `UIErrorsFrame:AddMessage(ERR_TAXINOPATHS, …)` (stock `TaxiFrame.lua:161-162`) then
/// hides the window — driven end-to-end because the message frame's Lua seam is what the
/// refusal is visible through.
#[test]
fn no_single_hop_destination_posts_the_error_and_closes() {
let mut s = taxi_script();
+1 -1
View File
@@ -665,7 +665,7 @@ pub(super) const BAG_UI: &[&str] = &[
// Our adapters over the reference's container files — the keyring tooltip wrapper and the
// three bag verbs 0561 shadows (`OpenBackpack`/`CloseBackpack`/`CloseAllBags`). It has to be
// AFTER `ContainerFrame.xml` and after the bar, which is why it is here and not up with
// UiPanels.xml.
// `UIParent.xml`.
"ContainerFrameAdapters.xml",
// `updateContainerFrameAnchors` measures every open bag against `BankFrame:GetRight()`
// (ContainerFrame.lua l.505) on EVERY open and close, so the bank window is not optional
@@ -1,5 +1,5 @@
//! `GameTooltipTemplate` as an ADDON sees it — the shipped `assets/ui/GameTooltip.xml` driven the
//! way the corpus drives it.
//! `GameTooltipTemplate` as an ADDON sees it — the stock `Interface\FrameXML\GameTooltip.xml`
//! driven the way the corpus drives it.
//!
//! **This is not a test of our tooltip window** (that is `tooltip_anchor_tests` /
//! `tooltip_compare_tests`, and they are the regression suite this refactor had to leave
@@ -1,12 +1,12 @@
//! The shipped **trainer window** driven end-to-end, engine-only (no Bevy): the real
//! `assets/ui/TrainerFrame.xml` — a client-sorted, collapsible **skill-line tree** with a **dropdown**
//! state filter and a draggable **scroll bar** (decisions 0247/0251) — loaded behind its deps
//! (`UiPanels.xml` + `UIDropDownMenu.xml` + `ScrollTemplates.xml` + `MerchantFrame.xml` for the
//! the stock money frames) and fed a synthetic service list. Covers what only a runtime load
//! exercises: the Lua parses and every referenced global resolves, the tree renders interleaved
//! header/service rows, a header click folds its group, the dropdown filter hides a state, the wheel
//! scrolls the list, the NPC name rides `arg1` into the title, the byte-exact GlobalStrings render, the
//! Train button gates on available-and-affordable, and the buy queues the row's spell id.
//! The reference's own `Blizzard_TrainerUI` addon — a client-sorted, collapsible **skill-line
//! tree** with a **dropdown** state filter and a draggable **scroll bar** (decisions 0247/0251) —
//! loaded behind its deps (`UiPanels.xml` + `UIDropDownMenu.xml` + `ScrollTemplates.xml` +
//! `MerchantFrame.xml` for the the stock money frames) and fed a synthetic service list. Covers
//! what only a runtime load exercises: the Lua parses and every referenced global resolves, the
//! tree renders interleaved header/service rows, a header click folds its group, the dropdown
//! filter hides a state, the wheel scrolls the list, the NPC name rides `arg1` into the title, the
//! byte-exact GlobalStrings render, the Train button gates on available-and-affordable, and the buy queues the row's spell id.
use benilla_ui::script::{
ExtractedQuad, QuadContent, ScriptValue, SoundRequest, TrainerAbilityReq, TrainerService,
@@ -1,4 +1,4 @@
//! `assets/ui/UIParent.xml`'s addon-facing helpers, driven from Lua the way an addon drives them.
//! Stock `Interface\FrameXML\UIParent.xml`'s addon-facing helpers, driven from Lua the way an addon drives them.
//!
//! The panel/ESC machinery in that file is covered by `panel_tests` and `escape_tests`; this is for
//! the loose functions the reference's `UIParent.lua` also defines, which benilla itself may never
@@ -603,13 +603,11 @@ fn raid_mark_clicks_through_the_nested_level() {
.unwrap();
s.mouse_button(sx as f32, sy as f32, "LeftButton", true);
s.mouse_button(sx as f32, sy as f32, "LeftButton", false);
// RED while the `SetRaidTarget` engine binding is absent, and deliberately left that way.
// `UnitPopup.xml`'s row calls `SetRaidTargetIcon(menu.unit, mark)`; the definition of that
// used to be ours, and since the migration it is the reference's own — `TargetFrame.lua`
// l.486-492 — whose whole body is `SetRaidTarget(unit, 0 or index)`. `SetRaidTarget` is an
// engine verb this house does not have yet, so the click raises
// "attempt to call global 'SetRaidTarget' (a nil value)" and no intent is queued. 1203: it
// gets built, never stubbed.
// l.486-492 — whose whole body is `SetRaidTarget(unit, 0 or index)`. That is the ENGINE
// verb, built by 1820 (`benilla-ui` `script/party.rs`), never the wrapper's name on our
// body: this asserts the intent it queues, and `:638` asserts nothing raised.
assert_eq!(
s.take_party_requests(),
vec![benilla_ui::script::PartyRequest::SetRaidTarget {
@@ -1211,8 +1209,8 @@ fn the_party_art_paints_over_the_bars() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The loot test's prefix (`loot_tests.rs`): PartyFrame's inline <Script> reads
// StaticPopupDialogs, which UiPanels.xml defines, and its per-member dropdown OnLoad walks the
// whole popup kit.
// StaticPopupDialogs, which the chain's `StaticPopup.xml` defines, and its per-member dropdown
// OnLoad walks the whole popup kit.
// GlobalStrings first, for the same reason `load_unit_frames` names it — the stock unit-frame
// files resolve it at LOAD (`CombatFeedback.lua` l.7-17, `UnitFrame.lua` l.1-6).
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
@@ -1865,10 +1863,9 @@ fn no_two_numeral_strings_overlap_on_any_frame() {
/// function UnitFrame_OnEnter() originalUnitFrame_OnEnter() … end
/// ```
///
/// Against a missing global that captured nil and raised on the first hover. Note these are
/// ADAPTERS, not renames: the reference's contract takes no arguments and reads `this`, ours takes
/// the frame explicitly, so a rename would have reached our body with a nil frame — reachable but
/// broken, which is worse than inert.
/// Against a missing global that captured nil and raised on the first hover. Our own adapter
/// (`BenillaUnitFrame_OnEnter(frame)`, which took the frame explicitly) went with the file 1751's
/// twenty-second window deleted: what runs here is the reference's own `this`-shaped body.
///
/// The same class as the Bagnon bag bug, and equally invisible to the corpus survey: it loads
/// addons and fires events, but never hovers anything.
@@ -17,7 +17,7 @@ fn load_popup_frames(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIParent.xml",
// `SmallMoneyFrame_OnLoad`, which UiPanels' own StaticPopup money rows call at load.
// `SmallMoneyFrame_OnLoad`, which the chain's StaticPopup money rows call at load.
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// `StaticPopupDialogs` and the `PanelTemplates_*` family, both of which FriendsFrame.xml
@@ -75,8 +75,9 @@ fn bake_strings(s: &UiScript) {
-- The newbie tooltip the stock unit frame raises on a HOVER, which every test in this file
-- takes on its way to a right-click. `UnitFrame_OnEnter` (ref `UnitFrame.lua:58-65`) runs
-- the detailed-tip branch whenever `SHOW_NEWBIE_TIPS == "1"` — 1.12's own default
-- (`UIOptionsFrame.lua:100`), which our `GameTooltip.xml:46` sets at load — and for a
-- player-controlled target that is not us it calls
-- (`UIOptionsFrame.lua:100`, a file benilla does not build; our `assets/ui/OptionsFrame.xml`
-- is the definer, and this harness loads no options file, so the branch is not reached
-- here). For a player-controlled target that is not us that branch calls
-- `GameTooltip_AddNewbieTip(PLAYER_OPTIONS_LABEL, 1, 1, 1, NEWBIE_TOOLTIP_PLAYEROPTIONS)`.
-- Both are nil in a bare harness, and `GameTooltip:SetText(nil)` raises. Verbatim from the
-- real `Interface\FrameXML\GlobalStrings.lua` off the 1.12.1 chain (l.3081 and l.2755).
@@ -476,7 +477,7 @@ fn solo_target_inspect_click_reaches_inspect_unit() {
// ── The PET menu (decision 1066; report B219) ───────────────────────────────────────────────────
/// The pet menu's own prefix. What joins the popup prefix is `UiPanels.xml` — the StaticPopup
/// The pet menu's own prefix. What joins the popup prefix is `StaticPopup.xml` — the StaticPopup
/// engine, because two of the four rows go behind a dialog, and since 1953 where the pet arc's
/// three dialogs are registered (they rode our pet-bar file until it became the reference's).
/// `Cooldown.xml` and
@@ -1,4 +1,4 @@
//! The always-up world-state readout (`assets/ui/WorldStateFrame.xml`) against the shipped XML —
//! The always-up world-state readout (stock `Interface\FrameXML\WorldStateFrame.xml`) —
//! report B190's second half, driven the way the client drives it: rows pushed, then
//! `UPDATE_WORLD_STATES` fired.
//!
+1 -1
View File
@@ -24,7 +24,7 @@
//! `CMSG_ACTIVATETAXIEXPRESS` with the full node chain; a click on the `Current` node is a
//! client-side no-op.
//!
//! `TaxiFrame.xml` (`crates/benilla-app/assets/ui/TaxiFrame.xml`) is the window; its header comment
//! Stock `Interface\FrameXML\TaxiFrame.xml` is the window; the toc's header comment
//! carries the three engine-forced deviations from the literal reference Lua (a static node-button
//! pool, the title reading an event arg, the error line's call target).
+1 -1
View File
@@ -1,5 +1,5 @@
//! The world-map data feed (decision 0203 phase 2) — the app half behind
//! `assets/ui/WorldMapFrame.xml` and benilla-ui's `script/worldmap.rs` bindings.
//! stock `Interface\FrameXML\WorldMapFrame.xml` and benilla-ui's `script/worldmap.rs` bindings.
//!
//! Two systems, the quest-log seam shape:
//! - [`load_world_map_ui`] (once, when the chain + VM + Map.dbc catalog all exist): builds the
+1 -1
View File
@@ -1,5 +1,5 @@
//! The always-up world-state readout (`WorldStateFrame`) — the app half behind
//! `assets/ui/WorldStateFrame.xml` and benilla-ui's `script/worldstate.rs` bindings.
//! stock `Interface\FrameXML\WorldStateFrame.xml` and benilla-ui's `script/worldstate.rs` bindings.
//!
//! This is report **B190**'s second half: the alliance↔horde progress UI. `WorldStateUI.dbc` says
//! which world states are *displayed*, where, and with what label; [`crate::world_state`] holds
+2 -1
View File
@@ -1,4 +1,5 @@
//! Drives the REAL `assets/ui/AuctionFrame.xml` through the engine (decision 1511) — the auction
//! Drives the reference's own `Blizzard_AuctionUI` addon through the engine (decision 1511; 1971
//! put it on the player's chain) — the auction
//! twin of `mail_frame.rs`: it loads the same file chain the app does (cut to the auction window's
//! dependency prefix), pushes a synthetic `AuctionState`, opens the window with the app's own
//! `AUCTION_HOUSE_SHOW`/`AUCTION_ITEM_LIST_UPDATE` events, and asserts the transcribed Lua actually
@@ -23,7 +23,7 @@ use benilla_ui::script::UiScript;
///
/// **Two of the templates under test are the REFERENCE's since 1860** — `FauxScrollFrameTemplate`
/// and `TabButtonTemplate` were ours until the dead-copy sweep, and both now come off the player's
/// chain from `Interface\FrameXML\UIPanelTemplates.xml`, seated below `UiPanels.xml` exactly as
/// chain from `Interface\FrameXML\UIPanelTemplates.xml`, seated below `UIParent.xml` exactly as
/// the manifest seats it. So this list carries the chain pair and the loader below has to be able
/// to READ a chain entry, which a disk-only provider under `assets/ui` cannot.
const FILES: &[&str] = &[
@@ -61,11 +61,11 @@ fn load_ui(script: &UiScript) {
/// The line an addon writes, and the globals it reads on the next one.
///
/// `TabButtonTemplate` (UiPanels.xml) is a `<Button>` carrying a `<Size>`, six `<Layers>` slices,
/// a `<ButtonText name="$parentText">`, a `<HighlightTexture name="$parentHighlightTexture">`, the
/// three state fonts and an `<OnUpdate>` — i.e. every decoration pass at once. Instantiating it as
/// `BenillaTemplateProbeTab` must publish `BenillaTemplateProbeTab*`, and must publish nothing
/// named after the template.
/// `TabButtonTemplate` (`UIPanelTemplates.xml`) is a `<Button>` carrying a `<Size>`, six `<Layers>`
/// slices, a `<ButtonText name="$parentText">`, a `<HighlightTexture
/// name="$parentHighlightTexture">`, the three state fonts and an `<OnUpdate>` — i.e. every
/// decoration pass at once. Instantiating it as `BenillaTemplateProbeTab` must publish
/// `BenillaTemplateProbeTab*`, and must publish nothing named after the template.
#[test]
fn a_real_template_reaches_an_addon_through_create_frame() {
let _data = benilla_formats::wow_data_or_skip!();
+3 -3
View File
@@ -1,6 +1,6 @@
//! Drives the REAL `assets/ui/MailFrame.xml` through the engine (decision 0544 P1/P2) — the mail
//! twin of `tradeskill_frame.rs`: it loads the same file chain the app does (cut to the mail
//! window's dependency prefix), pushes a synthetic inbox, opens the window with the app's own
//! Drives the stock `Interface\FrameXML\MailFrame.xml` through the engine (decision 0544 P1/P2) —
//! the mail twin of `tradeskill_frame.rs`: it loads the same file chain the app does (cut to the
//! mail window's dependency prefix), pushes a synthetic inbox, opens the window with the app's own
//! `MAIL_SHOW`/`MAIL_INBOX_UPDATE` events, and asserts the transcribed Lua actually paints — the
//! named regions exist, the rows populate from a fed `MailState`, the paging math is right, and the
//! unread/read row state tracks the wire `wasRead` flag.
+1 -1
View File
@@ -1,4 +1,4 @@
//! Drives the REAL `assets/ui/TalentFrame.xml` through the engine — the first test that
//! Drives the reference's own `Blizzard_TalentUI` addon through the engine — the first test that
//! executes the transcribed talent Lua at all (before this, the window's machinery only ever
//! ran inside a live client session; both of the director's day-one reports — no prereq branch
//! lines, no tooltip on first hover — slipped through that gap).
+7 -7
View File
@@ -1,10 +1,10 @@
//! Drives the REAL `assets/ui/TradeFrame.xml` through the engine (decision 0592 P1) — the trade twin
//! of `mail_frame.rs`: it loads the same file chain the app does (cut to the trade window's dependency
//! prefix), pushes a synthetic two-sided offer, opens the window with the app's own `TRADE_SHOW`
//! event, and asserts the transcribed Lua actually paints — the named regions exist, both columns
//! populate from a fed `TradeState`, the money coin trios render, and the accept glow tracks the
//! `TRADE_ACCEPT_UPDATE(my, his)` args. This is the machine gate for the XML (a Lua error / missing
//! global / wrong region name fails here); the director's eye judges only the *look*.
//! Drives the stock `Interface\FrameXML\TradeFrame.xml` through the engine (decision 0592 P1) — the
//! trade twin of `mail_frame.rs`: it loads the same file chain the app does (cut to the trade
//! window's dependency prefix), pushes a synthetic two-sided offer, opens the window with the app's
//! own `TRADE_SHOW` event, and asserts the transcribed Lua actually paints — the named regions
//! exist, both columns populate from a fed `TradeState`, the money coin trios render, and the
//! accept glow tracks the `TRADE_ACCEPT_UPDATE(my, his)` args. This is the machine gate for the XML
//! (a Lua error / missing global / wrong region name fails here); the director's eye judges only the *look*.
mod common;
+3 -3
View File
@@ -1,6 +1,6 @@
//! Drives the REAL `assets/ui/TradeSkillFrame.xml` through the engine — the crafting-book twin
//! of `talent_frame.rs`, and the first test that executes the transcribed tradeskill Lua at all
//! (the polish pass's own discovery: no suite loaded this file, so a runtime bug in the
//! Drives the reference's own `Blizzard_TradeSkillUI` addon through the engine — the crafting-book
//! twin of `talent_frame.rs`, and the first test that executes the transcribed tradeskill Lua at
//! all (the polish pass's own discovery: no suite loaded this file, so a runtime bug in the
//! Show/Update/dropdown code would only ever surface in a live session).
//!
//! The harness loads the same file chain the app does (`ui_script/mod.rs`'s list, cut to the
+2 -2
View File
@@ -162,8 +162,8 @@ impl Loader<'_> {
self.call(wrapper, "SetToplevel", true, dbg);
}
// `enableKeyboard="true"` — the XML half of the flag, which enables BOTH key kinds
// (`scripts-auto-enable.md` §1-2). The flag is now real and round-trips; key delivery is
// still not gated on it, which the method's own doc states rather than this warning.
// (`scripts-auto-enable.md` §1-2). The flag is real, and `script::keyboard`'s delivery
// walk is what reads it (1319).
if el.attr_bool("enableKeyboard") {
self.call(wrapper, "EnableKeyboard", true, dbg);
}
+21 -11
View File
@@ -428,18 +428,28 @@ impl Loader<'_> {
///
/// wow-re `system/ui/scratch/rf28-typed-widget-loadxml.md`: `<Button text=>` (l.36) and
/// `<FontString text=>` (l.115) BOTH resolve through `FrameScript_GetText 0x703bf0`, which
/// `scratch/inventory-change-failure-display.md` l.119 carves VERIFIED — it resolves the value as
/// a Lua global and, **when that global is not a string, returns a pre-seeded EMPTY string**
/// (`0x882748`), never the key name. That is why the reference's `text="LOGOUT"` renders "Logout",
/// and why `GlobalStrings.lua` runs before any XML (`ui_script::load_global_strings`).
/// `scratch/framescript.md` carves VERIFIED — it resolves the value as a Lua global and,
/// **when that global is not a string, returns a pre-seeded EMPTY string** (`0x882748`), never
/// the key name. That is why the reference's `text="LOGOUT"` renders "Logout", and why
/// `GlobalStrings.lua` runs before any XML (`ui_script::load_global_strings`).
///
/// **One deliberate divergence: a miss falls back to the LITERAL** rather than the reference's
/// empty string. benilla authors its own FrameXML (0068) and writes plain English in it —
/// `text="Send Mail"`, `text="No results found."` — which the reference's rule would blank. The
/// fallback is a strict superset for transcriptions (every real key resolves identically) and it
/// fails LOUDER than the reference: a **key-shaped** value that misses keeps its key on screen
/// *and* warns here — exactly the signal that was missing when the macro window shipped with
/// "CREATE_MACROS" across its title bar (0983 → 0991).
/// **The MISS falls back to the raw attribute, and that is the reference's own behaviour — not
/// a benilla divergence, which is what this comment used to claim.** `0x703bf0`'s empty return
/// never reaches a label: all three `text=` readers image-wide test it and substitute the raw
/// attribute string. `Button::LoadXML` at `0x778c07` — recorded in wow-re
/// `scratch/template-onload-replacement-law.md` §5, "a fallback to the raw attribute when the
/// lookup comes back empty (`0x778c31 mov eax,esi`)" — and byte-identically
/// `CSimpleFontString::LoadXML` at `0x771006` (`771012 mov esi,eax` … `771029 test eax,eax` /
/// `77102b je 0x771032` / `77102d cmp BYTE [eax],0` / `771030 jne` / `771032 mov eax,esi`), and
/// the third reader at `0x7292a6`, which expresses the same law through a copy
/// (`7292db mov al,[ebp-0x424]` / `7292e1 test al,al`, empty arm pushes `edi` = the raw).
///
/// So the reference's `PetPaperDollFrame.xml:70` `text="Level level race class"` really does
/// draw that placeholder until Lua overwrites it, and benilla's plain-English `text="Send
/// Mail"` renders for the same reason the reference's would. What IS ours is the extra signal:
/// a **key-shaped** value that misses warns here as well as keeping its key on screen — the
/// signal that was missing when the macro window shipped with "CREATE_MACROS" across its title
/// bar (0983 → 0991).
pub(super) fn resolve_text(&mut self, raw: &str, dbg: &str) -> String {
if let Ok(s) = self.lua().globals().get::<String>(raw) {
return s;
+3 -3
View File
@@ -46,9 +46,9 @@ pub const EQUIPMENT_BAG: i64 = -100;
/// What the cursor carries — the client's payload-mode global [0xb4d900] as a typed enum
/// (wow-re cursor-dragdrop-payload.md §1: 1 = live item, 3 = spell, **4 = pet action**, **8 =
/// macro**, **10 = stabled pet**; our Action arm is the client's bar-slot pickup; the
/// money/preview arms stay unbuilt). One transition seam for every surface, so sounds,
/// CURSOR_UPDATE, and lock display can't drift apart per window (decision 0216).
/// macro**, **10 = stabled pet**; our Action arm is the client's bar-slot pickup; the money arm
/// is mode 2 ([`CursorMoney`], 1962/1965) and the preview arm stays unbuilt). One transition seam
/// for every surface, so sounds, CURSOR_UPDATE, and lock display can't drift apart per window (decision 0216).
#[derive(Clone, Debug, PartialEq)]
pub enum CursorPayload {
Item(CursorItem),
+4 -3
View File
@@ -15,9 +15,10 @@
//! **`autoFocus` DOES focus on show** (corrected 2026-08-29, wow-re `editbox-selection-focus-law.md`
//! §6): the OnShow override tail-jumps `SetFocus` when nothing else holds focus, and the OnHide
//! mirror tail-jumps `ClearFocus`. The old "verified by absence" negative came from a `call`-only
//! census that could not see a tail-`jmp`. **Not implemented here yet** — see [`EditBoxState::
//! auto_focus`](crate::widget::EditBoxState::auto_focus) for why it waits on the attribute default.
//! The self-acquire-on-first-key half stands and is what this module implements.
//! census that could not see a tail-`jmp`. Both overrides are [`visibility_focus`] here, and the
//! construction default is `true` off the bytes (see
//! [`EditBoxState::auto_focus`](crate::widget::EditBoxState::auto_focus)). The
//! self-acquire-on-first-key half stands beside it.
//! - **Routing (§2):** a focused box processes and CONSUMES every key/char (`return 1` past the
//! guard); an unfocused non-autoFocus box ignores input. The override fires ONLY the specialized
//! scripts (Enter/Escape/Space/Tab/TextChanged/TextSet/focus), never generic `OnKeyDown`/`OnChar`.
@@ -20,12 +20,17 @@ fn text_quad(s: &UiScript) -> Option<String> {
// ── §1/§2 focus acquisition + routing ───────────────────────────────────────────────────────
/// **The self-acquire path**, path 2 of the two: a box that has had no show TRANSITION (created
/// already visible, so `visibility_focus` never runs for it) still takes the keyboard on the first
/// key or char event, and processes that same event. Path 1, focus-on-show, is
/// [`an_autofocus_box_takes_the_keyboard_when_it_is_shown`] below — this test's name used to assert
/// its absence, which stopped being true at decision 1686.
#[test]
fn autofocus_does_not_focus_on_show_but_self_acquires_first_event() {
fn an_autofocus_box_self_acquires_on_the_first_event() {
let mut s = script();
s.run(r#"E = CreateFrame("EditBox", "E"); E:SetAutoFocus(true)"#)
.unwrap();
// autoFocus does NOT focus on show — nothing owns the keyboard yet.
// No show transition has run for this box, so nothing owns the keyboard yet.
assert!(!s.has_keyboard_focus());
assert!(!s.eval::<bool>("return E:HasFocus()").unwrap());
+4 -2
View File
@@ -22,8 +22,10 @@
//! continuation** alone. The C dispatcher `0x4c2790(slot, flag)` takes the row only on `flag == 0`,
//! which no Lua binding reaches — the row click is the C `CLootButton`'s own behaviour — while
//! `LootSlot 0x4c2e70` passes `flag = 1`, whose arm refuses every slot but the one a bind confirm
//! is pending for. benilla has no `CLootButton`, so the click arm is `BenillaTakeLootSlot(slot)`
//! and `LootSlot` keeps the reference's meaning exactly.
//! is pending for. benilla builds that widget — [`crate::widget::FrameKind::LootButton`], decision
//! 1799 — so the row click's take runs where the reference runs it, in `script::button`'s click
//! path under the same no-modifier gate; `BenillaTakeLootSlot` is the Lua-reachable entry to the
//! same queue, and `LootSlot` keeps the reference's meaning exactly.
//!
//! The **coin pile is a synthesized client-side row** (first in the list when the loot carries gold):
//! `LootSlotIsCoin` is true for it, its `item` text is the formatted money amount, and `LootSlot(1)`
@@ -192,9 +192,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// Set/GetShadowOffset` are real entries on this class's table, not a courtesy. wow-re's
// registrar carve is explicit about the membership — *"Exposed on: FontString, Font object,
// EditBox, MessageFrame, ScrollingMessageFrame, SimpleHTML. NOT on Button"* — and names this
// class's own shims calling the shared implementations (`GetShadowColor 0x794810`). We shipped the block on two
// of the six and this is the third and fourth; SimpleHTML is a widget kind we do not have at
// all.
// class's own shims calling the shared implementations (`GetShadowColor 0x794810`). All six
// carry the block now — `SimpleHTML`'s is its own copy rather than `font_block::install`'s
// (`script/simplehtml/mod.rs`), because that class computes its own inter-block step.
//
// Demand is observed, not counted: `BigWigs/Plugins/Messages.lua:212` is
// `self.msgframe:SetFontObject(GameFontNormalLarge)` on a frame it has just given
@@ -252,9 +252,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// Set/GetShadowOffset` are real entries on this class's table, not a courtesy. wow-re's
// registrar carve is explicit about the membership — *"Exposed on: FontString, Font object,
// EditBox, MessageFrame, ScrollingMessageFrame, SimpleHTML. NOT on Button"* — and names this
// class's own shims calling the shared implementations (`GetShadowColor 0x792240`). We shipped the block on two
// of the six and this is the third and fourth; SimpleHTML is a widget kind we do not have at
// all.
// class's own shims calling the shared implementations (`GetShadowColor 0x792240`). All six
// carry the block now — `SimpleHTML`'s is its own copy rather than `font_block::install`'s
// (`script/simplehtml/mod.rs`), because that class computes its own inter-block step.
//
// Demand is observed, not counted: `BigWigs/Plugins/Messages.lua:212` is
// `self.msgframe:SetFontObject(GameFontNormalLarge)` on a frame it has just given
+7 -7
View File
@@ -75,12 +75,11 @@
//!
//! ## What is deliberately NOT here
//!
//! Five of `Model`'s own 23 — `SetFogNear 0x76f1e0`, `GetFogNear 0x76f2d0`, `SetFogFar 0x76f390`,
//! `GetFogFar 0x76f480`, `ClearFog 0x76f540`. No corpus caller, and their bodies are uncarved:
//! `ClearFog`'s exact effect on the colour/near/far triple is a guess until someone reads it, and
//! a guessed clear reads as knowledge. Named, not stubbed.
//! Nothing of `Model`'s own 23 — the fog near/far/clear set was the last hold-out and 2027 carved
//! it (`ClearFog 0x76f540` is `76f5c5 and [edi+0x3a4],-2`, bit 0 and nothing else, so the guess
//! that kept it out is gone; see the install below).
//!
//! Also absent, and correctly so: `SetCreature` and `SetCustomRace`. Neither string exists in
//! Absent, and correctly so: `SetCreature` and `SetCustomRace`. Neither string exists in
//! 5875 in any form (substring scan of the whole mapped image returns 0, against a positive
//! control of 27 hits for `Creature`) — they are later-expansion names, and publishing one would
//! be decision 1189's error.
@@ -704,8 +703,9 @@ fn playermodel_install(lua: &Lua) -> mlua::Result<()> {
// it inverted), plays it unless that id is already armed on bone slot 0, and then
// UNCONDITIONALLY sets `[+0x3e8] = 1` and `[+0x3ec] = now_ms + 100` — a 100 ms turn hold that
// the per-paint `0x505c50` expires. Every one of those is invisible to Lua (no getter reads
// them) and lands on a model renderer we have not built, so storing them here would be state
// nobody writes and nobody reads. The addresses are the pin for the day the renderer exists.
// them). The app's `<Model>` renderer (`benilla-app` `ui_models`, 2013/2019/2027) draws its
// pane off the file's own clock and does not model a turn hold, so storing the pair here
// would be state nobody writes and nobody reads. The addresses are the pin if it ever does.
m.set(
"SetRotation",
lua.create_function(|lua, (this, rad): (Table, Value)| {
@@ -299,42 +299,19 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
/// `system/ui/ui.md` l.544-556 summarises it), so what is missing here is never a mystery — it is a
/// deliberate not-yet. What the corpus actually asks for, and why each answer is what it is:
///
/// * **`OnKeyDown` / `OnKeyUp` / `OnChar`** (14 + 1 + 4 corpus sites over 13 addons) — **raising,
/// and now unblocked**: the delivery law was the second §5 this work dispatched, and it landed
/// (wow-re `scratch/frame-key-script-delivery.md`). benilla still has none of the machinery it
/// describes — `EnableKeyboard`/`IsKeyboardEnabled` now exist and the flag round-trips, but there
/// is still no keyboard index and no strata walk, and keys are routed straight to the focused
/// EditBox — so the names stay out until that exists, which is the whole rule above. What has to
/// get built, so the next pass does not have to re-derive it:
///
/// The frame must be in the hit-test root's **kind-0 / kind-1 bucket**
/// (`scratch/scripts-auto-enable.md` §1-2: `0x76af00(kind, …)`, `OnChar` = kind 0,
/// `OnKeyDown`/`OnKeyUp` = kind 1; XML `enableKeyboard` enables both, a `<Scripts>` block
/// auto-enables per handler, and Lua `SetScript` auto-enables **nothing** — so a Lua-only frame
/// is not in the bucket at all and its bound handler can never fire). The dispatcher then walks
/// **strata 8 → 0, level high → low, ties oldest-registration-first**, calling each frame's base
/// input virtual (`0x765f10` key-down → vtable `+0x60`; `0x765df0` char → `+0x5c`) and
/// **stopping at the first nonzero return**. `arg1` is a **key-name string** with no modifier
/// prefix, decoded from the *same* table the keybinding chord names use — verified byte-identical
/// over 273 codes, which is the opposite of the mouse case's two-table shape. `OnChar` gets the
/// literal character, UTF-8.
///
/// Two findings that will bite whoever implements it. The consumption gate is **existence, not
/// handling** — a 1.12 handler cannot signal "handled" (the fire's return is discarded at all
/// three sites), so merely having a key script bound suppresses the key's binding; and
/// asymmetrically, a frame with **only** an `OnKeyUp` script consumes every key-down and runs
/// nothing. And `CGWorldFrame` sits at **strata 0 / level 0** — last in the walk — which is
/// precisely why any keyboard-enabled frame pre-empts the entire binding system.
///
/// (The old gloss here called `0x76bba0` a "frame-script pre-gate" walking the index, after
/// `scratch/keybinding-dispatch-law.md` §1. That is refuted: `0x76bba0` is `CSimpleFrame::OnKeyUp`,
/// one frame's base virtual. The walk is one level up, in the dispatcher.)
/// * **`OnKeyDown` / `OnKeyUp` / `OnChar`** (14 + 1 + 4 corpus sites over 13 addons) — **accepted**
/// since decision 1319 built the delivery walk ([`crate::script::keyboard`], whose module doc is
/// the mechanism: the kind buckets, the strata 8→0 order, the key-name table, and the
/// existence-not-handling consumption gate). They were this list's standing exception; the rule
/// above let them in the moment something fired them.
/// * **`OnCursorChanged`** (4 sites over 3 addons — all of them the Era `ScrollingEdit_OnCursorChanged`
/// auto-scroll idiom) — **raising.** It is the EditBox's own slot (RF-28 `+0x428`), fired by the
/// caret flush `0x77da80` with **four float caret-POSITION args**, and caret geometry is the one
/// thing this engine deliberately does not have: text is measured host-side. Accepting it would
/// hand every caller four zeros, which for its single idiom means a scroll box that silently
/// never follows the caret.
/// auto-scroll idiom) — **raising**, by this list's own rule: nothing fires it. It is the
/// EditBox's own slot (RF-28 `+0x428`), fired by the reference's caret flush `0x77da80` with
/// **four float caret-POSITION args**. The geometry itself is no longer the obstacle — the paint
/// seam computes `caret_row`/`caret_x` off the host-answered advance table
/// ([`crate::script::editbox::seam`]) — but that is a per-extract recompute with no event edge,
/// so there is no counterpart to the flush to fire from. Accepting the name before building one
/// would mean a scroll box that silently never follows the caret.
/// * **`OnAttributeChanged`** (1 site, `Roid-Macros`) — **raising, permanently.** It is 2.0's secure
/// frame/attribute system; there is no such slot in any 1.12 resolver. That addon is asking for a
/// later client and should hear so.
@@ -47,7 +47,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
)?;
// GetPoint([n]) → point, relativeTo, relativePoint, xOfs, yOfs — the n-th (1-based, default
// first) anchor. relativeTo is nil when the target is the screen root (the client returns
// UIParent there; benilla has no UIParent wrapper yet — stated, a consensus-list call).
// UIParent there; ours is the distinct `script::SCREEN` sentinel, which has no wrapper of its
// own — the arena's `UIParent` frame is a different handle — stated, a consensus-list call).
m.set(
"GetPoint",
lua.create_function(|lua, (this, n): (Table, Option<i64>)| {
@@ -233,9 +234,13 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
)?;
// GetEffectiveScale() — the frame's real effective scale (parentScale · ownScale, the arena's
// propagated product). benilla has no uiScale CVar, so the root factor is 1; a SetScale'd
// subtree (the windowed world map) reports its true factor, and the reference's
// `GetCursorPosition()/GetEffectiveScale()` transcriptions convert screen→local correctly.
// propagated product). The ROOT factor is 1, and that is not because benilla lacks a `uiScale`
// CVar (it has one, `cvars.rs`, default 0.9): the dial is applied at the RASTER seam — it sets
// how many UI units tall the virtual screen is, `768/uiScale` (`ui_script::seam_scale`, 0584) —
// rather than as a scale on UIParent. So every coordinate the VM hands Lua is already in those
// units, `GetCursorPosition()` included, and the reference's
// `GetCursorPosition()/GetEffectiveScale()` transcriptions convert screen→local correctly with
// a root of 1; a SetScale'd subtree (the windowed world map) still reports its true factor.
m.set(
"GetEffectiveScale",
lua.create_function(|lua, this: Table| {
+8 -5
View File
@@ -887,11 +887,14 @@ pub(super) fn virtual_span(model: &Model, rh: RegionHandle) -> (f32, f32) {
// **The floor applies to a KNOWN extent, and a pending measure is not one.** On the
// reference every extent is known — the getter measures inline — so `0.0` never comes
// back and the question never arises. Ours can be *waiting*, which is not a size but
// the absence of an answer, and several of our own convergence drivers read exactly
// that: `BenillaGossipRow_Resize`, the tab fit and the quest panel all guard
// `if h <= 0 then return end` and re-run from `OnUpdate` until the round-trip lands.
// Flooring a pending measure to one unit hands them a number, so they stop waiting and
// seat every row at 3px (`shipped_gossip_frame_drives_end_to_end` catches it).
// the absence of an answer. **Where that state still lives is a VM with no measurer
// installed** (`script::measure`'s is optional): the app seats one at the load edge
// (2028), so no in-app caller observes a pending measure any more, and the three
// convergence drivers this used to name went with their windows —
// `BenillaGossipRow_Resize` (gossip, 1751), our tab fit (1993/2028) and our quest
// panel (1944). Flooring a pending measure to one unit hands a measurer-less caller a
// number where it should read "not yet"; the surviving `OnUpdate` fits
// (`OptionsScroll_Fit`, `BenillaScroll_ResizeChild`) guard on RECTS, not on this.
//
// A genuinely EMPTY string is a different thing: its extent is known and it is zero, so
// it floors — which is the case the reference's floor exists for. The layout sweep
@@ -242,8 +242,8 @@ fn the_string_setters_gate_their_argument_and_a_number_is_a_string() {
/// - **no superset** — a name the reference does not have on a table must not resolve there
/// (1189: a name we have and the reference lacks routes an addon down a path the real client
/// never takes);
/// - **the seven we deliberately do not build must stay absent**, so "unbuilt" cannot quietly
/// become "stubbed" without this test being edited to say so.
/// - **the `UNBUILT` set must stay absent** — it is empty since 2027, so "unbuilt" cannot
/// quietly become "stubbed" without this test being edited to say so.
#[test]
fn the_two_model_tables_are_the_references_own() {
/// `Model` — `CSimpleModel`, table `0x878948`, 23 entries, in table order.
@@ -1452,8 +1452,8 @@ fn a_frames_type_chain_matches_the_roster() {
("Minimap", "Minimap", &["Minimap", "Frame", "Region"]),
// `PlayerModel` derives from `Model`, which is why `SetUnit` on a portrait pane finds
// `SetCamera` too (wow-re `ui/scratch/widget-type-identity.md` §6) — and `DressUpModel`
// derives from it in turn: the roster's maximum depth, 5 (1969; the unbuilt `TabardModel`
// would be the other).
// derives from it in turn: the roster's maximum depth, 5 (1969; `TabardModel` is the
// other depth-5 chain — built by 1977).
(
"PlayerModel",
"PlayerModel",
@@ -1043,11 +1043,10 @@ fn the_size_getters_take_the_author_first_then_the_natural_width_and_wrapped_hei
/// The floor, and the state the reference does not have. `0x772930`/`0x772a60` end in a one-unit
/// clamp, so a genuinely empty string reads back **1**, never `0.0`. But a measure that has not
/// LANDED is not an extent at all — it is our async round-trip, which the reference has no
/// equivalent of — and our own convergence drivers (`BenillaGossipRow_Resize` and the tab fit,
/// which guard `if h <= 0 then return end` and re-run from `OnUpdate`) read that zero as
/// "not yet". Flooring it would tell them to stop waiting. So: floor a known extent, not the
/// absence of one.
/// LANDED is not an extent at all — it is the host round-trip a VM with **no measurer installed**
/// still takes, which the reference has no equivalent of. A caller that guards
/// `if h <= 0 then return end` reads that zero as "not yet"; flooring it would tell them to stop
/// waiting. So: floor a known extent, not the absence of one.
#[test]
fn an_empty_string_reads_back_one_unit_and_a_pending_measure_reads_back_zero() {
let mut s = script();
@@ -93,9 +93,9 @@ fn the_flag_round_trips_and_defaults_off() {
///
/// `enableKeyboard` used to be warned here on the same principle — implementing one clause is not
/// a licence to go quiet about the other. It now lands on a real `EnableKeyboard`, so the warning
/// is gone and this asserts the flag instead. What is still NOT built is key *delivery*, which the
/// method's own doc states; the flag and the delivery are separable in the reference too
/// (`frame-key-script-delivery.md` §3.2).
/// is gone and this asserts the flag instead. Key *delivery* reads the same flag
/// (`script::keyboard`'s walk, 1319); the flag and a handler stay separable in the reference too
/// (`frame-key-script-delivery.md` §3.2), which is why this asserts the flag alone.
#[test]
fn the_xml_toplevel_and_enable_keyboard_attributes_both_reach_their_methods() {
let s = script();
+9 -13
View File
@@ -60,28 +60,24 @@ pub struct EditBoxState {
/// Selection end (`E+0x360`). A non-empty selection is `sel_start != sel_end`; every insert
/// replaces it first (RF-0082 §3).
pub sel_end: usize,
/// `autoFocus` (`flags@E+0x318` bit0). Two acquisition paths, and benilla implements only the
/// second:
/// `autoFocus` (`flags@E+0x318` bit0). Two acquisition paths, and both are built:
///
/// 1. **On SHOW** — the box self-focuses when it becomes visible, gated on nothing else holding
/// focus. The EditBox's own OnShow vtable override (`0x81c910` slot +0x30, `0x77a750`) fires
/// the Lua handler and then tail-jumps `SetFocus`:
/// `if ([0xcf4dc8] == 0 && (flags & 1)) jmp 0x77e3d0` @`0x77a76d`. The mirror slot +0x34
/// (`0x77a780`) tail-jumps `ClearFocus`, so **hiding an edit box releases the keyboard**.
/// Ours is [`crate::script::editbox`]'s `visibility_focus`.
/// 2. **On the first key/char event** while nothing is focused — the self-acquire guard; the box
/// grabs focus and processes that same event.
///
/// **This corrects what stood here.** Both this comment and wow-re's own `ui.md`/RF-0082 said
/// autoFocus does NOT focus on show, "verified by absence" — a census written over `call` alone,
/// which cannot see a tail-`jmp`. A `(call|jmp)` census finds the eleventh site (wow-re
/// `editbox-selection-focus-law.md` §6, 2026-08-29, dispatched from benilla's login work). Path 1
/// is a real behavioural gap here, not just a stale sentence.
///
/// **`false` at construction is OUR value, not a verified one.** The client's construction-time
/// bit0 is unrecorded, and every `autoFocus` in the shipped 1.12.1 chain is `="false"` — ten
/// opt-outs, no opt-ins — which is the authoring signature of a default that is ON. Out to wow-re;
/// until it answers, this default and the missing path 1 are deliberately left alone together,
/// because turning path 1 on under a wrong default is what would actually break something.
/// **Both corrections landed together, decision 1686 (2026-08-29).** wow-re's own
/// `ui.md`/RF-0082 had published "autoFocus does NOT focus on show, verified by absence" off a
/// census written over `call` alone, which cannot see a tail-`jmp`; a `(call|jmp)` census finds
/// the eleventh site (`editbox-selection-focus-law.md` §6). The construction default came with
/// it and is byte-read, not chosen — see [`Default`]'s `flags = 1` note below. Every
/// `autoFocus` in the shipped 1.12.1 chain is `="false"`, ten opt-outs and no opt-ins, which is
/// the authoring signature of exactly that default.
pub auto_focus: bool,
/// `multiLine` (bit1): Enter inserts a newline (rather than firing `OnEnterPressed`) and `\n` is
/// accepted into the buffer.
+5 -5
View File
@@ -90,7 +90,7 @@ pub enum FrameKind {
/// ([`KindState::Model`], shared with [`FrameKind::Model`]); the pixels are the app renderer's.
/// `CGCharacterModelBase` adds `0x1c` bytes of members over `CSimpleModel` — the turn-animation
/// flag/expiry at `+0x3e8`/`+0x3ec` that `SetRotation` arms — and those are **not modeled**:
/// no getter reads them, and they drive a shuffle animation on a renderer we have not built.
/// no getter reads them, and the app's `<Model>` renderer draws no shuffle animation.
/// `script::modelframe`'s `SetRotation` carries the addresses.
PlayerModel,
/// `DressUpModel` — `CGDressUpModelFrame` (`Ui\DressUpModelFrame.cpp`, factory `0x495c00`,
@@ -357,10 +357,10 @@ pub const TOOLTIP_WRAP_WIDTH: f32 = 260.0;
/// table owns it*, and `SetUnit`'s single pooled string `0x84f22c` is referenced by two entries in
/// two different tables. Ownership now comes from a dword-reference count over the name's VA.
///
/// **Seven of `Model`'s 23 are not published yet** — `AdvanceTime 0x76eca0`,
/// `ReplaceIconTexture 0x76ed70`, `SetFogNear 0x76f1e0`, `GetFogNear 0x76f2d0`,
/// `SetFogFar 0x76f390`, `GetFogFar 0x76f480`, `ClearFog 0x76f540` — named here rather than
/// stubbed (decision 1134 §4). They have no corpus caller and their bodies are uncarved.
/// **All 23 of `Model`'s are published** (decision 2027 carved the last seven — `AdvanceTime`,
/// `ReplaceIconTexture` and the fog near/far/clear set). `script::tests::modelframe`'s `UNBUILT`
/// array is the live count and is empty; the wall around it is what notices a name arriving or
/// leaving (1134 §4's naming rule, with nothing left to name).
#[derive(Clone, Debug, PartialEq)]
pub struct ModelState {
/// The M2/MDX path last given to `SetModel`, or `None` after `ClearModel` / before any set.
+8 -9
View File
@@ -263,15 +263,14 @@ pub struct Frame {
/// `enableKeyboard`) — the kind-0/kind-1 bucket membership `0x76af00` writes as
/// `[frame+0xcc] |= 1<<kind`.
///
/// Default **false**, the client's own default. **The flag round-trips; key DELIVERY is not
/// gated on it yet** — the same shape [`Self::mouse_wheel_enabled`] shipped in (1198), and for
/// the same reason: the machinery it gates (the strata 8→0 walk, the kind buckets, the
/// `OnKeyDown`/`OnKeyUp`/`OnChar` script kinds) does not exist here, so gating on the flag
/// would change nothing while pretending otherwise. wow-re's
/// `scratch/frame-key-script-delivery.md` §3.2 is explicit that the two are separable:
/// `EnableKeyboard(true)` on a script-less frame "puts it in the walk where it is called and
/// declines — transparent to everything downstream", so **being enabled is not being a
/// handler**, and storing the flag alone is the faithful half rather than a stub of the whole.
/// Default **false**, the client's own default. **This is the field key delivery gates on**:
/// [`crate::script::keyboard`]'s walk builds its candidate set from
/// `effective_visible && keyboard_enabled`, then orders it strata 8→0 / level high→low /
/// oldest-registration-first (built 2026-08-14, decision 1319). wow-re's
/// `scratch/frame-key-script-delivery.md` §3.2 is explicit that the flag and a handler are
/// separable: `EnableKeyboard(true)` on a script-less frame "puts it in the walk where it is
/// called and declines — transparent to everything downstream", so **being enabled is not
/// being a handler**.
pub keyboard_enabled: bool,
/// Clamp-to-screen (`SetClampedToScreen` / XML `clampedToScreen` — the client's geometry
/// flags **bit4**, applied inside rect assembly `0x767a20`, wow-re `layout.md`): the layout
+2 -2
View File
@@ -452,8 +452,8 @@ impl WidgetArena {
}
}
/// `EnableKeyboard` (`0x776f90`) — kind-0/kind-1 bucket membership. Stored and answered; the
/// key path is not gated on it yet (see `crate::widget::WidgetState::keyboard_enabled`).
/// `EnableKeyboard` (`0x776f90`) — kind-0/kind-1 bucket membership. Stored and answered, and
/// it is what [`crate::script::keyboard`]'s delivery walk filters on (1319).
pub fn set_keyboard_enabled(&mut self, h: FrameHandle, enabled: bool) {
if let Some(f) = self.frame_mut(h) {
f.keyboard_enabled = enabled;
+1 -1
View File
@@ -98,7 +98,7 @@ impl Default for MatAnimTable {
impl MatAnimTable {
/// Allocate a slot (1-based; 0 is the identity row). `None` when the table is full — the
/// caller then simply doesn't register, and the batch stays frozen at its built seed: a
/// degraded look only a >511-material session could see, never a wrong pixel.
/// degraded look only a >2047-material session could see, never a wrong pixel.
pub fn alloc(&mut self) -> Option<u16> {
if let Some(slot) = self.free.pop() {
return Some(slot);
@@ -86,9 +86,9 @@ impl GlobalSeqDrive {
})
}
/// Pause/resume the joint writes (the doodad draw gate, and the booth park — a sleeping
/// booth camera renders nothing, so its scene's channels hold). While paused the joints
/// hold their last pose; resume lands on the anchored cursor with nothing to catch up.
/// Pause/resume the joint writes. One caller: the doodad draw gate
/// (`doodad_anim`'s `set_paused(!drawn)`). While paused the joints hold their last pose;
/// resume lands on the anchored cursor with nothing to catch up.
pub fn set_paused(&mut self, paused: bool) {
self.paused = paused;
}