the pet gets its frame, flooded rooms fill, and macros really fire

- the pet gets its own unit frame beside yours, and an Attack order no longer latches as the standing command
- five rooms that always read bone dry hold their water now, the Deeprun Tram's flooded stretches among them
- macros fire through the client's own chat door, and the macro window fits its tabs and scrolls its icon chooser
- the options window grows a Nameplates page and its first dropdown, and the name-display switches apply live in the world

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-05 19:03:43 +07:00
co-authored by Claude Fable 5
parent 77dc8f8ead
commit 3b6975fdef
542 changed files with 4320 additions and 1555 deletions
Generated
+14 -7
View File
@@ -570,6 +570,20 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "benilla"
version = "0.1.0"
dependencies = [
"benilla-app",
]
[[package]]
name = "benilla-adt"
version = "0.1.0"
dependencies = [
"benilla-bytes",
]
[[package]]
name = "benilla-app"
version = "0.1.0"
dependencies = [
"anyhow",
"arboard",
@@ -599,13 +613,6 @@ dependencies = [
"toml",
]
[[package]]
name = "benilla-adt"
version = "0.1.0"
dependencies = [
"benilla-bytes",
]
[[package]]
name = "benilla-assets"
version = "0.1.0"
+110
View File
@@ -0,0 +1,110 @@
[package]
name = "benilla-app"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "The Bevy app as a library: everything but the build-id stamp, which lives in the `benilla` launcher shim so a commit dirties ~30 lines instead of this crate (decision 0993)"
[features]
# Opt-in Tracy profiling: `cargo run --features tracy` streams per-system `info_span!` markers (on the
# hot systems) + render-pass CPU timings to a Tracy capture (decision 0026 — the dev/player seam;
# a `docs/howto/running.md` this once cited never landed).
tracy = ["bevy/trace_tracy"]
# Opt-in Chrome-trace profiling: `cargo run --features trace_chrome` writes a `trace-<ts>.json` of
# every system's per-run span next to the working dir. Tracy's offline twin — it needs no GUI and no
# live capture, so a per-system cost question is answerable from a terminal (and from a run someone
# else recorded). Aggregate it with `scripts/tracesum.py`.
trace_chrome = ["bevy/trace_chrome"]
[dependencies]
anyhow.workspace = true
# OS clipboard access for the UI EditBox copy/cut/paste chords (chat input). Already in the tree via
# bevy_egui; pinned to the same 3.x so there's one copy. Read on the main thread (the UI key feed is
# a NonSend system) — required for the macOS NSPasteboard path. Covers macOS, Windows, and Linux/BSD
# under X11; the Wayland backend is `smithay-clipboard` below (decision 0702).
arboard = "3"
benilla-ui = { path = "../benilla-ui" }
bevy.workspace = true
bevy_egui = "0.39"
# Zero-copy POD→bytes for the shared light storage buffer (one per-frame write_buffer). Already in
# the tree transitively; pinned to the same version as the dev-dep below.
bytemuck = { version = "1", features = ["derive"] }
# `f16` decode for the booth-target dump (`WOW_BOOTH_DUMP`): the booth render targets are
# `Rgba16Float` (decision 0804) and the PNG wants display bytes. Already in the tree via
# wgpu/bevy_image; declared so the call site is an honest dependency.
half = "2"
# `getrusage` — whole-process CPU time (all threads), the perf probes' `cpu_ms`/`cpu_pct` metric
# (`perf::process_cpu_secs`). Wall-clock fps is unusable as a regression instrument on a machine
# running parallel sessions' builds; CPU-per-frame is, and it is also the number the Mac report
# is about ("250 % CPU at 59 fps"). Already in the tree transitively; declared so the call site is
# an honest dependency.
libc = "0.2"
# Resample varying-size layer BLPs to a uniform size for the terrain texture array; `png` so we can
# encode the cursor image into data an NSImage can load (macOS hardware cursor).
image = { version = "0.25", default-features = false, features = ["png"] }
# The net↔ECS bridge: a background IO thread streams typed `SessionEvent`s to the ECS over a
# channel, and outbound `ClientCommand`s back. crossbeam's Sender/Receiver are `Send + Sync + Clone`
# (std's mpsc Sender is `!Sync`, so it can't be a Bevy resource without a mutex — the very thing this
# bridge deletes). The canonical Bevy background-thread→channel→system idiom. See decision 0006.
crossbeam-channel = "0.5"
benilla-assets = { path = "../benilla-assets" }
benilla-formats = { path = "../benilla-formats" }
benilla-protocol = { path = "../benilla-protocol" }
# The CVar config diff (`benilla/config.toml`, decision 0954): serde derive on the persisted
# shape, toml as its format. (serde_json left with the era manifest — 0978.)
serde = { version = "1", features = ["derive"] }
toml = "0.8"
# ECS-native physics: collider storage + broadphase BVH + shape-casts, with a thin custom kinematic
# character controller on top (decision 0009). Pinned 0.6: it targets Bevy 0.18 (verified single,
# unified bevy 0.18.1 in the tree); 0.7.0 jumps to Bevy 0.19 and would duplicate bevy. avian wraps
# parry under the hood, so adopting it lets us drop the *direct* parry3d dep once the bespoke
# CollisionWorld is gone. See docs… (decision 0007 superseded by 0009).
avian3d = "0.6"
# The glyph shaper + rasterizer for `FontString` text (decision 0068 §2, probe-verified in
# `probes/text-glyph`). Pinned to 0.16 — the exact version already in the tree transitively
# (`bevy_text`'s own dependency, see `Cargo.lock`) — so this is the *same* `cosmic-text` the decision
# already commits to, not a second copy. Used as a standalone shaper/rasterizer (`FontSystem` +
# `Buffer` + `SwashCache`) in `ui_text.rs`, never `bevy_text`'s `Text2d`/ECS integration (that path
# doesn't feed our custom z-ordered `UiQuad` stream).
cosmic-text = "0.16"
# The font database `cosmic-text` 0.16 uses (its `Source`/`ID`/`FaceInfo` appear in `ui_text.rs`'s
# public-in-crate types). Pinned to the exact version `cosmic-text` 0.16 depends on so there's one
# `fontdb` in the graph.
fontdb = "0.23"
kira = { version = "0.12", default-features = false, features = ["cpal", "mp3", "wav", "pcm", "adpcm"] }
mint = "0.5"
# The gossip greeting is a weighted random DRAW per menu-open, not a fixed pick (wow-re
# `gossip-npctext-law.md` — the reference re-rolls in its gossip handler), so the app needs a uniform
# source. Pinned to 0.8: the version `benilla-srp` already depends on, so this adds no crate.
rand = "0.8"
# Linux/BSD: the Wayland clipboard (decision 0702). We build with bevy's `wayland` feature, so on a
# Wayland session winit gives us a native Wayland window and `arboard` — X11-only without its
# non-default `wayland-data-control` feature — finds no X11 CLIPBOARD owner and every paste fails.
# smithay-clipboard is the right crate for a windowed app: it drives the *standard* `wl_data_device`
# off our own `wl_display`/seat, which every compositor supports. arboard's own Wayland feature would
# not do: it speaks `wlr-data-control`, a privileged manager protocol GNOME/Mutter deliberately
# doesn't implement. `raw-window-handle` (pinned to the 0.6 already in the tree via bevy_window, so
# this adds no crate) is only needed to read the `wl_display` out of bevy's `RawHandleWrapper`.
[target.'cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))'.dependencies]
raw-window-handle = "0.6"
smithay-clipboard = "0.7"
# macOS: drive a native NSCursor for a crisp hardware cursor. winit drives the cursor via AppKit
# cursor-rects, which our continuous Metal render loop doesn't honor on mouse-move (custom cursor /
# hide revert to the arrow); we bypass it with NSCursor + NSWindow::disableCursorRects. See
# docs/knowledge/camera-control.md.
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.5"
objc2-app-kit = { version = "0.2", features = [
"NSApplication",
"NSResponder",
# NSApplicationActivationPolicy (bgwin's Accessory demotion) is declared in this header.
"NSRunningApplication",
"NSWindow",
"NSView",
"NSImage",
"NSCursor",
"NSEvent",
] }
objc2-foundation = { version = "0.2", features = ["NSArray", "NSData", "NSGeometry"] }
@@ -542,6 +542,23 @@
</Button>
</Frames>
<Scripts>
<!-- EXECUTE_CHAT_LINE — how a MACRO line reaches the chat grammar (0996, wow-re
`macro-execution-law.md` §4, VERIFIED). The engine's macro runner (`0x4f14e0`)
names no Lua function and walks no command table: per body line it fires
`FrameScript_SignalEvent(0x188, "%s", line)`, and `0x188` is EXECUTE_CHAT_LINE
(the registry slot `0xbe17b8` is written once, at `0x51b4ff`, with `0x852470`).
ChatFrame.lua:1343 is the handler — `editBox:SetText(arg1)` → `ChatEdit_SendText`
→ `ChatEdit_OnEscapePressed` — and ChatFrame1's OnLoad
(FloatingChatFrame.xml:700) is its ONLY registrant in the whole default UI, so
every macro line runs through THIS frame's box whichever one has focus. benilla's
SubmitChatInput stands in for ChatEdit_SendText: the grammar behind it is Rust
(0881), which is the only part of that chain we replace. -->
<OnLoad>
this:RegisterEvent("EXECUTE_CHAT_LINE")
</OnLoad>
<OnEvent>
if event == "EXECUTE_CHAT_LINE" then SubmitChatInput(arg1) end
</OnEvent>
<OnUpdate>
BenillaFCF_OnUpdate(arg1)
BenillaChatFrame_OnUpdate(this, arg1)
@@ -35,10 +35,13 @@
(`BenillaMacroFrameText_Resize`), over-estimating wrapped lines rather than clipping.
- `MacroFrameTextButton` (the ref's invisible click-to-focus plate over the body) is kept
— this engine's EditBox does not self-focus on a click through a ScrollFrame.
- The ref's `PanelTemplates_TabResize(-15, ...)` width tuning and its manual
`HighlightTexture:SetWidth(GetTextWidth() + 31)` are dropped: `TabButtonTemplate`
(UiPanels.xml) already does its own text-fit on load, and re-doing it here would fight
it (the same call the trainer/talent tabs already omit).
- The ref's `PanelTemplates_TabResize(-15, ...)` width tuning is KEPT, but expressed as
two fields on the tab (`benillaTabPadding`/`benillaTabMaxWidth`) rather than as a call:
`TabButtonTemplate` (UiPanels.xml) owns the one text-fit call site, and an instance
calling TabResize itself would just be overwritten on the next settle. Dropping the
tuning (0983's original call) is what let the character tab run off the window's right
edge — 384 units does not hold two +0-fit tabs. The ref's manual
`HighlightTexture:SetWidth(GetTextWidth() + 31)` stays folded into that same settle.
- `PlaySound("igCharacterInfoOpen"/"Close"/"gsTitleOptionOK")` are kept — this engine has
PlaySound and those kit names resolve.
-->
@@ -704,6 +707,10 @@
<Button name="BenillaMacroFrameTab1" inherits="TabButtonTemplate" text="GENERAL_MACROS" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="65" y="-39"/></Offset></Anchor></Anchors>
<Scripts>
<!-- The ref's own `PanelTemplates_TabResize(-15)` (l.488) as a fit parameter —
the template's settle loop owns the one call site (UiPanels.xml). -15 is
what keeps a two-tab row inside a 384-wide window. -->
<OnLoad>self.benillaTabPadding = -15</OnLoad>
<OnClick>
PanelTemplates_SetTab(getglobal("BenillaMacroFrame"), self:GetID())
BenillaMacroFrame_SaveMacro()
@@ -715,7 +722,14 @@
<Button name="BenillaMacroFrameTab2" inherits="TabButtonTemplate" id="2">
<Anchors><Anchor point="LEFT" relativeTo="BenillaMacroFrameTab1" relativePoint="RIGHT"/></Anchors>
<Scripts>
<OnLoad>self:SetText(BenillaMacroFrame_TabTwoLabel())</OnLoad>
<!-- ref l.511: `PanelTemplates_TabResize(-15, nil, nil, 150)`. The cap is
load-bearing here and nowhere else — this label carries a player name, so
it is the one tab whose width is not known when the window is written. -->
<OnLoad>
self.benillaTabPadding = -15
self.benillaTabMaxWidth = 150
self:SetText(BenillaMacroFrame_TabTwoLabel())
</OnLoad>
<OnClick>
PanelTemplates_SetTab(getglobal("BenillaMacroFrame"), self:GetID())
BenillaMacroFrame_SaveMacro()
@@ -865,10 +879,19 @@
</Scripts>
</EditBox>
<!-- The icon grid's faux scroll (ref a ClassTrainerListScrollFrameTemplate). -->
<!-- The icon grid's faux scroll (ref a ClassTrainerListScrollFrameTemplate, l.759-775).
The frame itself is invisible; its ONE visible consequence is where the bar seats
(BenillaFauxScrollFrameTemplate hangs it off the frame's right edge +6, inset 16
top and bottom). So its rect is the reference's VERBATIM — 296x195 at TOPRIGHT
(-39,-67) — which puts the bar at popup x 264..280, on the plate's groove and
bracketing the 4-row grid. 0983 sized it to the grid instead (252x152 at TOPLEFT
24,-85) and the bar came out at x 282..298: hanging half off the plate's right
edge, over the world. -->
<Frame name="BenillaMacroPopupScrollFrame" inherits="BenillaFauxScrollFrameTemplate">
<Size><AbsDimension x="252" y="152"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="24" y="-85"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="296" y="195"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-39" y="-67"/></Offset></Anchor>
</Anchors>
<Scripts>
<!-- The faux kit's contract (ScrollTemplates.xml): the owner sets updateFunc,
the shared Slider handler calls it whenever the row offset moves. -->
@@ -1,7 +1,8 @@
<Ui>
<!--
The OPTIONS window (decision 0950) — the shell (slice A) and the real PAGES: Audio
(0957), Graphics (0959), Controls (0961). Opened from the game menu's Options button.
(0957), Graphics (0959), Controls (0961), Nameplates (0992, with the dropdown row
shape). Opened from the game menu's Options button.
Setting rows ride the CVar store (0954): rows read GetCVar on show, write SetCVar on
interaction, and Defaults resets the visible page's CVars to their registered defaults.
Pages without rows are still empty and keep Defaults disabled. One row defers — uiScale
@@ -134,8 +135,8 @@
Apply commits outright.
- Era rows this slice does not ship: the Accessibility group, and Languages/Network under
System — pages with no benilla knobs behind them yet (0950's "only rows backed by real
knobs" rule). Nameplates joined in slice B (real nameplate knobs exist to back its
page).
knobs" rule). Nameplates joined the tree in slice B on the promise of its knobs; the
page went LIVE in 0992 (the three UnitName* checkbox rows).
- **The window rides a scale knob** (slice B): fonts ride frame scale engine-wide now
(the 0219 §2 divergence closed), so ERA_WINDOW_SCALE × the era checkFit shrink
(UIPanelUpdateScaleForFit, transcribed in this file) sizes the whole window; 0.78 is
@@ -465,7 +466,8 @@
"RowEnableAll", "RowMaster", "RowSound", "RowMusic", "RowAmbience",
"RowEnableMusic", "RowEnableAmbience",
},
Graphics = { "RowUiScale" },
Graphics = { "RowUiScale", "RowWorldDetail" },
Nameplates = { "RowPlayerNames", "RowNpcNames", "RowOwnName" },
}
function OptionsRow_OnLoad(row, cvar, label, child)
@@ -500,6 +502,79 @@
row.refreshing = nil
end
-- ── The dropdown rows (0992): the 1.12 kit under the era row grammar ────────────────
-- A dropdown row's choices: entries = { {text=..., value=...}, ... }, values the CVar's
-- own strings. The kit does the rest: every open re-runs the init through the stored
-- initialize (Toggle's own law), and AddButton auto-checks the entry matching the
-- capsule's selectedValue — which OptionsDropdown_ShowValue keeps equal to the CVar.
function OptionsDropdownRow_Setup(row, entries)
row.entries = entries
local dd = getglobal(row:GetName() .. "Dropdown")
UIDropDownMenu_SetWidth(180, dd)
UIDropDownMenu_Initialize(dd, OptionsDropdownRow_InitMenu)
-- The capsule's toggle button is a mouse-enabled child — it steals hover from the
-- row, so it carries the row wash like the checkbox rows' box does. Hooked here
-- (SetScript) because the capsule template is the shared kit's, not this file's.
local btn = getglobal(dd:GetName() .. "Button")
btn:SetScript("OnEnter", function() OptionsRow_Hover(this:GetParent():GetParent(), 1) end)
btn:SetScript("OnLeave", function() OptionsRow_Hover(this:GetParent():GetParent(), nil) end)
end
function OptionsDropdownRow_InitMenu()
local dd = getglobal(UIDROPDOWNMENU_INIT_MENU)
local row = dd:GetParent()
for _, e in ipairs(row.entries) do
local info = {}
info.text = e.text
info.value = e.value
info.func = OptionsDropdownEntry_OnClick
UIDropDownMenu_AddButton(info)
end
end
-- The entry click (`this` = the list row, the kit's own calling law): the write, then
-- the capsule redisplay. The kit closes the list and plays its own click sound.
function OptionsDropdownEntry_OnClick()
local dd = UIDropDownMenu_GetCurrentDropDown()
local row = dd:GetParent()
SetCVar(row.cvar, this.value)
OptionsDropdown_ShowValue(row)
end
-- Paint the capsule from the CVar. selectedValue is written as a FIELD, not through
-- SetSelectedValue: that helper's Refresh walks the SHARED DropDownList1 buttons, which
-- may still hold another dropdown's rows on this refresh path (the list is closed) —
-- a foreign row matching by value would repaint our capsule with its text. An off-grid
-- CVar value (an env A/B, a hand edit) displays the NEAREST stop — 0959's out-of-range
-- law, dropdown-flavored: control and check agree on the nearest expressible value,
-- the engine keeps running the config's truth, nothing writes back until the player
-- actually picks an entry. (A non-numeric stray shows raw and checks nothing.)
function OptionsDropdown_ShowValue(row)
local dd = getglobal(row:GetName() .. "Dropdown")
local v = GetCVar(row.cvar)
local match
for _, e in ipairs(row.entries) do
if e.value == v then match = e end
end
if not match and tonumber(v) then
local best
for _, e in ipairs(row.entries) do
local d = tonumber(e.value) and math.abs(tonumber(e.value) - tonumber(v))
if d and (not best or d < best) then
best = d
match = e
end
end
end
if match then
dd.selectedValue = match.value
UIDropDownMenu_SetText(match.text, dd)
else
dd.selectedValue = v
UIDropDownMenu_SetText(v, dd)
end
end
-- The right-hand readout text (era MinimalSlider's RIGHT label seat): rounded percent
-- (era FormatPercentage, Audio.lua l.406-411) unless the row asked for the raw number.
function OptionsRow_FormatValue(row, value)
@@ -606,6 +681,8 @@
else
box:SetChecked(GetCVar(row.cvar))
end
elseif row.entries then
OptionsDropdown_ShowValue(row)
else
local slider = getglobal(base .. rkey .. "ControlSlider")
row.refreshing = 1
@@ -968,6 +1045,50 @@
</Scripts>
</Frame>
<!-- One SETTING row, dropdown flavor (0992 — the first): the same era 26-tall row and
label box; the control is the 1.12 dropdown CAPSULE verbatim (UIDropDownMenuTemplate —
the kit every 1.12 dropdown wears, UIDropDownMenu.xml), seated so its box ink opens at
the era control column (the capsule's Left cap carries ~10 units of air before its
ink, hence the -71 anchor against the sliders' CENTER-80 + 19 groove seat; width 180
makes the ink span read as the slider track's). SHAPE provenance: the era's own
control for an enumerated setting is a dropdown (SettingsDropdownControlTemplate — its
Graphics page is a zoo of them), while 1.12 shaped this row's setting as a 3-stop
slider; the era grammar wins the shape by the director's call ("some with dropdown"),
recorded in 0992. The list the capsule opens rides the kit's own law — shared
DropDownList1, FULLSCREEN_DIALOG strata (above this HIGH window), the owner's
effective scale (the kit's 0992 correction), the 2-second hover-away close. -->
<Frame name="OptionsDropdownRowTemplate" virtual="true" enableMouse="true">
<Size><AbsDimension y="26"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentHover" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
</Anchors>
<Color r="1" g="1" b="1" a="0.1"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentLabel" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="37" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativePoint="CENTER"><Offset><AbsDimension x="-85" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="$parentDropdown" inherits="UIDropDownMenuTemplate">
<Anchors><Anchor point="LEFT" relativePoint="CENTER"><Offset><AbsDimension x="-71" y="-2"/></Offset></Anchor></Anchors>
</Frame>
</Frames>
<Scripts>
<OnEnter>OptionsRow_Hover(this, 1)</OnEnter>
<OnLeave>OptionsRow_Hover(this, nil)</OnLeave>
</Scripts>
</Frame>
<Frame name="OptionsFrame" frameStrata="HIGH" enableMouse="true" hidden="true">
<Size><AbsDimension x="920" y="724"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
@@ -1383,14 +1504,17 @@
</Frames>
</Frame>
<!-- The Graphics page (0959; farclip row retired 0961 — the
director's call: the page holds UI Scale alone for now, the
farclip CVar/knob/env/debug-panel plumbing all stay). Range/
step/label are the 1.12 OptionsFrameSliders table verbatim
director's call, standing: the farclip CVar/knob/env/
debug-panel plumbing all stay, only the row is off the page).
UI Scale is the 1.12 OptionsFrameSliders table verbatim
(OptionsFrame.lua l.25): uiscale 0.64..1.0 step 0.01 (UI_SCALE
"UI Scale", percent readout — era's own
FormatPercentageRounded on its uiscale row). 1.12's useUiScale
gate has no benilla knob (our scale always applies; off ≡ 1.0)
so the checkbox doesn't ship. -->
so the checkbox doesn't ship. Environment Detail (0992) is the
same table's WorldDetail row (l.26: 0..2 step 1, label
ENVIRONMENT_DETAIL) over the clutter-density knob, worn as the
dropdown row — the shape call is the template's comment. -->
<Frame name="$parentGraphics" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
@@ -1412,6 +1536,58 @@
self.deferred = 1
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowWorldDetail" inherits="OptionsDropdownRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowUiScale" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowUiScale" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "WorldDetail", "Environment Detail")
-- The stop names are the era preset ladder's (its
-- quality dropdowns say Low/Medium/High); 1.12's
-- slider had no stop labels to quote.
OptionsDropdownRow_Setup(self, {
{ text = "Low", value = "0" },
{ text = "Medium", value = "1" },
{ text = "High", value = "2" },
})
</OnLoad></Scripts>
</Frame>
</Frames>
</Frame>
<!-- The Nameplates page (0992 — the page 0952 promised when the
category row joined): the three 1.12 UnitName* checkboxes over
the overhead-name gates (nameplates.rs NameConfig), rows in
the 1.12 Interface Options' own index order (21/30/67), labels
its GlobalStrings verbatim (SHOW_PLAYER_NAMES/SHOW_NPC_NAMES/
SHOW_OWN_NAME). -->
<Frame name="$parentNameplates" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Frame name="$parentRowPlayerNames" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-12"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-20" y="-12"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "UnitNamePlayer", "Player Names")</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowNpcNames" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowPlayerNames" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowPlayerNames" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "UnitNameNPC", "NPC Names")</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowOwnName" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowNpcNames" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowNpcNames" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "UnitNameOwn", "Show Own Name")</OnLoad></Scripts>
</Frame>
</Frames>
</Frame>
<!-- The search-results group headers — one per page WITH rows
@@ -1437,6 +1613,12 @@
getglobal(self:GetName() .. "Label"):SetText("Graphics")
</OnLoad></Scripts>
</Button>
<Button name="$parentSearchHeadNameplates" inherits="OptionsSearchHeadTemplate">
<Scripts><OnLoad>
self.categoryKey = "Nameplates"
getglobal(self:GetName() .. "Label"):SetText("Nameplates")
</OnLoad></Scripts>
</Button>
</Frames>
</Frame>
</Frames>
@@ -85,7 +85,7 @@
merge and calls `SetScript` for each handler in order; `merge()` (`framexml.rs`) appends the
base template's children before the instance's own, so the base's `<Scripts>` registers first
and this file's own registers LAST — `SetScript` is a single-slot assignment, so the last
registration wins outright. `BenillaFauxScrollBar_UpdateArrows`/`_Step` (ScrollTemplates.xml)
registration wins outright. `BenillaScrollBar_UpdateArrows`/`_Step` (ScrollTemplates.xml)
are reused as-is: both read only a Slider's own value/range/step, nothing faux-specific.
Named deferrals/OMITs (each re-cited at its own site below):
@@ -811,11 +811,11 @@
-- The real-ScrollFrame half of the documented Slider<->ScrollFrame wiring (`slider.rs`'s
-- own header comment: "OnValueChanged -> this:GetParent():SetVerticalScroll(arg1)") —
-- REPLACES BenillaScrollBarTemplate's inherited faux-offset handler (this file's header
-- comment, verified via loader/scripts.rs). BenillaFauxScrollBar_UpdateArrows is faux-
-- comment, verified via loader/scripts.rs). BenillaScrollBar_UpdateArrows is faux-
-- agnostic (ScrollTemplates.xml's own body) and reused as-is for the arrow enable state.
function BenillaTalentFrameScrollBar_OnValueChanged(scrollBar, value)
scrollBar:GetParent():SetVerticalScroll(value)
BenillaFauxScrollBar_UpdateArrows(scrollBar)
BenillaScrollBar_UpdateArrows(scrollBar)
end
]]></Script>
@@ -1178,8 +1178,8 @@
<OnLoad>BenillaTalentFrameScroll_OnLoad(self)</OnLoad>
<!-- WoW wheel convention: +1 per notch up (toward the top, decreasing scroll),
-1 down (ChatFrame.xml's own handler quotes the same convention). Reuses
BenillaFauxScrollBar_Step (this file's header comment: generic, Slider-only). -->
<OnMouseWheel>BenillaFauxScrollBar_Step(getglobal("BenillaTalentFrameScrollBar"), arg1 > 0 and -1 or 1)</OnMouseWheel>
BenillaScrollBar_Step (this file's header comment: generic, Slider-only). -->
<OnMouseWheel>BenillaScrollBar_Step(getglobal("BenillaTalentFrameScrollBar"), arg1 > 0 and -1 or 1)</OnMouseWheel>
<!-- The real ScrollFrame's own half of the documented pair (this file's header
comment / BenillaTalentFrameScrollBar_OnValueChanged's own comment). -->
<OnVerticalScroll>getglobal("BenillaTalentFrameScrollBar"):SetValue(arg1)</OnVerticalScroll>
@@ -18,9 +18,10 @@
OUT (each waits for its first customer): menu level 3 (UIDROPDOWNMENU_MAXLEVELS is 2 — the
unit popups' deepest law; the XML gains DropDownList3 the day a customer needs it), color
swatches + the color picker + opacity, info.textHeight's per-row SetFont, the newbie
tooltips on rows (the ref gates them behind a default-off cvar), the uiScale correction
(the engine owns global UI scale), message() on the row/level overflow guards (no dev-error
dialog surface; the guard returns silently).
tooltips on rows (the ref gates them behind a default-off cvar), message() on the row/level
overflow guards (no dev-error dialog surface; the guard returns silently). The uiScale
correction came IN with its first scaled customer (0992, the options window's dropdown
rows): Toggle wears the owner's effective scale on the list.
Deviations, deliberate:
- THE WIDTH SETTLE. The ref sizes each row at AddButton time from normalText:GetWidth() and
@@ -535,6 +536,13 @@
end
listFrame:Hide();
-- The ref's uiScale correction, arriving with its first scaled customer (0992: the
-- options window rides SetScale, and a list at scale 1 would draw its rows ~28%
-- larger than the capsule they serve): the shared list wears the OWNER's effective
-- scale for this open. Every pre-0992 customer sits at effective scale 1 — no-op.
local listScale = tempFrame:GetEffectiveScale();
listFrame:SetScale(listScale);
if ( level == 1 ) then
if ( not dropDownFrame ) then
dropDownFrame = this:GetParent();
@@ -551,12 +559,14 @@
relativeTo = dropDownFrame.relativeTo or dropDownFrame:GetName();
relativePoint = dropDownFrame.relativePoint or "BOTTOMLEFT";
elseif ( anchorName == "cursor" ) then
-- Cursor coords are scale-1 units; a scaled list's anchor offsets are in
-- ITS space, so divide (the ref's own cursor/uiScale dance).
local cursorX, cursorY = GetCursorPosition();
point = "TOPLEFT";
relativeTo = "UIParent";
relativePoint = "BOTTOMLEFT";
xOffset = cursorX + (xOffset or 0);
yOffset = cursorY + (yOffset or 0);
xOffset = cursorX / listScale + (xOffset or 0);
yOffset = cursorY / listScale + (yOffset or 0);
listFrame.cursorMode = 1;
else
point = "TOPLEFT";
@@ -1272,35 +1272,106 @@
getglobal(name .. "RightDisabled"):Show()
end
-- The text-fit (ref PanelTemplates_TabResize, UIPanelTemplates.lua l.31-88, the
-- padding-only branch the tab template's OnShow uses — TabResize(0) → tab width =
-- text + the two 20px end slices; both middle slices stretch to the text; the text box
-- itself un-sizes to natural). One deviation: the ref reads the label's laid-out
-- GetWidth (its C measure is synchronous); ours reads GetStringWidth — the same number
-- once the measure round-trip lands, which is why the caller below re-checks from
-- OnUpdate instead of OnShow.
function PanelTemplates_TabResize(padding, tab)
-- The text-fit (ref PanelTemplates_TabResize, UIPanelTemplates.lua l.31-88 — now the WHOLE
-- function, `(padding, tab, absoluteSize, maxWidth)`): the tab is text + padding wide, plus
-- the two end slices; both middle slices stretch to the text; the text box itself un-sizes
-- to natural. `maxWidth` CAPS it and pins the label's box to the cap instead — the arm that
-- keeps a long tab inside its window, which the first transcription dropped and the macro
-- window's character tab then overran (0983 → 0991). `absoluteSize` is the ref's third arm,
-- transcribed with it so the next caller has it.
--
-- One deviation: the ref reads the label's laid-out GetWidth (its C measure is synchronous);
-- ours reads GetStringWidth — the same number once the measure round-trip lands, which is
-- why the caller below re-checks from OnUpdate instead of OnShow.
function PanelTemplates_TabResize(padding, tab, absoluteSize, maxWidth)
local name = tab:GetName()
local text = getglobal(name .. "Text")
local width = text:GetStringWidth() + (padding or 24)
text:SetWidth(0)
local sideWidths = 2 * getglobal(name .. "Left"):GetWidth()
local width, tabWidth
if absoluteSize then
if absoluteSize < sideWidths then
width, tabWidth = 1, sideWidths
else
width, tabWidth = absoluteSize - sideWidths, absoluteSize
end
text:SetWidth(width)
else
width = text:GetStringWidth() + (padding or 24)
if maxWidth and width > maxWidth then
width = maxWidth + (padding or 24)
text:SetWidth(width)
else
text:SetWidth(0)
end
tabWidth = width + sideWidths
end
getglobal(name .. "Middle"):SetWidth(width)
getglobal(name .. "MiddleDisabled"):SetWidth(width)
local sideWidths = 2 * getglobal(name .. "Left"):GetWidth()
tab:SetWidth(width + sideWidths)
tab:SetWidth(tabWidth)
end
-- The tab template's per-frame settle: the ref does TabResize(0) + the highlight width
-- in OnShow (CharacterFrameTemplates.xml l.77-80), trusting a synchronous text measure.
-- Our GetStringWidth has a frame of latency, so the template's OnUpdate calls this until
-- the measured width settles (then it's a no-op compare per frame).
--
-- The fit PARAMETERS live on the tab (`benillaTabPadding`/`benillaTabMaxWidth`, defaulting
-- to the +0 fit every window tab has always used) rather than at the call site, because
-- there is exactly one call site and it is this settle loop: an instance that ran its own
-- TabResize in OnLoad — as the reference's macro tabs do — would just be overwritten on the
-- next frame. Setting the two fields IS how an instance asks for the reference's own fit.
-- THE STRUCTURAL CAP — a tab may never grow past its parent's right edge, whatever the
-- instance asked for (decision 0997, the director's "fix this properly to prevent it ever
-- again"). The reference's own caps are hand-tuned numbers against ITS font metrics: the
-- macro window's 150 is enough for the enUS FRIZQT__ the 1.12 client rasterizes and not
-- obviously enough for anything else, and a character name — the one label whose width no
-- window author can know — is exactly where that runs out. So the per-instance cap becomes
-- a PREFERENCE and the window's own edge becomes the law.
--
-- The arithmetic is TabResize's, inverted: its capped branch yields
-- `tabWidth = cap + padding + sideWidths`, so `cap = room - padding - sideWidths`.
function BenillaTab_Cap(tab, padding)
local cap = tab.benillaTabMaxWidth
local parent = tab:GetParent()
if not parent then return cap end
local left, right = tab:GetLeft(), parent:GetRight()
if not left or not right then return cap end
local sideWidths = 2 * getglobal(tab:GetName() .. "Left"):GetWidth()
local room = (right - left) - padding - sideWidths - (tab.benillaTabRightInset or 0)
if room < 1 then room = 1 end
if not cap or room < cap then cap = room end
return cap
end
-- The tab template's per-frame settle: the ref does TabResize(0) + the highlight width
-- in OnShow (CharacterFrameTemplates.xml l.77-80), trusting a synchronous text measure.
-- Our GetStringWidth has a frame of latency, so the template's OnUpdate calls this until
-- the measured width settles (then it's a no-op compare per frame).
--
-- The fit PARAMETERS live on the tab (`benillaTabPadding`/`benillaTabMaxWidth`, defaulting
-- to the +0 fit every window tab has always used) rather than at the call site, because
-- there is exactly one call site and it is this settle loop: an instance that ran its own
-- TabResize in OnLoad — as the reference's macro tabs do — would just be overwritten on the
-- next frame. Setting the two fields IS how an instance asks for the reference's own fit.
--
-- The settle compares BOTH inputs. Watching only the string width used to be enough; it
-- isn't once the cap can move, because a tab's room depends on where the tab to its left
-- ended up — so tab 2 must be able to re-fit on a frame where its own label did not change.
function BenillaTabButton_OnUpdate(tab)
local text = getglobal(tab:GetName() .. "Text")
local w = (text and text:GetStringWidth()) or 0
if w <= 0 or tab.benillaFitWidth == w then return end
tab.benillaFitWidth = w
PanelTemplates_TabResize(0, tab)
getglobal(tab:GetName() .. "HighlightTexture"):SetWidth(w + 30)
if w <= 0 then return end
local pad = tab.benillaTabPadding or 0
local cap = BenillaTab_Cap(tab, pad)
if tab.benillaFitWidth == w and tab.benillaFitCap == cap then return end
tab.benillaFitWidth, tab.benillaFitCap = w, cap
PanelTemplates_TabResize(pad, tab, nil, cap)
-- The ref sizes the highlight off the label too (MacroFrame's tabs spell it
-- `GetTextWidth() + 31`); +30 is this kit's long-standing number for the +0 fit. Capped
-- at the tab so a clamped tab never wears a highlight wider than itself.
local hl = w + 30
if hl > tab:GetWidth() + 14 then hl = tab:GetWidth() + 14 end
getglobal(tab:GetName() .. "HighlightTexture"):SetWidth(hl)
end
]]></Script>
@@ -1152,4 +1152,287 @@
<OnLoad>TargetFrameDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
<!--
THE PET FRAME (decision 0990; ref PetFrame.xml + PetFrame.lua, quoted). 128×53 parented to
the player frame at TOPLEFT (80,-60) — a static anchor, deliberately: the reference does not
list PetFrame in UIPARENT_MANAGED_FRAME_POSITIONS at all, because it rides its parent.
The art is the SMALL targeting frame, and it has two states the ref swaps at runtime
(PetFrame.lua's Update): `UI-SmallTargetingFrame` when the pet has a power bar, `-NoMana`
when it has none. That is not decoration — a warlock's imp has mana and a hunter's boar has
focus, but a skeleton has neither, and the plain art draws a mana rail with nothing in it.
DEFERRED, each with its reason (all named in 0990):
- HAPPINESS. The ref's PetFrameHappiness icon needs `GetPetHappiness 0x4be900`, whose body
is NOT carved in wow-re (its address is known from the binding table, nothing more). Its
three thresholds, the damage percentage and the loyalty rate are exactly the kind of
derived numbers 0988 was written about: vmangos has a HAPPINESS_LEVEL_SIZE and taking it
would repeat that record's own mistake. So the icon is absent rather than guessed, and
comes back with the carve — together with `GetPetLoyalty 0x4be700`,
`GetPetTrainingPoints 0x4be790` and `GetPetExperience 0x4be840`, its uncarved siblings.
- The PET right-click menu. Its four entries (UnitPopup.xml's PET_PAPERDOLL / PET_RENAME /
PET_ABANDON / PET_DISMISS) each need a binding that is its own build — the pet paper doll
most of all. They stay suppressed there; the frame takes no right click rather than
opening a menu of four dead rows.
- PartyMemberBuffTooltip on hover (the ref's OnEnter panel): it belongs to the party
frames' own deferred buff panel, not here.
-->
<Script><![CDATA[
function BenillaPetFrame_OnLoad(frame)
frame.portrait = BenillaPetFrameTextureFramePortrait
frame.healthBar = BenillaPetFrameHealthBar
frame.manaBar = BenillaPetFrameManaBar
frame.nameText = BenillaPetFrameTextureFrameName
-- The recessed-bar look, the same mechanism the player/target/party frames use
-- (decision 0884): push the bars one frame level BELOW the art's TextureFrame, because
-- the draw layer is bucket-wide and the frame level is the one key term above it.
local level = BenillaPetFrameTextureFrame:GetFrameLevel()
frame.healthBar:SetFrameLevel(level - 1)
frame.manaBar:SetFrameLevel(level - 1)
frame.attackModeCounter = 0
frame.attackModeSign = -1
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("UNIT_PET")
frame:RegisterEvent("UNIT_AURA")
frame:RegisterEvent("UNIT_HEALTH")
frame:RegisterEvent("UNIT_MAXHEALTH")
frame:RegisterEvent("UNIT_POWER_UPDATE")
frame:RegisterEvent("UNIT_MAXPOWER")
frame:RegisterEvent("UNIT_DISPLAYPOWER")
frame:RegisterEvent("UNIT_NAME_UPDATE")
frame:RegisterEvent("PET_ATTACK_START")
frame:RegisterEvent("PET_ATTACK_STOP")
frame:EnableMouse(true)
BenillaPetFrame_Update(frame)
end
-- ref PetFrame_OnEvent, with both of its argument gates kept: UNIT_PET names the OWNER
-- ("player" — wow-re §9 carves the arg), every other UNIT_* names the pet itself. Getting
-- those backwards is how a pet frame ends up repainting off the player's health.
function BenillaPetFrame_OnEvent(frame)
if event == "PET_ATTACK_START" then
BenillaPetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, 1.0)
BenillaPetFrameAttackMode:Show()
return
elseif event == "PET_ATTACK_STOP" then
BenillaPetFrameAttackMode:Hide()
return
elseif event == "UNIT_PET" then
if arg1 ~= "player" then return end
elseif string.sub(event, 1, 5) == "UNIT_" and arg1 ~= "pet" then
return
end
BenillaPetFrame_Update(frame)
end
function BenillaPetFrame_Update(frame)
if not UnitExists("pet") then
frame:Hide()
return
end
frame:Show()
SetPortraitTexture(frame.portrait, "pet")
frame.nameText:SetText(UnitName("pet") or "")
frame.healthBar:SetMinMaxValues(0, UnitHealthMax("pet"))
frame.healthBar:SetValue(UnitHealth("pet"))
-- The ref's art swap (PetFrame.lua's Update): a powerless pet wears the -NoMana plate,
-- which has no mana rail to leave empty. The bar hides with it, as on every other frame.
local maxPower = UnitPowerMax("pet")
if maxPower > 0 then
BenillaPetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame")
frame.manaBar:Show()
local _, token = UnitPowerType("pet")
local c = BENILLA_POWER_COLORS[token] or BENILLA_POWER_COLORS.MANA
frame.manaBar:SetStatusBarColor(c.r, c.g, c.b)
frame.manaBar:SetMinMaxValues(0, maxPower)
frame.manaBar:SetValue(UnitPower("pet"))
else
BenillaPetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame-NoMana")
frame.manaBar:Hide()
end
BenillaPetFrame_UpdateDebuffs()
end
-- The four debuff buttons (ref PetFrame.xml's $parentDebuff1..4 over RefreshBuffs). The pet
-- carries no aura timers — no unit but yourself does on the 1.12 wire (decision 0257 B6) —
-- so these are icon + border + stack count, the party rows' shape exactly.
function BenillaPetFrame_UpdateDebuffs()
for i = 1, 4 do
local button = getglobal("BenillaPetFrameDebuff" .. i)
local _, icon, count = UnitDebuff("pet", i)
if icon then
getglobal(button:GetName() .. "Icon"):SetTexture(icon)
local text = getglobal(button:GetName() .. "Count")
if count and count > 1 then
text:SetText(count)
text:Show()
else
text:Hide()
end
button:Show()
else
button:Hide()
end
end
end
-- ref PetFrame_OnUpdate: the attack overlay's alpha ramp — a 0.5 s-per-leg triangle between
-- 55/255 and 1, sign-flipped at each end (the ref's own counter shape, transcribed).
function BenillaPetFrame_OnUpdate(frame, elapsed)
if not BenillaPetFrameAttackMode:IsVisible() then return end
local counter = frame.attackModeCounter + elapsed
local sign = frame.attackModeSign
if counter > 0.5 then
sign = -sign
frame.attackModeSign = sign
end
counter = math.fmod(counter, 0.5)
frame.attackModeCounter = counter
local alpha
if sign == 1 then
alpha = (55 + (counter * 400)) / 255
else
alpha = (255 - (counter * 400)) / 255
end
BenillaPetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, alpha)
end
-- ref PetFrame_OnClick's left branch, all three legs in the ref's own order. The right
-- branch (the PET menu) is the named deferral in the header.
function BenillaPetFrame_OnClick(button)
if button ~= "LeftButton" then return end
if SpellIsTargeting() then
SpellTargetUnit("pet")
elseif CursorHasItem() then
DropItemOnUnit("pet")
else
TargetUnit("pet")
end
end
function BenillaPetFrame_OnEnter(frame)
if not UnitExists("pet") then return end
GameTooltip_SetDefaultAnchor(GameTooltip, frame)
if GameTooltip:SetUnit("pet") then
getglobal("GameTooltipTextLeft1"):SetTextColor(GameTooltip_UnitColor("pet"))
end
end
]]></Script>
<!-- The debuff button — ref PartyBuffButtonTemplate (PartyFrameTemplates.xml l.3-35), which is
what the ref's PetFrame inherits for its four: a 15×15 icon under a 17×17
UI-Debuff-Overlays border. Untinted: the ref's party/pet buttons never recolor it (only the
target frame's dispel-tint law does), so this stays the flat swatch the TexCoords select. -->
<Button name="BenillaPetDebuffTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="15" y="15"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon" setAllPoints="true"/>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentBorder" file="Interface\Buttons\UI-Debuff-Overlays">
<Size><AbsDimension x="17" y="17"/></Size>
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.296875" right="0.5703125" top="0" bottom="0.515625"/>
</Texture>
<FontString name="$parentCount" inherits="NumberFontNormalSmall">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-1" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnter><![CDATA[
if GameTooltip then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetUnitDebuff("pet", this:GetID())
end
]]></OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!-- frameStrata LOW matches the party frames' and the target's — above the player frame's
BACKGROUND (which it overlaps), still below the MEDIUM panels. -->
<Frame name="BenillaPetFrame" parent="BenillaPlayerFrame" hidden="true" frameStrata="LOW">
<Size><AbsDimension x="128" y="53"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="80" y="-60"/></Offset></Anchor>
</Anchors>
<Frames>
<!-- Bars first, pushed one frame level under the TextureFrame at load (see OnLoad). -->
<StatusBar name="$parentHealthBar">
<Size><AbsDimension x="70" y="8"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="47" y="-22"/></Offset></Anchor></Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="1.0" b="0"/>
</StatusBar>
<StatusBar name="$parentManaBar">
<Size><AbsDimension x="70" y="8"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="47" y="-29"/></Offset></Anchor></Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="0" b="1.0"/>
</StatusBar>
<Frame name="$parentTextureFrame">
<Size><AbsDimension x="128" y="53"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- Portrait 37×37 TOPLEFT (7,-6), under the art's round window (ref
PetPortrait, PetFrame.xml l.16-27). -->
<Texture name="$parentPortrait">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="BORDER">
<!-- 128×64 art at (0,-2) — it hangs 13 px below the frame rect, as
authored. The file is swapped at update (see the header). -->
<Texture name="BenillaPetFrameTexture" file="Interface\TargetingFrame\UI-SmallTargetingFrame">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The attacking overlay (ref PetAttackModeTexture, l.104-118): the right
29.7% of UI-Player-AttackStatus, ADD-blended, pulsed by OnUpdate. -->
<Texture name="BenillaPetFrameAttackMode" file="Interface\TargetingFrame\UI-Player-AttackStatus" alphaMode="ADD" hidden="true">
<Size><AbsDimension x="76" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-9"/></Offset></Anchor></Anchors>
<TexCoords left="0.703125" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentName" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="50" y="33"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- The four debuffs, ref geometry: the first at (48,-42), then +2 px chained right. -->
<Button name="$parentDebuff1" inherits="BenillaPetDebuffTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="48" y="-42"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff2" inherits="BenillaPetDebuffTemplate" id="2">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetFrameDebuff1" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff3" inherits="BenillaPetDebuffTemplate" id="3">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetFrameDebuff2" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff4" inherits="BenillaPetDebuffTemplate" id="4">
<Anchors><Anchor point="LEFT" relativeTo="BenillaPetFrameDebuff3" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaPetFrame_OnLoad(this)</OnLoad>
<OnEvent>BenillaPetFrame_OnEvent(this)</OnEvent>
<OnUpdate>BenillaPetFrame_OnUpdate(this, arg1)</OnUpdate>
<OnEnter>BenillaPetFrame_OnEnter(this)</OnEnter>
<OnLeave>GameTooltip:FadeOut()</OnLeave>
<OnClick>BenillaPetFrame_OnClick(arg1)</OnClick>
</Scripts>
</Frame>
</Ui>
+60
View File
@@ -0,0 +1,60 @@
//! **Which build is this?** — the commit the running binary was built from, stamped in at
//! compile time by the `benilla` launcher shim's `build.rs` (which owns the *how*, including why
//! the stamp can't go stale — and why it is stamped into the ~30-line shim rather than this
//! crate: cargo re-dirties whatever package carries the stamp on every commit, decision 0993)
//! and handed to [`crate::run`] as the [`BuildId`] resource.
//!
//! A report from someone else's machine — "the water reads wrong here", "it panicked on login" —
//! is only actionable against known code, and nothing else in the binary identifies it: the crate
//! version is a permanent `0.1.0`, and people build from a clone of the public snapshot repo whose
//! HEAD moves with every sync. So the sha is the version.
//!
//! Two surfaces, deliberately:
//!
//! - **The startup log line** ([`banner`], run from [`crate::preflight`]'s banner family) — this is
//! the one that matters for someone else's run. They launch from a terminal, so the id is already
//! in the output they paste; nobody has to know a hotkey to answer "what are you on?".
//! - **The debug panel's footer** (`` ` ``) — the same line where the reader already is when they
//! are looking at a readout, click-to-copy for the full sha.
//!
//! Mapping a reported sha back: a **public** snapshot sha is tagged `pub/<short-sha>` on the private
//! commit it shipped, so `git show pub/<sha>` (or `git rev-parse pub/<sha>^{commit}`) names the code
//! exactly; a **private** sha is just a commit here.
use bevy::prelude::*;
/// The launcher shim's compile-time git stamp, inserted as a resource by [`crate::run`]. The git
/// fields are empty when the build had no checkout to read (a downloaded source zip, or no `git`
/// on `PATH`), which [`BuildId::summary`] reports as an unknown build.
#[derive(Resource, Clone, Copy)]
pub struct BuildId {
/// The full 40-char sha of the commit this binary was built from (what the panel copies).
pub sha: &'static str,
/// Git's own abbreviation of [`sha`](Self::sha), from the same repo `pubsync` abbreviates in —
/// so a public build's string is literally the suffix of its `pub/<sha>` tag.
pub short: &'static str,
/// The commit date of [`sha`](Self::sha) (`YYYY-MM-DD`) — a property of the sha, so it can
/// never disagree with it.
pub date: &'static str,
/// The cargo profile directory this was built in: `debug`, `release`, or `ship` (0736). Half
/// of every "it runs badly" report is a debug build.
pub profile: &'static str,
}
impl BuildId {
/// The one-line build id: `f5fd009 · 2026-07-30 · release`.
pub(crate) fn summary(&self) -> String {
if self.short.is_empty() {
format!("unknown (built without a git checkout) · {}", self.profile)
} else {
format!("{} · {} · {}", self.short, self.date, self.profile)
}
}
}
/// Log the build id once at startup — the line that answers "what version are you on?" from a
/// pasted terminal log. Not env-gated, for [`crate::preflight`]'s reason: an id nobody knows to
/// switch on is not an id.
pub(crate) fn banner(build: Res<BuildId>) {
info!("benilla build {}", build.summary());
}
@@ -958,6 +958,21 @@ pub(super) fn seed_ui_fixture(
warn!("capture: ui-options-graphics seed failed: {e}");
}
}
UiFixture::OptionsWorldDetail => {
let Some(mut script) = script else {
return;
};
// The dropdown list open (0992), same posture as the page fixtures: real CVar set,
// the live open-select-toggle path. The list's width settles from its OnUpdate a
// frame later (the kit's WIDTH SETTLE law) — inside the capture's settle frames.
script.register_cvars(crate::cvars::REGISTERED.iter().copied());
if let Err(e) = script.run(
"ShowUIPanel(OptionsFrame); OptionsFrameCategoryListRowGraphics:Click(); \
OptionsFrameContainerBodyGraphicsRowWorldDetailDropdownButton:Click()",
) {
warn!("capture: ui-options-worlddetail seed failed: {e}");
}
}
UiFixture::OptionsSearch => {
let Some(mut script) = script else {
return;
@@ -1032,6 +1047,46 @@ pub(super) fn seed_ui_fixture(
warn!("capture: ui-spellbook seed failed to open the book: {e}");
}
}
UiFixture::Macro | UiFixture::MacroPopup => {
let Some(script) = script.as_mut() else {
return;
};
// Made through the LIVE `CreateMacro` path, not by pushing a table in — so the capture
// exercises engine table → `UPDATE_MACROS` → window exactly as a player's own edit
// does. Icon indices are into the real `SpellIcon.dbc` catalog `ui_macro` loads at
// PostStartup, so they resolve to real art. The bodies are the shapes that matter: a
// plain `/cast`, a MULTI-LINE macro (what the body box has to lay out), and a
// `/script` line (the non-`/cast` case the bar's bound-spell resolve must decline).
const SEED: [(&str, u32, &str); 4] = [
("Charge", 1, "/cast Charge"),
(
"Pull",
9,
"/cast Charge\n/say Incoming!\n/script CastSpellByName(\"Battle Shout\")",
),
("Shout", 17, "/cast Battle Shout"),
("Sit", 25, "/sit"),
];
let mut seed = String::new();
for (name, icon, body) in SEED {
seed.push_str(&format!(
"CreateMacro(\"{name}\", {icon}, \"{}\", 1, nil)\n",
// Lua-escape the body: a `/script` line carries its own quotes, and a macro
// body's newlines have to survive as newlines.
body.replace('"', "\\\"").replace('\n', "\\n")
));
}
// Slot 2 selected: the multi-line body, so the detail pane and the body box both have
// something in them. The popup fixture then opens the chooser over that selection via
// the ref's `MacroEditButton` path — an EDIT, so the name box arrives pre-filled.
seed.push_str("ShowMacroFrame()\nBenillaMacroButton2:Click()\n");
if fixture == UiFixture::MacroPopup {
seed.push_str("BenillaMacroEditButton:Click()\n");
}
if let Err(e) = script.run(&seed) {
warn!("capture: ui-macro seed failed: {e}");
}
}
UiFixture::ChatEdit => {
let Some(script) = script.as_mut() else {
return;
@@ -42,14 +42,14 @@
//! WOW_DATA=<vanilla Data dir> WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-unitframes \
//! WOW_CAPTURE_OUT=/tmp/shot.png cargo run -q -p benilla
//! ```
//! `cargo run` exports `CARGO_MANIFEST_DIR`, which is how Bevy's `AssetServer` finds
//! `crates/benilla/assets/` — in particular the WGSL shaders (`ui_quad.wgsl`, `sky.wgsl`, …).
//! Launching `target/debug/benilla` directly has no manifest dir, so it resolves `assets/` next
//! to the executable (`target/debug/assets/`, which doesn't exist), silently loads **no** shaders,
//! and every custom-shader layer — the *entire* player UI, sky, liquid, models — renders blank
//! while only the built-in-pipeline terrain still draws. A UI capture that comes back as bare sky
//! is this mistake, not a UI bug. `WOW_CAPTURE_UI=1` opts the player UI into the shot (off by
//! default so world baselines stay UI-free; omit it for world-only scenes). `WOW_CAPTURE=list`
//! `cargo run` rebuilds first; a bare `target/debug/benilla` can silently be **stale code** — the
//! classic way a capture "disproves" a fix that was never in the binary. (Before 0993 it was
//! worse: `assets/` resolved through Cargo's runtime `CARGO_MANIFEST_DIR`, so a bare run silently
//! loaded **no** WGSL shaders and every custom-shader layer — the *entire* player UI, sky, liquid,
//! models — rendered blank, read as a UI bug for hours once. The app now bakes the absolute
//! `crates/benilla-app/assets/` path in at compile time, so a bare run finds its shaders;
//! staleness is the trap that remains.) `WOW_CAPTURE_UI=1` opts the player UI into the shot (off
//! by default so world baselines stay UI-free; omit it for world-only scenes). `WOW_CAPTURE=list`
//! prints the scenario names. `scripts/visual.sh` wraps all of this.
use std::path::Path;

Some files were not shown because too many files have changed in this diff Show More