Files
benilla-octo/crates/benilla-app/assets/ui/UIParent.xml
T
SamandClaude Fable 5 6e3a67e039 your corpse answers the mouse, and a cinematic's last holes close
- a corpse is a body you can hover, cursor and click: it enters the same ray pass as units with the winner switched on type at publish (the reference's own shape — one trace over every object), the cursor classifier is transcribed whole (lootable-by-me → the pickup cursor, grayed by range; else the insignia leg behind the learned-skill latch; your own body falls through to the clear), and right-click is "not mounted AND lootable" → CMSG_LOOT; CORPSE_FIELD_FLAGS bit 5 is the PvP insignia flag, not "reclaimable" as an old note had it; the widening also caught that a GameObject anywhere on screen could steal the click from a nearer corpse, and that a left-click on a body cleared your target as if you had clicked ground
- the cinematic sweep closes eleven holes: a shot plays its sequence band's WIDTH, not its end — two shipped fly-bys band away from zero and ran long with their opening frames frozen (the Night Elf intro measured 102333 → 102000 ms); CMSG_NEXT_CINEMATIC_CAMERA announces every shot, the first included — the old "between shots" reading meant it was sent NEVER, since every 1.12 sequence row carries exactly one camera; the ack invariant is stated once (one CMSG_COMPLETE_CINEMATIC per accepted trigger, over-acking tolerated, under-acking never — a dropped ack despawns the world around you until relog); the hardware cursor hides for the shot; and the music cut lands in the frame that starts it — it was one frame late, exactly long enough for a first login's zone fanfare to start under the narration and be stamped as played
- RunBinding exists, and a fly-by stops handing the world its mouse: CinematicFrame's OnKeyDown hands the SCREENSHOT chord back to its binding through RunBinding, a global we never had, so every press during a fly-by raised and took no picture; and the mouse was fully live under a shot — right-drag turned the character, clicks picked targets through the flying camera, both buttons walked the body — so the pointer now reads as over the UI while a shot plays
- AdvanceTime and ReplaceIconTexture are carved off the bytes: AdvanceTime really is a no-op in the reference (its one reachable call is mov eax,1; ret), ReplaceIconTexture is a material swap over a model's type-14 texture slots that stores nothing on the widget, and SetModel(nil) RAISES — the "documented clear" our binding claimed was invented, not read; and the addon roster now says WHOSE package is short when a .toc names files the folder doesn't hold — nine of the corpus's twenty-nine load failures are the addon's own manifest, which the real client also shrugs off ("Couldn't open %s", carry on), so they stop reading as our defects

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:55:36 +07:00

719 lines
45 KiB
XML

<Ui>
<!--
UIParent itself (ref UIParent.xml l.5, quoted): the real, full-screen, invisible root
frame the reference UI hangs everything on. Until now benilla shipped only this file's
Lua (the manage pass below) and no frame - so the Lua global `UIParent` was nil, and
every ref-quoted `SetOwner(UIParent, ...)` / `GameTooltip_SetDefaultAnchor(this,
UIParent)` died before it could place anything (the world tooltip stayed parked on its
load-time anchor - the "tooltip on my character" bug). String anchors ("UIParent" in
SetPoint) had silently fallen back to the screen root, which has the same rect, so only
the *global* was ever missing. No enableMouse: the hit test gates on it (pointer.rs), so
the full-screen plate swallows nothing.
The ref's UIParent_OnLoad registers a long list of globally-routed events and its OnEvent
is a chain of `if event == ...` arms; ours ships exactly the ONE arm that has a consumer
(the extra bars' login seed, below) and grows an arm at a time as consumers appear. A
transcription of the whole routing table would be a list of no-ops that reads as coverage.
Known seam, stated: the real client PARENTS every FrameXML frame under UIParent (scale/
visibility cascade). benilla is PART WAY there - 41 of our top-level frames declare
parent="UIParent" where the reference declares 104, and 32 frames the reference parents
(MainMenuBar, PlayerFrame, TargetFrame, the four MultiBars, UIErrorsFrame, the panels)
still attach to the screen root because the transcription dropped the attribute. Same
rect, so anchors agree.
The cascade consumer this note was waiting for HAS appeared (decision 1724): a cinematic
hides the HUD by doing `UIParent:Hide()` in the reference, and benilla substitutes an
engine-side draw-kill that also switches the mouse feed off - which is how the world came
to take every click of a race intro. Restoring the dropped parents is the fix; it is a
whole-UI arc (draw order, strata, ALT-Z, the world map's own full-screen row) and so is
proposed rather than taken.
-->
<Frame name="UIParent" setAllPoints="true" frameStrata="MEDIUM">
<Scripts>
<OnLoad>
this:RegisterEvent("PLAYER_ENTERING_WORLD")
this:RegisterEvent("READY_CHECK")
</OnLoad>
<OnEvent>UIParent_OnEvent(event)</OnEvent>
</Scripts>
</Frame>
<!--
WorldFrame — the reference's full-screen, bottom-of-the-strata frame that the 3D world
renders into. **Ours renders nothing**: benilla draws the world through Bevy's render
graph, not through a FrameScript frame, so this is a NAMED HANDLE and nothing more. That
is a disclosed divergence, not a pretence — and it is what makes the two things addons
actually do with it work: `WorldFrame:GetWidth()`/`GetHeight()` (the screen rect, which
this frame really has) and `frame:SetParent(WorldFrame)` / `SetPoint(..., WorldFrame,
...)` (parking a tooltip or an overlay under everything). Measured demand: 29 of the 218
vanilla-corpus addons die on their first load with `attempt to index global 'WorldFrame'`
without it (the corpus harness, `benilla_app::addon_harness`).
**No `enableMouse`, deliberately** — the same reason UIParent above ships without it, and
here it is load-bearing rather than incidental. benilla's `PointerOverUi` is "is the
topmost mouse-enabled frame under the cursor a UI frame", and it gates camera look, world
clicks and hover targeting (`player/camera.rs`, `target/hover.rs`); a full-screen
mouse-enabled plate would answer yes for every world pixel and kill all three. The
reference gets away with it because *its* WorldFrame IS the world, so a click landing
there is a world click by construction. Reproducing that here means teaching the arbiter
that a WorldFrame hit is not a UI hit — engine work with its own tests, not a rename — so
until then the addon-facing `OnMouseDown`/`OnMouseUp` hooks on this frame never fire.
-->
<Frame name="WorldFrame" setAllPoints="true" frameStrata="BACKGROUND"/>
<!--
The UIParent bottom-stack position manager - UIPARENT_MANAGED_FRAME_POSITIONS and
UIParent_ManageFramePositions(), transcribed from the extracted 1.12 UIParent.lua
l.1560-1697 (patch-2.MPQ via mpqx, byte-diffed against this install's own chain;
decision 0272). The reference never leaves a bottom-anchored HUD frame on its static
XML anchor: every layout pass re-anchors it to a base offset plus per-bar deltas -
either bottom multibar, the pet/stance bar, the reputation watch bar - fired from those
bars' OnShow/OnHide (ref BonusActionBarFrame.xml:350-355 et al.) and applied once at
startup. The loader's post-load call (ui_script/mod.rs) is our startup seam; the stance
bar's OnShow/OnHide (StanceBar.xml) is the one live re-fire today.
Departures, each named:
- Lua 5.1 dialect (the 1.12 client ran 5.0): pairs() iteration, table.insert for
tinsert, _G[name] = v for setglobal, and the ref's undeclared `hasBottomRight`
declared local (it is never read).
- The ref gates the bottom flags on its interface-option globals
SHOW_MULTI_ACTIONBAR_1/2; benilla's bottom bars are always-on with no options state
(decision 0270), so the flags read the bars' own visibility - identical values, no
parallel global to drift.
- Frame names: our stance bar, pet bar and bottom multibars wear the reference's own
names (ShapeshiftBarFrame, PetActionBarFrame, MultiBarBottomLeft/Right), so their
rows and flags in the table below are LIVE, not the no-ops they were while those
frames carried a `Benilla` prefix. That is the payoff the bare names buy — the same
one `group_loot_tests::managed_positions_engage_for_the_bare_frame_name` pins for
GroupLootFrame1 — and it costs nothing on screen: each frame's static XML anchor was
already written to the value the row computes (the stance bar's XML (30,45) is the
row's xOffset 30 + baseY 0 + bottomLeft 45, and MultiBarBottomLeft's XML (0,17) is
its row's baseY 17), so the pass now re-derives what the XML asserted. TutorialFrameParent
and FramerateLabel still have no frame here and no-op through the ref's `if ( frame )`.
- Every frame these flags read now exists, so every flag is a real read — the nil guards
stay because the ref reads them bare only because it always creates them, and our
per-window test harnesses load file SUBSETS. `reputation` fires the moment a watched
faction raises ReputationWatchBar over a shown MainMenuExpBar (decision 1258; it could
not fire at all until that page landed, and `reputation_frame_tests` pins the 9px it
moves the bottom stack by). `maxLevel` fires exactly when the level-60 rail replaces the
XP bar — which is also why the two never fire together: at 60 the watch bar REPLACES
MainMenuExpBar rather than stacking above it, so `MainMenuExpBar:IsShown()` is false and
the stack stays put. Both are the ref's own arithmetic.
- No IsUserPlaced/IsObjectType/SIMPLE_CHAT legs on the SetPoint: benilla has no
user-placed frames and no simple-chat option, so the ref's guarded SetPoint
collapses to the plain one.
- Of the ref function's trailing custom positioning, the pet-bar half IS ours (the
sliding shelf art + the PETACTIONBAR_XPOS/YPOS re-seat, at the end of the pass,
decision 0988). BattlefieldMinimapTab and the shapeshift-bar art swap stay out of
scope - neither frame exists here, our stance bar shipping no end-caps at all (0270).
- The isVar globals (CONTAINER_OFFSET_*, PETACTIONBAR_YPOS, BATTLEFIELD_TAB_OFFSET_Y)
are pre-declared to their base values below - the ref declares them in
ContainerFrame.lua/PetActionBarFrame.lua, which we don't ship - so the ref's
`if ( frame )` guard (getglobal on a var row yields the number) behaves identically.
Nothing consumes them yet; they are set for the day the bag anchors do.
-->
<Script><![CDATA[
-- Declared by the ref's ContainerFrame.lua / PetActionBarFrame.lua / WorldStateFrame.lua
-- (files we don't ship); the manage pass below overwrites them every run.
CONTAINER_OFFSET_X = 0
CONTAINER_OFFSET_Y = 70
BATTLEFIELD_TAB_OFFSET_Y = 210
PETACTIONBAR_YPOS = 97
-- ref PetActionBarFrame.lua:3. Not a managed row (the manage pass computes only the Y);
-- the custom block at the end of the pass recomputes it, for the reason recorded there.
PETACTIONBAR_XPOS = 36
-- ref UIParent.lua:2 — the model panes' held-button spin rate (half a turn per second),
-- read by every `Model_OnUpdate` transcription (the character window's + inspect's).
ROTATIONS_PER_SECOND = .5
-- ref SecondsToTime (UIParent.lua:1004-1031) and its GlobalStrings, in the ref's own home
-- file — two consumers now (the quest-timer window and the quest log's "Time Remaining:"
-- row, decision 1150), so it lives here rather than being redefined per file the way
-- BuffFrame.xml had to for SecondsToTimeAbbrev when it was the only caller.
--
-- Departures, both named:
-- - `GetText(TOKEN, nil, n)` is the ref's plural-aware GlobalString lookup, a C binding we
-- don't ship. Its enUS rule is the singular token for exactly 1 and the `_P1` twin
-- otherwise — the same rule already codified in `tooltip_item/render.rs` and MailFrame.xml
-- — so it collapses to the local `plural()` below and the four shipped string pairs
-- (GlobalStrings.lua:895-896, 2296-2297, 2693-2694, 3404-3405).
-- - Lua 5.1 dialect: `math.mod`/`math.floor` for the 5.0 bare `mod`/`floor`.
--
-- Everything else is verbatim, INCLUDING the two behaviours that look like bugs and are
-- not: the thresholds are strict `>` (86400 s reads as hours, not "1 Day"), and `count < 2`
-- stops after two units, so a 3-day quest reads "3 Days 4 Hrs" and never mentions minutes.
DAYS_ABBR = DAYS_ABBR or "Day"
DAYS_ABBR_P1 = DAYS_ABBR_P1 or "Days"
HOURS_ABBR = HOURS_ABBR or "Hr"
HOURS_ABBR_P1 = HOURS_ABBR_P1 or "Hrs"
MINUTES_ABBR = MINUTES_ABBR or "Min"
MINUTES_ABBR_P1 = MINUTES_ABBR_P1 or "Mins"
SECONDS_ABBR = SECONDS_ABBR or "Sec"
SECONDS_ABBR_P1 = SECONDS_ABBR_P1 or "Secs"
if not SecondsToTime then
local function plural(singular, pluralForm, n)
if n == 1 then return singular end
return pluralForm
end
function SecondsToTime(seconds, noSeconds)
local time = ""
local count = 0
local tempTime
seconds = math.floor(seconds)
if seconds > 86400 then
tempTime = math.floor(seconds / 86400)
time = tempTime .. " " .. plural(DAYS_ABBR, DAYS_ABBR_P1, tempTime) .. " "
seconds = math.mod(seconds, 86400)
count = count + 1
end
if seconds > 3600 then
tempTime = math.floor(seconds / 3600)
time = time .. tempTime .. " " .. plural(HOURS_ABBR, HOURS_ABBR_P1, tempTime) .. " "
seconds = math.mod(seconds, 3600)
count = count + 1
end
if count < 2 and seconds >= 60 then
tempTime = math.floor(seconds / 60)
time = time .. tempTime .. " " .. plural(MINUTES_ABBR, MINUTES_ABBR_P1, tempTime) .. " "
seconds = math.mod(seconds, 60)
count = count + 1
end
if count < 2 and seconds > 0 and not noSeconds then
time = time .. seconds .. " " .. plural(SECONDS_ABBR, SECONDS_ABBR_P1, seconds) .. " "
end
return time
end
end
-- ── The login seed for the extra action bars ──────────────────────────────────────────
--
-- The four extra bars' visibility is stored SERVER-SIDE, in `PLAYER_FIELD_BYTES` byte 2,
-- and the client never writes that byte itself — `SetActionBarToggles` only posts
-- `CMSG_SET_ACTIONBAR_TOGGLES` and waits for the field to come back (wow-re
-- `system/ui/scratch/action-bar-toggles.md` §5: the setter's every store is into its own
-- stack frame, and nothing in the binary fires on a change to that byte). So the live UI
-- state is the four Lua globals `MultiBars.xml` owns, and `GetActionBarToggles()` is read
-- exactly ONCE, here, to seed them. Reading it again later would fight the optimistic
-- value the options rows just set and lose a round trip's worth of clicks.
--
-- This is the reference's own seam, arm for arm (its UIParent_OnEvent does the same two
-- statements on the same event). `MultiActionBar_Update` is existence-guarded because the
-- ~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)
if ( event == "PLAYER_ENTERING_WORLD" ) then
SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3,
SHOW_MULTI_ACTIONBAR_4 = GetActionBarToggles()
if ( MultiActionBar_Update ) then
MultiActionBar_Update()
end
end
-- The ready-check popup's opener (decision 1549), ref UIParent.lua l.570-572. The
-- event is the engine's own `0x218`, fired by the `MSG_RAID_READY_CHECK` open
-- handler; the frame and `ShowReadyCheck` live in RaidFrame.xml, so this is the one
-- place the two meet. Existence-guarded for `MultiActionBar_Update`'s reason: the
-- per-window harnesses load this file without the raid pane.
if ( event == "READY_CHECK" ) then
if ( ShowReadyCheck ) then
ShowReadyCheck()
end
end
end
-- ── Everything that has to move when the bottom of the screen fills up ──────────────────
--
-- One row per thing whose seat depends on which bars are raised. A row reads:
--
-- baseY where it sits on the y axis with nothing in the way
-- baseX / xOffset the same for the x axis, under two names: the rows that write a var
-- spell it `baseX`, the rows that seat a frame spell it `xOffset`, and
-- baseX wins if a row ever names both
-- bottomLeft, bottomRight, bottomEither, pet, reputation, maxLevel (y axis)
-- rightLeft, rightRight (x axis)
-- what this row owes each raised bar, ADDED for every flag standing
-- anchorTo / point / rpoint who to hang off; defaulted to UIParent BOTTOM-to-BOTTOM
-- isVar the row is not a frame at all: it names a GLOBAL, and the pass writes
-- the computed offset into it for other files to read
--
-- The KEY is the global name — a frame to seat, or the var to write — and other windows'
-- OnShow/OnHide drive the pass by it, so both the keys and the fields are a published
-- shape. Written as one constructor rather than a dozen index assignments: it is a
-- constant, and a constant reads better as a table than as a program that builds one.
UIPARENT_MANAGED_FRAME_POSITIONS = {
-- Frames to seat
MultiBarBottomLeft = {baseY = 17, reputation = 9, maxLevel = -5, anchorTo = "ActionButton1", point = "BOTTOMLEFT", rpoint = "TOPLEFT"},
GroupLootFrame1 = {baseY = 60, bottomEither = 42, pet = 42, reputation = 9},
TutorialFrameParent = {baseY = 55, bottomEither = 47, pet = 42, reputation = 9},
FramerateLabel = {baseY = 64, bottomEither = 42, pet = 42, reputation = 9},
CastingBarFrame = {baseY = 60, bottomEither = 40, pet = 40, reputation = 9},
ChatFrame1 = {baseY = 85, bottomLeft = 17, pet = 17, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMLEFT", rpoint = "BOTTOMLEFT", xOffset = 32},
-- ChatFrame2 is NOT managed here, and its absence is the fix for the latent
-- decision 1499 flagged in the ClearAllPoints comment below. The reference's row
-- seats an UNDOCKED ChatFrame2 against the screen's bottom-right; ours is DOCKED to
-- ChatFrame1, and a docked frame takes its rect from the dock, never from this pass.
-- With the row present the pass cleared ChatFrame2's authored anchors and re-seated it
-- with a single BOTTOMRIGHT point and no size — an unresolvable rect, so the window
-- rendered nothing at all. That was invisible while ChatFrame2 had no tab; the moment
-- one existed (1575) it became "the Combat Log tab shows an empty screen".
-- Re-add this row the day windows can be undocked, which is chat *settings* (0288 §2).
ShapeshiftBarFrame = {baseY = 0, bottomLeft = 45, reputation = 9, maxLevel = -5, anchorTo = "MainMenuBar", point = "BOTTOMLEFT", rpoint = "TOPLEFT", xOffset = 30},
-- Globals to write
CONTAINER_OFFSET_X = {baseX = 0, rightLeft = 90, rightRight = 45, isVar = "xAxis"},
CONTAINER_OFFSET_Y = {baseY = 70, bottomEither = 27, bottomRight = 0, reputation = 9, isVar = "yAxis", pet = 23},
BATTLEFIELD_TAB_OFFSET_Y = {baseY = 210, bottomRight = 40, reputation = 9, isVar = "yAxis"},
PETACTIONBAR_YPOS = {baseY = 97, bottomLeft = 43, reputation = 9, maxLevel = -5, isVar = "yAxis"},
}
-- ── Consumers of the VAR rows ──────────────────────────────────────────────────────────
-- A row with `isVar` leaves a NUMBER in a global and anchors nothing, so whoever seats
-- itself from that number has to be told when the number changes — otherwise it keeps the
-- seat it computed the first time and the bars grow underneath it. That is not a
-- hypothetical: the open-bag stack read a file-local copy of CONTAINER_OFFSET_X/Y and sat
-- at the no-bars corner forever, so a bottom multibar drew straight through the lowest bag
-- window (director-caught, decision 1499).
--
-- The reference closes the same loop by hard-calling `updateContainerFrameAnchors()` from
-- the end of its own pass (UIParent.lua:1774). This is that call generalized: a consumer
-- registers itself and the pass calls it after every recompute, so the NEXT var consumer
-- does not have to edit this file to be noticed. Order of registration is the order of
-- call; listeners are independent (nothing here reads what another writes).
--
-- The law, stated once so it can be pointed at: **nothing outside this file may keep its
-- own copy of a managed offset.** Read the global fresh at use, and if you cache a SEAT
-- derived from it, register here. `bottom_hud_tests.rs` enforces both halves.
UIPARENT_MANAGED_POSITION_LISTENERS = {}
function UIParent_RegisterManagedPositionListener(fn)
table.insert(UIPARENT_MANAGED_POSITION_LISTENERS, fn)
end
function UIParent_ManageFramePositions()
-- Frames that affect offsets in y axis
local yOffsetFrames = {}
-- Frames that affect offsets in x axis
local xOffsetFrames = {}
-- Set up flags. Departure: the bottom flags read the always-on bars' visibility,
-- not the ref's SHOW_MULTI_ACTIONBAR_1/2 option globals (0270 ships no such state).
local bottomLeftShown = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
local bottomRightShown = MultiBarBottomRight and MultiBarBottomRight:IsShown()
if ( bottomLeftShown or bottomRightShown ) then
table.insert(yOffsetFrames, "bottomEither")
end
if ( bottomRightShown ) then
table.insert(yOffsetFrames, "bottomRight")
end
if ( bottomLeftShown ) then
table.insert(yOffsetFrames, "bottomLeft")
end
if ( MultiBarLeft and MultiBarLeft:IsShown() ) then
table.insert(xOffsetFrames, "rightLeft")
elseif ( MultiBarRight and MultiBarRight:IsShown() ) then
table.insert(xOffsetFrames, "rightRight")
end
if ( ( PetActionBarFrame and PetActionBarFrame:IsShown() ) or ( ShapeshiftBarFrame and ShapeshiftBarFrame:IsShown() ) ) then
table.insert(yOffsetFrames, "pet")
end
if ( ReputationWatchBar and MainMenuExpBar and ReputationWatchBar:IsShown() and MainMenuExpBar:IsShown() ) then
table.insert(yOffsetFrames, "reputation")
end
if ( MainMenuBarMaxLevelBar and MainMenuBarMaxLevelBar:IsShown() ) then
table.insert(yOffsetFrames, "maxLevel")
end
-- Walk the rows and settle each one against the flags now standing. Rows are
-- independent — nothing here reads a var another row writes — so the arbitrary order
-- `pairs` hands back is not a hazard.
for index, row in pairs(UIPARENT_MANAGED_FRAME_POSITIONS) do
local frame = getglobal(index)
if ( frame ) then
-- Start from where the row sits with nothing in its way. A zero base is a real
-- base, not an absent one, which is why `or` chains do the right thing here.
local xOffset = row.baseX or row.xOffset or 0
local yOffset = row.baseY or 0
-- Then add what the row owes each raised bar, keeping note of which ones it
-- actually paid — two of them interact below.
local paid = {}
for _, flag in pairs(yOffsetFrames) do
if ( row[flag] ) then
yOffset = yOffset + row[flag]
paid[flag] = true
end
end
for _, flag in pairs(xOffsetFrames) do
if ( row[flag] ) then
xOffset = xOffset + row[flag]
end
end
-- The bottom-left bar and the pet/stance row do not simply stack: clearing both
-- costs 23px more than the two figures added together. The reference carries the
-- same correction (l.1657-1658) with no derivation, and it is a fact about the
-- bars' art rather than arithmetic we can recompute.
if ( paid.bottomLeft and paid.pet ) then
yOffset = yOffset + 23
end
-- A row either NAMES A GLOBAL to leave the number in, or is a frame to seat.
if ( row.isVar == "xAxis" ) then
_G[index] = xOffset
elseif ( row.isVar ) then
_G[index] = yOffset
elseif ( frame.IsUserPlaced and frame:IsUserPlaced() ) then
-- **A frame the PLAYER placed is not this pass's to seat.** benilla's rule,
-- not the reference's: 1.12 has no *movable* managed frame — its ChatFrame1
-- is docked and its tab click refuses to drag a docked window — so the case
-- cannot arise there. Ours does, since an unlocked chat window drags and
-- resizes, and without this the next pass would `ClearAllPoints` the window
-- and drop it back on the bottom-stack seat. That is not a rare path: the
-- stance bar fires this on every show and hide.
--
-- `IsUserPlaced` is exactly the right question rather than a chat-specific
-- one: the engine sets that bit at the drag that moved the frame, and the
-- layout cache (`benilla_app::ui_layout`) sets it again for every frame it
-- seats out of the player's file — so "the player owns this frame's
-- position" and "do not manage it" are the same fact.
--
-- The `isVar` rows above are untouched: they write a NUMBER and seat no
-- frame, so there is no position for a player to own.
else
-- ClearAllPoints first: the row states the frame's WHOLE seat, so the pass
-- must own it outright. `SetPoint` only replaces the same-NAMED point, so
-- any other anchor the frame was authored with survives and fights this
-- one — ChatFrame2 is declared in XML with both a TOPLEFT and a
-- BOTTOMRIGHT onto ChatFrame1, and the row's BOTTOMRIGHT replaced only
-- the second, leaving the pane stretched from ChatFrame1's top-left
-- corner to the screen's bottom-right. Masked today only because
-- ChatFrame2 ships hidden, which is the worst kind of latent (decision
-- 1499). Every row here supplies a single complete anchor, so this is a
-- no-op for the other eight and the reference's plain SetPoint is
-- behaviour-identical on its own frames.
frame:ClearAllPoints()
frame:SetPoint(row.point or "BOTTOM", row.anchorTo or "UIParent",
row.rpoint or "BOTTOM", xOffset, yOffset)
end
end
end
-- Custom positioning the row table cannot express (ref l.1705-1738). Both rules
-- belong to the pet/stance ROW — the one row two different bars share — and without
-- them the pet bar lands on top of the bottom-left multibar's buttons
-- (director-caught: the bar drew across a row of spells, border and all).
--
-- (a) The shelf art IS the pet bar's border, and it only makes sense when the bar
-- sits directly on the main bar. Raised a row it would draw across whatever is
-- underneath, so the ref hides both strips (l.1707-1708) — the same move it makes
-- for the stance bar's own end-caps, which is why benilla's stance bar has none
-- (0270 anchors it statically at the raised position).
-- (b) The pet bar is the only managed entry whose row is a VAR: the loop above writes
-- the NUMBER PETACTIONBAR_YPOS (97, +43 with the bottom-left bar up) and anchors
-- nothing, so the ref re-seats the frame with it here (l.1735-1736).
--
-- Departure: the ref computes PETACTIONBAR_XPOS in ShowPetActionBar() and reads it
-- here, which only works because its bar slides in over several frames. Ours appears
-- at once, so the value is computed where it is used. Same rule
-- (PetActionBarFrame.lua:170-174): the two bars share this row, so a pet bar shown
-- while the stance bar is up starts 20px past the last stance button. That case is
-- rare but real — a possessed or charmed unit gives a pet bar to a class that also
-- has forms.
local bottomLeftUp = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
if ( SlidingActionBarTexture0 and SlidingActionBarTexture1 ) then
if ( bottomLeftUp ) then
SlidingActionBarTexture0:Hide()
SlidingActionBarTexture1:Hide()
else
SlidingActionBarTexture0:Show()
SlidingActionBarTexture1:Show()
end
end
if ( PetActionBarFrame ) then
PETACTIONBAR_XPOS = 36
local forms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
if ( forms > 0 and ShapeshiftBarFrame and ShapeshiftBarFrame:IsShown() ) then
local last = getglobal("ShapeshiftButton"..forms)
local right = last and last:GetRight()
if ( right ) then
PETACTIONBAR_XPOS = right + 20
end
end
-- The ref guards this on IsShown() because a hidden bar of its is mid-slide and
-- owns its own anchor; ours has no slide, so re-seating a hidden frame is a
-- no-op and the guard would only add an ordering trap (OnShow fires the pass).
PetActionBarFrame:SetPoint("TOPLEFT", "MainMenuBar", "BOTTOMLEFT", PETACTIONBAR_XPOS, PETACTIONBAR_YPOS)
end
-- The under-minimap right stack (ref l.1740-1770): anchorY walks down capture bars →
-- quest timer → durability → quest watch, each shown frame pushing the next one down
-- by its own height. benilla ships the quest-timer, durability and quest-watch arms
-- (below); the capture bars above them don't exist yet, so anchorY starts at the ref's
-- initial 0. The reference never leaves DurabilityFrame on its static XML anchor
-- (whose +40 hangs the side glyphs off-screen — director-caught): every pass seats it
-- at -CONTAINER_OFFSET_X, minus 20 more when a showSeparate glyph
-- (shield/off-weapon/ranged) extends the art to the right — then drops the quest
-- tracker to the running anchorY so the two never collide at the same corner.
local anchorY = 0
-- ref l.1753-1757 verbatim (decision 1150). Seated whether shown or not — the ref
-- SetPoints unconditionally and only the height accrual is gated — so the frame is
-- already in place the frame it appears. Nil-guarded like every row here: the
-- durability-only test harness ships no quest-timer frame.
if ( QuestTimerFrame ) then
QuestTimerFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X, anchorY)
if ( QuestTimerFrame:IsShown() ) then
anchorY = anchorY - QuestTimerFrame:GetHeight()
end
end
if ( DurabilityFrame ) then
local durabilityOffset = 0
if ( DurabilityShield:IsShown() or DurabilityOffWeapon:IsShown() or DurabilityRanged:IsShown() ) then
durabilityOffset = 20
end
DurabilityFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X-durabilityOffset, anchorY)
if ( DurabilityFrame:IsShown() ) then
anchorY = anchorY - DurabilityFrame:GetHeight()
end
end
-- QuestWatchFrame is last in the ref's walk (l.1770), seated at the running anchorY so
-- it drops below the durability guy when he shows instead of overlapping him (both
-- anchor this same MinimapCluster corner — director-caught). benilla's tracker is
-- QuestWatchFrame; nil-guarded because the durability-only test harness (and
-- any config without a quest log) ships no such frame.
if ( QuestWatchFrame ) then
QuestWatchFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X, anchorY)
end
-- Last: everything that seats itself from a var row this pass just rewrote. After the
-- frames, so a listener may measure one (the bag stack reads GetScreenHeight and its
-- own windows' heights, not another managed frame — but the ordering costs nothing and
-- removes a class of question).
for _, fn in ipairs(UIPARENT_MANAGED_POSITION_LISTENERS) do
fn()
end
end
-- The published binding-label API — benilla's implementation of the behaviour ref
-- UIParent.lua:1819-1878 specifies (decision 1008, which closed 0997's "abbreviates
-- through KEY_ABBR" residue with the REAL mechanism: there never was a KEY_ABBR set, the
-- whole abbreviation is this one function). Addons call it by name, so the contract is:
--
-- * a nil name reads as the empty string;
-- * "CTRL-SHIFT-2" splits at its LAST dash — up to and including that dash is the
-- modifier run ("CTRL-SHIFT-"), what follows is the key ("2"). A name with no dash is
-- all key;
-- * the key renders through the `prefix..key` global (the KEY_* family), preferring a
-- `_MAC` twin on a Mac client, and falls back to the raw key name;
-- * returnAbbr — what the action bar's HotKey corners pass — shrinks ONE modifier to
-- its initial, so SHIFT-2 reads "s-2" (the director's ask), and collapses TWO OR MORE
-- to "·". The full form (tooltips, the Keybindings page) keeps the whole prefix.
--
-- Two knowing departures from the reference, both about its code rather than its
-- behaviour: its `gsub(tempName, "CAPSLOCK", "Caps")` line is dead — gsub returns a new
-- string and that result is discarded, so nothing can observe it — and has no counterpart
-- here; and its GetLocale()/IsMacClient() calls ride behind existence guards because this
-- VM defines neither yet, with the getglobal fallback covering the gap.
-- What a single modifier shrinks to on a button corner. German calls Control "STRG" (the
-- deDE swap below runs first), which is the one that needs two letters to stay legible.
-- The order is fixed so the pass is deterministic, but it does not matter: no abbreviation
-- occurs inside another modifier's name, so no substitution can feed the next.
local MODIFIER_ABBREVIATIONS = {
{ "CTRL", "c" },
{ "SHIFT", "s" },
{ "ALT", "a" },
{ "STRG", "st" },
}
function GetBindingText(name, prefix, returnAbbr)
if not name then
return ""
end
local modifiers, key = "", name
local modifierCount = 0
-- The greedy `.*` walks to the LAST dash, which is the one that divides the modifier
-- run from the key; gsub's second return then counts the dashes, one per modifier —
-- plus one more when the key ITSELF is a dash ("CTRL--"), which is why that binding
-- abbreviates as if it had two. The reference's dash-walking loop reaches both of the
-- same answers, and both are pinned by keybindings_tests.
local _, lastDash = strfind(name, "^.*%-")
if lastDash then
modifiers = strsub(name, 1, lastDash)
key = strsub(name, lastDash + 1)
_, modifierCount = gsub(modifiers, "%-", "-")
if GetLocale and GetLocale() == "deDE" then
modifiers = gsub(modifiers, "CTRL", "STRG")
end
end
if returnAbbr then
if modifierCount > 1 then
-- Two modifiers never fit a corner: the whole label becomes the dot.
return "·"
end
for _, abbreviation in ipairs(MODIFIER_ABBREVIATIONS) do
modifiers = gsub(modifiers, abbreviation[1], abbreviation[2])
end
end
prefix = prefix or ""
local label
if IsMacClient and IsMacClient() then
label = getglobal(prefix .. key .. "_MAC")
end
label = label or getglobal(prefix .. key) or key
return modifiers .. label
end
-- ref UIParent.lua:1051-1057, transcribed 1:1 (decision 1062). The reference's own
-- list joiner, and it is exactly as plain as it looks: a bare ", " between every item,
-- with NO "and" before the last one and NO localization string of any kind — 1.12 has
-- no LIST_DELIMITER/AND global for it to reach for, and it does not build one. (The
-- richer `BuildColoredListString` right below it in the reference — the per-item
-- met/unmet red — already lives in TradeSkillFrame.xml as a guarded global, 0437 phase
-- 3; folding that one up here beside this is a follow-up, not this slice's churn.)
--
-- Callers: the pet paper doll's diet tooltip and PetStable.lua's, both as
-- `format(PET_DIET_TEMPLATE, BuildListString(GetPetFoodTypes()))`.
--
-- ZERO arguments returns NIL, not "" — the ref reads `arg[1]` off an empty vararg and
-- returns it unchanged. That is load-bearing: `format("%s", nil)` errors, so a caller
-- that can pass nothing (a warlock minion's empty diet) must be gated by its own UI, and
-- it is — the diet icon is hunter-only. Quietly returning "" here would hide that.
--
-- Lua-5.0 `arg`/`arg.n` written as `select("#", ...)`/`select(i, ...)`, the 5.1-native
-- spelling — TradeSkillFrame.xml's own substitution.
function BuildListString(...)
local n = select("#", ...);
if ( n == 0 ) then
return nil;
end
local list = select(1, ...);
for i = 2, n do
local item = select(i, ...);
list = list..", "..item;
end
return list;
end
-- BenillaChatEdit_InsertLink(link) → true if it went in, else nil (decision 1059).
--
-- Every shift-click site in this UI does the same two things before inserting: check the
-- chat edit box is up, and — ours alone — check the link is not nil. That nil case is
-- **benilla's, not the reference's**: our item templates resolve asynchronously
-- (`CMSG_ITEM_QUERY_SINGLE`, answered a frame or many later), so a `Get…Link` getter
-- legitimately answers nil for a slot whose name and quality have not landed, where the real
-- client's synchronous item cache always has one. `EditBox:Insert` takes a string (the
-- byte-modelled `0x77bee0`), so an unguarded nil is a script error — which is exactly the
-- bug this function exists to make impossible to reintroduce at the next site. A dozen call
-- sites were growing a dozen copies of the same two-line guard, and three of them shipped
-- without it.
--
-- The RETURN is what lets a caller fork on "did chat take it?" — a bag slot's shift-click
-- posts the link when chat is open and opens the stack splitter when it is not (ref
-- ContainerFrame.lua:567-577). Blizzard themselves eventually factored this exact helper out
-- for this exact reason: 2.0's `ChatEdit_InsertLink(text)`, returning true/false, called from
-- every one of these sites. 1.12 has no such function, so ours carries a benilla name rather
-- than pretending to be reference API.
function BenillaChatEdit_InsertLink(link)
if not link then
return nil
end
if not ChatFrameEditBox or not ChatFrameEditBox:IsVisible() then
return nil
end
ChatFrameEditBox:Insert(link)
return true
end
-- ── LowerFrameLevel / RaiseFrameLevel (ref UIParent.lua l.1890-1896), verbatim ────────
--
-- Two lines each, and the reason to have them is that they are FRAMEXML, not engine: an
-- addon calling `RaiseFrameLevel(f)` is calling a function the client's own UI defines, and
-- without it that is `attempt to call global` at load (BetterCharacterStats dies there).
-- Our own ActionBar.xml already describes this operation in prose at three sites while
-- doing it by hand; those can move onto the real name whenever that file is next opened.
function LowerFrameLevel(frame)
frame:SetFrameLevel(frame:GetFrameLevel() - 1)
end
function RaiseFrameLevel(frame)
frame:SetFrameLevel(frame:GetFrameLevel() + 1)
end
-- ── The 1024x768 scale ratios (ref UIParent.lua l.1794-1802) ────────────────────────────
--
-- Two FrameXML helpers addons call by name (the raid pullouts and a dozen third-party
-- drag handlers), so they exist for the reason `MouseIsOver` and `RaiseFrameLevel` do:
-- missing, the caller dies on `attempt to call global`. The reference's constants are the
-- 1024x768 reference resolution, and its own callers multiply a `GetCursorPosition()`
-- reading by them to convert into pixel space.
--
-- **benilla's own drag code does NOT use them**, and that is deliberate rather than an
-- oversight: our UI space is 1:1 logical px and `GetCursorPosition` already reports in
-- the same space `SetPoint` offsets are read in (the same identity `MouseIsOver`'s
-- comment below records for `GetEffectiveScale`). Multiplying here would place a dragged
-- frame at 1.5x the cursor on a 1600-wide window. Kept for the addons, unused by us.
function GetScreenHeightScale()
return GetScreenHeight() / 768
end
function GetScreenWidthScale()
return GetScreenWidth() / 1024
end
-- ── MouseIsOver: is the cursor inside this frame, with optional per-edge padding ─────────
--
-- The idiom every hover-driven addon writes, and **9 corpus addons call it** by name.
-- Missing, each is `attempt to call global 'MouseIsOver'`. So the CONTRACT is 1.12's, quirks
-- and all (ref UIParent.lua l.1388-1417) — three of which look like bugs and are not:
--
-- · each offset is ADDED to its own edge, in that axis's positive direction. A positive
-- `topOffset` raises the ceiling (growing the box upward) while a positive
-- `bottomOffset` raises the floor (shrinking it from below). The reference's own caller
-- proves the intent: `MouseIsOver(chatFrame, 45, -10, -5, 5)` is 45 up into the tab
-- strip, 10 in from the bottom, 5 out each side.
-- · the padding is ALL OR NOTHING on `topOffset`: pass none and the other three are
-- dropped with it, so `MouseIsOver(f, nil, 5)` silently ignores the 5. Pass a topOffset
-- and omit any of the rest and it is an arithmetic error, not a zero — 1.12 raises
-- there too, and an addon written against it may lean on either half.
-- · the bounds are strict, so a cursor exactly on an edge is outside.
--
-- A frame that has never been laid out answers nil for its rect; nil is the right answer to
-- hand back for it, and the reference's own comment on that guard ("a symptom not the real
-- issue") is about where the unresolved frame came from, not about the guard.
--
-- ONE DIVERGENCE, and it is a no-op: the reference divides the cursor by
-- `frame:GetEffectiveScale()` because 1.12 hands Lua screen pixels. benilla's UI space is
-- 1:1 logical px and `GetEffectiveScale` is the constant 1 (`script/pointer.rs`,
-- `object.rs` l.271), so the division is written out rather than left in to divide by one
-- forever. If a real UI scale ever lands, this is one of the sites that needs it back.
function MouseIsOver(frame, topOffset, bottomOffset, leftOffset, rightOffset)
local x, y = GetCursorPosition()
local left, right = frame:GetLeft(), frame:GetRight()
local top, bottom = frame:GetTop(), frame:GetBottom()
if not left then
return
end
if not topOffset then
topOffset, bottomOffset, leftOffset, rightOffset = 0, 0, 0, 0
end
-- All four edges are padded before any of them is tested, and deliberately so: folding
-- the additions into the comparison would let a short circuit swallow the arithmetic
-- error that a half-supplied offset set is supposed to raise.
left, right = left + leftOffset, right + rightOffset
top, bottom = top + topOffset, bottom + bottomOffset
if x > left and x < right and y > bottom and y < top then
return 1
end
return nil
end
]]></Script>
</Ui>