seven more windows are the reference's own files

- seven more windows run off the player's own patch chain: the durability shield (`DurabilityFrame.xml`, with a Rust-side repair for the reference's own stale side-glyph inset the director caught on screen), `Localization.xml` + `GameTime.xml` (so `LocalizeFrames` finally runs from `VARIABLES_LOADED`, and the clock's 24-hour flag comes from where 1.12 sets it), the chat-link tooltip (`ItemRef.xml` — draggable now, and its shift-click arm fills the friend/ignore/who boxes as the reference's does), the mirror timers (`MirrorTimer.xml`, which exposed that our `STATICPOPUP_NUMDIALOGS` was 2 against the reference's 4, so a third breath/fatigue/feign bar could never be claimed), the zone splash with the fading kit, the item-text reader and the combo-point dots — each under the reference's published frame names, which is what an addon reaches for
- `AddMessage` swallows a text it cannot use, silently, as the reference does: `0x795590` is a `lua_isstring` gate whose failure edge jumps straight to the epilogue — no line, no error, no record — and stock FrameXML takes that path: a full keyring calls `AddMessage(NO_EMPTY_KEYRING_SLOTS, …)` and no such GlobalString exists in 1.12, so it is a dead click on a real client where benilla put a script-error dialog on it; byte-verified by a four-worker cross-check in wow-re, and NOT generalised — a bad r/g/b/a still adds the line opaque white, only a bad text shows nothing

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 16:58:13 +07:00
co-authored by Claude Fable 5.1
parent 6da7ed3fca
commit c3dd67e3f1
35 changed files with 735 additions and 2106 deletions
+1 -1
View File
@@ -847,7 +847,7 @@
-- declared width as `noResize` + a fixed list width (UIDropDownMenu.xml l.672-679),
-- so transcribing it would pin the menu at 50 px and cut "Background" in half. In the
-- reference, MENU mode never draws the capsule at all, so its width is inert there.
-- `BenillaFriendsDropDown` (FriendsFrame.xml) is already this same shape.
-- `FriendsDropDown` (FriendsFrame.xml) is already this same shape.
function FCFOptionsDropDown_OnLoad()
this.initialize = FCFOptionsDropDown_Initialize
this.displayMode = "MENU"
-199
View File
@@ -1,199 +0,0 @@
<Ui>
<!--
The combo-point dots (decision 0869) — our implementation of the behaviour the reference's
`FrameXML/ComboFrame.xml` + `ComboFrame.lua` specify, on our frames and driven by our
`PLAYER_COMBO_POINTS` feed. The Lua below is our own; what IS quoted, deliberately, is the
published surface and the measurements: the global function and constant names addons call,
and every number — offsets, sizes, TexCoords and the five fade durations are the reference's
own, because they are facts about how the real interface looks and moves, not choices.
ONE `Interface\ComboFrame\ComboPoint` sheet holds all three layers, split by TexCoords: the
dim socket (0 .. 0.375), the lit highlight (0.375 .. 0.5625) and the additive shine
(0.5625 .. 1). A point that comes up fades its highlight in, and the highlight's completion
hands off to the shine's fade-in → fade-out — the reference's two-hop `finishedFunc` chain,
which our `UIFrameFade` (UiPanels.xml) implements in full.
NO CLASS TEST HERE, and none is missing: this file gates on `GetComboPoints() > 0` and
nothing else because the reference does, and `0x51a190` carries both real gates itself —
rogue-or-druid (`cmp al,4 / cmp al,0xb`), and the points must be banked on the CURRENT
target. So a *warrior* never lights a dot even though the server really does bank them a
point on a victim's dodge — that byte greys Overpower through the usable walk's leg 5 and
goes no further (§5 byte-read, decision 0875). Re-targeting empties these dots with the
count untouched, which is exactly why the reference registers PLAYER_TARGET_CHANGED.
The five dots march down-left, each anchored TOP-to-BOTTOM of the one before with a small
rightward drift — the reference's diagonal, hugging the target frame's ring.
-->
<Frame name="BenillaComboPointTemplate" virtual="true">
<Size><AbsDimension x="12" y="12"/></Size>
<Layers>
<!-- The empty socket (ref l.8-19). -->
<Layer level="BACKGROUND">
<Texture file="Interface\ComboFrame\ComboPoint">
<Size><AbsDimension x="12" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"/>
</Anchors>
<TexCoords left="0" right="0.375" top="0" bottom="1"/>
</Texture>
</Layer>
<!-- The lit point, faded in as the point is earned (ref l.20-34). -->
<Layer level="ARTWORK">
<Texture name="$parentHighlight" file="Interface\ComboFrame\ComboPoint">
<Size><AbsDimension x="8" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.375" right="0.5625" top="0" bottom="1"/>
</Texture>
</Layer>
<!-- The additive flare, chained off the highlight's fade (ref l.35-48). -->
<Layer level="OVERLAY">
<Texture name="$parentShine" file="Interface\ComboFrame\ComboPoint" alphaMode="ADD">
<Size><AbsDimension x="14" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="4"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.5625" right="1" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
</Frame>
<Frame name="ComboFrame" frameStrata="MEDIUM" toplevel="true" parent="UIParent" hidden="true" alpha="0">
<Size><AbsDimension x="256" y="32"/></Size>
<!-- ref l.56-62: TOPRIGHT of the target frame, (-44,-9). -->
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="TargetFrame">
<Offset><AbsDimension x="-44" y="-9"/></Offset>
</Anchor>
</Anchors>
<Frames>
<Frame name="ComboPoint1" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="ComboPoint2" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="ComboPoint1" relativePoint="BOTTOM">
<Offset><AbsDimension x="7" y="4"/></Offset>
</Anchor>
</Anchors>
</Frame>
<Frame name="ComboPoint3" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="ComboPoint2" relativePoint="BOTTOM">
<Offset><AbsDimension x="5" y="2"/></Offset>
</Anchor>
</Anchors>
</Frame>
<Frame name="ComboPoint4" inherits="BenillaComboPointTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="ComboPoint3" relativePoint="BOTTOM">
<Offset><AbsDimension x="2" y="1"/></Offset>
</Anchor>
</Anchors>
</Frame>
<!-- The fifth is the odd one out: the reference sizes it 15×18 (ref l.101-104). -->
<Frame name="ComboPoint5" inherits="BenillaComboPointTemplate">
<Size><AbsDimension x="15" y="18"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="ComboPoint4" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="1"/></Offset>
</Anchor>
</Anchors>
</Frame>
</Frames>
<Scripts>
<OnLoad>
this:RegisterEvent("PLAYER_TARGET_CHANGED");
this:RegisterEvent("PLAYER_COMBO_POINTS");
ComboPoint1Highlight:SetAlpha(0);
ComboPoint1Shine:SetAlpha(0);
</OnLoad>
<OnEvent>
ComboPointsFrame_OnEvent();
</OnEvent>
</Scripts>
</Frame>
<Script>
<![CDATA[
-- The reference's global names, carrying the reference's measured timings: addons read
-- these by name, and the five durations are what the real interface's fades take, so both
-- halves are fixed. MAX_COMBO_POINTS lives in the ref's TargetFrame.lua; we have no such
-- file, and CombatText.xml's five-point flash reads it, so it is declared here.
MAX_COMBO_POINTS = 5;
COMBOFRAME_FADE_IN = 0.3;
COMBOFRAME_FADE_OUT = 0.5;
COMBOFRAME_HIGHLIGHT_FADE_IN = 0.4;
COMBOFRAME_SHINE_FADE_IN = 0.3;
COMBOFRAME_SHINE_FADE_OUT = 0.4;
COMBO_FRAME_LAST_NUM_POINTS = 0;
-- Dot `index`'s two animated layers: the lit point, and the additive flare over it. The
-- socket beneath them never moves, so nothing ever needs to reach for it.
local function comboPointLayers(index)
return getglobal("ComboPoint"..index.."Highlight"),
getglobal("ComboPoint"..index.."Shine");
end
-- Repaint the dots for the count `GetComboPoints` reports. Only points ABOVE the previous
-- count animate: a repaint at an unchanged count (PLAYER_TARGET_CHANGED fires one on every
-- re-select) must not re-flare dots that are already lit — which is why the count is
-- remembered across events instead of being read back off the dots.
function ComboPointsFrame_OnEvent()
local points = GetComboPoints();
if points > 0 then
if not ComboFrame:IsVisible() then
ComboFrame:Show();
UIFrameFadeIn(ComboFrame, COMBOFRAME_FADE_IN);
end
for i = 1, MAX_COMBO_POINTS do
local highlight, shine = comboPointLayers(i);
if i > points then
-- Past the count: dark at once, never animated back down.
highlight:SetAlpha(0);
shine:SetAlpha(0);
elseif i > COMBO_FRAME_LAST_NUM_POINTS then
-- Newly earned: light it, and hand the finished fade on to its flare.
UIFrameFade(highlight, {
mode = "IN",
timeToFade = COMBOFRAME_HIGHLIGHT_FADE_IN,
finishedFunc = ComboPointShineFadeIn,
finishedArg1 = shine,
});
end
end
else
-- Nothing banked. The frame goes away, so only the first dot is worth clearing:
-- the loop above blanks every dot past the count the next time one is earned.
local highlight, shine = comboPointLayers(1);
highlight:SetAlpha(0);
shine:SetAlpha(0);
ComboFrame:Hide();
end
COMBO_FRAME_LAST_NUM_POINTS = points;
end
-- Leg two of the chain, reached as the highlight finishes: flare the shine in, and book
-- leg three off ITS completion. The hand-off travels by NAME because a fade's finishedArg
-- rides on the frame's own fadeInfo, and the reference notes the same self-reference
-- problem where it does this.
function ComboPointShineFadeIn(shine)
UIFrameFade(shine, {
mode = "IN",
timeToFade = COMBOFRAME_SHINE_FADE_IN,
finishedFunc = ComboPointShineFadeOut,
finishedArg1 = shine:GetName(),
});
end
-- Leg three: the flare burns back out on its own.
function ComboPointShineFadeOut(shineName)
UIFrameFadeOut(getglobal(shineName), COMBOFRAME_SHINE_FADE_OUT);
end
]]>
</Script>
</Ui>
@@ -1,324 +0,0 @@
<Ui>
<!--
The durability alert — the "armor guy" under the minimap (director-directed this round,
with the doll-slot broken tint). Geometry, texture cells and colors are MEASUREMENTS of the
real 1.12 DurabilityFrame (extracted Interface\FrameXML\DurabilityFrame.xml + .lua) and the
show/hide law is the same law, written here in our own terms: 11 alert regions off one
Interface\Durability\UI-Durability-Icons atlas — 8 body pieces
(Head, Shoulders, Chest, Waist, Legs, Feet, Wrists, Hands) that show TOGETHER when
any of them alerts, plus 3 showSeparate pieces (Weapon, Shield/OffWeapon, Ranged) that each
show only when itself alerted. The whole frame hides at zero alerts.
The feed is the engine's GetInventoryAlertStatus(1..11) (benilla-ui char_stats: statuses
recomputed on every inventory push; a change fires UPDATE_INVENTORY_ALERTS — the same event
the real engine fires). Status colors are the ref's own table: 3 = yellow {1, 0.82, 0.18}
(damaged; the engine's threshold is INTERIM &lt; 20% pending the wow-re dispatch on the
0x4c94b0 binding's delegate), 4 = red {0.93, 0.07, 0.07} (broken). Statuses 1/2 are the
ref's own DISABLED enchant-expiry alerts (its color rows are commented out; its enchantTimer
arm too) — transcribed as-is, so they color nothing here either.
The XML anchor below is the ref's authored seat, and like every managed HUD frame it is
DEAD ON ARRIVAL: UIParent_ManageFramePositions re-seats the frame on every pass (ref
UIParent.lua:1758-1768 — -CONTAINER_OFFSET_X, minus 20 more when a side glyph shows; the
raw +40 hangs the sword/shield glyphs off-screen, director-caught). The loader's
post-load manage call seats it at startup; the OnShow/OnHide below (ref verbatim) re-seat
it on every alert transition.
OMITTED, named: the ref's OnUpdate — its whole body is the enchantTimer countdown, and
the timer is dead even in the ref (SetAlerts nils it; the only line setting it is
commented out) — a per-frame handler for provably dead state stays out.
SetFrameLevel(-1) in OnLoad is kept — the guy tucks under sibling HUD.
-->
<Script>
<![CDATA[
-- The alert regions, in the engine's own order: the array index IS the index
-- GetInventoryAlertStatus answers for, and `slot` names the texture that draws it. The
-- last three are the ones that speak for themselves; the eight above are one figure.
INVENTORY_ALERT_STATUS_SLOTS = {
[1] = {slot = "Head"},
[2] = {slot = "Shoulders"},
[3] = {slot = "Chest"},
[4] = {slot = "Waist"},
[5] = {slot = "Legs"},
[6] = {slot = "Feet"},
[7] = {slot = "Wrists"},
[8] = {slot = "Hands"},
[9] = {slot = "Weapon", showSeparate = 1},
[10] = {slot = "Shield", showSeparate = 1},
[11] = {slot = "Ranged", showSeparate = 1},
}
INVENTORY_ALERT_COLORS = {
-- [1]/[2]: the ref's commented-out enchant-alert colors (green/blue) — disabled there,
-- disabled here. [0] = no alert.
[3] = {r = 1, g = 0.82, b = 0.18},
[4] = {r = 0.93, g = 0.07, b = 0.07},
}
-- Which texture draws a slot. Every slot but the off hand has exactly one glyph named
-- after it; the off hand has TWO — a shield and a second sword — and only one of them is
-- ever on screen, so picking one is also hiding the other. That is the whole reason this
-- is a lookup with a branch in it rather than a name concatenation.
local function DurabilityGlyph(slot)
if slot ~= "Shield" then
return getglobal("Durability" .. slot)
end
if OffhandHasWeapon() then
DurabilityShield:Hide()
return DurabilityOffWeapon
end
DurabilityOffWeapon:Hide()
return DurabilityShield
end
-- Repaint the armour figure from the engine's per-region verdicts. Two passes, because the
-- eight body pieces are ONE alert between them: any of them alerting shows all eight, and
-- that is not known until the walk is over. (The enchantTimer nil is the reference's own
-- disabled residue — statuses 1/2 are its switched-off enchant-expiry alerts, so nothing
-- ever arms the timer and this only ever clears a field that was already nil.)
function DurabilityFrame_SetAlerts()
DurabilityFrame.enchantTimer = nil
local numAlerts = 0
local showDurability
local body, numBody = {}, 0
for index, value in pairs(INVENTORY_ALERT_STATUS_SLOTS) do
local texture = DurabilityGlyph(value.slot)
local color = INVENTORY_ALERT_COLORS[GetInventoryAlertStatus(index)]
-- Alerted pieces wear their status colour at full strength; the rest sit in the
-- faded white the art is authored against, which is what the body figure's
-- un-alerted pieces read as when one of their neighbours has lit up.
if color then
numAlerts = numAlerts + 1
texture:SetVertexColor(color.r, color.g, color.b, 1.0)
else
texture:SetVertexColor(1.0, 1.0, 1.0, 0.5)
end
if value.showSeparate then
-- Weapon, off hand, ranged: each is its own alert and its own decision.
if color then
texture:Show()
else
texture:Hide()
end
else
numBody = numBody + 1
body[numBody] = texture
if color then
showDurability = 1
end
end
end
for i = 1, numBody do
if showDurability then
body[i]:Show()
else
body[i]:Hide()
end
end
-- Nothing to warn about at all means no armour guy.
if numAlerts > 0 then
DurabilityFrame:Show()
else
DurabilityFrame:Hide()
end
end
]]>
</Script>
<Frame name="DurabilityFrame" parent="UIParent" frameStrata="LOW" hidden="false">
<Size>
<AbsDimension x="60" y="65"/>
</Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="MinimapCluster" relativePoint="BOTTOMRIGHT">
<Offset>
<AbsDimension x="40" y="15"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="DurabilityHead" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="18" y="22"/>
</Size>
<Anchors>
<Anchor point="TOP"/>
</Anchors>
<TexCoords left="0" right="0.140625" top="0" bottom="0.171875"/>
</Texture>
<Texture name="DurabilityShoulders" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="48" y="22"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityHead" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="16"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.140625" right="0.515625" top="0" bottom="0.171875"/>
</Texture>
<Texture name="DurabilityChest" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="20" y="22"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityShoulders" relativePoint="TOP">
<Offset>
<AbsDimension x="0" y="-7"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.515625" right="0.6640625" top="0" bottom="0.171875"/>
</Texture>
<Texture name="DurabilityWrists" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="44" y="22"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityShoulders" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="7"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.6640625" right="1.0" top="0" bottom="0.171875"/>
</Texture>
<Texture name="DurabilityHands" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="42" y="18"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityWrists" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="15"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0" right="0.328125" top="0.171875" bottom="0.3046875"/>
</Texture>
<Texture name="DurabilityWaist" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="16" y="5"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityChest" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="6"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.328125" right="0.46875" top="0.171875" bottom="0.203125"/>
</Texture>
<Texture name="DurabilityLegs" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="29" y="20"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityWaist" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="2"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.46875" right="0.6875" top="0.171875" bottom="0.3203125"/>
</Texture>
<Texture name="DurabilityFeet" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="41" y="32"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityLegs" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="8"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.6875" right="1.0" top="0.171875" bottom="0.4140625"/>
</Texture>
<Texture name="DurabilityWeapon" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="20" y="45"/>
</Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="DurabilityWrists" relativePoint="LEFT">
<Offset>
<AbsDimension x="0" y="-6"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0" right="0.140625" top="0.3203125" bottom="0.6640625"/>
</Texture>
<Texture name="DurabilityShield" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="25" y="31"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="DurabilityWrists" relativePoint="RIGHT">
<Offset>
<AbsDimension x="0" y="10"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.1875" right="0.375" top="0.3203125" bottom="0.5546875"/>
</Texture>
<Texture name="DurabilityOffWeapon" file="Interface\Durability\UI-Durability-Icons" hidden="true">
<Size>
<AbsDimension x="20" y="45"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="DurabilityWrists" relativePoint="RIGHT">
<Offset>
<AbsDimension x="0" y="-6"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0" right="0.140625" top="0.3203125" bottom="0.6640625"/>
</Texture>
<Texture name="DurabilityRanged" file="Interface\Durability\UI-Durability-Icons">
<Size>
<AbsDimension x="28" y="38"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="DurabilityShield" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="5"/>
</Offset>
</Anchor>
</Anchors>
<TexCoords left="0.1875" right="0.3984375" top="0.5546875" bottom="0.84375"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
this:SetFrameLevel(this:GetFrameLevel() - 1)
this:RegisterEvent("UPDATE_INVENTORY_ALERTS")
this:RegisterEvent("PLAYER_ENTERING_WORLD")
</OnLoad>
<OnEvent>
DurabilityFrame_SetAlerts()
-- benilla re-manages the HUD right-stack on every alert recompute. The ref keeps
-- the durability seat (and the quest tracker stacked under it) fresh because it
-- fires UIParent_ManageFramePositions from dozens of OnShow/OnHide sites; benilla
-- fires it from only a handful, so a showSeparate glyph that arrives AFTER the
-- frame is already shown — equipment streams in per-slot at login, the weapon
-- often breaking before the shield's slot lands — would otherwise leave
-- durabilityOffset stale and hang the shield off the screen's right edge
-- (director-caught). SetAlerts's own Show()/Hide() only re-manages on a
-- shown-state transition, which a mere glyph-set change is not.
UIParent_ManageFramePositions()
</OnEvent>
<OnShow>
UIParent_ManageFramePositions()
</OnShow>
<OnHide>
UIParent_ManageFramePositions()
</OnHide>
</Scripts>
</Frame>
</Ui>
+25 -4
View File
@@ -135,10 +135,31 @@
-- iterates it). Still zero corpus addons, so the demand here is ours, not theirs.
CHAT_FONT_HEIGHTS = { 12, 14, 16, 18 }
-- The two OTHER tables the reference's Fonts.xml `<Script>` block carries —
-- MATERIAL_TEXT_COLOR_TABLE and MATERIAL_TITLETEXT_COLOR_TABLE — are deliberately not
-- here: **zero corpus addons name either**, and nothing benilla ships reads them. They
-- cost nine lines each the day something does.
-- The page ink and the title ink per readable-material, ref Fonts.xml l.27-42 verbatim.
--
-- These were deliberately absent for a long time — "zero corpus addons name either, and
-- nothing benilla ships reads them; they cost nine lines each the day something does".
-- 1751's ninth window is that day: the reference's own `GetMaterialTextColors`
-- (UIParent.lua l.1880-1888) reads them by these names, and it is what the sourced
-- `ItemTextFrame.xml` calls. Our ItemTextFrame carried a private
-- `BENILLA_MATERIAL_*_COLOR_TABLE` pair; both are deleted, and this is where 1.12 keeps
-- the one that remains.
MATERIAL_TEXT_COLOR_TABLE = {
["Default"] = {0.18, 0.12, 0.06},
["Stone"] = {1.0, 1.0, 1.0},
["Parchment"] = {0.18, 0.12, 0.06},
["Marble"] = {0, 0, 0},
["Silver"] = {0.12, 0.12, 0.12},
["Bronze"] = {0.18, 0.12, 0.06},
}
MATERIAL_TITLETEXT_COLOR_TABLE = {
["Default"] = {0, 0, 0},
["Stone"] = {0.93, 0.82, 0},
["Parchment"] = {0, 0, 0},
["Marble"] = {0.93, 0.82, 0},
["Silver"] = {0.93, 0.82, 0},
["Bronze"] = {0.93, 0.82, 0},
}
]]></Script>
<!-- MasterFont: the shadow-only root the GameFont* families chain through (ref l.56). -->
+23 -12
View File
@@ -36,7 +36,7 @@
GuildRegistrar / PetitionFrame / TabardFrame, no charter or emblem path. Nothing in it
is reachable until you are IN a guild, which is what this file now covers.
· The row right-click menu IS the shared UnitPopup "FRIEND" menu, addressed by name
(see `BenillaFriendsFrame_ShowDropdown`). Its TARGET row stays hidden by UnitPopup's
(see `FriendsFrame_ShowDropdown`). Its TARGET row stays hidden by UnitPopup's
own deferred set; its GUILD_PROMOTE/GUILD_LEAVE rows are live but gate themselves on
`GuildFrame:IsVisible()`, so a friends-list or /who row still opens
Whisper / Invite / Cancel and a guild roster row opens more.
@@ -274,6 +274,16 @@
end
-- ── The row right-click menu (ref FriendsFrame_ShowDropdown l.35-47) ───────────────────
-- **The reference's own names, in the reference's own file, since 1751 window 6.** These
-- three carried a `Benilla` prefix for one reason: our ItemRef.xml declared a second
-- private copy of the same pair under the plain names, loaded 262 entries earlier, and
-- two definitions of one name is the collision this manifest's order exists to settle.
-- That window is the reference's own file now and its `SetItemRef` calls
-- `FriendsFrame_ShowDropdown` by name — so the duplicate is deleted, the one body answers
-- to the name its home file publishes in 1.12, and `FriendsDropDown` is seated where the
-- reference seats it (which retired a `frame_flag_gate` KNOWN entry whose whole reasoning
-- was that forward-parent problem).
-- A row's right-click opens the shared UnitPopup "FRIEND" menu addressed by NAME rather
-- than by a unit token — which is the whole reason that menu exists: a friend or a /who
-- hit is a name with no unit behind it (they need not even be in the world). The kit
@@ -282,22 +292,23 @@
--
-- `connected` gates the whole menu, ref-verbatim: there is nothing to offer an offline
-- player. A /who hit is by definition online, so that call passes 1.
function BenillaFriendsFrame_ShowDropdown(name, connected)
function FriendsFrame_ShowDropdown(name, connected)
HideDropDownMenu(1)
if not connected or not name or name == "" then
return
end
BenillaFriendsDropDown.initialize = BenillaFriendsFrameDropDown_Initialize
BenillaFriendsDropDown.displayMode = "MENU"
BenillaFriendsDropDown.name = name
ToggleDropDownMenu(1, nil, BenillaFriendsDropDown, "cursor")
FriendsDropDown.initialize = FriendsFrameDropDown_Initialize
FriendsDropDown.displayMode = "MENU"
FriendsDropDown.name = name
ToggleDropDownMenu(1, nil, FriendsDropDown, "cursor")
end
function BenillaFriendsFrameDropDown_Initialize()
function FriendsFrameDropDown_Initialize()
UnitPopup_ShowMenu(getglobal(UIDROPDOWNMENU_OPEN_MENU), "FRIEND", nil,
BenillaFriendsDropDown.name)
FriendsDropDown.name)
end
-- ── The window's repaint (ref FriendsFrame_Update l.81-126) ────────────────────────────
-- Each tab swaps the four corner art textures as well as the sub-frame: the ref reuses one
-- 384x512 window shell for lists that want different bottom art.
@@ -820,7 +831,7 @@
local guildIndex = this.guildIndex
local name, rank, rankIndex, level, class, zone, note, officernote, online =
GetGuildRosterInfo(guildIndex)
BenillaFriendsFrame_ShowDropdown(name, online)
FriendsFrame_ShowDropdown(name, online)
return
end
GuildFrame.previousSelectedGuildMember = GuildFrame.selectedGuildMember
@@ -1308,7 +1319,7 @@
function FriendsFrameFriendButton_OnClick(button)
if button == "RightButton" then
local name, _, _, _, connected = GetFriendInfo(this:GetID())
BenillaFriendsFrame_ShowDropdown(name, connected)
FriendsFrame_ShowDropdown(name, connected)
return
end
SetSelectedFriend(this:GetID())
@@ -1324,7 +1335,7 @@
local name = getglobal(this:GetName() .. "Name"):GetText()
if button == "RightButton" then
-- A /who hit is online by construction, so the menu is never gated shut here.
BenillaFriendsFrame_ShowDropdown(name, 1)
FriendsFrame_ShowDropdown(name, 1)
return
end
WhoFrame.selectedWho = this.whoIndex
@@ -2081,7 +2092,7 @@
<!-- The row right-click menu's host (ref `<Frame name="FriendsDropDown"
inherits="UIDropDownMenuTemplate"/>`): invisible until ToggleDropDownMenu opens
it at the cursor. -->
<Frame name="BenillaFriendsDropDown" inherits="UIDropDownMenuTemplate"/>
<Frame name="FriendsDropDown" inherits="UIDropDownMenuTemplate"/>
<!-- Close: the ref's own UIPanelCloseButton seat (TOPRIGHT -30,-8) on the
minimize-button art every close button in this house uses — no UIPanelCloseButton
-137
View File
@@ -1,137 +0,0 @@
<!--
The minimap's time-of-day indicator (GameTimeFrame) — our implementation of the behaviour the
1.12 FrameXML GameTime.xml + GameTime.lua specify, in their own file split (the reference TOC
loads GameTime.xml right after Minimap.xml, and the manifest mirrors that beside
MinimapCluster.xml). The Lua below is ours; the published names (GameTimeFrame_Update,
GameTime_GetTime, GAMETIME_DAWN/DUSK, GameTimeTexture) and every number are the reference's,
because addons call the first by name and the second are measurements of the real interface.
This closes the "day/night indicator" line in MinimapCluster.xml's OUT list.
The 50×50 frame overhanging the cluster's top-right corner shows
Interface\Minimap\UI-TOD-Indicator (128×64: the sun disc on the left half, the moon on the
right — verified by extraction); GameTimeFrame_Update re-reads GetGameTime() every OnUpdate,
and when the game minute changed it re-seats the 50-px texcoord window — +0.5 across the
dawn/dusk thresholds (before 5:30 AM / from 9:00 PM game time = night) — and refreshes the
tooltip if it is showing. Hover formats the same clock into the tooltip. GetGameTime() is the
app's SMSG_LOGIN_SETTIMESPEED clock, pushed on the minute (`crate::minimap::feed_game_time`).
The frame declares no enableMouse yet its tooltip hovers: the real XML `<Scripts>` walker
auto-enables the mouse kind when it attaches a mouse handler (wow-re
`ui/scratch/scripts-auto-enable.md`, resolved 2026-08-07) — benilla's loader models the same
law (`loader/scripts.rs`), so this file stays attribute-free like the reference.
Deltas from the reference's behaviour, each deliberate:
- No TEXT() wrapper around the format strings (this file set's convention: the ref's
build-time no-op localization macro; CharacterFrame.xml's note).
- TwentyFourHourTime = 1 is SET here: the reference sets it in Localization.lua's
LocalizeFrames() — this install is enGB, and enGB is a 24-hour locale — called at
VARIABLES_LOADED. benilla fires that event since decision 1128 and could carry this as a saved
variable (it is on 1128's follow-on list), but the value here is a LOCALE fact, not a player
toggle: this install is enGB and enGB is a 24-hour locale. So the tooltip reads "21:07" exactly
as the reference client on this install does; the 12-hour AM/PM path below is implemented and
kept live for the day a locale/CVar drives it.
-->
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Script><![CDATA[
-- Set here rather than by Localization.lua's LocalizeFrames — see the header.
TwentyFourHourTime = 1;
-- The reference's own thresholds, in minutes since midnight (game time), and its own
-- global names: an addon that wants to know when night falls reads exactly these.
GAMETIME_DAWN = ( 5 * 60) + 30; -- 5:30 AM
GAMETIME_DUSK = (21 * 60) + 0; -- 9:00 PM
-- UI-TOD-Indicator is one 128x64 sheet carrying two 50-px icons side by side: the sun at
-- u=0, the moon exactly half a sheet to its right. So the whole day/night decision is
-- which of the two u origins the 50-px window sits on.
local TOD_ICON_WIDTH = 50/128;
local TOD_ICON_HEIGHT = 50/64;
local TOD_NIGHT_ORIGIN = 0.5;
function GameTimeFrame_Update()
local hour, minute = GetGameTime();
local minuteOfDay = (hour * 60) + minute;
-- OnUpdate calls this every frame; only a changed game MINUTE is worth any work.
if minuteOfDay == this.timeOfDay then
return;
end
this.timeOfDay = minuteOfDay;
local origin = 0;
if minuteOfDay < GAMETIME_DAWN or minuteOfDay >= GAMETIME_DUSK then
origin = TOD_NIGHT_ORIGIN;
end
GameTimeTexture:SetTexCoord(origin, origin + TOD_ICON_WIDTH, 0, TOD_ICON_HEIGHT);
-- A tooltip we still own is showing last minute's clock; re-word it in place.
if GameTooltip:IsOwned(this) then
GameTimeFrame_UpdateTooltip(hour, minute);
end
end
-- The one clock formatter, since the tooltip and the getter below want the same string in
-- two different wrappers. TwentyFourHourTime picks the plain HH:MM form; otherwise the
-- hour folds into 1..12 (midnight and noon both being 12) and the AM/PM half is carried by
-- which format string is used, not by any text this file spells out.
local function clockText(hour, minute)
if TwentyFourHourTime then
return format(TIME_TWENTYFOURHOURS, hour, minute);
end
local afternoon = (hour >= 12);
if hour > 12 then
hour = hour - 12;
elseif hour == 0 then
hour = 12;
end
if afternoon then
return format(TIME_TWELVEHOURPM, hour, minute);
end
return format(TIME_TWELVEHOURAM, hour, minute);
end
function GameTimeFrame_UpdateTooltip(hours, minutes)
GameTooltip:SetText(clockText(hours, minutes));
end
function GameTime_GetTime()
return clockText(GetGameTime());
end
]]></Script>
<Frame name="GameTimeFrame" parent="MinimapCluster">
<Size>
<AbsDimension x="50" y="50"/>
</Size>
<Anchors>
<Anchor point="TOPRIGHT">
<Offset>
<AbsDimension x="4" y="-19"/>
</Offset>
</Anchor>
</Anchors>
<HitRectInsets>
<AbsInset left="6" right="0" top="5" bottom="10"/>
</HitRectInsets>
<Layers>
<Layer level="ARTWORK">
<Texture name="GameTimeTexture" file="Interface\Minimap\UI-TOD-Indicator"/>
</Layer>
</Layers>
<Scripts>
<OnLoad>
this.timeOfDay = 0;
this:SetFrameLevel(this:GetFrameLevel() + 2);
GameTimeFrame_Update();
</OnLoad>
<OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");
GameTimeFrame_UpdateTooltip(GetGameTime());
</OnEnter>
<OnLeave>
GameTooltip:Hide();
</OnLeave>
<OnUpdate>
GameTimeFrame_Update();
</OnUpdate>
</Scripts>
</Frame>
</Ui>
-134
View File
@@ -1,134 +0,0 @@
<Ui>
<!--
The chat-link tooltip (decision 0274 P4) - ref ItemRef.xml/ItemRef.lua, quoted: a
standalone GameTooltip instance parked above the bottom center that SetItemRef fills via
SetHyperlink when a chat item link is clicked, plus its corner close button. The plate,
the status bar and the OnHide come from GameTooltipTemplate — the same inheritance the ref
uses (ref ItemRef.xml l.4) — declared in GameTooltip.xml, which the manifest loads first.
Departures, each named:
- No coin row (ref: the template's $parentMoneyFrame): our money row is the coin trio the
GameTooltip instance declares, and its OnTooltipAddMoney/OnTooltipCleared pair stays
there with it, so a chat-link tooltip renders no price line. The reasoning is on
GameTooltipTemplate.
- No drag (ref: movable + RegisterForDrag + StartMoving/StopMovingOrSizing +
ValidateFramePosition): benilla has no user-moved-frame affordance yet - the window
sits on its declared anchor.
- SetItemRef's player-link branch lands here (ref l.3-46): left-click whispers,
right-click opens the name-only FRIEND dropdown (FriendsFrame_ShowDropdown, the
minimal FriendsFrame stand-in below), shift-click inserts into the chat editbox. The
ref's shift-click friends/ignore popup fills + SendWho("n-"..name) wait on the
friends/who arc (both guarded). The ITEM branch's Ctrl-DressUp landed with the dressing
room itself (decision 1060).
- No UIPanelWindows row (the ref has none either): ShowUIPanel's unregistered branch is
a plain Show, and SetHyperlink's own empty-gate shows/hides the plate.
-->
<Script><![CDATA[
-- ref ItemRef.lua SetItemRef (l.2-63): the chat-link click router. `link` is the payload
-- inside |H...|h ("item:2947:0:0:0" / "player:Name"), `text` the full escaped link,
-- `button` the mouse button.
function SetItemRef(link, text, button)
if strsub(link, 1, 6) == "player" then
local name = strsub(link, 8)
if name and strlen(name) > 0 then
-- ref l.6-7: collapse a space-separated name to its last token (a no-op for
-- the single-token names 1.12 uses).
name = gsub(name, "([^%s]*)%s+([^%s]*)%s+([^%s]*)", "%3")
name = gsub(name, "([^%s]*)%s+([^%s]*)", "%2")
if IsShiftKeyDown() then
-- ref fills a visible ADD_IGNORE/ADD_FRIEND/… popup editbox first (those
-- dialogs are the friends/ignore arc, deferred), else the chat editbox,
-- else SendWho (the /who arc, deferred) — we take the ref's editbox tail,
-- both branches guarded.
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(name)
elseif SendWho then
SendWho("n-"..name)
end
elseif button == "RightButton" then
FriendsFrame_ShowDropdown(name, 1)
else
ChatFrame_SendTell(name)
end
end
return
end
if IsControlKeyDown() then
-- ref l.50: ctrl-clicking an item link in chat previews it on your own character
-- (decision 1060). `text` is the FULL escaped link, not the bare payload —
-- DressUpItemLink pulls the id out of it itself.
DressUpItemLink(text)
return
end
if IsShiftKeyDown() then
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(text)
end
return
end
ShowUIPanel(ItemRefTooltip)
if not ItemRefTooltip:IsVisible() then
ItemRefTooltip:SetOwner(UIParent, "ANCHOR_PRESERVE")
end
ItemRefTooltip:SetHyperlink(link)
end
-- Minimal FriendsFrame stand-in for the chat-name right-click menu (ref FriendsFrame.lua
-- l.35-47). The full social window (friends/ignore/who tabs) is a later arc; this hosts
-- only the name-only FRIEND dropdown the chat links share with the unit popups.
function FriendsFrame_ShowDropdown(name, connected)
HideDropDownMenu(1)
if connected then
FriendsDropDown.initialize = FriendsFrameDropDown_Initialize
FriendsDropDown.displayMode = "MENU"
FriendsDropDown.name = name
ToggleDropDownMenu(1, nil, FriendsDropDown, "cursor")
end
end
function FriendsFrameDropDown_Initialize()
UnitPopup_ShowMenu(getglobal(UIDROPDOWNMENU_OPEN_MENU), "FRIEND", nil, FriendsDropDown.name)
end
]]></Script>
<!-- `toplevel` is the ref's (ItemRef.xml l.3) — a linked-item tooltip is a window that must
land above the HIGH-strata frames it is opened over (decision 1318). -->
<GameTooltip name="ItemRefTooltip" inherits="GameTooltipTemplate" enableMouse="true" parent="UIParent" frameStrata="HIGH" toplevel="true" hidden="true">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="80"/></Offset></Anchor>
</Anchors>
<Frames>
<!-- ref ItemRefCloseButton (ItemRef.xml l.17-36, quoted): the corner minimize-style
close; OnLoad's SetPadding(16) keeps it off the text. -->
<Button name="ItemRefCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="1" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>HideUIPanel(ItemRefTooltip)</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
</Button>
</Frames>
<Scripts>
<!-- The template's own OnLoad, re-declared to add the padding (ref ItemRef.xml
l.38-42's shape, minus its drag arming — see the no-drag departure above); the
loader's later SetScript wins, so GameTooltip_OnLoad has to be called here too.
OnHide is the template's unchanged, so it is not re-declared — ref ItemRef.xml
declares none either. -->
<OnLoad>
GameTooltip_OnLoad(self)
self:SetPadding(16)
</OnLoad>
</Scripts>
</GameTooltip>
<!-- The chat-name dropdown host (ref FriendsFrame's FriendsDropDown): a bare UIDropDownMenu
the SetItemRef player branch toggles at the cursor. UIDropDownMenuTemplate + UnitPopup
both load before this file (the ui_script load order). -->
<Frame name="FriendsDropDown" parent="UIParent" inherits="UIDropDownMenuTemplate" hidden="true"/>
</Ui>
@@ -1,416 +0,0 @@
<Ui>
<!--
The item-text reader — letters, books, plaques (the mail arc's "permanent copy" lands
here). The Lua below is OURS; the reference ItemTextFrame.xml (354 l) + ItemTextFrame.lua
(104 l) are the SPECIFICATION it satisfies — frame/region names, the event flow, the
material art paths and the geometry/art are the reference's and are quoted as such.
Driven by the engine's item-text seam (benilla-ui/src/script/item_text.rs) +
crate::ui_item_text's feed/drain: the app fires the reference event flow
ITEM_TEXT_BEGIN → ITEM_TEXT_READY → ITEM_TEXT_CLOSED.
ADAPTATIONS (the MailFrame.xml precedents):
· The body scroll and the page are the REFERENCE's own structure since decisions
1337/1338 (B240): a real ScrollFrame over a 10x10 scroll child holding a real
SimpleHTML. Before those, the scroll was a plain Frame with an inert bar and the page a
FontString — so an HTML body drew as its own markup and a long one was cut off with
"...", both of which Goudy reported.
· The two material colour TABLES are the reference's data (its Fonts.xml), quoted
verbatim — the numbers ARE the look. They live here Benilla-scoped (the MoneyInput-shim
precedent) because our own Fonts.xml deliberately doesn't ship the reference's two
globals; the lookup around them is ours, under the reference's published name.
· GlobalStrings inlined: PREV "Prev", NEXT "Next", ITEM_TEXT_FROM "From," (the reference
string really is comma'd).
· ITEM_TEXT_TRANSLATION (the foreign-language progress bar) is implemented but never
fires — no translation mechanic on our letters; the StatusBar stays hidden.
· <HitRectInsets> quoted verbatim for fidelity but currently inert (the loader ignores
the element).
-->
<Script><![CDATA[
-- ── The material palette ─────────────────────────────────────────────────────────────
-- The six materials a page can be printed on, and the ink each one takes. The COLOURS are
-- the reference's own data (its Fonts.xml l.27-42) and are quoted verbatim: a different
-- number is a different look. Benilla-scoped names — see the header.
BENILLA_MATERIAL_TEXT_COLOR_TABLE = {
["Default"] = {0.18, 0.12, 0.06},
["Stone"] = {1.0, 1.0, 1.0},
["Parchment"] = {0.18, 0.12, 0.06},
["Marble"] = {0, 0, 0},
["Silver"] = {0.12, 0.12, 0.12},
["Bronze"] = {0.18, 0.12, 0.06}
}
BENILLA_MATERIAL_TITLETEXT_COLOR_TABLE = {
["Default"] = {0, 0, 0},
["Stone"] = {0.93, 0.82, 0},
["Parchment"] = {0, 0, 0},
["Marble"] = {0.93, 0.82, 0},
["Silver"] = {0.93, 0.82, 0},
["Bronze"] = {0.93, 0.82, 0}
}
-- The page ink and the title ink for a material, under the reference's published name
-- (its UIParent.lua l.1880-1888). Falls back to "Default" unless BOTH halves are known, so
-- an unrecognised material never gets one table's colour and the other's default.
function GetMaterialTextColors(material)
local textColor = BENILLA_MATERIAL_TEXT_COLOR_TABLE[material]
local titleColor = BENILLA_MATERIAL_TITLETEXT_COLOR_TABLE[material]
if not (textColor and titleColor) then
textColor = BENILLA_MATERIAL_TEXT_COLOR_TABLE["Default"]
titleColor = BENILLA_MATERIAL_TITLETEXT_COLOR_TABLE["Default"]
end
return textColor, titleColor
end
-- ── The reader's regions, by role ────────────────────────────────────────────────────
-- The four material corners are one SET: a page is either bare parchment (all four off)
-- or dressed in a material, in which case each corner wears that material's own art.
-- Naming the suffixes once is also what makes the art path a single expression.
local MATERIAL_CORNERS = { "TopLeft", "TopRight", "BotLeft", "BotRight" }
-- Everything a fresh ITEM_TEXT_BEGIN clears away. At that point only the title is known,
-- so only the title stays on screen — the page and all of its paging chrome go.
local PAGE_CHROME = {
"ItemTextScrollFrame", "ItemTextCurrentPage", "ItemTextStatusBar",
"ItemTextPrevPageButton", "ItemTextNextPageButton",
}
local function ShowOrHide(region, wanted)
if wanted then
region:Show()
else
region:Hide()
end
end
-- A page always HAS a material: the server may name none, and the frame's own art is
-- already parchment, so that is what "none" means.
local function PageMaterial()
return ItemTextGetMaterial() or "Parchment"
end
-- Dress the page's four corners in its material. Parchment IS the frame's own art, so
-- parchment leaves the corners off and lets the bare frame show through; anything else
-- lights all four and points each at its own slice of that material's art set.
local function DressPage(material)
if material == "Parchment" then
for _, corner in ipairs(MATERIAL_CORNERS) do
getglobal("ItemTextMaterial" .. corner):Hide()
end
return
end
local art = "Interface\\ItemTextFrame\\ItemText-" .. material .. "-"
for _, corner in ipairs(MATERIAL_CORNERS) do
local layer = getglobal("ItemTextMaterial" .. corner)
layer:Show()
layer:SetTexture(art .. corner)
end
end
-- Put the reader on screen. The panel manager is allowed to refuse (something
-- native-center is holding the screen) — and then we are not reading after all, so the
-- server has to be told, or the session stays open behind a window that never appeared.
-- The reference makes the same check, on both of the paths that open this window.
local function OpenReader(frame)
ShowUIPanel(frame)
if not frame:IsVisible() then
CloseItemText()
end
end
-- ── The event flow ───────────────────────────────────────────────────────────────────
-- One function per event of the reference's flow: ItemTextFrame_OnLoad registers FLOW in
-- order and ItemTextFrame_OnEvent dispatches into Handlers by name. (The reference states
-- the same pairing twice — four RegisterEvent calls, then a four-arm `if event == …`
-- chain — which is how a fifth event gets added to one of them and not the other.)
local FLOW = {
"ITEM_TEXT_BEGIN", "ITEM_TEXT_TRANSLATION", "ITEM_TEXT_READY", "ITEM_TEXT_CLOSED",
}
local Handlers = {}
-- A text has been opened. Only its title is known yet, so the page is cleared away and
-- the ink is set for the material the pages are going to arrive on.
function Handlers.ITEM_TEXT_BEGIN(frame)
ItemTextTitleText:SetText(ItemTextGetItem())
for _, name in ipairs(PAGE_CHROME) do
getglobal(name):Hide()
end
local ink = GetMaterialTextColors(PageMaterial())
ItemTextPageText:SetTextColor(ink[1], ink[2], ink[3])
end
-- A foreign-language page is being translated: the turn buttons stand down and the
-- progress bar takes over, ranged across the arg1 seconds the server quoted (OnUpdate
-- fills it). Never fires on our letters — see the header.
function Handlers.ITEM_TEXT_TRANSLATION(frame)
ItemTextPrevPageButton:Hide()
ItemTextNextPageButton:Hide()
frame.translationElapsed = 0
ItemTextStatusBar:SetMinMaxValues(0, arg1)
ItemTextStatusBar:Show()
OpenReader(frame)
end
-- A page has arrived. A letter signs off with its sender (ITEM_TEXT_FROM — the reference
-- string really is comma'd); anything else just gets its trailing blank line. The paging
-- row appears only when there is another page to be on, and then each button shows only
-- if there is something in its direction.
function Handlers.ITEM_TEXT_READY(frame)
local creator = ItemTextGetCreator()
local body = "\n" .. ItemTextGetText()
if creator then
body = body .. "\n\n" .. "From," .. "\n" .. creator .. "\n\n"
else
body = body .. "\n"
end
ItemTextPageText:SetText(body)
ItemTextScrollFrame:Show()
-- A page is content the reader has never seen: re-measure what there is to scroll and
-- start at the top, or page two opens halfway down page one.
ItemTextScrollFrame:UpdateScrollChildRect()
ItemTextScrollFrameScrollBar:SetValue(0)
local pageNumber = ItemTextGetPage()
local hasNext = ItemTextHasNextPage()
DressPage(PageMaterial())
if pageNumber > 1 or hasNext then
ItemTextCurrentPage:SetText(pageNumber)
ItemTextCurrentPage:Show()
ShowOrHide(ItemTextPrevPageButton, pageNumber > 1)
ShowOrHide(ItemTextNextPageButton, hasNext)
end
ItemTextStatusBar:Hide()
OpenReader(frame)
end
function Handlers.ITEM_TEXT_CLOSED(frame)
HideUIPanel(frame)
end
function ItemTextFrame_OnLoad()
for _, event in ipairs(FLOW) do
this:RegisterEvent(event)
end
end
function ItemTextFrame_OnEvent(event)
local handler = Handlers[event]
if handler then
handler(this)
end
end
-- The translation bar's clock: while the bar is up it fills with the seconds spent so far.
function ItemTextFrame_OnUpdate(elapsed)
if not ItemTextStatusBar:IsVisible() then
return
end
local spent = this.translationElapsed + elapsed
ItemTextStatusBar:SetValue(spent)
this.translationElapsed = spent
end
-- Both page buttons are one gesture in two directions: the paging click, then the
-- engine's own page request. `turnPage` is ItemTextPrevPage or ItemTextNextPage.
function ItemTextFrame_TurnPage(turnPage)
PlaySound("igMainMenuOptionCheckBoxOn")
turnPage()
end
]]></Script>
<Frame name="ItemTextFrame" toplevel="true" movable="true" enableMouse="true" hidden="true" parent="UIParent">
<Size><AbsDimension x="384" y="512"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-104"/></Offset></Anchor></Anchors>
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="70"/></HitRectInsets>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\Spellbook\Spellbook-Icon">
<Size><AbsDimension x="58" y="58"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-8"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="BORDER">
<Texture file="Interface\ItemTextFrame\UI-ItemText-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
<Texture file="Interface\Spellbook\UI-SpellbookPanel-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="256" y="0"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\ItemTextFrame\UI-ItemText-BotLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-256"/></Offset></Anchor></Anchors>
</Texture>
<Texture file="Interface\Spellbook\UI-SpellbookPanel-BotRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="256" y="-256"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The non-parchment material corners (stone/marble/…): textures swapped in by
ITEM_TEXT_READY; hidden entirely for parchment (ref l.56-70). -->
<Texture name="ItemTextMaterialTopLeft" hidden="true">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="21" y="-75"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="ItemTextMaterialTopRight" hidden="true">
<Size><AbsDimension x="64" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="ItemTextMaterialTopLeft" relativePoint="TOPRIGHT"/></Anchors>
</Texture>
<Texture name="ItemTextMaterialBotLeft" hidden="true">
<Size><AbsDimension x="256" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="ItemTextMaterialTopLeft" relativePoint="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="ItemTextMaterialBotRight" hidden="true">
<Size><AbsDimension x="64" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="ItemTextMaterialTopLeft" relativePoint="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="ItemTextTitleText" inherits="GameFontNormal">
<Size><AbsDimension x="224" y="14"/></Size>
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="6" y="230"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="ItemTextCurrentPage" inherits="GameFontNormal">
<Size><AbsDimension x="192" y="0"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="10" y="-50"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- ref ItemTextScrollFrame l.145-219 — the REAL one now (decisions 1337/1338): an
engine ScrollFrame that pans and clips its scroll child, with the ref's own
flanking art quoted verbatim as the bar's track. `scrollBarHideable` keeps the bar
off a page that fits, which is most of them. -->
<ScrollFrame name="ItemTextScrollFrame" inherits="UIPanelScrollFrameTemplate" hidden="true">
<Size><AbsDimension x="280" y="355"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-66" y="-76"/></Offset></Anchor></Anchors>
<Layers>
<!-- The ref declares ARTWORK before BACKGROUND here, and the order is
load-bearing: Middle anchors to Top by name, and anchors resolve at
SetPoint time — a target declared later falls back to the parent (and
the black track lands on the parchment). -->
<Layer level="ARTWORK">
<Texture name="ItemTextScrollFrameTop" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size><AbsDimension x="28" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="0" y="5"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.484375" top="0" bottom="1.0"/>
</Texture>
<Texture name="ItemTextScrollFrameBottom" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size><AbsDimension x="28" y="107"/></Size>
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors>
<TexCoords left="0.515625" right="1.0" top="0" bottom="0.4140625"/>
</Texture>
</Layer>
<Layer level="BACKGROUND">
<Texture name="ItemTextScrollFrameMiddle">
<Size><AbsDimension x="25" y="365"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="ItemTextScrollFrameTop"/></Anchors>
<Color r="0" g="0" b="0" a="0.8"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<!-- The template's own OnLoad still has to run — a handler declared on an
instance REPLACES the template's, it does not chain. -->
<OnLoad>
ScrollFrame_OnLoad()
this.scrollBarHideable = 1
</OnLoad>
</Scripts>
<ScrollChild>
<!-- ref ItemTextPageScrollChild l.198-217, 10x10 and deliberately so: the
scroll range is the child's whole SUBTREE box (decision 1338), so the page
inside it is what gives the reader something to scroll. -->
<Frame name="ItemTextPageScrollChild">
<Size><AbsDimension x="10" y="10"/></Size>
<Frames>
<!-- ref ItemTextPageText l.203-215. A SimpleHTML for real now (1337):
an HTML body renders as blocks, and a plain one as a single block
through the engine's raw-text fallback. Its height is its content,
so a long page SCROLLS where it used to be cut off with "...". -->
<SimpleHTML name="ItemTextPageText">
<Size><AbsDimension x="270" y="304"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
<FontString inherits="ItemTextFontNormal"/>
</SimpleHTML>
</Frames>
</Frame>
</ScrollChild>
</ScrollFrame>
<!-- ref ItemTextStatusBar l.220-251 — the ITEM_TEXT_TRANSLATION progress bar; never
fires on our letters, kept for fidelity. -->
<StatusBar name="ItemTextStatusBar" hidden="true">
<Size><AbsDimension x="250" y="21"/></Size>
<Anchors><Anchor point="BOTTOM" relativeTo="ItemTextFrame"><Offset><AbsDimension x="-5" y="128"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorder">
<Size><AbsDimension x="256" y="23"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<TexCoords left="0" right="1.0" top="0.0625" bottom="0.75"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>this:SetFrameLevel(this:GetFrameLevel()+1)</OnLoad>
</Scripts>
<BarTexture file="Interface\TargetingFrame\UI-TargetingFrame-BarFill"/>
<BarColor r="0.25" g="0.75" b="0.25"/>
</StatusBar>
<Button name="ItemTextPrevPageButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="90" y="-56"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString inherits="GameFontNormal" justifyH="LEFT" text="Prev"><!-- PREV -->
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"/></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick>ItemTextFrame_TurnPage(ItemTextPrevPage)</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Up"/>
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Down"/>
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="ItemTextNextPageButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-55" y="-56"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString inherits="GameFontNormal" justifyH="RIGHT" text="Next"><!-- NEXT -->
<Anchors><Anchor point="RIGHT" relativePoint="LEFT"/></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick>ItemTextFrame_TurnPage(ItemTextNextPage)</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Up"/>
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Down"/>
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="ItemTextCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-45" y="-26"/></Offset></Anchor></Anchors>
<Scripts>
<OnClick>HideUIPanel(ItemTextFrame)</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
</Button>
</Frames>
<Scripts>
<OnLoad>ItemTextFrame_OnLoad()</OnLoad>
<OnEvent>ItemTextFrame_OnEvent(event)</OnEvent>
<OnUpdate>ItemTextFrame_OnUpdate(arg1)</OnUpdate>
<OnShow>PlaySound("igMainMenuOpen")</OnShow>
<OnHide>
CloseItemText()
PlaySound("igMainMenuClose")
</OnHide>
</Scripts>
</Frame>
</Ui>
@@ -1,232 +0,0 @@
<Ui>
<!--
The mirror timers - our MirrorTimer1/2/3. The extracted reference
(Interface\FrameXML\MirrorTimer.xml/.lua, mpqx; decision 0874) is the SPECIFICATION here,
not the source: it fixes the frame and function names addons bind to, the geometry, the
textures and the four colours, and this file is our own implementation of that contract.
"Mirror" is the client's word for a timer whose authority is the SERVER: the client never
computes breath or fatigue. The server ships a value + a signed rate and the frame
integrates `value + scale * elapsed` between packets - that OnUpdate line IS the whole of
the client-side motion. crate::ui_mirror fires the three events off the wire:
MIRROR_TIMER_START (name, remaining_ms, duration_ms, scale, paused, label) /
MIRROR_TIMER_PAUSE (name, paused) / MIRROR_TIMER_STOP (name).
arg1 is always the timer's NAME - "EXHAUSTION" / "BREATH" / "FEIGNDEATH" - the same key
MirrorTimerColors is indexed by, and the same key the client builds `<NAME>_LABEL` from to
look up the bar's caption ("Fatigue" / "Breath"; there is no FEIGNDEATH_LABEL in the 1.12
GlobalStrings, so that one shows an empty caption, exactly as the reference does).
Where our behaviour departs from the reference's, all three deliberate:
1. THE START DRIVER. The reference registers MIRROR_TIMER_START on UIParent itself and
calls MirrorTimer_Show from UIParent_OnEvent (ref UIParent.lua l.97 + l.374-375). Our
UIParent.xml is script-only - it has no frame of its own to register events on - so the
hidden MirrorTimerDriver below hosts that one registration and makes the identical call.
2. ONE WALK OVER THE POOL, NOT TWO. The reference asks its two questions - "is this timer
already on a bar?" then "is any bar idle?" - in two separate loops, and bounds the
second by STATICPOPUP_NUMDIALOGS, a copy-paste from StaticPopup.lua that would index a
nil MirrorTimer4 the day the two constants diverge. A bar already carrying the timer
wins over an idle one wherever either sits, so one pass answers both questions and
cannot run off the end of the pool.
3. THE PAUSE ARG. The reference's MIRROR_TIMER_PAUSE branch reads the same arg1 as both a
timer name (`arg1 ~= this.timer`, above) and a number (`arg1 > 0`), so a real pause
packet errors out in the reference UI - which is why vmangos refuses to send one and
substitutes a full START instead (Player::SendMirrorTimers says so in the source). We
read the flag from arg2, which is what both branches plainly intend.
POSITION: TOP, y=-96 - the reference's own anchor, under the top-center of the screen, with
timers 2 and 3 stacking downward from it.
-->
<Script><![CDATA[
MIRRORTIMER_NUMTIMERS = 3
-- The bar colour per timer type, keyed by the name that arrives on the wire. DEATH is in
-- the published set even though nothing in 1.12 ever raises a timer of that type - the
-- table is the contract, so it carries the whole set and not just the three we see.
MirrorTimerColors = {
EXHAUSTION = { r = 1.00, g = 0.90, b = 0.00 },
BREATH = { r = 0.00, g = 0.50, b = 1.00 },
DEATH = { r = 1.00, g = 0.70, b = 0.00 },
FEIGNDEATH = { r = 1.00, g = 0.70, b = 0.00 },
}
-- Which bar `timer` belongs on. A bar already carrying it wins wherever it sits - the
-- server re-sends a whole START on every change, and a re-STATE must land back in place
-- instead of eating another bar; failing that the first idle bar takes it, and if all
-- three are busy the timer simply goes unshown (see departure 2).
local function claim_bar(timer)
local idle
for index = 1, MIRRORTIMER_NUMTIMERS do
local bar = getglobal("MirrorTimer" .. index)
if bar:IsVisible() then
if bar.timer == timer then
return bar
end
elseif not idle then
idle = bar
end
end
return idle
end
-- Hand a bar back to the pool: off screen, holding nothing.
local function release_bar(bar)
bar:Hide()
bar.timer = nil
end
-- Raise or re-seat one timer. Milliseconds on the wire, seconds on the bar; `scale` is
-- the server's signed rate, which OnUpdate integrates from here.
function MirrorTimer_Show(timer, value, maxvalue, scale, paused, label)
local bar = claim_bar(timer)
if not bar then
return nil
end
bar.timer = timer
bar.value = value / 1000
bar.scale = scale
bar.paused = paused > 0 and 1 or nil
local name = bar:GetName()
getglobal(name .. "Text"):SetText(label)
local fill = getglobal(name .. "StatusBar")
local color = MirrorTimerColors[timer]
fill:SetMinMaxValues(0, maxvalue / 1000)
fill:SetValue(bar.value)
fill:SetStatusBarColor(color.r, color.g, color.b)
bar:Show()
return bar
end
function MirrorTimerFrame_OnLoad()
-- The two events that address a timer already running...
this:RegisterEvent("MIRROR_TIMER_PAUSE")
this:RegisterEvent("MIRROR_TIMER_STOP")
-- ...and the one that ends whatever this bar was holding.
this:RegisterEvent("PLAYER_ENTERING_WORLD")
this.timer = nil
end
function MirrorTimerFrame_OnEvent()
if event == "PLAYER_ENTERING_WORLD" then
-- A worldport ends every timer the old world was running; a stale breath bar must
-- not survive the loading screen.
release_bar(this)
return
end
-- Everything below names one running timer. Anything else is another bar's business.
if not this:IsShown() or arg1 ~= this.timer then
return
end
if event == "MIRROR_TIMER_PAUSE" then
-- The flag rides in arg2 (departure 3), and absent means running.
this.paused = (arg2 and arg2 > 0) and 1 or nil
elseif event == "MIRROR_TIMER_STOP" then
release_bar(this)
end
end
-- The whole of the client-side motion: between packets the bar walks itself along the
-- rate the server named. Nothing here decides anything - drowning is the server's call.
function MirrorTimerFrame_OnUpdate(frame, elapsed)
if not frame.paused then
frame.value = frame.value + frame.scale * elapsed
getglobal(frame:GetName() .. "StatusBar"):SetValue(frame.value)
end
end
-- The ref's UIParent_OnEvent MIRROR_TIMER_START arm, rehosted (see departure 1).
function MirrorTimerDriver_OnLoad()
this:RegisterEvent("MIRROR_TIMER_START")
end
function MirrorTimerDriver_OnEvent()
MirrorTimer_Show(arg1, arg2, arg3, arg4, arg5, arg6)
end
]]></Script>
<Frame name="MirrorTimerTemplate" toplevel="true" parent="UIParent" hidden="true" virtual="true">
<Size><AbsDimension x="206" y="26"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture>
<Size><AbsDimension x="195" y="13"/></Size>
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentText" inherits="GameFontHighlight">
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="0"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="$parentBorder" file="Interface\CastingBar\UI-CastingBar-Border">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="25"/></Offset>
</Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<StatusBar name="$parentStatusBar">
<Size><AbsDimension x="195" y="13"/></Size>
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>this:SetFrameLevel(this:GetFrameLevel() - 1)</OnLoad>
</Scripts>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
</StatusBar>
</Frames>
<Scripts>
<OnLoad>MirrorTimerFrame_OnLoad()</OnLoad>
<OnEvent>MirrorTimerFrame_OnEvent()</OnEvent>
<OnUpdate>MirrorTimerFrame_OnUpdate(this, arg1)</OnUpdate>
</Scripts>
</Frame>
<Frame name="MirrorTimer1" inherits="MirrorTimerTemplate">
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="-96"/></Offset>
</Anchor>
</Anchors>
</Frame>
<Frame name="MirrorTimer2" inherits="MirrorTimerTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="MirrorTimer1" relativePoint="BOTTOM"/>
</Anchors>
</Frame>
<Frame name="MirrorTimer3" inherits="MirrorTimerTemplate">
<Anchors>
<Anchor point="TOP" relativeTo="MirrorTimer2" relativePoint="BOTTOM"/>
</Anchors>
</Frame>
<!-- The START driver (departure 1): no art, no size - one event registration. -->
<Frame name="MirrorTimerDriver" parent="UIParent">
<Scripts>
<OnLoad>MirrorTimerDriver_OnLoad()</OnLoad>
<OnEvent>MirrorTimerDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>
+1 -1
View File
@@ -796,7 +796,7 @@
</Layer>
</Layers>
<Frames>
<!-- The row menu's host, the FriendsFrame's own pattern (`BenillaFriendsDropDown`). The
<!-- The row menu's host, the FriendsFrame's own pattern (`FriendsDropDown`). The
reference reuses `FriendsDropDown` for this; ours is its own frame so the raid
menu's userData cannot collide with a friends-row menu left half-open. -->
<Frame name="BenillaRaidDropDown" inherits="UIDropDownMenuTemplate"/>
+110
View File
@@ -32,6 +32,8 @@
<Frame name="UIParent" setAllPoints="true" frameStrata="MEDIUM">
<Scripts>
<OnLoad>
this:RegisterEvent("VARIABLES_LOADED")
this:RegisterEvent("MIRROR_TIMER_START")
this:RegisterEvent("PLAYER_ENTERING_WORLD")
this:RegisterEvent("READY_CHECK")
</OnLoad>
@@ -205,6 +207,32 @@
-- ~25 per-window harnesses that load this file load no bars; the binding itself is engine
-- level and always installed. Decision 1500.
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
-- that sets `TwentyFourHourTime` and the locale's frame adjustments; nothing called it
-- here before, which is why GameTime.xml used to set that global itself and why
-- HelpFrame.xml's header records a family of strings that never arrived. Guarded like
-- every other cross-file reach in this function: the per-window harnesses load
-- UIParent.xml with no chain behind it. `UpdateNameplates` is the ref's other call in
-- this arm and is not bound here — left absent rather than stubbed (1203).
if ( event == "VARIABLES_LOADED" ) then
if ( LocalizeFrames ) then
LocalizeFrames()
end
return
end
-- The mirror bars' START driver (ref UIParent.lua l.97 + l.374-377). It lived on a
-- hidden `MirrorTimerDriver` frame in our own MirrorTimer.xml until 1751 window 7,
-- on the reasoning that "our UIParent.xml is script-only — it has no frame of its own
-- to register events on". That stopped being true when this file grew a real UIParent
-- frame with a Scripts block, so the driver came home and the extra frame is gone.
-- `MirrorTimer_Show` is the sourced `Interface\FrameXML\MirrorTimer.lua`'s.
if ( event == "MIRROR_TIMER_START" ) then
if ( MirrorTimer_Show ) then
MirrorTimer_Show(arg1, arg2, arg3, arg4, arg5, arg6)
end
return
end
if ( event == "PLAYER_ENTERING_WORLD" ) then
SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3,
SHOW_MULTI_ACTIONBAR_4 = GetActionBarToggles()
@@ -292,6 +320,88 @@
table.insert(UIPARENT_MANAGED_POSITION_LISTENERS, fn)
end
-- ── GetMaterialTextColors (ref UIParent.lua l.1880-1888) ─────────────────────────────
--
-- The page ink and the title ink for a readable's material. Transcribed here rather than
-- left absent because the sourced `Interface\FrameXML\ItemTextFrame.xml` (1751 window 9)
-- calls it, and it belongs in this file because it belongs in `UIParent.lua`. The two
-- tables it reads are the reference's own, in the reference's own home — our Fonts.xml,
-- which grew them with the same window.
--
-- The fallback is BOTH-or-neither on purpose (the reference's own `not(a and b)`), so an
-- unrecognised material never gets one table's colour and the other's default.
function GetMaterialTextColors(material)
local textColor = MATERIAL_TEXT_COLOR_TABLE[material]
local titleColor = MATERIAL_TITLETEXT_COLOR_TABLE[material]
if ( not (textColor and titleColor) ) then
textColor = MATERIAL_TEXT_COLOR_TABLE["Default"]
titleColor = MATERIAL_TITLETEXT_COLOR_TABLE["Default"]
end
return textColor, titleColor
end
-- ── ValidateFramePosition (ref UIParent.lua l.1513-1557) ─────────────────────────────
--
-- Shove a frame the player has dragged mostly off the screen back on. Transcribed here
-- rather than left absent because its one caller in 1.12 is the sourced
-- `Interface\FrameXML\ItemRef.xml`'s OnDragStop (l.48) — so with that window migrated
-- (1751, window 6) a player dragging a chat-link tooltip would otherwise hit a nil global,
-- which is a raise on an ordinary gesture rather than a loud missing feature. It belongs
-- in this file because it belongs in `UIParent.lua`, which this file is our copy of.
--
-- Verbatim, quirks included. Two are worth naming so nobody "fixes" them later:
-- · the two axes are exclusive, not additive — a frame off BOTH corners takes the
-- `not newAnchorX` branch and re-derives x from `left`, so only one axis moves per
-- drop. That is the reference's own control flow (l.1544-1548), not a transcription
-- slip.
-- · the bottom test is against `MainMenuBar:GetHeight()`, so this raises with no action
-- bar loaded. The reference has the same dependency; the harnesses that drive
-- ItemRefTooltip load ActionBar.xml for it.
function ValidateFramePosition(frame, offscreenPadding, returnOffscreen)
if ( not frame ) then
return
end
local left = frame:GetLeft()
local right = frame:GetRight()
local top = frame:GetTop()
local bottom = frame:GetBottom()
local newAnchorX, newAnchorY
if ( not offscreenPadding ) then
offscreenPadding = 15
end
if ( top < (0 + MainMenuBar:GetHeight() + offscreenPadding) ) then
-- Off the bottom of the screen
newAnchorY = MainMenuBar:GetHeight() + frame:GetHeight() - GetScreenHeight()
elseif ( bottom > GetScreenHeight() ) then
-- Off the top of the screen
newAnchorY = 0
end
if ( right < 0 ) then
-- Off the left of the screen
newAnchorX = 0
elseif ( left > GetScreenWidth() ) then
-- Off the right of the screen
newAnchorX = GetScreenWidth() - frame:GetWidth()
end
if ( newAnchorX or newAnchorY ) then
if ( returnOffscreen ) then
return 1
else
if ( not newAnchorX ) then
newAnchorX = left
elseif ( not newAnchorY ) then
newAnchorY = top - GetScreenHeight()
end
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", nil, "TOPLEFT", newAnchorX, newAnchorY)
end
else
if ( returnOffscreen ) then
return nil
end
end
end
function UIParent_ManageFramePositions()
-- Frames that affect offsets in y axis
local yOffsetFrames = {}
+23 -4
View File
@@ -972,9 +972,15 @@
-- the StaticPopupDialogs registry + StaticPopup_Show(which, arg1, arg2) + the OnUpdate
-- countdown/StartDelay machinery — the death arc's dialogs exercise nearly all of it, and
-- StaticPopup_Show/StaticPopupDialogs are 1.14 addon-API surface (0068). v1 scope:
-- · TWO dialog instances (STATICPOPUP_NUMDIALOGS = 2; the ref runs 4) — the deepest
-- nesting exercised so far is decline-res re-showing DEATH (Show precedes the click's
-- hide, so a free second instance is required). Grow toward 4 when a third coexists.
-- · FOUR dialog instances, the reference's own count. It was 2 for a long time, on the
-- reasoning that the deepest nesting exercised was decline-res re-showing DEATH (Show
-- precedes the click's hide, so a free second instance is required) and that it should
-- "grow toward 4 when a third coexists". 1751 window 7 supplied that third caller from
-- an unexpected direction: the reference's own `MirrorTimer_Show` bounds its free-bar
-- search by this constant rather than by `MIRRORTIMER_NUMTIMERS` (MirrorTimer.lua
-- l.32, a copy-paste from StaticPopup.lua), so at 2 the third mirror bar was
-- unreachable for a new timer. The coupling is the reference's bug and this is not the
-- file to fix it in; matching its count is.
-- · info fields honored: text, button1, button2, DisplayButton2, OnShow(data),
-- OnHide(data), OnAccept(data, data2) (truthy return = keep open — RECOVER_CORPSE
-- waits for the server, XP_LOSS's first Accept re-asks), OnCancel(data, reason)
@@ -1007,7 +1013,7 @@
-- · Resize runs per tick while shown, not only on text swaps: GetHeight's measure has a
-- frame of latency (the measured-width convention below), so the height self-corrects
-- the frame after any text change.
STATICPOPUP_NUMDIALOGS = 2
STATICPOPUP_NUMDIALOGS = 4
StaticPopupDialogs = {}
local function StaticPopupTimeUnit(n, minutes)
@@ -2448,6 +2454,19 @@
<Frame name="StaticPopup2" inherits="StaticPopupTemplate" parent="UIParent">
<Anchors><Anchor point="TOP" relativeTo="StaticPopup1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
</Frame>
<!-- The ref's four (StaticPopup.xml l.245-254), each stacked under the last. Instances 3 and 4
joined with 1751's seventh window and the reason is a cross-file coupling worth naming:
`STATICPOPUP_NUMDIALOGS` is not only this engine's pool size — the reference's own
`MirrorTimer_Show` bounds its free-bar search by it (MirrorTimer.lua l.32, a copy-paste
from StaticPopup.lua). At 2 against MIRRORTIMER_NUMTIMERS = 3 the third mirror bar could
never be claimed by a new timer, silently. The v1 note above said "grow toward 4 when a
third coexists"; the reference's own body is that third caller. -->
<Frame name="StaticPopup3" inherits="StaticPopupTemplate" parent="UIParent">
<Anchors><Anchor point="TOP" relativeTo="StaticPopup2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="StaticPopup4" inherits="StaticPopupTemplate" parent="UIParent">
<Anchors><Anchor point="TOP" relativeTo="StaticPopup3" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
</Frame>
<!-- The delete-item confirm driver (decision 0216 §3): DELETE_ITEM_CONFIRM names no frame of
its own (the ref handles it in UIParent.lua's OnEvent), so this is a dedicated hidden
@@ -130,6 +130,15 @@
</Button>
<Script><![CDATA[
-- **`MAX_COMBO_POINTS` belongs to the TARGET frame in 1.12** — `TargetFrame.lua:1`, the
-- very first line of the file this one is our counterpart to — and this is where it lives
-- now (1751 window 10). It was declared in our ComboFrame.xml, whose own note said "it
-- lives in the ref's TargetFrame.lua; we have no such file", and that was the honest place
-- for it while both files were ours. The combo window is the reference's own file now and
-- reads this global without defining it, so it had to come home; `CombatText.xml`'s
-- five-point flash reads it too, and this file loads above both.
MAX_COMBO_POINTS = 5
-- The right-click-menu explanations the detailed-tooltip fork shows in place of the unit
-- lines (BenillaUnitFrame_OnEnter below) — ref GlobalStrings.lua l.2993/3081/2754/2755
-- (enUS), fallback form: the real localized GlobalStrings.lua runs before this file at
-425
View File
@@ -1,425 +0,0 @@
<!--
ZoneText.xml — the zone-entry splash (decision 0287): ZoneTextFrame (the THICK-outline zone
name — nominal FontHeight 102, DRAWN at the 32px one-to-one raster cap like the real client
(wow-re fontstring-overflow.md), which is why it fits its 512-wide box on one line — + the PvP
territory line) and SubZoneTextFrame (the 26px subzone line + the FFA "PvP Area" line), faded
by the FadingFrame kit when the app fires the ZONE_CHANGED family off the player's area
(`area.rs`).
The Lua below is OURS: our own implementation of the behaviour ref-ZoneText.xml specifies, as
the live 5875 patch chain ships that file (patch.MPQ overrides the interface.MPQ copy — decision
0246). What is quoted from it, deliberately, is the published surface and the measurements — the
global names addons call (SetZoneText, the three Zone*Duration constants, every frame and
fontstring name), the event names, the colours, and the frame geometry — because those are facts
about how the real interface looks and behaves rather than choices of ours. All THREE of the
reference file's frames are here, the third being AutoFollowStatus (the /follow status text, and
the only thing ref-ZoneText.lua actually contains), which waited here until follow support
existed and no longer does. The FadingFrame kit (ref-FadingFrame.lua, its own TOC line right
before ZoneText.xml) is implemented below too — our own code behind that file's published API,
since ZoneTextFrame/SubZoneTextFrame are its only consumers in the whole 1.12 FrameXML
(grep-verified over the extraction), so it rides with them. AutoFollowStatus is NOT one of them —
it rolls its own fade, which is why the kit stays a two-frame affair.
The subtle law worth naming (it looks like a bug and is not): SubZoneTextString — a child of
SubZoneTextFrame — anchors to regions of the *other* frame (ZoneTextString / PVPInfoTextString),
which is usually hidden when only the subzone changes. The resolver computes hidden frames'
rects all the same, so a subzone-only splash seats where it would sit *under the invisible zone
name* — the authentic lower position. ZoneTextFrame's OnEvent caches GetZoneText() silently on
plain ZONE_CHANGED and splashes only when an INDOORS/NEW_AREA event finds the zone text changed;
the territory line is asked for on NEW_AREA alone.
-->
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script><![CDATA[
-- ref-GlobalStrings.lua values this file formats (the CastingBar.xml guarded idiom):
CONTESTED_TERRITORY = CONTESTED_TERRITORY or "Contested Territory";
FACTION_CONTROLLED_TERRITORY = FACTION_CONTROLLED_TERRITORY or "%s Territory"; -- Faction name
FREE_FOR_ALL_TERRITORY = FREE_FOR_ALL_TERRITORY or "PvP Area";
AUTOFOLLOWSTART = AUTOFOLLOWSTART or "Following %s.";
AUTOFOLLOWSTOP = AUTOFOLLOWSTOP or "You stop following %s.";
-- The ref's localization identity (UIParent.lua), which the AutoFollowStatus body below
-- puts its message through as the reference does. Guarded like everything else here rather
-- than leaning on UnitPopup.xml happening to load first — a one-line helper is no place to
-- acquire a load-order edge.
if ( not TEXT ) then
TEXT = function(s) return s end
end
-- ── The FadingFrame kit ────────────────────────────────────────────────────────────────
-- Our implementation of the helper ref-FadingFrame.lua specifies. The global names and the
-- `this.*` fields ARE that file's published API — addons and the frames below reach for them
-- by name — so those are fixed; the code behind them is ours.
--
-- The contract: FadingFrame_Show() starts one timeline on the frame — ramp alpha up over
-- fadeInTime, hold at full for holdTime, ramp back down over fadeOutTime, then hide. A leg
-- configured to zero is simply skipped, and a frame that has never been configured (all
-- three zero, per OnLoad) hides on its very first update.
function FadingFrame_SetFadeInTime(this, time)
this.fadeInTime = time;
end
function FadingFrame_SetHoldTime(this, time)
this.holdTime = time;
end
function FadingFrame_SetFadeOutTime(this, time)
this.fadeOutTime = time;
end
function FadingFrame_OnLoad()
-- Through the setters, so the field names are written down in exactly one place.
FadingFrame_SetFadeInTime(this, 0);
FadingFrame_SetHoldTime(this, 0);
FadingFrame_SetFadeOutTime(this, 0);
this:Hide();
end
function FadingFrame_Show()
this.startTime = GetTime();
this:Show();
end
function FadingFrame_OnUpdate()
-- One timeline, one alpha: which leg `elapsed` has reached decides the value, and
-- running off the end of the last leg is the only way the frame stops.
local elapsed = GetTime() - this.startTime;
local fadeIn = this.fadeInTime;
local holdEnds = fadeIn + this.holdTime;
local alpha;
if ( elapsed < fadeIn ) then
alpha = elapsed / fadeIn;
elseif ( elapsed < holdEnds ) then
alpha = 1.0;
elseif ( elapsed < (holdEnds + this.fadeOutTime) ) then
alpha = 1.0 - ((elapsed - holdEnds) / this.fadeOutTime);
else
this:Hide();
return;
end
this:SetAlpha(alpha);
end
-- ── The zone splash ───────────────────────────────────────────────────────────────────
-- Our implementation of what ref-ZoneText.xml's own <Script> block and its two splash
-- frames' handlers specify. The names below are that file's published API — addons re-render
-- the splash through SetZoneText and retune it by writing the three durations — so the
-- names and the numbers are fixed; the code between them is ours.
ZoneFadeInDuration = 0.5;
ZoneHoldDuration = 1.0;
ZoneFadeOutDuration = 2.0;
-- How a HELD zone is tinted, keyed by who holds it. Three cases differing only in three
-- floats want to be data — the same shape MinimapCluster.xml keeps for the minimap's zone
-- label, which reads the same GetZonePVPInfo; duplicated there and here rather than
-- published as a global the reference does not have. A pvpType that is not in the table is
-- unheld (sanctuary, or a type we do not know), and gets no territory line and the zone
-- font's own parchment colour back — ZoneTextFont's <Color> in Fonts.xml is this triple.
local ZONE_PVP_TINT = {
friendly = { r = 0.1, g = 1.0, b = 0.1 },
hostile = { r = 1.0, g = 0.1, b = 0.1 },
contested = { r = 1.0, g = 0.7, b = 0 },
};
local UNHELD_ZONE_TINT = { r = 1.0, g = 0.9294, b = 0.7607 };
-- The line under the zone name saying who holds the place. The localisation globals are
-- read here, at call time, rather than folded into the table above: something later in the
-- load order is allowed to replace them.
local function ZoneTerritoryLine(pvpType, factionName)
if ( pvpType == "contested" ) then
return CONTESTED_TERRITORY;
end
return format(FACTION_CONTROLLED_TERRITORY, factionName);
end
-- Re-render the whole splash from the host's idea of where the player is: the zone name,
-- its tint, the territory line under it, the free-for-all call-out, and where the subzone
-- line hangs. `showPVPInfo` is the caller's answer to "is this a NEW zone?" — a hop between
-- subzones of one zone re-renders without it and gets the same tint with no territory line.
function SetZoneText(showPVPInfo)
ZoneTextString:SetText(GetZoneText());
local pvpType, factionName, isArena = GetZonePVPInfo();
local held = ZONE_PVP_TINT[pvpType];
local tint = held or UNHELD_ZONE_TINT;
ZoneTextString:SetTextColor(tint.r, tint.g, tint.b);
SubZoneTextString:SetTextColor(tint.r, tint.g, tint.b);
if ( held ) then
-- The territory line wears the tint too — including when the branch below finds it
-- has nothing to say. Nothing on screen can tell (a blank string has no colour to
-- show), but an addon that reads the tint off the blank line reads this zone's.
-- Guarded, because the unheld tint is the zone FONT's parchment and this line, a
-- SubZoneTextFont string, is never given it.
PVPInfoTextString:SetTextColor(tint.r, tint.g, tint.b);
end
-- The territory line and the subzone line's seat are one decision, not two: the line
-- is there only when somebody holds this zone AND the caller asked for it, and the
-- subzone line drops to hang off the line exactly when there is one to hang off.
if ( held and showPVPInfo ) then
PVPInfoTextString:SetText(ZoneTerritoryLine(pvpType, factionName));
SubZoneTextString:SetPoint("TOP", "PVPInfoTextString", "BOTTOM", 0, 0);
else
PVPInfoTextString:SetText("");
SubZoneTextString:SetPoint("TOP", "ZoneTextString", "BOTTOM", 0, 0);
end
-- A free-for-all pit is called out under the subzone line and drags that line to the
-- hostile red whatever the zone around it is — inside the pit, everyone is hostile.
if ( isArena ) then
local ffa = ZONE_PVP_TINT.hostile;
PVPArenaTextString:SetText(FREE_FOR_ALL_TERRITORY);
SubZoneTextString:SetTextColor(ffa.r, ffa.g, ffa.b);
else
PVPArenaTextString:SetText("");
end
end
-- Both splash frames are the same kind of thing — one fade timeline, and all three of the
-- events the client splits an area change into (a move within the current zone, an interior
-- threshold crossed, a genuinely new zone) — so the arming is written once and neither can
-- drift from the other. Each decides for itself, in its OnEvent, whether the line it owns
-- actually changed.
function ZoneSplashFrame_OnLoad()
FadingFrame_OnLoad();
FadingFrame_SetFadeInTime(this, ZoneFadeInDuration);
FadingFrame_SetHoldTime(this, ZoneHoldDuration);
FadingFrame_SetFadeOutTime(this, ZoneFadeOutDuration);
this:RegisterEvent("ZONE_CHANGED");
this:RegisterEvent("ZONE_CHANGED_INDOORS");
this:RegisterEvent("ZONE_CHANGED_NEW_AREA");
end
-- The big zone name. A plain ZONE_CHANGED never splashes it: that event fires whenever the
-- player crosses a subzone boundary inside one zone, so all it may do is keep the cached
-- name honest. That silent cache write IS the subtle law in the header — it is what makes a
-- later INDOORS/NEW_AREA that lands on the same name find nothing left to say.
function ZoneTextFrame_OnEvent(event)
local zoneText = GetZoneText();
local changed = zoneText ~= this.zoneText;
this.zoneText = zoneText;
if ( event == "ZONE_CHANGED" or not changed ) then
return;
end
-- Only a genuinely new zone earns the territory line under the name.
SetZoneText(event == "ZONE_CHANGED_NEW_AREA");
FadingFrame_Show();
end
function SubZoneTextFrame_OnLoad()
ZoneSplashFrame_OnLoad();
-- The free-for-all call-out is red for good, so it is tinted once here rather than on
-- every splash.
local ffa = ZONE_PVP_TINT.hostile;
PVPArenaTextString:SetTextColor(ffa.r, ffa.g, ffa.b);
-- Prime both lines from wherever the player already is: this runs at load, and the next
-- area event may not arrive until they walk somewhere.
SetZoneText(true);
SubZoneTextString:SetText(GetSubZoneText());
end
-- The small subzone line. Any of the three events can splash it, and the text it is already
-- showing IS its cache — it has no silent path to keep a separate one honest.
function SubZoneTextFrame_OnEvent()
local subZoneText = GetSubZoneText();
if ( subZoneText == SubZoneTextString:GetText() ) then
return;
end
SubZoneTextString:SetText(subZoneText);
-- Re-render the zone half too, territory line and all. Registered second, this frame is
-- dispatched last of the two (FIFO — wow-re `event-dispatch-order.md`), so on a new zone
-- it is this call that leaves the territory line standing under the name.
SetZoneText(true);
FadingFrame_Show();
end
-- ── The auto-follow status line ────────────────────────────────────────────────────────
-- Our implementation of what ref-ZoneText.lua specifies — the only thing that .lua actually
-- contains, and the reason it exists at all. Note it does NOT use the FadingFrame kit
-- above: it rolls a plain linear alpha fade with no fade-in and no hold, armed only by
-- AUTOFOLLOW_END. BEGIN is a full reset (fade cancelled, alpha restored, shown), which is
-- why a follow that switches subject needs no END first. The 4s is the reference's.
AUTOFOLLOW_STATUS_FADETIME = 4.0;
function AutoFollowStatus_OnLoad()
this:RegisterEvent("AUTOFOLLOW_BEGIN");
this:RegisterEvent("AUTOFOLLOW_END");
this:RegisterEvent("PLAYER_ENTERING_WORLD");
end
function AutoFollowStatus_OnEvent(event)
if ( event == "PLAYER_ENTERING_WORLD" ) then
-- A world load clears the line outright: nothing to say, and nothing to fade.
this:Hide();
return;
end
-- Both follow events land the same way — put up a line naming this.unit and show it —
-- so all they differ in is the message and what they do to the fade.
local message;
if ( event == "AUTOFOLLOW_BEGIN" ) then
this.unit = arg1;
this.fadeTime = nil;
this:SetAlpha(1.0);
message = AUTOFOLLOWSTART;
elseif ( event == "AUTOFOLLOW_END" ) then
this.fadeTime = AUTOFOLLOW_STATUS_FADETIME;
message = AUTOFOLLOWSTOP;
else
return;
end
AutoFollowStatusText:SetText(format(TEXT(message), this.unit));
this:Show();
end
function AutoFollowStatus_OnUpdate(elapsed)
-- Only AUTOFOLLOW_END arms the countdown; until then the line just sits at full alpha.
local remaining = this.fadeTime;
if ( not remaining ) then
return;
end
-- Out of time: hide, and leave fadeTime alone. It is stale from here until the next
-- BEGIN clears it or the next END re-arms it, and a hidden frame gets no OnUpdate.
if ( elapsed >= remaining ) then
this:Hide();
return;
end
remaining = remaining - elapsed;
this.fadeTime = remaining;
this:SetAlpha(remaining / AUTOFOLLOW_STATUS_FADETIME);
end
]]></Script>
<!-- The two splash frames. Same box, same seat on screen, same fade: one carries the zone name
and the territory line under it, the other the subzone line and the free-for-all call-out.
Geometry is the reference's measurement, not a choice of ours — the 512-wide strings inside
a 128 box anchored BOTTOM +512 are what the real splash occupies.
(ref carries toplevel="true" on both; still dropped here, but the reason has changed.
`SetToplevel` is no longer a gap — the raise law is built, `script::object::toplevel` —
and its real trigger is Show, not a click, so "click-raise is meaningless on mouse-less
splash text" was the wrong argument. What holds now: these are LOW-strata splash frames
that overlap nothing in LOW, so the occlusion gate declines. One word to re-add.) -->
<Frame name="ZoneTextFrame" toplevel="true" parent="UIParent" frameStrata="LOW">
<Anchors>
<Anchor point="BOTTOM">
<Offset>
<AbsDimension x="0" y="512"/>
</Offset>
</Anchor>
</Anchors>
<Size>
<AbsDimension x="128" y="128"/>
</Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="ZoneTextString" inherits="ZoneTextFont">
<Size>
<AbsDimension x="512" y="0"/>
</Size>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</FontString>
<FontString name="PVPInfoTextString" inherits="SubZoneTextFont">
<Size>
<AbsDimension x="512" y="0"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="ZoneTextString" relativePoint="BOTTOM"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
ZoneSplashFrame_OnLoad();
</OnLoad>
<OnEvent>
ZoneTextFrame_OnEvent(event);
</OnEvent>
<OnUpdate>
FadingFrame_OnUpdate();
</OnUpdate>
</Scripts>
</Frame>
<Frame name="SubZoneTextFrame" toplevel="true" parent="UIParent" frameStrata="LOW">
<Anchors>
<Anchor point="BOTTOM">
<Offset>
<AbsDimension x="0" y="512"/>
</Offset>
</Anchor>
</Anchors>
<Size>
<AbsDimension x="128" y="128"/>
</Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="SubZoneTextString" inherits="SubZoneTextFont">
<Size>
<AbsDimension x="512" y="0"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="ZoneTextString" relativePoint="BOTTOM"/>
</Anchors>
</FontString>
<FontString name="PVPArenaTextString" inherits="SubZoneTextFont">
<Size>
<AbsDimension x="512" y="0"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="SubZoneTextString" relativePoint="BOTTOM"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
SubZoneTextFrame_OnLoad();
</OnLoad>
<OnEvent>
SubZoneTextFrame_OnEvent();
</OnEvent>
<OnUpdate>
FadingFrame_OnUpdate();
</OnUpdate>
</Scripts>
</Frame>
<!-- The auto-follow status line — the third frame ref-ZoneText.xml declares, laid out as that
file specifies it (the handlers behind it, like every other line of Lua here, are ours).
It is the only thing ref-ZoneText.lua actually contains, and the reason that .lua exists at
all; it waited here until follow support did (decisions 0890, 0893). `setAllPoints` over
UIParent, so the line sits dead centre of the screen. BACKGROUND strata: it is under
everything, which is why a full-screen panel hides it.
ONE DEVIATION, forced: the ref anchors the string CENTER to **WorldFrame**, and benilla has
no WorldFrame — it is the reference's 3D-scene layer frame, which our renderer does not
model as a frame at all. This is the ONLY `relativeTo="WorldFrame"` in the whole 1.12
FrameXML (grep-verified over the extraction), so it buys no convention; left verbatim it
merely logged an unresolved-anchor WARN every boot and fell back to the owner. UIParent is
the right substitute and not an approximation: both are full-screen in the reference, so
their CENTERs coincide, and AutoFollowStatus is setAllPoints over UIParent anyway. -->
<Frame name="AutoFollowStatus" parent="UIParent" setAllPoints="true" frameStrata="BACKGROUND" hidden="true">
<Layers>
<Layer level="ARTWORK">
<FontString name="AutoFollowStatusText" inherits="GameFontNormal">
<Anchors>
<Anchor point="CENTER" relativeTo="UIParent">
<Offset>
<AbsDimension x="0" y="0"/>
</Offset>
</Anchor>
</Anchors>
<FontHeight>
<AbsValue val="20"/>
</FontHeight>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
AutoFollowStatus_OnLoad();
</OnLoad>
<OnEvent>
AutoFollowStatus_OnEvent(event);
</OnEvent>
<OnUpdate>
AutoFollowStatus_OnUpdate(arg1);
</OnUpdate>
</Scripts>
</Frame>
</Ui>
+155 -32
View File
@@ -39,6 +39,22 @@
# The named virtual Font objects (decision 0084) — must load first so any FontString's
# `inherits="GameFontNormal"` resolves against a registered object.
Fonts.xml
# ══ THE LOCALE ADJUSTMENTS — the reference's own, off the player's chain (1751, window 5) ══
# `Localize()` (which this file also CALLS at load, exactly as the reference's own two-part
# `<Script>` does) and `LocalizeFrames()`. Nothing of ours ever shipped either, and the gap was
# recorded in two places: `GameTime.xml` set `TwentyFourHourTime = 1` itself with a note saying the
# reference sets it in `LocalizeFrames`, and `HelpFrame.xml` names a whole family of strings that
# "`LocalizeFrames()`, which benilla does not ship, so it never fired here either" would have set.
#
# It is the reference's own slot (FrameXML.toc l.3, straight after Fonts.xml) and the order is the
# point: `Localize` overwrites `GlobalStrings.lua` keys, and GlobalStrings is loaded ahead of this
# whole manifest at VM setup (`ui_script/mod.rs`) — which is FrameXML.toc l.1, so the two land in
# the reference's own order. The file it patches is the PLAYER'S, so the locale is theirs by
# construction rather than ours to guess.
#
# `LocalizeFrames()` is called from `UIParent_OnEvent`'s VARIABLES_LOADED arm — the reference's own
# caller (UIParent.lua l.230-235), added to our UIParent.xml with this window.
Interface\FrameXML\Localization.xml
# ── A reference file sourced ABOVE ours, so OUR bodies win every collision ─────────────────────
# This one is not a migrated window: it is here for one family of frame-agnostic globals that
# addons call by name, while the window that file also drives is still ours. So it loads first and
@@ -112,22 +128,52 @@ UIDropDownMenu.xml
# — after the dropdown kit it drives, before the unit/party frames whose DropDown
# children's OnLoad initialize into it.
UnitPopup.xml
# The chat-link click router (SetItemRef): item links → ItemRefTooltip:SetHyperlink; a
# player name → left-click whisper / right-click FRIEND dropdown. After GameTooltip.xml —
# ItemRefTooltip inherits=GameTooltipTemplate, so this order is now REQUIRED, not just legible
# (GameTooltip_OnLoad was the old, runtime-only reason) — AND after the dropdown kit +
# UnitPopup, since its FriendsDropDown inherits UIDropDownMenuTemplate and opens a UnitPopup
# FRIEND menu.
ItemRef.xml
# ══ THE CHAT-LINK TOOLTIP — the reference's own, off the player's chain (1751, window 6) ══
# `SetItemRef` (the chat-link click router) and `ItemRefTooltip` / `ItemRefCloseButton`.
#
# THE POSITION IS THE REQUIREMENT: `inherits="GameTooltipTemplate"` is resolved at LOAD, so this
# sits below GameTooltip.xml. The dropdown kit and UnitPopup above it are a call-time need only,
# but the reference's own TOC has the same shape and the order costs nothing.
#
# THREE THINGS OUR COPY CARRIED THAT THE REFERENCE'S DOES NOT, each moved to the reference's own
# home rather than deleted:
# · `FriendsFrame_ShowDropdown` + its initializer, and the `FriendsDropDown` frame they toggle.
# 1.12 keeps all three in `FriendsFrame.lua`/`.xml`; ours had a second private copy here because
# nothing else called them. FriendsFrame.xml (far below) owns the one body now and publishes the
# reference's name over it — resolved at call time, so the order does not bind.
# · `ValidateFramePosition`, which the sourced `ItemRef.xml`'s OnDragStop calls (l.48). It lives in
# `UIParent.lua` in 1.12, so it is transcribed into our UIParent.xml with this window.
# · Two divergences in `SetItemRef` itself, both now the reference's own body: the shift-click arm
# fills a visible ADD_IGNORE/ADD_FRIEND/ADD_GUILDMEMBER/ADD_RAIDMEMBER popup's editbox before it
# considers the chat box, and falls through to `SendWho("n-"..name)`. Ours guarded both away as
# "the friends/who arc, deferred"; `StaticPopup_Visible` and `SendWho` both exist, so the guards
# were outliving their reason.
#
# AND ONE THING IT GAINS: the reference's tooltip is `movable` and drag-armed, so a chat-link
# tooltip can be dragged off the middle of the screen. Ours declared "benilla has no
# user-moved-frame affordance yet" — `StartMoving`/`StopMovingOrSizing` are bindings
# (`script/object/movable.rs`), so that note was stale too.
Interface\FrameXML\ItemRef.xml
# The shared status-bar numerals machinery (decision 1082): TextStatusBar_* + the opt-in
# template — before its customers (the XP bar in ActionBar.xml today; the unit-frame
# health/mana numerals are the ref's other users, when that slice lands). The ref's own TOC
# loads TextStatusBar right before its unit frames, same seat as here.
TextStatusBar.xml
UnitFrames.xml
# The combo-point dots (decision 0869) — anchored to TargetFrame, so after
# UnitFrames.xml; its fade chain needs UiPanels.xml's UIFrameFade above.
ComboFrame.xml
# ══ THE COMBO-POINT DOTS — the reference's own, off the player's chain (1751, window 10) ══
# `ComboFrame`, `ComboPoint1..5`, `ComboPointTemplate` and the `ComboPointsFrame_OnEvent` /
# `ComboPointShineFade*` chain with its five duration constants, out of `ComboFrame.lua`.
#
# THE POSITION IS THE REQUIREMENT: `relativeTo="TargetFrame"` is resolved at LOAD, so this sits
# below UnitFrames.xml. Its fade chain needs UiPanels.xml's `UIFrameFade` family, far above, at
# call time.
#
# Our copy's frames were `BenillaComboPointTemplate` and kin; the reference's are
# `ComboPointTemplate` / `ComboPoint<N>Highlight` / `ComboPoint<N>Shine`, which is what an addon
# skinning the dots reaches for. `MAX_COMBO_POINTS` moved to UnitFrames.xml with this window —
# 1.12 declares it on `TargetFrame.lua:1`, and our copy's own note said so while explaining that
# it had nowhere else to go.
Interface\FrameXML\ComboFrame.xml
# The center-screen scrolling combat text (decision 0578) — the Blizzard_CombatText
# transcription; consumes COMBAT_TEXT_UPDATE + the regen/health/power events, needs
# only Fonts.xml (CombatTextFont) + UIParent before it.
@@ -206,20 +252,51 @@ MicroMenu.xml
# <Minimap> widget hole (`crate::minimap` fills it). After GameTooltip (its zone-text
# hover uses the shared tooltip).
MinimapCluster.xml
# The minimap's time-of-day indicator (GameTimeFrame): the reference's own file split — its
# TOC loads GameTime.xml right after Minimap.xml. After MinimapCluster (its parent) and
# GameTooltip (the hover tooltip); driven by the app's game clock (`minimap::feed_game_time`).
GameTime.xml
# ══ THE TIME-OF-DAY INDICATOR — the reference's own, off the player's chain (1751, window 5) ══
# `GameTimeFrame`, `GameTimeTexture`, and `GameTime_GetTime` / `GameTimeFrame_Update` /
# `_UpdateTooltip` / `GAMETIME_DAWN`/`_DUSK` out of `GameTime.lua`. Ours was a re-implementation of
# the same behaviour under the same published names, and it is deleted.
#
# THE POSITION IS THE REQUIREMENT: `parent="MinimapCluster"` is resolved at LOAD, so this sits
# below MinimapCluster.xml — which is also the reference's own order (its TOC loads GameTime.xml
# right after Minimap.xml). GameTooltip above it for the hover; the clock itself is the app's
# `GetGameTime` (`minimap::feed_game_time`), resolved at call time.
#
# BOTH of our recorded divergences went away rather than being carried (1751 §2). The `TEXT()`
# wrapper the ref puts round its format strings is real here now — `BasicControls.xml` defines it.
# And `TwentyFourHourTime = 1`, which our file set itself with a note saying the reference sets it
# in `LocalizeFrames`, now comes from exactly there: `Interface\FrameXML\Localization.xml` is
# sourced at the top of this manifest and `UIParent_OnEvent`'s VARIABLES_LOADED arm calls it. The
# value is the PLAYER's locale rather than our assumption about it.
Interface\FrameXML\GameTime.xml
# Print screen's UI half (decision 1487): the centred "Screen Captured"/"Screen Capture Failed"
# line, and the `TakeScreenshot()` the SCREENSHOT binding runs. Needs Fonts.xml (SystemFont) and
# UIParent.xml (its seat) — both far above — and nothing else; seated beside ZoneText.xml, whose
# AutoFollowStatus is the same full-screen-frame-with-a-centred-line shape.
ScreenshotStatus.xml
# The zone-entry splash (decision 0287): ZoneTextFrame/SubZoneTextFrame + the inlined
# FadingFrame kit, driven by the ZONE_CHANGED family `crate::area` fires. Needs only
# Fonts (ZoneTextFont/SubZoneTextFont) + UIParent; kept by the minimap cluster — the
# other consumer of the zone-text host globals.
ZoneText.xml
# ══ THE ZONE SPLASH — the reference's own, off the player's chain (1751, window 8) ══
# Two files, in the reference's own order and for its own reason.
#
# `FadingFrame.lua`'s kit first (`FadingFrame_OnLoad`/`_Show`/`_OnUpdate` and the three
# `_Set*Time` setters). Our copy inlined it into ZoneText.xml on the argument that
# ZoneTextFrame/SubZoneTextFrame are its only consumers in the whole 1.12 FrameXML — true, and
# still the wrong shape once the file could simply be sourced: an addon that wants a fading frame
# reaches for the kit's published names, not for the splash. The reference gives it its own TOC
# line (l.25) and so do we. It declares no frames at all — it is a `<Script file=>` and nothing
# else — so it needs nothing above it.
Interface\FrameXML\FadingFrame.xml
# …then the splash itself: `ZoneTextFrame` / `SubZoneTextFrame` / `AutoFollowStatus`, `SetZoneText`
# and the three `Zone*Duration` constants, driven by the ZONE_CHANGED family `crate::area` fires
# and by AUTOFOLLOW_BEGIN/END. Needs Fonts (ZoneTextFont/SubZoneTextFont) + UIParent, both far
# above; kept by the minimap cluster, the other consumer of the zone-text host globals.
#
# Our copy's own header called its Lua "OURS: our own implementation of the behaviour
# ref-ZoneText.xml specifies", with the published names and the measurements quoted. The
# reference's file is that specification, so it replaces it whole — including the
# `CONTESTED_TERRITORY`/`FACTION_CONTROLLED_TERRITORY`/`FREE_FOR_ALL_TERRITORY`/`AUTOFOLLOW*`
# strings, which came from ours as `X = X or "…"` fallbacks and now come from the player's own
# GlobalStrings.lua like every other string in this interface.
Interface\FrameXML\ZoneText.xml
# The faux-scroll kit (decisions 0247/0250/0251): BenillaScrollBarTemplate (the draggable
# Slider scroll bar) + FauxScrollFrameTemplate + the reference's own FauxScrollFrame_*
# API (Update/GetOffset/SetOffset/OnVerticalScroll + ScrollFrame_OnLoad) —
@@ -265,10 +342,29 @@ WorldMapFrame.xml
# The cast bar (decision 0137 phase 1) — the extracted 1.12 CastingBarFrame, driven by
# the SPELLCAST_* events `crate::ui_cast` fires. Only needs Fonts before it.
CastingBar.xml
# The mirror timers (decision 0874): the breath/fatigue bars at top-center, driven by the
# MIRROR_TIMER_* events `crate::ui_mirror` fires. Needs only Fonts (GameFontHighlight)
# before it; kept beside the cast bar, whose border art it shares.
MirrorTimer.xml
# ══ THE MIRROR TIMERS — the reference's own, off the player's chain (1751, window 7) ══
# `MirrorTimer1..3`, `MIRRORTIMER_NUMTIMERS`, `MirrorTimerColors` and the `MirrorTimer_Show` /
# `MirrorTimerFrame_*` family out of `MirrorTimer.lua`. The breath/fatigue bars at top-centre,
# driven by the `MIRROR_TIMER_*` events `crate::ui_mirror` fires off the wire. Needs only Fonts
# (GameFontHighlight) before it; kept beside the cast bar, whose border art it shares.
#
# ALL THREE of our copy's recorded divergences resolved at the swap (1751 §2), and two of them had
# already stopped being true:
# · THE START DRIVER. Ours hosted `MIRROR_TIMER_START` on a hidden `MirrorTimerDriver` frame
# because "our UIParent.xml is script-only — it has no frame of its own to register events on".
# It has one now, so the registration and the `MirrorTimer_Show` call moved into
# `UIParent_OnEvent` where the reference keeps them (UIParent.lua l.97 + l.374-377), and the
# extra frame is gone.
# · THE POOL WALK. The reference asks its two questions in two loops and bounds the SECOND by
# `STATICPOPUP_NUMDIALOGS` — a copy-paste from StaticPopup.lua, and its own bug. Ours folded
# them into one walk to avoid it. Now faithful, which forced the real fix in the right place:
# our `STATICPOPUP_NUMDIALOGS` was 2 against the reference's 4, so the third mirror bar could
# never be claimed by a new timer. UiPanels.xml grew instances 3 and 4 with this window.
# · THE PAUSE ARG is now the reference's own bug, reproduced: `MirrorTimerFrame_OnEvent` reads
# `arg1` as a timer NAME and then as a number (`arg1 > 0`), so a real MIRROR_TIMER_PAUSE raises.
# 1.12 never reaches it — vmangos refuses to send that packet and substitutes a full START,
# saying so in `Player::SendMirrorTimers` — and `ui_mirror.rs`'s Pause edge carries the note.
Interface\FrameXML\MirrorTimer.xml
# The always-up world-state readout (decision 1586, report B190): the tower/flag/base counters an
# outdoor-PvP zone or a battleground shows at top-centre, driven by the UPDATE_WORLD_STATES event
# `crate::world_state_ui` fires. Needs Fonts (GameFontNormalSmall) and GameTooltip; sits beside the
@@ -278,10 +374,29 @@ WorldStateFrame.xml
# aura icons under the minimap, driven by the UnitAura feed (`crate::ui_aura`). Needs Fonts
# (its FontStrings) and ActionBar's `BENILLA_FALLBACK_ICON` global (both already loaded).
BuffFrame.xml
# The durability alert (the "armor guy"): under the minimap beside BuffFrame, driven by
# UPDATE_INVENTORY_ALERTS off the same inventory push the character window reads. Needs
# MinimapCluster (its anchor) — loaded above.
DurabilityFrame.xml
# ══ THE DURABILITY ALERT — the reference's own, off the player's chain (1751, window 4) ══
# The "armor guy" under the minimap: `DurabilityFrame`, its eleven glyph textures, and
# `DurabilityFrame_SetAlerts` / `_OnUpdate` with `INVENTORY_ALERT_STATUS_SLOTS` and
# `INVENTORY_ALERT_COLORS`, all from `DurabilityFrame.lua` through its own `<Script file=>`.
#
# Ours was a measurement-faithful transcription and it is deleted. The swap re-examined its one
# recorded divergence (1751 §2) and made it faithful: our copy OMITTED the ref's `OnUpdate`,
# because the whole body is an `enchantTimer` countdown and that timer is provably dead in 1.12
# (`SetAlerts` nils it; the only line that sets it is commented out, and so are the two colours it
# would have painted). The reference runs the handler anyway, so we do too — the cost is one call
# per frame on one frame, and it is the reference's own behaviour rather than our judgment of it.
#
# THE POSITION: `parent="UIParent"` and `relativeTo="MinimapCluster"` are both resolved at LOAD, so
# this sits below UIParent.xml and MinimapCluster.xml. It is far above the reference's own slot
# (FrameXML.toc l.86) and that is fine — nothing else reaches it at load, and its seat is decided
# at runtime regardless: like every managed HUD frame the authored anchor is dead on arrival, with
# `UIParent_ManageFramePositions` re-seating it on the loader's post-load pass and on every
# OnShow/OnHide (ref UIParent.lua l.1758-1768).
#
# `UpdateInventoryAlertStatus` is the one global the sourced file names that this client does not
# bind. It is reachable only from the dead `enchantTimer` branch above, so it is left absent rather
# than stubbed (1203) — the honest state is "nothing calls it", not "we answer it with a lie".
Interface\FrameXML\DurabilityFrame.xml
ErrorsFrame.xml
# ══ THE BAG WINDOWS — the reference's own, off the player's chain (decision 1751, window 1) ══
# `ContainerFrame1..12`, `ContainerFrameTemplate`, `ContainerFrameItemButtonTemplate` and the whole
@@ -440,11 +555,19 @@ MailFrame.xml
# MerchantFrame because it REUSES that file's global BenillaMoney_Set coin helpers (the two
# gold displays) — they must be defined first.
TradeFrame.xml
# The item-text reader: letters (mail-made permanent copies) and, later, books/plaques —
# the ItemTextGet* seam (`benilla-ui/src/script/item_text.rs`) over `crate::ui_item_text`'s
# feed/drain. Loaded right after MailFrame (the same arc; needs only Fonts/UiPanels, both
# loaded above).
ItemTextFrame.xml
# ══ THE ITEM-TEXT READER — the reference's own, off the player's chain (1751, window 9) ══
# `ItemTextFrame` and its page chrome, over the `ItemTextGet*` seam
# (`benilla-ui/src/script/item_text.rs`) and `crate::ui_item_text`'s feed/drain. Letters (mail-made
# permanent copies) and, later, books and plaques. Right after MailFrame, the same arc; needs
# Fonts/UiPanels/ScrollTemplates, all above.
#
# ONE THING OUR COPY CARRIED THAT THE REFERENCE'S DOES NOT, moved to the reference's own home:
# `GetMaterialTextColors` and the two colour tables it reads. 1.12 keeps the function in
# `UIParent.lua` and the tables in `Fonts.xml`, and both of ours now do the same — under the
# reference's published names (`MATERIAL_TEXT_COLOR_TABLE` / `MATERIAL_TITLETEXT_COLOR_TABLE`)
# rather than the `BENILLA_`-prefixed pair this file kept privately. Fonts.xml's own note said
# those two tables would arrive "the day something does" read them; the sourced file is that day.
Interface\FrameXML\ItemTextFrame.xml
# The trainer window (decision 0237 phase 3): the read-only class/profession trainer list.
# Loaded right after MerchantFrame per the director's brief (kept adjacent even though this
# v1 uses plain-text costs, not MerchantFrame's coin helpers) and before the other NPC
+1 -1
View File
@@ -112,7 +112,7 @@ pub(super) enum UiFixture {
/// swings it through the four alternate dock states (see `fixtures.rs`).
ChatTabHover,
/// The social pane (`FriendsFrame`), opened through the live toggle — the instrument for B264
/// (decision 1298). Its `BenillaFriendsDropDown` host is declared with no anchors, exactly as
/// (decision 1298). Its `FriendsDropDown` host is declared with no anchors, exactly as
/// the reference's own `FriendsDropDown` is; while an unpositioned owner stood in a zero rect
/// for its regions, the dropdown template's texture chain drew a stray capsule at the screen
/// origin every time the pane opened. Needs no server state to show it.
+7
View File
@@ -148,6 +148,13 @@ fn feed_mirror_timers(
ScriptValue::Str(caption(kind, spell_name(start.spell_id).as_deref())),
],
),
// **This edge reaches a reference body that raises**, and it is left that way on
// purpose (1751 window 7). `MirrorTimerFrame_OnEvent` reads `arg1` as the timer name
// and then as a number (`arg1 > 0`), comparing a string to a number — 1.12's own bug.
// vmangos never sends the packet: it substitutes a full START and says so in
// `Player::SendMirrorTimers`, which is presumably how the bug survived. If a server
// ever does send one, the raise is the correct loud failure (1203) and the repair
// belongs in an adapter over the reference's body, not in a second vocabulary here.
MirrorTimerEdge::Pause { paused, .. } => (
"MIRROR_TIMER_PAUSE",
vec![name, ScriptValue::Int(i64::from(paused))],
+9 -13
View File
@@ -1792,19 +1792,15 @@ fn a_key_dropped_on_the_button_files_itself() {
"the reference's own dangling string name prints nothing — un-quirking it is a decision, \
not a default"
);
// The nil ALSO raises, out of `MessageFrame:AddMessage`. That is deliberately not asserted as
// correct: our binding takes its text as an mlua `String`, which raises on nil by default —
// not from a carved gate (`messageframe/plain.rs` reads `0x795590`'s rgb/alpha parse off the
// bytes and says nothing about the text argument). Whether 1.12 raises here, no-ops, or prints
// a blank line is out with wow-re. So this pins the keyring's own behaviour — the refusal, and
// the silence — and lets exactly that one error through by name, which still fails the test on
// any OTHER error the click produces.
let errs = s.errors();
assert!(
errs.iter()
.all(|e| e.contains("AddMessage") && e.contains("PutKeyInKeyRing")),
"the only error may be the reference's own nil message: {errs:?}"
);
// And it does not RAISE either, which is the half this test could not assert until the bytes
// came back. `AddMessage`'s text is fetched through `lua_isstring 0x6f3510` whose failure edge
// is a silent jump to the function's own epilogue — no line, no error (wow-5875-re
// `addmessage-text-gate-silent-skip.md`, 4-worker cross-check). Ours took an mlua `String` and
// raised, which put a script-error dialog on an ordinary player gesture; the fix is in
// `messageframe/mod.rs::message_text` and its own test carries the full law.
//
// So a full keyring is a DEAD CLICK on a real 1.12 client, and now here.
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The item-push drop animation (decision 0887): `ITEM_PUSH(container, icon)` runs the pushed item's
@@ -1,5 +1,5 @@
//! The combo-point dots (decisions 0869, 0875) — our `ComboFrame` implementation driven
//! through the real loader: the show/hide edges, the per-point highlight/shine fade chain, the
//! The combo-point dots (decisions 0869, 0875) — the reference's own `ComboFrame.xml`/`.lua`,
//! executed off the player's chain since 1751's tenth window, driven through the real loader: the show/hide edges, the per-point highlight/shine fade chain, the
//! "only newly-earned points flare" rule that `COMBO_FRAME_LAST_NUM_POINTS` exists to enforce, and
//! the two gates that live in `GetComboPoints` rather than in this Lua — rogue-or-druid only, and
//! the points must be banked on the CURRENT target.
@@ -10,22 +10,7 @@ use benilla_ui::script::{PlayerReqState, UiScript, UnitState};
const MOB_A: u64 = 0xF130_0000_0000_0001;
const MOB_B: u64 = 0xF130_0000_0000_0002;
/// Load one shipped `assets/ui/<file>` (the panel tests' loader), panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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}: loader errors: {:?}",
report.errors
);
}
use super::test_ui::load_ui as load_xml;
/// The manifest order this file actually ships in: `UiPanels.xml` for `UIFrameFade`,
/// `UnitFrames.xml` for the `TargetFrame` the dots anchor to.
@@ -40,7 +25,7 @@ fn load_combo_frame() -> UiScript {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "UnitFrames.xml");
load_xml(&s, "ComboFrame.xml");
load_xml(&s, "Interface\\FrameXML\\ComboFrame.xml");
s.resolve();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
@@ -83,6 +68,7 @@ fn highlight_alpha(s: &mut UiScript, i: u32) -> f64 {
/// completion first, since the highlight arrives via `UIFrameFade`, not a straight `SetAlpha`.
#[test]
fn combo_frame_follows_the_point_count() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combo_frame();
play_as(&mut s, 4, MOB_A); // rogue, mob A selected
@@ -135,6 +121,7 @@ fn combo_frame_follows_the_point_count() {
/// combo event, so re-targeting mid-window fires exactly this repaint.
#[test]
fn a_repaint_at_the_same_count_does_not_re_flare() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combo_frame();
play_as(&mut s, 4, MOB_A);
s.set_combo_points(2, MOB_A);
@@ -164,6 +151,7 @@ fn a_repaint_at_the_same_count_does_not_re_flare() {
/// the rogue ladder's first rung, so only the class differs.
#[test]
fn a_warriors_overpower_point_lights_no_dot() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combo_frame();
play_as(&mut s, 1, MOB_A); // warrior
s.set_combo_points(1, MOB_A);
@@ -184,6 +172,7 @@ fn a_warriors_overpower_point_lights_no_dot() {
/// target entirely (guid 0) empties them too.
#[test]
fn re_targeting_empties_the_dots_without_the_count_moving() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combo_frame();
play_as(&mut s, 4, MOB_A);
s.set_combo_points(3, MOB_A);
@@ -30,7 +30,7 @@ const JERKY_LINK: &str = "|cffffffff|Hitem:117|h[Tough Jerky]|h|r";
const ROOM_UI: &[&str] = &[
"UIDropDownMenu.xml",
"UnitPopup.xml",
"ItemRef.xml",
"Interface\\FrameXML\\ItemRef.xml",
"MerchantFrame.xml",
"StackSplit.xml",
"DressUpFrame.xml",
@@ -1,30 +1,16 @@
//! The durability alert ("armor guy", `assets/ui/DurabilityFrame.xml`) against the shipped XML:
//! The durability alert ("armor guy") against the reference's own `DurabilityFrame.xml`/`.lua`,
//! executed off the player's chain since 1751's fourth window:
//! the engine's `GetInventoryAlertStatus` statuses (recomputed on every inventory push, a change
//! firing `UPDATE_INVENTORY_ALERTS`) drive the ref's own SetAlerts law — body pieces show
//! together when any body region alerts, showSeparate pieces (Weapon/Shield/Ranged) each show
//! only themselves, the shield glyph swaps for the off-weapon glyph when the off hand holds a
//! WEAPON, and the whole frame hides at zero alerts.
use super::test_ui::load_ui as load_xml;
use benilla_ui::script::{
InvSlotView, InventorySlots, ItemTemplateView, QuadContent, ScriptValue, UiScript,
};
fn load_xml(s: &UiScript, file: &str) {
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}: loader errors: {:?}",
report.errors
);
}
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
@@ -35,10 +21,17 @@ fn harness() -> UiScript {
"UIParent.xml",
"GameTooltip.xml",
"MinimapCluster.xml",
"DurabilityFrame.xml",
// The reference's own file. This module carried a private disk-only `load_xml` until the
// swap, which structurally could not name a chain entry — [`super::test_ui::load_ui`] is
// the one reader that speaks both stores, and it is why that helper exists.
"Interface\\FrameXML\\DurabilityFrame.xml",
] {
load_xml(&s, f);
}
// The app installs this at the end of every real load; a test VM has to say so itself. It is
// benilla's stated repair of a reference bug — the seat going stale when a side glyph appears
// while the frame is already shown — and its whole reasoning is at the constant.
super::manifest::install_durability_reseat(&s).unwrap();
s
}
@@ -88,6 +81,7 @@ const YELLOW: [f32; 4] = [1.0, 0.82, 0.18, 1.0];
/// white), then repaired (hidden again).
#[test]
fn armor_guy_shows_red_broken_yellow_damaged_and_hides_clean() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
// The ref's initial settle: the frame is authored shown; PLAYER_ENTERING_WORLD's SetAlerts
// hides it while everything is clean.
@@ -166,6 +160,7 @@ fn armor_guy_shows_red_broken_yellow_damaged_and_hides_clean() {
/// 1.12 FrameXML never reads it.
#[test]
fn flag_bits_and_the_low_ammo_region() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
@@ -216,6 +211,7 @@ fn flag_bits_and_the_low_ammo_region() {
/// broken off-hand WEAPON (template class 2) swaps it for the off-weapon glyph.
#[test]
fn off_hand_glyph_follows_what_the_hand_holds() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_item_template(
2362,
@@ -269,6 +265,7 @@ fn off_hand_glyph_follows_what_the_hand_holds() {
/// the pass on every alert transition.
#[test]
fn manage_pass_seats_the_frame_inside_the_cluster_edge() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
@@ -313,6 +310,7 @@ fn manage_pass_seats_the_frame_inside_the_cluster_edge() {
/// (director-caught). The OnEvent re-manage keeps it fresh on every alert recompute.
#[test]
fn a_late_side_glyph_refreshes_the_seat_while_the_frame_stays_shown() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_item_template(
25,
@@ -383,6 +381,7 @@ fn a_late_side_glyph_refreshes_the_seat_while_the_frame_stays_shown() {
/// colliding there (director-caught, over "The Captain's Chest").
#[test]
fn the_quest_tracker_stacks_below_the_durability_guy() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for f in [
@@ -393,11 +392,12 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
"GameTooltip.xml",
"MinimapCluster.xml",
"ScrollTemplates.xml",
"DurabilityFrame.xml",
"Interface\\FrameXML\\DurabilityFrame.xml",
"QuestLogFrame.xml",
] {
load_xml(&s, f);
}
super::manifest::install_durability_reseat(&s).unwrap();
// A right-side glyph shows the 65-tall durability frame under the minimap.
let mut inv: InventorySlots = Default::default();
@@ -364,16 +364,6 @@ const KNOWN: &[Known] = &[
// question this flag exists to ask (decision 1757). A frame whose seat crosses the boundary
// between UIParent's tree and the top level is a defect, because `SetFullScreenFrame` hides
// `UIParent` and everything below it; a frame seated one rung along inside that tree is not.
Known {
frame: "FriendsDropDown",
flag: Flag::Parent,
why: "the reference nests this menu HOST in FriendsFrame; ours sits on UIParent in \
ItemRef.xml, beside the SetItemRef branch that is its only caller — and it has to, \
because FriendsFrame.xml loads 262 lines further down the manifest and a forward \
parent reference resolves to nothing. Both seats are inside UIParent's tree, and a \
host frame is never shown: ToggleDropDownMenu seats the visible DropDownList1, \
which is top-level on both sides.",
},
Known {
frame: "SendMailBodyEditBox",
flag: Flag::Parent,
@@ -658,8 +658,7 @@ fn right_clicking_a_who_row_opens_the_friend_menu() {
"the menu opens"
);
assert_eq!(
s.eval::<String>("return BenillaFriendsDropDown.name")
.unwrap(),
s.eval::<String>("return FriendsDropDown.name").unwrap(),
"Tigole",
"addressed by name, not by a unit token"
);
@@ -1108,8 +1108,7 @@ fn right_clicking_a_roster_row_offers_the_guild_rows() {
"the menu opens"
);
assert_eq!(
s.eval::<String>("return BenillaFriendsDropDown.name")
.unwrap(),
s.eval::<String>("return FriendsDropDown.name").unwrap(),
"Furor",
"addressed by name — a roster row has no unit token behind it"
);
@@ -1166,8 +1165,7 @@ fn the_guild_rows_stay_off_a_who_row_menu() {
// NOT our own name: the WHISPER row hides on yourself, and with INVITE gated the same way the
// menu would have nothing to show and never open — which would make the assertion below pass
// for the wrong reason.
s.run("BenillaFriendsFrame_ShowDropdown(\"Thrall\", 1)")
.unwrap();
s.run("FriendsFrame_ShowDropdown(\"Thrall\", 1)").unwrap();
assert!(s.eval::<bool>("return DropDownList1:IsVisible()").unwrap());
let present = r#"
for i = 1, UIDROPDOWNMENU_MAXBUTTONS do
@@ -66,9 +66,49 @@ fn bootstrap_positions(script: &UiScript) -> Vec<String> {
error!("ui_script: managed-positions bootstrap: {e}");
return vec![format!("managed-positions bootstrap: {e}")];
}
if let Err(e) = install_durability_reseat(script) {
error!("ui_script: durability re-seat hook: {e}");
return vec![format!("durability re-seat hook: {e}")];
}
Vec::new()
}
/// **A stated divergence from the reference, installed rather than edited in** (1751 window 4).
///
/// The reference seats `DurabilityFrame` 20 further in when one of its three side glyphs is up
/// (`UIParent.lua` l.1759-1768), and recomputes that only when something calls
/// `UIParent_ManageFramePositions`. Stock `DurabilityFrame.xml` calls it from `OnShow`/`OnHide`
/// only — so the case where the frame is **already shown** and a side glyph *then* appears leaves
/// the seat stale, and the shield glyph's ~17-unit overhang hangs off the screen edge until some
/// unrelated frame happens to trigger the next pass. That is the reference's own bug and the
/// director caught it on their screen; it is not ours to reproduce faithfully.
///
/// So the fix rides on the frame's `OnEvent` — which is exactly the recompute the glyphs change on
/// — and it is installed **here** rather than written into anything of Blizzard's. Two reasons for
/// that home rather than a FrameXML one: `assets/ui` does not grow (1779), and this is the wrong
/// side of the line for a `ContainerFrameAdapters`-class shim anyway — that clause is for a genuine
/// engine difference (1751 §2), and this is a deliberate repair of a reference defect. Rust is also
/// the durable home: our `UIParent.xml` is itself a transcription awaiting its own window, and a
/// hook parked there would be homeless again the day it migrates.
///
/// Idempotent by its own latch, so a `ReloadUI` cannot stack wrappers. The reference's handler
/// still runs first and unchanged: `this`, `event` and `arg1` are globals the engine has already
/// set for the dispatch, so calling it through the captured reference is the same call.
pub(super) fn install_durability_reseat(script: &UiScript) -> Result<(), String> {
script.run(DURABILITY_RESEAT).map_err(|e| e.to_string())
}
const DURABILITY_RESEAT: &str = r#"
if DurabilityFrame and not DurabilityFrame.benillaReseat then
DurabilityFrame.benillaReseat = 1
local refOnEvent = DurabilityFrame:GetScript("OnEvent")
DurabilityFrame:SetScript("OnEvent", function()
if refOnEvent then refOnEvent() end
UIParent_ManageFramePositions()
end)
end
"#;
/// Load benilla's own default UI — every file [`MANIFEST`] names — through the engine-free loader.
/// This is our content (MIT/Apache), committed and **compiled into the binary**
/// ([`super::content`], decision 1175); a dev build still prefers the copy on disk, so editing a
@@ -5,23 +5,7 @@
use benilla_ui::script::UiScript;
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the other tests'
/// loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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}: loader errors: {:?}",
report.errors
);
}
use super::test_ui::load_ui as load_xml;
fn enabled(s: &UiScript, button: &str) -> bool {
s.eval::<bool>(&format!("return {button}:IsEnabled() ~= 0"))
@@ -30,6 +14,7 @@ fn enabled(s: &UiScript, button: &str) -> bool {
#[test]
fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// Host globals the cluster's OnLoad/clicks lean on that a bare engine doesn't install.
@@ -93,6 +78,7 @@ fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
/// tracking state pushed, hide when it clears.
#[test]
fn tracking_frame_follows_get_tracking_texture_across_player_auras_changed() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::TrackingState;
let mut s = UiScript::new().unwrap();
@@ -134,18 +120,24 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript {
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
// Byte-exact 1.12 GlobalStrings (l.4251-4253) — the engine-only session loads no
// GlobalStrings off a chain.
s.run("TIME_TWELVEHOURAM = \"%d:%02d AM\"; TIME_TWELVEHOURPM = \"%d:%02d PM\"; TIME_TWENTYFOURHOURS = \"%d:%02d\"")
.unwrap();
s.run(&format!(
"__benilla_game_hour = {hour}; __benilla_game_minute = {minute}"
))
.unwrap();
// The player's own strings and locale rather than a hand-typed copy of three of them: the
// window is the reference's file since 1751 window 5, and `TIME_TWENTYFOURHOURS` and
// `TwentyFourHourTime` are exactly what it reads. `Localization.xml` only DEFINES
// `LocalizeFrames`; the reference calls it from `UIParent_OnEvent`'s VARIABLES_LOADED arm, and
// this session has no UIParent, so it calls it directly.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\Localization.xml");
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, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
load_xml(&s, "GameTime.xml");
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
s
}
@@ -161,6 +153,7 @@ fn tod_window(s: &UiScript) -> (f64, f64, f64, f64) {
/// the ref compares (`< DAWN or >= DUSK`).
#[test]
fn game_time_frame_slides_the_sun_moon_window_on_the_game_clock() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = game_time_session(10, 30);
// OnLoad seeded `timeOfDay = 0` and 10:30 ≠ 0, so the very first update already seated the
// window — no OnUpdate tick needed for the initial state.
@@ -194,6 +187,7 @@ fn game_time_frame_slides_the_sun_moon_window_on_the_game_clock() {
/// hit-test never captures) and the `<HitRectInsets>` hull.
#[test]
fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = game_time_session(21, 7);
s.resolve();
@@ -239,6 +233,7 @@ fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
/// Calling `Minimap_OnClick()` from `s.run` would prove none of it (1234 §2).
#[test]
fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
@@ -282,6 +277,7 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
/// under its own feet.
#[test]
fn get_ping_position_is_nil_until_there_is_a_ping() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
@@ -1,7 +1,7 @@
//! The breath / fatigue bars against the transcribed reference behavior (decision 0874): the
//! extracted 1.12 `MirrorTimer.lua` is the spec — a bar per running timer, the timer's own colour
//! and caption, the value integrating `scale * elapsed` between packets, and the frame released
//! back to the pool on STOP.
//! The breath / fatigue bars against the reference's own `MirrorTimer.xml`/`.lua`, executed off
//! the player's chain since 1751's seventh window (decision 0874 is the arc): a bar per running
//! timer, the timer's own colour and caption, the value integrating `scale * elapsed` between
//! packets, and the frame released back to the pool on STOP.
//!
//! Two halves, and the second is the load-bearing one (the cast bar's lesson, `cast_tests`): the
//! state tests read the Lua back, which is blind to what actually *paints*. The
@@ -10,28 +10,22 @@
use benilla_ui::script::{ExtractedQuad, QuadContent, ScriptValue, UiScript};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
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}: loader errors: {:?}",
report.errors
);
}
use super::test_ui::load_ui as load_xml;
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
load_xml(&s, "MirrorTimer.xml");
// The bars' START driver: `UIParent_OnEvent`'s MIRROR_TIMER_START arm is what calls
// `MirrorTimer_Show`, which is where the reference keeps it (UIParent.lua l.97 + l.374-377)
// and where window 7 moved ours back to. Without this file the event reaches nothing.
load_xml(&s, "UIParent.xml");
// `STATICPOPUP_NUMDIALOGS`, which the reference's own `MirrorTimer_Show` bounds its free-bar
// search by (MirrorTimer.lua l.32 — a copy-paste from StaticPopup.lua, and its own bug). A
// session without it searches `1, nil` and finds no free bar at all, so this is not scenery.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "Interface\\FrameXML\\MirrorTimer.xml");
s
}
@@ -105,6 +99,7 @@ fn tex_quad<'a>(quads: &'a [ExtractedQuad], leaf: &str) -> Option<&'a ExtractedQ
/// Every bar starts hidden — no timer, no chrome on screen.
#[test]
fn the_three_timers_load_hidden() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
for i in 1..=3 {
assert!(!shown(&s, &format!("MirrorTimer{i}")), "MirrorTimer{i}");
@@ -119,6 +114,7 @@ fn the_three_timers_load_hidden() {
/// seated at the value the server named.
#[test]
fn breath_takes_the_first_bar_in_the_reference_blue() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
@@ -140,6 +136,7 @@ fn breath_takes_the_first_bar_in_the_reference_blue() {
/// client's key for it is `EXHAUSTION`, not the server's `FATIGUE`.
#[test]
fn fatigue_is_the_reference_yellow_under_its_own_caption() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
@@ -156,6 +153,7 @@ fn fatigue_is_the_reference_yellow_under_its_own_caption() {
/// reference's draining rate that is one bar-second per real second.
#[test]
fn the_bar_drains_at_the_servers_signed_rate() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
@@ -184,6 +182,7 @@ fn the_bar_drains_at_the_servers_signed_rate() {
/// A paused timer holds its value — the frozen state the server sends when it stops the clock.
#[test]
fn a_paused_timer_holds_its_value() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event(
"MIRROR_TIMER_START",
@@ -205,8 +204,18 @@ fn a_paused_timer_holds_its_value() {
"frozen: no integration at all"
);
// MIRROR_TIMER_PAUSE(name, 0) releases it (our arg2 reading — the reference's own branch is
// bugged and the packet is never sent by vmangos; see MirrorTimer.xml departure 3).
// **And `MIRROR_TIMER_PAUSE` cannot release it, because the reference's own branch is
// bugged.** `MirrorTimerFrame_OnEvent` reads `arg1` as the timer NAME (`arg1 ~= this.timer`)
// and then, two lines later, as a number (`arg1 > 0`) — comparing a string to a number, which
// raises in Lua 5.0. So the handler dies before it can clear `paused` and the bar stays
// frozen.
//
// Ours read the flag from `arg2` until 1751's seventh window, which is what both branches
// plainly intend; the swap made the reference's body the live one and this pins what it
// actually does. It is unreachable in play: vmangos refuses to send that packet and
// substitutes a full START, saying so in `Player::SendMirrorTimers` — which is presumably why
// the bug survived 1.12 at all. Repairing it is a decision someone can make in the adapters;
// it is not the default, and it is not silent.
s.fire_event(
"MIRROR_TIMER_PAUSE",
vec![ScriptValue::Str("BREATH".into()), ScriptValue::Int(0)],
@@ -214,9 +223,15 @@ fn a_paused_timer_holds_its_value() {
for _ in 0..20 {
frame(&mut s, 0.05);
}
assert_eq!(
bar_value(&s, "MirrorTimer1"),
45.0,
"the reference's own bugged branch cannot unfreeze a bar"
);
let errs = s.errors();
assert!(
bar_value(&s, "MirrorTimer1") < 44.5,
"unfrozen: draining again"
errs.iter().any(|e| e.contains("compare")),
"…and says so, loudly, rather than failing quietly: {errs:?}"
);
}
@@ -225,6 +240,7 @@ fn a_paused_timer_holds_its_value() {
/// consuming a third.
#[test]
fn two_timers_stack_and_restate_in_place() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
@@ -244,6 +260,7 @@ fn two_timers_stack_and_restate_in_place() {
/// STOP hides only the named timer and hands its frame back to the pool.
#[test]
fn stop_hides_that_timer_and_frees_its_frame() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
@@ -269,6 +286,7 @@ fn stop_hides_that_timer_and_frees_its_frame() {
/// (the reference's `PLAYER_ENTERING_WORLD` arm).
#[test]
fn entering_the_world_clears_every_bar() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
@@ -283,6 +301,7 @@ fn entering_the_world_clears_every_bar() {
/// out from under a different timer.
#[test]
fn a_stop_for_an_idle_timer_touches_nothing() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
s.fire_event(
@@ -297,6 +316,7 @@ fn a_stop_for_an_idle_timer_touches_nothing() {
/// state tests above all pass on a bar that draws nothing.
#[test]
fn a_running_timer_paints_its_chrome_and_fill() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
let quads = frame(&mut s, 0.016);
@@ -326,6 +346,7 @@ fn a_running_timer_paints_its_chrome_and_fill() {
/// …and a stopped timer takes all of it off screen again.
#[test]
fn a_stopped_timer_paints_nothing() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
frame(&mut s, 0.016);
@@ -346,6 +367,7 @@ fn a_stopped_timer_paints_nothing() {
/// fill in the painter's order — i.e. strictly greater `z`.
#[test]
fn the_border_and_caption_draw_over_the_fill() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
let quads = frame(&mut s, 0.016);
@@ -31,7 +31,7 @@ fn harness() -> UiScript {
"UiPanels.xml",
"UIParent.xml",
"GameTooltip.xml",
"ItemRef.xml",
"Interface\\FrameXML\\ItemRef.xml",
"MerchantFrame.xml",
"Cooldown.xml",
"CharacterFrame.xml",
@@ -56,7 +56,11 @@ fn harness_with_bags() -> UiScript {
for f in BAG_UI {
load_xml(&s, f);
}
for f in ["ItemRef.xml", "MerchantFrame.xml", "CharacterFrame.xml"] {
for f in [
"Interface\\FrameXML\\ItemRef.xml",
"MerchantFrame.xml",
"CharacterFrame.xml",
] {
load_xml(&s, f);
}
s.set_money(0);
@@ -1,39 +1,35 @@
//! The unit right-click popups reached the two ways a solo player invites someone (director
//! report, 2026-07-17): right-clicking a **target** unit frame, and right-clicking a **chat
//! name**. Both drive the shared UnitPopup engine (`UnitPopup.xml`); the chat path adds the
//! `SetItemRef` player branch + its minimal `FriendsFrame` stand-in (`ItemRef.xml`). Proven
//! serverless through the real hit/route paths.
//! `SetItemRef` player branch — the reference's own since 1751 window 6 — and the FRIEND dropdown
//! it opens, which lives in `FriendsFrame.xml` where 1.12 keeps it. Proven serverless through the
//! real hit/route paths.
use benilla_ui::script::{FollowRequest, PartyRequest, UiScript, UnitState};
fn load_xml(s: &UiScript, file: &str) {
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}: loader errors: {:?}",
report.errors
);
}
use super::test_ui::load_ui as load_xml;
/// The production prefix the popups need (ui_script/mod.rs order): the dropdown kit, the unit
/// popups, the chat-link router (`ItemRef.xml`, after UnitPopup — its `FriendsDropDown` inherits
/// `UIDropDownMenuTemplate` and opens a UnitPopup FRIEND menu), then the unit frames.
/// The production prefix the popups need, in `benilla.toc`'s own order: the dropdown kit, the
/// unit popups, the reference's chat-link router, the unit frames — and `FriendsFrame.xml`, which
/// is where `FriendsFrame_ShowDropdown` and the `FriendsDropDown` host live in 1.12 and where they
/// live here since window 6 (our ItemRef.xml used to carry a private second copy of both).
fn load_popup_frames(s: &UiScript) {
for file in [
"Fonts.xml",
"UIParent.xml",
// `SmallMoneyFrame_OnLoad`, which UiPanels' own StaticPopup money rows call at load.
"MoneyFrame.xml",
// `StaticPopupDialogs` and the `PanelTemplates_*` family, both of which FriendsFrame.xml
// reaches at LOAD (its tab row and its confirm dialogs).
"UiPanels.xml",
"GameTooltip.xml",
"UIDropDownMenu.xml",
"UnitPopup.xml",
"ItemRef.xml",
"Interface\\FrameXML\\ItemRef.xml",
"UnitFrames.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
"FriendsFrame.xml",
] {
load_xml(s, file);
}
@@ -69,6 +65,7 @@ fn bake_strings(s: &UiScript) {
/// hit — the player branch of `SetItemRef` used to be a no-op stub.
#[test]
fn chat_name_right_click_opens_the_invite_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_strings(&s);
@@ -137,6 +134,7 @@ fn chat_name_right_click_opens_the_invite_menu() {
/// off for players — only NPC/self targets, whose every row needs a party, open nothing.
#[test]
fn solo_target_right_click_invites_a_player() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_strings(&s);
@@ -236,6 +234,7 @@ fn solo_target_right_click_invites_a_player() {
/// so a break between `this.value == "TRADE"` and the queued initiate token shows up here.
#[test]
fn solo_target_trade_click_queues_an_initiate() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_strings(&s);
@@ -303,6 +302,7 @@ fn solo_target_trade_click_queues_an_initiate() {
/// exactly the kind of break a row-label assertion cannot see.
#[test]
fn solo_target_follow_click_queues_an_exact_by_name_follow() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_strings(&s);
@@ -369,6 +369,7 @@ fn solo_target_follow_click_queues_an_exact_by_name_follow() {
/// popup's dispatch, and `inspect_tests.rs` owns what the window then does.
#[test]
fn solo_target_inspect_click_reaches_inspect_unit() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_strings(&s);
@@ -536,6 +537,7 @@ fn right_click_the_pet_frame(s: &mut UiScript) {
/// offered a row the reference never shows.
#[test]
fn the_pet_menu_forks_between_abandon_and_dismiss() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_pet_strings(&s);
@@ -605,6 +607,7 @@ fn the_pet_menu_forks_between_abandon_and_dismiss() {
/// giving up a tamed pet is a server-side delete.
#[test]
fn dismiss_sends_immediately_and_abandon_waits_for_the_confirm() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_pet_strings(&s);
@@ -661,6 +664,7 @@ fn dismiss_sends_immediately_and_abandon_waits_for_the_confirm() {
/// opens while the name dialog is still up, so the second one lands in instance 2.
#[test]
fn renaming_a_pet_reads_the_name_back_before_sending_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_pet_strings(&s);
@@ -712,6 +716,7 @@ fn renaming_a_pet_reads_the_name_back_before_sending_it() {
/// was blocked on, and the only row here that opens a window rather than a wire verb.
#[test]
fn the_pet_details_row_opens_the_pet_paper_doll() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_pet_strings(&s);
@@ -742,6 +747,7 @@ fn the_pet_details_row_opens_the_pet_paper_doll() {
/// unit reconnects mid-open — and closing the menu re-parks the driver.
#[test]
fn an_idle_unit_popup_driver_parks_itself_off_the_tick() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
bake_strings(&s);
@@ -1,4 +1,5 @@
//! The shipped `ZoneText.xml` driven engine-only (decision 0287): the zone splash shows on the
//! The reference's own `ZoneText.xml` — executed off the player's chain since 1751's eighth
//! window — driven engine-only (decision 0287): the zone splash shows on the
//! right events with the right strings/colors, fades on the reference 0.5/1.0/2.0 timeline, and
//! honors the subtle law — a plain `ZONE_CHANGED` re-caches the zone name *silently*, so a later
//! `ZONE_CHANGED_NEW_AREA` that lands on the already-cached name never re-splashes.
@@ -8,23 +9,7 @@
use benilla_ui::script::UiScript;
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the sibling
/// tests' loader, duplicated so this file is self-contained).
fn load_xml(s: &UiScript, file: &str) {
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}: loader errors: {:?}",
report.errors
);
}
use super::test_ui::load_ui as load_xml;
fn visible(s: &UiScript, frame: &str) -> bool {
s.eval::<bool>(&format!("return {frame}:IsVisible() and true or false"))
@@ -50,16 +35,26 @@ fn set_area(s: &UiScript, zone: &str, sub: &str, pvp: &str, faction: &str) {
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The player's own strings: the splash formats CONTESTED_TERRITORY,
// FACTION_CONTROLLED_TERRITORY, FREE_FOR_ALL_TERRITORY and the AUTOFOLLOW pair straight out of
// GlobalStrings. Our deleted copy carried `X = X or "…"` fallbacks for a harness with no
// chain; the reference's file has none, and it should not.
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, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "ZoneText.xml");
// The fading kit, its own manifest entry since window 8 — as it is in the reference's TOC.
load_xml(&s, "Interface\\FrameXML\\FadingFrame.xml");
load_xml(&s, "Interface\\FrameXML\\ZoneText.xml");
s
}
#[test]
fn new_area_splashes_zone_pvp_and_subzone_then_fades_out() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
// OnLoad hides both fading frames.
@@ -89,6 +84,7 @@ fn new_area_splashes_zone_pvp_and_subzone_then_fades_out() {
#[test]
fn subzone_hop_shows_only_the_small_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
@@ -108,6 +104,7 @@ fn subzone_hop_shows_only_the_small_line() {
#[test]
fn plain_zone_changed_recaches_silently_so_new_area_wont_resplash() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
@@ -143,6 +140,7 @@ fn plain_zone_changed_recaches_silently_so_new_area_wont_resplash() {
/// SubZoneTextFrame's handler calls SetZoneText(1), which SETS the (hidden) PVP string.
#[test]
fn abbey_grounds_subzone_hop_shows_no_territory_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
@@ -178,6 +176,7 @@ fn abbey_grounds_subzone_hop_shows_no_territory_line() {
/// truth, produced by the engine feed rather than by handler-order luck.
#[test]
fn abbey_interior_shows_the_room_in_the_small_line_alone() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
@@ -210,6 +209,7 @@ fn abbey_interior_shows_the_room_in_the_small_line_alone() {
/// shows under it, exactly as on a NEW_AREA splash.
#[test]
fn inn_entry_splashes_the_inn_name_with_territory_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "Goldshire", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
@@ -231,6 +231,7 @@ fn inn_entry_splashes_the_inn_name_with_territory_line() {
/// exactly like the entry hop.
#[test]
fn indoor_exit_returns_the_subzone_line_alone() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
@@ -265,6 +266,7 @@ fn indoor_exit_returns_the_subzone_line_alone() {
/// the big frame never shows, so no territory line.
#[test]
fn room_to_room_hop_splashes_the_room_name_alone() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Elwynn Forest", "Main Hall", "friendly", "Alliance");
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
@@ -284,6 +286,7 @@ fn room_to_room_hop_splashes_the_room_name_alone() {
/// the "PvP Area" arena string under it, red per the quote's PVPArenaTextString color.
#[test]
fn arena_pit_shows_the_ffa_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(&s, "Stranglethorn Vale", "", "contested", "");
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
@@ -304,6 +307,7 @@ fn arena_pit_shows_the_ffa_line() {
/// anchors to PVPInfoTextString's BOTTOM — the three-line stack the director's screenshot shows.
#[test]
fn subzone_seat_hangs_under_the_territory_line_on_new_area() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_area(
&s,
@@ -336,6 +340,7 @@ fn subzone_seat_hangs_under_the_territory_line_on_new_area() {
/// name at start rather than re-reading it — by the time a follow ends, the followee is often gone.
#[test]
fn the_autofollow_status_line_names_the_followee_and_fades_on_end() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
assert!(
!visible(&s, "AutoFollowStatus"),
@@ -382,6 +387,7 @@ fn the_autofollow_status_line_names_the_followee_and_fades_on_end() {
/// than inheriting the dying one's alpha.
#[test]
fn a_begin_during_the_end_fade_resets_the_line() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event(
"AUTOFOLLOW_BEGIN",
+54 -20
View File
@@ -1,8 +1,12 @@
//! Drives the REAL `assets/ui/ItemTextFrame.xml` through the engine — the reader window for bag
//! letters (mail-made permanent copies) and, later, books. Loads the same file chain the app does
//! (cut to the reader's dependency prefix), pushes an `ItemTextState`, fires the reference event
//! flow `ITEM_TEXT_BEGIN` → `ITEM_TEXT_READY` → `ITEM_TEXT_CLOSED`, and asserts the transcribed
//! Lua actually paints (title, the "From," creator tail, page-button visibility, the close intent).
//! Drives the REAL reader window through the engine — bag letters (mail-made permanent copies)
//! and, later, books. Loads the same file chain the app does (cut to the reader's dependency
//! prefix), pushes an `ItemTextState`, fires the reference event flow `ITEM_TEXT_BEGIN` →
//! `ITEM_TEXT_READY` → `ITEM_TEXT_CLOSED`, and asserts the Lua actually paints (title, the "From,"
//! creator tail, page-button visibility, the close intent).
//!
//! The window itself is `Interface\FrameXML\ItemTextFrame.xml`, off the player's own patch chain
//! since 1751's ninth window — so every test here gates on the install, as every chain-backed
//! test does.
use benilla_ui::script::{ItemTextState, UiScript};
@@ -11,32 +15,56 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// The reader's dependency prefix, in the manifest's own order. `ScrollTemplates.xml` and
/// `UIPanelTemplates.xml` joined it with decisions 1337/1338: the page sits in a real ScrollFrame
/// now, whose template is in the second and whose `ScrollFrame_OnLoad` is in the first.
const FILES: [&str; 6] = [
const FILES: [&str; 8] = [
// `ITEM_TEXT_FROM`, which the reference's READY arm concatenates into the creator tail — and
// `attempt to concatenate a nil value` kills the handler before it reaches its `ShowUIPanel`,
// so the window simply never opens. Our deleted copy carried the string as a local fallback.
"Interface\\FrameXML\\GlobalStrings.lua",
"Fonts.xml",
"MoneyFrame.xml",
"UiPanels.xml",
// `GetMaterialTextColors`, which the reference's own `ItemTextFrame_OnEvent` calls to pick the
// page and title ink. 1.12 keeps it in UIParent.lua and ours does the same (1751 window 9).
"UIParent.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
"ItemTextFrame.xml",
"Interface\\FrameXML\\ItemTextFrame.xml",
];
/// **Both stores, told apart the manifest's own way** — a bare filename is a file we ship, a path
/// is the reference's own off the player's install (`ui_script::reference_ui::is_chain_entry`,
/// re-expressed here because that module is crate-private and this is an integration test).
///
/// The provider half matters as much as the loop: the reference's `ItemTextFrame.xml` pulls its
/// Lua through `<Script file="ItemTextFrame.lua"/>`, which the loader resolves against the
/// document's own directory — `Interface\FrameXML\ItemTextFrame.lua`, a chain path — so a
/// disk-only provider would leave every one of its globals nil and every test below red for a
/// reason that looks nothing like the cause.
fn load_ui(script: &UiScript) {
let dir = std::path::Path::new(UI_DIR);
let provider = |req: &str| -> Option<Vec<u8>> {
let norm = req.replace('\\', "/");
let base = norm.rsplit('/').next().unwrap_or(&norm);
std::fs::read(dir.join(&norm))
.or_else(|_| std::fs::read(dir.join(base)))
.ok()
let chain = benilla_formats::wow_data()
.and_then(|data| benilla_formats::open_chain(&data).ok())
.expect("client data — every test here gates on it");
let read = |req: &str| -> Option<Vec<u8>> {
if req.contains('\\') || req.contains('/') {
return chain.read(req).ok();
}
std::fs::read(dir.join(req)).ok()
};
for file in FILES {
let text = std::fs::read_to_string(dir.join(file)).unwrap_or_else(|e| {
panic!("reading {file}: {e}");
});
let doc = benilla_ui::framexml::parse(&text).unwrap_or_else(|e| {
panic!("parsing {file}: {e}");
});
let report = benilla_ui::loader::load(script, &doc, &provider);
let bytes = read(file).unwrap_or_else(|| panic!("reading {file}"));
// A `.lua` entry is a chunk, not a document — `GlobalStrings.lua` is one, exactly as it is
// in the reference's own TOC. Bytes, not text: a chunk goes to Lua as it sits in the
// archive and only an XML parse decodes (1193).
if file.to_ascii_lowercase().ends_with(".lua") {
script
.run_chunk_named(&bytes, &format!("@{file}"))
.unwrap_or_else(|e| panic!("running {file}: {e}"));
continue;
}
let doc = benilla_ui::framexml::parse(&benilla_ui::source::decode(&bytes))
.unwrap_or_else(|e| panic!("parsing {file}: {e}"));
let report = benilla_ui::loader::load_in(script, &doc, &file.replace('\\', "/"), &read);
assert!(
report.errors.is_empty(),
"{file} loaded with errors: {:#?}",
@@ -78,6 +106,7 @@ fn letter() -> ItemTextState {
/// and shows the window; a single page shows no page number and no page-turn buttons.
#[test]
fn a_letter_reads_with_the_creator_tail() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
assert!(!s.eval::<bool>("return ItemTextFrame:IsShown()").unwrap());
@@ -118,6 +147,7 @@ fn a_letter_reads_with_the_creator_tail() {
/// check pins the tripwire that now catches any such unresolved named anchor at load.
#[test]
fn the_scrollbar_track_sits_right_of_the_page() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
let unresolved: Vec<String> = s
@@ -149,6 +179,7 @@ fn the_scrollbar_track_sits_right_of_the_page() {
/// buttons show per the reference visibility rules (page 1 with a next → Next only).
#[test]
fn a_book_page_shows_the_paging_chrome() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_item_text(Some(ItemTextState {
@@ -186,6 +217,7 @@ fn a_book_page_shows_the_paging_chrome() {
/// clears the session and fires `ITEM_TEXT_CLOSED` — which hides again, harmlessly).
#[test]
fn closing_queues_the_close_intent() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_item_text(Some(letter()));
@@ -216,6 +248,7 @@ fn closing_queues_the_close_intent() {
/// one block that still has angle brackets in it, which is exactly what was reported.
#[test]
fn the_reported_html_page_draws_as_blocks_not_as_its_own_markup() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_item_text(Some(ItemTextState {
@@ -404,6 +437,7 @@ fn the_reported_book_crest_draws_at_the_blps_own_size() {
/// OnHide ending its own session exactly as the merchant/gossip pair already does.
#[test]
fn a_quest_givers_gossip_displaces_the_open_reader() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::GossipMenu;
let mut s = UiScript::new().unwrap();
@@ -36,6 +36,48 @@ pub(super) use plain::REG_MESSAGEFRAME_METHODS;
pub(super) use scrolling::REG_SCROLLINGMESSAGEFRAME_METHODS;
/// Install both classes' method tables (and the chat input globals the scrolling one carries).
/// **`AddMessage`'s text argument, and the three ways it silently swallows the whole call**
/// (wow-5875-re `system/ui/scratch/addmessage-text-gate-silent-skip.md`, 4-worker cross-check
/// 2026-08-31). `None` means *do nothing at all* — no line, no record, no error.
///
/// `CSimpleMessageFrame::AddMessage 0x795590` fetches Lua index 2 through `lua_isstring 0x6f3510`
/// and then `lua_tostring 0x6f3690`, and **three** results jump to the function's own epilogue at
/// `0x79582b` (`pop edi / pop esi / xor eax,eax / pop ebx / leave / ret` — zero Lua values, no
/// push, and the virtual enqueue at `0x795825 call [eax+0x90]` never reached):
///
/// ```text
/// 79561b mov edx,2
/// 795625 call 0x6f3510 ; lua_isstring — tags 4 (string) and 3 (number) only
/// 79562c je 0x79582b ; nil / boolean / table / function -> silent
/// 795642 je 0x79582b ; lua_tostring returned NULL -> silent
/// 79564b je 0x79582b ; cmp byte ptr [ebx],0 — EMPTY string -> silent
/// ```
///
/// `ScrollingMessageFrame:AddMessage 0x792900` is the same shape (gate `0x79299c`, `je 0x792b81` =
/// its own epilogue), so `DEFAULT_CHAT_FRAME:AddMessage(nil)` is silent too.
///
/// **This is not a general argument law and must not be read as one** (1717's rule: the shape is
/// settled per binding). The *colour* arguments beside it behave the opposite way — a bad `r`/`g`/
/// `b`/`a` jumps to `0x79581c`, which still calls the enqueue with the `0xffffffff` opaque white
/// staged at `0x795658`. Only a bad **text** shows nothing. The three *receiver* guards
/// (`0x847ef8` "used '.' instead of ':'", `0x847ec0`, `0x847e98`) do still raise, and they are
/// upstream of this.
///
/// **Why it is load-bearing rather than pedantry.** Stock FrameXML takes this path: 1.12's
/// `ContainerFrame.lua:753` reports a full keyring with
/// `UIErrorsFrame:AddMessage(NO_EMPTY_KEYRING_SLOTS, 1.0, 0.1, 0.1, 1.0)` and **no such
/// GlobalString exists** — the shipped `GlobalStrings.lua` defines only
/// `NO_EMPTY_KEYRING_SLOTS_ERROR`, a rename that updated the definition and not the use. So a
/// player dropping a key onto a full keyring gets a dead click on a real client. Taking a `String`
/// here (which is what this did until 1751's fourth window swapped the bag bar in) raised instead,
/// putting a script-error dialog on an ordinary player gesture that the reference never shows.
///
/// A **number** is accepted and rendered as its decimal text — `0x6f7c80` retags the slot in
/// place — which is exactly [`super::binding_abi::optional_string`]'s coercion.
pub(super) fn message_text(lua: &mlua::Lua, v: &mlua::Value) -> Option<String> {
super::binding_abi::optional_string(lua, v).filter(|s| !s.is_empty())
}
/// Install `SetJustifyH`/`GetJustifyH`/`SetJustifyV`/`GetJustifyV` on a message-frame method
/// table. Both classes carry all four (see [`crate::widget::ScrollingMessageState::justify`] for
/// the method-table bytes that settled it), and the law they obey is
@@ -70,9 +70,18 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// AddMessage(text [, r, g, b [, a]]) — binding 0x795590.
//
// The TEXT is `super::message_text` — an `lua_isstring` gate whose failure is a silent jump to
// this function's own epilogue, not a raise, and the same for a NULL coercion and for the
// empty string. Its doc carries the bytes and the reason it matters; the short version is that
// stock `ContainerFrame.lua` really does reach `AddMessage(nil, …)` on a full keyring, and
// taking a `String` here (which this did until 1751's fourth window) put a script-error dialog
// on a gesture the reference answers with a dead click.
//
// r/g/b are required **as a trio** (three presence checks ANDed at 0x7956xx); absent ⇒ hard
// 0xFFFFFFFF white. The fourth numeric is a real alpha, `lua_isnumber`-gated with a default of
// 1.0 (`795752: mov [ebp-0x18],0x3f800000`).
// 1.0 (`795752: mov [ebp-0x18],0x3f800000`). **Deliberately NOT the same shape as the text:** a
// bad colour jumps to `0x79581c`, which still enqueues the line in opaque white. Only a bad
// text shows nothing.
//
// **The signature stops there, and the closure's arity is the enforcement.** The corpus's three
// blocked callers all pass a sixth argument they believe is a hold time; mlua hands us the
@@ -82,7 +91,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
m.set(
"AddMessage",
lua.create_function(
|lua, (this, text, r, g, b, a): (Table, String, Value, Value, Value, Value)| {
|lua, (this, text, r, g, b, a): (Table, Value, Value, Value, Value, Value)| {
let Some(text) = super::message_text(lua, &text) else {
return Ok(());
};
let has_rgb = !matches!(
(&r, &g, &b),
(Value::Nil, _, _) | (_, Value::Nil, _) | (_, _, Value::Nil)
@@ -245,6 +257,61 @@ mod tests {
);
}
/// **A text `AddMessage` cannot use is swallowed whole — silently, with nothing added and
/// nothing raised** (`0x795590`'s three jumps to its own epilogue; see [`super::super`]'s
/// `message_text`).
///
/// The defect this pins is not hypothetical and not ours to have guessed at: our binding took
/// its text as an mlua `String`, which raises on nil, and 1.12's own `ContainerFrame.lua:753`
/// reports a full keyring through `UIErrorsFrame:AddMessage(NO_EMPTY_KEYRING_SLOTS, …)` — a
/// GlobalString the shipped `GlobalStrings.lua` never defines. So dropping a key onto a full
/// keyring put a script-error dialog on screen where the reference gives a dead click. It
/// surfaced the moment 1751's third window made the bag bar the reference's own file.
///
/// The three silent cases and the two that are NOT silent are pinned together, because the
/// pair is the finding: a bad **colour** still adds the line (opaque white), and the receiver
/// guards still raise. Reading the text gate as a general argument law is exactly 1717's error.
#[test]
fn addmessage_swallows_a_text_it_cannot_use_and_only_the_text() {
let s = UiScript::new().unwrap();
s.run("CreateFrame('MessageFrame', 'MF')").unwrap();
// `lua_isstring 0x6f3510` accepts tags 4 and 3 only — everything else jumps to 0x79582b.
for bad in ["nil", "", "true", "{}", "print"] {
s.run(&format!("MF:AddMessage({bad})"))
.unwrap_or_else(|e| panic!("AddMessage({bad}) must not raise: {e}"));
}
// …and so does the empty string, on its own `cmp byte ptr [ebx],0` at 0x79564b.
s.run("MF:AddMessage('')").unwrap();
assert_eq!(
num_messages(&s, "MF"),
0,
"nil, absent, boolean, table, function and \"\" each add NOTHING"
);
// A number IS a usable text — tag 3 passes the gate and `0x6f7c80` retags the slot in
// place, so it renders as its decimal form.
s.run("MF:AddMessage(42)").unwrap();
assert_eq!(num_messages(&s, "MF"), 1, "a number is a text");
// The colour arguments are the OPPOSITE shape and that is the load-bearing negative: a bad
// r/g/b jumps to 0x79581c, which still enqueues with the 0xffffffff white staged at
// 0x795658. Suppressing the line here would be the same over-generalisation in reverse.
s.run("MF:AddMessage('kept', {}, nil, 'x')").unwrap();
assert_eq!(
num_messages(&s, "MF"),
2,
"a bad colour still adds the line"
);
// The receiver guards are upstream of all of it and DO raise (0x847ef8 — "used '.' instead
// of ':'").
assert!(
s.run("MF.AddMessage('dot')").is_err(),
"a '.'-instead-of-':' call still raises"
);
}
/// `SetInsertMode` is a MessageFrame method and **only** a MessageFrame method — a plain Frame
/// and a ScrollingMessageFrame both resolve it to nil, which is what a duck-typing addon reads.
#[test]
@@ -92,13 +92,20 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// method-table bytes that put all four on BOTH tables.
super::install_justify(lua, &m, "ScrollingMessageFrame")?;
// AddMessage(text [, r, g, b [, id]]) — the id (5th arg) is accepted and ignored (v1 has no
// per-line UpdateColorByID). rgb absent ⇒ white; the state quantizes round-half-up and forces
// the line opaque, then the fade drives its alpha.
// AddMessage(text [, r, g, b [, id]]) — binding 0x792900, the id (5th arg) accepted and
// ignored (v1 has no per-line UpdateColorByID). rgb absent ⇒ white; the state quantizes
// round-half-up and forces the line opaque, then the fade drives its alpha.
//
// The TEXT gate is the sibling class's, verbatim — gate `0x79299c`, `je 0x792b81` = this
// function's own epilogue — so `DEFAULT_CHAT_FRAME:AddMessage(nil)` is silent too. See
// `super::message_text`.
m.set(
"AddMessage",
lua.create_function(
|lua, (this, text, r, g, b): (Table, String, Value, Value, Value)| {
|lua, (this, text, r, g, b): (Table, Value, Value, Value, Value)| {
let Some(text) = super::message_text(lua, &text) else {
return Ok(());
};
let has_rgb = !matches!(
(&r, &g, &b),
(Value::Nil, _, _) | (_, Value::Nil, _) | (_, _, Value::Nil)
@@ -185,7 +185,7 @@ fn a_region_resolves_even_when_its_owner_frame_has_no_rect() {
/// visible geometry a few links down.
///
/// Reported as B264 (carni, 2026-08-13): opening the social pane drew a stray dropdown capsule at
/// the bottom of the screen next to the action bar. `BenillaFriendsDropDown` carries no anchors —
/// the bottom of the screen next to the action bar. `FriendsDropDown` carries no anchors —
/// *exactly* as the reference's own `FriendsDropDown` does (`FriendsFrame.xml` l.598), and the
/// reference draws nothing — so every texture of `UIDropDownMenuTemplate` hung off a phantom rect
/// at (0,0). The shape below is that template's first two textures verbatim.