the pet bar and skills tab go stock, and the raid's profile is flat
- the pet action bar is the reference's own `PetActionBarFrame.xml`; our 559-line transcription is deleted, and the reference's bar brings what it had left out — the slide, the Ctrl-held and locked gates on show/hide, the hotkey pass whose string-versus-number comparison leaves every corner empty — with the three pet dialogs moved verbatim into the popup slice; one adapter draws the autocast shine (the reference's `<Model>` draws nothing in this engine, so each button gets the token-texture shine at the reference's own 1.2); the stock file registers four events nothing fired, built rather than listed — `PET_BAR_UPDATE_COOLDOWN` when the ten cooldown triples alone move, `PLAYER_CONTROL_LOST`/`GAINED` on the edge of the control flag (`0x4958e0`'s change-gated fire), `PLAYER_FARSIGHT_FOCUS_CHANGED` on the edge of the farsight descriptor, and `UNIT_FLAGS` on a change of the raw flags (its exact fire law a dispatched question) (1953) - the skills tab is the reference's own `SkillFrame.xml` at its slot between ReputationFrame and HonorFrame; our 874-line transcription is deleted, the `UNLEARN_SKILL` dialog moves verbatim into the popup slice, and `CancelSkillUps` is built — the page's live Close button calls it before hiding, so every close reaches it; it is the reset leg of the temp-point table (`0x4d3e30`, no arguments, no returns) over a table this client keeps empty by construction, so the reset moves nothing, which is the binding's whole effect on this data; `AddSkillUp`, `RemoveSkillUp` and `BuySkillTier` sit on buttons the reference shows only for skill points or step/rank costs no 1.12 server sends and stay named as open gaps; and the expand tab's fit is the reference's load-time law (`GetTextWidth()+45` once, in OnLoad), so the transcription's zero-measure guard and re-fit are gone (1956) - a 45-bot Blackrock raid circling with sound on holds 60 fps with the main thread at 6–7 ms while the HUD's ms readout says 17–18: that number is the sum of every thread's CPU per frame (main, render, compute pool, audio), not the frame time — the raid costs +6 ms spread over four threads with no thread owning it; two suspects refuted by measurement: mimalloc as the process allocator (interleaved: no change on any thread, reverted the same hour) and page faults (59 a frame on the probe, tens of microseconds, not the render thread's 2.1 ms of kernel time, which is the Metal driver's own work and scales with draws); so the probe counts draws per phase family (transparent outnumbers opaque, mostly the buff wave's particles and nameplates) and `WOW_PROBE_SHARED_SKIN=1` prices cross-body instancing before it is built: nothing moved, because bevy instances only identical meshes and a geared body's merged geoset set is its own mesh (1954) - three fixes, each removing work a 15 s sample named: booth cameras skip cluster assignment (no bevy light ever reaches a booth, and the default config rebuilt an empty cluster grid per booth view per frame — 0.2 ms of the raid's main thread), the rig palette's watermark clone reuses the row buffer from two publishes ago instead of calloc'ing the 650 KB table every publish (safe because the GPU only ever receives dirty ranges), and prop probes upload the dirty span rather than every allocated row (circling a city streams props in and out most frames, and each change re-sent hundreds of KB for one row); no bucket of the main thread's profile is above a millisecond — the raid's +2.5 ms there is forty-five bodies' worth of every per-entity lane at ~55 µs a body, and the one lever left that is all ours, one texture array per body folding ~9 draws to ~3, is sized at ~1.5 ms and not started (1954) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,559 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The pet action bar (ref PetActionBarFrame.xml/.lua, extracted from patch.MPQ; decision
|
||||
0982). Hunter, warlock, and every possessed/charmed unit: ten buttons over the ten packed
|
||||
words the server sends in SMSG_PET_SPELLS, driven by `crate::ui_pet`'s feed through the
|
||||
engine's pet seam (GetPetActionInfo & co.).
|
||||
|
||||
THE THING TO KNOW ABOUT THIS BAR: the server owns its CONTENTS (all ten slots arrive in
|
||||
one SMSG_PET_SPELLS) but the CLIENT owns its lit state. A press repaints immediately and
|
||||
is never confirmed, because the server does not answer these packets at all — the real
|
||||
client writes its own state before the send (decision 0988). So a button whose ring does
|
||||
not light on click is BROKEN, not patient.
|
||||
|
||||
The ten slots are not a layout. `GetPetActionInfo` reports each slot's own class off its
|
||||
type byte, and the familiar 3 commands / 4 spells / 3 reactions arrangement is just what
|
||||
vmangos's CharmInfo::InitPetActionBar happens to write; a charmed creature arrives with a
|
||||
different mix through the same ten words. Nothing below indexes by position.
|
||||
|
||||
Token slots (the commands and reactions) return the NAMES OF GLOBALS rather than values,
|
||||
which is the reference's own convention (`PetActionBarFrame.lua:98-104`): the name keys are
|
||||
GlobalStrings' (PET_ACTION_ATTACK, PET_MODE_DEFENSIVE, …), and the texture globals are
|
||||
declared below. Both halves of that mapping are a contract we do not get to choose — the
|
||||
names are what `ui_pet/bar.rs` hands back and the icons are real files in the install — so
|
||||
the specification for them is `PetActionBarFrame.lua:6-12`; see the declaration's own note
|
||||
for what IS ours about the layout.
|
||||
|
||||
Geometry QUOTED from the ref (PetActionBarFrame.xml): frame 509x43; the two UI-PetBar shelf
|
||||
strips (256x44 at TOPLEFT with TexCoords 0..1 / 0.015625..0.359375, then 184x44 to its
|
||||
right with 0..0.71875 / 0.375..0.71875); buttons 30x30, button1 at frame BOTTOMLEFT +(36,2),
|
||||
chained +8 (with the ref's own +7 between 6 and 7); the 54x54 UI-Quickslot2 ring CENTER
|
||||
(0,-1). The bar rides `MainMenuBar` BOTTOMLEFT — our MainMenuBar, the same 1024x53
|
||||
frame at screen BOTTOM the ref anchors to — at the managed PETACTIONBAR_YPOS (UIParent.xml
|
||||
declares the row already).
|
||||
|
||||
Ref divergences, each deliberate:
|
||||
- ONE refresh event. The app diffs the whole pushed bar and fires PET_BAR_UPDATE on any
|
||||
change, so the ref's PET_BAR_UPDATE / PET_BAR_UPDATE_COOLDOWN / UNIT_PET / the
|
||||
UNIT_FLAGS+UNIT_AURA pair it filters for arg1=="pet" collapse into one — the same
|
||||
collapse our stance bar made, for the same reason (the stock shapeshift bar,
|
||||
BonusActionBarFrame.xml since 1938, registers the reference's six).
|
||||
- The drag is ONE verb. PickupPetAction serves shift-click, OnDragStart and OnReceiveDrag
|
||||
alike (decision 1010) — that is the reference's own shape, not a collapse of ours: the
|
||||
binding itself forks on whether the cursor is already carrying a pet payload, so there is
|
||||
no "place" counterpart the way PlaceAction answers PickupAction on the main bar. Nothing
|
||||
else can produce that payload, which is why the pet bar can be rearranged and never
|
||||
populated: its contents are the server's.
|
||||
- PET_BAR_SHOWGRID/HIDEGRID are live, and are the PET payload's alone. The engine derives
|
||||
them off the same cursor transition that drives ACTIONBAR_SHOWGRID/HIDEGRID but on a
|
||||
disjoint predicate, so a held spell lights the action bar's empty slots and a held pet
|
||||
action lights this bar's — never both, because neither payload can land on the other bar.
|
||||
- NO HotKey corner, and none is missing (1052). The ref's button inherits
|
||||
ActionButtonTemplate's $parentHotKey and repaints it on UPDATE_BINDINGS — but
|
||||
PetActionButton_SetHotkeys (l.311-319) writes the label only when
|
||||
`bindingSuffix == this:GetID()`, a STRING against a NUMBER, which Lua never equates. So
|
||||
the real 1.12 pet bar renders an empty hotkey on every slot, always, and the reporter's
|
||||
own reference shot for B218 shows exactly that: CTRL-1..CTRL-5 bound in the window, no
|
||||
glyph on any pet button. An empty corner is the faithful render; a filled one would be
|
||||
the divergence. (Our own action bar's corners are real — ActionBar.xml — because the
|
||||
ref's ActionButton_UpdateHotkeys has no such bug.)
|
||||
- The autocast sparkle is TEXTURES, not the ref's <Model>. benilla has no Model widget
|
||||
(the Cooldown and the bag ItemAnim took the same posture): model the mechanism, let our
|
||||
own renderer draw it. The numbers below are the shipped model's own — see BENILLA_PET_
|
||||
AUTOCAST_* .
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
NUM_PET_ACTION_SLOTS = 10
|
||||
|
||||
-- ── The token icons ───────────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- These are GLOBALS on purpose, and the names are a contract, not a choice:
|
||||
-- `GetPetActionInfo` hands back the NAME of one of them for a token slot and the paint
|
||||
-- below resolves it with getglobal — the reference's own convention, and the one
|
||||
-- `ui_pet/bar.rs` writes against. The icon paths are the reference's too; they name real
|
||||
-- .blp files in the install, so there is nothing to choose there either.
|
||||
--
|
||||
-- What IS ours is the grouping. The bar's tokens come in two families and `ui_pet/bar.rs`
|
||||
-- already separates them at the seam that names these — `command_token` (the orders you
|
||||
-- give, keyed `PET_ACTION_*`) and `reaction_token` (the stance the pet holds, keyed
|
||||
-- `PET_MODE_*`) — so they are laid out in those two families, in that file's own order,
|
||||
-- and the two halves of the mapping read side by side. The reference keeps them in one
|
||||
-- undifferentiated block, which hides the split.
|
||||
|
||||
-- Commands — the four orders, in `ui_pet/bar.rs`'s `command_token` order.
|
||||
PET_WAIT_TEXTURE = "Interface\\Icons\\Spell_Nature_TimeStop"
|
||||
PET_FOLLOW_TEXTURE = "Interface\\Icons\\Ability_Tracking"
|
||||
PET_ATTACK_TEXTURE = "Interface\\Icons\\Ability_GhoulFrenzy"
|
||||
PET_DISMISS_TEXTURE = "Interface\\Icons\\Spell_Shadow_Teleport"
|
||||
|
||||
-- Reactions — the three stances, in `ui_pet/bar.rs`'s `reaction_token` order.
|
||||
PET_PASSIVE_TEXTURE = "Interface\\Icons\\Ability_Seal"
|
||||
PET_DEFENSIVE_TEXTURE = "Interface\\Icons\\Ability_Defend"
|
||||
PET_AGGRESSIVE_TEXTURE = "Interface\\Icons\\Ability_Racial_BloodRage"
|
||||
|
||||
-- ── The autocast sparkle ──────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- NATIVE since decision 1383 (B282). The reference's "autocast is on" mark is a <Model>
|
||||
-- playing Interface\Buttons\UI-AutoCastButton.mdx — a widget OUTSIDE the FrameXML layout
|
||||
-- tree — and our old emulation here (a per-button pool of ~92 OVERLAY textures SetPoint'ed
|
||||
-- every frame from OnUpdate) made the most expensive possible use of the script layer:
|
||||
-- every frame it dirtied the layout epoch and forced two whole-roster anchor solves,
|
||||
-- +4.25 ms/frame for ONE autocasting button at a default roster, scaling with the whole
|
||||
-- UI (B282, nazriel's ~11 ms under addons). What a button carries now is ONE hidden
|
||||
-- marker texture, $parentShine, whose path is the `benilla:autocast-shine` token: the
|
||||
-- extract's conversion records its rect/layer as a shine site and draws nothing, and
|
||||
-- `autocast_shine::emit_shine` (benilla-app) animates the four comet trails there on the
|
||||
-- UI quad append lane — zero per-frame script traffic, the reference's own architecture
|
||||
-- on our substrate. The M2 derivation (bones, emitters, ramps, the 30.72-unit projection,
|
||||
-- decision 1321's truncating millisecond clock) moved whole into that module's header;
|
||||
-- this file's only job is showing the marker exactly while autocast is enabled.
|
||||
|
||||
-- ── The buttons ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
function BenillaPetActionButton_OnLoad(button, index)
|
||||
-- Ref PetActionButton_OnLoad l.234-235. The right-click registration is load-bearing,
|
||||
-- not boilerplate: it is the whole autocast toggle, and without it a right click is
|
||||
-- simply never delivered.
|
||||
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
-- The drag trio (decision 1010). Both buttons drag, ref-verbatim
|
||||
-- (PetActionButton_OnLoad l.234) — and unlike the action bar's, ALL THREE gestures here
|
||||
-- reach the same verb: the reference's OnClick-under-shift, OnDragStart and
|
||||
-- OnReceiveDrag all call PickupPetAction, which forks on whether the cursor is already
|
||||
-- carrying. Both drag ends honour LOCK_ACTIONBAR since 1136 — the same uvar the main bar
|
||||
-- declares (ActionBar.xml), which is the ref's own arrangement too: PetActionBarFrame.lua
|
||||
-- reads a global UIOptionsFrame owns.
|
||||
button:RegisterForDrag("LeftButton", "RightButton")
|
||||
button.index = index
|
||||
button.icon = getglobal(button:GetName() .. "Icon")
|
||||
button.cooldown = getglobal(button:GetName() .. "Cooldown")
|
||||
button.autocastable = getglobal(button:GetName() .. "AutoCastable")
|
||||
button.shine = getglobal(button:GetName() .. "Shine")
|
||||
end
|
||||
|
||||
-- Ref PetActionButton_OnClick. The CheckButton widget toggles itself BEFORE OnClick fires,
|
||||
-- and the reference immediately clears that (`this:SetChecked(0)`) — nothing about this bar
|
||||
-- is locally latched, because the server owns every bit of it. Left = run the slot, unless
|
||||
-- the pet is already on the attack, in which case it calls off; right = flip autocast.
|
||||
function PetActionButton_OnClick(mouseButton)
|
||||
local button = this
|
||||
button:SetChecked(0)
|
||||
-- Shift outranks the button fork entirely (ref l.253-255): a shift-click on the pet bar
|
||||
-- is a pick-up whichever mouse button it came from, and it never toggles autocast.
|
||||
if IsShiftKeyDown() then
|
||||
PickupPetAction(button.index)
|
||||
PetActionBar_Update()
|
||||
elseif mouseButton == "RightButton" then
|
||||
TogglePetAutocast(button.index)
|
||||
elseif IsPetAttackActive(button.index) then
|
||||
PetStopAttack()
|
||||
else
|
||||
CastPetAction(button.index)
|
||||
end
|
||||
end
|
||||
|
||||
-- The KEYBIND pair (decision 1052) — ref PetActionButtonDown/Up l.218-231, which is where
|
||||
-- the BONUSACTIONBUTTONn bindings land: their bodies call BonusActionButtonDown/Up, and
|
||||
-- those are one-liners onto these (BonusActionBarFrame.lua:106-112). The ref's own
|
||||
-- runOnUp shape: DOWN only shows the pushed art, UP fires.
|
||||
--
|
||||
-- Note what the key path does NOT do — it is a bare CastPetAction, with none of the click
|
||||
-- path's forks: no shift pick-up, no right-button autocast, and no IsPetAttackActive
|
||||
-- call-off. That is the reference's split, not a simplification of ours: the fork lives in
|
||||
-- PetActionButton_OnClick, which a key press never reaches.
|
||||
--
|
||||
-- On a PET bar none of that is visible, because the call-off fork is DEAD CODE there:
|
||||
-- IsPetAttackActive needs the [0xb714b0] latch, and `0x4bd420` raises it only when the
|
||||
-- bar's unit is the one you are POSSESSING (ui_pet.rs's `possessing` carries the carve —
|
||||
-- UNIT_FLAG_POSSESSED, never a hunter's or warlock's pet). A pet's Attack button can never
|
||||
-- latch lit, so click and key both simply order the attack. The split is visible on a
|
||||
-- POSSESS bar (Mind Control, Eye of Kilrogg, Eyes of the Beast): there a click sends
|
||||
-- PetStopAttack and CTRL-1 re-issues the order instead.
|
||||
function PetActionButtonDown(id)
|
||||
local button = getglobal("PetActionButton" .. id)
|
||||
if ( button:GetButtonState() == "NORMAL" ) then
|
||||
button:SetButtonState("PUSHED")
|
||||
end
|
||||
end
|
||||
|
||||
function PetActionButtonUp(id)
|
||||
local button = getglobal("PetActionButton" .. id)
|
||||
if ( button:GetButtonState() == "PUSHED" ) then
|
||||
button:SetButtonState("NORMAL")
|
||||
CastPetAction(id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Both drag ends are the SAME verb (ref l.269-283, byte-verified at `0x4be180`): with an
|
||||
-- empty cursor it picks the slot up, with a pet payload already held it drops onto this
|
||||
-- slot. There is no separate "place" binding on this bar, which is also why a spell or an
|
||||
-- item can never land here — nothing but a pet slot produces the payload it accepts.
|
||||
--
|
||||
-- Both also honour the lock (the ref wraps each body in `if LOCK_ACTIONBAR ~= "1" then …`).
|
||||
-- The shift-click in OnClick above is unguarded on this bar too, exactly as the reference
|
||||
-- leaves it (l.253-255): the lock stops the drag, shift-click stays the way through.
|
||||
function PetActionButton_OnDragStart()
|
||||
local button = this
|
||||
if LOCK_ACTIONBAR == "1" then return end
|
||||
PickupPetAction(button.index)
|
||||
PetActionBar_Update()
|
||||
end
|
||||
|
||||
function PetActionButton_OnReceiveDrag()
|
||||
local button = this
|
||||
if LOCK_ACTIONBAR == "1" then return end
|
||||
PickupPetAction(button.index)
|
||||
PetActionBar_Update()
|
||||
end
|
||||
|
||||
-- Ref PetActionButton_OnEnter (PetActionBarFrame.lua l.285-305). ONE predicate,
|
||||
-- `isToken or UberTooltips == "0"`, decides two things at once:
|
||||
--
|
||||
-- * WHICH TOOLTIP. A token slot has no spell to look up, so its tooltip is built here out
|
||||
-- of the two strings the info call already returned — and the BINDING (B230). Anything
|
||||
-- else is a real pet SPELL and goes through the engine's tooltip channel, which appends
|
||||
-- nothing. So the parenthesised key belongs to Attack/Follow/Stay and the reactions, and
|
||||
-- NOT to the pet's own spells: putting it on every slot would be the wrong fix.
|
||||
-- * WHERE IT SITS. Only the UberTooltips-off leg moves the tooltip beside the button
|
||||
-- (ANCHOR_RIGHT); a token with the CVar on, and every spell, take the default corner.
|
||||
-- The CVar registers "1" (cvars.rs carries the byte-read), so the off leg is the
|
||||
-- player's to reach and not what ships.
|
||||
--
|
||||
-- The suffix is the ref's own expression and it is UNCONDITIONAL — no `if the key exists`
|
||||
-- guard, unlike MicroButtonTooltipText's version of the same idea (MicroMenu.xml). That is
|
||||
-- visible, not academic: GetBindingText answers "" for a nil key (UIParent.xml, transcribed
|
||||
-- from the ref's own l.1819-1821), so 1.12 really does render "Attack ()" on a token slot
|
||||
-- whose row is unbound. Stock, none are: BONUSACTIONBUTTON1-10 default to CTRL-1..CTRL-0
|
||||
-- (bindings/commands.rs, byte-real from the install's bindings-cache.wtf).
|
||||
--
|
||||
-- Those same ten rows are the binding B218 found the CORNER hotkey glyph can never show
|
||||
-- (the header's ref-divergence list has the `bindingSuffix == this:GetID()` string-vs-number
|
||||
-- reason). Corner empty, tooltip parenthesised — both faithful, at once.
|
||||
function PetActionButton_OnEnter()
|
||||
local button = this
|
||||
if not GameTooltip then return end -- engine tests load file subsets
|
||||
if not button.tooltipName then return end
|
||||
local uber = GetCVar("UberTooltips")
|
||||
if button.isToken or uber == "0" then
|
||||
if uber == "0" then
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
else
|
||||
GameTooltip_SetDefaultAnchor(GameTooltip, button)
|
||||
end
|
||||
local key = GetBindingText(GetBindingKey("BONUSACTIONBUTTON" .. button.index), "KEY_")
|
||||
GameTooltip:SetText(button.tooltipName .. NORMAL_FONT_COLOR_CODE .. " (" .. key .. ")"
|
||||
.. FONT_COLOR_CODE_CLOSE, 1.0, 1.0, 1.0)
|
||||
if button.tooltipSubtext then
|
||||
GameTooltip:AddLine(button.tooltipSubtext, "", 0.5, 0.5, 0.5)
|
||||
end
|
||||
GameTooltip:Show()
|
||||
else
|
||||
GameTooltip_SetDefaultAnchor(GameTooltip, button)
|
||||
GameTooltip:SetPetAction(button.index)
|
||||
end
|
||||
end
|
||||
|
||||
function PetActionButton_OnLeave()
|
||||
local button = this
|
||||
if GameTooltip then GameTooltip:Hide() end
|
||||
end
|
||||
|
||||
-- ── The bar ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Ref PetActionBar_Update + PetActionBar_UpdateCooldowns, fused (one refresh event, see the
|
||||
-- header). Every branch below is the reference's, including the two that are easy to lose:
|
||||
-- an unnamed slot HIDES its button, and a slot with no texture swaps the button's ring from
|
||||
-- the filled UI-Quickslot2 to the empty UI-Quickslot.
|
||||
function PetActionBar_Update()
|
||||
local usable = GetPetActionsUsable()
|
||||
for i = 1, NUM_PET_ACTION_SLOTS do
|
||||
local button = getglobal("PetActionButton" .. i)
|
||||
local name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled = GetPetActionInfo(i)
|
||||
button.isToken = isToken
|
||||
button.tooltipSubtext = subtext
|
||||
if isToken then
|
||||
-- The token convention: what came back are the NAMES of globals.
|
||||
button.icon:SetTexture(getglobal(texture))
|
||||
button.tooltipName = getglobal(name)
|
||||
else
|
||||
button.icon:SetTexture(texture)
|
||||
button.tooltipName = name
|
||||
end
|
||||
|
||||
if isActive then button:SetChecked(1) else button:SetChecked(0) end
|
||||
if autoCastAllowed then button.autocastable:Show() else button.autocastable:Hide() end
|
||||
-- The shine marker (decision 1383, header above): showing it IS the whole
|
||||
-- enable — the native producer draws the trails wherever a shown marker sits.
|
||||
if autoCastEnabled then button.shine:Show() else button.shine:Hide() end
|
||||
|
||||
-- An unnamed slot hides — UNLESS the drop grid is up, which is the whole point of
|
||||
-- the grid: while a pet action rides the cursor every slot shows, so you can see
|
||||
-- where it may land (ref PetActionBar_Update l.122-128 + PetActionBar_ShowGrid).
|
||||
if name or (PetActionBarFrame.showgrid or 0) > 0 then
|
||||
button:Show()
|
||||
else
|
||||
button:Hide()
|
||||
end
|
||||
|
||||
if texture then
|
||||
SetDesaturation(button.icon, not usable and 1 or nil)
|
||||
button.icon:Show()
|
||||
button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2")
|
||||
else
|
||||
button.icon:Hide()
|
||||
button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot")
|
||||
end
|
||||
|
||||
local start, duration, enable = GetPetActionCooldown(i)
|
||||
CooldownFrame_SetTimer(button.cooldown, start, duration, enable)
|
||||
end
|
||||
end
|
||||
|
||||
function PetActionBar_OnLoad()
|
||||
local frame = this
|
||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
frame:RegisterEvent("PET_BAR_UPDATE")
|
||||
-- The drop grid (decision 1010). A counter rather than a flag, ref-verbatim
|
||||
-- (PetActionBar_ShowGrid/_HideGrid): our engine derives the pair off one payload edge
|
||||
-- so they strictly alternate, but the counter is what the reference keeps and it costs
|
||||
-- nothing to keep it too.
|
||||
frame:RegisterEvent("PET_BAR_SHOWGRID")
|
||||
frame:RegisterEvent("PET_BAR_HIDEGRID")
|
||||
frame.showgrid = 0
|
||||
PetActionBar_OnEvent()
|
||||
end
|
||||
|
||||
-- The bar exists exactly while PetHasActionBar() does. Its show/hide re-fires the managed
|
||||
-- bottom-stack pass (UIParent.xml, decision 0272) — the pet term of that stack is what
|
||||
-- lifts the cast bar and the chat frame clear of it, which is the ref's own
|
||||
-- PetActionBarFrame.xml:205-210.
|
||||
-- Declares nothing and reads the ambient `event`, exactly as the reference does
|
||||
-- (PetActionBarFrame.lua:33 / .xml's bare `PetActionBar_OnEvent();`). PetActionBar_OnLoad's
|
||||
-- own tail call therefore lands with `event` nil, which is the no-grid-edge path — the same
|
||||
-- thing the old explicit `eventName` argument did when omitted.
|
||||
function PetActionBar_OnEvent()
|
||||
local frame = this
|
||||
if event == "PET_BAR_SHOWGRID" then
|
||||
frame.showgrid = frame.showgrid + 1
|
||||
elseif event == "PET_BAR_HIDEGRID" and frame.showgrid > 0 then
|
||||
frame.showgrid = frame.showgrid - 1
|
||||
end
|
||||
if PetHasActionBar() then
|
||||
PetActionBar_Update()
|
||||
frame:Show()
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- ── The pet menu's two dialogs (ref StaticPopup.lua l.738-748 + l.1069-1102 + l.365-374,
|
||||
-- decision 1066) ────────────────────────────────────────────────────────────────────
|
||||
-- They live here rather than with the rows that raise them (UnitPopup.xml) for the same
|
||||
-- reason every other dialog in this codebase lives with its arc's frame file: registering
|
||||
-- into StaticPopupDialogs is a LOAD-TIME dependency on UiPanels.xml's engine, and
|
||||
-- UnitPopup.xml is a shared engine that eight isolation prefixes load without it. Raising
|
||||
-- them is not a dependency — StaticPopup_Show resolves at click time.
|
||||
--
|
||||
-- ABANDON_PET is the plain confirm. RENAME_PET is a CHAIN of two, which is the reference's
|
||||
-- own shape and not a flourish: the first collects the name, the second reads it back
|
||||
-- ("Name your pet 'Bruce'?") before anything is sent — because a rename is one-shot, so a
|
||||
-- typo is permanent in exactly the way the abandon is. That second Show happens while the
|
||||
-- first dialog is still up, which is why the engine's two instances are both needed.
|
||||
StaticPopupDialogs["ABANDON_PET"] = {
|
||||
text = ABANDON_PET,
|
||||
button1 = OKAY,
|
||||
button2 = CANCEL,
|
||||
OnAccept = function()
|
||||
PetAbandon();
|
||||
end,
|
||||
timeout = 0,
|
||||
exclusive = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["RENAME_PET"] = {
|
||||
text = PET_RENAME_LABEL,
|
||||
button1 = ACCEPT,
|
||||
button2 = CANCEL,
|
||||
hasEditBox = 1,
|
||||
-- The 12-letter cap is the reference's, and it is the client's only name check: the
|
||||
-- server re-validates with CheckPetName and refuses with SMSG_PET_NAME_INVALID.
|
||||
maxLetters = 12,
|
||||
OnAccept = function()
|
||||
-- `this` is the clicked button, so its parent is THIS dialog — never
|
||||
-- StaticPopup1 by name, which would read the wrong box the moment this dialog
|
||||
-- lands in the second instance (the chain below is exactly that case).
|
||||
local text = getglobal(this:GetParent():GetName() .. "EditBox"):GetText()
|
||||
local dialog = StaticPopup_Show("PETRENAMECONFIRM", text)
|
||||
if dialog then
|
||||
dialog.data = text
|
||||
end
|
||||
end,
|
||||
EditBoxOnEnterPressed = function()
|
||||
local text = getglobal(this:GetParent():GetName() .. "EditBox"):GetText()
|
||||
local dialog = StaticPopup_Show("PETRENAMECONFIRM", text)
|
||||
if dialog then
|
||||
dialog.data = text
|
||||
end
|
||||
this:GetParent():Hide()
|
||||
end,
|
||||
OnShow = function()
|
||||
getglobal(this:GetName() .. "EditBox"):SetFocus()
|
||||
end,
|
||||
timeout = 0,
|
||||
exclusive = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["PETRENAMECONFIRM"] = {
|
||||
-- "Name your pet '%s'?" — the name arrives as text_arg1 AND as `data`; the first
|
||||
-- fills the question, the second is what actually gets sent.
|
||||
text = PET_RENAME_CONFIRMATION,
|
||||
button1 = YES,
|
||||
button2 = NO,
|
||||
OnAccept = function(data)
|
||||
PetRename(data);
|
||||
end,
|
||||
timeout = 0,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
]]></Script>
|
||||
|
||||
<!--
|
||||
One pet button (ref PetActionButtonTemplate: 30x30, inherits ActionButtonTemplate's art
|
||||
set, plus the AutoCastable ring on OVERLAY at 58x58 CENTER). The Shine texture is the
|
||||
ref's $parentAutoCast <Model> re-expressed as the native lane's marker (decision 1383,
|
||||
the sparkle header above): its token path draws nothing itself — a shown marker is where
|
||||
`autocast_shine` plays the trails, at exactly this OVERLAY layer's paint order.
|
||||
-->
|
||||
<CheckButton name="BenillaPetActionButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="30" y="30"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentIcon"/>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="$parentAutoCastable" file="Interface\Buttons\UI-AutoCastableOverlay" hidden="true">
|
||||
<Size><AbsDimension x="58" y="58"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<!-- The ref's $parentAutoCast <Model> here IS setAllPoints on the 30x30 button,
|
||||
at scale="1.2" — the suffix carries that scale the way the ref's own XML
|
||||
does (its sibling in SpellBookFrame.xml writes a different one). -->
|
||||
<Texture name="$parentShine" file="benilla:autocast-shine:1.2" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMRIGHT"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- Ref PetActionButton_OnLoad sizes this 33x33 at CENTER (-2,-1) by hand rather than
|
||||
taking the template's own rect — quoted. -->
|
||||
<Cooldown name="$parentCooldown" hidden="true">
|
||||
<Size><AbsDimension x="33" y="33"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-2" y="-1"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Cooldown>
|
||||
</Frames>
|
||||
<NormalTexture name="$parentNormalTexture" file="Interface\Buttons\UI-Quickslot2">
|
||||
<Size><AbsDimension x="54" y="54"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</NormalTexture>
|
||||
<PushedTexture file="Interface\Buttons\UI-Quickslot-Depress"/>
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
|
||||
<CheckedTexture file="Interface\Buttons\CheckButtonHilight" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnClick>PetActionButton_OnClick(arg1)</OnClick>
|
||||
<OnDragStart>PetActionButton_OnDragStart()</OnDragStart>
|
||||
<OnReceiveDrag>PetActionButton_OnReceiveDrag()</OnReceiveDrag>
|
||||
<OnEnter>PetActionButton_OnEnter()</OnEnter>
|
||||
<OnLeave>PetActionButton_OnLeave()</OnLeave>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
|
||||
<Frame name="PetActionBarFrame" enableMouse="true" parent="MainMenuBar" hidden="true">
|
||||
<Size><AbsDimension x="509" y="43"/></Size>
|
||||
<!-- This anchor is the LOAD-TIME value only: UIParent_ManageFramePositions owns the real
|
||||
one and re-seats the frame on every pass (OnShow/OnHide below fire it), because the
|
||||
Y is a managed var — the ref's own PETACTIONBAR_YPOS, 97 here and 97+43 with the
|
||||
bottom-left multibar up. Leaving it seated at the base 97 is what drew the bar across
|
||||
that bar's buttons (decision 0988). The X likewise: 36, or past the last stance
|
||||
button when the two bars share this row.
|
||||
The ref also slides the frame in over 0.09 s; we don't. The slide's start and end
|
||||
anchors are the same two points, and a frame that only ever appears with a pet reads
|
||||
the same arriving seated as it does arriving in 90 ms (the stance bar's own posture,
|
||||
decision 0270). -->
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="MainMenuBar" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="36" y="97"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="SlidingActionBarTexture0" file="Interface\PetActionBar\UI-PetBar">
|
||||
<Size><AbsDimension x="256" y="44"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<TexCoords left="0" right="1.0" top="0.015625" bottom="0.359375"/>
|
||||
</Texture>
|
||||
<Texture name="SlidingActionBarTexture1" file="Interface\PetActionBar\UI-PetBar">
|
||||
<Size><AbsDimension x="184" y="44"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="SlidingActionBarTexture0" relativePoint="RIGHT"/></Anchors>
|
||||
<TexCoords left="0" right="0.71875" top="0.375" bottom="0.71875"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<CheckButton name="PetActionButton1" inherits="BenillaPetActionButtonTemplate" id="1">
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="36" y="2"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 1)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton2" inherits="BenillaPetActionButtonTemplate" id="2">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton1" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 2)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton3" inherits="BenillaPetActionButtonTemplate" id="3">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton2" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 3)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton4" inherits="BenillaPetActionButtonTemplate" id="4">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton3" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 4)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton5" inherits="BenillaPetActionButtonTemplate" id="5">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton4" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 5)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton6" inherits="BenillaPetActionButtonTemplate" id="6">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton5" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 6)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<!-- The ref's own +7 here, not +8 (PetActionBarFrame.xml l.158-166). -->
|
||||
<CheckButton name="PetActionButton7" inherits="BenillaPetActionButtonTemplate" id="7">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton6" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 7)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton8" inherits="BenillaPetActionButtonTemplate" id="8">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton7" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 8)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton9" inherits="BenillaPetActionButtonTemplate" id="9">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton8" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 9)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="PetActionButton10" inherits="BenillaPetActionButtonTemplate" id="10">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetActionButton9" relativePoint="RIGHT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaPetActionButton_OnLoad(self, 10)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>PetActionBar_OnLoad()</OnLoad>
|
||||
<OnEvent>PetActionBar_OnEvent()</OnEvent>
|
||||
<OnShow>UIParent_ManageFramePositions()</OnShow>
|
||||
<OnHide>UIParent_ManageFramePositions()</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,42 @@
|
||||
<Ui>
|
||||
<!--
|
||||
BENILLA_ALLOW_OWN_UI=1 — the one adapter the reference's own PetActionBarFrame.xml needs
|
||||
from this engine (decision 1953; 1751 §2 keeps an adapter for exactly a genuine engine
|
||||
difference).
|
||||
|
||||
The autocast shine. The reference's `$parentAutoCast` on every pet button is a <Model>
|
||||
playing `Interface\Buttons\UI-AutoCastButton.mdx`, setAllPoints on the 30x30 button at
|
||||
scale 1.2, shown and hidden by its own `PetActionBar_Update` (PetActionBarFrame.lua
|
||||
l.135-139). This engine's <Model> holds the scene the Lua API reads and writes and draws no
|
||||
pixels (`script/modelframe.rs`); the shine benilla draws is its own lane — the
|
||||
`benilla:autocast-shine:<scale>` token texture (decisions 1383/1387, `autocast_shine.rs`),
|
||||
which reads the model's OWN tracks out of the shipped .m2. So each button gets a Shine
|
||||
texture over its whole rect carrying the reference's own 1.2, and the reference's
|
||||
`PetActionBar_Update` is wrapped to carry the shine with the model.
|
||||
|
||||
Nothing else. The reference's file is sourced unedited: its Models exist and are harmless.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
for i = 1, NUM_PET_ACTION_SLOTS do
|
||||
local button = getglobal("PetActionButton" .. i)
|
||||
local shine = button:CreateTexture("PetActionButton" .. i .. "Shine", "OVERLAY")
|
||||
shine:SetTexture("benilla:autocast-shine:1.2")
|
||||
shine:SetAllPoints(button)
|
||||
shine:Hide()
|
||||
end
|
||||
|
||||
local reference_PetActionBar_Update = PetActionBar_Update
|
||||
function PetActionBar_Update()
|
||||
reference_PetActionBar_Update()
|
||||
for i = 1, NUM_PET_ACTION_SLOTS do
|
||||
local model = getglobal("PetActionButton" .. i .. "AutoCast")
|
||||
local shine = getglobal("PetActionButton" .. i .. "Shine")
|
||||
if model:IsShown() then
|
||||
shine:Show()
|
||||
else
|
||||
shine:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
</Ui>
|
||||
@@ -1,874 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's Skills tab (decision 0437 phase 4): the 1.12 `SkillFrame`, re-expressed from the
|
||||
reference FrameXML `SkillFrame.{xml,lua}` (session scratchpad `ref-ui/`, decision 0246
|
||||
discipline — never committed) onto our own engine/feeds. Geometry/art-paths below are QUOTED
|
||||
from the ref (line citations inline, into that same `SkillFrame.xml`/`.lua`); the Lua is
|
||||
the ref's logic turned onto benilla's Era API (`GetNumSkillLines`/`GetSkillLineInfo`/
|
||||
`Expand`/`CollapseSkillHeader`/`SetSelectedSkill`/`GetSelectedSkill`, decision 0437's skills
|
||||
seam, `crates/benilla-ui/src/script/skills.rs`, built alongside this file).
|
||||
|
||||
A CHILD PAGE of `CharacterFrame` (ref: `parent="CharacterFrame"`, `id="4"` — ours too, since
|
||||
the Reputation page landed and took slot 3 back), shown/
|
||||
hidden by `CharacterFrame_ShowSubFrame` exactly like `PaperDollFrame` — but NOT a
|
||||
physically nested `<Frame>` the way PaperDollFrame is: PaperDollFrame is declared INSIDE
|
||||
CharacterFrame.xml's own `<Frames>` tree (a real engine parent/child), while this page is a
|
||||
SEPARATE file's top-level `<Frame>` (CharacterFrame.xml is shared-tree with another session
|
||||
this pass, so only its tab machinery is touched here — not its frame tree). `parent=` really attaches
|
||||
(decision 1734; the loader resolves the name globally at load and it wins over the enclosing
|
||||
element — `shipped_xml_tests::every_declared_parent_really_attaches` asserts it on the loaded
|
||||
tree). The line above used to say the attribute was read nowhere, which was true once and had
|
||||
outlived itself. Position still rides `<Anchors relativeTo="CharacterFrame">` — a NAMED
|
||||
lookup that works regardless of tree position, the same mechanism every sibling-anchor in
|
||||
this codebase uses, kept because it is the geometry this page ships with — loaded right after
|
||||
`CharacterFrame.xml` in the load list so it
|
||||
paints ON TOP of it (later file = higher z, the established house convention every top-level
|
||||
window already relies on implicitly). Because a true engine Hide-cascade doesn't reach a
|
||||
non-child, `CharacterFrame.xml`'s own `CharacterFrame_OnHide` was extended (its own
|
||||
header comment) to loop-hide every `CHARACTERFRAME_SUBFRAMES` entry — the fix this
|
||||
page's non-nested shape requires, done once so later non-nested tabs (Pet/Reputation/Honor)
|
||||
inherit it for free.
|
||||
|
||||
NO OWN PORTRAIT (matches the ref: `ref-ui/SkillFrame.xml` l.130-213 draws no portrait Texture
|
||||
at all): the shared quadrant background art below (`UI-Character-General-TopLeft/TopRight`,
|
||||
the SAME "General" variant the ref uses, l.136-159) carries the SAME baked-in portrait-ring
|
||||
cutout every PaperDollInfoFrame quadrant variant shares, so `CharacterFrame`'s own
|
||||
portrait/name/level (drawn at a lower z, underneath) shows through unchanged — exactly the
|
||||
ref's own real-client compositing (SkillFrame is a nested child there too, relying on the
|
||||
same cutout rather than redrawing the portrait itself).
|
||||
|
||||
THE DETAIL PANE NESTS, as the ref's does. This file used to carry the FLAT-layout claim —
|
||||
"only a plain child `<Frame>` loses [its Layers]" — quoted from TrainerFrame.xml. That claim
|
||||
is FALSE and was retracted in decision 1517, which corrected TrainerFrame/TradeSkillFrame/
|
||||
CraftFrame and missed this file: a fourth restatement, still asserting the constraint five
|
||||
weeks after it was disproved. A runtime-shown child `<Frame>` renders its own `<Layers>`
|
||||
FontStrings in this engine, and the ref's `SkillDetailScrollFrame` +
|
||||
`SkillDetailScrollChildFrame` are therefore built here verbatim (below).
|
||||
1517 §2 says a retracted reason is not by itself cause to move approved geometry — so the
|
||||
mover here is not the retraction, it is DEMAND: pfUI's `skins/blizzard/character.lua` l.299-320
|
||||
indexes `SkillDetailScrollFrameScrollBar`, `SkillDetailScrollChildFrame`, `SkillDetailCostText`
|
||||
and `SkillDetailStatusBarUnlearnButton` by name, and its own skin then hides the scroll child
|
||||
and reparents the three detail regions onto the scroll frame — i.e. it flattens the pane
|
||||
ITSELF, which it can only do if the pane is nested to begin with. The pane also could not
|
||||
keep its old size inside the ref's 296-wide viewport: the old detail bar reused the 271-wide
|
||||
LIST-ROW template, whose 281-wide Border put the abandon button at x 312..344 against a
|
||||
viewport ending at 318 — 26 of its 32 px clipped. So the detail bar is now the ref's OWN
|
||||
declaration (211×15, Border 220×32, centred under the list) rather than a borrowed list row,
|
||||
which is both what fits and what the real client draws. LOOK CHANGE, called out for the
|
||||
director: the detail bar narrows and centres, and the ref's two `UI-ClassTrainer-ScrollBar`
|
||||
trough textures now flank the pane's right edge (the ref draws them unconditionally, bar or
|
||||
no bar). Reverting is one anchor + one Size.
|
||||
|
||||
v1 DEVIATIONS (each re-cited at its own site below):
|
||||
1. THE DETAIL PANE OMITS THE TRAINING-POINT SPEND ROW, and nothing else. Its structure
|
||||
(`SkillDetailScrollFrame` → `SkillDetailScrollChildFrame` → `SkillDetailCostText` +
|
||||
`SkillDetailDescriptionText` + `SkillDetailStatusBar` → `$parentUnlearnButton`) is the
|
||||
ref's, verbatim. What stays OUT is the ref's `$parentLeftArrow`/`$parentRightArrow`/
|
||||
`$parentLearnSkillButton` trio and the `AddSkillUp`/`RemoveSkillUp`/`BuySkillTier`
|
||||
calls behind them — driven by `stepCost`/`rankCost`/`numTempPoints`, which
|
||||
`GetSkillLineInfo` always reports nil/0 (0437's own named out-of-scope: 1.12 training
|
||||
points are dead data).
|
||||
`SkillDetailCostText` IS built, because the ref's own no-cost path is a real path, not
|
||||
a stub: ref `SkillFrame.lua` l.361 `SkillDetailCostText:Hide()` is the branch a live
|
||||
1.12 server always lands on, and `BenillaSkillFrame_UpdateDetail` takes exactly that
|
||||
branch every time. An empty hidden FontString here is the reference's own behaviour,
|
||||
not a name published to satisfy a lookup (1189/1203).
|
||||
The ABANDON BUTTON is real: `SkillDetailStatusBarUnlearnButton` (the ref's
|
||||
`$parentUnlearnButton`) → the `UNLEARN_SKILL` confirm → `AbandonSkill` →
|
||||
`CMSG_UNLEARN_SKILL`; the isAbandonable predicate is `SkillRaceClassInfo.flags & 0x20`
|
||||
— the server's own gate, real 5875 data: primary professions only. It used to be
|
||||
published as `SkillDetailUnlearnButton`, a name the reference does not have, while the
|
||||
comment directly above it cited `$parentUnlearnButton` correctly — the fifth instance
|
||||
this session of right-behaviour-under-a-private-name (1189).
|
||||
2. NO SORT CHECKBOX (ref l.215-241's own XML-commented-out "Replace once code is ready..."
|
||||
`SkillSortButton`): the ref itself ships this dead/commented, so there is nothing to
|
||||
transcribe (TradeSkillFrame.xml's own "ref's own dead code is dropped" precedent).
|
||||
3. NO ACCEPT BUTTON (ref l.318-337's `SkillFrameAcceptButton`) — it is XML-commented dead
|
||||
in the ref, and it backs the training-point economy this pane doesn't ship (deviation 1),
|
||||
so there is nothing to transcribe. Its neighbour `SkillFrameCancelButton` (l.339-357) is
|
||||
NOT dead and IS built: the ref's comment closes at l.337, one line above it, and the real
|
||||
client draws a CLOSE button in the page's bottom-right seat. This list used to claim both
|
||||
were commented out, and that misread is exactly why that seat rendered empty — decision
|
||||
1496. Only its `CancelSkillUps()` call drops (deviation 1's dead economy again); the two
|
||||
hides are transcribed.
|
||||
4. THE RANK TEXT FORMAT IS THE REF'S BYTE-EXACT ONE, not the task brief's own paraphrase:
|
||||
`ref-ui/SkillFrame.lua` l.178-186 builds `rank .. " (" .. color .. modifier .. close ..
|
||||
")/" .. max` (e.g. "57 (|cff20ff20+3|r)/75") — transcribed verbatim (the
|
||||
reference file is the authority on the tuple/format, the brief's prose is a summary).
|
||||
5. NO FILLBAR (ref's `$parentFillBar`, driven by `numTempPoints`): [`GetSkillLineInfo`]
|
||||
always reports `numTempPoints = 0` (skills.rs's own module doc — 1.12 training points are
|
||||
dead data), so the fill bar could never show — dropped outright, `TradeSkillFrame.xml`'s
|
||||
OWN `BenillaTradeSkillRankFrame` (the identical StatusBar shape) already sets this local
|
||||
precedent for the same reason.
|
||||
6. NO HOVER TOOLTIP on a row: the ref's own `SkillFrame.lua` calls `GameTooltip` NOWHERE
|
||||
(grepped the full reference file) — a skill line is pure descriptor state, nothing to
|
||||
preview. Unlike Trainer/TradeSkill, this window needs no `GameTooltip:Set*Item` channel.
|
||||
7. `LockHighlight`/`UnlockHighlight` ENGINE GAP (TalentFrame.xml/TradeSkillFrame.xml's own
|
||||
"no engine equivalent" posture, grepped `benilla-ui/src/script/button.rs` — ships
|
||||
texture-pairs/SetText/Enable/Disable/RegisterForClicks/SetButtonState/Click only): the
|
||||
persistent selection glow is `SkillHighlight`, the separate overlay Frame
|
||||
Trainer/TradeSkillFrame already use, moved onto the selected row's StatusBar.
|
||||
8. GlobalStrings this window itself needs: NONE beyond the tab label `SKILLS` (already
|
||||
declared in CharacterFrame.xml, quoted there byte-exact with its GlobalStrings.lua line
|
||||
cite) — the ref's own `SkillFrame.lua` reads no other GlobalString.
|
||||
|
||||
Rows repaint on `SKILL_LINES_CHANGED` (registered here; `crates/benilla/src/ui_char.rs`'s
|
||||
`watch_skill_ups`/`feed_skills` already fire it) and on `OnShow` (ref `SkillFrame_OnShow`,
|
||||
l.4-6). The list is the house faux-scroll kit (ScrollTemplates.xml, the TrainerFrame/
|
||||
TradeSkillFrame precedent) over 12 fixed row slots (ref `SKILLS_TO_DISPLAY = 12`,
|
||||
`SkillFrame.lua:1` — the ref's own row count, and this page shares the ref's own 384×512
|
||||
coordinate space since it fills the identical CharacterFrame page area).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ── Constants (ref SkillFrame.lua:1-2) ──────────────────────────────────────────────────────
|
||||
-- SKILLS_TO_DISPLAY is the reference's own GLOBAL and is published as one. It was a chunk
|
||||
-- local named SKILLS_TO_DISPLAY with the real name in a trailing comment — the
|
||||
-- value transcribed, the NAME kept private, which is the same shape that cost us
|
||||
-- BlackoutWorld and the five SkillFrame* children. Addons read it: pfUI's character skin
|
||||
-- loops `for i = 1, SKILLS_TO_DISPLAY` over the row labels
|
||||
-- (skins/blizzard/character.lua:277) and died on "'for' limit must be a number".
|
||||
-- 12 is honest here rather than copied — this file declares exactly 12 SkillTypeLabel rows.
|
||||
SKILLS_TO_DISPLAY = 12
|
||||
local SKILLFRAME_SKILL_HEIGHT = 15 -- SKILLFRAME_SKILL_HEIGHT
|
||||
|
||||
-- GlobalStrings (ref GlobalStrings.lua:3478), chunk-local per the codebase idiom — safe
|
||||
-- because only THIS chunk's functions read it; the nil-"All" trap (TradeSkillFrame.xml's
|
||||
-- deviation notes) is the inline-XML-handler case, and no inline handler references it.
|
||||
-- Its earlier ABSENCE here made format() throw in UpdateDetail on every row selection —
|
||||
-- half of the 2026-07-17 "tabs are flaky" report (the silent-error half).
|
||||
local SKILL_DESCRIPTION = "|cffffffff%s|r %s"
|
||||
local UNLEARN_SKILL = "Do you want to unlearn %s?" -- GlobalStrings.lua:4446
|
||||
local UNLEARN = "Unlearn" -- GlobalStrings.lua:4445
|
||||
local UNLEARN_SKILL_TOOLTIP = "Unlearn this profession" -- GlobalStrings.lua:4447
|
||||
|
||||
-- Paint the rank StatusBar `bar` + its `SkillName`/`SkillRank` FontStrings for one skill-line
|
||||
-- ENTRY row (never a header — callers guard that): the ref's own bar-fill + rank-text half of
|
||||
-- `SkillFrame_SetStatusBar` (l.76-187) and the detail's twin `SkillDetailFrame_SetStatusBar`
|
||||
-- (l.161-187, l.370-399) — the SAME formatting either pane uses, factored once here since our
|
||||
-- minimal detail pane (deviation 1) needs nothing else from that function.
|
||||
function BenillaSkillFrame_PaintBar(bar, skillName, skillRank, skillModifier, skillMaxRank)
|
||||
local nameText = getglobal(bar:GetName() .. "SkillName")
|
||||
local rankText = getglobal(bar:GetName() .. "SkillRank")
|
||||
-- The trough behind the fill (ref `$parentBackground`), recoloured per branch alongside
|
||||
-- the fill itself — ref SkillFrame.lua:158 (normal) and :167 (proficiency), and the
|
||||
-- detail pane's identical pair at :356/:376. The ref sets the normal-skill pair BEFORE
|
||||
-- its `skillMaxRank` gate and lets the proficiency arm overwrite it; setting each arm's
|
||||
-- own pair here is the same result with no dead write. The ref's THIRD writer of this
|
||||
-- texture — the `skillCostType == 1/2/3` green/yellow/red block at :96-111, and the
|
||||
-- `stepCost` "Learn <skill>" gray at :126 — is unreachable for us and would be
|
||||
-- overwritten here even if it weren't: `stepCost`/`rankCost` are always nil and
|
||||
-- `numTempPoints` always 0 (skills.rs's module doc — dead 1.12 data, deviation 1/5), so
|
||||
-- every entry row takes the ref's own `else -- Normal skill` arm.
|
||||
local bgTexture = getglobal(bar:GetName() .. "Background")
|
||||
nameText:SetText(skillName)
|
||||
nameText:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
|
||||
rankText:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
|
||||
if skillMaxRank == 1 then
|
||||
-- ref SkillFrame.lua:161-167 verbatim: "If max rank in a skill is 1 assume that
|
||||
-- its a proficiency" — a FULL gray bar and NO rank text. Two kinds of line land
|
||||
-- here, and the ref can't tell them apart because it doesn't need to: the armor
|
||||
-- proficiencies, which the server really does report as 1/1, and the single-rank
|
||||
-- lines (class skills, Dual Wield, racials, the per-mount riding lines) whose
|
||||
-- `skillMaxRank` the engine already overrode to 1 off
|
||||
-- `SkillRaceClassInfo.flags & 0x400` (skills.rs's `SkillEntry::mono`) — which is
|
||||
-- why a hunter's `Beast Mastery 300/300` draws gray and numberless here, exactly
|
||||
-- as it does in the real client.
|
||||
bar:SetMinMaxValues(0, 1)
|
||||
bar:SetValue(1)
|
||||
bar:SetStatusBarColor(0.5, 0.5, 0.5)
|
||||
rankText:SetText("")
|
||||
bgTexture:SetVertexColor(1.0, 1.0, 1.0, 0.5)
|
||||
elseif skillMaxRank == 0 then
|
||||
-- Not reachable off a live server (a line the player HAS always carries a max ≥ 1)
|
||||
-- and the ref leaves this case to fall through its own if/elseif untouched —
|
||||
-- which, on a recycled row, would leave the PREVIOUS skill's bar and rank text
|
||||
-- sitting there. We blank instead: a stale-state hole is a quirk, not behaviour.
|
||||
bar:SetMinMaxValues(0, 1)
|
||||
bar:SetValue(0)
|
||||
bar:SetStatusBarColor(0, 0, 0, 0)
|
||||
rankText:SetText("")
|
||||
-- back to the template's own declared base, so a recycled row shows a neutral empty
|
||||
-- trough rather than the previous skill's blue or white. The base lives in the
|
||||
-- TEXEL (the template's `<Color 1,1,1,0.2>`), so restoring it means an IDENTITY
|
||||
-- vertex colour — writing 0.2 here would multiply it to 0.04 (decision 1101).
|
||||
bgTexture:SetVertexColor(1.0, 1.0, 1.0, 1.0)
|
||||
else
|
||||
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5)
|
||||
bgTexture:SetVertexColor(0.0, 0.0, 0.75, 0.5)
|
||||
bar:SetMinMaxValues(0, skillMaxRank)
|
||||
bar:SetValue(skillRank)
|
||||
-- the ref's rank-text law (SkillFrame.lua:178-186), re-expressed here (this file's
|
||||
-- header comment, deviation 4).
|
||||
if skillModifier == 0 then
|
||||
rankText:SetText(skillRank .. "/" .. skillMaxRank)
|
||||
else
|
||||
local color = RED_FONT_COLOR_CODE
|
||||
if skillModifier > 0 then
|
||||
color = GREEN_FONT_COLOR_CODE .. "+"
|
||||
end
|
||||
rankText:SetText(skillRank .. " (" .. color .. skillModifier .. FONT_COLOR_CODE_CLOSE .. ")/" .. skillMaxRank)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fill the detail pane (deviation 1) for the current `GetSelectedSkill()`, or blank it
|
||||
-- when nothing is selected / the selection isn't a visible entry. The description body is
|
||||
-- the ref's own (ref SkillFrame.lua l.211-217 hide/show alongside the bar; l.262 the
|
||||
-- SKILL_DESCRIPTION format): `skillType` stays "" in EVERY reachable 1.12 branch — the
|
||||
-- ref's single assignment ("Tertiary Skill:") is commented out in the reference itself —
|
||||
-- so the format's first slot renders empty, byte-faithful. The unlearn button rides the
|
||||
-- same fill (ref l.220-226): shown only for an abandonable line, carrying the name the
|
||||
-- confirm popup formats and the index its accept abandons.
|
||||
function BenillaSkillFrame_UpdateDetail()
|
||||
local bar = getglobal("SkillDetailStatusBar")
|
||||
local desc = getglobal("SkillDetailDescriptionText")
|
||||
local unlearn = getglobal("SkillDetailStatusBarUnlearnButton")
|
||||
-- The ref's own no-cost branch (ref SkillFrame.lua l.361), taken unconditionally:
|
||||
-- stepCost/rankCost are always nil on a 1.12 server (deviation 1). Hidden here rather
|
||||
-- than left `hidden="true"` from XML so the branch is visible as a branch — the ref
|
||||
-- shows this string on the paths we don't have, and this is where it would be re-shown.
|
||||
getglobal("SkillDetailCostText"):Hide()
|
||||
local index = GetSelectedSkill()
|
||||
if index == 0 then
|
||||
bar:Hide()
|
||||
desc:Hide()
|
||||
unlearn:Hide()
|
||||
return
|
||||
end
|
||||
local skillName, isHeader, _, skillRank, _, skillModifier, skillMaxRank, isAbandonable = GetSkillLineInfo(index)
|
||||
if not skillName or isHeader then
|
||||
bar:Hide()
|
||||
desc:Hide()
|
||||
unlearn:Hide()
|
||||
return
|
||||
end
|
||||
bar:Show()
|
||||
BenillaSkillFrame_PaintBar(bar, skillName, skillRank, skillModifier, skillMaxRank)
|
||||
if isAbandonable then
|
||||
unlearn.skillName = skillName
|
||||
unlearn.index = index
|
||||
unlearn:Show()
|
||||
else
|
||||
unlearn:Hide()
|
||||
end
|
||||
local skillType = ""
|
||||
local skillDescription = select(13, GetSkillLineInfo(index)) or ""
|
||||
desc:SetText(format(SKILL_DESCRIPTION, skillType, skillDescription))
|
||||
desc:Show()
|
||||
end
|
||||
|
||||
-- ── The abandon confirm + the unlearn button's handlers ──────────────────────────────────
|
||||
-- The dialog is ref StaticPopup.lua:1121-1133 verbatim (minus the vestigial TEXT()
|
||||
-- wrapper); its OnAccept receives dialog.data — the visible index the button stashed.
|
||||
-- CANCEL stays the bare GlobalStrings global (DeathFrame.xml's own convention — the app
|
||||
-- loads the real GlobalStrings.lua before any window file). The handlers are chunk
|
||||
-- functions, NOT inline bodies, so the chunk-local strings above are visible to them
|
||||
-- (the nil-"All" chunk-boundary rule, TradeSkillFrame.xml's deviation notes).
|
||||
StaticPopupDialogs["UNLEARN_SKILL"] = {
|
||||
text = UNLEARN_SKILL,
|
||||
button1 = UNLEARN,
|
||||
button2 = CANCEL,
|
||||
OnAccept = function(index)
|
||||
AbandonSkill(index)
|
||||
end,
|
||||
timeout = 60,
|
||||
exclusive = 1,
|
||||
whileDead = 1,
|
||||
showAlert = 1,
|
||||
hideOnEscape = 1
|
||||
}
|
||||
|
||||
function BenillaSkillUnlearnButton_OnClick(btn)
|
||||
local dialog = StaticPopup_Show("UNLEARN_SKILL", btn.skillName)
|
||||
if dialog then
|
||||
dialog.data = btn.index
|
||||
end
|
||||
end
|
||||
|
||||
function BenillaSkillUnlearnButton_OnEnter(btn)
|
||||
GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(UNLEARN_SKILL_TOOLTIP)
|
||||
end
|
||||
|
||||
-- Paint list slot `i` (1..12) from the visible row at `index` (ref SkillFrame_SetStatusBar,
|
||||
-- l.24-188 — the header branch, l.55-69, and the entry branch, l.76-187, collapsed: the
|
||||
-- skillCostType coloring + stepCost/rankCost training-up arrows never apply here, since
|
||||
-- GetSkillLineInfo always reports them nil/0, this file's header comment deviation 1).
|
||||
function BenillaSkillFrame_SetRow(i, index, numRows)
|
||||
local bar = getglobal("SkillRankFrame" .. i)
|
||||
local label = getglobal("SkillTypeLabel" .. i)
|
||||
|
||||
if index > numRows then
|
||||
bar:Hide()
|
||||
bar.skillIndex = nil
|
||||
label:Hide()
|
||||
label.skillIndex = nil
|
||||
return
|
||||
end
|
||||
|
||||
local skillName, isHeader, isExpanded, skillRank, _, skillModifier, skillMaxRank = GetSkillLineInfo(index)
|
||||
|
||||
if isHeader then
|
||||
bar:Hide()
|
||||
bar.skillIndex = nil
|
||||
label:Show()
|
||||
label.skillIndex = index
|
||||
label:SetText(skillName)
|
||||
if isExpanded then
|
||||
label:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
|
||||
else
|
||||
label:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
label:Hide()
|
||||
label.skillIndex = nil
|
||||
bar:Show()
|
||||
bar.skillIndex = index
|
||||
BenillaSkillFrame_PaintBar(bar, skillName, skillRank, skillModifier, skillMaxRank)
|
||||
end
|
||||
|
||||
-- The list page (ref SkillFrame_UpdateSkills, l.402-448 — the dead
|
||||
-- `UnitCharacterPoints("player")`/`SkillFrame.UpdatedSkillPoints` read, l.437-440, whose
|
||||
-- result nothing downstream ever consumes even in the ref itself, is dropped — the
|
||||
-- TradeSkillFrame.xml "ref's own dead code is dropped" precedent).
|
||||
-- The scroll frame's `<OnVerticalScroll>` is its own chunk and cannot see this file's
|
||||
-- `local SKILLFRAME_SKILL_HEIGHT`, so the pitch is handed over here rather than written as
|
||||
-- a literal in the XML (1868; the trainer paid for this lesson first, in 1860).
|
||||
function BenillaSkillList_OnVerticalScroll()
|
||||
FauxScrollFrame_OnVerticalScroll(SKILLFRAME_SKILL_HEIGHT, BenillaSkillFrame_Update)
|
||||
end
|
||||
|
||||
function BenillaSkillFrame_Update()
|
||||
local numRows = GetNumSkillLines()
|
||||
local scrollFrame = getglobal("SkillListScrollFrame")
|
||||
FauxScrollFrame_Update(scrollFrame, numRows, SKILLS_TO_DISPLAY, SKILLFRAME_SKILL_HEIGHT)
|
||||
-- Our trough follows the bar the reference's `_Update` just decided about; nothing in
|
||||
-- its kit shows one, because the reference has no trough at all (1860).
|
||||
BenillaScrollTrough_Follow("SkillListScrollFrame")
|
||||
local offset = FauxScrollFrame_GetOffset(scrollFrame)
|
||||
|
||||
local highlight = getglobal("SkillHighlight")
|
||||
highlight:Hide()
|
||||
local selected = GetSelectedSkill()
|
||||
|
||||
for i = 1, SKILLS_TO_DISPLAY do
|
||||
local index = i + offset
|
||||
BenillaSkillFrame_SetRow(i, index, numRows)
|
||||
if selected == index and index <= numRows then
|
||||
highlight:SetPoint("TOPLEFT", getglobal("SkillRankFrame" .. i), "TOPLEFT", -5, 0)
|
||||
highlight:Show()
|
||||
end
|
||||
end
|
||||
|
||||
-- The collapse-all tab's +/- icon (ref l.417-429's own numHeaders/notExpanded scan).
|
||||
local allExpanded = true
|
||||
for i = 1, numRows do
|
||||
local _, isHeader, isExpanded = GetSkillLineInfo(i)
|
||||
if isHeader and not isExpanded then
|
||||
allExpanded = false
|
||||
end
|
||||
end
|
||||
local collapseAll = getglobal("SkillFrameCollapseAllButton")
|
||||
if numRows == 0 then
|
||||
collapseAll:Disable()
|
||||
else
|
||||
collapseAll:Enable()
|
||||
end
|
||||
if allExpanded then
|
||||
collapseAll.collapsed = nil
|
||||
collapseAll:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
|
||||
else
|
||||
collapseAll.collapsed = 1
|
||||
collapseAll:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
|
||||
end
|
||||
|
||||
BenillaSkillFrame_UpdateDetail()
|
||||
BenillaSkillFrameExpandButtonFrame_Fit()
|
||||
end
|
||||
|
||||
-- A rank-bar click selects it (ref SkillBar_OnClick, l.450-455).
|
||||
function BenillaSkillRankFrame_OnClick(bar)
|
||||
local index = bar.skillIndex
|
||||
if not index then
|
||||
return
|
||||
end
|
||||
SetSelectedSkill(index)
|
||||
BenillaSkillFrame_Update()
|
||||
end
|
||||
|
||||
-- A header click folds/unfolds its group (ref SkillLabelTemplate's own OnClick, l.72-80).
|
||||
function BenillaSkillTypeLabel_OnClick(label)
|
||||
local index = label.skillIndex
|
||||
if not index then
|
||||
return
|
||||
end
|
||||
local _, _, isExpanded = GetSkillLineInfo(index)
|
||||
if isExpanded then
|
||||
CollapseSkillHeader(index)
|
||||
-- A fold reshuffles visible indices — an UNLEARN_SKILL confirm armed for a row
|
||||
-- that no longer holds the selection must not fire against the wrong line (ref
|
||||
-- SkillLabelTemplate's own OnClick, ref SkillFrame.xml l.72-78).
|
||||
local dialog = StaticPopup_FindVisible("UNLEARN_SKILL")
|
||||
if dialog and dialog.data ~= GetSelectedSkill() then
|
||||
StaticPopup_Hide("UNLEARN_SKILL")
|
||||
end
|
||||
else
|
||||
ExpandSkillHeader(index)
|
||||
end
|
||||
BenillaSkillFrame_Update()
|
||||
end
|
||||
|
||||
-- The collapse-all tab (ref SkillFrameCollapseAllButton's own isExpanded flag, l.303-309,
|
||||
-- driven the TrainerFrame.xml BenillaTrainerCollapseAllButton way since this engine's
|
||||
-- CollapseSkillHeader(0)/ExpandSkillHeader(0) already fold/unfold every group at once).
|
||||
function BenillaSkillFrameCollapseAllButton_OnClick()
|
||||
local b = getglobal("SkillFrameCollapseAllButton")
|
||||
if b.collapsed then
|
||||
ExpandSkillHeader(0)
|
||||
else
|
||||
CollapseSkillHeader(0)
|
||||
FauxScrollFrame_SetOffset(getglobal("SkillListScrollFrame"), 0)
|
||||
end
|
||||
BenillaSkillFrame_Update()
|
||||
end
|
||||
|
||||
-- The tab is as wide as its own label (ref SkillFrameExpandButtonFrame's OnLoad, l.312-316:
|
||||
-- SetWidth(SkillFrameCollapseAllButton:GetTextWidth()+45)) — the 45 is the two 8px caps plus
|
||||
-- the label's 25px seat plus the margin the ref leaves before the right cap, and the middle
|
||||
-- slab stretches into whatever that leaves off its own two anchors.
|
||||
--
|
||||
-- Run from BOTH the ref's own OnLoad and every Update, because a measure here is not the
|
||||
-- ref's synchronous one: benilla seats the VM's font engine at the frame boundary
|
||||
-- (`ui_script::extract`, ahead of the tick), and XML load happens BEFORE any frame — so the
|
||||
-- OnLoad call reads 0 and the tab would keep a 45px stump forever. The zero guard is what
|
||||
-- makes the pair safe: an unmeasured call leaves the width alone rather than squashing the
|
||||
-- tab, and the first Update after the atlas exists fits it. (The house's own preference is
|
||||
-- two opposing anchors and no Lua width — UiPanels.xml's tab-highlight note — but a span
|
||||
-- can only be resolved from two anchors when both edges are frames; a text width has to be
|
||||
-- asked for.)
|
||||
function BenillaSkillFrameExpandButtonFrame_Fit()
|
||||
local label = getglobal("SkillFrameCollapseAllButton")
|
||||
local w = label:GetTextWidth()
|
||||
if w and w > 0 then
|
||||
getglobal("SkillFrameExpandButtonFrame"):SetWidth(w + 45)
|
||||
end
|
||||
end
|
||||
|
||||
-- The page's own CLOSE (ref SkillFrameCancelButton's OnClick, l.350-356). The ref's
|
||||
-- CancelSkillUps() drops out with the training-point economy this pane doesn't ship (this
|
||||
-- file's header comment deviation 1); the two hides are the ref's own, in its own order —
|
||||
-- the page first, then the window (CharacterFrame_OnHide's subframe loop would reach this
|
||||
-- page anyway, but the ref hides it explicitly and so do we).
|
||||
function BenillaSkillFrameCancelButton_OnClick()
|
||||
getglobal("SkillFrame"):Hide()
|
||||
HideUIPanel(getglobal("CharacterFrame"))
|
||||
end
|
||||
|
||||
-- The window's wheel handler (bubbled up the parent chain from a row) → one row via the faux
|
||||
-- kit (TrainerFrame.xml's own note: the engine bubbles a spin up the parent chain).
|
||||
function BenillaSkillFrame_OnMouseWheel(delta)
|
||||
BenillaFauxScrollFrame_OnMouseWheel(getglobal("SkillListScrollFrame"), delta)
|
||||
end
|
||||
|
||||
-- ── Lifecycle (ref SkillFrame_OnLoad/_OnShow/_OnEvent, l.4-22, collapsed to the events this
|
||||
-- page's feed actually fires: CHARACTER_POINTS_CHANGED backs the training-point economy this
|
||||
-- pane doesn't ship, this file's header comment deviation 1). ─────────────────────────────────
|
||||
function SkillFrame_OnLoad()
|
||||
local frame = this
|
||||
frame:RegisterEvent("SKILL_LINES_CHANGED")
|
||||
end
|
||||
|
||||
function SkillFrame_OnShow()
|
||||
BenillaSkillFrame_Update()
|
||||
end
|
||||
|
||||
function SkillFrame_OnEvent(event)
|
||||
local frame = this
|
||||
if event == "SKILL_LINES_CHANGED" and frame:IsVisible() then
|
||||
BenillaSkillFrame_Update()
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!--
|
||||
The rank StatusBar row (ref SkillStatusBarTemplate, l.4-65 — the FillBar dropped, this file's
|
||||
header comment deviation 5): 271×15, name+rank overlaid, the Border child Button for OnClick
|
||||
+ hover highlight (TradeSkillFrame.xml's own BenillaTradeSkillRankFrame shape, verbatim).
|
||||
-->
|
||||
<StatusBar name="BenillaSkillRankTemplate" virtual="true" hidden="true" drawLayer="BACKGROUND" minValue="0" maxValue="1" defaultValue="0">
|
||||
<Size><AbsDimension x="271" y="15"/></Size>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentSkillName" inherits="GameFontNormalSmall">
|
||||
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="6" y="1"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentSkillRank" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Size><AbsDimension x="128" y="0"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="$parentSkillName" relativePoint="RIGHT"><Offset><AbsDimension x="13" y="0"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentBackground"><Color r="1.0" g="1.0" b="1.0" a="0.2"/></Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentBorder">
|
||||
<Size><AbsDimension x="281" y="32"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
<NormalTexture file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorder"/>
|
||||
<HighlightTexture file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorderHighlight"/>
|
||||
<Scripts><OnClick>BenillaSkillRankFrame_OnClick(self:GetParent())</OnClick></Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<BarTexture name="$parentBar" file="Interface\PaperDollInfoFrame\UI-Character-Skills-Bar"/>
|
||||
<BarColor r="0.25" g="0.25" b="0.75"/>
|
||||
</StatusBar>
|
||||
|
||||
<!--
|
||||
The category header row (ref SkillLabelTemplate, l.66-129, minus the DisabledTexture — this
|
||||
window never Disable()s a header): 285×14, the +/- fold icon + name text.
|
||||
-->
|
||||
<Button name="BenillaSkillHeaderTemplate" virtual="true" hidden="true">
|
||||
<Size><AbsDimension x="285" y="14"/></Size>
|
||||
<Scripts><OnClick>BenillaSkillTypeLabel_OnClick(self)</OnClick></Scripts>
|
||||
<NormalTexture file="Interface\Buttons\UI-MinusButton-UP">
|
||||
<Size><AbsDimension x="16" y="16"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
|
||||
</NormalTexture>
|
||||
<HighlightTexture file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD">
|
||||
<Size><AbsDimension x="16" y="16"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
|
||||
</HighlightTexture>
|
||||
<ButtonText>
|
||||
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="25" y="0"/></Offset></Anchor></Anchors>
|
||||
</ButtonText>
|
||||
<NormalFont inherits="GameFontHighlight" justifyH="LEFT"/>
|
||||
</Button>
|
||||
|
||||
<!-- id=4, the ref's own: a page's id is the slot it occupies in the WINDOW'S OWN tab row, and
|
||||
ToggleCharacter selects the tab from it (ref CharacterFrame.lua:11). This was 3 while
|
||||
Reputation (the ref's 3) was not built; ReputationFrame.xml landed and took that slot, so
|
||||
Skills is back on the reference's number. `character_tests.rs` pins every page's id against
|
||||
its slot in CHARACTERFRAME_SUBFRAMES, which is what made the renumber loud rather
|
||||
than silent — exactly as this comment predicted. -->
|
||||
<Frame name="SkillFrame" enableMouse="true" parent="CharacterFrame" hidden="true" id="4">
|
||||
<!-- The real 384×512 page. No <Anchors relativeTo> substitute for true parenting exists
|
||||
(this file's header comment) — pinned to CharacterFrame's own TOPLEFT by name. -->
|
||||
<Size><AbsDimension x="384" y="512"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="CharacterFrame" relativePoint="TOPLEFT"/></Anchors>
|
||||
<!-- Transcribed for fidelity (ref l.131-133); no loader wiring exists yet for the hit-rect
|
||||
shrink (TradeSkillFrame.xml's own "transcribe the hook even if nothing drives it yet"
|
||||
posture, TalentFrame.xml's precedent). -->
|
||||
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="75"/></HitRectInsets>
|
||||
<Layers>
|
||||
<!-- The shared quadrant background (ref l.136-183): NO portrait redraw here (this file's
|
||||
header comment) — "General" TopLeft/TopRight (the same variant every PaperDollInfoFrame
|
||||
page shares, carrying the portrait-ring cutout) + SkillFrame's own BotLeft/BotRight. -->
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopLeft">
|
||||
<Size><AbsDimension x="256" y="256"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopRight">
|
||||
<Size><AbsDimension x="128" y="256"/></Size>
|
||||
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\PaperDollInfoFrame\SkillFrame-BotLeft">
|
||||
<Size><AbsDimension x="256" y="256"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\PaperDollInfoFrame\SkillFrame-BotRight">
|
||||
<Size><AbsDimension x="128" y="256"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<!-- The divider bar below the list, above the detail pane (ref l.186-211's own
|
||||
TOPLEFT(15,-290); nudged to -305 here to clear this file's own 12-row chain, whose
|
||||
last row sits slightly lower than the ref's fixed-slot geometry — a pixel-fit
|
||||
adjustment, not a structural deviation). -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="SkillHorizontalBarLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
|
||||
<Size><AbsDimension x="256" y="16"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-305"/></Offset></Anchor></Anchors>
|
||||
<TexCoords left="0" right="1.0" top="0" bottom="0.25"/>
|
||||
</Texture>
|
||||
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
|
||||
<Size><AbsDimension x="75" y="16"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="SkillHorizontalBarLeft" relativePoint="RIGHT"/></Anchors>
|
||||
<TexCoords left="0" right="0.29296875" top="0.25" bottom="0.5"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The expand/collapse-all tab (ref SkillFrameExpandButtonFrame, l.242-317 — the SAME
|
||||
QuestLogSortTab art TrainerFrame.xml's own BenillaTrainerExpandButtonFrame uses).
|
||||
The tab is SIZED BY ITS LABEL, the ref's own OnLoad law (l.312-316:
|
||||
SetWidth(GetTextWidth()+45)) — the middle slab stretches between the two 8px caps
|
||||
off its OWN two anchors (ref l.279-287), so the 45 is exactly the two caps plus the
|
||||
label's seat. Was a fixed 54 with a fixed 38 middle, copied off
|
||||
TrainerFrame.xml rather than read off THIS ref; the label's own font fix below
|
||||
(12px, not 10) would have run under the right cap at that width. -->
|
||||
<Frame name="SkillFrameExpandButtonFrame">
|
||||
<Size><AbsDimension x="54" y="32"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="70" y="-49"/></Offset></Anchor></Anchors>
|
||||
<Layers><Layer level="BACKGROUND">
|
||||
<Texture name="SkillFrameExpandTabLeft" file="Interface\QuestFrame\UI-QuestLogSortTab-Left"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors></Texture>
|
||||
<Texture name="SkillFrameExpandTabRight" file="Interface\QuestFrame\UI-QuestLogSortTab-Right"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors></Texture>
|
||||
<Texture name="SkillFrameExpandTabMiddle" file="Interface\QuestFrame\UI-QuestLogSortTab-Middle"><Size><AbsDimension x="0" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="SkillFrameExpandTabLeft" relativePoint="RIGHT"/><Anchor point="RIGHT" relativeTo="SkillFrameExpandTabRight" relativePoint="LEFT"/></Anchors></Texture>
|
||||
</Layer></Layers>
|
||||
<Frames>
|
||||
<!-- The ALL fold (ref SkillFrameCollapseAllButton, l.292-310). Its face is the
|
||||
ref's SkillLabelTemplate one — the SAME face every SkillTypeLabel row wears
|
||||
(BenillaSkillHeaderTemplate above, l.83-128 of the ref): the +/- glyph at
|
||||
LEFT+3, the label at LEFT+25, and GameFontHighlight — WHITE at 12px, not
|
||||
GameFontNormalSmall's yellow 10. Spelled out here rather than inherited
|
||||
because this button keeps its own OnClick (this engine's
|
||||
CollapseSkillHeader(0)/ExpandSkillHeader(0) fold everything at once, so it
|
||||
needs no skillIndex/isExpanded bookkeeping) and this loader CONCATENATES an
|
||||
instance's children onto its template's rather than replacing them, so an
|
||||
inherited <Scripts> would stand alongside ours. Its seat is the ref's own
|
||||
(-3,-3) off the left cap: the tab art's visible lip sits BELOW the
|
||||
texture's geometric middle, so the label rides 3px low, not 3px high — the
|
||||
+3 copied from TrainerFrame.xml put the whole "- All" 6px above where the
|
||||
reference seats it. -->
|
||||
<Button name="SkillFrameCollapseAllButton" text="ALL">
|
||||
<Size><AbsDimension x="40" y="22"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="SkillFrameExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="-3" y="-3"/></Offset></Anchor></Anchors>
|
||||
<NormalTexture file="Interface\Buttons\UI-MinusButton-Up"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors></NormalTexture>
|
||||
<HighlightTexture file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors></HighlightTexture>
|
||||
<ButtonText><Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="25" y="0"/></Offset></Anchor></Anchors></ButtonText>
|
||||
<NormalFont inherits="GameFontHighlight" justifyH="LEFT"/>
|
||||
<Scripts><OnClick>BenillaSkillFrameCollapseAllButton_OnClick()</OnClick></Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts><OnLoad>BenillaSkillFrameExpandButtonFrame_Fit()</OnLoad></Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- The page's own CLOSE button (ref SkillFrameCancelButton, l.339-357: 80x22,
|
||||
UIPanelButtonTemplate, CENTER of TOPLEFT (305,-422), text CLOSE). LIVE in the
|
||||
reference, not dead: the ref's XML comment that swallows SkillFrameAcceptButton
|
||||
CLOSES at l.337, one line ABOVE this button — this file's own header comment
|
||||
deviation 3 read that boundary wrong and dropped a button the real client draws,
|
||||
which is why the page's bottom-right seat sat empty (decision 1496). The
|
||||
UIPanelButtonTemplate face is reconstructed locally, the house convention every
|
||||
window here follows (CraftFrame.xml's BenillaCraftPanelButtonTemplate note) — one
|
||||
site, so inline rather than a virtual template (GroupLootFrame.xml's own precedent
|
||||
for a single ref-template button). -->
|
||||
<Button name="SkillFrameCancelButton" text="CLOSE">
|
||||
<Size><AbsDimension x="80" y="22"/></Size>
|
||||
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="305" y="-422"/></Offset></Anchor></Anchors>
|
||||
<NormalFont inherits="GameFontNormal"/>
|
||||
<HighlightFont inherits="GameFontHighlight"/>
|
||||
<DisabledFont inherits="GameFontDisable"/>
|
||||
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
|
||||
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
|
||||
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
|
||||
<Scripts><OnClick>BenillaSkillFrameCancelButton_OnClick()</OnClick></Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- The persistent selection highlight (Trainer/TradeSkillFrame's own precedent, this
|
||||
file's header comment deviation 7): 281×15, ARTWORK-layer UI-Listbox-Highlight2.
|
||||
Declared BEFORE the rows so the row text paints OVER the glow (same z-tiebreak). -->
|
||||
<Frame name="SkillHighlightFrame" hidden="true">
|
||||
<Size><AbsDimension x="281" y="15"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
|
||||
<Layers><Layer level="ARTWORK">
|
||||
<Texture name="SkillHighlight" file="Interface\Buttons\UI-Listbox-Highlight2">
|
||||
<Anchors><Anchor point="TOPLEFT"/><Anchor point="BOTTOMRIGHT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
|
||||
<!-- 12 header-row slots (ref SkillTypeLabel1..12, l.358-465): 285×14, row1 LEFT of
|
||||
TOPLEFT(22,-86); rows 2-12 chain by (0,-18). -->
|
||||
<Button name="SkillTypeLabel1" inherits="BenillaSkillHeaderTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-86"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="SkillTypeLabel2" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel1"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel3" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel2"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel4" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel3"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel5" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel4"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel6" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel5"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel7" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel6"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel8" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel7"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel9" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel8"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel10" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel9"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel11" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel10"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
<Button name="SkillTypeLabel12" inherits="BenillaSkillHeaderTemplate"><Anchors><Anchor point="LEFT" relativeTo="SkillTypeLabel11"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors></Button>
|
||||
|
||||
<!-- 12 rank-bar slots (ref SkillRankFrame1..12, l.746-853): 271×15, row1 TOPLEFT(38,-79);
|
||||
rows 2-12 chain BOTTOMLEFT→TOPLEFT by (0,-3) — the SAME 18px pitch as the label chain
|
||||
(15 bar height + 3 gap). -->
|
||||
<StatusBar name="SkillRankFrame1" inherits="BenillaSkillRankTemplate" id="1">
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="38" y="-79"/></Offset></Anchor></Anchors>
|
||||
</StatusBar>
|
||||
<StatusBar name="SkillRankFrame2" inherits="BenillaSkillRankTemplate" id="2"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame3" inherits="BenillaSkillRankTemplate" id="3"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame4" inherits="BenillaSkillRankTemplate" id="4"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame5" inherits="BenillaSkillRankTemplate" id="5"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame6" inherits="BenillaSkillRankTemplate" id="6"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame7" inherits="BenillaSkillRankTemplate" id="7"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame8" inherits="BenillaSkillRankTemplate" id="8"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame9" inherits="BenillaSkillRankTemplate" id="9"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame8" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame10" inherits="BenillaSkillRankTemplate" id="10"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame11" inherits="BenillaSkillRankTemplate" id="11"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame10" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
<StatusBar name="SkillRankFrame12" inherits="BenillaSkillRankTemplate" id="12"><Anchors><Anchor point="TOPLEFT" relativeTo="SkillRankFrame11" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors></StatusBar>
|
||||
|
||||
<!-- The faux-scroll frame (FauxScrollFrameTemplate, ScrollTemplates.xml): invisible
|
||||
container over the 12 rows; only its right-hand Slider scroll bar renders. Sized to the
|
||||
12-row span (SKILLS_TO_DISPLAY × 18px pitch), TrainerFrame.xml's own
|
||||
same-TOPLEFT-as-row1 convention. Its `<OnVerticalScroll>` re-binds the rows (1868). -->
|
||||
<ScrollFrame name="SkillListScrollFrame" inherits="FauxScrollFrameTemplate">
|
||||
<Size><AbsDimension x="296" y="216"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-79"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<!-- The reference's kit writes `frame.offset` ONLY from here: the bar's
|
||||
OnValueChanged calls `SetVerticalScroll`, which lands in this handler.
|
||||
Our transcription used a `frame.updateFunc` field instead, which the
|
||||
reference does not have — without this the bar moves and the list does
|
||||
not follow (1860). -->
|
||||
<OnVerticalScroll>BenillaSkillList_OnVerticalScroll()</OnVerticalScroll>
|
||||
</Scripts>
|
||||
</ScrollFrame>
|
||||
|
||||
<!-- ═══ THE DETAIL PANE ═══ ref SkillFrame.xml l.513-747, transcribed as the ref
|
||||
declares it (header: the FLAT-layout claim this file used to carry is retracted,
|
||||
decision 1517). A real ScrollFrame over a real scroll child; `scrollBarHideable`
|
||||
keeps the bar off, which for a one-paragraph description is every time. The two
|
||||
flanking textures are the ref's own trough art on the SCROLL FRAME's own Layers
|
||||
(ItemTextFrame.xml's ItemTextScrollFrame is this file's precedent for both) — the
|
||||
ref draws them unconditionally, so they stand whether or not the bar does.
|
||||
The scroll range is the child's whole SUBTREE box (decision 1338), which is why
|
||||
the ref's 320x50 child is not the undersized thing it looks like. -->
|
||||
<ScrollFrame name="SkillDetailScrollFrame" inherits="UIPanelScrollFrameTemplate">
|
||||
<Size><AbsDimension x="296" y="107"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="SkillListScrollFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-ScrollBar">
|
||||
<Size><AbsDimension x="30" y="100"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-3" y="5"/></Offset></Anchor></Anchors>
|
||||
<TexCoords left="0" right="0.46875" top="0" bottom="0.78125"/>
|
||||
</Texture>
|
||||
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-ScrollBar">
|
||||
<Size><AbsDimension x="30" y="100"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-3" y="-2"/></Offset></Anchor></Anchors>
|
||||
<TexCoords left="0.53125" right="1.0" top="0.21875" bottom="1.0"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<!-- A handler declared on an instance REPLACES the template's, it does not
|
||||
chain (ItemTextFrame.xml's own note), so the template's OnLoad is re-called
|
||||
by hand before this instance's own line. -->
|
||||
<OnLoad>
|
||||
ScrollFrame_OnLoad()
|
||||
self.scrollBarHideable = 1
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
<ScrollChild>
|
||||
<Frame name="SkillDetailScrollChildFrame">
|
||||
<Size><AbsDimension x="320" y="50"/></Size>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<!-- The training-point price tag (ref l.567-577). Built, never
|
||||
filled: `GetSkillLineInfo` reports no stepCost/rankCost on a
|
||||
1.12 server, so BenillaSkillFrame_UpdateDetail takes the ref's
|
||||
OWN no-cost branch (ref SkillFrame.lua l.361) and hides it —
|
||||
deviation 1. It still earns its place as the DESCRIPTION's
|
||||
anchor target, which is the ref's own arrangement. -->
|
||||
<FontString name="SkillDetailCostText" inherits="GameFontNormalSmall" justifyH="CENTER" hidden="true">
|
||||
<Size><AbsDimension x="200" y="0"/></Size>
|
||||
<Anchors><Anchor point="CENTER" relativeTo="$parent" relativePoint="TOP"><Offset><AbsDimension x="-10" y="-40"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<!-- The description body (ref l.579-591). Its anchor is XML now:
|
||||
the flat layout had to set it in SkillFrame_OnLoad because the
|
||||
bar was a forward reference on the page, and inside the child
|
||||
the ref's own static TOP anchor resolves. -->
|
||||
<FontString name="SkillDetailDescriptionText" inherits="GameFontHighlightSmall" justifyH="LEFT" justifyV="TOP" hidden="true">
|
||||
<Size><AbsDimension x="275" y="0"/></Size>
|
||||
<Anchors><Anchor point="TOP" relativeTo="$parent" relativePoint="TOP"><Offset><AbsDimension x="-5" y="-35"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The selected skill's rank bar (ref l.594-747), the ref's OWN
|
||||
declaration rather than the borrowed 271-wide list row it used to
|
||||
be: 211x15 under a 220x32 Border, centred on the child's TOP. The
|
||||
Border is a TEXTURE here, as the ref has it — the list row's
|
||||
Border is a Button because a row is clickable, and the detail bar
|
||||
is not, so the old "its Border click is harmless here" hedge is
|
||||
gone with the borrowing. Names match the list template's
|
||||
($parentSkillName/$parentSkillRank/$parentBackground), so
|
||||
BenillaSkillFrame_PaintBar drives both unchanged. -->
|
||||
<StatusBar name="SkillDetailStatusBar" drawLayer="BACKGROUND" minValue="0" maxValue="1" defaultValue="0" hidden="true">
|
||||
<Size><AbsDimension x="211" y="15"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativeTo="SkillDetailScrollChildFrame" relativePoint="TOP"><Offset><AbsDimension x="-10" y="-20"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentBorder" file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorder">
|
||||
<Size><AbsDimension x="220" y="32"/></Size>
|
||||
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<FontString name="$parentSkillName" inherits="GameFontNormalSmall">
|
||||
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="6" y="1"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentSkillRank" inherits="GameFontHighlightSmall">
|
||||
<Size><AbsDimension x="128" y="0"/></Size>
|
||||
</FontString>
|
||||
</Layer>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentBackground">
|
||||
<Color r="1.0" g="1.0" b="1.0" a="0.2"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The abandon button (ref $parentUnlearnButton, l.705-737,
|
||||
verbatim art/geometry): the red circle-slash right of the
|
||||
Border, shown by UpdateDetail only for an abandonable line.
|
||||
HitRectInsets transcribed-inert (no loader wiring —
|
||||
CharacterFrame.xml's engine-gap note). Handlers are chunk
|
||||
functions (the chunk-boundary rule — OnEnter reads the
|
||||
chunk-local UNLEARN_SKILL_TOOLTIP). -->
|
||||
<Button name="$parentUnlearnButton" hidden="true">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentBorder" relativePoint="RIGHT">
|
||||
<Offset><AbsDimension x="-2" y="-1"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<HitRectInsets><AbsInset left="9" right="7" top="-7" bottom="10"/></HitRectInsets>
|
||||
<Scripts>
|
||||
<OnClick>BenillaSkillUnlearnButton_OnClick(self)</OnClick>
|
||||
<OnEnter>BenillaSkillUnlearnButton_OnEnter(self)</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\Buttons\CancelButton-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\CancelButton-Down"/>
|
||||
<HighlightTexture file="Interface\Buttons\CancelButton-Highlight" alphaMode="ADD"/>
|
||||
</Button>
|
||||
</Frames>
|
||||
<BarTexture name="$parentBar" file="Interface\PaperDollInfoFrame\UI-Character-Skills-Bar"/>
|
||||
<BarColor r="0.25" g="0.25" b="0.75"/>
|
||||
</StatusBar>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</ScrollChild>
|
||||
</ScrollFrame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>SkillFrame_OnLoad()</OnLoad>
|
||||
<OnShow>SkillFrame_OnShow()</OnShow>
|
||||
<OnEvent>SkillFrame_OnEvent(event)</OnEvent>
|
||||
<OnMouseWheel>BenillaSkillFrame_OnMouseWheel(arg1)</OnMouseWheel>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -124,8 +124,10 @@
|
||||
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.
|
||||
-- Declared by the ref's ContainerFrame.lua / PetActionBarFrame.lua / WorldStateFrame.lua;
|
||||
-- PetActionBarFrame.lua is on the chain (1953) and re-declares its two below, the other
|
||||
-- two files are not shipped. Pre-declared here so a manage pass that runs before the pet
|
||||
-- file loads reads a number; the pass overwrites them every run.
|
||||
CONTAINER_OFFSET_X = 0
|
||||
CONTAINER_OFFSET_Y = 70
|
||||
BATTLEFIELD_TAB_OFFSET_Y = 210
|
||||
|
||||
@@ -2034,6 +2034,88 @@
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
-- The pet arc's three (decision 1066; ref StaticPopup.lua l.738, l.1069, l.365). ABANDON_PET
|
||||
-- is the plain confirm. RENAME_PET is a CHAIN of two, which is the reference's own shape and
|
||||
-- not a flourish: the first collects the name, the second reads it back ("Name your pet
|
||||
-- 'Bruce'?") before anything is sent — a rename is one-shot, so a typo is permanent the way
|
||||
-- the abandon is. That second Show happens while the first dialog is still up, which is
|
||||
-- why the engine's two instances are both needed. Registered beside the other dialogs since
|
||||
-- the pet bar became the reference's file (1953).
|
||||
StaticPopupDialogs["ABANDON_PET"] = {
|
||||
text = ABANDON_PET,
|
||||
button1 = OKAY,
|
||||
button2 = CANCEL,
|
||||
OnAccept = function()
|
||||
PetAbandon();
|
||||
end,
|
||||
timeout = 0,
|
||||
exclusive = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["RENAME_PET"] = {
|
||||
text = PET_RENAME_LABEL,
|
||||
button1 = ACCEPT,
|
||||
button2 = CANCEL,
|
||||
hasEditBox = 1,
|
||||
-- The 12-letter cap is the reference's, and it is the client's only name check: the
|
||||
-- server re-validates with CheckPetName and refuses with SMSG_PET_NAME_INVALID.
|
||||
maxLetters = 12,
|
||||
OnAccept = function()
|
||||
-- `this` is the clicked button, so its parent is THIS dialog — never
|
||||
-- StaticPopup1 by name, which would read the wrong box the moment this dialog
|
||||
-- lands in the second instance (the chain below is exactly that case).
|
||||
local text = getglobal(this:GetParent():GetName() .. "EditBox"):GetText()
|
||||
local dialog = StaticPopup_Show("PETRENAMECONFIRM", text)
|
||||
if dialog then
|
||||
dialog.data = text
|
||||
end
|
||||
end,
|
||||
EditBoxOnEnterPressed = function()
|
||||
local text = getglobal(this:GetParent():GetName() .. "EditBox"):GetText()
|
||||
local dialog = StaticPopup_Show("PETRENAMECONFIRM", text)
|
||||
if dialog then
|
||||
dialog.data = text
|
||||
end
|
||||
this:GetParent():Hide()
|
||||
end,
|
||||
OnShow = function()
|
||||
getglobal(this:GetName() .. "EditBox"):SetFocus()
|
||||
end,
|
||||
timeout = 0,
|
||||
exclusive = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["PETRENAMECONFIRM"] = {
|
||||
-- "Name your pet '%s'?" — the name arrives as text_arg1 AND as `data`; the first
|
||||
-- fills the question, the second is what actually gets sent.
|
||||
text = PET_RENAME_CONFIRMATION,
|
||||
button1 = YES,
|
||||
button2 = NO,
|
||||
OnAccept = function(data)
|
||||
PetRename(data);
|
||||
end,
|
||||
timeout = 0,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
-- The skills page's unlearn confirm (ref StaticPopup.lua l.1121-1133), registered beside
|
||||
-- the other dialogs since the page became the reference's file (1956).
|
||||
StaticPopupDialogs["UNLEARN_SKILL"] = {
|
||||
text = UNLEARN_SKILL,
|
||||
button1 = UNLEARN,
|
||||
button2 = CANCEL,
|
||||
OnAccept = function(index)
|
||||
AbandonSkill(index)
|
||||
end,
|
||||
timeout = 60,
|
||||
exclusive = 1,
|
||||
whileDead = 1,
|
||||
showAlert = 1,
|
||||
hideOnEscape = 1
|
||||
}
|
||||
|
||||
-- ref UIParent.lua l.324-343 + l.356-360, the four arms on one driver. Each equip arm
|
||||
-- hides the OTHER dialog first — the two questions are mutually exclusive by construction
|
||||
-- (one item, one destination), and the hide is what cancels the record the new question
|
||||
|
||||
@@ -419,17 +419,19 @@ Interface\FrameXML\ActionBarFrame.xml
|
||||
# that fills the four SHOW_MULTI_ACTIONBAR_* globals from the server's byte; the manage pass runs
|
||||
# from the options rows' setters, as the stock options window runs it on hide.
|
||||
Interface\FrameXML\MultiActionBars.xml
|
||||
# The pet action bar (decision 0982): hidden until `crate::ui_pet`'s feed reports a bar,
|
||||
# i.e. until the server sends `SMSG_PET_SPELLS` for a live pet or charm — so a class that
|
||||
# never has one never sees it. Right after the stance bar, whose row it shares (either
|
||||
# one showing raises UIParent.xml's managed "pet" delta), and after Cooldown.xml
|
||||
# (CooldownFrame_SetTimer), MainMenuBar.xml (the `MainMenuBar` anchor target),
|
||||
# GameTooltip.xml (its hover), Fonts.xml (NORMAL_FONT_COLOR_CODE, which wraps the binding
|
||||
# a token slot's tooltip appends) and UIParent.xml (GetBindingText, which renders it).
|
||||
# It is also the pet ARC's file, so decision 1066's three
|
||||
# right-click-menu dialogs (ABANDON_PET / RENAME_PET / PETRENAMECONFIRM) register here —
|
||||
# which adds UiPanels.xml to its dependency set, already far above.
|
||||
PetActionBar.xml
|
||||
# ══ THE PET ACTION BAR — the reference's own, off the player's chain (1751; decision 1953) ══
|
||||
# PetActionBarFrame.xml + PetActionBarFrame.lua: the ten buttons over `crate::ui_pet`'s feed
|
||||
# (GetPetActionInfo & co., decision 0982's engine), the slide, the grid, the hotkey pass. Right
|
||||
# after the stance bar, whose row it shares (UIParent.xml's managed "pet" delta), and after
|
||||
# Cooldown.xml, MainMenuBar.xml (its parent), GameTooltip.xml, ActionBarFrame.xml
|
||||
# (ActionButton_UpdateState) and UiPanels.xml (SetDesaturation, and where 1066's three
|
||||
# right-click dialogs — ABANDON_PET / RENAME_PET / PETRENAMECONFIRM — now live). 0982's
|
||||
# transcription is gone.
|
||||
Interface\FrameXML\PetActionBarFrame.xml
|
||||
# The one adapter the reference's file needs from this engine: its autocast shine is a <Model>
|
||||
# (UI-AutoCastButton.mdx) and this engine's <Model> draws no pixels; the shine benilla draws is
|
||||
# its own token texture (1383). BENILLA_ALLOW_OWN_UI=1.
|
||||
PetActionBarAdapters.xml
|
||||
# The bonus (stance-page) bar AND the shapeshift bar — one stock file declares both, and the
|
||||
# reference's toc seats it right after PetActionBarFrame (l.73-74): the bonus buttons'
|
||||
# Up/Down call PetActionButtonUp/Down, which PetActionBar.xml above defines.
|
||||
@@ -739,14 +741,13 @@ ContainerFrameAdapters.xml
|
||||
# `GroupLootFrame` did for months (1838). The bar, its update, its two text verbs,
|
||||
# `MAX_PLAYER_LEVEL` and `FACTION_BAR_COLORS` all went home with it (1875).
|
||||
Interface\FrameXML\ReputationFrame.xml
|
||||
# The Skills tab (decision 0437 phase 4): a CharacterFrame child page, but a separate
|
||||
# top-level file — it declares `parent="CharacterFrame"`, which really attaches (1734), and
|
||||
# is positioned by name via `<Anchors relativeTo="CharacterFrame">`, the geometry it ships
|
||||
# with. Loaded after CharacterFrame.xml so it paints ON TOP of
|
||||
# it (later file = higher z) and so `CharacterFrame` already exists for that anchor to
|
||||
# resolve against — seated at the real client's own tab 4, between Reputation above and
|
||||
# Honor below.
|
||||
SkillFrame.xml
|
||||
# ══ THE SKILLS TAB — the reference's own, off the player's chain (1751; decision 1956) ══
|
||||
# SkillFrame.xml + SkillFrame.lua: CharacterFrame's fourth page (`parent="CharacterFrame"`,
|
||||
# `id="4"`), the twelve-row list over 0437's skills seam (GetNumSkillLines/GetSkillLineInfo's
|
||||
# twelve-value tuple, the expand/collapse pair, the selection), the detail bar, the unlearn
|
||||
# button (its UNLEARN_SKILL dialog lives with the other dialogs in UiPanels.xml). At the
|
||||
# reference's own slot, after ReputationFrame and before HonorFrame. 0437's transcription is gone.
|
||||
Interface\FrameXML\SkillFrame.xml
|
||||
# The Honor tab: the same separate-top-level-file shape as the three pages above it, and LAST of
|
||||
# them because it is the last of the tabs (the real client's 5, ref HonorFrame.xml:5) — which
|
||||
# CLOSES the row: the character window's five tabs are now the reference's five, in its order, at
|
||||
|
||||
@@ -58,6 +58,7 @@ impl Plugin for LiveFpsPlugin {
|
||||
load_samples: Vec::new(),
|
||||
paced_samples: Vec::new(),
|
||||
threads_at_start: None,
|
||||
faults_at_start: None,
|
||||
cpu_at_start: None,
|
||||
sys_at_start: None,
|
||||
occluded_now: false,
|
||||
@@ -112,6 +113,8 @@ struct LiveFps {
|
||||
/// Per-thread CPU at the window's first frame (`perf::thread_cpu_table`), keyed by pthread
|
||||
/// identity — subtracted at the window's end for `thr=[name:ms/frame,…]`.
|
||||
threads_at_start: Option<Vec<(usize, String, f64, f64)>>,
|
||||
/// The page-fault counters at the window's first frame (`perf::process_faults`).
|
||||
faults_at_start: Option<(u64, u64)>,
|
||||
/// Process CPU seconds at the first sampled frame ([`crate::perf::process_cpu_secs`]) — the
|
||||
/// baseline for the window's `cpu_ms`/`cpu_pct`.
|
||||
cpu_at_start: Option<f64>,
|
||||
@@ -164,7 +167,7 @@ type ScreenParams<'w, 's> = (
|
||||
(
|
||||
&'static bevy::camera::visibility::ViewVisibility,
|
||||
Has<MeshMaterial3d<benilla_assets::materials::TerrainMaterial>>,
|
||||
Has<benilla_world::model_render::ModelPart>,
|
||||
Option<&'static benilla_world::model_render::ModelPart>,
|
||||
Has<MeshMaterial3d<benilla_assets::materials::LiquidMaterial>>,
|
||||
),
|
||||
With<Mesh3d>,
|
||||
@@ -302,6 +305,7 @@ fn drive_live_fps(
|
||||
if probe.samples.is_empty() {
|
||||
probe.cpu_at_start = crate::perf::process_cpu_secs();
|
||||
probe.threads_at_start = crate::perf::thread_cpu_table();
|
||||
probe.faults_at_start = crate::perf::process_faults();
|
||||
main_split.restart();
|
||||
probe.sys_at_start = crate::perf::system_cpu_ticks();
|
||||
// The churn census restarts with the window — warmup noise (streaming, shader
|
||||
@@ -419,10 +423,14 @@ fn drive_live_fps(
|
||||
a.load(std::sync::atomic::Ordering::Relaxed)
|
||||
};
|
||||
format!(
|
||||
" wgpu_bufs={} wgpu_texs={} wgpu_bgs={}",
|
||||
" wgpu_bufs={} wgpu_texs={} wgpu_bgs={} draws=o{}/t{}/s{}/ui{}",
|
||||
ld(&c.0.buffers),
|
||||
ld(&c.0.textures),
|
||||
ld(&c.0.bind_groups)
|
||||
ld(&c.0.bind_groups),
|
||||
ld(&c.0.draws_opaque),
|
||||
ld(&c.0.draws_transparent),
|
||||
ld(&c.0.draws_shadow),
|
||||
ld(&c.0.draws_ui)
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@@ -504,7 +512,19 @@ fn drive_live_fps(
|
||||
.inside_ms()
|
||||
.map(|m| format!(" main_in={m:.2}"))
|
||||
.unwrap_or_default();
|
||||
format!(" thr=[{list}]{inside}")
|
||||
// Page faults per frame beside the split: `sys` time with no
|
||||
// syscall under it is the kernel zero-filling pages the allocator
|
||||
// gave back and asked for again.
|
||||
let faults =
|
||||
match (probe.faults_at_start, crate::perf::process_faults()) {
|
||||
(Some((mi0, ma0)), Some((mi1, ma1))) => format!(
|
||||
" faults={:.0}/{:.1}",
|
||||
(mi1 - mi0) as f64 / v_len as f64,
|
||||
(ma1 - ma0) as f64 / v_len as f64
|
||||
),
|
||||
_ => String::new(),
|
||||
};
|
||||
format!(" thr=[{list}]{inside}{faults}")
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
@@ -609,13 +629,19 @@ fn drive_live_fps(
|
||||
ui_batches.iter().count(),
|
||||
vis = {
|
||||
let mut n = [0usize; 4];
|
||||
// Of the model parts, how many draw in the transparent pass (M2 `Blend`
|
||||
// batches) — the sorted phase whose draws never batch across materials.
|
||||
let mut blended = 0usize;
|
||||
for (vv, terrain, model, liquid) in vis_meshes.iter() {
|
||||
if !vv.get() {
|
||||
continue;
|
||||
}
|
||||
let k = if terrain {
|
||||
0
|
||||
} else if model {
|
||||
} else if let Some(part) = model {
|
||||
if part.blend == benilla_formats::ModelBlend::Blend {
|
||||
blended += 1;
|
||||
}
|
||||
1
|
||||
} else if liquid {
|
||||
2
|
||||
@@ -625,7 +651,7 @@ fn drive_live_fps(
|
||||
n[k] += 1;
|
||||
}
|
||||
format!(
|
||||
" vis_terrain={} vis_model={} vis_liquid={} vis_other={}",
|
||||
" vis_terrain={} vis_model={}(blend {blended}) vis_liquid={} vis_other={}",
|
||||
n[0], n[1], n[2], n[3]
|
||||
)
|
||||
},
|
||||
|
||||
@@ -257,6 +257,12 @@ pub(super) type CharSkinMaterials = (
|
||||
(Option<MatQuint>, Option<MatQuint>),
|
||||
);
|
||||
|
||||
/// The `WOW_PROBE_SHARED_SKIN` pricing lever (see its use in [`build_char_skin_materials`]).
|
||||
fn shared_skin_probe() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var_os("WOW_PROBE_SHARED_SKIN").is_some())
|
||||
}
|
||||
|
||||
/// Build a character body's per-appearance materials — the **body** atlas (as a (single-sided,
|
||||
/// two-sided) pair — a body batch keeps its own M2 0x04, e.g. the robe skirt) and the **hair**-mesh
|
||||
/// texture (a single CharSections BLP, decision 0045) — each as a (steady, interior-matte, fade,
|
||||
@@ -313,7 +319,7 @@ pub(super) fn build_char_skin_materials(
|
||||
let body_tex: Option<Handle<Image>> = match &look.body {
|
||||
BodySkin::Baked(name) => Some(asset_server.load::<Image>(baked_npc_url(name))),
|
||||
BodySkin::Composite { face } => world_assets.and_then(|world| {
|
||||
let key = SkinKey {
|
||||
let mut key = SkinKey {
|
||||
race: look.race,
|
||||
sex: look.sex,
|
||||
skin: look.skin,
|
||||
@@ -324,6 +330,23 @@ pub(super) fn build_char_skin_materials(
|
||||
equip,
|
||||
emblem,
|
||||
};
|
||||
// `WOW_PROBE_SHARED_SKIN=1` — a PRICING lever, never a look: every body composites
|
||||
// the same key, so every body part of one mesh shares one material and bevy's
|
||||
// batcher can instance them. What it measures is the ceiling of the shared-body-
|
||||
// material lane (draw count → render-thread CPU) before that lane is built.
|
||||
if shared_skin_probe() {
|
||||
key = SkinKey {
|
||||
race: 1,
|
||||
sex: 0,
|
||||
skin: 0,
|
||||
face: 0,
|
||||
facial_hair: 0,
|
||||
hair_style: 0,
|
||||
hair_color: 0,
|
||||
equip: [0; 8],
|
||||
emblem: None,
|
||||
};
|
||||
}
|
||||
match skin_cache.fetch(&key) {
|
||||
Some(handle) => Some(handle),
|
||||
None => {
|
||||
|
||||
@@ -37,6 +37,29 @@ pub(crate) fn process_cpu_secs() -> Option<f64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The process's page-fault counters so far — `(minor, major)` from `getrusage`. A minor fault
|
||||
/// is a page the kernel had to map or zero-fill on first touch: memory the allocator handed back
|
||||
/// to the OS and then asked for again. It is CPU time the process pays in the kernel with no
|
||||
/// syscall in any user stack — invisible to a sampler, visible to [`thread_cpu_table`]'s `sys`
|
||||
/// column. Per frame, it is the number that says whether that column is allocator churn.
|
||||
pub(crate) fn process_faults() -> Option<(u64, u64)> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// SAFETY: as in `process_cpu_secs` — `getrusage` fills the out-param completely.
|
||||
unsafe {
|
||||
let mut ru: libc::rusage = std::mem::zeroed();
|
||||
if libc::getrusage(libc::RUSAGE_SELF, &mut ru) != 0 {
|
||||
return None;
|
||||
}
|
||||
Some((ru.ru_minflt as u64, ru.ru_majflt as u64))
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// CPU seconds consumed by **the calling thread** (`CLOCK_THREAD_CPUTIME_ID`).
|
||||
///
|
||||
/// The twin [`process_cpu_secs`] cannot answer the question a hitch actually poses. It sums every
|
||||
|
||||
@@ -20,16 +20,21 @@
|
||||
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::core_pipeline::core_2d::{Opaque2d, Transparent2d};
|
||||
use bevy::core_pipeline::core_3d::{AlphaMask3d, Opaque3d, Transparent3d};
|
||||
use bevy::pbr::Shadow;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::graph::CameraDriverLabel;
|
||||
use bevy::render::render_graph::{
|
||||
Node, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel,
|
||||
};
|
||||
use bevy::render::render_phase::{ViewBinnedRenderPhases, ViewSortedRenderPhases};
|
||||
use bevy::render::render_resource::{
|
||||
Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, MapMode,
|
||||
};
|
||||
use bevy::render::renderer::{RenderContext, RenderDevice, RenderQueue};
|
||||
use bevy::render::{Render, RenderApp, RenderSystems};
|
||||
use bevy::ui_render::TransparentUi;
|
||||
use wgpu::{PollType, QuerySet, QuerySetDescriptor, QueryType};
|
||||
|
||||
/// Is the meter armed? One read; everything registers behind it.
|
||||
@@ -59,6 +64,11 @@ pub(crate) struct WgpuCensus {
|
||||
pub buffers: AtomicU64,
|
||||
pub textures: AtomicU64,
|
||||
pub bind_groups: AtomicU64,
|
||||
/// Draw calls the render phases will issue this frame, per family — see [`count_draws`].
|
||||
pub draws_opaque: AtomicU64,
|
||||
pub draws_transparent: AtomicU64,
|
||||
pub draws_shadow: AtomicU64,
|
||||
pub draws_ui: AtomicU64,
|
||||
}
|
||||
|
||||
/// Render world, `Cleanup`: refresh the census. wgpu-core's report takes each registry's read
|
||||
@@ -77,6 +87,78 @@ fn census(instance: Res<bevy::render::renderer::RenderInstance>, shared: Res<Wgp
|
||||
.store(hub.bind_groups.num_allocated as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Render world, `Cleanup`: the frame's draw-call count per phase family, off the phases bevy
|
||||
/// just rendered — the number the pass-encode CPU on the render thread scales with (wgpu-core
|
||||
/// validates and wgpu-hal encodes per draw, and on Metal a draw is also what the driver's
|
||||
/// kernel-side work is measured in). A binned phase issues one draw per bin (Metal has no
|
||||
/// multi-draw indirect, so a multidrawable batch set still draws bin by bin) plus one per
|
||||
/// unbatchable entity and per non-mesh item; a sorted phase issues one per item whose batch
|
||||
/// range survived batching (a merged item's range is emptied into its predecessor's).
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
fn count_draws(
|
||||
shared: Res<WgpuCensusShared>,
|
||||
opaque: Option<Res<ViewBinnedRenderPhases<Opaque3d>>>,
|
||||
mask: Option<Res<ViewBinnedRenderPhases<AlphaMask3d>>>,
|
||||
transparent: Option<Res<ViewSortedRenderPhases<Transparent3d>>>,
|
||||
shadow: Option<Res<ViewBinnedRenderPhases<Shadow>>>,
|
||||
ui: Option<Res<ViewSortedRenderPhases<TransparentUi>>>,
|
||||
two_d: Option<Res<ViewSortedRenderPhases<Transparent2d>>>,
|
||||
opaque_2d: Option<Res<ViewBinnedRenderPhases<Opaque2d>>>,
|
||||
) {
|
||||
fn binned<BPI: bevy::render::render_phase::BinnedPhaseItem>(
|
||||
phases: Option<Res<ViewBinnedRenderPhases<BPI>>>,
|
||||
) -> u64 {
|
||||
phases
|
||||
.map(|p| {
|
||||
p.0.values()
|
||||
.map(|phase| {
|
||||
let multi: usize =
|
||||
phase.multidrawable_meshes.values().map(|b| b.len()).sum();
|
||||
let unbatchable: usize = phase
|
||||
.unbatchable_meshes
|
||||
.values()
|
||||
.map(|u| u.entities.len())
|
||||
.sum();
|
||||
let non_mesh: usize = phase
|
||||
.non_mesh_items
|
||||
.values()
|
||||
.map(|n| n.entities.len())
|
||||
.sum();
|
||||
multi + phase.batchable_meshes.len() + unbatchable + non_mesh
|
||||
})
|
||||
.sum::<usize>() as u64
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
fn sorted<SPI: bevy::render::render_phase::SortedPhaseItem>(
|
||||
phases: Option<Res<ViewSortedRenderPhases<SPI>>>,
|
||||
) -> u64 {
|
||||
phases
|
||||
.map(|p| {
|
||||
p.0.values()
|
||||
.map(|phase| {
|
||||
phase
|
||||
.items
|
||||
.iter()
|
||||
.filter(|i| !i.batch_range().is_empty())
|
||||
.count()
|
||||
})
|
||||
.sum::<usize>() as u64
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
let c = &shared.0;
|
||||
c.draws_opaque
|
||||
.store(binned(opaque) + binned(mask), Ordering::Relaxed);
|
||||
c.draws_transparent
|
||||
.store(sorted(transparent), Ordering::Relaxed);
|
||||
c.draws_shadow.store(binned(shadow), Ordering::Relaxed);
|
||||
c.draws_ui.store(
|
||||
sorted(ui) + sorted(two_d) + binned(opaque_2d),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
const RING: usize = 4;
|
||||
|
||||
/// Ring-slot states: a copy may only target a FREE slot — submitting into a mapped (or
|
||||
@@ -275,7 +357,7 @@ pub(crate) fn plugin(app: &mut App) {
|
||||
render_app.insert_resource(WgpuCensusShared(counts));
|
||||
render_app.add_systems(
|
||||
Render,
|
||||
(init_stamp, readback, census)
|
||||
(init_stamp, readback, census, count_draws)
|
||||
.chain()
|
||||
.in_set(RenderSystems::Cleanup),
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ mod trace;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::diagnostic::RenderDiagnosticsPlugin;
|
||||
|
||||
pub(crate) use clock::{process_cpu_secs, system_cpu_ticks, thread_cpu_table};
|
||||
pub(crate) use clock::{process_cpu_secs, process_faults, system_cpu_ticks, thread_cpu_table};
|
||||
pub(crate) use gpu::{GpuMsShared, WgpuCensusShared};
|
||||
pub(crate) use hud::PerfHud;
|
||||
pub(crate) use journal::FpsJournalPlugin;
|
||||
|
||||
@@ -1297,6 +1297,12 @@ fn booth_view_shape() -> impl Bundle {
|
||||
bevy::render::view::Hdr,
|
||||
Tonemapping::None,
|
||||
Msaa::Off,
|
||||
// No bevy light ever reaches a booth (its lighting is the material variant,
|
||||
// `portrait::light`), so the per-view cluster assignment is dead work here exactly as
|
||||
// it is on the world camera (`player::setup`): with the default config every booth view
|
||||
// rebuilt its empty cluster grid each frame — 0.2 ms of the raid's main thread for
|
||||
// nothing drawn by it.
|
||||
bevy::light::cluster::ClusterConfig::None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ pub(super) fn actions_usable(bar: &PetBar, pet_flags: Option<u32>) -> bool {
|
||||
#[derive(Default)]
|
||||
pub(super) struct PetBarMemory {
|
||||
pushed: Option<(u32, bool, bool, bool, Vec<PetActionView>)>,
|
||||
/// The ten slots' cooldown triples as last pushed — `PET_BAR_UPDATE_COOLDOWN`'s own edge.
|
||||
cooldowns: Vec<Option<(i64, u32, bool)>>,
|
||||
}
|
||||
|
||||
/// The command tokens' `(GlobalStrings key, texture-global name)` pair.
|
||||
@@ -254,12 +256,14 @@ pub(super) fn active_aura_press(
|
||||
crate::ui_action::toggle::active_action_toggle(spell_id, spell?, pet?).then_some(spell_id)
|
||||
}
|
||||
|
||||
/// Rebuild the ten slot views each frame and diff-push them, firing `PET_BAR_UPDATE` on a change.
|
||||
/// Rebuild the ten slot views each frame and diff-push them, firing `PET_BAR_UPDATE` on a change
|
||||
/// of the bar and `PET_BAR_UPDATE_COOLDOWN` on a change of its cooldowns alone.
|
||||
///
|
||||
/// One event where the reference has four (`PET_BAR_UPDATE`, `PET_BAR_UPDATE_COOLDOWN`,
|
||||
/// `UNIT_PET`, and the `UNIT_FLAGS`/`UNIT_AURA` pair its bar filters for `arg1 == "pet"`) — the
|
||||
/// deliberate collapse `crate::ui_shapeshift` already makes for the stance bar, and for the same
|
||||
/// reason: we diff the whole pushed state, so one event carries every change there can be.
|
||||
/// The reference fires `PET_BAR_UPDATE` from nine sites, each right after a state change, and
|
||||
/// `PET_BAR_UPDATE_COOLDOWN` from the cooldown subsystem when the PET bank mutates (wow-re
|
||||
/// `pet-action-bar-api.md` §9). Diffing the pushed state is the same edge from the other side:
|
||||
/// the slots' content and usability are the bar's, the ten triples are the bank's. `UNIT_PET` is
|
||||
/// `feed_pet_unit`'s; `UNIT_FLAGS`/`UNIT_AURA` for `"pet"` are the unit feed's.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn feed_pet_bar(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
@@ -322,9 +326,25 @@ pub(super) fn feed_pet_bar(
|
||||
};
|
||||
|
||||
// `bar.bar_signals` rides the key so a press the state does not move still repaints — the
|
||||
// `0x4bc940`/`0x4bc960` signal, which the button's own `SetChecked(0)` makes mandatory.
|
||||
let key = (bar.bar_signals, has_bar, usable, pickup_allowed, fresh);
|
||||
if memory.pushed.as_ref() != Some(&key) {
|
||||
// `0x4bc940`/`0x4bc960` signal, which the button's own `SetChecked(0)` makes mandatory. The
|
||||
// cooldown triples are keyed apart: their edge is the bank's, not the bar's.
|
||||
let cooldowns: Vec<Option<(i64, u32, bool)>> = fresh.iter().map(|s| s.cooldown).collect();
|
||||
let content: Vec<PetActionView> = fresh
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut s| {
|
||||
s.cooldown = None;
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
let key = (bar.bar_signals, has_bar, usable, pickup_allowed, content);
|
||||
let bar_changed = memory.pushed.as_ref() != Some(&key);
|
||||
let cooldowns_changed = memory.cooldowns != cooldowns;
|
||||
if bar_changed || cooldowns_changed {
|
||||
script.set_pet_actions(has_bar, usable, pickup_allowed, fresh);
|
||||
memory.cooldowns = cooldowns;
|
||||
}
|
||||
if bar_changed {
|
||||
debug!(
|
||||
"ui_pet: bar {} ({} occupied slot(s), {}{})",
|
||||
if key.1 { "shown" } else { "hidden" },
|
||||
@@ -332,8 +352,9 @@ pub(super) fn feed_pet_bar(
|
||||
if key.2 { "usable" } else { "disabled" },
|
||||
if key.3 { "" } else { ", possessed" },
|
||||
);
|
||||
memory.pushed = Some(key.clone());
|
||||
script.set_pet_actions(key.1, key.2, key.3, key.4);
|
||||
memory.pushed = Some(key);
|
||||
script.fire_event("PET_BAR_UPDATE", vec![]);
|
||||
} else if cooldowns_changed {
|
||||
script.fire_event("PET_BAR_UPDATE_COOLDOWN", vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,3 +883,81 @@ fn the_dismiss_word_is_the_carved_literal() {
|
||||
assert_eq!(entry.action(), PET_COMMAND_DISMISS);
|
||||
assert!(!entry.is_spell(), "dismiss is a command, never a cast");
|
||||
}
|
||||
|
||||
/// The bar's two events are two edges (1953): a change of the slots fires `PET_BAR_UPDATE`; a
|
||||
/// change of the cooldown triples alone fires `PET_BAR_UPDATE_COOLDOWN` — the reference's
|
||||
/// cooldown-subsystem fire for the pet bank (wow-re `pet-action-bar-api.md` §9) — and the
|
||||
/// pushed state carries the new triple either way.
|
||||
#[test]
|
||||
fn a_cooldown_alone_fires_the_cooldown_event_and_not_the_bar_update() {
|
||||
use bevy::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
const CLAW: u32 = 3010;
|
||||
let claw = || benilla_formats::SpellDisplay {
|
||||
recovery_ms: 8_000,
|
||||
..spell("Claw", None)
|
||||
};
|
||||
let mut bar = PetBar {
|
||||
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
|
||||
..Default::default()
|
||||
};
|
||||
bar.spells.bar[3] = packed(CLAW, PET_ACT_ENABLED);
|
||||
let mut app = App::new();
|
||||
app.init_resource::<crate::ui_script::UiClock>()
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.insert_resource(crate::ui_action::Spells {
|
||||
catalog: benilla_formats::SpellCatalog::from_displays(HashMap::from([(CLAW, claw())])),
|
||||
..crate::ui_action::Spells::empty_for_tests()
|
||||
})
|
||||
.insert_resource(bar)
|
||||
.add_systems(Update, super::bar::feed_pet_bar);
|
||||
let script = benilla_ui::script::UiScript::new().unwrap();
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
SEEN = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PET_BAR_UPDATE")
|
||||
f:RegisterEvent("PET_BAR_UPDATE_COOLDOWN")
|
||||
f:SetScript("OnEvent", function() table.insert(SEEN, event) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
app.insert_non_send_resource(script);
|
||||
let seen = |app: &mut App| -> Vec<String> {
|
||||
app.update();
|
||||
let mut s = app
|
||||
.world_mut()
|
||||
.non_send_resource_mut::<benilla_ui::script::UiScript>();
|
||||
s.resolve();
|
||||
let out = s.eval::<Vec<String>>("return SEEN").unwrap();
|
||||
s.run("SEEN = {}").unwrap();
|
||||
out
|
||||
};
|
||||
assert_eq!(
|
||||
seen(&mut app),
|
||||
vec!["PET_BAR_UPDATE".to_string()],
|
||||
"the bar arrives"
|
||||
);
|
||||
assert_eq!(seen(&mut app), Vec::<String>::new(), "nothing moved");
|
||||
|
||||
app.world_mut()
|
||||
.resource_mut::<PetBar>()
|
||||
.cooldowns
|
||||
.start_spell(CLAW, &claw(), 0, std::time::Instant::now());
|
||||
assert_eq!(
|
||||
seen(&mut app),
|
||||
vec!["PET_BAR_UPDATE_COOLDOWN".to_string()],
|
||||
"a cooldown alone is the bank's edge, not the bar's"
|
||||
);
|
||||
let (start, duration, enable): (f64, f64, i32) = app
|
||||
.world_mut()
|
||||
.non_send_resource_mut::<benilla_ui::script::UiScript>()
|
||||
.eval("return GetPetActionCooldown(4)")
|
||||
.unwrap();
|
||||
assert!(
|
||||
duration > 7.9 && duration < 8.1,
|
||||
"the triple was pushed: {start} {duration}"
|
||||
);
|
||||
assert_eq!(enable, 1);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,9 @@ fn shipped_xml() -> Vec<(String, String)> {
|
||||
})
|
||||
.collect();
|
||||
out.sort();
|
||||
assert!(out.len() >= 38, "only {} xml files swept", out.len());
|
||||
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751), so
|
||||
// the floor sits well under the count rather than one step above it (1956).
|
||||
assert!(out.len() >= 20, "only {} xml files swept", out.len());
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -603,10 +603,11 @@ fn the_shipped_frames_carry_the_references_flags() {
|
||||
}
|
||||
|
||||
assert!(
|
||||
// 1948 retired our ChatFrame.xml and 1952 our SpellBookFrame.xml (their frames are the
|
||||
// reference's own now), which took the paired count from the low 400s to the low 360s;
|
||||
// the floor guards the pairing, not the census.
|
||||
compared > 350,
|
||||
// 1948 retired our ChatFrame.xml, 1952 our SpellBookFrame.xml, 1953 our PetActionBar.xml
|
||||
// and 1956 our SkillFrame.xml (their frames are the reference's own now), which took the
|
||||
// paired count from the low 400s to the high 310s; the floor guards the pairing, not the
|
||||
// census.
|
||||
compared > 300,
|
||||
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The pet action bar (`PetActionBar.xml`) driven end to end through the REAL shipped XML
|
||||
//! The pet action bar (the reference's `PetActionBarFrame.xml`, 1953) driven end to end through the REAL shipped XML
|
||||
//! (decision 0982) — the `multibar_stance_tests` pattern: a self-contained loader, then the
|
||||
//! whole chain from a pushed slot list to the quads it actually paints.
|
||||
|
||||
@@ -9,33 +9,42 @@ use super::test_ui::load_ui as load_xml;
|
||||
/// The pet bar's own load prerequisites, in manifest order: UiPanels (`SetDesaturation`, the
|
||||
/// disabled-bar grey), UIParent (the managed bottom stack its OnShow/OnHide re-fires), Cooldown
|
||||
/// (`CooldownFrame_SetTimer`), ActionBar (the `MainMenuBar` anchor target) — then the bar.
|
||||
fn load_pet_bar(s: &UiScript) {
|
||||
/// The reference's pet bar needs what the manifest loads before it: the action-bar chain
|
||||
/// (`ActionButton_UpdateState`, `MainMenuBar` as its parent, `CooldownFrame_SetTimer`), the
|
||||
/// options window's uvars for `MultiActionBars.lua`, `SetDesaturation` and `GetBindingText` from
|
||||
/// our UIParent.lua slices, `TEXT` — and the chat window, whose edit box `ShowPetActionBar`
|
||||
/// raises over the sliding bar (PetActionBarFrame.lua l.175). One chain, in the manifest's order,
|
||||
/// for every test here (1953).
|
||||
pub(super) fn load_pet_bar(s: &UiScript) {
|
||||
for file in [
|
||||
"MoneyFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
// two template files its check boxes inherit through (1875).
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"PetActionBar.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"Interface\\FrameXML\\MultiActionBars.xml",
|
||||
"Interface\\FrameXML\\PetActionBarFrame.xml",
|
||||
"PetActionBarAdapters.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
}
|
||||
@@ -44,7 +53,7 @@ fn load_pet_bar(s: &UiScript) {
|
||||
/// GlobalStrings is loaded from the MPQ at runtime, not by the loader — a token slot's `name` is
|
||||
/// a KEY into it, so the tests declare the two keys they read. (That the real keys exist in the
|
||||
/// shipped file is a separate fact, asserted in `ui_pet`'s own tests by name.)
|
||||
fn declare_token_strings(s: &UiScript) {
|
||||
pub(super) fn declare_token_strings(s: &UiScript) {
|
||||
s.run(
|
||||
"PET_ACTION_ATTACK = 'Attack' \
|
||||
PET_MODE_DEFENSIVE = 'Defensive'",
|
||||
@@ -59,7 +68,7 @@ const CLAW_WORD: u32 = 0xC100_0BC2;
|
||||
|
||||
/// A hunter's bar as the server actually sends it: Attack (a lit command token), an empty spell
|
||||
/// slot, Claw (a spell with autocast running), and Defensive (a lit reaction token).
|
||||
fn hunter_slots() -> Vec<PetActionView> {
|
||||
pub(super) fn hunter_slots() -> Vec<PetActionView> {
|
||||
let mut slots = vec![PetActionView::default(); 10];
|
||||
slots[0] = PetActionView {
|
||||
name: Some("PET_ACTION_ATTACK".into()),
|
||||
@@ -208,10 +217,13 @@ fn the_shipped_pet_bar_drives_end_to_end() {
|
||||
(72.0, 56.0, 102.0, 86.0)
|
||||
);
|
||||
|
||||
// The pet goes: the bar hides again, shine marker and all.
|
||||
// The pet goes: the bar slides out over the reference's PETACTIONBAR_SLIDETIME (0.09 s,
|
||||
// `PetActionBarFrame_OnUpdate`) and hides at the end of the slide, shine marker and all.
|
||||
s.set_pet_actions(false, true, true, Vec::new());
|
||||
s.fire_event("PET_BAR_UPDATE", vec![]);
|
||||
s.tick(0.05);
|
||||
for _ in 0..3 {
|
||||
s.tick(0.05);
|
||||
}
|
||||
s.resolve();
|
||||
let gone = s.extract();
|
||||
assert_eq!(textures(&gone, "Interface\\PetActionBar\\UI-PetBar"), 0);
|
||||
@@ -367,47 +379,11 @@ fn a_disabled_bar_greys_rather_than_hides() {
|
||||
fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in [
|
||||
"MoneyFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
// two template files its check boxes inherit through (1875).
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_pet_bar(&s);
|
||||
if with_multibar {
|
||||
s.run("SHOW_MULTI_ACTIONBAR_1 = 1 MultiActionBar_Update()")
|
||||
.unwrap();
|
||||
}
|
||||
load_xml(&s, "PetActionBar.xml");
|
||||
declare_token_strings(&s);
|
||||
|
||||
s.set_pet_actions(true, true, true, hunter_slots());
|
||||
@@ -670,40 +646,6 @@ fn the_keybind_pair_pushes_and_casts_without_the_clicks_forks() {
|
||||
// law, the no-seam continuity pin) live with the drawing now: `crate::autocast_shine`'s own
|
||||
// test module — the script layer no longer moves a single spark (decision 1383).
|
||||
|
||||
/// The pet bar plus the three files its TOOLTIP needs, in the TOC's own order: Fonts.xml (the
|
||||
/// colour codes that wrap the binding), GameTooltip.xml (the plate), and UIParent.xml — already a
|
||||
/// prerequisite for the managed stack — for `GetBindingText`, which renders the key.
|
||||
fn load_pet_bar_with_tooltip(s: &UiScript) {
|
||||
for file in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"UiPanels.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
// two template files its check boxes inherit through (1875).
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"PetActionBar.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
}
|
||||
}
|
||||
|
||||
/// A hunter bar hovered through the REAL gesture, with the real binding registry and the real CVar
|
||||
/// table behind it — `mouse_move` runs the shipped `<OnEnter>` with `this` bound, which is the only
|
||||
/// way this fork gets exercised the way nazriel exercised it.
|
||||
@@ -722,7 +664,7 @@ fn hovered_pet_bar() -> UiScript {
|
||||
// `GetCVar("UberTooltips")` answers "1" (its byte-read registrar value) rather than nil.
|
||||
s.register_bindings(&crate::bindings::registry_commands());
|
||||
s.register_cvars(crate::cvars::registered_pairs());
|
||||
load_pet_bar_with_tooltip(&s);
|
||||
load_pet_bar(&s);
|
||||
declare_token_strings(&s);
|
||||
s.set_pet_actions(true, true, true, hunter_slots());
|
||||
s.fire_event("PET_BAR_UPDATE", vec![]);
|
||||
|
||||
@@ -1479,6 +1479,33 @@ mod tests {
|
||||
keyring tooltips through `ContainerFrameAdapters.xml`'s wrapper (0765), which is \
|
||||
why nothing has hit it — the wrapper answers first for our own rows.",
|
||||
),
|
||||
(
|
||||
"SkillFrame.xml",
|
||||
"BuySkillTier",
|
||||
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
|
||||
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's LearnSkillButton calls it, and \
|
||||
that button shows only while `UnitCharacterPoints`'s second value or a row's \
|
||||
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
|
||||
skill-point wire exists; not built (1956).",
|
||||
),
|
||||
(
|
||||
"SkillFrame.xml",
|
||||
"AddSkillUp",
|
||||
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
|
||||
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's RightArrow calls it, and \
|
||||
that button shows only while `UnitCharacterPoints`'s second value or a row's \
|
||||
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
|
||||
skill-point wire exists; not built (1956).",
|
||||
),
|
||||
(
|
||||
"SkillFrame.xml",
|
||||
"RemoveSkillUp",
|
||||
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
|
||||
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's LeftArrow calls it, and \
|
||||
that button shows only while `UnitCharacterPoints`'s second value or a row's \
|
||||
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
|
||||
skill-point wire exists; not built (1956).",
|
||||
),
|
||||
(
|
||||
"ClassTrainerFrameTemplates.xml",
|
||||
"ClassTrainerSkillButton_OnClick",
|
||||
@@ -2070,10 +2097,6 @@ mod tests {
|
||||
// built; none is 1819-shaped, because no PAIR is split (a half-fired pair is the tell).
|
||||
const UNPRODUCED: &[(&str, &str)] = &[
|
||||
("BAG_OPEN", "ContainerFrame.lua"),
|
||||
(
|
||||
"CVAR_UPDATE",
|
||||
"TextStatusBar.lua — a CVar change does not repaint the bar text",
|
||||
),
|
||||
("DISPLAY_SIZE_CHANGED", "the four paperdoll files"),
|
||||
(
|
||||
"GMSURVEY_DISPLAY",
|
||||
@@ -2177,8 +2200,13 @@ mod tests {
|
||||
stack.push(p);
|
||||
} else if p.extension().is_some_and(|x| x == "rs") {
|
||||
let text = std::fs::read_to_string(&p).unwrap_or_default();
|
||||
const CALLS: [&str; 2] =
|
||||
[concat!("fire_event", "("), concat!("fire_event_into", "(")];
|
||||
// The engine's deferred lane (`pending_events.push((name, args))`,
|
||||
// `cursor.rs`) is a fire too — the pet grid pair rides it (1953).
|
||||
const CALLS: [&str; 3] = [
|
||||
concat!("fire_event", "("),
|
||||
concat!("fire_event_into", "("),
|
||||
concat!("pending_events.push", "(("),
|
||||
];
|
||||
let mut fires_indirectly = false;
|
||||
for call in CALLS {
|
||||
let mut from = 0;
|
||||
|
||||
@@ -30,7 +30,9 @@ fn every_shipped_ui_xml_parses() {
|
||||
// Never let the sweep pass by finding nothing — a moved assets dir would otherwise turn this
|
||||
// into a test that guards zero files while staying green.
|
||||
assert!(
|
||||
checked >= 38, // 41 until 1944 took the two quest files stock
|
||||
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751),
|
||||
// so the floor sits well under the count rather than one step above it (1956).
|
||||
checked >= 20,
|
||||
"only {checked} xml files swept — sweep broke"
|
||||
);
|
||||
}
|
||||
@@ -123,6 +125,23 @@ fn loading_the_shipped_ui_queues_no_sounds() {
|
||||
/// a white slab over its autocast ring and another under its tab row. Every gate was green.
|
||||
///
|
||||
/// The shape half runs everywhere; the resolve half needs client data and skips without it (a
|
||||
/// The reference's pet bar, up with the hunter fixture (Claw autocasting), settled past its
|
||||
/// slide — the pet half of the two shine pins below.
|
||||
fn pet_bar_vm() -> benilla_ui::script::UiScript {
|
||||
let mut s = benilla_ui::script::UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::pet_bar_tests::load_pet_bar(&s);
|
||||
super::pet_bar_tests::declare_token_strings(&s);
|
||||
s.set_pet_actions(true, true, true, super::pet_bar_tests::hunter_slots());
|
||||
s.fire_event("PET_BAR_UPDATE", vec![]);
|
||||
for _ in 0..3 {
|
||||
s.tick(0.05);
|
||||
}
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "pet bar errors: {:?}", s.errors());
|
||||
s
|
||||
}
|
||||
|
||||
/// The autocast **corner brackets** (`UI-AutoCastableOverlay`), per template — the sibling pin to
|
||||
/// the shine's, and the same invariant: not a magic size, but the **bracket square against its
|
||||
/// button**, which is the thing an eye actually reads.
|
||||
@@ -199,6 +218,12 @@ fn the_autocast_brackets_reach_each_buttons_corners() {
|
||||
.eval("return SpellButton1AutoCastable:GetWidth(), SpellButton1:GetWidth()")
|
||||
.unwrap();
|
||||
found.push(("SpellButton1AutoCastable".into(), overlay * ART, button));
|
||||
// The pet button's overlay is the reference's own template (1953) — measured the same way.
|
||||
let s = pet_bar_vm();
|
||||
let (overlay, button): (f32, f32) = s
|
||||
.eval("return PetActionButton1AutoCastable:GetWidth(), PetActionButton1:GetWidth()")
|
||||
.unwrap();
|
||||
found.push(("PetActionButton1AutoCastable".into(), overlay * ART, button));
|
||||
assert_eq!(found.len(), 2, "expected two autocast overlays: {found:?}");
|
||||
for (name, brackets, button) in &found {
|
||||
assert!(
|
||||
@@ -316,6 +341,23 @@ fn the_shine_tokens_ask_for_the_rims_we_meant() {
|
||||
let scale = crate::autocast_shine::token_model_scale(&token).expect("a shine token");
|
||||
let view: f32 = s.eval("return SpellButton1Shine:GetWidth()").unwrap();
|
||||
found.push(("SpellButton1Shine".into(), 0.02 * 1280.0 * scale, view));
|
||||
// The pet button's shine is PetActionBarAdapters.xml's, over the whole button (1953): the
|
||||
// token off the shown marker's quad, the viewport off that quad's own rect.
|
||||
let s = pet_bar_vm();
|
||||
let (token, view) = s
|
||||
.extract()
|
||||
.into_iter()
|
||||
.find_map(|q| match (&q.content, q.rect) {
|
||||
(benilla_ui::script::QuadContent::Texture { path: Some(p), .. }, Some(r))
|
||||
if p.starts_with(crate::autocast_shine::SHINE_TOKEN) =>
|
||||
{
|
||||
Some((p.clone(), r.right - r.left))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("the pet bar's shine marker (Claw autocasts in the hunter fixture)");
|
||||
let scale = crate::autocast_shine::token_model_scale(&token).expect("a shine token");
|
||||
found.push(("PetActionButton1Shine".into(), 0.02 * 1280.0 * scale, view));
|
||||
assert_eq!(
|
||||
found.len(),
|
||||
2,
|
||||
@@ -797,7 +839,8 @@ fn no_shipped_script_sets_a_global_string_key_as_display_text() {
|
||||
}
|
||||
assert!(offenders.is_empty(), "{}", offenders.join("\n"));
|
||||
// The sweep must never pass by finding nothing to sweep.
|
||||
assert!(swept >= 38, "only {swept} xml files swept — sweep broke");
|
||||
// The same walk floor as above (1956).
|
||||
assert!(swept >= 20, "only {swept} xml files swept — sweep broke");
|
||||
}
|
||||
|
||||
/// **The `$parentTextureFrame` idiom's contract, over the whole shipped UI**: a frame whose art is
|
||||
@@ -1394,7 +1437,9 @@ fn every_declared_parent_really_attaches() {
|
||||
|
||||
let declared = shipped_frame_parents();
|
||||
assert!(
|
||||
declared.len() > 30, // 67 before 1938 took the three bar files (25 of them) stock
|
||||
// A sanity floor for the scan, not a census — the declarations retire with the files
|
||||
// that carry them (1751); 67 before 1938, 30 after 1956.
|
||||
declared.len() >= 15,
|
||||
"only {} parent declarations found — the scan broke",
|
||||
declared.len()
|
||||
);
|
||||
|
||||
@@ -57,8 +57,18 @@ fn skill(
|
||||
/// silently never got built (`SkillDetailScrollFrame` inherits `UIPanelScrollFrameTemplate`).
|
||||
/// [`super::test_ui::load_ui_strict`] is that lesson made into a check.
|
||||
fn shown_skills_page() -> UiScript {
|
||||
shown_skills_page_with(None)
|
||||
}
|
||||
|
||||
/// [`shown_skills_page`] with a text measurer seated BEFORE the files load — the state the app's
|
||||
/// VM is in at world entry (the glue screens' frames seat it), which a stock `OnLoad` that measures
|
||||
/// text relies on.
|
||||
fn shown_skills_page_with(measurer: Option<Box<dyn benilla_ui::script::TextMeasure>>) -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
if let Some(m) = measurer {
|
||||
s.set_text_measurer(m);
|
||||
}
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
@@ -201,7 +211,7 @@ fn a_single_rank_line_paints_gray_with_no_rank_text() {
|
||||
);
|
||||
|
||||
// Selecting the single-rank row paints the detail pane the same way (the shared PaintBar).
|
||||
s.run("SetSelectedSkill(2) BenillaSkillFrame_Update()")
|
||||
s.run("SetSelectedSkill(2) SkillFrame_UpdateSkills()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SkillDetailStatusBarSkillRank:GetText() or \"\"")
|
||||
@@ -458,30 +468,20 @@ fn the_collapse_all_fold_wears_the_row_font_and_the_references_seat() {
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The tab's **fit law** (ref `SkillFrameExpandButtonFrame`'s OnLoad, l.312-316:
|
||||
/// `SetWidth(GetTextWidth()+45)`). benilla seats the VM's font engine at the frame boundary, so
|
||||
/// the XML-load call reads 0 — the guard leaves the declared width standing, and the first Update
|
||||
/// with a measurer fits the tab. Both states are pinned here because only the second one is the
|
||||
/// reference's, and only the first is what a cold load sees.
|
||||
/// The tab's **fit law** (ref `SkillFrameExpandButtonFrame`'s OnLoad:
|
||||
/// `SetWidth(SkillFrameCollapseAllButton:GetTextWidth()+45)`), which the reference applies ONCE,
|
||||
/// at load, with its font engine already up. The app's VM is in that state at world entry (the
|
||||
/// glue screens seat the measurer on it), so the test seats one first; the transcription used to
|
||||
/// guard a 0 measure and re-fit on the first Update, which the reference never does (1956).
|
||||
#[test]
|
||||
fn the_expand_tab_fits_its_label_once_a_measure_answers() {
|
||||
fn the_expand_tab_fits_its_label_at_load() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_skills_page();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return SkillFrameExpandButtonFrame:GetWidth()")
|
||||
.unwrap(),
|
||||
54.0,
|
||||
"unmeasured, the declared width stands — a 0 measure must not squash the tab to 45"
|
||||
);
|
||||
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(7.0)));
|
||||
s.run("BenillaSkillFrame_Update()").unwrap();
|
||||
s.resolve();
|
||||
let mut s = shown_skills_page_with(Some(Box::new(super::FixedWidthFont(7.0))));
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return SkillFrameExpandButtonFrame:GetWidth()")
|
||||
.unwrap(),
|
||||
7.0 * 3.0 + 45.0,
|
||||
"then the ref's own law: the label's width + 45"
|
||||
"the ref's own law at load: the ALL label's width + 45"
|
||||
);
|
||||
// The middle slab is the span between the two caps, so the fit reaches the art for free.
|
||||
let (mid_l, mid_r, cap_r_l) = s
|
||||
@@ -496,5 +496,13 @@ fn the_expand_tab_fits_its_label_once_a_measure_answers() {
|
||||
66.0 - 16.0,
|
||||
"and carries the whole span minus the two caps"
|
||||
);
|
||||
// An Update leaves the fit alone — the reference sizes the tab at load and never again.
|
||||
s.run("SkillFrame_UpdateSkills()").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return SkillFrameExpandButtonFrame:GetWidth()")
|
||||
.unwrap(),
|
||||
66.0
|
||||
);
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ pub(super) const CHARACTER_UI: &[&str] = &[
|
||||
// bag row. It comes with `ContainerFrame.xml`, manifest 585 against the pane's 693 (1875).
|
||||
"Interface\\FrameXML\\ContainerFrame.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"SkillFrame.xml",
|
||||
"Interface\\FrameXML\\SkillFrame.xml",
|
||||
"Interface\\FrameXML\\HonorFrame.xml",
|
||||
];
|
||||
|
||||
|
||||
@@ -457,9 +457,10 @@ fn solo_target_inspect_click_reaches_inspect_unit() {
|
||||
|
||||
// ── The PET menu (decision 1066; report B219) ───────────────────────────────────────────────────
|
||||
|
||||
/// The pet menu's own prefix. Two things join the popup prefix: `UiPanels.xml` (the StaticPopup
|
||||
/// engine, because two of the four rows go behind a dialog) and `PetActionBar.xml`, which is where
|
||||
/// those three dialogs are registered — with the pet arc, not with the rows. `Cooldown.xml` and
|
||||
/// The pet menu's own prefix. What joins the popup prefix is `UiPanels.xml` — the StaticPopup
|
||||
/// engine, because two of the four rows go behind a dialog, and since 1953 where the pet arc's
|
||||
/// three dialogs are registered (they rode our pet-bar file until it became the reference's).
|
||||
/// `Cooldown.xml` and
|
||||
/// `ActionBar.xml` are the pet bar's own load deps, not this menu's.
|
||||
fn load_pet_menu_frames(s: &UiScript) {
|
||||
for file in [
|
||||
@@ -487,7 +488,6 @@ fn load_pet_menu_frames(s: &UiScript) {
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"PetActionBar.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
}
|
||||
|
||||
@@ -187,6 +187,8 @@ impl Plugin for UiUnitPlugin {
|
||||
(
|
||||
feed_units,
|
||||
feed_unit_reach,
|
||||
feed_player_control,
|
||||
feed_farsight_focus,
|
||||
melee_unit_combat,
|
||||
fire_unit_combat,
|
||||
fire_combat_text,
|
||||
@@ -730,6 +732,55 @@ fn dist_sq(q: Vec3, p: Vec3) -> f64 {
|
||||
(dz * dz + dx * dx) + dy * dy
|
||||
}
|
||||
|
||||
/// `PLAYER_CONTROL_LOST` / `PLAYER_CONTROL_GAINED` — `SMSG_CLIENT_CONTROL_UPDATE` naming the
|
||||
/// local player reaches `0x4958e0`, which writes the player-control flag and, **on a change**,
|
||||
/// fires LOST when the byte is zero and GAINED when it is not; the boot init is "in control"
|
||||
/// (wow-re `farsight-and-client-control.md` §5, `incoming-trade-request-law.md`). The flag is
|
||||
/// [`crate::player::Player::control_lost`], which `player::wire_in` writes from that packet; this
|
||||
/// fires the edge, and a fresh VM's memo is the boot value.
|
||||
fn feed_player_control(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
player: Option<Res<crate::player::Player>>,
|
||||
mut lost: Local<crate::ui_script::VmMemo<bool>>,
|
||||
) {
|
||||
let (Some(mut script), Some(player)) = (script, player) else {
|
||||
return;
|
||||
};
|
||||
let lost = lost.get(&script);
|
||||
if *lost != player.control_lost {
|
||||
*lost = player.control_lost;
|
||||
let event = if player.control_lost {
|
||||
"PLAYER_CONTROL_LOST"
|
||||
} else {
|
||||
"PLAYER_CONTROL_GAINED"
|
||||
};
|
||||
script.fire_event(event, vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
/// `PLAYER_FARSIGHT_FOCUS_CHANGED` — the `PLAYER_FARSIGHT` field-change callback (`0x5de0d0`)
|
||||
/// fires it on both of its legs, whether or not the new guid resolves to a streamed object
|
||||
/// (wow-re `farsight-and-client-control.md` §2). The edge is the FIELD's, so this diffs the
|
||||
/// descriptor value the camera's `publish_view_subject` reads, never the resolved pose.
|
||||
fn feed_farsight_focus(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
self_q: Query<&ObjectStore, With<SelfPlayer>>,
|
||||
mut focus: Local<crate::ui_script::VmMemo<Option<u64>>>,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
};
|
||||
let Some(store) = self_q.iter().next() else {
|
||||
return;
|
||||
};
|
||||
let anchor = store.0.player_farsight();
|
||||
let focus = focus.get(&script);
|
||||
if *focus != anchor {
|
||||
*focus = anchor;
|
||||
script.fire_event("PLAYER_FARSIGHT_FOCUS_CHANGED", vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed the **unit reach map** — for every token that resolves to a live unit object, its squared
|
||||
/// distance from us, plus whether that unit passes inspect's own two non-distance refusals.
|
||||
///
|
||||
@@ -899,6 +950,7 @@ pub(crate) fn snapshot(
|
||||
// leg of the selection ring). Wider than `is_player` above: a pet or a charmed creature
|
||||
// sets it without being a player.
|
||||
player_controlled: store.0.unit_flags() & 0x8 != 0,
|
||||
flags: store.0.unit_flags(),
|
||||
// `UnitAffectingCombat 0x517e10` — the SAME `UNIT_FIELD_FLAGS` word, bit 19
|
||||
// (`shr ecx,0x13; test cl,1`). One flag for every token: wow-re's whole-image census of
|
||||
// that idiom found the local-player readers reading this identical bit, so there is no
|
||||
@@ -1175,6 +1227,12 @@ pub(crate) fn fire_transitions(
|
||||
if changed(|u| u64::from(u.power_type)) {
|
||||
script.fire_event("UNIT_DISPLAYPOWER", vec![tok()]);
|
||||
}
|
||||
// `UNIT_FLAGS` — on a change of the unit's `UNIT_FIELD_FLAGS`, the field the reference's
|
||||
// per-unit reflex is registered on by byte offset (wow-re `pet-action-bar-api.md` §9's
|
||||
// `0x5ff580`). The stock pet bar filters it for `"pet"` and re-derives its usability (1953).
|
||||
if changed(|u| u64::from(u.flags)) {
|
||||
script.fire_event("UNIT_FLAGS", vec![tok()]);
|
||||
}
|
||||
// The POWER pair is named per resource in 1.12, not once with the token as arg2: the reference's
|
||||
// `UnitFrameManaBar_Initialize` registers `UNIT_MANA`/`UNIT_RAGE`/`UNIT_FOCUS`/`UNIT_ENERGY`/
|
||||
// `UNIT_HAPPINESS` and the five `UNIT_MAX*` twins (`UnitFrame.lua:190-199`), and nothing in
|
||||
@@ -1928,6 +1986,128 @@ mod tests {
|
||||
/// Bit `0x8` has **no** arm, in the reference or here — proven there by enumerating all 122
|
||||
/// instructions and 12 branches of the watcher. The negative half is asserted too, because
|
||||
/// "fire on both, it's cheaper" is the obvious wrong simplification.
|
||||
/// `UNIT_FLAGS` rides the raw field: any change of `UNIT_FIELD_FLAGS` fires it with the
|
||||
/// token, an unchanged field does not, and a unit's first snapshot is a transition (1953).
|
||||
#[test]
|
||||
fn a_flags_change_fires_unit_flags_with_the_token() {
|
||||
let fired = |prev: Option<UnitState>, cur: UnitState| -> Vec<String> {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
SEEN = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UNIT_FLAGS")
|
||||
f:SetScript("OnEvent", function() table.insert(SEEN, event .. ":" .. arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
fire_transitions(&mut s, "pet", prev.as_ref(), &cur);
|
||||
s.eval::<Vec<String>>("return SEEN").unwrap()
|
||||
};
|
||||
let base = UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
flags: 0x8,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
fired(
|
||||
Some(base.clone()),
|
||||
UnitState {
|
||||
flags: 0x8 | 0x0400_0000,
|
||||
..base.clone()
|
||||
}
|
||||
),
|
||||
vec!["UNIT_FLAGS:pet".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
fired(Some(base.clone()), base.clone()),
|
||||
Vec::<String>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
fired(None, base.clone()),
|
||||
vec!["UNIT_FLAGS:pet".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
/// The two edges on the player's own state (1953): the control flag's, which fires LOST on
|
||||
/// the way down and GAINED on the way up and nothing while it holds (the boot value is "in
|
||||
/// control"), and the far-sight field's, which fires on every change including the clear.
|
||||
#[test]
|
||||
fn the_control_and_far_sight_edges_fire_once_each_way() {
|
||||
use bevy::prelude::*;
|
||||
const FIELD_PLAYER_FARSIGHT: u16 = 712;
|
||||
let mut app = App::new();
|
||||
app.init_resource::<crate::player::Player>()
|
||||
.add_systems(Update, (feed_player_control, feed_farsight_focus));
|
||||
let script = UiScript::new().unwrap();
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
SEEN = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_CONTROL_LOST")
|
||||
f:RegisterEvent("PLAYER_CONTROL_GAINED")
|
||||
f:RegisterEvent("PLAYER_FARSIGHT_FOCUS_CHANGED")
|
||||
f:SetScript("OnEvent", function() table.insert(SEEN, event) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
app.insert_non_send_resource(script);
|
||||
let me = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
SelfPlayer,
|
||||
Guid(0x77),
|
||||
ObjectStore(benilla_protocol::ObjectFields::default()),
|
||||
))
|
||||
.id();
|
||||
let seen = |app: &mut App| -> Vec<String> {
|
||||
app.update();
|
||||
let mut s = app.world_mut().non_send_resource_mut::<UiScript>();
|
||||
s.resolve();
|
||||
let out = s.eval::<Vec<String>>("return SEEN").unwrap();
|
||||
s.run("SEEN = {}").unwrap();
|
||||
out
|
||||
};
|
||||
assert_eq!(
|
||||
seen(&mut app),
|
||||
Vec::<String>::new(),
|
||||
"in control, no far sight: quiet"
|
||||
);
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::player::Player>()
|
||||
.control_lost = true;
|
||||
assert_eq!(seen(&mut app), vec!["PLAYER_CONTROL_LOST".to_string()]);
|
||||
assert_eq!(seen(&mut app), Vec::<String>::new(), "held, not repeated");
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::player::Player>()
|
||||
.control_lost = false;
|
||||
assert_eq!(seen(&mut app), vec!["PLAYER_CONTROL_GAINED".to_string()]);
|
||||
|
||||
let set_farsight = |app: &mut App, guid: u64| {
|
||||
app.world_mut().entity_mut(me).insert(ObjectStore(
|
||||
benilla_protocol::ObjectFields::from_pairs(&[
|
||||
(FIELD_PLAYER_FARSIGHT, guid as u32),
|
||||
(FIELD_PLAYER_FARSIGHT + 1, (guid >> 32) as u32),
|
||||
]),
|
||||
));
|
||||
};
|
||||
set_farsight(&mut app, 0xf130_0000_0000_0042);
|
||||
assert_eq!(
|
||||
seen(&mut app),
|
||||
vec!["PLAYER_FARSIGHT_FOCUS_CHANGED".to_string()],
|
||||
"set — whether or not the guid resolves"
|
||||
);
|
||||
assert_eq!(seen(&mut app), Vec::<String>::new());
|
||||
set_farsight(&mut app, 0);
|
||||
assert_eq!(
|
||||
seen(&mut app),
|
||||
vec!["PLAYER_FARSIGHT_FOCUS_CHANGED".to_string()],
|
||||
"cleared — the other leg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tapped_bit_fires_unit_faction_and_the_by_player_bit_fires_nothing() {
|
||||
let fired = |prev: UnitState, cur: UnitState| -> Vec<String> {
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
//! clears it. `GetAdjustedSkillPoints()` is a vestigial 1.12 leftover the ref reads; it always
|
||||
//! returns `0` — there is no training-point economy behind a skill line in this client.
|
||||
//!
|
||||
//! `CancelSkillUps()` is bound (the page's Close button calls it, 1956): the temp-point reset
|
||||
//! over a table this model keeps empty.
|
||||
//!
|
||||
//! The ref Lua's other globals (`SkillBar_OnClick`'s `RemoveSkillUp`/`AddSkillUp`/`BuySkillTier`,
|
||||
//! `UnitCharacterPoints`) back the training-up machinery this pane doesn't ship (0437's named
|
||||
//! out-of-scope) — none are transcribed, engine-side or XML-side, rather than stubbing dead call
|
||||
@@ -535,6 +538,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// CancelSkillUps() — `0x4d3e30` → `0x4d35c9` (reference `1.12-shapes.tsv`: no arguments, no
|
||||
// returns): the reset leg of the temp-point table, zeroing every entry's `numTempPoints` and
|
||||
// giving the points back to the pool. The stock page's LIVE Close button calls it before
|
||||
// hiding (`SkillFrame.xml`'s SkillFrameCancelButton, decision 1496), so it is reached on
|
||||
// every close. This model carries no temp points (module docs, 1919): the table it resets is
|
||||
// empty by construction, so the reset moves nothing — the binding's whole effect on this
|
||||
// client's data, not a stand-in for it. The day temp points are modelled, this is where
|
||||
// they are cleared.
|
||||
g.set("CancelSkillUps", lua.create_function(|_, ()| Ok(()))?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +195,9 @@ pub struct UnitState {
|
||||
/// player-options newbie tip. `UnitIsPlayer`'s note used to call this reach a gap we did not
|
||||
/// carry; the unit-frame migration is what closed it.
|
||||
pub player_controlled: bool,
|
||||
/// `UNIT_FIELD_FLAGS`, raw — what `UNIT_FLAGS` fires on (the app's `fire_transitions`); the
|
||||
/// three readings above are bits of it.
|
||||
pub flags: u32,
|
||||
/// The unit's guild membership (`GetGuildInfo(unit)`, decision 1257). `None` = guildless, or
|
||||
/// a creature, or a player whose `PLAYER_GUILDID` has not streamed yet. Filled from the
|
||||
/// PUBLIC descriptor fields 191/192 joined against the app's guild-identity cache — see
|
||||
|
||||
@@ -57,6 +57,10 @@ pub struct PropProbes {
|
||||
/// Peak concurrent DISTINCT occupancy (diagnostics: logged by the spawner on overflow; a peak
|
||||
/// near capacity is the signal to grow the table).
|
||||
peak: usize,
|
||||
/// Slots written since the last publish, as a `[lo, hi)` span — what the upload writes,
|
||||
/// instead of every allocated row: a walk streams props in and out most frames and each
|
||||
/// change re-sent the whole span (hundreds of KB) for one row.
|
||||
dirty: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl Default for PropProbes {
|
||||
@@ -69,6 +73,7 @@ impl Default for PropProbes {
|
||||
slots: vec![None; MAX_PROP_PROBES],
|
||||
generation: 0,
|
||||
peak: 0,
|
||||
dirty: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +95,7 @@ impl PropProbes {
|
||||
Arc::make_mut(&mut self.rows)[slot as usize] = coeffs.map(|v| v.to_array());
|
||||
self.by_key.insert(key.clone(), slot);
|
||||
self.slots[slot as usize] = Some((1, Some(key)));
|
||||
self.generation += 1;
|
||||
self.touch(slot);
|
||||
self.peak = self.peak.max(self.high - self.free.len());
|
||||
Some(slot)
|
||||
}
|
||||
@@ -103,7 +108,7 @@ impl PropProbes {
|
||||
let slot = self.take_free_slot()?;
|
||||
Arc::make_mut(&mut self.rows)[slot as usize] = coeffs.map(|v| v.to_array());
|
||||
self.slots[slot as usize] = Some((1, None));
|
||||
self.generation += 1;
|
||||
self.touch(slot);
|
||||
self.peak = self.peak.max(self.high - self.free.len());
|
||||
Some(slot)
|
||||
}
|
||||
@@ -118,6 +123,16 @@ impl PropProbes {
|
||||
return;
|
||||
}
|
||||
Arc::make_mut(&mut self.rows)[slot as usize] = coeffs.map(|v| v.to_array());
|
||||
self.touch(slot);
|
||||
}
|
||||
|
||||
/// A row changed: widen the dirty span and bump the generation.
|
||||
fn touch(&mut self, slot: u16) {
|
||||
let s = slot as usize;
|
||||
self.dirty = Some(match self.dirty {
|
||||
Some((lo, hi)) => (lo.min(s), hi.max(s + 1)),
|
||||
None => (s, s + 1),
|
||||
});
|
||||
self.generation += 1;
|
||||
}
|
||||
|
||||
@@ -161,7 +176,7 @@ impl PropProbes {
|
||||
// Zero the freed probe so a stale MeshTag (a frame of despawn skew) reads black, not the
|
||||
// previous occupant's light.
|
||||
Arc::make_mut(&mut self.rows)[slot as usize] = [[0.0; 4]; 7];
|
||||
self.generation += 1;
|
||||
self.touch(slot);
|
||||
self.free.push(slot);
|
||||
}
|
||||
}
|
||||
@@ -174,6 +189,9 @@ pub(crate) struct PropProbeExtract {
|
||||
rows: Arc<Vec<[[f32; 4]; 7]>>,
|
||||
high: usize,
|
||||
generation: u64,
|
||||
/// The rows this generation changed, `[lo, hi)`; `None` = everything up to `high` (the
|
||||
/// first publish, and a render world that missed a generation).
|
||||
dirty: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl Default for PropProbeExtract {
|
||||
@@ -183,13 +201,25 @@ impl Default for PropProbeExtract {
|
||||
high: 0,
|
||||
// != PropProbes' initial 0, so the first refresh publishes even an empty table.
|
||||
generation: u64::MAX,
|
||||
dirty: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main world, after the spawners: publish the table for extraction when it changed.
|
||||
pub(super) fn publish_prop_probes(probes: Res<PropProbes>, mut out: ResMut<PropProbeExtract>) {
|
||||
pub(super) fn publish_prop_probes(
|
||||
mut probes: ResMut<PropProbes>,
|
||||
mut out: ResMut<PropProbeExtract>,
|
||||
) {
|
||||
if out.generation != probes.generation {
|
||||
// Consecutive generations carry their own span; a publish that skipped one (the
|
||||
// change-gated system did not run in between) sends the whole allocated span once.
|
||||
let consecutive = out.generation.wrapping_add(1) == probes.generation
|
||||
|| out
|
||||
.dirty
|
||||
.is_some_and(|_| out.generation != u64::MAX && probes.generation > out.generation);
|
||||
let dirty = probes.dirty.take();
|
||||
out.dirty = if consecutive { dirty } else { None };
|
||||
out.rows = Arc::clone(&probes.rows);
|
||||
out.high = probes.high;
|
||||
out.generation = probes.generation;
|
||||
@@ -215,13 +245,22 @@ pub(super) fn upload_prop_probes(
|
||||
if *last == Some(data.generation) {
|
||||
return;
|
||||
}
|
||||
// The changed span when this render frame follows the last one it uploaded; the whole
|
||||
// allocated span (freed slots inside it are zeroed rows) on the first upload and whenever a
|
||||
// generation went by unseen, so the GPU never keeps a row the CPU has since rewritten.
|
||||
let high = data.high.min(data.rows.len());
|
||||
let (lo, hi) = match (data.dirty, *last) {
|
||||
(Some((lo, hi)), Some(seen)) if seen.wrapping_add(1) == data.generation => {
|
||||
(lo.min(high), hi.min(high))
|
||||
}
|
||||
_ => (0, high),
|
||||
};
|
||||
*last = Some(data.generation);
|
||||
// Upload the whole allocated span (high slots). Freed slots inside it are zeroed rows.
|
||||
let rows = &data.rows[..data.high.min(data.rows.len())];
|
||||
let rows = &data.rows[lo..hi];
|
||||
if !rows.is_empty() {
|
||||
queue.write_buffer(
|
||||
&buffer.0,
|
||||
prop_probe_region_offset(),
|
||||
prop_probe_region_offset() + (lo * std::mem::size_of::<[[f32; 4]; 7]>()) as u64,
|
||||
bytemuck::cast_slice(rows),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ pub(crate) fn palette_regions_bytes() -> u64 {
|
||||
pub struct RigPalettes {
|
||||
/// `3 × MAX_PALETTE_BONES` vec4 rows — the CPU mirror (the mouseover picker reads it).
|
||||
rows: Arc<Vec<[f32; 4]>>,
|
||||
/// The row buffer retired by the last clone (`rows_make_mut`), reused once unshared.
|
||||
spare: Option<Arc<Vec<[f32; 4]>>>,
|
||||
/// Slot → base bone index. Slot 0 is the tag's "no rig" sentinel and never allocated.
|
||||
table: Arc<Vec<u32>>,
|
||||
/// Slot → the world position this rig's rows are measured from (decision 0974); `w` unused.
|
||||
@@ -154,6 +156,7 @@ impl Default for RigPalettes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rows: Arc::new(vec![[0.0; 4]; 3 * MAX_PALETTE_BONES]),
|
||||
spare: None,
|
||||
table: Arc::new(vec![0; MAX_RIG_SLOTS]),
|
||||
origins: Arc::new(vec![[0.0; 4]; MAX_RIG_SLOTS]),
|
||||
origin_generation: 0,
|
||||
@@ -196,6 +199,7 @@ pub fn rig_cost_enabled() -> bool {
|
||||
/// invariant (see [`RigPalettes::bone_watermark`]), so the prefix IS the whole content.
|
||||
fn rows_make_mut<'a>(
|
||||
rows: &'a mut Arc<Vec<[f32; 4]>>,
|
||||
spare: &mut Option<Arc<Vec<[f32; 4]>>>,
|
||||
watermark_bones: u32,
|
||||
copies: &mut u32,
|
||||
copy_us: &mut f32,
|
||||
@@ -203,19 +207,24 @@ fn rows_make_mut<'a>(
|
||||
if Arc::strong_count(rows) > 1 || Arc::weak_count(rows) > 0 {
|
||||
let t = std::time::Instant::now();
|
||||
let live = 3 * watermark_bones as usize;
|
||||
let mut new = vec![[0.0f32; 4]; rows.len()];
|
||||
// The retired buffer from two publishes ago, once the render world has let go of it:
|
||||
// copy the live prefix in and take it back. Its tail is stale — and never read: the GPU
|
||||
// only ever receives dirty ranges, so what the CPU vector holds past the watermark
|
||||
// reaches nothing (the calloc'd zeros it replaced were never uploaded either). A fresh
|
||||
// allocation only while the render world still holds both.
|
||||
let mut new = match spare.take().and_then(|a| Arc::try_unwrap(a).ok()) {
|
||||
Some(v) if v.len() == rows.len() => v,
|
||||
_ => vec![[0.0f32; 4]; rows.len()],
|
||||
};
|
||||
new[..live].copy_from_slice(&rows[..live]);
|
||||
*rows = Arc::new(new);
|
||||
*spare = Some(std::mem::replace(rows, Arc::new(new)));
|
||||
*copy_us += t.elapsed().as_secs_f32() * 1e6;
|
||||
*copies += 1;
|
||||
}
|
||||
// Sole owner here either way: no clone left to happen.
|
||||
// Uniquely owned now — `make_mut` is a plain borrow.
|
||||
Arc::make_mut(rows)
|
||||
}
|
||||
|
||||
/// Move a world affine into the rig's own frame (decision 0974): same 3×3, translation measured
|
||||
/// from `origin`. The subtraction is where the ~9 k-yard magnitude leaves the number — everything
|
||||
/// downstream of it is a rig-sized quantity.
|
||||
fn rebase(mut world: Affine3A, origin: Vec3) -> Affine3A {
|
||||
world.translation -= bevy::math::Vec3A::from(origin);
|
||||
world
|
||||
@@ -317,6 +326,7 @@ impl RigPalettes {
|
||||
let wm = self.bone_watermark();
|
||||
let rows = rows_make_mut(
|
||||
&mut self.rows,
|
||||
&mut self.spare,
|
||||
wm,
|
||||
&mut self.cost_copies,
|
||||
&mut self.cost_copy_us,
|
||||
@@ -385,6 +395,7 @@ impl RigPalettes {
|
||||
let wm = self.bone_watermark();
|
||||
let rows = rows_make_mut(
|
||||
&mut self.rows,
|
||||
&mut self.spare,
|
||||
wm,
|
||||
&mut self.cost_copies,
|
||||
&mut self.cost_copy_us,
|
||||
@@ -429,6 +440,7 @@ impl RigPalettes {
|
||||
let wm = self.bone_watermark();
|
||||
let rows = rows_make_mut(
|
||||
&mut self.rows,
|
||||
&mut self.spare,
|
||||
wm,
|
||||
&mut self.cost_copies,
|
||||
&mut self.cost_copy_us,
|
||||
@@ -487,6 +499,7 @@ impl RigPalettes {
|
||||
let wm = self.bone_watermark();
|
||||
let rows = rows_make_mut(
|
||||
&mut self.rows,
|
||||
&mut self.spare,
|
||||
wm,
|
||||
&mut self.cost_copies,
|
||||
&mut self.cost_copy_us,
|
||||
@@ -1188,7 +1201,13 @@ mod tests {
|
||||
// The extract's held reference — the shared state every first-write-of-a-frame sees.
|
||||
let held = p.rows.clone();
|
||||
let wm = p.bone_watermark();
|
||||
let rows = rows_make_mut(&mut p.rows, wm, &mut p.cost_copies, &mut p.cost_copy_us);
|
||||
let rows = rows_make_mut(
|
||||
&mut p.rows,
|
||||
&mut p.spare,
|
||||
wm,
|
||||
&mut p.cost_copies,
|
||||
&mut p.cost_copy_us,
|
||||
);
|
||||
rows[0] = [9.0; 4];
|
||||
assert_eq!(p.cost_copies, 1, "the shared write copied");
|
||||
assert_eq!(
|
||||
@@ -1218,7 +1237,13 @@ mod tests {
|
||||
// A fresh shared write after the shrink still carries the survivor whole.
|
||||
let _held3 = p.rows.clone();
|
||||
let wm = p.bone_watermark();
|
||||
let rows = rows_make_mut(&mut p.rows, wm, &mut p.cost_copies, &mut p.cost_copy_us);
|
||||
let rows = rows_make_mut(
|
||||
&mut p.rows,
|
||||
&mut p.spare,
|
||||
wm,
|
||||
&mut p.cost_copies,
|
||||
&mut p.cost_copy_us,
|
||||
);
|
||||
rows[1] = [7.0; 4];
|
||||
assert_eq!(
|
||||
p.rows[0], [9.0; 4],
|
||||
|
||||
Reference in New Issue
Block a user