Files
benilla-octo/crates/benilla-app/assets/ui/WorldStateFrame.xml
T
SamandClaude Fable 5 db86a9c7ad the zone counters line up, and the ballista shine holds still
- the world-state readout — the top-centre counters a battleground or Silithus keeps up — takes the reference's real geometry: every icon those rows name is a sprite authored into the upper-left corner of a power-of-two canvas (`AllianceTower` is 16×16 of ink at (3,2) in a 32×32 file), nothing anywhere crops it, and the reference compensates geometrically — a 42×42 slot hung 6 units off the row's left edge with the label seated 10 above its centreline, taken as one system; our snug 20×20 box stripped exactly that compensation, so the art drew at half size floating away from its own `0/200` label — and an empty icon column now reserves its rect instead of sliding the text left, which straightens the readout's left edge
- the dynamic icon is a second slot, not a swap: `Icon` and `DynamicIcon` feed two different regions and shipped content never mixes them, so the flag is a 32×32 button off the row's right edge with an ADD-blend flash overlay pulsing 0.5 s in / 0.5 s out, lit only at `uiState == 2` — the flag-taken value; before, any non-zero state replaced the static art, which fires on the ordinary state 1 and hid a faction shield that should never have moved, and the pulse's alpha ramp is now pinned by a test, not just its art
- the dev cost pill yields the band: the readout is the game, the pill is scaffolding, and a stock 1.12 UI has no free corner to move house to — so it steps below whatever the readout draws, reading `WorldStateAlwaysUpFrame:GetBottom()` on its own 4 Hz cadence and only while it is drawing, rather than mirroring the frame's row pitch in Rust; the probe's cost is stated at the site (an edge read settles the layout — up to one graph solve, ~47 µs, four times a second)
- the Hearthglen ballista's bolt heads and shields stop flickering: a shine layer is a second batch over its base's own triangles, and ours split the pair across lanes — the base rode the retained lane's f64 CPU bake while the env-mapped, additive, no-depth-write shine stayed on the entity path's f32 GPU rotate, and a few ULPs between coplanar surfaces turns the depth test into a per-pixel coin flip re-rolled by every camera move; a batch now names the M2 skin section it draws, neither member of a shared section diverts into a consolidator (depths equal by construction), and a corpus census bounds the cost — 1,080 of 9,691 models draw a section more than once, 264 such sections across 221 `World\` doodads — with toggling pixels at the pin dropping 0.26% to 0.14%, matching the consolidators-off arm bit-for-bit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 15:07:21 +07:00

281 lines
15 KiB
XML

<Ui>
<!--
The always-up world-state readout — report B190's second half: the alliance/horde progress
line an outdoor-PvP zone or a battleground shows at the top of the screen. In Eastern
Plaguelands that is the two tower counters ("Towers Controlled: 3"); in Warsong Gulch the
flag captures; in Arathi Basin bases and resources.
The SPEC is the reference's two C bindings, byte-carved (wow-re
system/ui/scratch/worldstate-ui-law.md, 0x4c5a40 / 0x4c5a70) and re-expressed here:
GetNumWorldStateUI() -> how many rows the current scope admits
GetWorldStateUIInfo(i) -> TEN values, in this order:
uiState, text, icon, dynamicIcon,
tooltip, dynamicTooltip, extendedUI,
extUIState1, extUIState2, extUIState3
Note what that return is NOT: there is no uiType and no hidden in 1.12 — those are a later
expansion of the API, and reading this list as if they were there shifts every value by two.
`text` arrives already macro-expanded (the host runs the reference's %<id>w expander);
every other string is its DBC column verbatim, and never nil — an empty column is "".
Which rows exist at all is the host's (crates/benilla-app/src/world_state_ui.rs), including
the gate that makes this frame look broken if you don't know about it: the world-PvP rows
(Eastern Plaguelands, Silithus) are Type 1, which the reference admits only while the player
has joined the zone-dependent DEFENSE chat channel. Leave LocalDefense and the readout goes
away while the map icons stay.
THE GEOMETRY IS THE REFERENCE'S, and it is not what you would guess (wow-re
worldstate-ui-law.md §12, carved 2026-08-25; decision 1604 folds it back). The first pass
of this frame invented a snug 20x20 icon in a 22px row, and that was wrong in a way that
showed: EVERY icon these rows name is a sprite authored into the UPPER-LEFT corner of a
power-of-two canvas with the rest transparent — AllianceTower/HordeTower are a 16x16 sprite
at (3,2) of a 32x32 file, UI-PVP-Alliance/Horde a ~40x40 crest in a 64x64 one. Squeeze that
whole canvas into a snug box and the visible art floats up and to the left of the box,
away from its label. NOTHING IS CROPPED to fix that — there is no <TexCoords> on any of
these regions, and a texture with none samples the full 0..1 quad (CSimpleTexture's ctor
0x76fc40 stamps it). The compensation is geometric, and these three numbers are one tuned
system: the icon slot is OVERSIZED to 42x42 (a 32px file upscaled 1.3125x), hung SIX UNITS
OFF the row's left edge, and the text is seated TEN UNITS ABOVE the slot's centreline. The
upper-left ink then lands as a ~21x21 square whose centre sits just under the text's, which
is what "icon next to its label" is supposed to look like.
THERE ARE TWO ICON SLOTS, fed by two different DBC columns, and shipped content never puts
the same art in both:
Icon (+0x0c) -> the 42x42 slot below. AllianceTower, HordeTower,
UI-PVP-Alliance, UI-PVP-Horde.
DynamicIcon (+0x64) -> a SEPARATE 32x32 button off the row's right edge, shown only
while uiState == 2. HordeFlag/AllianceFlag, Warsong Gulch only.
So the dynamic icon does NOT replace the static one — both are on screen together, which is
the Warsong Gulch look: the faction shield with its capture count, and a flashing enemy
flag to its right while a carrier is running. `uiState == 2` is the flag-taken value; the
first pass swapped the art on `uiState ~= 0`, which fired on the ordinary state 1 and
replaced a shield that should never have moved.
OUT, named: the CAPTUREPOINT extended UI — the sliding bar over
Interface\WorldStateFrame\WorldState-CaptureBar that the one Eastern Plaguelands progress
row drives. Its two states come through the binding already (extUIState1/2 carry the
alliance/horde fractions), so it is a widget to add, not a seam to change. NOW KNOWN, and
worth writing down before someone builds it: in the reference that row never reaches this
frame at all — the update path branches on a non-empty extendedUI FIRST, and id 138 is the
only always-up-class row with one, so EPL's real readout is two text rows here plus a
separate bar anchored under the MinimapCluster. Until that bar exists we keep showing the
row as its text line (decision 1590 §7's stand-in, unchanged) so the progress number is on
screen; it costs a third line the reference doesn't draw, and it goes when the bar lands.
Also out: the battleground scoreboard (the Type 2 rows), which the reference builds from the
same DBC through a DIFFERENT handler and a different pair of bindings whose bail leg is not
even the same shape — GetNumBattlefieldStats/GetBattlefieldStatInfo, a separate slice.
-->
<Script>
<![CDATA[
NUM_ALWAYS_UP_ROWS = 0;
-- The reference's own numbers (worldstate-ui-law.md §12). The row is an anchor node, not a
-- box: its 42-wide icon slot hangs off its left edge and its text runs past its right, so
-- none of these are "the readout's size" — they are the offsets that seat corner-authored
-- art against a label. The pitch is the template height, because rows chain TOP to the
-- previous row's BOTTOM with no gap.
WORLD_STATE_ROW_HEIGHT = 24;
-- The dynamic-icon flash: 0.5 s in, 0.5 s out, indefinitely.
WORLD_STATE_FLASH_HALF = 0.5;
-- The flash overlay's ramp. The art (…Flash.blp) is DXT1 with no alpha channel — black IS
-- the transparency — which is why it draws ADD; the pulse is the region's own alpha.
function WorldStateDynamicIcon_OnUpdate(elapsed)
local t = (this.flashTime or 0) + elapsed;
local period = 2 * WORLD_STATE_FLASH_HALF;
while ( t >= period ) do
t = t - period;
end
this.flashTime = t;
local level = t / WORLD_STATE_FLASH_HALF;
if ( level > 1 ) then
level = 2 - level;
end
getglobal(this:GetName() .. "Flash"):SetAlpha(level);
end
-- Rebuild the readout from the host's list. Cheap enough to run whole on every fire: the
-- list is at most a handful of rows, and the events are a zone's worth apart.
function WorldStateAlwaysUpFrame_Update()
local numUI = GetNumWorldStateUI();
-- Grow the row pool on demand and never shrink it, the way every pooled list here
-- does: the tail from a busier zone is parked hidden rather than destroyed. Row 1
-- hangs off the container; every later row chains to the one above it, so the pitch is
-- the template's height and lives in exactly one place.
if ( numUI > NUM_ALWAYS_UP_ROWS ) then
for i = NUM_ALWAYS_UP_ROWS + 1, numUI do
local row = CreateFrame("Frame", "WorldStateAlwaysUpFrame" .. i,
WorldStateAlwaysUpFrame, "WorldStateAlwaysUpTemplate");
if ( i == 1 ) then
row:SetPoint("TOP", WorldStateAlwaysUpFrame, "TOP", -23, -20);
else
row:SetPoint("TOP", getglobal("WorldStateAlwaysUpFrame" .. (i - 1)),
"BOTTOM", 0, 0);
end
end
NUM_ALWAYS_UP_ROWS = numUI;
end
for i = 1, NUM_ALWAYS_UP_ROWS do
local row = getglobal("WorldStateAlwaysUpFrame" .. i);
if ( i > numUI ) then
row:Hide();
else
local uiState, text, icon, dynamicIcon, tooltip, dynamicTooltip = GetWorldStateUIInfo(i);
local name = row:GetName();
-- The static slot takes its column verbatim. An EMPTY column is not a special
-- case: SetTexture("") clears the art and draws nothing while the authored
-- 42x42 rect — and so everything anchored to it — stays exactly where it was.
-- The slot is reserved and blank, never collapsed; a row that slid its text
-- left into the empty slot would ragged the readout's left edge, and the
-- reference does no such thing.
getglobal(name .. "Icon"):SetTexture(icon);
getglobal(name .. "Text"):SetText(text);
row.tooltip = tooltip;
-- The dynamic slot is a SEPARATE button off the row's right edge, and it is
-- the flag-taken state alone that shows it.
local dynamic = getglobal(name .. "DynamicIcon");
if ( dynamicIcon ~= "" and uiState == 2 ) then
getglobal(dynamic:GetName() .. "Texture"):SetTexture(dynamicIcon);
getglobal(dynamic:GetName() .. "Flash"):SetTexture(dynamicIcon .. "Flash");
dynamic.tooltip = dynamicTooltip;
dynamic.flashTime = 0;
dynamic:Show();
else
dynamic:Hide();
end
row:Show();
end
end
-- OURS, and non-visual: the reference's container is a bare 10x10 anchor node it never
-- resizes, so its own bottom says nothing about how far the rows reach. We keep the
-- authored size but grow the height to the rows' real extent, so "how much of the top
-- of the screen is this using?" is a question the frame can answer — the dev cost pill
-- asks it, and seats itself below (crates/benilla-app/src/perf/hud.rs). Rows anchor to
-- the container's TOP, which is fixed, so nothing moves.
if ( numUI > 0 ) then
WorldStateAlwaysUpFrame:SetHeight(20 + numUI * WORLD_STATE_ROW_HEIGHT);
WorldStateAlwaysUpFrame:Show();
else
WorldStateAlwaysUpFrame:Hide();
end
end
function WorldStateAlwaysUpFrame_OnEvent()
WorldStateAlwaysUpFrame_Update();
end
]]>
</Script>
<!-- One row: the oversized icon slot, its line of text, and the dynamic-icon button that only
Warsong Gulch ever lights. Grown on demand into WorldStateAlwaysUpFrame by the update
above. Every offset here is the reference's — see the header for why they are not the
snug numbers you would pick by eye. -->
<Frame name="WorldStateAlwaysUpTemplate" virtual="true" enableMouse="true">
<Size><AbsDimension x="45" y="24"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon">
<Size><AbsDimension x="42" y="42"/></Size>
<Anchors>
<Anchor point="LEFT">
<Offset><AbsDimension x="-6" y="0"/></Offset>
</Anchor>
</Anchors>
</Texture>
<FontString name="$parentText" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentIcon" relativePoint="RIGHT">
<Offset><AbsDimension x="-12" y="10"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentDynamicIcon" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="RIGHT">
<Offset><AbsDimension x="40" y="9"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentTexture">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentFlash" alphaMode="ADD">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnUpdate>
WorldStateDynamicIcon_OnUpdate(arg1);
</OnUpdate>
<OnEnter>
if ( this.tooltip and this.tooltip ~= "" ) then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");
GameTooltip:SetText(this.tooltip);
GameTooltip:Show();
end
</OnEnter>
<OnLeave>
GameTooltip:Hide();
</OnLeave>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnEnter>
if ( this.tooltip and this.tooltip ~= "" ) then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");
GameTooltip:SetText(this.tooltip);
GameTooltip:Show();
end
</OnEnter>
<OnLeave>
GameTooltip:Hide();
</OnLeave>
</Scripts>
</Frame>
<!-- The container. Top-centre of the screen, which is where an outdoor-PvP zone's counters and
a battleground's score belong: the minimap owns the top-right and the readout must not
fight it. The reference declares it 10x10 and never touches it again — it is an anchor
node, and rows overflow it freely (see the update for the one thing we do differently, and
why). Hidden until a scope admits a row. -->
<Frame name="WorldStateAlwaysUpFrame" parent="UIParent" toplevel="true" hidden="true">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="UIParent" relativePoint="TOP">
<Offset><AbsDimension x="-5" y="-15"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
-- UPDATE_WORLD_STATES is the reference's own event for this list (0x20e). The
-- entering-world fire is what paints the readout on a login that lands inside a
-- scope the server has already sent states for.
this:RegisterEvent("UPDATE_WORLD_STATES");
this:RegisterEvent("PLAYER_ENTERING_WORLD");
</OnLoad>
<OnEvent>
WorldStateAlwaysUpFrame_OnEvent();
</OnEvent>
</Scripts>
</Frame>
</Ui>