Files
benilla-octo/crates/benilla-app/assets/ui/MirrorTimer.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

233 lines
10 KiB
XML

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