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:
@@ -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"
|
||||
|
||||
@@ -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 < 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>
|
||||
@@ -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). -->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user