Files
benilla-octo/crates/benilla-app/assets/ui/ZoneText.xml
T
SamandClaude Fable 5 d1140829d7 the interface code is ours: the transcription debt is paid
- forty files that carried the reference's literal Lua are rewritten as our own expression of the behaviour — worst identical run 61 lines down to 7 — and the README now says exactly that
- the overlap gate measures the code itself across all four reference corpora, and ratchets: the number may only go down
- a lost session ends at the login screen instead of silently re-authenticating and stealing the account back
- a building's skybox is a real model: Caverns of Time's white ceiling becomes its painted nebula and additive stars

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

426 lines
22 KiB
XML

<!--
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" 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" 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>