the raid and reputation panes go stock, and a faction header folds

- the raid and reputation panes are the reference's own files: two manifest lines replace our `RaidFrame.xml` (83 of 84 frame names were already the reference's; the tear-off pullouts arrive) and one replaces our `ReputationFrame.xml`; the raid swap surfaced a loader defect (a font object inherited one level down was the 26 unexplained warnings), rehomed `RaidFrame_LoadUI` and two popup dialogs, and reproduces one reference DEFECT: an emptied lockout list keeps its last row painted (1874); the reputation swap moved the WATCH BAR out of `ActionBar.xml`, where it would have shadowed the chain's own, and exposed an ENGINE gap: `CollapseFactionHeader`/`ExpandFactionHeader` fire `UPDATE_FACTION` (`0x4d5dab`) and ours fired nothing, so with the stock pane repainting on that event alone, a fold painted nothing and the header kept its minus — invisible while our own file repainted itself (1875)
- a wanted poster is a questgiver on the wire as a creature is, and benilla never asked about one; the RE round came back in two halves: the reference DOES send `CMSG_QUESTGIVER_STATUS_QUERY` for a GameObject, gated on `GAMEOBJECT_FLAGS` bit 2, seen in 20 of 62 sniffed sessions — and it can never render the answer (the status handler's typemask excludes a GameObject, its status slot lies past the object's end, and no poster, sign or plaque carries attachment 18); vmangos DOES answer, so the sweep gains a GameObject arm on the reference's own gate and the apply side refuses a non-unit guid, without which a gold `!` would hang over a poster no 1.12 client draws; two shipped claims fall with it (`0x5eb3c0` is the FULL re-query sweep; a status never suppresses a GameObject's minimap dot), and `WOW_PROBE_GOQUEST` at the Goldshire wanted poster reads the server's 1 then 5, both refused (1872)
- the money entry comes off the chain: Trade, Mail and Auction switch to the reference's `MoneyInputFrameTemplate` and its verbs, and our `BenillaMoneyInput_*` kit retires — it was the exact answer while benilla fired `OnTextChanged` synchronously inside `SetText`, and the engine now raises it deferred and coalesced, matching `0x77d3e0` (1831), so the reference's `expectChanges` guard works and our bracket flag was a duplicate; most of the 430 deleted lines are verbatim art — all three windows carried an inline copy of the template's three edit boxes, AuctionFrame a whole second copy; one visible change: TradeFrame now makes the reference's own `MoneyInputFrame_SetMode(this, "compact")` call, which tightens the trade money boxes (Gold 58→56 wide, Silver's offset 26→22, Copper's 16→11), and two stale claims in the kit's comments are corrected, and a verb nothing called goes (1882)
- the `UIFrameFlash` family is transcribed — six FrameXML names from `UIParent.lua`, which we do not load — unblocking SpellBookFrame and HelpFrame, which called it into silence; re-arming a flashing frame is a RETURN and `-1` flashes forever (1879); UIParent is recounted — `UpdateNameplates` and the `*_Show`/`*_Hide` family around it are FrameXML, not engine, missed by a survey that skipped the LoadOnDemand addons — so its engine surface is four verbs, not a wall: `GetLocale` answers `enUS`, `IsMacClient` answers nil (the whole verb on the PC build), and `CheckReadyCheckTime` and `RequestBattlefieldPositions` are the two it waits on (1880); and `BasicControls.xml` comes off the chain by pure deletion — ours differed by nothing but line-wrapping and one comment — with `FailedAddOnLoad` and `UIParentLoadAddOn` moving to `UIParent.xml`, their reference home, below their four callers (1881)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:50:12 +07:00
co-authored by Claude Fable 5.1
parent 94cc4429eb
commit 4ad347cec0
49 changed files with 1604 additions and 3127 deletions
+14 -337
View File
@@ -1241,238 +1241,11 @@
ExhaustionTick.timer = nil
end
-- ══ The reputation WATCH BAR (ref-ReputationFrame.xml l.869-1036 + ref-ReputationFrame.lua
-- l.3-14 and l.161-270) ═══════════════════════════════════════════════════════════════
--
-- **THE PLACEMENT IS A DELIBERATE CALL, and this is its justification.** The reference
-- declares `ReputationWatchBar` and defines its update, its two text verbs, MAX_PLAYER_LEVEL
-- and FACTION_BAR_COLORS in `ReputationFrame.{xml,lua}`, because FrameXML loads MainMenuBar
-- and ReputationFrame in one breath and neither can exist without the other. benilla's
-- manifest is not one breath: this file is entry 94, `ReputationFrame.xml` is 213, and the
-- engine's per-window test harnesses deliberately load FILE SUBSETS — the XP bar's harness
-- loads no character window, the character window's loads no action bar.
--
-- Every frame `ReputationWatchBar_Update` touches except the watch bar itself is declared
-- in THIS file: MainMenuExpBar, MainMenuBarMaxLevelBar, ExhaustionTick, MainMenuBarArtFrame
-- and MainMenuBar. So does every frame that reads the bar back: `ExhaustionTick_Update`
-- below consults `ReputationWatchBar:IsShown()` twice, which is the reference's own pair of
-- guards. Splitting the bar's FRAME from the bar's UPDATE across a 120-entry manifest gap
-- would leave each subset holding half a mechanism and would put a nil-guard on every one
-- of those sites — the exact "special case to dodge a problem" shape the contract forbids.
-- The bar therefore stays whole, in the file that owns its collaborators, and
-- `ReputationFrame.xml` — which owns the PANE — reaches `ReputationWatchBar_Update()` by
-- name at click time, the direction that always resolves.
--
-- What DID move is the half-mechanism. `BenillaMaxLevelBar_Update` covered "the
-- no-watched-faction branch only" of the reference's function, parked on the rail frame
-- because the watch bar was driven by nothing; it is gone, replaced in place by the whole
-- `ReputationWatchBar_Update(newLevel)`. `MainMenuBarMaxLevelBar` accordingly carries NO
-- scripts any more — the reference's rail carries none either, because this function is its
-- only driver — and the event fan that sat on it is now the watch bar's own OnLoad/OnEvent,
-- which is where the reference puts it.
MAX_PLAYER_LEVEL = 60 -- ref-ReputationFrame.lua l.14 (with the ref's own "Hard coded =(")
-- The eight rank colours, quoted from ref-ReputationFrame.lua l.3-12 — a table of the
-- client's own rgb facts, which have no other value they could take. Indexed by the 1-based
-- standingID both `GetFactionInfo` and `GetWatchedFactionInfo` report (benilla-ui
-- `script::reputation`), so the pane's 15 bars and this one bar can never disagree about a
-- colour. Declared HERE rather than in ReputationFrame.xml (its reference home) for the
-- placement reason above: this file must be able to index it with no reputation file
-- loaded, and ReputationFrame.xml cannot be loaded without this one.
FACTION_BAR_COLORS = {
[1] = { r = 0.8, g = 0.3, b = 0.22 },
[2] = { r = 0.8, g = 0.3, b = 0.22 },
[3] = { r = 0.75, g = 0.27, b = 0 },
[4] = { r = 0.9, g = 0.7, b = 0 },
[5] = { r = 0, g = 0.6, b = 0.1 },
[6] = { r = 0, g = 0.6, b = 0.1 },
[7] = { r = 0, g = 0.6, b = 0.1 },
[8] = { r = 0, g = 0.6, b = 0.1 },
}
-- THE TWO POSTURES the watched strip can take, as data. `ref-ReputationFrame.lua`
-- l.184-230 is the specification and every number below is quoted from it; the shape is
-- ours, because the reference's two arms differ in exactly four numbers and which of the
-- two end-art sets is up, and that is a table and one application rather than two
-- hand-written branches that have to be diffed to be read.
--
-- STACKED is the below-max-level posture: an 8px strip sitting 3 above MainMenuBar, wearing
-- the UI-ReputationWatchBar end art, with the XP bar still underneath it. REPLACING is the
-- max-level one: 13px on MainMenuBar's own TOP, wearing the dwarf XP-bar art instead so the
-- strip reads as the bar it took over from.
BENILLA_WATCHBAR_POSTURE = {
stacked = { height = 8, seat = "BOTTOM", seatY = -3, textY = 3, art = "ReputationWatchBarTexture" },
replacing = { height = 13, seat = "TOP", seatY = 0, textY = 1, art = "ReputationXPBarTexture" },
}
-- Seat the strip in one posture and take the other's art down with it. Both end-art sets
-- are four 256-wide slices of the 1024 strip, named `<set>0` .. `<set>3`.
function BenillaWatchBar_SetPosture(stacked)
local wear, drop = BENILLA_WATCHBAR_POSTURE.replacing, BENILLA_WATCHBAR_POSTURE.stacked
if stacked then
wear, drop = drop, wear
end
ReputationWatchStatusBar:SetHeight(wear.height)
ReputationWatchBar:ClearAllPoints()
ReputationWatchBar:SetPoint(wear.seat, MainMenuBar, "TOP", 0, wear.seatY)
ReputationWatchStatusBarText:SetPoint("CENTER", ReputationWatchBarOverlayFrame, "CENTER", 0, wear.textY)
for slice = 0, 3 do
getglobal(wear.art .. slice):Show()
getglobal(drop.art .. slice):Hide()
end
end
-- The XP strip and the rested tick that rides it go up and down together: with no strip
-- there is nothing for the tick to sit on. Every site the reference hides the strip from it
-- also hides the tick, and `ExhaustionTick_Update` enforces the same rule from the other
-- side on every event (its own last statement) — so it is one verb, not a pairing three
-- call sites have to remember.
function BenillaExpStrip_SetShown(shown)
if shown then
MainMenuExpBar:Show()
else
MainMenuExpBar:Hide()
ExhaustionTick:Hide()
end
end
-- The watched faction's strip on the main bar — `ref-ReputationFrame.lua`
-- `ReputationWatchBar_Update` (l.161-250) is the specification. THREE states: a watched
-- faction takes one of the two postures above (the level picks), and no watched faction
-- leaves the XP bar — or, at max level, the brass rail — holding that space instead.
--
-- `newLevel` is the reference's own parameter: PLAYER_LEVEL_UP carries the new level as
-- arg1 because the level FIELD may not have landed yet, and every other caller re-reads
-- UnitLevel. Note the two level tests are deliberately NOT the same test: watched forks on
-- `< MAX_PLAYER_LEVEL`, unwatched on `== MAX_PLAYER_LEVEL`, which is the reference's own
-- asymmetry and only differs above the cap, where nothing can go.
--
-- The tail used to call `updateContainerFrameAnchors()` itself, guarded, because that
-- global was a benilla no-op standing in for a body that had no windows to anchor.
-- Decision 1751 gave it the reference's real body and put the call where the reference
-- puts it — the end of `UIParent_ManageFramePositions` (UIParent.lua:1774) — so this
-- caller gets it for free, along with every other one.
function ReputationWatchBar_Update(newLevel)
newLevel = newLevel or UnitLevel("player")
local name, reaction, rankMin, rankMax, standing = GetWatchedFactionInfo()
-- Whether the strip's visibility FLIPS is all the tail below needs, so read it once
-- here and compare once at the end rather than threading a flag through both arms.
local wasShown = ReputationWatchBar:IsShown()
if name then
-- The engine reports the rank window ABSOLUTE for exactly this reason (benilla-ui
-- `script::reputation`'s module doc): the strip's range is the window's WIDTH, and
-- its value the distance into it.
local width = rankMax - rankMin
local progress = standing - rankMin
ReputationWatchStatusBar:SetMinMaxValues(0, width)
ReputationWatchStatusBar:SetValue(progress)
ReputationWatchStatusBarText:SetText(name .. " " .. progress .. " / " .. width)
local color = FACTION_BAR_COLORS[reaction]
ReputationWatchStatusBar:SetStatusBarColor(color.r, color.g, color.b)
ReputationWatchBar:Show()
ReputationWatchStatusBar:SetFrameLevel(MainMenuBarArtFrame:GetFrameLevel() - 1)
-- Stacked, the XP bar is still the bar underneath; replacing, it goes away with its
-- tick. Either way the brass rail is not wanted — the strip fills that space.
local stacked = newLevel < MAX_PLAYER_LEVEL
BenillaWatchBar_SetPosture(stacked)
BenillaExpStrip_SetShown(stacked)
MainMenuBarMaxLevelBar:Hide()
else
-- Nothing watched (the arm decision 1094 shipped alone): the strip goes down, and
-- at the cap the brass rail is what takes the XP bar's place.
ReputationWatchBar:Hide()
local capped = newLevel == MAX_PLAYER_LEVEL
BenillaExpStrip_SetShown(not capped)
if capped then
MainMenuBarMaxLevelBar:Show()
else
MainMenuBarMaxLevelBar:Hide()
end
end
if wasShown ~= ReputationWatchBar:IsShown() then
UIParent_ManageFramePositions()
end
end
-- The numerals lock, to ref-ReputationFrame.lua l.252-270. A two-key lock: `cvarLocked`
-- records that the statusBarText cvar (not a hover) asked for the text, and `textLocked`
-- that the text is currently pinned on — which is what the bar's own OnLeave reads before
-- hiding it again. Note the Show verb only ever shows the text AT MAX LEVEL, where the
-- strip has replaced the XP bar and is tall enough to hold numerals; below that it falls
-- through to the Hide one. Both function names, both field names and the FontString are
-- the addon-visible contract; the shapes below (early exits where the ref nests) are ours.
function ShowWatchedReputationBarText(lock)
if lock then ReputationWatchBar.cvarLocked = lock end
if UnitLevel("player") ~= MAX_PLAYER_LEVEL or not ReputationWatchBar:IsVisible() then
HideWatchedReputationBarText()
return
end
ReputationWatchBar.textLocked = 1
ReputationWatchStatusBarText:Show()
end
function HideWatchedReputationBarText(unlock)
-- Same gate as the ref's `unlock or not cvarLocked`, spun as the early exit.
if not unlock and ReputationWatchBar.cvarLocked then return end
ReputationWatchStatusBarText:Hide()
ReputationWatchBar.cvarLocked = nil
ReputationWatchBar.textLocked = nil
end
-- ── The watch bar's own handlers ────────────────────────────────────────────────────────
-- Chunk functions rather than the reference's inline bodies, so the departures below have
-- somewhere to be explained (ReputationFrame.xml's chunk-boundary rule).
-- `ref-ReputationFrame.xml` l.996-1035 is the specification.
--
-- PLAYER_ENTERING_WORLD is benilla's ADDITION (decision 1094's own): our faction feed only
-- fires once the catalog and the player's race/class are up, which can be a tick or two
-- after world entry, and the max-level rail must be right from the first frame. The arm it
-- lands in is idempotent, so the extra registration only ever costs a no-op. The event fan
-- is here rather than on `MainMenuBarMaxLevelBar`, where it sat while the watch bar was
-- driven by nothing — the rail is this function's output, not its own driver.
function BenillaWatchBar_OnLoad(bar)
bar:RegisterEvent("UPDATE_FACTION")
bar:RegisterEvent("PLAYER_LEVEL_UP")
bar:RegisterEvent("CVAR_UPDATE")
bar:RegisterEvent("PLAYER_ENTERING_WORLD")
end
-- Every event but one ends in a repaint of the bar, so the odd one out — the cvar that
-- pins the numerals on and off — is answered first and leaves.
--
-- TWO ARMS read as odd and are the reference's own:
-- · a faction push repaints the PANE, from the BAR's handler. The pane registers
-- UPDATE_FACTION itself (ReputationFrame.xml), so this is the reference double-driving
-- it; kept rather than "fixed", and harmless because the pane's repaint is idempotent.
-- Guarded on existence because this file loads WITHOUT ReputationFrame.xml in the
-- engine's per-window subset harnesses.
-- · the `statusBarText` token is DEAD CODE in shipped 1.12 and stays: the CVAR_UPDATE
-- token is the caller's third SetCVar argument, and 1.12's own options panels pass the
-- uppercase display name ("STATUS_BAR_TEXT"), never the cvar's own name (benilla-ui
-- `script::cvars`, decision 1140, which carves exactly this).
function BenillaWatchBar_OnEvent(bar, event)
if event == "CVAR_UPDATE" and arg1 == "statusBarText" then
if arg2 == "1" then ShowWatchedReputationBarText("lock") else HideWatchedReputationBarText("unlock") end
return
end
if event == "UPDATE_FACTION" and bar:IsVisible() and ReputationFrame_Update then
ReputationFrame_Update()
end
-- PLAYER_LEVEL_UP is the one event that knows the level; every other asks for nil and
-- lets the update re-read UnitLevel.
if event == "PLAYER_LEVEL_UP" then
ReputationWatchBar_Update(arg1)
else
ReputationWatchBar_Update()
end
end
-- Coming up with the cvar set means the numerals were meant to be pinned on all along.
function BenillaWatchBar_OnShow()
if GetCVar("statusBarText") == "1" then ShowWatchedReputationBarText("lock") end
UIParent_ManageFramePositions()
end
-- The watch bar's own script — `ReputationWatchBar_Update`, the two text verbs,
-- `MAX_PLAYER_LEVEL` and `FACTION_BAR_COLORS` — is the REFERENCE's, in the same file that
-- declares the bar (1875). `ExhaustionTick_Update` below still reads
-- `ReputationWatchBar:IsShown()` twice: that coupling is the reference's own, and it is why
-- every action-bar-shaped harness now loads the reputation pane.
-- ── The exhaustion tick (ref-MainMenuBar.lua ExhaustionTick_*) ────────────────────────
-- The round marker riding the XP bar at the point where rested XP runs out, plus the
@@ -1928,7 +1701,8 @@
flat brass rail that replaces the XP strip at MAX_PLAYER_LEVEL — four 256×7
quarters of UI-MainMenuBar-MaxLevel, riding just above the frame's own 7 px rect
(each quarter's BOTTOM sits on the frame's TOP, the ref's own geometry).
Shown and hidden by ReputationWatchBar_Update (above) and by nothing else — which
Shown and hidden by the reference's `ReputationWatchBar_Update` (its own file since
1875) and by nothing else — which
is the reference's own arrangement, and why this frame carries NO Scripts. It held
a benilla OnLoad/OnEvent pair while the watch bar was driven by nothing (decision
1094); those were a transcription of the WATCH BAR's own handlers parked here, and
@@ -2309,7 +2083,7 @@
this file's script block. It
shipped built-and-hidden with 1223 (three corpus addons index it by name — `FuBar_FuXPFu`,
`CT_BottomBar`, `zBarEx` — and FuXPFu dies at `FuBar_FuXPFu.lua:227` on the nil); it is
DRIVEN now, by `ReputationWatchBar_Update` in this file's script block, whose own comment
DRIVEN now, by the reference's `ReputationWatchBar_Update` (its own file since 1875), whose comment
carries the reason the reputation watch-bar slice lives here rather than in
ReputationFrame.xml where the reference declares it.
@@ -2321,107 +2095,10 @@
replaces-the-XP-bar posture at max level, where the strip has to look like the bar it took
over from. The numerals live on their own DIALOG-strata overlay child so they draw over
both. -->
<Frame name="ReputationWatchBar" enableMouse="true" parent="MainMenuBar" hidden="true">
<Size><AbsDimension x="1024" y="11"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativeTo="MainMenuBar" relativePoint="TOP"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor>
</Anchors>
<Frames>
<StatusBar name="ReputationWatchStatusBar" drawLayer="ARTWORK">
<Size><AbsDimension x="1024" y="8"/></Size>
<Anchors><Anchor point="TOP"/></Anchors>
<Layers>
<Layer level="OVERLAY">
<!-- ref l.890-929: four 256-wide quarters of the 1024 strip, each its own
band of the sheet. The TexCoords were MISSING while the frame was a
stub that could not show; without them every quarter draws the whole
sheet. -->
<Texture name="ReputationWatchBarTexture0" file="Interface\PaperDollInfoFrame\UI-ReputationWatchBar">
<Size><AbsDimension x="256" y="11"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="2"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.171875"/>
</Texture>
<Texture name="ReputationWatchBarTexture1" file="Interface\PaperDollInfoFrame\UI-ReputationWatchBar">
<Size><AbsDimension x="256" y="11"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="ReputationWatchBarTexture0" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.171875" bottom="0.34375"/>
</Texture>
<Texture name="ReputationWatchBarTexture2" file="Interface\PaperDollInfoFrame\UI-ReputationWatchBar">
<Size><AbsDimension x="256" y="11"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="ReputationWatchBarTexture1" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.34375" bottom="0.515625"/>
</Texture>
<Texture name="ReputationWatchBarTexture3" file="Interface\PaperDollInfoFrame\UI-ReputationWatchBar">
<Size><AbsDimension x="256" y="11"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="ReputationWatchBarTexture2" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.515625" bottom="0.6875"/>
</Texture>
<!-- ref l.930-969: the max-level alternate, four bands of the SAME dwarf
sheet the XP bar's own notch art comes from, so the replaced bar reads
as the bar it replaced. Hidden until the max-level arm shows them. -->
<Texture name="ReputationXPBarTexture0" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf" hidden="true">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.79296875" bottom="0.83203125"/>
</Texture>
<Texture name="ReputationXPBarTexture1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf" hidden="true">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="ReputationXPBarTexture0" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.54296875" bottom="0.58203125"/>
</Texture>
<Texture name="ReputationXPBarTexture2" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf" hidden="true">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="ReputationXPBarTexture1" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.29296875" bottom="0.33203125"/>
</Texture>
<Texture name="ReputationXPBarTexture3" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf" hidden="true">
<Size><AbsDimension x="256" y="10"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="ReputationXPBarTexture2" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="1.0" top="0.04296875" bottom="0.08203125"/>
</Texture>
</Layer>
<!-- ref l.971-975: the half-alpha black backing the fill rides over. -->
<Layer level="BACKGROUND">
<Texture name="ReputationWatchStatusBarBackground">
<Color r="0.0" g="0.0" b="0.0" a="0.5"/>
</Texture>
</Layer>
</Layers>
<!-- ref l.977-978. The purple is only the load-time default: every painted state
takes FACTION_BAR_COLORS[reaction] instead. -->
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0.58" g="0.0" b="0.55"/>
</StatusBar>
<!-- ref l.980-994: the numerals sit on a DIALOG-strata child of their own so they clear
both sets of end art. Hidden by default; the hover shows them, and at max level the
statusBarText cvar can pin them (ShowWatchedReputationBarText). -->
<Frame name="ReputationWatchBarOverlayFrame" frameStrata="DIALOG" setAllPoints="true">
<Layers>
<Layer level="ARTWORK">
<FontString name="ReputationWatchStatusBarText" inherits="NumberFontNormal" hidden="true">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<!-- ref l.996-1035. The three that carry any thought are chunk functions in this file's
script block, where their departures are documented (`BenillaWatchBar_OnLoad` /
`_OnEvent` / `_OnShow`); the three below are one statement each and stay where the
reference has them. The numerals hover is the pair: coming onto the bar shows them
unconditionally, leaving takes them down UNLESS the statusBarText cvar has pinned them
on — `textLocked` is the pin, and the two text verbs above are what set it. -->
<Scripts>
<OnLoad>BenillaWatchBar_OnLoad(self)</OnLoad>
<OnEvent>BenillaWatchBar_OnEvent(self, event)</OnEvent>
<OnShow>BenillaWatchBar_OnShow()</OnShow>
<OnHide>UIParent_ManageFramePositions()</OnHide>
<OnEnter>ReputationWatchStatusBarText:Show()</OnEnter>
<OnLeave>
if not ReputationWatchBar.textLocked then
ReputationWatchStatusBarText:Hide()
end
</OnLeave>
</Scripts>
</Frame>
<!-- The reputation WATCH BAR is declared by the REFERENCE's own `ReputationFrame.xml`, off the
player's chain (1875). It stood here because every frame its update touches is declared in
this file — but frame globals publish NON-overwritingly, and this file loads at manifest
340 against the pane's 681, so ours would have kept the name and the chain's bar would
have been built and unreachable. That is precisely what `GroupLootFrame` did for months
(1838), so the placement note that used to defend this spot is retired with the block. -->
</Ui>
+18 -163
View File
@@ -199,22 +199,6 @@
collapse = 1,
}
-- ── The money-entry kit (ref MoneyInputFrame.lua) ───────────────────────────────────────
-- The three boxes ride the shared BenillaMoneyInput_* helpers (MerchantFrame.xml), which
-- own the gold/silver/copper fold and the SetCopper suppression bracket. All this adds is
-- the per-instance value-changed callback the reference sets with
-- `MoneyInputFrame_SetOnvalueChangedFunc`: the create-auction pane re-validates on every
-- keystroke, the two bid boxes do not.
function BenillaAuctionMoneyInput_OnChanged(box)
local frame = box:GetParent()
-- False while a programmatic SetCopper is in flight — the reference's `expectChanges`
-- counter, as MerchantFrame.xml's bracket flag.
if not BenillaMoneyInput_Consume(frame:GetName()) then return end
if frame.onValueChanged then
frame.onValueChanged()
end
end
-- ── Load / show / hide / events (ref-lua l.65-125) ──────────────────────────────────────
function AuctionFrame_OnLoad(frame)
PanelTemplates_SetNumTabs(frame, 3)
@@ -227,8 +211,8 @@
AuctionsBuyoutText:SetText(gs("BUYOUT_PRICE") .. " |cff808080(" .. gs("OPTIONAL") .. ")|r")
-- The create pane's two inputs re-validate on every keystroke (ref-xml l.1811/1877);
-- the gold box is capped at six digits there, which is the price ceiling in practice.
StartPrice.onValueChanged = AuctionsFrameAuctions_ValidateAuction
BuyoutPrice.onValueChanged = AuctionsFrameAuctions_ValidateAuction
MoneyInputFrame_SetOnvalueChangedFunc(StartPrice, AuctionsFrameAuctions_ValidateAuction)
MoneyInputFrame_SetOnvalueChangedFunc(BuyoutPrice, AuctionsFrameAuctions_ValidateAuction)
StartPriceGold:SetMaxLetters(6)
-- Each faux list repaints through its own tab's Update.
end
@@ -925,8 +909,8 @@
-- The bid box is SEATED at the required bid, and the gate is read back off
-- the box rather than off the number — so a player who types a different
-- amount is gated on what they typed (ref-lua l.555-559).
BenillaMoneyInput_SetCopper("BrowseBidPrice", requiredBid)
local offered = BenillaMoneyInput_GetCopper("BrowseBidPrice")
MoneyInputFrame_SetCopper(BrowseBidPrice, requiredBid)
local offered = MoneyInputFrame_GetCopper(BrowseBidPrice)
if not highBidder and owner ~= UnitName("player")
and GetMoney() >= offered and offered <= BENILLA_MAX_BID then
BrowseBidButton:Enable()
@@ -1040,11 +1024,11 @@
elseif not (buyoutPrice > 0 and buyoutPrice >= currentBid) then
AuctionFrame.buyoutPrice = nil
end
BenillaMoneyInput_SetCopper("BidBidPrice", currentBid + minIncrement)
MoneyInputFrame_SetCopper(BidBidPrice, currentBid + minIncrement)
-- Looser than Browse's: this list is auctions you already bid on, so the
-- "not your own auction" test is redundant and the ceiling is not applied
-- (ref-lua l.737).
if not highBidder and GetMoney() >= BenillaMoneyInput_GetCopper("BidBidPrice") then
if not highBidder and GetMoney() >= MoneyInputFrame_GetCopper(BidBidPrice) then
BidBidButton:Enable()
end
end
@@ -1249,9 +1233,9 @@
local name, texture, count, quality, canUse, price = GetAuctionSellItemInfo()
-- The suggested opening price: half again the stack's vendor value, floored at one
-- silver (ref-lua l.1023).
BenillaMoneyInput_SetCopper("StartPrice", max(100, floor(price * 1.5)))
MoneyInputFrame_SetCopper(StartPrice, max(100, floor(price * 1.5)))
UpdateDeposit()
BenillaMoneyInput_SetCopper("BuyoutPrice", 0)
MoneyInputFrame_SetCopper(BuyoutPrice, 0)
AuctionsFrameAuctions_ValidateAuction()
end
@@ -1279,8 +1263,8 @@
if not GetAuctionSellItemInfo() then
return
end
local start = BenillaMoneyInput_GetCopper("StartPrice")
local buyout = BenillaMoneyInput_GetCopper("BuyoutPrice")
local start = MoneyInputFrame_GetCopper(StartPrice)
local buyout = MoneyInputFrame_GetCopper(BuyoutPrice)
-- A buyout under the opening price is the one error this form explains out loud.
if buyout > 0 and start > buyout then
AuctionsBuyoutErrorText:Show()
@@ -1293,141 +1277,12 @@
end
function AuctionsCreateAuctionButton_OnClick()
StartAuction(BenillaMoneyInput_GetCopper("StartPrice"),
BenillaMoneyInput_GetCopper("BuyoutPrice"),
StartAuction(MoneyInputFrame_GetCopper(StartPrice),
MoneyInputFrame_GetCopper(BuyoutPrice),
AuctionFrameAuctions.duration)
end
]]></Script>
<!-- The editable money entry (ref MoneyInputFrameTemplate, MoneyInputFrame.xml l.3-260, quoted
verbatim: 176x18 overall; gold 58x20 at TOPLEFT, silver/copper 30x20 chained +26/+16; each
box's L slice 8x20 at (-5,0), R slice at RIGHT (gold 0, silver/copper -10), M stretched
between; the denomination coin 13x13 at LEFT->RIGHT +2 (gold) / -8 (silver, copper)). The
boxes are named $parentGold/Silver/Copper, which is the prefix contract the shared
BenillaMoneyInput_* helpers read. -->
<Frame name="BenillaAuctionMoneyInputTemplate" virtual="true">
<Size><AbsDimension x="176" y="18"/></Size>
<Frames>
<EditBox name="$parentGold" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="58" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnTabPressed>getglobal(this:GetParent():GetName() .. "Silver"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName() .. "Silver"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>BenillaAuctionMoneyInput_OnChanged(this)</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<EditBox name="$parentSilver" letters="2" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="30" y="20"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentGold" relativePoint="RIGHT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnTabPressed>getglobal(this:GetParent():GetName() .. "Copper"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName() .. "Copper"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>BenillaAuctionMoneyInput_OnChanged(this)</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<EditBox name="$parentCopper" letters="2" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="30" y="20"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentSilver" relativePoint="RIGHT"><Offset><AbsDimension x="16" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>BenillaAuctionMoneyInput_OnChanged(this)</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
</Frames>
</Frame>
<!-- ref-tmpl l.3-28: the duration radio. Three crops of one 64x16 strip. -->
<CheckButton name="AuctionRadioButtonTemplate" virtual="true">
@@ -2173,7 +2028,7 @@
<OnShow>BrowseDropDown_OnLoad()</OnShow>
</Scripts>
</Frame>
<Frame name="BrowseBidPrice" inherits="BenillaAuctionMoneyInputTemplate">
<Frame name="BrowseBidPrice" inherits="MoneyInputFrameTemplate">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="25" y="18"/></Offset></Anchor></Anchors>
</Frame>
<Button name="BrowseCloseButton" inherits="UIPanelButtonTemplate" text="CLOSE">
@@ -2191,7 +2046,7 @@
<Anchors><Anchor point="RIGHT" relativeTo="BrowseBuyoutButton" relativePoint="LEFT"/></Anchors>
<Scripts>
<OnClick>
PlaceAuctionBid("list", GetSelectedAuctionItem("list"), BenillaMoneyInput_GetCopper("BrowseBidPrice"))
PlaceAuctionBid("list", GetSelectedAuctionItem("list"), MoneyInputFrame_GetCopper(BrowseBidPrice))
this:Disable()
</OnClick>
</Scripts>
@@ -2293,7 +2148,7 @@
<Scripts><OnClick>BidPageButton_OnClick(1)</OnClick></Scripts>
</Button>
<Frame name="BidBidPrice" inherits="BenillaAuctionMoneyInputTemplate">
<Frame name="BidBidPrice" inherits="MoneyInputFrameTemplate">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="-15" y="18"/></Offset></Anchor></Anchors>
</Frame>
<Button name="BidCloseButton" inherits="UIPanelButtonTemplate" text="CLOSE">
@@ -2311,7 +2166,7 @@
<Anchors><Anchor point="RIGHT" relativeTo="BidBuyoutButton" relativePoint="LEFT"/></Anchors>
<Scripts>
<OnClick>
PlaceAuctionBid("bidder", GetSelectedAuctionItem("bidder"), BenillaMoneyInput_GetCopper("BidBidPrice"))
PlaceAuctionBid("bidder", GetSelectedAuctionItem("bidder"), MoneyInputFrame_GetCopper(BidBidPrice))
this:Disable()
</OnClick>
</Scripts>
@@ -2450,7 +2305,7 @@
</Scripts>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
</Button>
<Frame name="StartPrice" inherits="BenillaAuctionMoneyInputTemplate">
<Frame name="StartPrice" inherits="MoneyInputFrameTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="AuctionsPriceText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="3" y="-2"/></Offset></Anchor></Anchors>
</Frame>
<CheckButton name="AuctionsShortAuctionButton" inherits="AuctionRadioButtonTemplate" id="1">
@@ -2474,7 +2329,7 @@
<OnClick>AuctionsRadioButton_OnClick(3)</OnClick>
</Scripts>
</CheckButton>
<Frame name="BuyoutPrice" inherits="BenillaAuctionMoneyInputTemplate">
<Frame name="BuyoutPrice" inherits="MoneyInputFrameTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="AuctionsBuyoutText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="2" y="-2"/></Offset></Anchor></Anchors>
</Frame>
<!-- The deposit (ref-xml l.1882). Its own money type, and the ONLY money in
@@ -1,163 +0,0 @@
<Ui>
<!--
Interface\FrameXML\BasicControls.xml, re-expressed — the reference's second file, right
after Fonts.xml, and one of the smallest in FrameXML. It carries four things an addon can
reach and we had none of:
TEXT(text) the identity function 1.12 wraps every displayed string in. **26 corpus
addons call it**, because half the shipped FrameXML does and they copied
the idiom. Missing, it is `attempt to call global 'TEXT'`.
message(text) the client's "show the player this" dialog. Its own popup, not a chat
line. Present in 64 corpus files — but read the line before ranking it
(1207): 55 of those are copies of `Dewdrop-2.0.lua`, and the genuine
callers are about ten addons.
_ERRORMESSAGE what `message` is implemented in terms of, and the reference's default
error handler. Two corpus addons replace it outright (ImprovedErrorFrame
is one), which they cannot do to a name that does not exist.
ScriptErrors the dialog both of the above put text into, plus DialogBoxFrame and the
DialogButton*Texture trio it is built from — three of which the corpus
also inherits from directly (`inherits_demand`, 2 addons each).
The three fonts this needs — GameFontRedLarge, DialogButtonNormalText,
DialogButtonHighlightText — are already in Fonts.xml, so this file is self-contained
against the one that loads before it, exactly as it is in the reference's own toc order.
-->
<Script><![CDATA[
-- ── _ERRORMESSAGE / message (ref BasicControls.xml l.5-15) ─────────────────────────────
--
-- COMPLETE, including `debuginfo()`. That line was guarded here when this file landed,
-- because `debuginfo` was an engine global we lacked and whose behaviour was uncarved —
-- and declaring a no-op under a real 1.12 name is exactly the "capability absent without a
-- failure" class 1203 named. wow-re has since carved it (2026-08-11): `0x7027f0` is
-- `33 c0 c3` — `xor eax,eax; ret`, three bytes, no call, no memory write. It genuinely
-- does nothing and returns nothing, and five of its `debug*` siblings are byte-identical.
-- So the no-op in `stdlib.rs` is the REFERENCE's no-op, the guard was measuring our own
-- ignorance rather than a real branch, and it is gone.
function _ERRORMESSAGE(msg)
debuginfo()
if not ScriptErrors:IsVisible() then
ScriptErrors_Message:SetText(msg)
ScriptErrors:Show()
end
return msg
end
function message(text)
_ERRORMESSAGE(text)
end
-- ── seterrorhandler(_ERRORMESSAGE): the reference's closing line, now transcribed ──────
-- This file withheld exactly this line from when it landed until decision 1305, because
-- the engine's error channel (`UiScript::errors()`) was the only place a Lua fault was
-- visible to any instrument and a modal would have blinded it. That trade is dissolved,
-- not decided differently: the engine now records every caught script error on the host
-- channel FIRST and then dispatches it to whatever `geterrorhandler()` answers
-- (`dispatch_script_errors_to_handler`, 1305) — so the harness's blocker tables read
-- exactly what they always read, and the player gets what the reference always gave them:
-- an addon that errors puts the red ScriptErrors dialog on screen instead of failing
-- somewhere only a terminal can see (B271's report was debugged entirely off WARN lines).
-- An addon that installs its OWN handler still wins outright, exactly as in 1195.
seterrorhandler(_ERRORMESSAGE)
-- ── TEXT (ref l.22-26, verbatim) ──────────────────────────────────────────────────────
-- "This function is used to translate text (actually it's already translated as-is)" is
-- the reference's own comment. It really is the identity function: 1.12 shipped one
-- localisation per client and the hook was never used.
function TEXT(text)
return text
end
-- ── UIParentLoadAddOn (ref UIParent.lua l.638-647) ────────────────────────────────────
-- The on-demand loader every "open the auction house / tradeskill / talent panel" path
-- goes through, and the one 5 corpus addons name — FuBar_MCPFu, Fubar_RareTrackerFu, MCP,
-- MikScrollingBattleText, SpecialTalent: five separate addons, not one library replicated
-- (1207/1240).
--
-- Every piece was already here and only the join was missing: `LoadAddOn` answers the
-- reference's own `(1, nil)` / `(nil, reason)` pair, `message` and `TEXT` are directly
-- above, and the `ADDON_*` reason words come from the real `GlobalStrings.lua` we RUN off
-- the patch chain rather than copy (1234).
--
-- **`FailedAddOnLoad` is the reference's own latch, and it is not decoration.** Without it
-- an addon that retries the load from a handler raises a modal on every attempt; with it
-- the failure shows once per addon name per session. It is exactly the detail this would
-- have lacked had it been reconstructed from the name instead of read.
--
-- Every reason our `LoadAddOn` can answer resolves to a string: MISSING, DISABLED,
-- NOT_DEMAND_LOADED, DEP_MISSING and DEP_DISABLED all have an `ADDON_..reason` word
-- (GlobalStrings l.16-31), the same reason set wow-re's `ui.md` l.2396 carves off
-- `0x51e780`. So the `getglobal` below cannot hand `format` a nil.
FailedAddOnLoad = {};
function UIParentLoadAddOn(name)
local loaded, reason = LoadAddOn(name)
if not loaded then
if not FailedAddOnLoad[name] then
message(format(TEXT(ADDON_LOAD_FAILED), name, TEXT(getglobal("ADDON_" .. reason))))
FailedAddOnLoad[name] = true
end
end
return loaded
end
]]></Script>
<!-- The dialog button faces (ref l.28-37). Same three-piece art as UIPanelButtonTemplate's,
with their own names because the reference's dialogs inherit these directly. -->
<Texture name="DialogButtonNormalTexture" file="Interface\Buttons\UI-Panel-Button-Up" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="DialogButtonPushedTexture" file="Interface\Buttons\UI-Panel-Button-Down" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture name="DialogButtonHighlightTexture" file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD" virtual="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<!-- DialogBoxFrame (ref l.38-77): a centered tooltip-bordered box with one OKAY button that
hides its parent. Virtual — ScriptErrors below is the only thing in shipped FrameXML that
inherits it, but two corpus addons inherit it too. -->
<Frame name="DialogBoxFrame" virtual="true" toplevel="true" frameStrata="DIALOG" hidden="true">
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<EdgeSize><AbsValue val="32"/></EdgeSize>
<TileSize><AbsValue val="32"/></TileSize>
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
</Backdrop>
<Frames>
<Button name="$parentButton">
<Size><AbsDimension x="128" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="16"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>
this:GetParent():Hide()
</OnClick>
</Scripts>
<NormalTexture inherits="DialogButtonNormalTexture"/>
<PushedTexture inherits="DialogButtonPushedTexture"/>
<HighlightTexture inherits="DialogButtonHighlightTexture"/>
<ButtonText text="OKAY"/>
<NormalFont inherits="DialogButtonNormalText"/>
<HighlightFont inherits="DialogButtonHighlightText"/>
</Button>
</Frames>
</Frame>
<!-- "This is the general scripting error dialog" (ref l.79-100), verbatim. -->
<Frame name="ScriptErrors" inherits="DialogBoxFrame">
<Size><AbsDimension x="384" y="128"/></Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="ScriptErrors_Message" inherits="GameFontRedLarge" justifyH="CENTER" justifyV="TOP" nonspacewrap="true">
<Size><AbsDimension x="360" y="0"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</Ui>
+19 -135
View File
@@ -48,8 +48,10 @@
MAX_COD_AMOUNT = 10000 -- MAX_COD_AMOUNT (ref l.6)
STATIONERY_PATH = "Interface\\Stationery\\" -- STATIONERY_PATH (ref l.5)
-- The money entry uses the shared BenillaMoneyInput_* kit (MerchantFrame.xml, loaded first) —
-- the send tab's three numeric edit boxes are named SendMailMoneyGold/Silver/Copper.
-- The money entry is the reference's own `MoneyInputFrame_*` off the chain (1882 retired
-- our BenillaMoneyInput_* duplicate) — the send tab's three numeric edit boxes are named
-- SendMailMoneyGold/Silver/Copper, which is what both kits read and why the swap was a
-- rename plus an `inherits=`.
-- ── Load / event dispatch (ref MailFrame_Onload l.8-24 / MailFrame_OnEvent l.26-62) ──────
function BenillaMailFrame_OnLoad(frame)
@@ -538,7 +540,7 @@
-- ── Send tab (ref SendMail* l.484-611 / l.936-960) ──────────────────────────────────────
function SendMailMailButton_OnClick()
local copper = BenillaMoneyInput_GetCopper("SendMailMoney")
local copper = MoneyInputFrame_GetCopper(SendMailMoney)
if SendMailSendMoneyButton:GetChecked() then
SetSendMailCOD(0)
if copper > 0 then
@@ -593,7 +595,7 @@
SendMailNameEditBox:SetText("")
SendMailSubjectEditBox:SetText("")
SendMailBodyEditBox:SetText("")
BenillaMoneyInput_ResetMoney("SendMailMoney")
MoneyInputFrame_ResetMoney(SendMailMoney)
SendMailRadioButton_OnClick(1)
SendMailFrame_Update()
end
@@ -607,7 +609,7 @@
if string.len(SendMailSubjectEditBox:GetText()) > 0 then checks = checks + 1 end
if SendMailCODButton:GetChecked() then
checksRequired = 3
if BenillaMoneyInput_GetCopper("SendMailMoney") > MAX_COD_AMOUNT * 10000 then
if MoneyInputFrame_GetCopper(SendMailMoney) > MAX_COD_AMOUNT * 10000 then
SendMailErrorText:Show()
SendMailErrorCoin:Show()
else
@@ -684,7 +686,7 @@
button1 = "Accept",
button2 = "Cancel",
OnAccept = function()
if SetSendMailMoney(BenillaMoneyInput_GetCopper("SendMailMoney")) then
if SetSendMailMoney(MoneyInputFrame_GetCopper(SendMailMoney)) then
SendMailFrame_SendMail()
end
end,
@@ -1114,136 +1116,18 @@
</Layer>
</Layers>
<Frames>
<!-- ref SendMailMoney l.892-905: MoneyInputFrameTemplate at BOTTOMLEFT
relativePoint BOTTOMRIGHT +15,1 — the template's three boxes +
Common-Input-Border art quoted verbatim (MoneyInputFrame.xml
l.3-260: gold 58×20 TOPLEFT, silver/copper 30×20 chained +26/+16;
each box's L slice 8×20 at (-5,0), R slice at RIGHT (gold 0,
silver/copper -10), M stretched between; coin 13×13 LEFT→RIGHT at
+2 (gold) / -8 (silver, copper)). -->
<Frame name="SendMailMoney">
<Size><AbsDimension x="176" y="18"/></Size>
<!-- ref SendMailMoney l.892-905. The three boxes come off the chain since 1882:
`MoneyInputFrameTemplate` out of `Interface\FrameXML\MoneyInputFrame.xml`. Our inline
trio was a verbatim copy of it — same child names, textures, TexCoords and 58/30/30
geometry — and the template carries the 176x18 itself, so the reference declares no
Size here and neither do we now. The per-box `OnTextChanged`s are gone: the callback is
registered through the reference's own slot instead, which is what the template's
`expectChanges` guard is written against. -->
<Frame name="SendMailMoney" inherits="MoneyInputFrameTemplate">
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="15" y="1"/></Offset></Anchor></Anchors>
<Frames>
<EditBox name="$parentGold" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="58" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnTabPressed>getglobal(this:GetParent():GetName().."Silver"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName().."Silver"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>SendMailFrame_CanSend()</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<EditBox name="$parentSilver" letters="2" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="30" y="20"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentGold" relativePoint="RIGHT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnTabPressed>getglobal(this:GetParent():GetName().."Copper"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName().."Copper"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>SendMailFrame_CanSend()</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<EditBox name="$parentCopper" letters="2" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="30" y="20"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentSilver" relativePoint="RIGHT"><Offset><AbsDimension x="16" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="10" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>SendMailFrame_CanSend()</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
</Frames>
<Scripts>
<OnLoad>MoneyInputFrame_SetOnvalueChangedFunc(this, SendMailFrame_CanSend)</OnLoad>
</Scripts>
</Frame>
<!-- ref SendMailSendMoneyButton l.906-919 / SendMailCODButton l.920-933. -->
<CheckButton name="SendMailSendMoneyButton" inherits="BenillaSendMailRadioButtonTemplate" id="1">
+4 -51
View File
@@ -164,56 +164,6 @@
end
end
-- ── The editable money-input kit (ref MoneyInputFrame.lua) ────────────────────────────────
-- A <prefix> naming three numeric EditBoxes <prefix>Gold/Silver/Copper — the shared money entry
-- both the send-mail and trade windows inline (decision 0592 P2 builds it here, the money home,
-- so both reuse it). GetCopper folds the three digits into a copper total; SetCopper splits a
-- copper amount back across the boxes. SetCopper is a *programmatic* fill, so it must not re-fire
-- a box's OnTextChanged value-changed callback (the loop the reference's `expectChanges` counter
-- guards, MoneyInputFrame.lua l.44-73). Because benilla fires OnTextChanged synchronously inside
-- SetText, a bracket flag is exact and simpler than the counter: a window's box calls
-- BenillaMoneyInput_Consume(prefix) at the top of its OnTextChanged — false (swallow) while a
-- SetCopper is in flight, true for a genuine keystroke.
BENILLA_MONEYINPUT_SUPPRESS = {}
function BenillaMoneyInput_ResetMoney(prefix)
getglobal(prefix .. "Gold"):SetText("")
getglobal(prefix .. "Silver"):SetText("")
getglobal(prefix .. "Copper"):SetText("")
end
function BenillaMoneyInput_GetCopper(prefix)
local function n(box) return tonumber(getglobal(prefix .. box):GetText()) or 0 end
return n("Gold") * 10000 + n("Silver") * 100 + n("Copper")
end
function BenillaMoneyInput_ClearFocus(prefix)
getglobal(prefix .. "Gold"):ClearFocus()
getglobal(prefix .. "Silver"):ClearFocus()
getglobal(prefix .. "Copper"):ClearFocus()
end
function BenillaMoneyInput_SetCopper(prefix, money)
money = money or 0
local parts = {
Gold = math.floor(money / 10000),
Silver = math.floor((money % 10000) / 100),
Copper = money % 100,
}
BENILLA_MONEYINPUT_SUPPRESS[prefix] = true
for box, val in pairs(parts) do
local eb = getglobal(prefix .. box)
if (tonumber(eb:GetText()) or 0) ~= val then
eb:SetText(val == 0 and "" or tostring(val))
end
end
BENILLA_MONEYINPUT_SUPPRESS[prefix] = false
end
function BenillaMoneyInput_Consume(prefix)
return not BENILLA_MONEYINPUT_SUPPRESS[prefix]
end
-- ── What a coin is worth ─────────────────────────────────────────────────────────────────
-- Published globals: an addon that formats a price does the same split we do below, and
-- reads these rather than spelling 100 twice.
@@ -394,7 +344,10 @@
function MoneyFrame_SetType(type)
local info = MoneyTypeInfo[type]
if not info then
-- ref l.145 diagnoses this with `message()`, which we do not ship (see the header).
-- ref l.145 diagnoses this with `message()`, which we DO have: it comes off the
-- chain in `Interface\FrameXML\BasicControls.xml` (1881 migrated that file; before
-- it, our own copy carried the same function). The `if` is therefore belt-and-braces
-- for a harness that loads this file without BasicControls, not a missing verb.
if message then
message("Invalid money type: " .. type)
end
File diff suppressed because it is too large Load Diff
@@ -1,922 +0,0 @@
<Ui>
<!--
benilla's Reputation tab: our own build of the 1.12 `ReputationFrame`. The reference
FrameXML `ReputationFrame.{xml,lua}` is the SPECIFICATION throughout — what the page must
look like and do — and it is cited line by line at every site (cites read `ref l.N` into
`ReputationFrame.xml`, `ref-lua l.N` into `ReputationFrame.lua`). Every geometry number, art
path and TexCoord below is QUOTED from it, because those are the client's facts and there is
no other value they could take; the Lua is ours, written against that specification and onto
benilla's Era API — the twelve reputation globals of
`crates/benilla-ui/src/script/reputation.rs`, whose module doc is the display law this file
paints, over `crates/benilla-app/src/ui_reputation.rs`'s feed. Where a line still reads
exactly like the reference's it is because the frame names and the API are the
addon-visible contract and neither is ours to vary.
A CHILD PAGE of `CharacterFrame` (ref l.211: `parent="CharacterFrame"`, `id="3"`), shown and
hidden by `CharacterFrame_ShowSubFrame` — a SEPARATE top-level file, the SkillFrame.xml /
PetPaperDollFrame.xml shape, and it carries the reference's `parent="CharacterFrame"` for
real: the loader does read a cross-file `parent=`, resolving the name by global lookup at
load (decision 1734). That name must exist by the time this file loads — an unresolvable one
warns and falls back to the enclosing element rather than erroring — which the manifest order
gives it. The page is still pinned to `CharacterFrame`'s own TOPLEFT by NAME and still loaded
right after it so it paints on top (later file = higher z): that arrangement produces the
identical rect and is what the page was built and approved against, not a workaround for
something the engine cannot do.
`CharacterFrame.xml`'s `CharacterFrame_OnHide` hide-loop over
`CHARACTERFRAME_SUBFRAMES` is what closes it with the window; joining that table as
entry 3 is all this page has to do to get that.
THE TAB IS THE REFERENCE'S OWN SLOT 3 AGAIN. Until this page, `SkillFrame` sat at id 3 with
a comment saying it was 3rd only because Reputation did not exist; Skills is id 4 now, which
is the real client's number, and `PetTab_Update` (PetPaperDollFrame.xml) re-anchors
`CharacterFrameTab3` exactly as the reference's does — same tab, same page, no divergence
left to carry.
NO OWN PORTRAIT: the "General" quadrant art below carries the same portrait-ring cutout
every PaperDollInfoFrame page shares, so `CharacterFrame`'s portrait/name/level (drawn
underneath) shows through — the reference's own compositing, and SkillFrame.xml's note.
DEVIATIONS (each re-cited at its own site below):
1. THE WATCH BAR IS NOT IN THIS FILE. The reference declares `ReputationWatchBar` here
(ref l.869-1036) and defines `ReputationWatchBar_Update` / `MAX_PLAYER_LEVEL` /
`FACTION_BAR_COLORS` / `Show`/`HideWatchedReputationBarText` in `ReputationFrame.lua`;
benilla keeps all of that in `ActionBar.xml`, where the frame already lived, and that
file's own comment at the block carries the full justification. The short of it: the
watch bar is a MainMenuBar fixture, and EVERY frame its update touches
(`MainMenuExpBar`, `MainMenuBarMaxLevelBar`, `ExhaustionTick`, `MainMenuBarArtFrame`,
`MainMenuBar` itself) is declared in `ActionBar.xml`, ~120 manifest entries above this
one. This page reaches `ReputationWatchBar_Update()` by name at click time, which is
the direction that always resolves.
2. `GetText("FACTION_STANDING_LABEL"..standingID, gender)` (ref-lua l.65) is the client's
C-side localization lookup, which benilla does not ship (UIParent.xml's `SecondsToTime`
records the same departure). Its one load-bearing rule — take the `_FEMALE` twin when
gender is 3 — is transcribed into `BenillaReputation_StandingLabel` below.
3. THE OPTIMISTIC VERBS FIRE NO EVENT, so this file repaints where the reference lets
`UPDATE_FACTION` do it. `CollapseFactionHeader`/`ExpandFactionHeader` and
`SetFactionInactive`/`SetFactionActive` change what the list shows the instant they are
called (the fold pair hide or reveal a group; the inactive pair RE-PARENT the row under
the "Inactive" header and rebuild the tree — reputation.rs's own note at
`0x4d69b0`), and none of them raises an event here the way the client's C bindings do.
Both call sites below therefore end in `ReputationFrame_Update()`, exactly as the
reference's own at-war box already does (ref l.765) — SkillFrame.xml's header click has
the identical addition for the identical reason.
`SetWatchedFactionIndex` gets NO such addition, and that is the interesting half:
it is **not** optimistic (`0x4d6b60` — the watched slot is a server field with no
client mirror), so there is nothing new for a repaint to paint, and forcing one would
un-tick the box the player just ticked before the round trip could answer. The
reference calls only `ReputationWatchBar_Update()` there, and so do we.
4. THE SCROLL FRAME IS A `<ScrollFrame>` ON THE CHAIN'S FAUX KIT, and this note used to
say otherwise. It read "a plain `<Frame inherits=\"FauxScrollFrameTemplate\">` rather
than the reference's `<ScrollFrame>`" — true while the kit was ours. 1846 put
`FauxScrollFrameTemplate` on the chain, where it IS a `<ScrollFrame>`, so every one of
our instances became one too: a `<Frame>` inheriting it has no `SetScrollChild` and the
loader throws on the template's own `<ScrollChild>`. The two
loose trough textures the reference hangs beside the bar (ref l.572-600) are replaced
by the kit's shared `BenillaScrollTroughTemplate`; see the site for why the fit is
exact rather than approximate.
5. `enableMouse="true"` ADDED in three places (the bar template, the two label-hover
frames, the at-war indicator). In this engine only Button/CheckButton/EditBox/
ScrollingMessageFrame are mouse-enabled by construction (`widget/mod.rs`'s ctor
comment; UnitFrames.xml and CharacterFrame.xml both carry the same note), so a
StatusBar or a plain Frame with `<OnEnter>` needs the attribute or the handler silently
never fires. This REPRODUCES the reference's behaviour rather than departing from it.
6. TWO PIECES OF THE REFERENCE'S OWN DEAD WEIGHT DROPPED (the TradeSkillFrame.xml
precedent): `ReputationFrame_Update`'s `rightBarTexture` local (ref-lua l.36/71),
assigned and never read even in the reference, and the `text="Revered"` placeholder on
`$parentFactionStanding` (ref l.102), which only ever shows on a row that has not been
painted yet — and rows are hidden until they are.
7. A WHEEL HANDLER, where the reference has a bare `return;` (ref l.864-866). Its scroll
frame is a real `<ScrollFrame>` and takes the wheel itself; ours is a plain Frame, so
the page drives the faux kit. No wheel-catcher Button is needed (SkillFrame.xml ships
one): this page is mouse-enabled over its whole rect, so a spin in a gap between rows
lands on the page itself, and a spin on a row bubbles up the parent chain to it — and
a full-page catcher is precisely what ate SkillFrame's tab row in the 2026-07-17
report.
8. NO `LockHighlight` (SkillFrame.xml deviation 7): not needed here — the reference's own
selection glow is the pair of `$parentHighlight1`/`2` textures on the bar, shown and
hidden by hand, which is already the affordance this engine has.
-->
<Script><![CDATA[
-- ── Constants (ref-lua l.1-2) ───────────────────────────────────────────────────────────
-- Globals, as the reference declares them: NUM_FACTIONS_DISPLAYED is the row-slot count the
-- XML below instantiates, REPUTATIONFRAME_FACTIONHEIGHT the faux kit's pixel value step.
NUM_FACTIONS_DISPLAYED = 15
REPUTATIONFRAME_FACTIONHEIGHT = 26
-- ── GlobalStrings (ref-GlobalStrings.lua, cited per line) ───────────────────────────────
-- The `X = X or "..."` idiom is this codebase's convention: the app runs the real localized
-- GlobalStrings.lua off the patch chain BEFORE any window file, so the installed locale
-- wins and these copies only fill in for the test harnesses, which load no chain.
FACTION = FACTION or "Faction" -- :1938
STANDING = STANDING or "Standing" -- :4193
AT_WAR = AT_WAR or "At War" -- :77
MOVE_TO_INACTIVE = MOVE_TO_INACTIVE or "Move to Inactive" -- :2706
SHOW_FACTION_ON_MAINSCREEN = SHOW_FACTION_ON_MAINSCREEN or "Show as Experience Bar" -- :3438
REPUTATION_FACTION_DESCRIPTION = REPUTATION_FACTION_DESCRIPTION or
"These are the factions that you have interacted with." -- :3330
REPUTATION_STANDING_DESCRIPTION = REPUTATION_STANDING_DESCRIPTION or
"The further to the right your standing bar is the closer you are to improving your reputation to the next tier with that faction. The Standing Levels in order are Hated, Hostile, Unfriendly, Neutral, Friendly, Honored, Revered and Exalted." -- :3331
REPUTATION_STATUS_AT_WAR = REPUTATION_STATUS_AT_WAR or
"You are currently at war with this group." -- :3333
REPUTATION_AT_WAR_DESCRIPTION = REPUTATION_AT_WAR_DESCRIPTION or
"This determines how you will interact with members of this faction. If you have at war checked then your default behavior will be to attack them. If you do not have it checked you will not be able to attack them. " -- :3329
-- The two SYNTHETIC header names. The engine reads these globals out of the VM itself
-- (reputation.rs `synthetic_header`), which is the only reason either string has a consumer
-- at all: GlobalStrings.lua defines both and no reference FrameXML file anywhere reads
-- them, because the real client builds the list C-side. `FACTION_OTHER` heads the
-- parentless bucket (key 0); `FACTION_INACTIVE` heads the bucket a faction moves to when
-- the popup's "Move to Inactive" box is ticked (key -1), which the engine folds by default
-- on every rebuild, exactly as the client does.
FACTION_OTHER = FACTION_OTHER or "Other" -- :1943
FACTION_INACTIVE = FACTION_INACTIVE or "Inactive" -- :1942
-- The eight standing names, and their `_FEMALE` twins (:1947-1962). enUS spells both the
-- same; a localized client's own GlobalStrings.lua overwrites the pair and the gender rule
-- in BenillaReputation_StandingLabel starts mattering.
FACTION_STANDING_LABEL1 = FACTION_STANDING_LABEL1 or "Hated"
FACTION_STANDING_LABEL2 = FACTION_STANDING_LABEL2 or "Hostile"
FACTION_STANDING_LABEL3 = FACTION_STANDING_LABEL3 or "Unfriendly"
FACTION_STANDING_LABEL4 = FACTION_STANDING_LABEL4 or "Neutral"
FACTION_STANDING_LABEL5 = FACTION_STANDING_LABEL5 or "Friendly"
FACTION_STANDING_LABEL6 = FACTION_STANDING_LABEL6 or "Honored"
FACTION_STANDING_LABEL7 = FACTION_STANDING_LABEL7 or "Revered"
FACTION_STANDING_LABEL8 = FACTION_STANDING_LABEL8 or "Exalted"
FACTION_STANDING_LABEL1_FEMALE = FACTION_STANDING_LABEL1_FEMALE or "Hated"
FACTION_STANDING_LABEL2_FEMALE = FACTION_STANDING_LABEL2_FEMALE or "Hostile"
FACTION_STANDING_LABEL3_FEMALE = FACTION_STANDING_LABEL3_FEMALE or "Unfriendly"
FACTION_STANDING_LABEL4_FEMALE = FACTION_STANDING_LABEL4_FEMALE or "Neutral"
FACTION_STANDING_LABEL5_FEMALE = FACTION_STANDING_LABEL5_FEMALE or "Friendly"
FACTION_STANDING_LABEL6_FEMALE = FACTION_STANDING_LABEL6_FEMALE or "Honored"
FACTION_STANDING_LABEL7_FEMALE = FACTION_STANDING_LABEL7_FEMALE or "Revered"
FACTION_STANDING_LABEL8_FEMALE = FACTION_STANDING_LABEL8_FEMALE or "Exalted"
-- `FACTION_BAR_COLORS` (ref-lua l.3-12) is NOT declared here: it lives in ActionBar.xml
-- beside `ReputationWatchBar_Update`, its other consumer, because that file must be able to
-- index it with no reputation file loaded and this one cannot be loaded without it (this
-- file's header comment, deviation 1). ActionBar.xml is manifest entry 94 against this
-- file's 213, so the table is always there by the time a row paints.
-- The standing name for `standingID` (1..8) at the player's gender — the reference's
-- `GetText("FACTION_STANDING_LABEL"..standingID, gender)` (ref-lua l.65) with its C-side
-- lookup collapsed (this file's header comment, deviation 2). `UnitSex` answers 2 for male
-- and 3 for female, which is the value the reference passes straight through.
function BenillaReputation_StandingLabel(standingID, gender)
if gender == 3 then
local female = getglobal("FACTION_STANDING_LABEL" .. standingID .. "_FEMALE")
if female then
return female
end
end
return getglobal("FACTION_STANDING_LABEL" .. standingID) or ""
end
-- A checkbox takes the client's 1/nil rather than a boolean, and every box on this page is
-- driven straight off a flag out of the faction tuple — so one verb, not an if/else pair
-- per box.
function BenillaReputation_SetBoxChecked(box, on)
box:SetChecked(on and 1 or nil)
end
-- The selection glow is a PAIR of textures, because the row's frame art is two pieces
-- (`$parentHighlight1` reaches back over the name, `2` caps the bar's right end). They are
-- always raised and dropped together, from three places: the update below, and the row's
-- own OnEnter/OnLeave.
function BenillaReputationBar_SetGlow(barName, lit)
local left, right = getglobal(barName .. "Highlight1"), getglobal(barName .. "Highlight2")
if lit then
left:Show()
right:Show()
else
left:Hide()
right:Hide()
end
end
-- Fill the detail popup for the row at `factionIndex` (ref-lua l.106-133 is the
-- specification). Called only when the popup is up AND this row is the selected one.
-- Everything it touches is a `ReputationDetail*` widget, which is the addon-visible name
-- and so not ours to vary; what IS ours is that the three tick states are one verb and the
-- at-war label's colour is written once instead of once per arm.
function BenillaReputationDetail_Fill(factionIndex, name, description, atWarWith, canToggleAtWar, isWatched)
ReputationDetailFactionName:SetText(name)
ReputationDetailFactionDescription:SetText(description)
BenillaReputation_SetBoxChecked(ReputationDetailAtWarCheckBox, atWarWith)
-- A peace-forced faction cannot be fought, so its box is dead and the label greys off
-- the angry red to say so.
local label = GRAY_FONT_COLOR
if canToggleAtWar then
label = RED_FONT_COLOR
ReputationDetailAtWarCheckBox:Enable()
else
ReputationDetailAtWarCheckBox:Disable()
end
ReputationDetailAtWarCheckBoxText:SetTextColor(label.r, label.g, label.b)
BenillaReputation_SetBoxChecked(ReputationDetailInactiveCheckBox, IsFactionInactive(factionIndex))
BenillaReputation_SetBoxChecked(ReputationDetailMainScreenCheckBox, isWatched)
end
-- A row SLOT is a pair of twins on one line — the header BUTTON `ReputationHeader<i>` and
-- the faction STATUSBAR `ReputationBar<i>` with all its furniture — of which exactly one is
-- ever up. The two verbs below cover every state a slot can be in (a header, a faction, or
-- nothing at all), and each raises the twin it wants and drops the other, so no caller has
-- to remember the pairing.
-- Slot `i` has nothing to hold: both twins go dark (ref-lua l.141-143).
function BenillaReputationSlot_Clear(i)
getglobal("ReputationHeader" .. i):Hide()
getglobal("ReputationBar" .. i):Hide()
end
-- Slot `i` holds the visible row at `factionIndex`. Split out of the update loop so that
-- loop is about the SCROLL WINDOW and this is about ONE ROW; the eleven-wide faction tuple
-- then has exactly one reader in the file. ref-lua l.51-139 is the specification, and the
-- reference's own unread `rightBarTexture` local is not carried (this file's header
-- comment, deviation 6).
function BenillaReputationSlot_Paint(i, factionIndex, gender)
local bar = getglobal("ReputationBar" .. i)
local header = getglobal("ReputationHeader" .. i)
local check = getglobal("ReputationBar" .. i .. "Check")
local name, description, standingID, barMin, barMax, barValue, atWarWith,
canToggleAtWar, isHeader, isCollapsed, isWatched = GetFactionInfo(factionIndex)
if isHeader then
-- A GROUP HEADER: the group's name, the fold icon its state calls for, and the
-- visible index a click on it folds by. The icon says what the click WILL do — an
-- open group offers the minus that shuts it, a shut one the plus that opens it.
local foldIcon = "Interface\\Buttons\\UI-MinusButton-Up"
if isCollapsed then
foldIcon = "Interface\\Buttons\\UI-PlusButton-Up"
end
header.index = factionIndex
header.isCollapsed = isCollapsed
header:SetText(name)
header:SetNormalTexture(foldIcon)
header:Show()
bar:Hide()
check:Hide()
return
end
-- A FACTION: the name and the standing word it currently wears.
local standingText = BenillaReputation_StandingLabel(standingID, gender)
local factionName = getglobal("ReputationBar" .. i .. "FactionName")
factionName:SetText(name)
getglobal("ReputationBar" .. i .. "FactionStanding"):SetText(standingText)
-- The crossed-swords pennant off the row's right end.
local atWarIndicator = getglobal("ReputationBar" .. i .. "AtWarCheck")
if atWarWith then
atWarIndicator:Show()
else
atWarIndicator:Hide()
end
-- The engine hands the rank window over ABSOLUTE for exactly this reason:
-- reputation.rs's module doc names the reference's own normalization (ref-lua l.79-82)
-- as the contract the tuple is written against. The bar's range is the window's WIDTH,
-- its value the distance into it, and the hover tooltip is the same pair spelled out.
local rankWidth = barMax - barMin
local progress = barValue - barMin
bar.id = factionIndex
bar.standingText = standingText
bar.tooltip = HIGHLIGHT_FONT_COLOR_CODE .. " " .. progress .. " / " .. rankWidth .. FONT_COLOR_CODE_CLOSE
bar:SetMinMaxValues(0, rankWidth)
bar:SetValue(progress)
-- UNGUARDED, deliberately, exactly as the reference has it (ref-lua l.89).
-- `GetFactionInfo` is eleven wide on EVERY path including a miss, and its miss puts the
-- client's own `1` in the standingID slot for precisely this index (reputation.rs
-- `unknown_faction`) — so the caller's `factionIndex <= numFactions` gate is belt, and
-- that `1` is braces. Adding a nil-check here would be inventing a branch the client
-- does not have and cannot reach.
local color = FACTION_BAR_COLORS[standingID]
bar:SetStatusBarColor(color.r, color.g, color.b)
bar:SetID(factionIndex)
bar:Show()
header:Hide()
-- The watched checkmark rides the END of the name string, so the name box gives up 10
-- of its 110px to make room for it (ref-lua l.95-103).
if isWatched then
check:Show()
factionName:SetWidth(100)
check:SetPoint("LEFT", factionName, "LEFT", factionName:GetStringWidth(), 0)
else
check:Hide()
factionName:SetWidth(110)
end
-- The selected row keeps its glow with the mouse away, and is the one that fills the
-- popup while the popup is open (ref-lua l.105-139).
local selected = factionIndex == GetSelectedFaction()
if selected and ReputationDetailFrame:IsShown() then
BenillaReputationDetail_Fill(factionIndex, name, description, atWarWith,
canToggleAtWar, isWatched)
end
BenillaReputationBar_SetGlow("ReputationBar" .. i, selected)
end
-- The list page (ref-lua ReputationFrame_Update, l.32-149 is the specification): fit the
-- scroll window to the list, then bind each of the fixed slots to the row the offset puts
-- under it, on the house faux-scroll kit rather than the reference's real ScrollFrame
-- (this file's header comment, deviation 4).
function ReputationFrame_Update()
local numFactions = GetNumFactions()
local scrollFrame = getglobal("ReputationListScrollFrame")
-- ref-lua l.39-41. The reset is a no-op in every reachable case — FauxScrollFrame_Update
-- has already clamped `frame.offset` to 0 when the list fits and seated the bar at
-- `offset * valueStep`, so this SetValue writes the value the Slider already holds and
-- its change-gate (decision 0250) drops it before it could re-enter this function.
-- Kept because it is the reference's line, not because it does anything.
local showBar = FauxScrollFrame_Update(scrollFrame, numFactions, NUM_FACTIONS_DISPLAYED, REPUTATIONFRAME_FACTIONHEIGHT)
-- Our trough is not the reference's business: it follows the bar the ref just decided
-- about (1860).
BenillaScrollTrough_Follow("ReputationListScrollFrame")
if not showBar then
getglobal("ReputationListScrollFrameScrollBar"):SetValue(0)
end
local factionOffset = FauxScrollFrame_GetOffset(scrollFrame)
local gender = UnitSex("player")
for i = 1, NUM_FACTIONS_DISPLAYED do
local factionIndex = factionOffset + i
if factionIndex <= numFactions then
BenillaReputationSlot_Paint(i, factionIndex, gender)
else
BenillaReputationSlot_Clear(i)
end
end
-- ref-lua l.146-148: nothing selected means nothing to detail. `GetSelectedFaction`
-- answers 0 both for "never selected" and for "the selected faction is folded away
-- under a collapsed header" (reputation.rs), so a fold closes the popup too.
if GetSelectedFaction() == 0 then
ReputationDetailFrame:Hide()
end
end
-- A bar click toggles the popup for that faction (ref-lua ReputationBar_OnClick,
-- l.151-159): the row that is already showing shuts it, any other row moves the selection
-- and opens it. Parameterless and reading `this`, exactly as the reference writes it: our
-- dispatcher sets `this` before invoking a handler (script/event.rs `invoke_with_globals`)
-- and it stays set through the call, the same contract ScrollTemplates.xml's
-- `FauxScrollFrame_OnVerticalScroll` already relies on.
function ReputationBar_OnClick()
if ReputationDetailFrame:IsShown() and GetSelectedFaction() == this.id then
ReputationDetailFrame:Hide()
return
end
SetSelectedFaction(this.id)
ReputationDetailFrame:Show()
ReputationFrame_Update()
end
-- A header click folds or unfolds its group (ref l.9-15, the template's own OnClick).
-- THE ADDED LINE is the repaint: the reference's `Collapse`/`ExpandFactionHeader` are C
-- bindings that raise UPDATE_FACTION on their way out, ours are pure model writes (this
-- file's header comment, deviation 3) — SkillFrame.xml's `BenillaSkillTypeLabel_OnClick`
-- carries the identical addition for the identical reason.
--
-- `header.index` is the slot's own VISIBLE row index, stamped by the update loop that last
-- painted it, and it must stay that: an index that is not a header — `0`, a bar row, a stale
-- one past the end — folds or unfolds EVERY header instead (the client's own fall-through at
-- `0x4d6a50`/`0x4d6aa0`, which reputation.rs reproduces). Nothing here can reach that arm,
-- because a slot only ever shows its header twin while the loop has just written a header
-- index into it; the note is here so a future edit does not reach it by accident.
function BenillaReputationHeader_OnClick(header)
if header.isCollapsed then
ExpandFactionHeader(header.index)
else
CollapseFactionHeader(header.index)
end
ReputationFrame_Update()
end
-- The popup's three boxes (ref l.758-766, l.810-818, l.839-848). The at-war box is the
-- reference's own body unchanged — it already repaints. The inactive box gains the added
-- repaint (this file's header comment, deviation 3): its flip re-parents the row under the
-- "Inactive" header there and then, so the list on screen is stale the moment it returns.
-- The main-screen box gets NO addition — see the same note for why the round trip, not a
-- repaint, is what moves the watched checkmark.
function BenillaReputationAtWarBox_OnClick(box)
FactionToggleAtWar(GetSelectedFaction())
if box:GetChecked() then
PlaySound("igMainMenuOptionCheckBoxOn")
else
PlaySound("igMainMenuOptionCheckBoxOff")
end
ReputationFrame_Update()
end
function BenillaReputationInactiveBox_OnClick(box)
if box:GetChecked() then
PlaySound("igMainMenuOptionCheckBoxOn")
SetFactionInactive(GetSelectedFaction())
else
PlaySound("igMainMenuOptionCheckBoxOff")
SetFactionActive(GetSelectedFaction())
end
ReputationFrame_Update()
end
function BenillaReputationMainScreenBox_OnClick(box)
if box:GetChecked() then
PlaySound("igMainMenuOptionCheckBoxOn")
SetWatchedFactionIndex(GetSelectedFaction())
else
PlaySound("igMainMenuOptionCheckBoxOff")
SetWatchedFactionIndex(0)
end
ReputationWatchBar_Update()
end
-- The window's wheel handler (this file's header comment, deviation 7).
function BenillaReputationFrame_OnMouseWheel(delta)
BenillaFauxScrollFrame_OnMouseWheel(getglobal("ReputationListScrollFrame"), delta)
end
-- ── Lifecycle (ref-lua l.16-30) ─────────────────────────────────────────────────────────
function ReputationFrame_OnLoad()
local frame = this
frame:RegisterEvent("UPDATE_FACTION")
-- The repaint is wired in the scroll frame's own `<OnVerticalScroll>` below, which is
-- where the reference's kit puts it (1860).
end
function ReputationFrame_OnShow()
ReputationFrame_Update()
end
function ReputationFrame_OnEvent(event)
if event == "UPDATE_FACTION" then
if this:IsVisible() then
ReputationFrame_Update()
end
end
end
]]></Script>
<!--
The category header row (ref l.4-56): 302x13, the +/- fold icon at LEFT(3,0) and the group
name at LEFT(20,0). The handler is a chunk function, not the reference's inline body, so the
added repaint has a home with a comment on it (chunk-boundary rule, SkillFrame.xml l.96-100).
-->
<Button name="ReputationHeaderTemplate" virtual="true">
<Size><AbsDimension x="302" y="13"/></Size>
<Scripts>
<OnClick>BenillaReputationHeader_OnClick(self)</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-MinusButton-UP">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</NormalTexture>
<HighlightTexture name="$parentHighlight" file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</HighlightTexture>
<ButtonText name="$parentNormalText">
<Size><AbsDimension x="275" y="12"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="20" y="0"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
<DisabledFont inherits="GameFontHighlight" justifyH="LEFT"/>
</Button>
<!--
The faction row (ref l.57-210): a 137x13 StatusBar carrying the whole row's furniture — the
two-piece frame art that reaches 126px LEFT of the bar itself, the name and standing
FontStrings, the hover/selection highlight pair, the watched checkmark, and the at-war
pennant hanging off the right end. The HitRectInsets stretch the row's MOUSE rect back over
the name (ref l.61-63: a NEGATIVE left inset widens), so clicking the faction's name selects
it just like clicking its bar.
`enableMouse="true"` is ADDED (this file's header comment, deviation 5) — a StatusBar is not
mouse-enabled by construction in this engine, and without it the OnEnter/OnLeave/OnMouseUp
below would never fire.
-->
<StatusBar name="ReputationBarTemplate" virtual="true" enableMouse="true" drawLayer="BACKGROUND" minValue="0" maxValue="1" defaultValue="1">
<Size><AbsDimension x="137" y="13"/></Size>
<HitRectInsets><AbsInset left="-126" right="3" top="-2" bottom="-2"/></HitRectInsets>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentReputationBarLeft" file="Interface\PaperDollInfoFrame\UI-Character-ReputationBar">
<Size><AbsDimension x="256" y="22"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-126" y="4"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.34375"/>
</Texture>
<Texture name="$parentReputationBarRight" file="Interface\PaperDollInfoFrame\UI-Character-ReputationBar">
<Size><AbsDimension x="16" y="24"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentReputationBarLeft" relativePoint="TOPRIGHT"/></Anchors>
<TexCoords left="0" right="0.0625" top="0.34375" bottom="0.71875"/>
</Texture>
<FontString name="$parentFactionName" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-119" y="0"/></Offset></Anchor></Anchors>
</FontString>
<!-- The reference's `text="Revered"` placeholder is dropped (this file's header
comment, deviation 6). -->
<FontString name="$parentFactionStanding" inherits="GameFontHighlightSmall">
<Anchors><Anchor point="CENTER"/></Anchors>
</FontString>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentHighlight1" file="Interface\PaperDollInfoFrame\UI-Character-ReputationBar-Highlight" alphaMode="ADD" hidden="true">
<Size><AbsDimension x="256" y="28"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentReputationBarLeft"><Offset><AbsDimension x="-2" y="3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1" top="0" bottom="0.4375"/>
</Texture>
<Texture name="$parentHighlight2" file="Interface\PaperDollInfoFrame\UI-Character-ReputationBar-Highlight" alphaMode="ADD" hidden="true">
<Size><AbsDimension x="17" y="28"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentHighlight1" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.06640625" top="0.4375" bottom="0.875"/>
</Texture>
<Texture name="$parentCheck" file="Interface\Buttons\UI-CheckBox-Check" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentFactionName" relativePoint="RIGHT"/></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The crossed-swords pennant at the row's right end, shown for a faction you are at
war with (ref l.150-187). `enableMouse` added for its hover, same reason as the
bar's. -->
<Frame name="$parentAtWarCheck" enableMouse="true" hidden="true">
<Size><AbsDimension x="24" y="22"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentReputationBarRight" relativePoint="RIGHT">
<Offset><AbsDimension x="-2" y="0"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ReputationBar">
<Size><AbsDimension x="24" y="22"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0.0625" right="0.15625" top="0.34375" bottom="0.71875"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(REPUTATION_STATUS_AT_WAR, nil, nil, nil, nil, 1)
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
</Frames>
<!-- ref l.189-207 is the specification: hovering swaps the standing word for the raw
"value / max" the update stamped on the bar and lights the glow; leaving puts the word
back and drops the glow again — unless this row is the SELECTED one, which keeps it.
Both strings and both glow states were put on the bar by the update above, so the
hover only chooses between them; the glow pair goes through that function's own verb
so a hover and a repaint can never disagree about which textures are up. -->
<Scripts>
<OnEnter>
if this.tooltip then
getglobal(this:GetName() .. "FactionStanding"):SetText(this.tooltip)
end
BenillaReputationBar_SetGlow(this:GetName(), 1)
</OnEnter>
<OnLeave>
getglobal(this:GetName() .. "FactionStanding"):SetText(this.standingText)
BenillaReputationBar_SetGlow(this:GetName(), GetSelectedFaction() == this.id)
</OnLeave>
<OnMouseUp>ReputationBar_OnClick()</OnMouseUp>
</Scripts>
<BarTexture file="Interface\PaperDollInfoFrame\UI-Character-Skills-Bar"/>
<BarColor r=".25" g=".25" b=".75"/>
</StatusBar>
<!-- id=3, the reference's own (ref l.211): a page's id is the slot it occupies in the WINDOW'S
OWN tab row, and ToggleCharacter selects the tab from it (ref CharacterFrame.lua:11).
`character_tests.rs` pins every page's id against its slot in
CHARACTERFRAME_SUBFRAMES. -->
<Frame name="ReputationFrame" enableMouse="true" parent="CharacterFrame" hidden="true" id="3">
<!-- The real 384x512 page. The explicit `<Size>` + TOPLEFT `<Anchor>` below is an exact
equivalent of the reference's `setAllPoints="true"` here — CharacterFrame, the parent
this frame declares, is itself 384x512 (ref CharacterFrame.xml l.4-7) — and it is kept
rather than swapped because it is the geometry the page ships with, not because the
attribute would not work. SkillFrame.xml and PetPaperDollFrame.xml both carry the
identical pair. -->
<Size><AbsDimension x="384" y="512"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="CharacterFrame" relativePoint="TOPLEFT"/></Anchors>
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="75"/></HitRectInsets>
<Layers>
<Layer level="BACKGROUND">
<!-- The four quadrant slabs (ref l.216-264), at the reference's own four TOPLEFT
offsets. The "General" pair is the variant every PaperDollInfoFrame page shares
and carries the portrait-ring cutout (this file's header comment). -->
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="258" y="-1"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-BottomLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-257"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-BottomRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="258" y="-257"/></Offset></Anchor></Anchors>
</Texture>
<!-- The two column headings (ref l.265-282). `text=` carries the GlobalStrings KEY,
which the loader resolves against the shipped strings (loader `resolve_text`),
so the row is localized like the reference's. -->
<FontString name="ReputationFrameFactionLabel" inherits="GameFontHighlight" text="FACTION">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="70" y="-57"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="ReputationFrameStandingLabel" inherits="GameFontHighlight" text="STANDING">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="215" y="-59"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The 15 row slots (ref l.286-555): bar 1 at TOPLEFT(150,-86), each next chained
BOTTOMLEFT to TOPLEFT by (0,-10) — a 23px pitch over a 13px bar. Every header sits
at its own bar's TOPLEFT minus 125, so the two occupy the same row and only one of
them is ever shown. Interleaved bar/header exactly as the reference declares them,
which is also their z order. -->
<StatusBar name="ReputationBar1" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="150" y="-86"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader1" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar1"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar2" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader2" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar2"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar3" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader3" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar3"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar4" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader4" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar4"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar5" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader5" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar5"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar6" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader6" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar6"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar7" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader7" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar7"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar8" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader8" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar8"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar9" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar8" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader9" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar9"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar10" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader10" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar10"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar11" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar10" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader11" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar11"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar12" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar11" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader12" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar12"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar13" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar12" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader13" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar13"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar14" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar13" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader14" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar14"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<StatusBar name="ReputationBar15" inherits="ReputationBarTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar14" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</StatusBar>
<Button name="ReputationHeader15" inherits="ReputationHeaderTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="ReputationBar15"><Offset><AbsDimension x="-125" y="0"/></Offset></Anchor></Anchors>
</Button>
<!-- The scroll bar's TROUGH — the shared kit's template, replacing the reference's own
two loose textures (ref l.572-600; this file's header comment, deviation 4). It
carries NO anchors: it seats itself on its bar the first time
BenillaScrollBar_SetShown touches it, and the offsets that seat it were DERIVED
from this very window — ScrollTemplates.xml's `BenillaScrollTrough_Seat` cites
ref l.573-599 against ref-UIPanelTemplates.xml l.166-181 for its 21-above /
20-below / 8-left hang. Checked against the numbers below rather than assumed: the
kit's bar hangs at the scroll frame's TOPRIGHT+(6,-16) / BOTTOMRIGHT+(6,+16), so
the seated trough lands at TOPRIGHT+(-2,+5) and BOTTOMRIGHT+(29,-4) — 31 wide,
and the reference's own (-2,+5)/(-2,-4) for a 31-wide piece, to the pixel.
Declared BEFORE the faux frame so it draws beneath the bar. -->
<Frame name="ReputationListScrollFrameScrollBarTrough" inherits="BenillaScrollTroughTemplate"/>
<!-- The faux scroll frame (ref l.556-602): 296x354 at the page's TOPRIGHT(-66,-76),
which puts its left edge on the header column and its right edge 66 in from the
page. Its `<OnVerticalScroll>` is what re-binds the rows (1860). -->
<ScrollFrame name="ReputationListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="296" y="354"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-66" y="-76"/></Offset></Anchor></Anchors>
<Scripts>
<!-- The reference's kit writes `frame.offset` ONLY from here: the bar's
OnValueChanged calls `SetVerticalScroll`, which lands in this handler.
Our transcription used a `frame.updateFunc` field instead, which the
reference does not have — without this the bar moves and the list does
not follow (1860). -->
<OnVerticalScroll>FauxScrollFrame_OnVerticalScroll(REPUTATIONFRAME_FACTIONHEIGHT, ReputationFrame_Update)</OnVerticalScroll>
</Scripts>
</ScrollFrame>
<!-- The two newbie tips over the column headings (ref l.603-630): invisible frames
exactly covering their FontString. `enableMouse` added, same engine rule as the bar
template's. -->
<Frame name="FactionMouseOver" enableMouse="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ReputationFrameFactionLabel"/>
<Anchor point="BOTTOMRIGHT" relativeTo="ReputationFrameFactionLabel"/>
</Anchors>
<Scripts>
<OnEnter>GameTooltip_AddNewbieTip(FACTION, 1.0, 1.0, 1.0, REPUTATION_FACTION_DESCRIPTION, 1)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<Frame name="StandingMouseOver" enableMouse="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ReputationFrameStandingLabel"/>
<Anchor point="BOTTOMRIGHT" relativeTo="ReputationFrameStandingLabel"/>
</Anchors>
<Scripts>
<OnEnter>GameTooltip_AddNewbieTip(STANDING, 1.0, 1.0, 1.0, REPUTATION_STANDING_DESCRIPTION, 1)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Frame>
<!--
The detail popup (ref l.631-852): a 212x203 dialog hanging off the window's own
TOPRIGHT, opened by clicking a bar and carrying the faction's description plus the
three switches. Declared LAST so it draws over the list.
-->
<Frame name="ReputationDetailFrame" enableMouse="true" hidden="true">
<Size><AbsDimension x="212" y="203"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ReputationFrame" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="-33" y="-28"/></Offset>
</Anchor>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="ReputationDetailFactionName" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="0" y="12"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="20" y="-21"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="ReputationDetailFactionDescription" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="170" y="0"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ReputationDetailFactionName" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Reputation-DetailBackground">
<Size><AbsDimension x="256" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="11" y="-11"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture name="ReputationDetailCorner" file="Interface\DialogFrame\UI-DialogBox-Corner">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-6" y="-7"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="ReputationDetailDivider" file="Interface\DialogFrame\UI-DialogBox-Divider">
<Size><AbsDimension x="256" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="9" y="-131"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The reference's own `inherits="UIPanelCloseButton"` (ref l.720), and this is
the FIRST benilla window to consume that shared template rather than
carrying a private copy of the art — the posture UIPanelTemplates.xml's toc
entry describes as the queue it is waiting in. Its OnClick is
`HideUIPanel(this:GetParent())`, which for a frame that is not a registered
UIPanel falls through UiPanels.xml's walk to a plain `frame:Hide()` — the
reference's behaviour on this same button. -->
<Button name="ReputationDetailCloseButton" inherits="UIPanelCloseButton">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-3" y="-3"/></Offset></Anchor></Anchors>
</Button>
<!-- At War (ref l.729-791): the ONE box the reference builds from bare textures
rather than a template, because its checked art is the crossed swords
(UI-CheckBox-SwordCheck) at 32x32 rather than the 26x26 tick. -->
<CheckButton name="ReputationDetailAtWarCheckBox">
<Size><AbsDimension x="26" y="26"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="14" y="34"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontNormalSmall">
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
getglobal(this:GetName() .. "Text"):SetText(AT_WAR)
getglobal(this:GetName() .. "Text"):SetVertexColor(RED_FONT_COLOR.r, RED_FONT_COLOR.g, RED_FONT_COLOR.b)
</OnLoad>
<OnClick>BenillaReputationAtWarBox_OnClick(self)</OnClick>
<OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(REPUTATION_AT_WAR_DESCRIPTION, nil, nil, nil, nil, 1)
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
<HighlightTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\UI-CheckBox-SwordCheck">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="3" y="-5"/></Offset></Anchor></Anchors>
</CheckedTexture>
<DisabledCheckedTexture file="Interface\Buttons\UI-CheckBox-Check-Disabled"/>
</CheckButton>
<!-- Move to Inactive (ref l.792-820) and Show as Experience Bar (ref l.821-850),
both on the reference's own `OptionsCheckButtonTemplate`
(OptionsFrameTemplates.xml). The template also carries an OnClick that plays
the on/off tick; the instance's own block below REPLACES it (the loader
SetScripts each `<Scripts>` in order, the instance last — ScrollTemplates.xml's
header states the rule), which is why the sound is not doubled. -->
<CheckButton name="ReputationDetailInactiveCheckBox" inherits="OptionsCheckButtonTemplate">
<Size><AbsDimension x="26" y="26"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="ReputationDetailAtWarCheckBoxText" relativePoint="RIGHT">
<Offset><AbsDimension x="3" y="0"/></Offset>
</Anchor>
</Anchors>
<HitRectInsets><AbsInset left="0" right="0" top="0" bottom="0"/></HitRectInsets>
<Scripts>
<OnLoad>getglobal(this:GetName() .. "Text"):SetText(MOVE_TO_INACTIVE)</OnLoad>
<OnClick>BenillaReputationInactiveBox_OnClick(self)</OnClick>
</Scripts>
</CheckButton>
<CheckButton name="ReputationDetailMainScreenCheckBox" inherits="OptionsCheckButtonTemplate">
<Size><AbsDimension x="26" y="26"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ReputationDetailAtWarCheckBox" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="3"/></Offset>
</Anchor>
</Anchors>
<HitRectInsets><AbsInset left="0" right="0" top="0" bottom="0"/></HitRectInsets>
<Scripts>
<OnLoad>getglobal(this:GetName() .. "Text"):SetText(SHOW_FACTION_ON_MAINSCREEN)</OnLoad>
<OnClick>BenillaReputationMainScreenBox_OnClick(self)</OnClick>
</Scripts>
</CheckButton>
</Frames>
</Frame>
</Frames>
<Scripts>
<OnLoad>ReputationFrame_OnLoad()</OnLoad>
<OnShow>ReputationFrame_OnShow()</OnShow>
<OnEvent>ReputationFrame_OnEvent(event)</OnEvent>
<OnMouseWheel>BenillaReputationFrame_OnMouseWheel(arg1)</OnMouseWheel>
</Scripts>
</Frame>
</Ui>
+24 -73
View File
@@ -63,7 +63,7 @@
TradeFrameTradeButton:Enable()
TradeFrame_Update()
elseif event == "PLAYER_TRADE_MONEY" then
BenillaMoneyInput_SetCopper("TradePlayerInputMoney", GetPlayerTradeMoney())
MoneyInputFrame_SetCopper(TradePlayerInputMoney, GetPlayerTradeMoney())
elseif event == "TRADE_CLOSED" then
HideUIPanel(frame)
elseif event == "TRADE_PLAYER_ITEM_CHANGED" then
@@ -82,17 +82,17 @@
function TradeFrame_OnHide()
-- The client-side close verb (ref TradeFrame_OnHide l.144) → CMSG_CANCEL_TRADE + a local
-- clear (the app's drain); clear our money entry so a later trade opens fresh (ref l.146).
BenillaMoneyInput_ResetMoney("TradePlayerInputMoney")
MoneyInputFrame_ResetMoney(TradePlayerInputMoney)
CloseTrade()
end
-- A keystroke in our money input (ref TradeFrame_UpdateMoney l.162): read the copper total,
-- clamp to what we actually carry, and offer it → SetTradeMoney → CMSG_SET_TRADE_GOLD.
function TradeFrame_UpdateMoney()
local copper = BenillaMoneyInput_GetCopper("TradePlayerInputMoney")
local copper = MoneyInputFrame_GetCopper(TradePlayerInputMoney)
if GetMoney and copper > GetMoney() then
copper = GetPlayerTradeMoney()
BenillaMoneyInput_SetCopper("TradePlayerInputMoney", copper)
MoneyInputFrame_SetCopper(TradePlayerInputMoney, copper)
end
SetTradeMoney(copper)
end
@@ -468,77 +468,28 @@
<Scripts><OnClick>HideUIPanel(TradeFrame)</OnClick></Scripts>
</Button>
<!-- Our gold — EDITABLE (decision 0592 P2, ref TradePlayerInputMoneyFrame l.553): the shared
BenillaMoneyInput trio (three numeric edit boxes Gold/Silver/Copper). A keystroke drives
TradeFrame_UpdateMoney → SetTradeMoney → CMSG_SET_TRADE_GOLD; the server's echo is
reflected back through PLAYER_TRADE_MONEY (below). Box layout copied from the proven
send-mail widget (MailFrame.xml). -->
<Frame name="TradePlayerInputMoney">
<!-- Our gold — EDITABLE (ref TradeFrame.xml l.553 `TradePlayerInputMoneyFrame`). The three
boxes come off the chain now: `MoneyInputFrameTemplate` out of
`Interface\FrameXML\MoneyInputFrame.xml`, which benilla.toc has sourced since 1751
window 23. Our inline trio was a verbatim copy of it — same child names, same
textures and TexCoords, same tab chain, same 58/30/30 geometry — so this is the
rare swap that changes no pixel by itself (1882).
`SetMode("compact")` is the reference's own call on this frame and DOES move
things: Gold 58->56 wide, Silver's offset 26->22, Copper's 16->11. We did not have
it, so the trade money boxes tighten by a few pixels. That is the migration
restoring the reference's layout, not a new choice.
A keystroke drives TradeFrame_UpdateMoney -> SetTradeMoney -> CMSG_SET_TRADE_GOLD,
registered through the reference's own callback slot rather than a hand-written
OnTextChanged on each box; the server's echo comes back on PLAYER_TRADE_MONEY. -->
<Frame name="TradePlayerInputMoney" inherits="MoneyInputFrameTemplate">
<Size><AbsDimension x="176" y="18"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="-73"/></Offset></Anchor></Anchors>
<Frames>
<EditBox name="$parentGold" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="58" y="20"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="8" y="20"/></Size><Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors><TexCoords left="0" right="0.0625" top="0" bottom="0.625"/></Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="8" y="20"/></Size><Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors><TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/></Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="10" y="20"/></Size><Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/><Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/></Anchors><TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/></Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors><TexCoords left="0" right="0.25" top="0" bottom="1"/></Texture>
</Layer>
</Layers>
<Scripts>
<OnTabPressed>getglobal(this:GetParent():GetName().."Silver"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName().."Silver"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() end</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<EditBox name="$parentSilver" letters="2" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="30" y="20"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentGold" relativePoint="RIGHT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="8" y="20"/></Size><Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors><TexCoords left="0" right="0.0625" top="0" bottom="0.625"/></Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="8" y="20"/></Size><Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors><TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/></Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="10" y="20"/></Size><Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/><Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/></Anchors><TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/></Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor></Anchors><TexCoords left="0.25" right="0.5" top="0" bottom="1"/></Texture>
</Layer>
</Layers>
<Scripts>
<OnTabPressed>getglobal(this:GetParent():GetName().."Copper"):SetFocus()</OnTabPressed>
<OnEnterPressed>getglobal(this:GetParent():GetName().."Copper"):SetFocus()</OnEnterPressed>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() end</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<EditBox name="$parentCopper" letters="2" historyLines="0" numeric="true" autoFocus="false">
<Size><AbsDimension x="30" y="20"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentSilver" relativePoint="RIGHT"><Offset><AbsDimension x="16" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="8" y="20"/></Size><Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors><TexCoords left="0" right="0.0625" top="0" bottom="0.625"/></Texture>
<Texture name="$parentRight" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="8" y="20"/></Size><Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors><TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/></Texture>
<Texture name="$parentMiddle" file="Interface\Common\Common-Input-Border"><Size><AbsDimension x="10" y="20"/></Size><Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/><Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/></Anchors><TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/></Texture>
<Texture file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor></Anchors><TexCoords left="0.5" right="0.75" top="0" bottom="1"/></Texture>
</Layer>
</Layers>
<Scripts>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
<OnTextChanged>if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() end</OnTextChanged>
<OnEditFocusLost>this:HighlightText(0, 0)</OnEditFocusLost>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
</Frames>
<Scripts>
<OnLoad>
MoneyInputFrame_SetMode(this, "compact")
MoneyInputFrame_SetOnvalueChangedFunc(this, TradeFrame_UpdateMoney)
</OnLoad>
</Scripts>
</Frame>
<!-- The partner's gold (read-only — GetTargetTradeMoney; ref TradeRecipientMoneyFrame l.574). -->
<Frame name="TradeRecipientMoneyFrame">
+49
View File
@@ -215,6 +215,41 @@
-- statements on the same event). `MultiActionBar_Update` is existence-guarded because the
-- ~25 per-window harnesses that load this file load no bars; the binding itself is engine
-- level and always installed. Decision 1500.
-- ── UIParentLoadAddOn (ref UIParent.lua l.638-647, and now our slot for it) ──────
-- It lived in our own BasicControls.xml until 1881 migrated that file to the chain;
-- the reference never put it there, so the move is back to where it belongs.
-- The on-demand loader every "open the auction house / tradeskill / talent panel" path
-- goes through, and the one 5 corpus addons name — FuBar_MCPFu, Fubar_RareTrackerFu, MCP,
-- MikScrollingBattleText, SpecialTalent: five separate addons, not one library replicated
-- (1207/1240).
--
-- Every piece was already here and only the join was missing: `LoadAddOn` answers the
-- reference's own `(1, nil)` / `(nil, reason)` pair, `message` and `TEXT` are directly
-- above, and the `ADDON_*` reason words come from the real `GlobalStrings.lua` we RUN off
-- the patch chain rather than copy (1234).
--
-- **`FailedAddOnLoad` is the reference's own latch, and it is not decoration.** Without it
-- an addon that retries the load from a handler raises a modal on every attempt; with it
-- the failure shows once per addon name per session. It is exactly the detail this would
-- have lacked had it been reconstructed from the name instead of read.
--
-- Every reason our `LoadAddOn` can answer resolves to a string: MISSING, DISABLED,
-- NOT_DEMAND_LOADED, DEP_MISSING and DEP_DISABLED all have an `ADDON_..reason` word
-- (GlobalStrings l.16-31), the same reason set wow-re's `ui.md` l.2396 carves off
-- `0x51e780`. So the `getglobal` below cannot hand `format` a nil.
FailedAddOnLoad = {};
function UIParentLoadAddOn(name)
local loaded, reason = LoadAddOn(name)
if not loaded then
if not FailedAddOnLoad[name] then
message(format(TEXT(ADDON_LOAD_FAILED), name, TEXT(getglobal("ADDON_" .. reason))))
FailedAddOnLoad[name] = true
end
end
return loaded
end
-- ref UIParent.lua:223, whose slot this is — immediately above `UIParent_OnEvent`, the
-- same neighbour it has there. It moved here from our own InspectFrame.xml when 1832
-- retired that file: the name belongs to UIParent in the reference, and the window it
@@ -263,6 +298,20 @@
end
end
-- ref UIParent.lua's own `RaidFrame_LoadUI` (l.182-184), verbatim — the fourth squatter,
-- after `InspectUnit` (1832), `ToggleTalentFrame` (1833) and `ShowMacroFrame` (1848).
--
-- **Unlike `MacroFrame_LoadUI`, this one cannot be left out.** That one was omitted because
-- nothing on the chain calls it; the reference's own `RaidFrame.lua` calls THIS on
-- `RAID_ROSTER_UPDATE` and again at `PLAYER_LOGIN` with a raid up (l.34, l.39), so its
-- absence is a raise every time the roster moves. It is a FrameXML function, not an engine
-- verb, and this file is our stand-in for `UIParent.lua` — the same relationship the three
-- above have. `UIParentLoadAddOn` is just above, and it no-ops on an addon that is
-- already loaded, which ours is: benilla.toc takes `Blizzard_RaidUI` eagerly (1874).
function RaidFrame_LoadUI()
UIParentLoadAddOn("Blizzard_RaidUI");
end
function UIParent_OnEvent(event)
-- The reference's own first arm (UIParent.lua l.230-235). `LocalizeFrames` comes from
-- the sourced `Interface\FrameXML\Localization.xml` (1751 window 5) and is the file
+165
View File
@@ -1583,6 +1583,57 @@
end
end
-- ── Two dialogs rehomed from our retired RaidFrame.xml (1874) ──────────────────────────
-- Their reference home is `StaticPopup.lua`, which we do not load — this file is our
-- stand-in for that engine, so this is where they belong. They came here because the raid
-- pane went to the chain and the reference's own `RaidFrame.lua` does not declare them:
-- deleting our file took `CONFIRM_RESET_INSTANCES` with it, and `UnitPopup.xml`'s "Reset
-- all instances" row calls `StaticPopup_Show` on exactly that name.
-- ── Add Member's dialog (ref StaticPopup.lua l.871-895) ─────────────────────────────────
StaticPopupDialogs["ADD_RAIDMEMBER"] = {
text = ADD_RAIDMEMBER_LABEL,
button1 = "Accept",
button2 = "Cancel",
hasEditBox = 1,
maxLetters = 12,
timeout = 0,
hideOnEscape = 1,
OnAccept = function()
InviteByName(getglobal("StaticPopup1EditBox"):GetText())
end,
OnShow = function()
getglobal("StaticPopup1EditBox"):SetFocus()
end,
EditBoxOnEnterPressed = function()
InviteByName(getglobal("StaticPopup1EditBox"):GetText())
StaticPopup_Hide("ADD_RAIDMEMBER")
end,
}
-- ── "Reset all instances" (ref StaticPopup.lua l.23-33; decision 1748) ──────────────────
-- Raised by UnitPopup's SELF menu, and registered HERE because this file is the instance
-- house — the same rule that keeps the pet dialogs in PetActionBar.xml. `ResetInstances()`
-- is the only call in the client that sends `CMSG_RESET_INSTANCES`; everything else in the
-- lockout family is the server talking.
--
-- The answer is per instance rather than one verdict, and it arrives as CHAT_MSG_SYSTEM
-- lines the engine composes: an `SMSG_INSTANCE_RESET` per dungeon that reset, and — on
-- the group path only — an `SMSG_INSTANCE_RESET_FAILED` per dungeon that would not.
-- Nothing back at all is the ordinary answer for a character with nothing resettable. So
-- there is nothing to show here and no result to wait for.
StaticPopupDialogs["CONFIRM_RESET_INSTANCES"] = {
text = CONFIRM_RESET_INSTANCES,
button1 = YES,
button2 = NO,
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
OnAccept = function()
ResetInstances()
end,
}
StaticPopupDialogs["PARTY_INVITE"] = {
text = INVITATION,
button1 = ACCEPT,
@@ -2042,6 +2093,110 @@
return nil
end
-- ── The FLASH kit (ref UIParent.lua l.1166, l.1231-1333), transcribed ────────────────
-- Its home is `UIParent.lua`, which we do not load; this file is our stand-in for that
-- engine's half, and the FADE kit it drives through already lives here for the same reason.
-- Every field it sets on the frame — `fadeInTime`, `fadeOutTime`, `flashDuration`,
-- `showWhenDone`, `flashTimer`, `flashMode`, `flashInHoldTime`, `flashOutHoldTime` — is the
-- reference's own, and it reaches the fade kit through `fadeHoldTime`/`finishedFunc`, both
-- of which `UIFrameFade` above already honours.
--
-- Built because two stock windows call it and nothing answered: `SpellBookFrame` and
-- `HelpFrame` (1879). It is FrameXML, not an engine verb, so this is a transcription and
-- not the stub 1203 forbids.
FLASHFRAMES = {}
function UIFrameFlashRemoveFrame(frame)
local index = 1
while FLASHFRAMES[index] do
if FLASHFRAMES[index] == frame then
table.remove(FLASHFRAMES, index)
else
index = index + 1
end
end
end
function UIFrameFlash(frame, fadeInTime, fadeOutTime, flashDuration, showWhenDone, flashInHoldTime, flashOutHoldTime)
if not frame then return end
-- Already flashing: the reference returns rather than re-arming (l.1234-1240).
local index = 1
while FLASHFRAMES[index] do
if FLASHFRAMES[index] == frame then return end
index = index + 1
end
frame.fadeInTime = fadeInTime
frame.fadeOutTime = fadeOutTime
frame.flashDuration = flashDuration
frame.showWhenDone = showWhenDone
frame.flashTimer = 0
frame.flashMode = "IN"
frame.flashInHoldTime = flashInHoldTime
frame.flashOutHoldTime = flashOutHoldTime
table.insert(FLASHFRAMES, frame)
BenillaFlashDriver:Show()
end
function UIFrameFlashSwitch(frameName, mode)
local frame = getglobal(frameName)
if frame then frame.flashMode = mode end
end
function UIFrameIsFlashing(frame)
local index = 1
while FLASHFRAMES[index] do
if FLASHFRAMES[index] == frame then return 1 end
index = index + 1
end
return nil
end
function UIFrameFlashStop(frame)
frame.flashDuration = 0
frame:Hide()
end
-- `flashDuration == -1` means flash forever, which is why the timer check carries that
-- guard rather than a plain `>` (ref l.1272).
function UIFrameFlashUpdate(elapsed)
local index = 1
while FLASHFRAMES[index] do
local frame = FLASHFRAMES[index]
frame.flashTimer = (frame.flashTimer or 0) + elapsed
if frame.flashTimer > frame.flashDuration and frame.flashDuration ~= -1 then
UIFrameFadeRemoveFrame(frame)
UIFrameFlashRemoveFrame(frame)
frame:SetAlpha(1.0)
frame.flashTimer = nil
if frame.showWhenDone then frame:Show() else frame:Hide() end
else
-- A flashMode is only set once the previous fade finished (ref l.1283).
if frame.flashMode then
local fadeInfo = {}
if frame.flashMode == "IN" then
fadeInfo.timeToFade = frame.fadeInTime
fadeInfo.mode = "IN"
fadeInfo.finishedFunc = UIFrameFlashSwitch
fadeInfo.finishedArg1 = frame:GetName()
fadeInfo.finishedArg2 = "OUT"
fadeInfo.fadeHoldTime = frame.flashOutHoldTime
UIFrameFade(frame, fadeInfo)
elseif frame.flashMode == "OUT" then
fadeInfo.timeToFade = frame.fadeOutTime
fadeInfo.mode = "OUT"
fadeInfo.finishedFunc = UIFrameFlashSwitch
fadeInfo.finishedArg1 = frame:GetName()
fadeInfo.finishedArg2 = "IN"
fadeInfo.fadeHoldTime = frame.flashInHoldTime
UIFrameFade(frame, fadeInfo)
end
frame.flashMode = nil
end
index = index + 1
end
end
end
function UIFrameFadeRemoveFrame(frame)
local index = FadeIndexOf(frame)
while index do
@@ -2319,6 +2474,16 @@
It runs only while FADEFRAMES has entries: the engine dispatches OnUpdate to visible
frames only (tick.rs), so hiding itself on an empty list stops the per-frame walk cold,
and UIFrameFade — the single inserter — shows it again (decision 1396's class). -->
<!-- The flash kit's tick source, the fade driver's twin (1879) — same reason, same shape: the
reference hangs `UIFrameFlashUpdate` on UIParent's OnUpdate and walks an empty list every
frame; this runs only while FLASHFRAMES has entries, and `UIFrameFlash` is the single
inserter that shows it again. Hidden at load: nothing flashes until something asks. -->
<Frame name="BenillaFlashDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts><OnUpdate>if not FLASHFRAMES[1] then this:Hide() return end UIFrameFlashUpdate(arg1)</OnUpdate></Scripts>
</Frame>
<Frame name="BenillaFadeDriver">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
+27 -4
View File
@@ -96,7 +96,7 @@ Interface\FrameXML\LocaleProperties.lua
# ScriptErrors dialog, and the DialogBoxFrame + DialogButton*Texture kit it is built from.
# Straight after Fonts.xml because those are the only things it needs (GameFontRedLarge,
# DialogButtonNormalText/HighlightText) and everything above it in FrameXML.toc is engine.
BasicControls.xml
Interface\FrameXML\BasicControls.xml
# **UIParent itself, and it must come before every frame that declares `parent="UIParent"`** —
# a parent name is resolved at LOAD, and a name that does not exist yet does not error: the loader
# warns and silently falls back to the enclosing frame (`loader/mod.rs`, "names no frame"). With
@@ -678,7 +678,19 @@ ContainerFrameAdapters.xml
# file's own comment carries the justification — so `FACTION_BAR_COLORS` and
# `ReputationWatchBar_Update` are already defined when this page paints a row or ticks the
# "Show as Experience Bar" box.
ReputationFrame.xml
# ══ THE REPUTATION PANE — the reference's own, off the player's chain (1751, scoped by 1844) ══
# One line replaces one of ours: 47 of our 48 declared names were already the reference's, and the
# one that was not (`ReputationListScrollFrameScrollBarTrough`) is our own scroll kit's. Nothing
# gated it — the stock file inherits its own two row templates plus `UIPanelCloseButton`, fonts and
# `OptionsCheckButtonTemplate`, and that last came off the chain with 1841.
#
# THE CO-EDIT was compulsory. The reference declares the reputation WATCH BAR in this same file;
# ours lived in `ActionBar.xml` behind a note defending the placement. Frame globals publish
# NON-overwritingly and ActionBar.xml is manifest line 340 against this file's — so ours would have
# kept the name and the chain's bar would have been built and unreachable, which is exactly what
# `GroupLootFrame` did for months (1838). The bar, its update, its two text verbs,
# `MAX_PLAYER_LEVEL` and `FACTION_BAR_COLORS` all went home with it (1875).
Interface\FrameXML\ReputationFrame.xml
# The Skills tab (decision 0437 phase 4): a CharacterFrame child page, but a separate
# top-level file — it declares `parent="CharacterFrame"`, which really attaches (1734), and
# is positioned by name via `<Anchors relativeTo="CharacterFrame">`, the geometry it ships
@@ -767,10 +779,21 @@ Interface\AddOns\Blizzard_TalentUI\Blizzard_TalentUI.xml
# GameTooltip (the newbie tips) and UIDropDownMenu (the who list's variable column) —
# all loaded above.
FriendsFrame.xml
# ══ THE RAID PANE — the reference's own, off the player's chain (1751, scoped by 1839) ══
# The social window's fourth tab, in its own file because RaidFrame is a child of FriendsFrame and
# a child cannot be declared before its parent — the reference splits it the same way and for the
# same reason (its FrameXML.toc has RaidFrame.xml on the very next line). Decision 1549.
RaidFrame.xml
#
# TWO entries replace one of ours: the pane itself, and the LoadOnDemand half that carries the
# group frames and the tear-off pullouts. 1839 measured the swap — 83 of our 84 frame names were
# already the reference's, 20 of our 22 Lua functions were, and NOTHING needed building. The one
# name that was not is `BenillaRaidDropDown`, and the reference has no raid drop-down at all: it
# reuses `FriendsDropDown`, which FriendsFrame.xml declares four manifest lines above.
#
# GAINED: `RaidPulloutFrameTemplate`, `RaidPulloutButtonTemplate`, `RaidBuffButtonTemplate` and the
# fifteen `RaidPullout*` verbs — the tear-off group pullouts we never transcribed.
Interface\FrameXML\RaidFrame.xml
Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml
# ══ THE GOSSIP WINDOW — the reference's own, off the player's chain (1751) ══════════════════
# No engine verb, no widget method, no shadowed name — all three instruments clean. It was
# deferred once on `GossipResize` reading `GetTextHeight()` in the same repaint that sets the
@@ -877,7 +900,7 @@ Interface\FrameXML\PetStable.xml
# windows grouped" cluster beside Trainer/Bank/Taxi, and loaded AFTER MoneyFrame (its eight
# money displays are SmallMoneyFrameTemplate, and it registers the AUCTION_DEPOSIT money type
# into that file's MoneyTypeInfo), after MerchantFrame (grouping only now — that file's global
# BenillaMoneyInput_* helpers for its four money inputs), after ScrollTemplates (four faux lists), after
# chain's MoneyInputFrame_* for its four money inputs, 1882), after ScrollTemplates (four faux lists), after
# UIPanelTemplates (UIPanelButtonTemplate / UIPanelCloseButton / InputBoxTemplate /
# UICheckButtonTemplate), after UIDropDownMenu (the rarity capsule) and after UiPanels (the
# panel manager's `doublewide` seat, `CharacterFrameTabButtonTemplate`, the PanelTemplates
+2
View File
@@ -98,6 +98,7 @@ mod probe_chest;
mod probe_clam;
mod probe_crossing;
mod probe_gm_ticket;
mod probe_goquest;
mod probe_guard_poi;
mod probe_mail;
mod probe_melee;
@@ -124,6 +125,7 @@ pub(crate) use probe_chest::ProbeChestPlugin;
pub(crate) use probe_clam::ProbeClamPlugin;
pub(crate) use probe_crossing::ProbeCrossingPlugin;
pub(crate) use probe_gm_ticket::ProbeGmTicketPlugin;
pub(crate) use probe_goquest::ProbeGoQuestPlugin;
pub(crate) use probe_guard_poi::ProbeGuardPoiPlugin;
pub(crate) use probe_mail::ProbeMailPlugin;
pub(crate) use probe_melee::ProbeMeleePlugin;
@@ -0,0 +1,434 @@
//! The GameObject-questgiver live probe (`WOW_PROBE_GOQUEST=1`) — the instrument for *"wanted
//! posters and quest objects are never status-queried"*, and for the answer that question turned
//! out to have (decision 1872).
//!
//! A quest-giving **GameObject** — a wanted poster, a half-eaten body, a suspicious barrel — is a
//! questgiver on the wire exactly as a creature is, and the reference client asks about it: the
//! sweep callback tests typemask bit 5 and sends `CMSG_QUESTGIVER_STATUS_QUERY` for any GameObject
//! carrying `GAMEOBJECT_FLAGS` bit 2 whose reaction toward us is `> 1`. benilla asked about no
//! GameObject at all, which is a real wire gap.
//!
//! But **the answer is refused, and nothing is ever drawn**: the reference's handler `0x5dc9f0`
//! resolves the GUID with typemask 8, a GameObject's `0x21` misses bit 3, and the packet dies at
//! `0x5dca2f`. And the real 1.12 service never sent one anyway — zero GameObject GUIDs across the
//! 1292 in the genuine sniff corpus. **vmangos does send one** (`TYPEMASK_CREATURE_OR_GAMEOBJECT`,
//! `QuestHandler.cpp:41`), so on our server the refusal is load-bearing rather than theoretical:
//! without it a wanted poster would wear a gold `!` the reference client leaves bare.
//!
//! That makes this probe's job the three-way distinction a unit test cannot make at the real
//! object: **the query went out**, **the answer came back**, and **we refused it** — where "never
//! asked" and "asked and correctly refused" both read as an empty status map. The refusal readout
//! ([`QuestGiver::refused`]) is what separates them, and an arriving status can only exist if our
//! query did.
//!
//! Four readings, in order, and the last two are the controls that make it a regression test:
//!
//! 1. **LOW** — at level [`LOW_LEVEL`], below the quest's own `MinLevel`: the server must answer
//! `1` (`UNAVAILABLE`, the grey `!` a *creature* would wear) and we must refuse it.
//! 2. **HIGH** — after a ding past `MinLevel`: the answer must change to `5` (`AVAILABLE`). A probe
//! that only ever saw one value could not tell a live query from a single stale packet.
//! 3. **NO STATUS** — `QuestGiver::status()` for the poster stays `None` throughout, sampled every
//! frame rather than at the windows. The marker layer and the minimap dot both draw from exactly
//! that map, so an empty entry *is* "no `!`, no dot".
//! 4. **THE UNIT CONTROL** — a creature questgiver in the same scene still gets and keeps its own
//! status. This is the half that must not change, and it is what would catch a refusal written
//! one bit too wide.
//!
//! ## The run recipe
//!
//! ```text
//! WOW_NOSOUND=1 WOW_USER=probe4 WOW_PASS=pprobe4 WOW_CHAR=Probefour \
//! WOW_PROBE_GOQUEST=1 cargo run -q -p benilla
//! ```
//! (the slot-keyed probe identity — `pool-N` → `probeN`/`pprobeN`/`Probe<N-spelled>`, `method.md`
//! "The local vmangos server"). `WOW_PROBE_GOQUEST=<x>,<y>,<z>[,<map>]` aims it elsewhere; the
//! default is the **Goldshire wanted poster** — `gameobject.guid` 26843, template 68
//! `Wanted Poster`, `type` 2 `GAMEOBJECT_TYPE_QUESTGIVER`, `flags` 4 `INTERACT_COND`, offering
//! quest 176 `Wanted: "Hogger"` (`MinLevel` 5, `QuestLevel` 11). It is a single **unpooled** spawn
//! with a 900 s respawn, so it is there every run — unlike the chest probe's first target, which
//! was a spawn-pool member and read "missing" most of the time (decision 1471's trap). Goldshire
//! also supplies the unit control (Marshal Dughan and the innkeeper are both questgiver-flagged),
//! which is why both halves can be read at one spot.
//!
//! Each window forces its own sweep, because that is the *only* way a GameObject GUID reaches the
//! wire: it sets the level (the reference's own `UNIT_FIELD_LEVEL` watch, and what makes the answer
//! change) and then bumps the packet epoch, so the window sweeps even if the character already
//! happened to be standing at the level the window wants. The starting level is restored on exit.
//!
//! **One trap the scan line will show you:** every probe character is permanently in GM mode
//! (`characters.extra_flags` bit `0x1`), and vmangos ORs `target->IsGameMaster()` into a
//! GameObject's `IsActivateToQuest` (`Object.cpp`) — so `GAMEOBJECT_DYN_FLAGS` reads `0x1` on
//! every quest object regardless of whether the character could actually take the quest. The
//! *dialog status* is computed without any GM term, so the windows below are unaffected; but do
//! not read the dyn-flag as "this quest is available to me", and do not gate a query on it.
//!
//! Grep `PROBE_GOQUEST:` for the verdict; the probe self-exits when it lands.
use bevy::prelude::*;
use benilla_protocol::EntityKind;
use super::probes::ProbeClock;
use crate::net::{ChatKind, ClientCommand, Guid, NetCommands, NetEntity, ObjectStore, SelfPlayer};
use crate::player::Player;
use crate::ui_quest::QuestGiver;
/// The probe's default object: the `Wanted Poster` outside the Goldshire inn (`gameobject` 26843,
/// template 68, quest 176). `[x, y, z, map]`.
const POSTER_AT: [f32; 4] = [-9668.23, 683.39, 36.33, 0.0];
/// `GAMEOBJECT_TYPE_QUESTGIVER` — the strategy type a wanted poster carries.
const GO_TYPE_QUESTGIVER: i32 = 2;
/// `GAMEOBJECT_FLAGS` bit 2 — the reference's whole GameObject query gate (`0x5eb0f2 shr eax,0x2`).
const GO_FLAG_INTERACT_COND: u32 = 0x4;
/// `UNIT_NPC_FLAGS` questgiver bit — how the unit control names itself.
const NPC_FLAG_QUESTGIVER: u32 = 0x2;
/// The two levels the two windows are read at, chosen against quest 176's own `MinLevel` 5 and
/// `QuestLevel` 11: below it the server answers `UNAVAILABLE`, above it `AVAILABLE` (and not
/// `CHAT`, which would need `level > QuestLevel + Quests.LowLevelHideDiff`).
const LOW_LEVEL: u32 = 2;
const HIGH_LEVEL: u32 = 8;
/// `DialogStatus` ids the verdict is written in (wow-re `questgiver-marker.md` §Q1's own map).
const STATUS_UNAVAILABLE: u32 = 1;
const STATUS_AVAILABLE: u32 = 5;
const SETTLE_SECS: f64 = 5.0;
const SCAN_TIMEOUT_SECS: f64 = 25.0;
/// How long a window waits for the refused answer. The query goes out on the sweep the window
/// itself forces; nothing arriving in this long means it was never sent.
const STATUS_TIMEOUT_SECS: f64 = 15.0;
/// How long a `.levelup` is given to come back down the wire as a `UNIT_FIELD_LEVEL` change.
const LEVEL_TIMEOUT_SECS: f64 = 15.0;
/// Scan radius around the landing spot, in yards.
const SCAN_RANGE: f32 = 40.0;
pub(crate) struct ProbeGoQuestPlugin;
impl Plugin for ProbeGoQuestPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<GoQuestProbe>()
.add_systems(Update, goquest_probe);
}
}
#[derive(Resource, Default)]
struct GoQuestProbe {
phase: Phase,
/// The poster's guid, once the scan finds it.
poster: Option<u64>,
/// The unit control's guid — a questgiver-flagged creature in the same scene.
control: Option<u64>,
/// The level the character was at when the probe started — restored before it exits.
start_level: Option<u32>,
/// The refused status read in each window.
low: Option<u32>,
high: Option<u32>,
/// Whether the poster ever acquired a stored status (it must not) and the control's own stored
/// status (it must have one).
poster_ever_stored: bool,
control_stored: Option<u32>,
fails: u32,
exited: bool,
}
#[derive(Default, Clone, Copy, PartialEq)]
enum Phase {
#[default]
Wait,
/// `.go` issued; letting the world stream the poster in.
Settling {
sent_at: f64,
},
/// Levelling into a window's level, then sweeping.
Level {
to: u32,
since: f64,
},
/// Waiting for the refused answer at the window's level.
Read {
level: u32,
since: f64,
},
/// Levelling back to where the character started.
Restore {
since: f64,
},
Done,
}
/// Where the probe is aimed: `WOW_PROBE_GOQUEST=<x>,<y>,<z>[,<map>]`, else [`POSTER_AT`]. Anything
/// unparseable falls back to the default rather than failing the run — the common value is `1`.
fn target() -> [f32; 4] {
let Ok(raw) = std::env::var("WOW_PROBE_GOQUEST") else {
return POSTER_AT;
};
let parts: Vec<f32> = raw
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
match parts.len() {
3 => [parts[0], parts[1], parts[2], POSTER_AT[3]],
4 => [parts[0], parts[1], parts[2], parts[3]],
_ => POSTER_AT,
}
}
/// `.levelup <delta>` on the probe's own character. Nothing is ever selected by this probe, so the
/// command's creature branch (`GetSelectedCreature`) can't fire.
fn level_to(net: &NetCommands, from: u32, to: u32) {
let delta = i64::from(to) - i64::from(from);
if delta == 0 {
return;
}
let _ = net.0.send(ClientCommand::Chat {
kind: ChatKind::Say,
target: None,
text: format!(".levelup {delta}"),
});
}
/// One window's verdict line.
fn report(label: &str, expect: u32, seen: Option<u32>) -> u32 {
match seen {
Some(s) if s == expect => {
info!("PROBE_GOQUEST: {label:<4} PASS — the server answered {s}, and we refused it");
0
}
Some(s) => {
error!("PROBE_GOQUEST: {label:<4} FAIL — refused status {s}, expected {expect}");
1
}
None => {
error!(
"PROBE_GOQUEST: {label:<4} FAIL — no status ever arrived for the object (expected \
{expect}). vmangos answers every status query it can resolve, so nothing arriving \
means the query was never SENT — the sweep's GameObject leg is not firing"
);
1
}
}
}
#[allow(clippy::too_many_arguments)]
fn goquest_probe(
time: ProbeClock,
mut probe: ResMut<GoQuestProbe>,
self_store: Query<&ObjectStore, With<SelfPlayer>>,
player: Res<Player>,
objects: Query<(&Guid, &NetEntity, &ObjectStore, &Transform), Without<SelfPlayer>>,
mut quest: ResMut<QuestGiver>,
net: Res<NetCommands>,
mut exit: MessageWriter<AppExit>,
) {
let Ok(me) = self_store.single() else {
return; // not in-world yet
};
let Some(level) = me.0.unit_level() else {
return; // our own descriptor hasn't landed
};
let now = time.elapsed_secs_f64();
// Sampled every frame, not just at the windows: a status that flashed on for one frame and was
// pruned would still have been a rendered `!`, and the claim is that none is ever stored.
if probe.poster.is_some_and(|p| quest.status(p).is_some()) {
probe.poster_ever_stored = true;
}
if let Some(s) = probe.control.and_then(|c| quest.status(c)) {
probe.control_stored = Some(s);
}
match probe.phase {
Phase::Wait => {
let [x, y, z, map] = target();
probe.start_level = Some(level);
info!(
"PROBE_GOQUEST: heading to the quest object ({x} {y} {z} map {map}) — \
GameObject type {GO_TYPE_QUESTGIVER}, starting level {level}"
);
let _ = net.0.send(ClientCommand::Chat {
kind: ChatKind::Say,
target: None,
text: format!(".go xyz {x} {y} {z} {map}"),
});
probe.phase = Phase::Settling { sent_at: now };
}
Phase::Settling { sent_at } => {
if now - sent_at < SETTLE_SECS {
return;
}
let here = player.pos;
let in_range = |tf: &Transform| tf.translation.distance(here) < SCAN_RANGE;
let poster = objects.iter().find(|(_, net_e, store, tf)| {
net_e.kind == EntityKind::GameObject
&& store.0.gameobject_type_id() == GO_TYPE_QUESTGIVER
&& in_range(tf)
});
let control = objects.iter().find(|(_, net_e, store, tf)| {
net_e.kind == EntityKind::Unit
&& store.0.unit_npc_flags() & NPC_FLAG_QUESTGIVER != 0
&& in_range(tf)
});
let Some((guid, _, store, _)) = poster else {
if now - sent_at > SCAN_TIMEOUT_SECS {
error!(
"PROBE_GOQUEST: FAIL — no type-{GO_TYPE_QUESTGIVER} GameObject within \
{SCAN_RANGE} yd in {SCAN_TIMEOUT_SECS}s. This is NOT a passing run"
);
probe.fails += 1;
probe.phase = Phase::Done;
}
return;
};
let flags = store.0.gameobject_flags();
info!(
"PROBE_GOQUEST: quest object {:#x} in range — GAMEOBJECT_FLAGS {flags:#x} \
(INTERACT_COND {}), GAMEOBJECT_DYN_FLAGS {:#x}",
guid.0,
flags & GO_FLAG_INTERACT_COND != 0,
store.0.gameobject_dynamic_flags(),
);
if flags & GO_FLAG_INTERACT_COND == 0 {
error!(
"PROBE_GOQUEST: FAIL — this object does not carry GAMEOBJECT_FLAGS bit 2, so \
the reference would never query it either. Aim the probe at a quest object"
);
probe.fails += 1;
probe.phase = Phase::Done;
return;
}
probe.poster = Some(guid.0);
match control {
Some((g, ..)) => {
info!(
"PROBE_GOQUEST: unit control — questgiver creature {:#x}",
g.0
);
probe.control = Some(g.0);
}
None => warn!(
"PROBE_GOQUEST: no questgiver-flagged creature within {SCAN_RANGE} yd — the \
unit control cannot be read at this spot"
),
}
probe.phase = Phase::Level {
to: LOW_LEVEL,
since: now,
};
level_to(&net, level, LOW_LEVEL);
}
Phase::Level { to, since } => {
if level == to {
// Force the sweep this window depends on. A GameObject GUID reaches the wire ONLY
// from a sweep (wow-re §W14.8), and the level change above is one — but it is a
// no-op when the character already stood at this level, so the epoch bump makes
// the window's sweep unconditional.
quest.bump_reask();
info!("PROBE_GOQUEST: at level {to}, swept — reading the window");
probe.phase = Phase::Read {
level: to,
since: now,
};
} else if now - since > LEVEL_TIMEOUT_SECS {
error!("PROBE_GOQUEST: FAIL — still level {level} after a `.levelup` to {to}");
probe.fails += 1;
probe.phase = Phase::Done;
}
}
Phase::Read { level: at, since } => {
let (refused, count) = quest.refused();
let mine = refused
.filter(|(g, _)| Some(*g) == probe.poster)
.map(|(_, s)| s);
// The HIGH window must see a *different* answer than LOW, or one stale refusal would
// satisfy both.
let settled = if at == LOW_LEVEL {
mine
} else {
mine.filter(|s| Some(*s) != probe.low)
};
if settled.is_none() && now - since <= STATUS_TIMEOUT_SECS {
return;
}
if at == LOW_LEVEL {
probe.low = settled;
info!(
"PROBE_GOQUEST: LOW refused {settled:?} (refusals so far: {count}) — dinging \
to level {HIGH_LEVEL}"
);
probe.phase = Phase::Level {
to: HIGH_LEVEL,
since: now,
};
level_to(&net, level, HIGH_LEVEL);
} else {
probe.high = settled;
let back = probe.start_level.unwrap_or(level);
info!(
"PROBE_GOQUEST: HIGH refused {settled:?} (refusals so far: {count}) — \
restoring level {back}"
);
level_to(&net, level, back);
probe.phase = Phase::Restore { since: now };
}
}
Phase::Restore { since } => {
let back = probe.start_level.unwrap_or(level);
if level == back {
probe.phase = Phase::Done;
} else if now - since > LEVEL_TIMEOUT_SECS {
warn!("PROBE_GOQUEST: left the character at level {level}, not {back}");
probe.phase = Phase::Done;
}
}
Phase::Done => {
if probe.exited {
return;
}
probe.exited = true;
let mut fails = probe.fails;
fails += report("LOW", STATUS_UNAVAILABLE, probe.low);
fails += report("HIGH", STATUS_AVAILABLE, probe.high);
if probe.poster_ever_stored {
error!(
"PROBE_GOQUEST: STORE FAIL — a status was stored for the GameObject. The \
marker layer and the minimap dot both read that map, so this is a `!` the \
reference client does not draw"
);
fails += 1;
} else {
info!(
"PROBE_GOQUEST: STORE PASS — no status ever stored for the GameObject, so no \
marker and no minimap dot"
);
}
match (probe.control, probe.control_stored) {
(Some(g), Some(s)) => info!(
"PROBE_GOQUEST: CTRL PASS — the questgiver creature {g:#x} still has its own \
status ({s})"
),
(Some(g), None) => {
error!(
"PROBE_GOQUEST: CTRL FAIL — the questgiver creature {g:#x} never got a \
status. The refusal is one bit too wide and units lost theirs"
);
fails += 1;
}
(None, _) => warn!("PROBE_GOQUEST: CTRL SKIPPED — no creature questgiver in range"),
}
info!(
"PROBE_GOQUEST: DONE object={:#x} low={:?} high={:?} refusals={} fail={fails}",
probe.poster.unwrap_or_default(),
probe.low,
probe.high,
quest.refused().1,
);
// The probe self-exit pattern (`ProbeExitPlugin::fire_probe_exit`): a polite AppExit
// plus a hard backstop thread, so a net/winit teardown hang can't leave a zombie
// client holding the probe account.
exit.write(AppExit::Success);
std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_secs(5));
warn!("PROBE_GOQUEST: still alive 5s after AppExit — hard exit");
std::process::exit(0);
});
}
}
}
+7
View File
@@ -363,6 +363,13 @@ impl Plugin for DevProbesPlugin {
if std::env::var("WOW_PROBE_CHEST").is_ok() {
app.add_plugins(crate::capture::ProbeChestPlugin);
}
// The GameObject-questgiver live probe: `WOW_PROBE_GOQUEST=1` parks at the Goldshire
// wanted poster and reports the dialog status the server answers for it, below and
// above the quest's own MinLevel — the numeric answer to "quest objects are never
// status-queried" (decision 1872; see `capture::ProbeGoQuestPlugin`).
if std::env::var("WOW_PROBE_GOQUEST").is_ok() {
app.add_plugins(crate::capture::ProbeGoQuestPlugin);
}
// The openable-item live probe: `WOW_PROBE_CLAM=1` stocks a clam, right-clicks it
// through the live VM's own `UseContainerItem` and reports whether a loot window opens
// on the item's own guid — the numeric answer to the director's "clams don't open"
+10 -1
View File
@@ -244,10 +244,19 @@ pub(in crate::minimap) fn emit_tracking_dots(
}
};
// Cell 0 — tracked GameObjects (gold): template lockId through Lock.dbc.
//
// **No quest-status precedence on this leg** (decision 1872). This loop used to skip a
// GameObject whose status was 7, mirroring the unit loop below — but the classifier's
// `cmp dword ptr [edi+0xcb8],7` at `0x4eac31` is reachable **only** from the UNIT and PLAYER
// legs (machine-enumerated predecessors, wow-re `minimap-poi-questdot.md` via
// `questgiver-marker.md` §W14.9): the GameObject leg at `0x4eab43` falls straight into
// `0x5ed2b0`, GameObject *tracking*, and emits category 0. A GameObject never draws a quest
// dot in the reference, so nothing about a quest status may suppress its tracking dot — and
// a GameObject can no longer *hold* a status here anyway (`net/apply` drops it, §W14.1).
if tracking.resources != 0 {
if let Some(locks) = locks {
for (guid, net, tf, _) in candidates.iter() {
if net.kind != EntityKind::GameObject || statuses.get(&guid.0).copied() == Some(7) {
if net.kind != EntityKind::GameObject {
continue;
}
let Some(t) = templates.get(guid.0) else {
+76 -2
View File
@@ -18,9 +18,36 @@ use crate::ui_quest_share::QuestShare;
use super::super::{ClientCommand, NetCommands};
/// A questgiver dialog status for one NPC (`SMSG_QUESTGIVER_STATUS`) — the `!`/`?` marker's
/// [`crate::messages::dialog_status`] value. Stored per guid now; the world marker is a later
/// slice (decision 0088).
/// [`crate::messages::dialog_status`] value, stored per guid for the marker layer and the minimap
/// quest dot.
///
/// **A GameObject's answer is DROPPED, and that is the reference's own behaviour** (decision 1872,
/// wow-re `questgiver-marker.md` §W14.1): the handler `0x5dc9f0` resolves the packet's GUID with
/// `0x5dca22 mov ecx,8` — typemask `TYPEMASK_UNIT` — and `0x468460` is a bitmask AND against
/// `OBJECT_FIELD_TYPE`, so a GameObject's `0x21` misses bit 3, returns NULL, and the handler exits
/// at `0x5dca2f` without ever reaching the `UNIT_NPC_FLAGS` test, let alone the marker.
///
/// This is not a hypothetical branch here: benilla now *sends* the query for quest-flagged
/// GameObjects, exactly as the reference does ([`crate::quest_markers`]), and **vmangos answers
/// it** — `GetObjectByTypeMask(guid, TYPEMASK_CREATURE_OR_GAMEOBJECT)`, `QuestHandler.cpp:41` —
/// where the real 1.12 service never did (zero GameObject GUIDs across 1292 in the sniff corpus).
/// So the drop is what keeps a vmangos-only answer from putting a `!` over a wanted poster that
/// the reference client leaves bare.
///
/// The test is the GUID's own shape rather than a live type lookup on purpose: it gives the same
/// partition as typemask 8 for anything a server can send us, and it cannot be defeated by a status
/// that arrives in the same drain as its object's create block (descriptors are flushed at the end
/// of the drain, so a type test would read a not-yet-seeded store and drop a *unit's* answer that
/// would then never be re-asked for). The reference's second conjunct — `UNIT_NPC_FLAGS & 0x2` on
/// the resolved unit — is deliberately not modelled here for that same ordering reason;
/// [`crate::quest_markers::query`]'s teardown leg covers the flag-clearing case from the other end.
pub(super) fn quest_giver_status(npc: u64, status: u32, quest: &mut QuestGiver) {
use benilla_protocol::guid;
if !(guid::is_player(npc) || guid::is_creature_or_pet(npc)) {
debug!("net: dropping a non-unit questgiver status ({npc:#x} → {status}) — typemask 8");
quest.refuse_status(npc, status);
return;
}
quest.set_status(npc, status);
}
@@ -248,6 +275,53 @@ pub(super) fn quest_confirm_accept(c: QuestConfirmAccept, share: &mut QuestShare
mod tests {
use super::*;
/// **The typemask-8 refusal** (decision 1872, wow-re `questgiver-marker.md` §W14.1). benilla
/// now sends `CMSG_QUESTGIVER_STATUS_QUERY` for quest-flagged GameObjects because the reference
/// does — and vmangos, unlike the real 1.12 service, *answers*. The reference's handler drops
/// that answer at the lookup (`0x5dca22 mov ecx,8`), so we must too: otherwise a wanted poster
/// would wear a gold `!` the reference client never puts there.
///
/// The control is the half that must not change: a creature's answer, and a player's, still
/// land — typemask 8 is `TYPEMASK_UNIT`, which a creature (`0x9`) and a player (`0x19`) both
/// carry.
#[test]
fn a_gameobjects_dialog_status_is_dropped_and_a_creatures_is_not() {
use benilla_protocol::messages::dialog_status;
// The Goldshire `Wanted Poster` (HIGHGUID_GAMEOBJECT), an elevator (HIGHGUID_TRANSPORT —
// a GameObject in every respect but its high word), a creature, and a player.
const POSTER: u64 = 0xf110_0000_0044_68db;
const ELEVATOR: u64 = 0xf120_0000_0384_1092;
const CREATURE: u64 = 0xf130_0000_0060_0abc;
const PLAYER: u64 = 0x0000_0000_0000_0007;
let mut quest = QuestGiver::default();
for (guid, status) in [
(POSTER, dialog_status::AVAILABLE),
(ELEVATOR, dialog_status::AVAILABLE),
(CREATURE, dialog_status::AVAILABLE),
(PLAYER, dialog_status::REWARD2),
] {
quest_giver_status(guid, status, &mut quest);
}
assert_eq!(
quest.status(POSTER),
None,
"a GameObject GUID misses typemask bit 3 and the handler exits at 0x5dca2f"
);
assert_eq!(quest.status(ELEVATOR), None, "…and so does a transport GO");
assert_eq!(
quest.status(CREATURE),
Some(dialog_status::AVAILABLE),
"the control: a creature's answer is what this packet is FOR"
);
assert_eq!(
quest.status(PLAYER),
Some(dialog_status::REWARD2),
"and a player passes the same typemask, as it does in the reference"
);
}
fn open_detail(quest_id: u32) -> QuestGiver {
let mut giver = QuestGiver::default();
giver.open(
+258 -31
View File
@@ -1,5 +1,14 @@
//! The **query** half of the overhead questgiver markers: when to ask the server what an NPC's
//! `!`/`?` should be. The rendering half is [`super`].
//! `!`/`?` should be — and, for a GameObject, when to ask a question whose answer is thrown away.
//! The rendering half is [`super`].
//!
//! **A GameObject is queried and never rendered** (decision 1872, wow-re `questgiver-marker.md`
//! §W14). The reference sends `CMSG_QUESTGIVER_STATUS_QUERY` for a quest-flagged GameObject from
//! both object sweeps, but its answer handler `0x5dc9f0` resolves the GUID with typemask **8**
//! (`0x468460` is a bitmask AND against `OBJECT_FIELD_TYPE`), so a GameObject's `0x21` returns NULL
//! and the packet dies at `0x5dca2f` — and even if it did not, a GameObject has no `+0xcb8` status
//! slot, no `+0xb2c` marker slot, and (11 models out of ~1600, none of them a poster) no
//! attachment 18 to hang a marker from. So: **send the query, drop the answer, render nothing.**
//!
//! The server only ever *answers* `CMSG_QUESTGIVER_STATUS_QUERY` (vmangos `QuestHandler.cpp`) — it
//! never pushes — so every refresh point is the client's own to trigger, and a status that is never
@@ -11,11 +20,27 @@ use std::collections::HashMap;
use bevy::prelude::*;
use benilla_protocol::EntityKind;
use crate::net::{ClientCommand, GuidIndex, NetCommands, NetEntity, ObjectStore, SelfPlayer};
use crate::target::cursor_mode::go_reaction;
use crate::target::ring::Factions;
use crate::ui_quest::QuestGiver;
/// `UNIT_NPC_FLAGS` questgiver service bit (vanilla; `update_object.rs`'s accessor doc).
const NPC_FLAG_QUESTGIVER: u32 = 0x2;
/// `GAMEOBJECT_FLAGS` bit **2** (mask `0x4`) — the whole of the sweep's GameObject gate, and the
/// one place a GameObject GUID can reach this opcode at all.
///
/// Byte-pinned (wow-re `questgiver-marker.md` §W14.6): `0x5eb0ef mov eax,[edx+0xc];
/// 0x5eb0f2 shr eax,0x2; 0x5eb0f5 test al,1` — shift **2**, so mask `0x4`, not the `0x2` the unit
/// leg tests 25 bytes later. `[<GO block>+0xc]` is absolute field index 9 = `GAMEOBJECT_FLAGS`,
/// named from the binary's own UpdateField name table (`0x83b8a8` → the string at `0x83be84`).
/// The *name* `GO_FLAG_INTERACT_COND` is vmangos's, not the image's — but the bit is settled from
/// shipped content as well as from the branch: across the genuine 2005-06 sniff corpus, all 122
/// GameObjects the real client sent a status query for carry it, and none without it does, against
/// a 79.2 % base rate.
const GO_FLAG_INTERACT_COND: u32 = 0x4;
/// Ask the dialog status of every questgiver-flagged creature, and re-ask when the answer could
/// have changed. The server only ever *answers* queries (vmangos `QuestHandler.cpp`), so every
@@ -42,8 +67,28 @@ const NPC_FLAG_QUESTGIVER: u32 = 0x2;
/// leaves [`GuidIndex`], which is the create-path query (the reference caches the answer *on* the
/// unit at `unit+0xcb8`, so its cache dies with the object — ours is a map and needs the prune).
///
/// **Per GameObject — on a sweep, and ONLY on a sweep.** A quest-giving GameObject (a wanted
/// poster, a suspicious barrel) is a questgiver on the wire exactly as a creature is: the same
/// sweep callback tests typemask bit 5 and sends the same opcode for it (`0x5eb0a0` @ `0x5eb159`,
/// and its sibling `0x5eb3f0` @ `0x5eb456`), gated on [`GO_FLAG_INTERACT_COND`] and on the
/// GameObject's own reaction toward us being **> 1** ([`go_reaction`], the reference's `0x5f7fd0`).
/// The wire corpus shows it happening in 20 of 62 real sessions.
///
/// But there is **no bring-up query for a GameObject** — the contrast with units is exact and it
/// is the point (wow-re §W14.8, a closed caller census: a GameObject GUID can reach this opcode
/// through exactly two instructions in the whole image and both are inside a sweep callback; the
/// `CGGameObject_C` ctor and its vtable slot 3 call no sender, where `CGUnit_C`'s slot 3 does). So
/// a GameObject is asked about only when a sweep happens to run while it is in view, and a player
/// who completes a quest elsewhere and then walks up to the turn-in object never asks about it at
/// all. That is a real, slightly surprising reference behaviour, and **a per-guid asked-set that
/// queries on first sight is a deviation, not a fix** — hence the `swept` gate below rather than
/// an [`unit_ask_key`] analogue. Nothing visible turns on any of this: the answer is dropped
/// ([`crate::net::apply`]'s own typemask gate), which is decision 1872's whole point.
///
/// **The teardown leg** — an NPC whose questgiver bit goes off loses its cached status, so the
/// marker goes with it (`0x5eb0a0`'s own branch), and is re-asked if the bit returns.
/// marker goes with it (`0x5eb0a0`'s own branch), and is re-asked if the bit returns. There is no
/// GameObject counterpart: a GameObject has no status to invalidate (`+0xcb8` is `0x9a8` bytes past
/// the end of a `0x310`-byte `CGGameObject_C`), and all 8 of `0x6073f0`'s call sites are unit-side.
///
/// **Still not covered**, and why: the reference also sweeps when an *item* moves between
/// containers (an `ITEM` typeid watch, `0x5d9375` — quest availability can depend on carried
@@ -55,8 +100,9 @@ const NPC_FLAG_QUESTGIVER: u32 = 0x2;
#[allow(clippy::type_complexity)] // a Bevy system: each param is one resource, the app's convention
pub(super) fn query_statuses(
self_q: Query<Ref<ObjectStore>, With<SelfPlayer>>,
units: Query<(&crate::net::Guid, &ObjectStore), (With<NetEntity>, Without<SelfPlayer>)>,
objects: Query<(&crate::net::Guid, &NetEntity, &ObjectStore), Without<SelfPlayer>>,
index: Res<GuidIndex>,
factions: Option<Res<Factions>>,
mut quest: ResMut<QuestGiver>,
commands: Res<NetCommands>,
mut state: Local<QueryState>,
@@ -71,7 +117,13 @@ pub(super) fn query_statuses(
state.fields = self_generation(&store.0);
}
let generation = state.fields ^ (u64::from(quest.reask_epoch()) << 32);
if state.generation != generation {
// **This frame IS the sweep.** Both of the reference's object sweeps (`0x5eb070` and the full
// re-query `0x5eb3c0`) walk every object in the manager once, synchronously, when one of the
// 13 local-player state changes fires them; a changed generation is that moment, here. The
// GameObject leg below fires only on it, because that is the only way a GameObject GUID ever
// reaches the wire (§W14.8).
let swept = state.generation != generation;
if swept {
state.generation = generation;
state.asked.clear();
}
@@ -79,29 +131,59 @@ pub(super) fn query_statuses(
// its "already asked" mark and its cached status, so re-entering view re-asks from scratch.
state.asked.retain(|guid, _| index.0.contains_key(guid));
quest.retain_statuses(|npc| index.0.contains_key(&npc));
for (guid, obj) in &units {
if obj.0.unit_npc_flags() & NPC_FLAG_QUESTGIVER == 0 {
// No longer a questgiver: drop the stale answer (and the marker with it) —
// **unconditionally**, never "only if we had asked". The reference's sweep callback
// (`0x5eb0a0`) tears the marker down on the flag test alone, with no prior-asked
// precondition (decision 0647), and the gate we used to put here was disarmed by the
// very sweep that arrives with it: an escort's giver drops `UNIT_NPC_FLAGS` in the same
// server tick as the quest-log write (vmangos `FollowerAI::StartFollow`,
// `ScriptedEscortAI`'s "disable npcflags"), so the quest-log change bumps the
// generation, `asked` is cleared above, `remove` finds nothing, and the cached
// AVAILABLE status — with its `!` — was frozen over the NPC for the whole escort, with
// no way back: the flag stays off, so this arm never queries either (B257).
state.asked.remove(&guid.0);
quest.clear_status(guid.0);
continue;
}
// Re-ask when this unit's own key moves — its service bits or its faction template. The
// reference does the same per-unit query off those field watches.
let key = unit_ask_key(&obj.0);
if state.asked.insert(guid.0, key) != Some(key) {
let _ = commands
.0
.send(ClientCommand::QuestgiverStatusQuery { npc: guid.0 });
for (guid, net, obj) in &objects {
match net.kind {
// `0x5eb0a0` @ `0x5eb0d3`: `cmp eax,9; je` — typemask EXACTLY `OBJECT|UNIT`, a plain
// creature. A player is `0x19` and falls out of the sweep here, before any flag test.
EntityKind::Unit => {
if obj.0.unit_npc_flags() & NPC_FLAG_QUESTGIVER == 0 {
// No longer a questgiver: drop the stale answer (and the marker with it) —
// **unconditionally**, never "only if we had asked". The reference's sweep
// callback (`0x5eb0a0`) tears the marker down on the flag test alone, with no
// prior-asked precondition (decision 0647), and the gate we used to put here
// was disarmed by the very sweep that arrives with it: an escort's giver drops
// `UNIT_NPC_FLAGS` in the same server tick as the quest-log write (vmangos
// `FollowerAI::StartFollow`, `ScriptedEscortAI`'s "disable npcflags"), so the
// quest-log change bumps the generation, `asked` is cleared above, `remove`
// finds nothing, and the cached AVAILABLE status — with its `!` — was frozen
// over the NPC for the whole escort, with no way back: the flag stays off, so
// this arm never queries either (B257).
state.asked.remove(&guid.0);
quest.clear_status(guid.0);
continue;
}
// Re-ask when this unit's own key moves — its service bits or its faction template.
// The reference does the same per-unit query off those field watches.
let key = unit_ask_key(&obj.0);
if state.asked.insert(guid.0, key) != Some(key) {
let _ = commands
.0
.send(ClientCommand::QuestgiverStatusQuery { npc: guid.0 });
}
}
// `0x5eb0da shr ecx,5; test cl,1` — typemask bit 5, `TYPEMASK_GAMEOBJECT`. Sweep-only,
// no asked-set, no teardown: see this function's doc and §W14.8.
EntityKind::GameObject if swept => {
if obj.0.gameobject_flags() & GO_FLAG_INTERACT_COND == 0 {
continue;
}
// `0x5eb101 cmp eax,1; jle` — the GameObject's reaction toward us must be at least
// **2**. Not a friendliness test: Unfriendly(2) and Neutral(3) both pass, and a
// faction-less GameObject resolves Neutral. `None` = unresolvable (no catalog yet)
// and passes, the same "no opinion" rule the cursor's own gate uses.
let reaction = go_reaction(
factions.as_deref(),
obj.0.gameobject_faction(),
Some(&store),
);
if !reaction.is_none_or(|r| r > 1) {
continue;
}
let _ = commands
.0
.send(ClientCommand::QuestgiverStatusQuery { npc: guid.0 });
}
_ => {}
}
}
}
@@ -141,10 +223,15 @@ fn self_generation(fields: &benilla_protocol::ObjectFields) -> u64 {
// packets, 70 → 2 HP, with zero bursts; the single packet crossing to 0 has the only burst in
// the stretch).
//
// One named over-refresh: our bit flips on BOTH crossings, so we sweep units on resurrect too.
// The reference sweeps GameObject questgivers there (`0x5eb3c0`) plus one conditional unit
// query — and benilla has no GameObject markers at all, so a unit sweep is the nearest thing we
// can do with what we actually render. It costs one extra sweep per resurrect. 0654.
// Our bit flips on BOTH crossings, and that is now known to be exactly right — the note this
// comment used to carry ("one named over-refresh... the reference sweeps GameObject questgivers
// there") was wrong on both halves and is CORRECTED by wow-re §W14.5/§W14.11. `0x5eb3c0` is not
// a GameObject sweep: `0x468380` applies no type filter at all, so it walks **every** object in
// the manager — it is the FULL re-query sweep, and its creature arm is the heavier one
// (`0x607380`: an unconditional marker teardown plus two queries, questgiver `0x182` and taxi
// `0x1aa`), the GameObject arm the lighter. So the death edge sweeps (`0x5eb070`) and the
// **revive** edge sweeps too (`0x5eb3c0`), and one sweep per crossing is the reference's own
// behaviour rather than a deviation we were paying for. 0654, 1872.
fold(u64::from(fields.unit_health().unwrap_or(1) == 0));
fold(u64::from(fields.player_flags()));
fold(u64::from(fields.player_money().unwrap_or(0)));
@@ -399,6 +486,146 @@ mod tests {
);
}
/// **A GameObject is asked about on a SWEEP, and only on a sweep** (decision 1872, wow-re
/// `questgiver-marker.md` §W14.6/§W14.8). Three things are being pinned here, and the third is
/// the one a client "improves" without noticing:
///
/// - the gate is `GAMEOBJECT_FLAGS` bit 2 (mask `0x4`) — `0x5eb0ef shr eax,0x2; test al,1` —
/// and a quest-less GameObject beside it is never asked about at all;
/// - a sweep asks exactly once, and the frames between sweeps are silent;
/// - **there is no bring-up query.** A GameObject entering view asks nothing; it waits for the
/// next sweep. The unit control in the same world does the opposite on the same frame, which
/// is what makes this a contrast and not an accident of ordering.
#[test]
fn a_gameobject_is_asked_on_a_sweep_and_never_at_first_sight() {
use crate::net::{Guid, NetEntity};
use benilla_protocol::ObjectFields;
/// The Goldshire `Wanted Poster`'s real guid (`HIGHGUID_GAMEOBJECT`, template 68, spawn
/// 26843) and its real `GAMEOBJECT_FLAGS` — INTERACT_COND | NODESPAWN, read off the live
/// wire by `WOW_PROBE_GOQUEST`.
const POSTER: u64 = 0xf110_0000_0044_68db;
const POSTER_FLAGS: u32 = 0x24;
/// A GameObject next to it with no quest condition: a plain door, `NODESPAWN` only.
const DOOR: u64 = 0xf110_0000_0045_0001;
const DOOR_FLAGS: u32 = 0x20;
/// The control: an ordinary creature questgiver, which the same sweep treats differently.
const NPC: u64 = 0x2222;
const FIELD_LEVEL: u16 = 34;
const FIELD_NPC_FLAGS: u16 = 147;
const FIELD_GAMEOBJECT_FLAGS: u16 = 9;
let (tx, rx) = crossbeam_channel::unbounded();
let mut app = App::new();
app.insert_resource(NetCommands(tx))
.init_resource::<GuidIndex>()
.init_resource::<QuestGiver>()
.add_systems(Update, query_statuses);
let spawn = |app: &mut App, guid: u64, kind: EntityKind, fields: &[(u16, u32)]| {
let e = app
.world_mut()
.spawn((
NetEntity {
kind,
display_id: None,
scale: 1.0,
},
Guid(guid),
ObjectStore(ObjectFields::from_pairs(fields)),
))
.id();
app.world_mut()
.resource_mut::<GuidIndex>()
.0
.insert(guid, e);
};
app.world_mut().spawn((
SelfPlayer,
Guid(1),
ObjectStore(ObjectFields::from_pairs(&[(FIELD_LEVEL, 5)])),
));
// Drain per update, then count per guid — several objects are in flight at once here.
let asked = |app: &mut App| -> Vec<u64> {
app.update();
rx.try_iter()
.filter_map(|c| match c {
ClientCommand::QuestgiverStatusQuery { npc } => Some(npc),
_ => None,
})
.collect()
};
// Settle the generation first: the very first frame in a fresh world IS a sweep (nothing
// has been folded yet), so "first sight" can only be asked of a later frame.
assert!(asked(&mut app).is_empty(), "nothing in view yet");
spawn(
&mut app,
POSTER,
EntityKind::GameObject,
&[(FIELD_GAMEOBJECT_FLAGS, POSTER_FLAGS)],
);
spawn(
&mut app,
DOOR,
EntityKind::GameObject,
&[(FIELD_GAMEOBJECT_FLAGS, DOOR_FLAGS)],
);
spawn(
&mut app,
NPC,
EntityKind::Unit,
&[(FIELD_NPC_FLAGS, NPC_FLAG_QUESTGIVER)],
);
assert_eq!(
asked(&mut app),
vec![NPC],
"first sight: the creature is asked from its own create path, the poster is NOT — \
the GameObject class has no bring-up query (§W14.8)"
);
assert!(
asked(&mut app).is_empty(),
"and the frames after it stay silent"
);
// A sweep: any of the reference's 13 local-player triggers. The packet epoch stands in for
// its four packet handlers.
app.world_mut().resource_mut::<QuestGiver>().bump_reask();
let swept = asked(&mut app);
assert!(
swept.contains(&POSTER),
"the sweep asks about the poster — {swept:x?}"
);
assert!(
swept.contains(&NPC),
"and re-asks the creature, as it always did — {swept:x?}"
);
assert!(
!swept.contains(&DOOR),
"but never the GameObject without GAMEOBJECT_FLAGS bit 2 — {swept:x?}"
);
assert_eq!(
swept.iter().filter(|g| **g == POSTER).count(),
1,
"exactly once per sweep"
);
assert!(
asked(&mut app).is_empty(),
"between sweeps, nothing — a GameObject has no per-object key to re-ask on"
);
// ...and it is not one-shot: the next sweep asks again, because the reference keeps no
// per-GameObject memo of having asked (there is nowhere on a CGGameObject_C to keep one).
app.world_mut().resource_mut::<QuestGiver>().bump_reask();
assert!(
asked(&mut app).contains(&POSTER),
"and again on the next sweep"
);
}
/// The rest of the reference's trigger set (0654): the remaining self-descriptor watches, the
/// packet epoch that stands in for its four packet handlers, and the per-unit key that catches
/// a unit's own service bits or faction moving. Each leg must re-ask exactly once.
+23
View File
@@ -71,6 +71,18 @@ pub(crate) struct QuestGiver {
messages: Vec<UiError>,
/// The re-ask epoch — see [`Self::bump_reask`].
reask: u32,
/// The last `SMSG_QUESTGIVER_STATUS` we **refused**, and how many we have refused this session
/// — `(guid, status)`.
///
/// The only trace of a packet class benilla deliberately throws on the floor. benilla asks the
/// server about quest-flagged GameObjects because the reference does, and vmangos — unlike the
/// real 1.12 service, which never answered one — replies; the reference's own handler drops
/// that reply at the typemask-8 lookup and so does ours (decision 1872,
/// [`crate::net::apply`]'s `quest_giver_status`). Without this, the *whole* GameObject half of
/// the feature is invisible from inside the client: a correct drop and a query that was never
/// sent both read as "no status", which is exactly the pair a live probe has to tell apart.
refused: Option<(u64, u32)>,
refused_count: u32,
}
impl QuestGiver {
@@ -141,6 +153,17 @@ impl QuestGiver {
self.reask
}
/// Record a dialog status the typemask gate refused — see [`Self::refused`].
pub(crate) fn refuse_status(&mut self, npc: u64, status: u32) {
self.refused = Some((npc, status));
self.refused_count = self.refused_count.saturating_add(1);
}
/// The last refused `(guid, status)` and the session's refusal count — see the field.
pub(crate) fn refused(&self) -> (Option<(u64, u32)>, u32) {
(self.refused, self.refused_count)
}
/// The stored dialog status for `npc`, if any. The store-now half of DIALOG_STATUS
/// (decision 0088): no consumer yet — the `!`/`?` world marker is a later nameplate slice — so
/// this accessor is deliberately unused for now.
@@ -30,13 +30,25 @@ fn shipped_action_bar_drives_end_to_end() {
);
if file == "ActionBar.xml" {
assert_eq!(
report.frames, 62,
report.frames, 59,
"bar + XP StatusBar (+ its numerals overlay) + exhaustion tick + max-level rail + art frame + 12 buttons (each with a Cooldown child) + 2 page buttons + the performance meter and its hover button, \
+ BonusActionBarFrame and its 12 buttons with their Cooldown children (25 — hidden, as the reference's is; decision 1223), + ReputationWatchBar with its status bar and its numerals overlay (3 — hidden, ref ReputationFrame.xml:869-994)"
+ BonusActionBarFrame and its 12 buttons with their Cooldown children (25 — hidden, as the reference's is; decision 1223) — and NO ReputationWatchBar: its three frames went home to the reference's own ReputationFrame.xml with 1875"
);
}
}
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
// 1875 that file is the reference's own, so a harness that drives the XP bar has to load it too
// or the tick raises on its first event.
// In manifest order: the fonts its check-box labels colour from, the panel templates those
// boxes inherit through, then the pane.
super::test_ui::load_ui(&s, "Fonts.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\ReputationFrame.xml");
// A warrior in battle stance: offset 1 ⇒ the bar shows actions 73..84.
s.set_bonus_bar_offset(1);
s.set_action(
@@ -164,6 +176,18 @@ fn load_action_bar(s: &UiScript) {
report.errors
);
}
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
// 1875 that file is the reference's own, so a harness that drives the XP bar has to load it too
// or the tick raises on its first event.
// In manifest order: the fonts its check-box labels colour from, the panel templates those
// boxes inherit through, then the pane.
super::test_ui::load_ui(s, "Fonts.xml");
super::test_ui::load_ui(s, r"Interface\FrameXML\UIPanelTemplates.lua");
super::test_ui::load_ui(s, r"Interface\FrameXML\UIPanelTemplates.xml");
super::test_ui::load_ui(s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
super::test_ui::load_ui(s, r"Interface\FrameXML\ReputationFrame.xml");
}
/// The state/feedback layer (decision 0137 phase 4) through the REAL shipped XML: a pushed
@@ -1273,20 +1297,24 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
// Fonts first: the pane's check-box labels colour from `RED_FONT_COLOR` in their own OnLoad.
"Fonts.xml",
"UIParent.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"MultiBars.xml",
] {
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets/ui")
.join(file),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
// `test_ui::load_ui`, not a disk read: this list names chain entries now (the reputation
// pane and the templates it inherits through), and `assets/ui` cannot answer for those.
super::test_ui::load_ui(&s, file);
}
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 1);
@@ -1413,6 +1441,18 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
}
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
// 1875 that file is the reference's own, so a harness that drives the XP bar has to load it too
// or the tick raises on its first event.
// In manifest order: the fonts its check-box labels colour from, the panel templates those
// boxes inherit through, then the pane.
super::test_ui::load_ui(&s, "Fonts.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\ReputationFrame.xml");
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.tick(10.0); // a nonzero clock epoch
let _ = s.take_sounds();
@@ -1592,6 +1632,18 @@ fn bonus_bar_turnaround_continues_from_position() {
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
}
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
// 1875 that file is the reference's own, so a harness that drives the XP bar has to load it too
// or the tick raises on its first event.
// In manifest order: the fonts its check-box labels colour from, the panel templates those
// boxes inherit through, then the pane.
super::test_ui::load_ui(&s, "Fonts.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\ReputationFrame.xml");
s.tick(10.0);
let _ = s.take_sounds();
@@ -1647,6 +1699,18 @@ fn the_page_arrows_do_not_steal_each_other_s_clicks() {
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
}
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
// 1875 that file is the reference's own, so a harness that drives the XP bar has to load it too
// or the tick raises on its first event.
// In manifest order: the fonts its check-box labels colour from, the panel templates those
// boxes inherit through, then the pane.
super::test_ui::load_ui(&s, "Fonts.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\ReputationFrame.xml");
// The post-login state, which is what a player clicks into. Without it `ExhaustionTick_Update`
// never runs, and the rested marker — DIALOG strata, declared CENTER on the XP strip, which is
// exactly where the arrows are — sits unhidden over both and eats every click. That is the
@@ -13,7 +13,7 @@ use super::test_ui::load_ui as load_xml;
fn basic_controls() -> UiScript {
let mut s = UiScript::new().unwrap();
load_xml(&s, "Fonts.xml");
load_xml(&s, "BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
s.set_screen_size(1024.0, 768.0);
s.resolve();
s
@@ -22,6 +22,14 @@ fn exp_bar_harness() -> UiScript {
"Interface\\FrameXML\\TextStatusBar.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
] {
super::test_ui::load_ui(&s, file);
}
@@ -106,3 +106,75 @@ fn a_started_fade_still_ramps_and_the_driver_reparks_after() {
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **The FLASH kit alternates and stops** — the fade kit's twin, transcribed from `UIParent.lua`
/// because two stock windows call it and nothing answered (1879).
///
/// Driven rather than merely loaded: a kit that is present but never exercised is exactly how
/// twelve faux lists shipped unable to scroll (1868), so this runs a real flash to completion.
#[test]
fn a_flash_alternates_then_stops_and_the_driver_reparks() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
s.resolve();
// Parked until something asks — the whole point of the driver over UIParent's own OnUpdate.
assert!(
!s.eval::<bool>("return BenillaFlashDriver:IsShown()")
.unwrap(),
"an empty FLASHFRAMES parks the driver"
);
s.run(
r#"Probe = CreateFrame("Frame", "BenillaFlashProbe", UIParent)
Probe:SetWidth(10) Probe:SetHeight(10) Probe:SetPoint("TOPLEFT")
UIFrameFlash(Probe, 0.05, 0.05, 0.3, nil, 0, 0)"#,
)
.unwrap();
assert!(
s.eval::<bool>("return UIFrameIsFlashing(BenillaFlashProbe) == 1")
.unwrap(),
"the frame is on the flash list"
);
assert!(
s.eval::<bool>("return BenillaFlashDriver:IsShown()")
.unwrap(),
"UIFrameFlash is the single inserter and wakes the driver"
);
// Re-arming an already-flashing frame is a no-op, not a second entry (ref UIParent.lua:1234).
s.run("UIFrameFlash(BenillaFlashProbe, 1, 1, 1, nil, 0, 0)")
.unwrap();
assert_eq!(
s.eval::<i64>("return table.getn(FLASHFRAMES)").unwrap(),
1,
"already flashing: the reference returns rather than re-arming"
);
// Run past flashDuration: the frame leaves both lists, is restored to full alpha, and — with
// `showWhenDone` nil — ends hidden.
for _ in 0..40 {
s.tick(0.016);
s.resolve();
}
assert!(s.errors().is_empty(), "flashing raised: {:?}", s.errors());
assert_eq!(
s.eval::<i64>("return table.getn(FLASHFRAMES)").unwrap(),
0,
"the flash finished and the frame left the list"
);
assert!(
!s.eval::<bool>("return BenillaFlashProbe:IsShown()")
.unwrap(),
"showWhenDone was nil, so it ends hidden"
);
assert!(
!s.eval::<bool>("return BenillaFlashDriver:IsShown()")
.unwrap(),
"and the driver re-parks on the first empty tick"
);
}
@@ -38,7 +38,26 @@ fn setup() -> UiScript {
// guards, because there RaidFrame.xml is FrameXML and always loaded — so the guard belongs in
// the harness's load order, not in shipped Lua defending against a state the client cannot be
// in (decision 1549).
load_xml(&s, "RaidFrame.xml");
// Stock `RaidFrame_OnLoad` reconciles the party frames the moment it loads
// (`RaidOptionsFrame_UpdatePartyFrames` -> `HidePartyFrame`/`ShowPartyFrame`), so this pane
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
// The reference's own `BasicControls.xml`, which is manifest entry 3 — far above the pane
// (1874). It is here for the error/message pair and `TEXT`. NOTE: unlike `raid_tests`, this
// harness does not load `UIParent.xml`, so `UIParentLoadAddOn` — which 1881 moved there out
// of BasicControls — is absent. Nothing this file exercises reaches it; if a future
// assertion drives `RaidFrame_LoadUI`, that is the line to add.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\BuffFrame.xml");
load_xml(&s, r"Interface\FrameXML\UnitFrame.xml");
load_xml(&s, r"Interface\FrameXML\CombatFeedback.xml");
load_xml(&s, r"Interface\FrameXML\PartyFrame.xml");
load_xml(&s, r"Interface\FrameXML\RaidFrame.xml");
load_xml(&s, r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml");
s
}
@@ -204,7 +204,7 @@ fn setup() -> UiScript {
}),
);
load_xml(&s, "Fonts.xml");
load_xml(&s, "BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -221,7 +221,20 @@ fn setup() -> UiScript {
// guards, because there RaidFrame.xml is FrameXML and always loaded — so the guard belongs in
// the harness's load order, not in shipped Lua defending against a state the client cannot be
// in (decision 1549).
load_xml(&s, "RaidFrame.xml");
// Stock `RaidFrame_OnLoad` reconciles the party frames the moment it loads
// (`RaidOptionsFrame_UpdatePartyFrames` -> `HidePartyFrame`/`ShowPartyFrame`), so this pane
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\BuffFrame.xml");
load_xml(&s, r"Interface\FrameXML\UnitFrame.xml");
load_xml(&s, r"Interface\FrameXML\CombatFeedback.xml");
load_xml(&s, r"Interface\FrameXML\PartyFrame.xml");
load_xml(&s, r"Interface\FrameXML\RaidFrame.xml");
load_xml(&s, r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml");
s
}
@@ -20,7 +20,7 @@ fn setup() -> UiScript {
s.set_screen_size(1024.0, 768.0);
for file in [
"Fonts.xml",
"BasicControls.xml",
"Interface\\FrameXML\\BasicControls.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"ScrollTemplates.xml",
@@ -368,7 +368,7 @@ fn shown_inspect_honor_page() -> UiScript {
for file in [
"Fonts.xml",
// `TEXT`, the stock level line's formatter.
"BasicControls.xml",
"Interface\\FrameXML\\BasicControls.xml",
"MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -116,7 +116,7 @@ fn armed() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
load_xml(&s, "BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -163,7 +163,7 @@ fn shipped_inspect_frame_loads_clean() {
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
load_xml(&s, "BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -15,6 +15,7 @@ use super::test_ui::load_ui as load_xml;
/// `ResetInstances` a queue.
#[test]
fn the_three_bindings_have_the_reference_shapes() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
// Nothing pushed yet: the app has not said where we are.
@@ -66,13 +67,17 @@ fn the_three_bindings_have_the_reference_shapes() {
/// and Yes is the only thing that sends.
#[test]
fn the_self_menu_row_gates_on_the_binding_and_confirms_before_sending() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The row labels a bare harness has no GlobalStrings.lua for. Verbatim 1.12 values
// (`RESET_INSTANCES` l.3342, `CONFIRM_RESET_INSTANCES` l.851, `YES` l.5463, `NO` l.2794) —
// production runs the player's own string table at boot.
// (`RESET_INSTANCES` l.3342, `CONFIRM_RESET_INSTANCES` l.851, `YES` l.5463, `NO` l.2794,
// `GROUP` l.2029) — production runs the player's own string table at boot.
s.run(
r#"
-- The stock raid pane concatenates this into each of its eight group headers inside
-- their own OnLoad, so it has to exist before the addon loads (1874).
GROUP = "Group"
RESET_INSTANCES = "Reset all instances"
CONFIRM_RESET_INSTANCES = "Do you really want to reset all of your instances?"
YES = "Yes"
@@ -100,7 +105,8 @@ fn the_self_menu_row_gates_on_the_binding_and_confirms_before_sending() {
"Interface\\FrameXML\\PartyFrame.xml",
"Interface\\FrameXML\\TargetFrame.xml",
"Interface\\FrameXML\\PetFrame.xml",
"RaidFrame.xml",
r"Interface\FrameXML\RaidFrame.xml",
r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml",
] {
load_xml(&s, file);
}
@@ -90,7 +90,7 @@ fn harness_with(player: &str) -> UiScript {
for file in [
r"Interface\FrameXML\GlobalStrings.lua",
"Fonts.xml",
"BasicControls.xml", // `TEXT`
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`
"MoneyFrame.xml",
"UiPanels.xml",
// The chain's `PanelTemplates_SelectTab` reaches for `GameTooltip` unguarded.
@@ -1078,6 +1078,9 @@ fn trade_recipient_money_renders_the_digit_not_ellipsis() {
load_xml(&s, f);
}
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // the BenillaMoney_* helpers
// TradeFrame's money entry is the chain's own since 1882 — its OnLoad calls MoneyInputFrame_*.
load_xml(&s, "Interface\\FrameXML\\MoneyInputFrame.lua");
load_xml(&s, "Interface\\FrameXML\\MoneyInputFrame.xml");
load_xml(&s, "TradeFrame.xml");
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
@@ -134,7 +134,7 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript {
s.run("LocalizeFrames()").unwrap();
load_xml(&s, "Fonts.xml");
// `TEXT()`, which the reference's own tooltip and label formatting passes every string through.
load_xml(&s, "BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
@@ -23,6 +23,14 @@ fn load_action_bar(s: &UiScript) {
"GameTooltip.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
] {
load_xml(s, file);
}
@@ -429,6 +437,14 @@ fn multibar_hover_renders_the_buttons_own_action() {
"GameTooltip.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"MultiBars.xml",
] {
load_xml(&s, file);
@@ -750,6 +750,14 @@ fn interface_harness() -> UiScript {
"Interface\\FrameXML\\ColorPickerFrame.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"QuestFrame.xml",
@@ -801,6 +809,14 @@ fn actionbars_harness() -> UiScript {
"UIParent.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"MultiBars.xml",
],
);
@@ -13,11 +13,21 @@ fn load_pet_bar(s: &UiScript) {
for file in [
"MoneyFrame.xml",
"UiPanels.xml",
"Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"UIParent.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
"Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"PetActionBar.xml",
] {
load_xml(s, file);
@@ -353,11 +363,21 @@ fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
for file in [
"MoneyFrame.xml",
"UiPanels.xml",
"Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"UIParent.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
"Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
] {
load_xml(&s, file);
}
@@ -643,6 +663,14 @@ fn load_pet_bar_with_tooltip(s: &UiScript) {
"GameTooltip.xml",
"Cooldown.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"PetActionBar.xml",
] {
load_xml(s, file);
@@ -91,7 +91,7 @@ fn setup() -> UiScript {
// never paints.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
load_xml(&s, "BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
+50 -52
View File
@@ -35,7 +35,26 @@ fn setup() -> UiScript {
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "FriendsFrame.xml");
load_xml(&s, "RaidFrame.xml");
// Stock `RaidFrame_OnLoad` reconciles the party frames the moment it loads
// (`RaidOptionsFrame_UpdatePartyFrames` -> `HidePartyFrame`/`ShowPartyFrame`), so this pane
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
// The reference's own `BasicControls.xml`, which is manifest entry 3 — far above the pane
// (1874). `UIParentLoadAddOn` used to be what this line was here for; 1881 moved that
// function to `UIParent.xml`, its home in the reference, and this harness already loads
// `UIParent.xml` above, so the `RaidFrame_LoadUI` arm on `RAID_ROSTER_UPDATE` still
// resolves. What is left here is the error/message pair and `TEXT`.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\BuffFrame.xml");
load_xml(&s, r"Interface\FrameXML\UnitFrame.xml");
load_xml(&s, r"Interface\FrameXML\CombatFeedback.xml");
load_xml(&s, r"Interface\FrameXML\PartyFrame.xml");
load_xml(&s, r"Interface\FrameXML\RaidFrame.xml");
load_xml(&s, r"Interface\AddOns\Blizzard_RaidUI\Blizzard_RaidUI.xml");
s
}
@@ -738,7 +757,18 @@ fn the_raid_info_panel_lists_the_saved_lockouts() {
.unwrap(),
0
);
assert!(!visible(&s, "RaidInfoInstance1"));
// **The stale row STAYS, and that is the reference's own defect.** `RaidInfoFrame_Update`
// wraps its whole row loop in `if ( savedInstances > 0 )` (RaidFrame.lua:93-106), so the
// count dropping to zero never reaches the `Hide()` in its else-arm — the last lockout
// remains painted under an already-open panel. Our retired file hid them; the migration
// reverts that, and it is the director's call, not ours to re-author (1874).
//
// What DOES protect the player is the line above: the button is disabled, so the flyout
// cannot be reopened onto the stale row.
assert!(
visible(&s, "RaidInfoInstance1"),
"the reference leaves the last row standing when the count hits zero"
);
// The button toggles the flyout.
assert!(visible(&s, "RaidInfoFrame"));
@@ -779,12 +809,21 @@ fn a_player_with_no_lockouts_loses_the_raid_info_button_on_the_second_answer() {
"an empty lockout list is a dead button"
);
// And the panel behind it is emptied, not left in the state the XML loaded: every row away,
// and no scroll bar standing over a list of nothing.
// **The panel IS left in the state the XML loaded, and that is the reference's own defect.**
// `RaidInfoFrame_Update` wraps its entire row loop in `if ( savedInstances > 0 )`
// (RaidFrame.lua:93-106), so with a lockout list that has never had anything in it the loop
// never runs at all and no row is ever hidden — they keep the visibility stock
// `Blizzard_RaidUI.xml` declared. Our retired file cleared them; the migration reverts that,
// and a look the migration reverts is the director's call, not ours to re-author (1874).
//
// What protects the player is the line above: the button is disabled, so this panel cannot be
// reached by a click at all. The rows below are only visible because the test showed the frame
// by hand.
s.run("RaidInfoFrame:Show()").unwrap();
assert!(!visible(&s, "RaidInfoInstance1"));
assert!(!visible(&s, "RaidInfoInstance10"));
assert!(!visible(&s, "RaidInfoScrollFrameScrollBar"));
assert!(
visible(&s, "RaidInfoInstance1") && visible(&s, "RaidInfoInstance10"),
"with no lockouts the reference's update never runs, so the XML's own rows stand"
);
s.run("RaidInfoFrame:Hide()").unwrap();
// The button is dead to a real click, not merely drawn grey — the half the director asked for
@@ -909,51 +948,10 @@ fn the_ready_check_popup_opens_and_answers() {
);
}
// ─────────────────────────────────────────────────────────────────────────────────────────────
// The reference-geometry diff (decision 0675's discipline)
// ─────────────────────────────────────────────────────────────────────────────────────────────
/// Every element this file shares with the reference carries the reference's numbers.
///
/// The pane's specification is TWO reference files — `RaidFrame.xml` from FrameXML and
/// `Blizzard_RaidUI.xml` from the load-on-demand addon — so this runs twice. The addon half is
/// read straight out of the patch chain rather than from an extraction on disk: `_extracted_framexml/`
/// holds FrameXML only, and requiring someone to have extracted the addon first would leave half
/// the window's geometry guarded by nothing.
#[test]
fn the_geometry_matches_both_reference_files() {
let _data = benilla_formats::wow_data_or_skip!();
// The FrameXML half: the pane, its two buttons, the info flyout and the lockout row template.
if let Some(reference) = super::framexml_diff::reference("RaidFrame.xml") {
super::framexml_diff::assert_geometry_matches(
"RaidFrame.xml",
&reference,
&[
// The reference's close button inherits `UIPanelCloseButton`, which carries the
// 32x32; no such template ships in this house (FriendsFrame's own close button
// says the same), so ours states the size the template would have given it. The
// ANCHOR offsets match; only the extra size pair differs.
"RaidInfoCloseButton",
],
10,
);
}
// The addon half: the row, the slot, the group frame and the ready-check popup.
let Some(data) = benilla_formats::wow_data() else {
return;
};
let chain = benilla_formats::open_chain(&data).expect("open chain");
let bytes = chain
.read("Interface\\AddOns\\Blizzard_RaidUI\\Blizzard_RaidUI.xml")
.expect("Blizzard_RaidUI.xml is in the patch chain");
super::framexml_diff::assert_geometry_matches_text(
"RaidFrame.xml",
&String::from_utf8_lossy(&bytes),
&[],
10,
);
}
// The reference-geometry diff that stood here is GONE with the transcription it policed (1874).
// It compared our `RaidFrame.xml`'s numbers against the reference's; the pane is now the
// reference's own file off the player's chain, so there is nothing left to diff — the geometry is
// the reference's by construction rather than by assertion.
/// **A drag the cursor carries off the window edge ENDS — it does not glue the row to the mouse
/// for the rest of the session** (B310).
@@ -218,8 +218,9 @@ fn the_reputation_page_opens_on_the_windows_third_tab() {
"slot 9 has nothing to hold"
);
assert!(!shown(&mut s, "ReputationHeader9"), "neither twin shows");
// No trough line here: it was ours, and the stock pane declares none (1875). Asserting a frame
// that cannot exist would pass for the wrong reason.
assert!(!visible(&mut s, "ReputationListScrollFrameScrollBar"));
assert!(!visible(&mut s, "ReputationListScrollFrameScrollBarTrough"));
// Switching away hides it again through the same one-page-at-a-time switch.
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
@@ -383,28 +384,11 @@ fn the_scroll_offset_rebinds_the_fixed_row_slots() {
visible(&mut s, "ReputationListScrollFrameScrollBar"),
"21 rows in 15 slots raises the scroll bar"
);
assert!(
visible(&mut s, "ReputationListScrollFrameScrollBarTrough"),
"and the trough it rides in comes up with it (the kit shows the pair together)"
);
// **The trough fits this window to the pixel, and that is not a coincidence.**
// `BenillaScrollTrough_Seat`'s 21-above / 20-below / 8-left hang was DERIVED from
// ReputationFrame's own reference anchors (ScrollTemplates.xml cites ref l.573-599 against
// ref-UIPanelTemplates.xml l.166-181), so the seated trough has to land back on the reference's
// own numbers here: TOPRIGHT + (-2, +5), BOTTOMRIGHT + (+29, -4), 31 wide. Miss it by 4 and the
// arrow buttons ride out of their sockets onto the caps, which is what B224 reported.
let d = |s: &mut UiScript, expr: &str| s.eval::<f64>(&format!("return {expr}")).unwrap();
let sf_right = d(&mut s, "ReputationListScrollFrame:GetRight()");
let sf_top = d(&mut s, "ReputationListScrollFrame:GetTop()");
let sf_bottom = d(&mut s, "ReputationListScrollFrame:GetBottom()");
let t = "ReputationListScrollFrameScrollBarTrough";
assert_eq!(d(&mut s, &format!("{t}:GetLeft()")), sf_right - 2.0);
assert_eq!(d(&mut s, &format!("{t}:GetRight()")), sf_right + 29.0);
assert_eq!(d(&mut s, &format!("{t}:GetTop()")), sf_top + 5.0);
assert_eq!(d(&mut s, &format!("{t}:GetBottom()")), sf_bottom - 4.0);
assert_eq!(d(&mut s, &format!("{t}:GetWidth()")), 31.0);
// **The trough is GONE with our pane.** `ReputationListScrollFrameScrollBarTrough` was the one
// name in this window that was never the reference's (1844) — it belongs to our own scroll
// templates, and stock `ReputationFrame.xml` declares no such thing. B224's law (the 21/20/8
// hang that drops each arrow into its socket) is unchanged and still asserted, by the
// keybindings page's own trough, which is a window we still own (1875).
// Scroll three rows down: every slot re-binds, and slot 1 stops being a header.
s.run("FauxScrollFrame_SetOffset(ReputationListScrollFrame, 3) ReputationFrame_Update()")
.unwrap();
@@ -127,6 +127,14 @@ fn shipped_spellbook_drives_end_to_end() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "ActionBar.xml");
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
load_xml(&s, r"Interface\FrameXML\ReputationFrame.xml");
load_xml(&s, "SpellBookFrame.xml");
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
@@ -657,7 +665,7 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
for file in [
r"Interface\FrameXML\GlobalStrings.lua",
"Fonts.xml",
"BasicControls.xml", // `TEXT`
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`
"MoneyFrame.xml",
"UiPanels.xml",
// `ShowMacroFrame` lives here since 1848.
@@ -675,6 +683,14 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
r"Interface\FrameXML\ClassTrainerFrameTemplates.xml",
"MicroMenu.xml",
"ActionBar.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
r"Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml",
"SpellBookFrame.xml",
] {
+12 -4
View File
@@ -143,7 +143,7 @@ fn read(req: &str) -> Option<Vec<u8>> {
pub(super) const MERCHANT_UI: &[&str] = &[
"Interface\\FrameXML\\GlobalStrings.lua",
"Fonts.xml",
"BasicControls.xml", // TEXT()
"Interface\\FrameXML\\BasicControls.xml", // TEXT()
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
"UiPanels.xml",
@@ -220,7 +220,11 @@ pub(super) const CHARACTER_UI: &[&str] = &[
// concatenates `SPELL_STAT0_NAME`..`4` on every repaint.
"Interface\\FrameXML\\GlobalStrings.lua",
"Fonts.xml",
"BasicControls.xml", // TEXT(), which every one of those label sets goes through
// `GetText` — the reference's gendered-string helper, which stock
// `ReputationFrame.lua:65` calls for every row's standing label. Manifest line 94
// (1875).
r"Interface\FrameXML\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml", // TEXT(), which every one of those label sets goes through
"Interface\\FrameXML\\ItemButtonTemplate.xml", // PaperDollItemSlotButtonTemplate's base
"MoneyFrame.xml",
"UIParent.xml", // Model_OnLoad/_Rotate*/_OnUpdate — the model panes' turntable
@@ -267,7 +271,11 @@ pub(super) const CHARACTER_UI: &[&str] = &[
"Interface\\FrameXML\\CharacterFrame.xml",
"Interface\\FrameXML\\PaperDollFrame.xml",
"Interface\\FrameXML\\PetPaperDollFrame.xml",
"ReputationFrame.xml",
// `updateContainerFrameAnchors` — `ReputationWatchBar_Update` hard-calls it when the bar
// moves (ReputationFrame.lua:248), because in the reference the bar's presence reflows the
// bag row. It comes with `ContainerFrame.xml`, manifest 585 against the pane's 693 (1875).
"Interface\\FrameXML\\ContainerFrame.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"SkillFrame.xml",
"Interface\\FrameXML\\HonorFrame.xml",
];
@@ -286,7 +294,7 @@ pub(super) const BAG_UI: &[&str] = &[
// `MainMenuBarBackpackButton`'s OnEnter calls (`GameTooltip:SetText(TEXT(BACKPACK_TOOLTIP)…)`)
// and `BagSlotButton_OnEnter` calls for `EQUIP_CONTAINER`. Manifest entry 3, and not optional
// for the bag bar since 1751's third window made that bar the reference's own.
"BasicControls.xml",
"Interface\\FrameXML\\BasicControls.xml",
// `UIParent` itself: the twelve `ContainerFrame`s declare `parent="UIParent"`, and
// `updateContainerFrameAnchors` anchors each open bag to `frame:GetParent()` while
// `OpenAllBags` opens with `if not UIParent:IsVisible() then return end`. Without it the
@@ -42,7 +42,7 @@ fn harness() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
// `TEXT()`, which AutoFollowStatus_OnEvent puts its message through.
load_xml(&s, "BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
+10 -7
View File
@@ -17,12 +17,16 @@ mod common;
/// Every one of these is a real dependency: UiPanels for the panel manager + tab kit + StaticPopup
/// engine, ScrollTemplates for the faux lists, UIPanelTemplates for the button/input/checkbox
/// templates, UIDropDownMenu for the rarity capsule, MoneyFrame for `SmallMoneyFrameTemplate` +
/// the `MoneyTypeInfo` table this window registers `AUCTION_DEPOSIT` into, The `BenillaMoneyInput_*` money-entry
/// the `MoneyTypeInfo` table this window registers `AUCTION_DEPOSIT` into, The `MoneyInputFrame_*` money-entry
/// helpers used to mean loading MerchantFrame.xml as well; 1751 moved that kit to MoneyFrame.xml
/// on its way to the chain, so the dependency is gone.
const FILES: [&str; 9] = [
const FILES: [&str; 11] = [
"Fonts.xml",
"MoneyFrame.xml",
// The four money-entry frames come off the chain since 1882 — `MoneyInputFrameTemplate` and
// the `MoneyInputFrame_*` verbs, replacing our own verbatim copy of both.
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
"UiPanels.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
@@ -368,7 +372,7 @@ fn the_bid_and_buyout_gates_read_the_purse() {
"50s is affordable on 2g"
);
assert_eq!(
s.eval::<i64>("return BenillaMoneyInput_GetCopper('BrowseBidPrice')")
s.eval::<i64>("return MoneyInputFrame_GetCopper(BrowseBidPrice)")
.unwrap(),
1000,
"with no bids the required bid IS the minimum bid"
@@ -529,9 +533,9 @@ fn the_create_gate_and_the_deposit() {
);
// With no item, the form does not even reach the price checks — the buyout error stays hidden.
s.run("BenillaMoneyInput_SetCopper('StartPrice', 10000)")
s.run("MoneyInputFrame_SetCopper(StartPrice, 10000)")
.unwrap();
s.run("BenillaMoneyInput_SetCopper('BuyoutPrice', 5000)")
s.run("MoneyInputFrame_SetCopper(BuyoutPrice, 5000)")
.unwrap();
s.run("AuctionsFrameAuctions_ValidateAuction()").unwrap();
assert!(!s
@@ -604,8 +608,7 @@ fn the_create_gate_and_the_deposit() {
.unwrap());
// Clear the buyout and the form opens; pressing Create sends exactly what is on screen.
s.run("BenillaMoneyInput_SetCopper('BuyoutPrice', 0)")
.unwrap();
s.run("MoneyInputFrame_SetCopper(BuyoutPrice, 0)").unwrap();
s.run("AuctionsFrameAuctions_ValidateAuction()").unwrap();
assert!(
s.eval::<bool>("return AuctionsCreateAuctionButton:IsEnabled() ~= 0")
+1 -1
View File
@@ -36,7 +36,7 @@ use benilla_ui::script::{UiScript, UnitCombatStats, UnitState};
const FILES: [&str; 12] = [
"Interface\\FrameXML\\GlobalStrings.lua",
"Fonts.xml",
"BasicControls.xml",
"Interface\\FrameXML\\BasicControls.xml",
// The reference's own since 1751 window 24 — `common::load_ui` speaks both stores.
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
+5 -1
View File
@@ -12,9 +12,13 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// The mail window's load prefix — the app's own order (`ui_script/mod.rs`), members only.
/// MerchantFrame.xml rides along because MailFrame.xml reuses its global `BenillaMoney_*` coin
/// helpers (postage display), so a load error in either fails here.
const FILES: [&str; 7] = [
const FILES: [&str; 9] = [
"Fonts.xml",
"MoneyFrame.xml",
// The send tab's money entry comes off the chain since 1882 — `MoneyInputFrameTemplate` and
// the `MoneyInputFrame_*` verbs. Seated straight after MoneyFrame.xml, benilla.toc's order.
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
+1 -1
View File
@@ -25,7 +25,7 @@ const FILES: [&str; 13] = [
"Interface\\FrameXML\\GlobalStrings.lua",
"Fonts.xml",
// `TEXT`.
"BasicControls.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
"UiPanels.xml",
+6 -1
View File
@@ -13,9 +13,14 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// The trade window's load prefix — the app's own order (`ui_script/mod.rs`), members only.
/// MerchantFrame.xml rides along because TradeFrame.xml reuses its global `BenillaMoney_*` coin
/// helpers (the two gold displays), so a load error in either fails here.
const FILES: [&str; 7] = [
const FILES: [&str; 9] = [
"Fonts.xml",
"MoneyFrame.xml",
// The money entry comes off the chain since 1882 — `MoneyInputFrameTemplate` and the
// `MoneyInputFrame_*` verbs the window's OnLoad calls. Seated straight after MoneyFrame.xml,
// which is benilla.toc's own order.
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
+27 -5
View File
@@ -323,14 +323,32 @@ pub fn expand(
element: &Element,
templates: &HashMap<&str, &Element>,
warnings: &mut Vec<String>,
) -> Element {
expand_known(element, templates, &HashSet::new(), warnings)
}
/// [`expand`], plus the names that are known to be **font objects** rather than element templates.
///
/// A `<FontString inherits="GameFontNormalSmall">` names a font, not a template, and the font is
/// applied later by its own path (`apply_fontstring_font`) — so the name must be skipped here
/// silently rather than warned as unknown. `Loader::expand_region` already made that distinction
/// for an INSTANCE; it could not make it one level down, and stock `BuffFrame.xml` is exactly that
/// case: `BuffButtonDurationTemplate` is a virtual `<FontString>` that inherits a font object, so
/// every instance of it warned twenty-six times over (decision 1874).
pub fn expand_known(
element: &Element,
templates: &HashMap<&str, &Element>,
fonts: &HashSet<&str>,
warnings: &mut Vec<String>,
) -> Element {
let mut active = HashSet::new();
expand_inner(element, templates, warnings, &mut active)
expand_inner(element, templates, fonts, warnings, &mut active)
}
fn expand_inner(
element: &Element,
templates: &HashMap<&str, &Element>,
fonts: &HashSet<&str>,
warnings: &mut Vec<String>,
active: &mut HashSet<String>,
) -> Element {
@@ -372,13 +390,17 @@ fn expand_inner(
.map(|(_, v)| *v)
});
let Some(template) = hit else {
warnings.push(format!(
"unknown template '{name}' referenced by inherits; skipping"
));
// A FONT OBJECT is not an unknown template: it is a different namespace, applied by
// `apply_fontstring_font` after this. Skip it without a word (1874).
if !fonts.contains(name) {
warnings.push(format!(
"unknown template '{name}' referenced by inherits; skipping"
));
}
active.remove(name);
continue;
};
let expanded_template = expand_inner(template, templates, warnings, active);
let expanded_template = expand_inner(template, templates, fonts, warnings, active);
active.remove(name);
base = Some(match base {
None => expanded_template,
+8 -1
View File
@@ -593,8 +593,15 @@ impl Loader<'_> {
let templates = model.framexml_templates.borrow();
let view: HashMap<&str, &Element> =
templates.iter().map(|(k, v)| (k.as_str(), v)).collect();
// The font namespace, so a font object inside an inherit chain is skipped rather than
// warned as an unknown template (1874).
let fonts = model.framexml_fonts.borrow();
let font_names: std::collections::HashSet<&str> =
fonts.keys().map(|k| k.as_str()).collect();
let mut warns = Vec::new();
let out = framexml::expand(el, &view, &mut warns);
let out = framexml::expand_known(el, &view, &font_names, &mut warns);
drop(font_names);
drop(fonts);
drop(view);
drop(templates);
drop(model);
+41
View File
@@ -42,6 +42,25 @@ const BUILD_DATE: &str = "Sep 19 2006";
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
// `GetLocale()` -> one string, and `IsMacClient()` -> nil, both ARITY-EXACT in the reference's
// own shape table (`re/audit/binding-shapes.tsv`: `GetLocale 0x46ce40`/`0x48d8b0` is
// `0 -> 1 (string?)`, `IsMacClient 0x48c980` is `0 -> 1 (nil)`, both `exact`/`agree`).
//
// **`IsMacClient` answering nil is the whole verb, not a placeholder.** The table records the
// return KIND as `(nil)` — this binding pushes nil on the PC build the reference was carved
// from, and there is nothing else for it to say. FrameXML branches on it for Mac-only key
// labels; nil takes the PC arm, which is the arm this client wants.
//
// `GetLocale` is the two-registration case the shape gate warns about (1843): a glue copy and
// an in-game copy, same shape. Ours answers the one locale this client ships against — the
// enUS `GlobalStrings.lua` the manifest loads and 1804's reference defaults are read from.
// Decision 1880.
g.set(
"GetLocale",
lua.create_function(|lua, ()| lua.create_string("enUS"))?,
)?;
g.set("IsMacClient", lua.create_function(|_, ()| Ok(Value::Nil))?)?;
// version, build, date — three, and no fourth (decision 1842)
g.set(
"GetBuildInfo",
@@ -286,4 +305,26 @@ mod tests {
"a malformed script must raise, not vanish"
);
}
/// `GetLocale` answers one string and `IsMacClient` answers nil — the reference's own shapes
/// (`binding-shapes.tsv`, both `exact`/`agree`), and the arity is the half a caller branches on.
#[test]
fn the_client_identity_pair_answers_its_reference_arity() {
let s = UiScript::new().unwrap();
assert_eq!(
s.eval::<i64>("return select('#', GetLocale())").unwrap(),
1,
"GetLocale pushes exactly one value"
);
assert_eq!(s.eval::<String>("return GetLocale()").unwrap(), "enUS");
assert_eq!(
s.eval::<i64>("return select('#', IsMacClient())").unwrap(),
1,
"IsMacClient pushes one value, and it is nil"
);
assert!(
s.eval::<bool>("return IsMacClient() == nil").unwrap(),
"nil is the PC arm, which is the arm this client wants"
);
}
}
+18 -4
View File
@@ -445,19 +445,33 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// CollapseFactionHeader(index) / ExpandFactionHeader(index) — a VISIBLE header index; `0` or a
// non-header row acts on EVERY header (the client's own fall-through).
// **Both fire `UPDATE_FACTION`, and that is the engine's own doing, not a convenience.**
// `CollapseFactionHeader 0x4d6a50` and `ExpandFactionHeader 0x4d6aa0` both reach `0x4d6400`,
// which tail-jumps to `0x4d5c40` — and `0x4d5dab`, inside that function, is one of the FOUR
// whole-image fire sites of `UPDATE_FACTION` (wow-re `reputation-panel-law.md` §7.5 and §8,
// both VERIFIED). The stock pane has no other repaint path after a fold: its header `<OnClick>`
// calls the binding and nothing else (ReputationFrame.xml:9-15), and `ReputationFrame_OnEvent`
// repaints on `UPDATE_FACTION` alone. Our own retired file repainted itself, which is why this
// was invisible until the pane went stock (1875).
g.set(
"CollapseFactionHeader",
lua.create_function(|lua, index: usize| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
set_collapsed(&mut model, index, true);
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
set_collapsed(&mut model, index, true);
}
super::event::fire_global(lua, "UPDATE_FACTION", &[]);
Ok(())
})?,
)?;
g.set(
"ExpandFactionHeader",
lua.create_function(|lua, index: usize| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
set_collapsed(&mut model, index, false);
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
set_collapsed(&mut model, index, false);
}
super::event::fire_global(lua, "UPDATE_FACTION", &[]);
Ok(())
})?,
)?;