the character sheet is the reference's own file
- the character sheet is the reference's own — `CharacterFrame.xml`, `PaperDollFrame.xml` and `PetPaperDollFrame.xml` off the player's chain in the reference's own order, 2800 lines of ours deleted — and the one engine verb it needed had no pixels: a bare `<PlayerModel name="CharacterModelFrame">` drew nothing here, so the swap as written would have replaced a working paper doll with an empty rectangle, invisible to every loader and gap check; a `<Model>`/`<PlayerModel>` widget now draws its window's booth, keyed on the reference's own frame names, and the booth mirrors take their yaw off `PlayerModel:SetRotation`'s own state, which is what the reference's `Model_Rotate*` write - what the stock file calls that was not there: `MicroButtonTooltipText(text, action)` (all five tab hovers), the two scripts `CharacterFrameTabButtonTemplate` carries — without them the five tabs are DEAD, since the reference's `PaperDollFrame` is `enableMouse` with a hit inset the tabs sit inside — `GetGuildInfo` on a recognised-but-unresolvable token answering `(nil, nil, 0)` rather than raising (`0x4c9330`), and `UnitAttackBothHands` pushing four values not two (`0x518810` calls the hand accessor twice; the reference's own Lua destructures two and says `FIXME` about the off hand, so no amount of running the sheet could expose it) — both skill verbs clamp a debuff deeper than the skill to zero as `0x5188a7` and `0x519298` do - a gate for the question that bites AFTER a swap: for every chain window, census the bare `name(` call sites across it and its `.lua`, subtract what those define, keep what the reference's own `_G` carries, and require every survivor non-nil in a VM with the whole interface up — `LootFrame.xml` had shipped load-clean and raised at line 85, and the stock character sheet would have raised on the first tab hover; the stock sheet is driven end to end over a level-60 body with a +105 buff, a −12 debuff and a resistance school whose debuff outweighs its buff, at the rendered strings Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -459,11 +459,18 @@
|
||||
PlaySound("igInventoryRotateCharacter")
|
||||
end
|
||||
|
||||
-- The character window's twin (CharacterFrame.xml's BenillaPaperDollModel_OnUpdate — the
|
||||
-- same ref Model_OnUpdate, UIParent.lua:1444-1462, that EVERY 1.12 model pane wires:
|
||||
-- PaperDollFrame l.222-224, DressUpFrame.xml:202-204, TabardFrame.xml:406-408). Held button
|
||||
-- The ref's own Model_OnUpdate (UIParent.lua:1444-1462), which EVERY 1.12 model pane wires:
|
||||
-- PaperDollFrame l.222-224, DressUpFrame.xml:202-204, TabardFrame.xml:406-408. Held button
|
||||
-- = half a turn per second; the 0.03 click above is the tap. The ref's held-vs-click sign
|
||||
-- flip is quoted, not corrected.
|
||||
--
|
||||
-- **This quartet retires with this window.** The character sheet's twin of it is gone: the
|
||||
-- migrated paper doll is a real `<PlayerModel>` driven by the reference's own
|
||||
-- `Model_OnLoad`/`Model_Rotate*`/`Model_OnUpdate` (UIParent.xml), because `<Model>` panes
|
||||
-- draw their booth now and carry their own facing (decision 1810). This pane is still a
|
||||
-- booth-texture Frame, so it still needs the `frame.facing` + `*_SetFacing` shape — until
|
||||
-- the inspect window is the reference's own, which it cannot be while 1.12 ships it as a
|
||||
-- LoadOnDemand addon packed as `.pub`.
|
||||
function BenillaInspectModel_OnUpdate(frame, elapsed)
|
||||
local step = elapsed * 2 * PI * ROTATIONS_PER_SECOND
|
||||
if getglobal(frame:GetName() .. "RotateLeftButton"):GetButtonState() == "PUSHED" then
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<!--
|
||||
The reference's item-SLOT button templates: `PaperDollItemSlotButtonTemplate` (PaperDollFrame.xml
|
||||
l.4-38) and `BagSlotButtonTemplate` (MainMenuBarBagButtons.xml l.17-53), which inherits it. Both
|
||||
sit on `ItemButtonTemplate` (ItemButtonTemplate.xml), so the chain is
|
||||
|
||||
ItemButtonTemplate <- PaperDollItemSlotButtonTemplate <- BagSlotButtonTemplate
|
||||
|
||||
and all three are now declared, in that order, across three files the toc loads in that order.
|
||||
|
||||
**Why they are here rather than in CharacterFrame.xml and BagFrame.xml, where their subjects
|
||||
live.** Load order, and only load order: BagFrame.xml (toc l.241) comes BEFORE
|
||||
CharacterFrame.xml (l.256), so a bag template declared in the former could not inherit a doll
|
||||
template declared in the latter. Neither is consumed by our own windows — our doll slots and our
|
||||
four bag-bar slots are hand-wired to `BenillaPaperDollSlot_*` / `BenillaBagBarSlot_*` and do not
|
||||
inherit these — so a file of their own, loaded after both, costs nothing and makes the chain
|
||||
readable. This is the reference's published surface for ADDONS, kept where its dependencies are
|
||||
satisfied.
|
||||
|
||||
**The handlers are the reference's own, not transcriptions.** `PaperDollItemSlotButton_OnLoad`,
|
||||
`_OnEvent`, `_OnClick`, `_Update`, `_UpdateLock`, `_OnEnter` and `_OnUpdate` come from
|
||||
`PaperDollFrame.lua`, which `ui_script::reference_ui` now SOURCES off the player's own patch
|
||||
chain. That file earned its place because every one of those acts on `this` and `this:GetID()`
|
||||
through live APIs and never on a named reference window — see the note beside SOURCED for what
|
||||
was checked first, including the 18 collisions that order settles in our favour.
|
||||
|
||||
`MainMenuBarBagButtons.lua` is deliberately NOT sourced, which is why `BagSlotButton_OnClick`,
|
||||
`_OnDrag`, `_OnShiftClick` and `_OnEnter` are ours in BagFrame.xml: all four of its versions walk
|
||||
`ContainerFrame1..NUM_CONTAINER_FRAMES`, windows this client does not build, and two of them
|
||||
call verbs (`PutItemInBag`, `PickupBagFromSlot`) it does not bind. Sourcing it would have handed
|
||||
every caller a body that raises inside the addon's own click wrapper.
|
||||
|
||||
**pfUI is the measured caller**, and it fits the reference's contract exactly rather than by
|
||||
luck: it names its bag-bar buttons `pfUIBagsBBag0Slot`.. with a NINE-character prefix, so the
|
||||
reference's own `strsub(name, 10)` in `PaperDollItemSlotButton_OnLoad` yields `Bag0Slot` and
|
||||
`GetInventorySlotInfo` answers slot 20. It then reads `$parentIconTexture`, `$parentNormalTexture`,
|
||||
`GetHighlightTexture()` and `GetPushedTexture()` off what it built, and CHAINS the template's
|
||||
OnEnter/OnLeave rather than replacing them (modules/bags.lua l.513-585).
|
||||
|
||||
OUT, named rather than stubbed (1134 §4): the bank arm. `BankItemButtonBagTemplate` and
|
||||
`BankItemButtonGenericTemplate` inherit `BankItemButtonTemplate`, whose handlers live in
|
||||
`BankFrame.lua` — not sourced — and whose OnEnter calls `this:GetInventorySlot()`, a widget
|
||||
method we do not bind. pfUI reaches those only for its bank window.
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<!-- Ref PaperDollFrame.xml l.4-38, structure and scripts verbatim. The `$parentCooldown` Model
|
||||
is not optional decoration: the sourced `PaperDollItemSlotButton_Update` resolves it by
|
||||
name (`getglobal(this:GetName().."Cooldown")`) and calls `:Hide()` / CooldownFrame_SetTimer
|
||||
on it every repaint, so a button without one raises on its first update. -->
|
||||
<Button name="PaperDollItemSlotButtonTemplate" inherits="ItemButtonTemplate" virtual="true">
|
||||
<Frames>
|
||||
<Model name="$parentCooldown" inherits="CooldownFrameTemplate"/>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>PaperDollItemSlotButton_OnLoad();</OnLoad>
|
||||
<OnEvent>PaperDollItemSlotButton_OnEvent(event);</OnEvent>
|
||||
<OnClick>PaperDollItemSlotButton_OnClick(arg1);</OnClick>
|
||||
<OnShow></OnShow>
|
||||
<OnDragStart>PaperDollItemSlotButton_OnClick("LeftButton", 1);</OnDragStart>
|
||||
<OnReceiveDrag>PaperDollItemSlotButton_OnClick("LeftButton", 1);</OnReceiveDrag>
|
||||
<OnEnter>PaperDollItemSlotButton_OnEnter();</OnEnter>
|
||||
<OnLeave>
|
||||
this.updateTooltip = nil;
|
||||
GameTooltip:Hide();
|
||||
ResetCursor();
|
||||
</OnLeave>
|
||||
<OnUpdate>PaperDollItemSlotButton_OnUpdate(arg1);</OnUpdate>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- `BagSlotButtonTemplate` was declared here, transcribed from MainMenuBarBagButtons.xml
|
||||
because that file could not be sourced. It IS sourced now — benilla.toc entry 48, one line
|
||||
below this file — so the reference's own declaration is the live one and the copy is gone
|
||||
(decision 1751, window 3). pfUI and anything else that inherits the template now inherits
|
||||
the reference's, including the `$parentItemAnim` Model our copy structurally could not
|
||||
carry. -->
|
||||
</Ui>
|
||||
@@ -136,6 +136,33 @@
|
||||
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
end
|
||||
|
||||
-- ref MicroButtonTooltipText (MainMenuBarMicroButtons.lua l.11-18), VERBATIM — the
|
||||
-- reference's own two-argument form: a label and a binding ACTION name, returning the
|
||||
-- label with the bound key in NORMAL_FONT_COLOR parentheses, or the bare label when the
|
||||
-- action is unbound.
|
||||
--
|
||||
-- It lives here because this file is our `MainMenuBarMicroButtons.xml`, which is where the
|
||||
-- reference declares it. It is not the micro bar's own helper: the stock `CharacterFrame`
|
||||
-- tab row calls it at all five `<OnEnter>` sites
|
||||
-- (`MicroButtonTooltipText(TEXT(CHARACTER_INFO), "TOGGLECHARACTER0")` and its four
|
||||
-- siblings), so migrating that window onto the chain (decision 1751) needs this name to
|
||||
-- answer or every character-tab hover raises. `SpellBookFrame.xml` already guards on it
|
||||
-- with `if MicroButtonTooltipText then`; that guard is now always true.
|
||||
--
|
||||
-- Ours below is the BUTTON-shaped variant, kept because our own micro buttons carry the
|
||||
-- action in `button.binding` and the label in `button.label`, and because it renders the
|
||||
-- key through the Key Bindings window's display law. The reference's version prints
|
||||
-- `GetBindingKey`'s raw token, which is what the real client shows, so the two are not
|
||||
-- folded together: this one is the reference's text, the other is ours.
|
||||
function MicroButtonTooltipText(text, action)
|
||||
if GetBindingKey(action) then
|
||||
return text .. " " .. NORMAL_FONT_COLOR_CODE .. "(" .. GetBindingKey(action) .. ")"
|
||||
.. FONT_COLOR_CODE_CLOSE
|
||||
else
|
||||
return text
|
||||
end
|
||||
end
|
||||
|
||||
-- ref MicroButtonTooltipText (l.11-18), the LIVE contract since 0997: `button.binding`
|
||||
-- is the binding ACTION name and the key resolves through GetBindingKey at hover time
|
||||
-- (rendered through the Key Bindings window's display law when it is loaded — the
|
||||
|
||||
@@ -1,773 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's PET paper doll (decision 1057) — the character window's **tab 2**, dressed from the
|
||||
real 1.12 `PetPaperDollFrame.xml`/`.lua` (line cites below are into that extraction:
|
||||
`ref-PetPaperDollFrame.xml`/`.lua`, plus `ref-PaperDollFrame.lua` for the shared setters and
|
||||
`ref-CharacterFrame.lua` for the tab dispatch).
|
||||
|
||||
**Its own top-level file, loaded straight after `CharacterFrame.xml`** — the SkillFrame.xml
|
||||
precedent. It carries the reference's `parent="CharacterFrame"`, and that attaches for real:
|
||||
the loader does read a cross-file `parent=`, resolving the name by global lookup at load
|
||||
(decision 1734), which is why the load order matters — an unresolvable name is not an error,
|
||||
it warns and falls back to the enclosing element. The page is still sized and pinned to
|
||||
`CharacterFrame`'s TOPLEFT by name, which gives the identical rect and is what the page was
|
||||
built and approved against, and `BENILLA_CHARACTERFRAME_SUBFRAMES` (which
|
||||
this page joins as entry 2) is what hides it when the whole window closes.
|
||||
|
||||
**Almost none of the page's Lua lives here, and that is the point.** The reference's own
|
||||
`PaperDollFrame_Set{Armor,Damage,AttackPower,AttackBothHands,Defense}` take `(unit, prefix)`
|
||||
and `PetPaperDollFrame_Update` (ref l.62-94) calls every one of them with `("pet", "Pet")` —
|
||||
the pet sheet is the SAME code as the character sheet pointed at another unit. Our
|
||||
transcription in `CharacterFrame.xml` already carries those parameters (and already carries
|
||||
the ref's own `if unit == "pet" then return end` early-outs in the two RANGED setters, ref
|
||||
l.470-473/l.541-544 — the pet page has no ranged rows, so the ref no-ops rather than
|
||||
nil-indexing). So this file adds only what is genuinely pet-only: the stats/resistances pair
|
||||
that reads the pet's own frames, the XP bar, loyalty, training points and the diet icon.
|
||||
`prefix` here is `"Pet"`, so `prefix.."ArmorFrame"` resolves `PetArmorFrame`,
|
||||
exactly as `"Character"` resolves the sibling page's.
|
||||
|
||||
**The tab is conditional, and `PetTab_Update` is the whole mechanism** (ref
|
||||
`PetPaperDollFrame.lua:189-198`): no pet UI ⇒ tab 2 hides and tab 3 re-anchors onto tab 2's
|
||||
LEFT, closing the gap; a pet ⇒ tab 2 shows and tab 3 goes back to the -16 chain. This used
|
||||
to be the ref's arithmetic on a DIFFERENT page (tab 3 was Skills here, Reputation there);
|
||||
`ReputationFrame.xml` took slot 3, so it is now the reference's function moving the
|
||||
reference's tab, with nothing left to reconcile.
|
||||
|
||||
Named deferrals / deviations (each re-cited at its own site below):
|
||||
- The model pane is a **booth-texture stand-in**, not a live `<PlayerModel>` widget — the
|
||||
0208 §5 decision the character page already ships under. The ref's
|
||||
`PetModelFrame:SetUnit("pet")` (ref l.67) therefore has no analogue: the app's
|
||||
`petdoll` booth resolves the pet itself (decision 1057), so the pane samples a bake
|
||||
that is already pointed at the right unit. `DISPLAY_SIZE_CHANGED`/`RefreshUnit` drop
|
||||
with the widget, exactly as they do on the character page.
|
||||
- The XP bar ships with **no numerals**: `TextStatusBar_Initialize`/
|
||||
`SetTextStatusBarTextPrefix`/`ShowTextStatusBarText` have no engine surface (the same
|
||||
gap `ActionBar.xml`'s own XP bar records — "No TextStatusBar text layer exists here at
|
||||
all"). The bar itself, its dwarf-notch art, its range and its colour are all faithful;
|
||||
only the "1234/5678" overlay is missing, and it lands for both bars at once when that
|
||||
machinery does.
|
||||
- `PET_UI_UPDATE` / `PET_UI_CLOSE` are **registered but nothing fires them yet** (nothing
|
||||
in the app sources either event). Registered anyway rather than dropped: the ref's own
|
||||
handler arms are re-expressed whole, and `UNIT_PET` + `PET_BAR_UPDATE` — both of which we
|
||||
DO fire — already cover the same "the pet went away while its page was open" case
|
||||
through the identical branch.
|
||||
- **CLOSED (decision 1062):** the level line's family word and the diet tooltip both
|
||||
landed. `UnitCreatureFamily("pet")` and `GetPetFoodTypes()` are real bindings now, fed
|
||||
from `CreatureFamily.dbc` (the word + its pet-food mask) and `ItemPetFood.dbc` (the
|
||||
eight diet names) through the pet's cached creature template; `BuildListString` is the
|
||||
ref's own `UIParent.lua:1051` joiner, re-expressed in `UIParent.xml`. Both call sites
|
||||
below dropped their existence guards. The ref's OWN `if ( UnitCreatureFamily("pet") )`
|
||||
guard stays — nil is still reachable (a template with no family; the creature query not
|
||||
yet answered) and a blank level line is the ref's answer to it.
|
||||
- `PetPaperDollFrame_SetResistances` reads the school ids from the shared
|
||||
`BENILLA_MAGICRES_IDS` table where the ref reads each frame's own `id=` attr
|
||||
(`frame:GetID()`, ref l.112). Same five ids in the same order (6/2/3/4/5); the table is
|
||||
what the sibling page already uses, and one convention across the two pages beats
|
||||
matching the ref's two.
|
||||
- The ref's `PetPaperDollCloseButton` (ref l.276-292) inherits `UIPanelButtonTemplate`,
|
||||
which this codebase has never needed to define; its art/size/font are transcribed inline
|
||||
from the shape every other panel button here uses (QuestFrame.xml's own precedent).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ── GlobalStrings this page uses (ref-GlobalStrings.lua, cited per line — the
|
||||
-- `X = X or "..."` idiom is this codebase's convention: the real localized file, loaded off
|
||||
-- the patch chain first, always wins; these fill in for the test harnesses, which load no
|
||||
-- chain. Redeclared here rather than leaned on from CharacterFrame.xml so a subset load of
|
||||
-- this file alone still resolves.) ────────────────────────────────────────────────────────
|
||||
ATTACK_COLON = ATTACK_COLON or "Attack:" -- :70
|
||||
DAMAGE_COLON = DAMAGE_COLON or "Damage:" -- :888
|
||||
ATTACK_POWER_COLON = ATTACK_POWER_COLON or "Power:" -- :71
|
||||
DEFENSE_COLON = DEFENSE_COLON or "Defense:" -- :917
|
||||
ARMOR_COLON = ARMOR_COLON or "Armor:" -- :63
|
||||
ATTACK_TOOLTIP = ATTACK_TOOLTIP or "Attack Rating" -- :75
|
||||
DEFENSE_TOOLTIP = DEFENSE_TOOLTIP or "Defense Rating" -- :918
|
||||
TRAINING_POINTS = TRAINING_POINTS or "Training Points:" -- :4305
|
||||
UNIT_LEVEL_TEMPLATE = UNIT_LEVEL_TEMPLATE or "Level %d" -- :4436
|
||||
PET_DIET_TEMPLATE = PET_DIET_TEMPLATE or "Diet: %s" -- :3037
|
||||
|
||||
NUM_PET_STATS = 5 -- (ref l.2)
|
||||
NUM_PET_RESISTANCE_TYPES = 5 -- NUM_PET_RESISTANCE_TYPES (ref l.1)
|
||||
|
||||
-- ── The page ────────────────────────────────────────────────────────────────────────────
|
||||
-- ref PetPaperDollFrame_OnLoad (l.4-29). The five labels live ONLY in the row template's
|
||||
-- $parentLabel and Lua fills them — never a re-declared region in an instance (the sibling
|
||||
-- page's OnLoad carries the why: this engine's template expansion APPENDS children, so a
|
||||
-- re-declared $parentLabel makes a second anchorless FontString instead of merging).
|
||||
-- `SetTextStatusBarTextPrefix(…, XP)` is dropped with the bar's numerals (header).
|
||||
function PetPaperDollFrame_OnLoad()
|
||||
local frame = this
|
||||
getglobal("PetAttackFrameLabel"):SetText(ATTACK_COLON)
|
||||
getglobal("PetDamageFrameLabel"):SetText(DAMAGE_COLON)
|
||||
getglobal("PetAttackPowerFrameLabel"):SetText(ATTACK_POWER_COLON)
|
||||
getglobal("PetDefenseFrameLabel"):SetText(DEFENSE_COLON)
|
||||
getglobal("PetArmorFrameLabel"):SetText(ARMOR_COLON)
|
||||
frame:RegisterEvent("PET_UI_UPDATE")
|
||||
frame:RegisterEvent("PET_BAR_UPDATE")
|
||||
frame:RegisterEvent("PET_UI_CLOSE")
|
||||
frame:RegisterEvent("UNIT_PET")
|
||||
frame:RegisterEvent("UNIT_PET_EXPERIENCE")
|
||||
frame:RegisterEvent("UNIT_MODEL_CHANGED")
|
||||
frame:RegisterEvent("UNIT_LEVEL")
|
||||
frame:RegisterEvent("UNIT_RESISTANCES")
|
||||
frame:RegisterEvent("UNIT_STATS")
|
||||
frame:RegisterEvent("UNIT_DAMAGE")
|
||||
frame:RegisterEvent("UNIT_RANGEDDAMAGE")
|
||||
frame:RegisterEvent("UNIT_ATTACK_SPEED")
|
||||
frame:RegisterEvent("UNIT_ATTACK_POWER")
|
||||
frame:RegisterEvent("UNIT_RANGED_ATTACK_POWER")
|
||||
frame:RegisterEvent("UNIT_DEFENSE")
|
||||
frame:RegisterEvent("UNIT_ATTACK")
|
||||
frame:RegisterEvent("UNIT_PET_TRAINING_POINTS")
|
||||
PetTab_Update()
|
||||
end
|
||||
|
||||
-- ref PetPaperDollFrame_OnEvent (l.31-48), all four arms. NOTE the shape: unlike the
|
||||
-- character page, this one does NOT gate on visibility — the first arm has to run while the
|
||||
-- page is HIDDEN, because that is what keeps the tab itself in step with whether a pet
|
||||
-- exists. Only the last arm (a plain "the pet's numbers moved") is unit-token gated.
|
||||
function PetPaperDollFrame_OnEvent()
|
||||
local frame = this
|
||||
if event == "PET_UI_UPDATE" or event == "PET_BAR_UPDATE"
|
||||
or (event == "UNIT_PET" and arg1 == "player") then
|
||||
-- The pet left while its own page was open: close the window (ToggleCharacter on
|
||||
-- the visible page is the ref's own way of saying HideUIPanel), then re-fit the tab.
|
||||
if frame:IsVisible() and not HasPetUI() then
|
||||
ToggleCharacter("PetPaperDollFrame")
|
||||
end
|
||||
PetTab_Update()
|
||||
PetPaperDollFrame_Update()
|
||||
elseif event == "PET_UI_CLOSE" then
|
||||
if frame:IsVisible() then
|
||||
ToggleCharacter("PetPaperDollFrame")
|
||||
end
|
||||
PetTab_Update()
|
||||
elseif event == "UNIT_PET_EXPERIENCE" then
|
||||
PetExpBar_Update()
|
||||
elseif arg1 == "pet" then
|
||||
PetPaperDollFrame_Update()
|
||||
end
|
||||
end
|
||||
|
||||
-- ref PetPaperDollFrame_OnShow/_OnHide (l.50-60): the window's ONE name line is shared —
|
||||
-- the pet page borrows it and puts the player's back on the way out.
|
||||
function PetPaperDollFrame_OnShow()
|
||||
getglobal("CharacterNameText"):Hide()
|
||||
local petName = getglobal("PetNameText")
|
||||
petName:Show()
|
||||
petName:SetText(UnitName("pet"))
|
||||
PetPaperDollFrame_Update()
|
||||
end
|
||||
|
||||
function PetPaperDollFrame_OnHide()
|
||||
getglobal("CharacterNameText"):Show()
|
||||
getglobal("PetNameText"):Hide()
|
||||
end
|
||||
|
||||
-- ref PetPaperDollFrame_Update (l.62-94). The `hasPetUI` early-out is load-bearing, not
|
||||
-- defensive: every setter below reads the "pet" token, and with no pet those all answer the
|
||||
-- absent shape — repainting from it would blank a page the ref leaves showing its last state
|
||||
-- until the tab itself goes.
|
||||
function PetPaperDollFrame_Update()
|
||||
local hasPetUI, canGainXP = HasPetUI()
|
||||
if not hasPetUI then return end
|
||||
-- ref l.67 `PetModelFrame:SetUnit("pet")` — no analogue: the app's `petdoll` booth
|
||||
-- resolves the pet itself (header).
|
||||
--
|
||||
-- The ref's own `if ( UnitCreatureFamily("pet") ) then` guard, kept (decision 1062):
|
||||
-- nil is genuinely reachable — no family on the template, or the creature query that
|
||||
-- carries the family id has not answered yet — and the reference's answer is a BLANK
|
||||
-- level line rather than a bare "Level 58". Hoisted to a local because the ref calls
|
||||
-- the binding twice for one answer.
|
||||
local family = UnitCreatureFamily("pet")
|
||||
if family then
|
||||
getglobal("PetLevelText"):SetText(
|
||||
format(UNIT_LEVEL_TEMPLATE, UnitLevel("pet")) .. " " .. family)
|
||||
end
|
||||
getglobal("PetLoyaltyText"):SetText(GetPetLoyalty())
|
||||
PetExpBar_Update()
|
||||
|
||||
-- The two panes this page has to draw itself, for two DIFFERENT reasons (both below):
|
||||
-- the resistance column genuinely differs from the character page's, while the stat
|
||||
-- block is the same logic that only duplicates because PetStatFrame/CharacterStatFrame
|
||||
-- share no prefix for a `prefix..` renderer to take.
|
||||
PetPaperDollFrame_SetResistances()
|
||||
PetPaperDollFrame_SetStats()
|
||||
|
||||
-- ...and then the shared rows, which are not this page's code at all: every one of
|
||||
-- these is CharacterFrame.xml's own renderer, handed ("pet", "Pet") where the character
|
||||
-- page hands it ("player", "Character"), so `prefix.."ArmorFrame"` resolves PetArmorFrame
|
||||
-- here and CharacterArmorFrame there. That substitution IS the page (header comment).
|
||||
-- Called top-to-bottom in the order the rows are anchored on screen — each renderer only
|
||||
-- writes its own row's FontStrings from live API state, so nothing here reads anything
|
||||
-- another one wrote and the order is free.
|
||||
PaperDollFrame_SetAttackBothHands("pet", "Pet")
|
||||
PaperDollFrame_SetAttackPower("pet", "Pet")
|
||||
PaperDollFrame_SetDamage("pet", "Pet")
|
||||
PaperDollFrame_SetDefense("pet", "Pet")
|
||||
PaperDollFrame_SetArmor("pet", "Pet")
|
||||
-- The pet sheet has no ranged rows at all, and these two say so themselves — both open
|
||||
-- with the reference's own `if unit == "pet" then return end` (CharacterFrame.xml). They
|
||||
-- are called anyway so that both pages drive the identical set of renderers and neither
|
||||
-- can quietly fall behind the other.
|
||||
PaperDollFrame_SetRangedDamage("pet", "Pet")
|
||||
PaperDollFrame_SetRangedAttackPower("pet", "Pet")
|
||||
|
||||
-- The hunter-only block (ref l.83-93): the diet icon and the training-point line are
|
||||
-- the two things a warlock's minion never has. `canGainXP` is HasPetUI's second return.
|
||||
if canGainXP then
|
||||
getglobal("PetPaperDollPetInfo"):Show()
|
||||
local totalPoints, spent = GetPetTrainingPoints()
|
||||
local text = getglobal("PetTrainingPointText")
|
||||
text:SetText(totalPoints - spent)
|
||||
text:Show()
|
||||
getglobal("PetTrainingPointLabel"):Show()
|
||||
else
|
||||
getglobal("PetPaperDollPetInfo"):Hide()
|
||||
getglobal("PetTrainingPointText"):Hide()
|
||||
getglobal("PetTrainingPointLabel"):Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- ref PetPaperDollFrame_SetResistances (l.96-137). Deliberately NOT the character page's
|
||||
-- version: the pet's has no resistance-quality subtext (no `RESISTANCE_TOOLTIP_SUBTEXT`
|
||||
-- line at all) and colours the NUMBER off its own sign rather than off pos-vs-neg
|
||||
-- magnitude — two real differences in the reference, kept rather than unified.
|
||||
function PetPaperDollFrame_SetResistances()
|
||||
for i = 1, NUM_PET_RESISTANCE_TYPES do
|
||||
local resistId = BENILLA_MAGICRES_IDS[i]
|
||||
local text = getglobal("PetMagicResText" .. i)
|
||||
local frame = getglobal("PetMagicResFrame" .. i)
|
||||
local base, resistance, positive, negative = UnitResistance("pet", resistId)
|
||||
frame.tooltip = getglobal("RESISTANCE" .. resistId .. "_NAME")
|
||||
-- Resistances can be negative: red below zero, green above, plain white at zero.
|
||||
if resistance < 0 then
|
||||
text:SetText(RED_FONT_COLOR_CODE .. resistance .. FONT_COLOR_CODE_CLOSE)
|
||||
elseif resistance == 0 then
|
||||
text:SetText(resistance)
|
||||
else
|
||||
text:SetText(GREEN_FONT_COLOR_CODE .. resistance .. FONT_COLOR_CODE_CLOSE)
|
||||
end
|
||||
if positive ~= 0 or negative ~= 0 then
|
||||
frame.tooltip = frame.tooltip .. " ( " .. HIGHLIGHT_FONT_COLOR_CODE .. base
|
||||
if positive > 0 then
|
||||
frame.tooltip = frame.tooltip .. GREEN_FONT_COLOR_CODE .. " +" .. positive
|
||||
end
|
||||
if negative < 0 then
|
||||
frame.tooltip = frame.tooltip .. " " .. RED_FONT_COLOR_CODE .. negative
|
||||
end
|
||||
frame.tooltip = frame.tooltip .. FONT_COLOR_CODE_CLOSE .. " )"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ref PetPaperDollFrame_SetStats (l.139-181) — byte-for-byte the character page's
|
||||
-- _SetStats with the pet's own frame names and the "pet" token (the ref carries the same
|
||||
-- duplication; unifying them would mean inventing a prefix the reference's own
|
||||
-- `PetStatFrame`/`CharacterStatFrame` naming doesn't share).
|
||||
function PetPaperDollFrame_SetStats()
|
||||
for i = 1, NUM_PET_STATS do
|
||||
local label = getglobal("PetStatFrame" .. i .. "Label")
|
||||
local text = getglobal("PetStatFrame" .. i .. "StatText")
|
||||
local frame = getglobal("PetStatFrame" .. i)
|
||||
local statName = getglobal("SPELL_STAT" .. (i - 1) .. "_NAME")
|
||||
label:SetText(statName .. ":")
|
||||
local stat, effectiveStat, posBuff, negBuff = UnitStat("pet", i)
|
||||
local tooltipText = HIGHLIGHT_FONT_COLOR_CODE .. statName .. " "
|
||||
if posBuff == 0 and negBuff == 0 then
|
||||
text:SetText(effectiveStat)
|
||||
frame.tooltip = tooltipText .. effectiveStat .. FONT_COLOR_CODE_CLOSE
|
||||
else
|
||||
tooltipText = tooltipText .. effectiveStat
|
||||
if posBuff > 0 or negBuff < 0 then
|
||||
tooltipText = tooltipText .. " (" .. (stat - posBuff - negBuff) .. FONT_COLOR_CODE_CLOSE
|
||||
end
|
||||
if posBuff > 0 then
|
||||
tooltipText = tooltipText .. FONT_COLOR_CODE_CLOSE .. GREEN_FONT_COLOR_CODE .. "+" .. posBuff .. FONT_COLOR_CODE_CLOSE
|
||||
end
|
||||
if negBuff < 0 then
|
||||
tooltipText = tooltipText .. RED_FONT_COLOR_CODE .. " " .. negBuff .. FONT_COLOR_CODE_CLOSE
|
||||
end
|
||||
if posBuff > 0 or negBuff < 0 then
|
||||
tooltipText = tooltipText .. HIGHLIGHT_FONT_COLOR_CODE .. ")" .. FONT_COLOR_CODE_CLOSE
|
||||
end
|
||||
frame.tooltip = tooltipText
|
||||
if negBuff < 0 then
|
||||
text:SetText(RED_FONT_COLOR_CODE .. effectiveStat .. FONT_COLOR_CODE_CLOSE)
|
||||
else
|
||||
text:SetText(GREEN_FONT_COLOR_CODE .. effectiveStat .. FONT_COLOR_CODE_CLOSE)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ref PetExpBar_Update (l.183-187). `min(0, currXP)` is the ref's own; XP can't be negative,
|
||||
-- so it is just 0 — quoted, not simplified.
|
||||
function PetExpBar_Update()
|
||||
local currXP, nextXP = GetPetExperience()
|
||||
local bar = getglobal("PetPaperDollFrameExpBar")
|
||||
bar:SetMinMaxValues(math.min(0, currXP), nextXP)
|
||||
bar:SetValue(currXP)
|
||||
end
|
||||
|
||||
-- ref PetTab_Update (l.189-198) — the conditional tab, whole, and byte-for-byte the
|
||||
-- reference's since the Reputation page took tab 3 (this file's header comment). The
|
||||
-- re-anchor is what keeps the row gapless: with tab 2 hidden, tab 3 sits at tab 2's own LEFT
|
||||
-- (i.e. exactly where tab 2 would have started) instead of -16 past its RIGHT. Only tab 3
|
||||
-- is touched — tab 4 chains off tab 3, so the whole tail follows it.
|
||||
function PetTab_Update()
|
||||
local tab2 = getglobal("CharacterFrameTab2")
|
||||
local tab3 = getglobal("CharacterFrameTab3")
|
||||
-- Nil-guarded like CharacterFrame_ShowSubFrame's own loop: a test subset may load this
|
||||
-- file without the window's tab row.
|
||||
if not tab2 or not tab3 then return end
|
||||
if not HasPetUI() then
|
||||
tab2:Hide()
|
||||
tab3:SetPoint("LEFT", "CharacterFrameTab2", "LEFT", 0, 0)
|
||||
else
|
||||
tab2:Show()
|
||||
tab3:SetPoint("LEFT", "CharacterFrameTab2", "RIGHT", -16, 0)
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<Frame name="PetPaperDollFrame" enableMouse="true" parent="CharacterFrame" hidden="true" id="2">
|
||||
<!-- The real 384×512 page (ref l.4, `setAllPoints="true"`). The explicit `<Size>` + TOPLEFT
|
||||
`<Anchor>` is an exact equivalent of that here — CharacterFrame, the parent this frame
|
||||
declares, is itself 384x512 (ref CharacterFrame.xml l.4-7) — and it is kept rather than
|
||||
swapped because it is the geometry the page ships with, the SkillFrame.xml precedent. -->
|
||||
<Size><AbsDimension x="384" y="512"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="CharacterFrame" relativePoint="TOPLEFT"/></Anchors>
|
||||
<HitRectInsets><AbsInset left="0" right="30" top="0" bottom="75"/></HitRectInsets>
|
||||
<Layers>
|
||||
<!-- The quadrant background (ref l.9-57): the "General" top halves every
|
||||
PaperDollInfoFrame page shares (they carry the portrait-ring cutout), plus this
|
||||
page's own bottom pair. -->
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-General-TopLeft">
|
||||
<Size><AbsDimension x="256" y="256"/></Size>
|
||||
<Anchors><Anchor point="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"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\PetPaperDollFrame\UI-PetPaperDollFrame-BotLeft">
|
||||
<Size><AbsDimension x="256" y="256"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-257"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<Texture file="Interface\PetPaperDollFrame\UI-PetPaperDollFrame-BotRight">
|
||||
<Size><AbsDimension x="128" y="256"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="258" y="-257"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<!-- The pet's name (ref l.58-69), sitting exactly where the window's own player
|
||||
name line does — the two swap on this page's Show/Hide. -->
|
||||
<FontString name="PetNameText" inherits="GameFontHighlight">
|
||||
<Size><AbsDimension x="109" y="16"/></Size>
|
||||
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="6" y="232"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<!-- "Level %d <Family>" (ref l.70-78) and the loyalty level under it (l.79-87). -->
|
||||
<FontString name="PetLevelText" inherits="GameFontNormalSmall">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="PetNameText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="PetLoyaltyText" inherits="GameFontNormalSmall">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="PetLevelText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<!-- Unspent training points (ref l.88-105) — shown for hunter pets only. -->
|
||||
<FontString name="PetTrainingPointText" inherits="GameFontHighlightSmall">
|
||||
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-135" y="86"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<FontString name="PetTrainingPointLabel" inherits="GameFontNormalSmall" text="TRAINING_POINTS">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="PetTrainingPointText" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The pet XP bar (ref l.109-176): 319×11 at BOTTOMLEFT (23,105), the two 160/159-wide
|
||||
dwarf notch strips over it, the shared purple status-bar fill. The numerals are the
|
||||
header's named deferral; everything else here is the ref's. -->
|
||||
<!-- No `enableMouse`: the ref's bar takes the mouse only to show its numerals on hover
|
||||
(l.166-172), and those are the deferral above — a mouse-enabled bar with nothing to
|
||||
show would just eat clicks. It comes back with the numerals. -->
|
||||
<StatusBar name="PetPaperDollFrameExpBar">
|
||||
<Size><AbsDimension x="319" y="11"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="23" y="105"/></Offset></Anchor></Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="PetPaperDollXPBar1" file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
|
||||
<Size><AbsDimension x="160" y="13"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<TexCoords left="0.203125" right="0.8046875" top="0.2890625" bottom="0.33984375"/>
|
||||
</Texture>
|
||||
<Texture file="Interface\MainMenuBar\UI-MainMenuBar-Dwarf">
|
||||
<Size><AbsDimension x="159" y="13"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="PetPaperDollXPBar1" relativePoint="RIGHT"/></Anchors>
|
||||
<TexCoords left="0.203125" right="0.8046875" top="0.2890625" bottom="0.33984375"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.58" g="0.0" b="0.55"/>
|
||||
<Scripts>
|
||||
<!-- The ref's OnEvent is the TextStatusBar CVar hook plus PetExpBar_Update; only
|
||||
the latter has anything to do here, and the page's own UNIT_PET_EXPERIENCE
|
||||
arm already drives it, so no per-bar registration ships. -->
|
||||
<OnLoad>PetExpBar_Update()</OnLoad>
|
||||
</Scripts>
|
||||
</StatusBar>
|
||||
|
||||
<!-- The model pane (ref PlayerModel PetModelFrame, l.177-244): 318×224 at TOPLEFT
|
||||
(25,-78) — wider than the character page's 233 because there is no equipment column
|
||||
beside it. A plain Frame sampling the `petdoll` booth bake (header). -->
|
||||
<Frame name="PetModelFrame">
|
||||
<Size><AbsDimension x="318" y="224"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="25" y="-78"/></Offset></Anchor></Anchors>
|
||||
<Layers>
|
||||
<!-- BACKGROUND, not ARTWORK — see decision 1070. The booth quad stands in for a
|
||||
<PlayerModel> WIDGET, whose 3D content the reference renders outside the
|
||||
layer batches entirely and with a transparent background; ours is one
|
||||
OPAQUE texture inside them. The draw layer is BUCKET-WIDE and outranks the
|
||||
frame (0884), so at ARTWORK this quad painted over every same-level sibling
|
||||
region below OVERLAY — which is exactly how the diet icon two frames down
|
||||
(BACKGROUND, the reference's own layer for it) became a mouse-enabled hole
|
||||
with a working tooltip and nothing drawn (director, 2026-08-06). Sitting at
|
||||
BACKGROUND, the pane's single region ties with such siblings and the link
|
||||
stamp decides, restoring the reference's declaration order. A pane can only
|
||||
ever stop covering things this way, never start. -->
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentTexture">
|
||||
<Size><AbsDimension x="318" y="224"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- Rotate buttons (ref l.201-242): 35×35. NOTE the ref anchors the left one to
|
||||
the PAGE, not to the pane (l.206) — (23,-76), two pixels up and left of the
|
||||
pane's own corner — and the right one chains off its TOPRIGHT. -->
|
||||
<Button name="PetModelFrameRotateLeftButton">
|
||||
<Size><AbsDimension x="35" y="35"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetPaperDollFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="23" y="-76"/></Offset></Anchor></Anchors>
|
||||
<NormalTexture file="Interface\Buttons\UI-RotationLeft-Button-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-RotationLeft-Button-Down"/>
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
|
||||
<OnClick>BenillaPaperDollModel_RotateLeft(getglobal("PetModelFrame"))</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="PetModelFrameRotateRightButton">
|
||||
<Size><AbsDimension x="35" y="35"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetModelFrameRotateLeftButton" relativePoint="TOPRIGHT"/></Anchors>
|
||||
<NormalTexture file="Interface\Buttons\UI-RotationRight-Button-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-RotationRight-Button-Down"/>
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnLoad>self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
|
||||
<OnClick>BenillaPaperDollModel_RotateRight(getglobal("PetModelFrame"))</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<!-- The four Model_* helpers are the sibling page's, shared: they drive
|
||||
`frame.setFacing`, which each pane sets to its own booth's yaw binding
|
||||
(decision 1057 — that generalization is what let this page reuse them
|
||||
instead of growing a third copy of the held-spin loop). No OnMouseUp/
|
||||
OnReceiveDrag here: the ref's pet pane is not a drop target (a pet wears
|
||||
nothing), so it stays mouse-inert. -->
|
||||
<OnLoad>
|
||||
self.setFacing = BenillaPetPaperDollModel_SetFacing
|
||||
BenillaPaperDollModel_OnLoad(self)
|
||||
BenillaSetBoothTexture(getglobal(self:GetName() .. "Texture"), "petdoll")
|
||||
</OnLoad>
|
||||
<OnUpdate>BenillaPaperDollModel_OnUpdate(self, arg1)</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- The diet icon (ref PetPaperDollPetInfo, l.245-275): 24×23 under the rotate buttons.
|
||||
Its texcoords are STATIC in the reference — this icon is the pet's DIET affordance
|
||||
(its tooltip lists what it eats), not a happiness readout; the happiness face that
|
||||
does move lives on the pet unit frame (decision 1005/0990). Shown for hunter pets
|
||||
only. -->
|
||||
<Frame name="PetPaperDollPetInfo" enableMouse="true">
|
||||
<Size><AbsDimension x="24" y="23"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="PetModelFrameRotateLeftButton" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PetPaperDollFrame\UI-PetHappiness">
|
||||
<Size><AbsDimension x="24" y="23"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<TexCoords left="0" right="0.1875" top="0" bottom="0.359375"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>
|
||||
-- ref l.267-270, unguarded now that both halves exist (decision 1062):
|
||||
-- `GetPetFoodTypes` is a real binding and `BuildListString` is the ref's
|
||||
-- own UIParent.lua joiner, re-expressed in UIParent.xml.
|
||||
--
|
||||
-- `BuildListString()` returns NIL on an empty list and `format`'s "%s"
|
||||
-- would error on it, so the pairing matters. Two things keep it safe, and
|
||||
-- both are the reference's: this frame is shown for HUNTER pets only (the
|
||||
-- `canGainXP` block above), and in the shipped `CreatureFamily.dbc` every
|
||||
-- family with a ZERO food mask is a warlock minion or "Remote Control" —
|
||||
-- every hunter-tameable family has a real diet. `GetPetFoodTypes` is
|
||||
-- itself hunter-gated in the binary too (`0x6116e0`), so all three agree.
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(format(PET_DIET_TEMPLATE, BuildListString(GetPetFoodTypes())))
|
||||
GameTooltip:Show()
|
||||
</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- The page's own Close button (ref PetPaperDollCloseButton, l.276-292) — the panel
|
||||
button art/size/font inline, since no UIPanelButtonTemplate ships (header). -->
|
||||
<Button name="PetPaperDollCloseButton" 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"/>
|
||||
<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>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
|
||||
<Scripts>
|
||||
<!-- ref l.289: `HideUIPanel(this:GetParent():GetParent())` — the window, two up
|
||||
from the button through the page. Named directly here: this engine has no
|
||||
cross-file parenting, so the page is not really the window's child (header).
|
||||
CLOSE is the ref's own literal `text="CLOSE"` attribute — the GlobalStrings
|
||||
key, resolved by the loader's text lookup like every other one here. -->
|
||||
<OnClick>HideUIPanel(getglobal("CharacterFrame"))</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- The attributes block (ref PetAttributesFrame, l.293-516): 230×78 at TOPLEFT
|
||||
(67,-300), two 114×78 stat-background plates side by side. The pet's plate is the
|
||||
SIMPLE two-column one — the character page's is three stacked 3-piece columns
|
||||
because it also carries a ranged block, which a pet has none of. -->
|
||||
<Frame name="PetAttributesFrame">
|
||||
<Size><AbsDimension x="230" y="78"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="67" y="-300"/></Offset></Anchor></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-StatBackground">
|
||||
<Size><AbsDimension x="114" y="78"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<TexCoords left="0" right="0.89" top="0" bottom="0.61"/>
|
||||
</Texture>
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-StatBackground">
|
||||
<Size><AbsDimension x="114" y="78"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="115" y="0"/></Offset></Anchor></Anchors>
|
||||
<TexCoords left="0" right="0.89" top="0" bottom="0.61"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The 5 primaries (ref l.335-419): TOPLEFT (6,-5), chained BOTTOMLEFT with a
|
||||
1px gap. Labels are the template's $parentLabel, filled by _SetStats. -->
|
||||
<Frame name="PetStatFrame1" inherits="BenillaStatRowTemplate" id="1">
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-5"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>PaperDollStatTooltip("pet", "STRENGTH")</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetStatFrame2" inherits="BenillaStatRowTemplate" id="2">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>PaperDollStatTooltip("pet", "AGILITY")</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetStatFrame3" inherits="BenillaStatRowTemplate" id="3">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>PaperDollStatTooltip("pet", "STAMINA")</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetStatFrame4" inherits="BenillaStatRowTemplate" id="4">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>PaperDollStatTooltip("pet", "INTELLECT")</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetStatFrame5" inherits="BenillaStatRowTemplate" id="5">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetStatFrame4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>PaperDollStatTooltip("pet", "SPIRIT")</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- The right-hand column (ref l.420-514): attack rating, attack power, damage,
|
||||
defense, armor — the second plate's own chain, rooted at (122,-5) of the
|
||||
attributes frame. -->
|
||||
<Frame name="PetAttackFrame" inherits="BenillaStatRowTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetAttributesFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="122" y="-5"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<!-- ref l.429-433: the pet's attack row shows the BARE tooltip title, with
|
||||
no ATTACK_TOOLTIP_SUBTEXT line — the character page's own row adds it.
|
||||
Kept as the ref has it. -->
|
||||
<OnEnter>
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(ATTACK_TOOLTIP, 1.0, 1.0, 1.0)
|
||||
GameTooltip:Show()
|
||||
</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetAttackPowerFrame" inherits="BenillaStatRowTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetAttackFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(self.tooltip)
|
||||
GameTooltip:AddLine(self.tooltipSubtext, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
|
||||
GameTooltip:Show()
|
||||
</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetDamageFrame" inherits="BenillaStatRowTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetAttackPowerFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<!-- ref l.468-470: the pet's damage row reuses the CHARACTER damage
|
||||
hover verbatim — it reads `self.damage`/`self.dps`, which
|
||||
_SetDamage filled with the pet's numbers. -->
|
||||
<OnEnter>CharacterDamageFrame_OnEnter()</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetDefenseFrame" inherits="BenillaStatRowTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetDamageFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(DEFENSE_TOOLTIP, 1.0, 1.0, 1.0)
|
||||
GameTooltip:Show()
|
||||
</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Frame name="PetArmorFrame" inherits="BenillaStatRowTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="PetDefenseFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<OnEnter>
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(self.tooltip)
|
||||
GameTooltip:AddLine(self.tooltipSubtext, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
|
||||
GameTooltip:Show()
|
||||
</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- The resistance column (ref PetResistanceFrame, l.517-635): 32×160 whose RIGHT edge
|
||||
sits 347px in from the page's left — 50px further right than the character page's
|
||||
297, because the pet page's wider model pane pushes it out. The five icons carry
|
||||
their own texcoords into the shared ResistanceIcons sheet, in the ref's own school
|
||||
order (arcane/fire/nature/frost/shadow = BENILLA_MAGICRES_IDS). -->
|
||||
<Frame name="PetResistanceFrame">
|
||||
<Size><AbsDimension x="32" y="160"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="PetPaperDollFrame" relativePoint="TOPLEFT"><Offset><AbsDimension x="347" y="-77"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame name="PetMagicResFrame1" inherits="BenillaMagicResistanceFrameTemplate" id="6">
|
||||
<Anchors><Anchor point="TOP" relativeTo="PetResistanceFrame" relativePoint="TOP"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
|
||||
<TexCoords left="0" right="1.0" top="0.2265625" bottom="0.33984375"/>
|
||||
</Texture>
|
||||
<FontString name="PetMagicResText1" inherits="GameFontHighlightSmall" text="X">
|
||||
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Frame name="PetMagicResFrame2" inherits="BenillaMagicResistanceFrameTemplate" id="2">
|
||||
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame1" relativePoint="BOTTOM"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
|
||||
<TexCoords left="0" right="1.0" top="0" bottom="0.11328125"/>
|
||||
</Texture>
|
||||
<FontString name="PetMagicResText2" inherits="GameFontHighlightSmall" text="X">
|
||||
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Frame name="PetMagicResFrame3" inherits="BenillaMagicResistanceFrameTemplate" id="3">
|
||||
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame2" relativePoint="BOTTOM"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
|
||||
<TexCoords left="0" right="1.0" top="0.11328125" bottom="0.2265625"/>
|
||||
</Texture>
|
||||
<FontString name="PetMagicResText3" inherits="GameFontHighlightSmall" text="X">
|
||||
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Frame name="PetMagicResFrame4" inherits="BenillaMagicResistanceFrameTemplate" id="4">
|
||||
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame3" relativePoint="BOTTOM"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
|
||||
<TexCoords left="0" right="1.0" top="0.33984375" bottom="0.453125"/>
|
||||
</Texture>
|
||||
<FontString name="PetMagicResText4" inherits="GameFontHighlightSmall" text="X">
|
||||
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Frame name="PetMagicResFrame5" inherits="BenillaMagicResistanceFrameTemplate" id="5">
|
||||
<Anchors><Anchor point="TOP" relativeTo="PetMagicResFrame4" relativePoint="BOTTOM"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-ResistanceIcons">
|
||||
<TexCoords left="0" right="1.0" top="0.453125" bottom="0.56640625"/>
|
||||
</Texture>
|
||||
<FontString name="PetMagicResText5" inherits="GameFontHighlightSmall" text="X">
|
||||
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>PetPaperDollFrame_OnLoad()</OnLoad>
|
||||
<OnEvent>PetPaperDollFrame_OnEvent()</OnEvent>
|
||||
<OnShow>PetPaperDollFrame_OnShow()</OnShow>
|
||||
<OnHide>PetPaperDollFrame_OnHide()</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -23,20 +23,22 @@
|
||||
|
||||
Named deferrals / deviations, each re-cited at its own site below:
|
||||
|
||||
- **The model pane is a booth-texture stand-in, not a live `<PlayerModel>`** — the same
|
||||
0208 §5 decision the character and pet paper dolls already ship under. The reference
|
||||
declares `<PlayerModel name="PetStableModel">` (xml l.182-280); ours is a plain
|
||||
`<Frame>` whose one texture samples an app-side bake, so the widget's own script trio
|
||||
goes with the widget: `Model_OnLoad`/`Model_OnUpdate` (xml l.194-203) and the
|
||||
`DISPLAY_SIZE_CHANGED` → `this:RefreshUnit()` pair (xml l.196-200) have no analogue
|
||||
here, exactly as `PetPaperDollFrame.xml` records for its own pane. `Model_RotateLeft`/
|
||||
`Model_RotateRight` (xml l.222/241) become `BenillaPaperDollModel_RotateLeft`/`_Right`,
|
||||
the shared helpers `CharacterFrame.xml` defines.
|
||||
- The pane's booth is the `"stable"` slot in `crates/benilla-app/src/portrait`, and its yaw
|
||||
binding is `BenillaStableModel_SetFacing`, the fourth beside the paperdoll/petdoll/
|
||||
inspect three. The app resolves `GetSelectedStablePet()`'s slot into either the live
|
||||
pet's entity or — for a stabled or dismissed pet, which has no world object at all —
|
||||
the creature query's `CreatureDisplayInfo` id, and the booth mirrors whichever it got.
|
||||
- **The model pane is the reference's own `<PlayerModel name="PetStableModel">`**
|
||||
(xml l.182-280), and so are the four scripts on it: `Model_OnLoad`, `Model_OnUpdate`
|
||||
and the two `Model_Rotate*` the buttons call (UIParent.xml). It was a plain `<Frame>`
|
||||
sampling a booth bake through `BenillaSetBoothTexture`, with the rotate quartet borrowed
|
||||
from our CharacterFrame.xml — both went when the character window migrated (decision
|
||||
1751): the widget draws its own booth now, and the yaw the app mirrors is the pane's
|
||||
own `SetRotation` state.
|
||||
One thing the reference's pane does that ours still does not: `DISPLAY_SIZE_CHANGED` →
|
||||
`this:RefreshUnit()` (xml l.196-200) is a no-op here, because the pane holds the unit
|
||||
TOKEN and the app re-resolves it every frame — there is no cached appearance to
|
||||
invalidate (`script/modelframe.rs`'s `RefreshUnit`).
|
||||
- The pane's booth is the `"stable"` slot in `crates/benilla-app/src/portrait`, joined to
|
||||
the pane by name in that module's `MODEL_PANE_BOOTHS`. The app resolves
|
||||
`GetSelectedStablePet()`'s slot into either the live pet's entity or — for a stabled or
|
||||
dismissed pet, which has no world object at all — the creature query's
|
||||
`CreatureDisplayInfo` id, and the booth mirrors whichever it got.
|
||||
- **`GetPetIcon()` does not exist in this client** (reference/1.12-globals.tsv has it as
|
||||
an engine global; benilla has never needed it). `PetStable_Update` calls it at lua l.51
|
||||
and l.161-162 and it is transcribed verbatim rather than guarded: the window's current
|
||||
@@ -84,16 +86,6 @@
|
||||
-- iterate 1..NUM_PET_STABLE_SLOTS and handle 0 on its own.
|
||||
NUM_PET_STABLE_SLOTS = 2
|
||||
|
||||
-- ── The model pane's yaw ─────────────────────────────────────────────────────────────────
|
||||
-- Every booth pane names its own engine yaw binding in `frame.setFacing`
|
||||
-- (`BenillaPaperDollModel_SetFacing`, `BenillaPetPaperDollModel_SetFacing`,
|
||||
-- `BenillaInspectModel_SetFacing`); this pane's is `BenillaStableModel_SetFacing`. The
|
||||
-- named forwarder stays because `frame.setFacing` wants a plain function value and this
|
||||
-- file's OnLoad is where the pane's own name for it belongs.
|
||||
function BenillaPetStableModel_SetFacing(radians)
|
||||
BenillaStableModel_SetFacing(radians)
|
||||
end
|
||||
|
||||
-- ── The window ──────────────────────────────────────────────────────────────────────────
|
||||
-- lua l.3-9, verbatim. UNIT_PET is here because a pet summoned or dismissed while the
|
||||
-- window is open changes slot 0 without the server sending a stable list.
|
||||
@@ -525,40 +517,29 @@
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The model pane (ref `<PlayerModel name="PetStableModel">`, xml l.182-280): 313x223
|
||||
at TOPLEFT (23,-76). A plain Frame sampling a booth bake, not a live 3D widget
|
||||
(header, deferral 1) — so the reference's `Model_OnLoad`, its
|
||||
`DISPLAY_SIZE_CHANGED`/`RefreshUnit` pair and its `Model_OnUpdate` all drop with
|
||||
the widget, exactly as they do on the two paper dolls.
|
||||
at TOPLEFT (23,-76), the reference's own widget type since decision 1751 gave
|
||||
`<Model>`/`<PlayerModel>` panes a draw of their own (portrait/mod.rs's
|
||||
`MODEL_PANE_BOOTHS`).
|
||||
|
||||
The `$parentTexture` this used to carry is gone with it, and so is decision 1070's
|
||||
BACKGROUND-not-ARTWORK note: that workaround existed because a booth *texture* is
|
||||
an opaque region in a BUCKET-WIDE draw layer that outranks the frame (0884), so at
|
||||
ARTWORK it painted over the diet icon below. The pane's own draw slot has no such
|
||||
problem — it emits at the FRAME's z, which is what puts every child above it, and
|
||||
that is the reference's own layering rather than a tie we had to arrange.
|
||||
|
||||
The pane keeps its reference NAME because the Lua passes it by name to
|
||||
`SetPetStablePaperdoll` at four sites, and an addon may too. -->
|
||||
<Frame name="PetStableModel">
|
||||
`SetPetStablePaperdoll` at four sites, an addon may too, and the app now joins the
|
||||
booth to the pane by that name. -->
|
||||
<PlayerModel name="PetStableModel">
|
||||
<Size><AbsDimension x="313" y="223"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="23" y="-76"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<!-- BACKGROUND, not ARTWORK — decision 1070, and PetPaperDollFrame.xml's own
|
||||
pane carries the same note. The booth quad stands in for a widget whose 3D
|
||||
content the reference renders outside the layer batches with a transparent
|
||||
background; ours is one OPAQUE texture inside them, and the draw layer is
|
||||
BUCKET-WIDE and outranks the frame (0884). At ARTWORK this quad would paint
|
||||
over the diet icon below it, which is exactly the bug that pane hit. At
|
||||
BACKGROUND the two tie and declaration order decides, which restores the
|
||||
reference's own ordering. -->
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentTexture">
|
||||
<Size><AbsDimension x="313" y="223"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- Rotate buttons (xml l.206-247): 35x35, the left one at the pane's own
|
||||
TOPLEFT and the right one chained off its TOPRIGHT. The click helpers are
|
||||
CharacterFrame.xml's shared `BenillaPaperDollModel_Rotate*` in place of the
|
||||
reference's `Model_RotateLeft/Right`, and they drive `frame.setFacing`,
|
||||
which the pane names for itself in OnLoad below. -->
|
||||
TOPLEFT and the right one chained off its TOPRIGHT, calling the
|
||||
reference's own `Model_RotateLeft`/`Model_RotateRight` (UIParent.xml). -->
|
||||
<Button name="PetStableModelRotateLeftButton">
|
||||
<Size><AbsDimension x="35" y="35"/></Size>
|
||||
<Anchors>
|
||||
@@ -569,7 +550,7 @@
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnLoad>this:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
|
||||
<OnClick>BenillaPaperDollModel_RotateLeft(getglobal("PetStableModel"))</OnClick>
|
||||
<OnClick>Model_RotateLeft(this:GetParent())</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="PetStableModelRotateRightButton">
|
||||
@@ -582,7 +563,7 @@
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Round" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnLoad>this:RegisterForClicks("LeftButtonDown", "LeftButtonUp")</OnLoad>
|
||||
<OnClick>BenillaPaperDollModel_RotateRight(getglobal("PetStableModel"))</OnClick>
|
||||
<OnClick>Model_RotateRight(this:GetParent())</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- The diet icon (xml l.248-278): 24x23 under the rotate buttons, its texcoords
|
||||
@@ -621,19 +602,14 @@
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<!-- The pane's booth + its yaw. `BenillaPaperDollModel_OnLoad` seeds the
|
||||
reference's own 0.61 default facing and pushes it through `setFacing`; the
|
||||
OnUpdate is the shared held-button continuous spin. The booth token is
|
||||
"stable" and the yaw goes through this file's named forwarder onto
|
||||
`BenillaStableModel_SetFacing`. -->
|
||||
<OnLoad>
|
||||
this.setFacing = BenillaPetStableModel_SetFacing
|
||||
BenillaPaperDollModel_OnLoad(this)
|
||||
BenillaSetBoothTexture(getglobal(this:GetName() .. "Texture"), "stable")
|
||||
</OnLoad>
|
||||
<OnUpdate>BenillaPaperDollModel_OnUpdate(this, arg1)</OnUpdate>
|
||||
<!-- The reference's own pair (xml l.194-203): `Model_OnLoad` seeds the 0.61
|
||||
default facing through `SetRotation`, and `Model_OnUpdate` is the
|
||||
held-button continuous spin. The app mirrors this pane's facing onto the
|
||||
"stable" booth each frame (`ui_stable`). -->
|
||||
<OnLoad>Model_OnLoad()</OnLoad>
|
||||
<OnUpdate>Model_OnUpdate(arg1, this)</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</PlayerModel>
|
||||
|
||||
<!-- The three slots (xml l.281-333). `id=` is what `this:GetID()` reads in the
|
||||
template's handlers, and it IS the binding's slot index: 0 is the pet at your side,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
identical rect and is what the page was built and approved against, not a workaround for
|
||||
something the engine cannot do.
|
||||
`CharacterFrame.xml`'s `CharacterFrame_OnHide` hide-loop over
|
||||
`BENILLA_CHARACTERFRAME_SUBFRAMES` is what closes it with the window; joining that table as
|
||||
`CHARACTERFRAME_SUBFRAMES` is what closes it with the window; joining that table as
|
||||
entry 3 is all this page has to do to get that.
|
||||
|
||||
THE TAB IS THE REFERENCE'S OWN SLOT 3 AGAIN. Until this page, `SkillFrame` sat at id 3 with
|
||||
@@ -587,7 +587,7 @@
|
||||
<!-- id=3, the reference's own (ref l.211): 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).
|
||||
`character_tests.rs` pins every page's id against its slot in
|
||||
BENILLA_CHARACTERFRAME_SUBFRAMES. -->
|
||||
CHARACTERFRAME_SUBFRAMES. -->
|
||||
<Frame name="ReputationFrame" enableMouse="true" parent="CharacterFrame" hidden="true" id="3">
|
||||
<!-- The real 384x512 page. The explicit `<Size>` + TOPLEFT `<Anchor>` below is an exact
|
||||
equivalent of the reference's `setAllPoints="true"` here — CharacterFrame, the parent
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
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 `BENILLA_CHARACTERFRAME_SUBFRAMES` entry — the fix this
|
||||
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.
|
||||
|
||||
@@ -542,7 +542,7 @@
|
||||
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 BENILLA_CHARACTERFRAME_SUBFRAMES, which is what made the renumber loud rather
|
||||
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
|
||||
|
||||
@@ -2418,10 +2418,36 @@
|
||||
pre-fit state). Its OnShow *also* sets the highlight width, which is dead code in the
|
||||
reference and dropped here: this highlight is anchored LEFT/RIGHT, and two opposing anchors
|
||||
resolve the size from the span without consulting the width at all (wow-re layout.md,
|
||||
VERIFIED). The small tab now says the same thing the same way (1004). One deviation: the ref's OnLoad
|
||||
SetFrameLevel(+4) raise (our loader has no cross-frame level raise wired into templates
|
||||
yet; the tabs sit below the window art's hit-rect so nothing overlaps them). OnClick is
|
||||
the instance's to define (the ref merchant overrides it too).
|
||||
VERIFIED). The small tab now says the same thing the same way (1004).
|
||||
|
||||
**The OnLoad raise and the OnClick are the reference's own, and both were dropped here
|
||||
until decision 1810.** The note that stood in their place said the raise was unnecessary —
|
||||
"the tabs sit below the window art's hit-rect so nothing overlaps them" — and that "OnClick
|
||||
is the instance's to define". Both were true of our own CharacterFrame.xml, and both are
|
||||
false of the reference's, which is the file that runs now:
|
||||
|
||||
- the reference's `PaperDollFrame` is `setAllPoints enableMouse="true"` with a
|
||||
`bottom=45` hit inset, and the reference's tabs are anchored INSIDE the window (CENTER
|
||||
to BOTTOMLEFT +60,+62), so the page's hit rect covers the whole row. Frame levels come
|
||||
out CharacterFrame 1 / PaperDollFrame 2 / tabs 2, later-created winning — the page eats
|
||||
every tab click. `SetFrameLevel(+4)` is what lifts the row out, and is why the
|
||||
reference has it.
|
||||
- stock `CharacterFrame.xml` gives its five tabs only `<OnEnter>`/`<OnLeave>`; the
|
||||
dispatch lives HERE. Without it `CharacterFrameTab4:GetScript("OnClick")` is nil and
|
||||
the character window's tabs do nothing at all.
|
||||
|
||||
The body hardcodes `CharacterFrame`, which reads odd on a shared template until you check
|
||||
the other consumers: the reference's `MerchantFrameTab1/2` and every one of ours
|
||||
(Auction/Friends/Inspect/Mail/Talent) declares its own `<OnClick>`, and an instance's
|
||||
handler REPLACES the inherited one rather than adding to it. So this reaches exactly the
|
||||
window it names, which is the reference's own arrangement.
|
||||
|
||||
Still ours, and still why this template is not simply sourced off the chain:
|
||||
`Interface\FrameXML\CharacterFrameTemplates.xml` fits its text in `<OnShow>` through
|
||||
`PanelTemplates_TabResize(0)`, which needs the reference's SYNCHRONOUS string measure. Ours
|
||||
lands a frame late, so the fit runs from `BenillaTabButton_OnUpdate` instead and an
|
||||
OnShow-only fit would leave every tab at its pre-fit 115px. That divergence is 1004's, it
|
||||
spans six windows, and it is what this template's own migration waits on.
|
||||
-->
|
||||
<Button name="CharacterFrameTabButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="115" y="32"/></Size>
|
||||
@@ -2472,6 +2498,12 @@
|
||||
</Anchors>
|
||||
</HighlightTexture>
|
||||
<Scripts>
|
||||
<!-- ref CharacterFrameTemplates.xml:69-81, verbatim. -->
|
||||
<OnLoad>this:SetFrameLevel(this:GetFrameLevel() + 4)</OnLoad>
|
||||
<OnClick>
|
||||
PanelTemplates_Tab_OnClick(CharacterFrame)
|
||||
CharacterFrameTab_OnClick()
|
||||
</OnClick>
|
||||
<OnUpdate>BenillaTabButton_OnUpdate(self)</OnUpdate>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
@@ -74,26 +74,6 @@ Interface\FrameXML\Localization.xml
|
||||
# words with `GetText("FACTION_STANDING_LABEL"..id, gender)` — and fourteen more call sites across
|
||||
# FriendsFrame, StaticPopup and the quest windows are waiting behind it.
|
||||
Interface\FrameXML\LocaleProperties.lua
|
||||
# ── A reference file sourced ABOVE ours, so OUR bodies win every collision ─────────────────────
|
||||
# This one is not a migrated window: it is here for one family of frame-agnostic globals that
|
||||
# addons call by name, while the window that file also drives is still ours. So it loads first and
|
||||
# our later files overwrite the names we own. (A migrated window is the other case, and sits at its
|
||||
# window's own position — `Interface\FrameXML\ContainerFrame.xml` below is the first of them.)
|
||||
#
|
||||
# PaperDollFrame.lua earns its place for ONE family: `PaperDollItemSlotButton_OnLoad`/`_OnEvent`/
|
||||
# `_OnClick`/`_Update`/`_UpdateLock`/`_OnEnter`/`_OnUpdate` — frame-agnostic in the same sense,
|
||||
# acting on `this`/`this:GetID()` through live APIs and never on a named reference window.
|
||||
# `PaperDollItemSlotButtonTemplate` and `BagSlotButtonTemplate` are declared over them
|
||||
# (ItemSlotButtonTemplates.xml), and pfUI builds its bag bar from the latter.
|
||||
#
|
||||
# Its top level is four constants (NUM_RESISTANCE_TYPES, NUM_STATS, NUM_SHOPPING_TOOLTIPS,
|
||||
# ATTACK_POWER_MAGIC_NUMBER) — nothing touches a frame at load, so sourcing it cannot raise. 18 of
|
||||
# its 29 functions collide with ours (the PaperDollFrame_Set* stat family, the two damage-frame
|
||||
# hovers, PaperDollFormatStat, PaperDollStatTooltip) and this position settles all of them the
|
||||
# documented way: CharacterFrame.xml loads far below and its bodies win. Not one of the seven
|
||||
# names we are here for is in that overlap, and our own doll slots keep calling
|
||||
# `BenillaPaperDollSlot_*`, untouched.
|
||||
Interface\FrameXML\PaperDollFrame.lua
|
||||
# The reference's own second file, in its own position: TEXT/message/_ERRORMESSAGE, the
|
||||
# ScriptErrors dialog, and the DialogBoxFrame + DialogButton*Texture kit it is built from.
|
||||
# Straight after Fonts.xml because those are the only things it needs (GameFontRedLarge,
|
||||
@@ -559,18 +539,52 @@ Interface\FrameXML\RaidWarning.xml
|
||||
# (Cooldown.xml), `UIPanelCloseButton` (UIPanelTemplates.xml) — plus `parent="UIParent"`. It
|
||||
# cannot load "first" the way a Lua-only sourcing had to.
|
||||
Interface\FrameXML\ContainerFrame.xml
|
||||
# The reference's item-SLOT button base, `PaperDollItemSlotButtonTemplate` — published for addons
|
||||
# (pfUI builds its bag bar from what inherits it) and, since window 3, a LOAD-TIME dependency of the
|
||||
# entry directly below: stock `MainMenuBarBagButtons.xml` inherits it on all five bag buttons, and
|
||||
# an `inherits=` is resolved at load. That requirement is what moved this file up from below
|
||||
# CharacterFrame.xml, where it sat only because the chain crossed the old BagFrame -> CharacterFrame
|
||||
# the wrong way. It needs ItemButtonTemplate.xml above it for the base and Cooldown.xml for the
|
||||
# $parentCooldown the doll template carries — both far above.
|
||||
# ══ THE CHARACTER WINDOW — the reference's own, off the player's chain (decision 1751) ══
|
||||
# `CharacterFrame` + its five-tab row and portrait/name header (CharacterFrame.xml), the paper doll
|
||||
# itself — model pane, 19 equipment slots, the ammo slot, the five attributes, the melee and ranged
|
||||
# stat blocks, the five resistances (PaperDollFrame.xml) — and the pet page over the same templates
|
||||
# (PetPaperDollFrame.xml). Our `CharacterFrame.xml` (2027 lines) and `PetPaperDollFrame.xml` (773)
|
||||
# stood here and are deleted.
|
||||
#
|
||||
# `BagSlotButtonTemplate` used to be declared here too, transcribed from MainMenuBarBagButtons.xml
|
||||
# because that file could not be sourced. It is sourced now, one entry down, so the reference's own
|
||||
# declaration is the live one and the copy is deleted — the whole point of the migration.
|
||||
ItemSlotButtonTemplates.xml
|
||||
# THREE FILES IN THE REFERENCE'S OWN ORDER, and every edge between them is resolved at LOAD:
|
||||
# - CharacterFrame.xml declares `CharacterFrame`, which the other two name in `parent=` (1734).
|
||||
# - PaperDollFrame.xml declares `StatFrameTemplate`, `AttributeFrameTemplate` and
|
||||
# `MagicResistanceFrameTemplate`, which PetPaperDollFrame.xml inherits on 11 frames.
|
||||
# - PaperDollFrame.xml also declares `PaperDollItemSlotButtonTemplate`, which stock
|
||||
# `MainMenuBarBagButtons.xml` below inherits on all five bag buttons.
|
||||
#
|
||||
# THAT LAST EDGE IS WHY THE BLOCK SITS HERE rather than below the bag bar where ours did.
|
||||
# `ItemSlotButtonTemplates.xml` held a transcribed copy of that template for exactly one reason:
|
||||
# our CharacterFrame.xml loaded after the bag bar and could not declare it in time. The reference's
|
||||
# own toc has no such problem (CharacterFrame 53, PaperDollFrame 54, MainMenuBarBagButtons 59), so
|
||||
# the copy is deleted and the reference's declaration is the live one — the same retirement
|
||||
# `BagSlotButtonTemplate` took one window earlier.
|
||||
#
|
||||
# `PaperDollFrame.lua` used to be a manifest line of its own, sourced far above everything so that
|
||||
# our bodies would win its 18 collisions. It arrives through PaperDollFrame.xml's own
|
||||
# `<Script file=>` now, at this position, and the collisions went with the file that had them.
|
||||
#
|
||||
# What the block needs from above, all far up: Fonts.xml (every `inherits=` font object),
|
||||
# ItemButtonTemplate.xml (the slot template's base, and the `SetItemButton*` family),
|
||||
# Cooldown.xml (`CooldownFrameTemplate` + `CooldownFrame_SetTimer`), GameTooltip.xml, UiPanels.xml
|
||||
# (`CharacterFrameTabButtonTemplate`, `UIPanelWindows`), UIPanelTemplates.xml (`UIPanelCloseButton`,
|
||||
# `UIPanelButtonTemplate`, and `PanelTemplates_SetNumTabs`, which `CharacterFrame_OnLoad` calls at
|
||||
# LOAD), TextStatusBar.xml (`SetTextStatusBarTextPrefix`, called at load too, plus the
|
||||
# `TextStatusBar` the pet page's XP bar inherits), UnitFrames.xml (`PlayerFrameHealthBar` /
|
||||
# `PlayerFrameManaBar`, named in that same OnLoad), ActionBar.xml (`MainMenuExpBar`, likewise),
|
||||
# MicroMenu.xml (`MicroButtonTooltipText`, the five tab hovers) and UIParent.xml
|
||||
# (`Model_OnLoad`/`_RotateLeft`/`_RotateRight`/`_OnUpdate` — the model pane's turntable, the
|
||||
# reference's own code living in our UIParent.lua counterpart).
|
||||
#
|
||||
# Two names it writes live BELOW it, and that is fine because both are runtime rather than load:
|
||||
# `HonorLevelText` and `HonorGuildText` (HonorFrame.xml), which `PaperDollFrame_SetLevel` and
|
||||
# `_SetGuild` fill "while we're at it" — reached only from OnShow and the UNIT_* events, long after
|
||||
# that file has loaded.
|
||||
Interface\FrameXML\CharacterFrame.xml
|
||||
Interface\FrameXML\PaperDollFrame.xml
|
||||
# Tab 2, the reference's file as well. `PetTab_Update` shows or hides `CharacterFrameTab2` off
|
||||
# `HasPetUI()` and re-anchors tab 3 over the gap, so a classless character's row has no hole.
|
||||
Interface\FrameXML\PetPaperDollFrame.xml
|
||||
# ══ THE BAG BAR — the reference's own, off the player's chain (decision 1751, window 3) ══
|
||||
# `MainMenuBarBackpackButton`, `CharacterBag0Slot`..`CharacterBag3Slot`, `KeyRingButton`,
|
||||
# `BagSlotButtonTemplate` and the `BagSlotButton_*`/`BackpackButton_*`/`Disable_BagButtons` family
|
||||
@@ -584,8 +598,10 @@ ItemSlotButtonTemplates.xml
|
||||
#
|
||||
# THE POSITION IS THE REQUIREMENT: `inherits="PaperDollItemSlotButtonTemplate"` on the four bag
|
||||
# slots and `inherits="ItemButtonTemplate"` on the backpack are resolved at LOAD, and so is
|
||||
# `parent="MainMenuBarArtFrame"` — so this sits below ItemSlotButtonTemplates.xml (above) and
|
||||
# ActionBar.xml (which declares the art frame). Being BELOW the sourced ContainerFrame.xml is
|
||||
# `parent="MainMenuBarArtFrame"` — so this sits below the character-window block (above, which is
|
||||
# where stock `PaperDollFrame.xml` declares that template) and below ActionBar.xml (which declares
|
||||
# the art frame). It sat below a transcribed copy of the template in `ItemSlotButtonTemplates.xml`
|
||||
# until the character window migrated and made that copy redundant (decision 1810). Being BELOW the sourced ContainerFrame.xml is
|
||||
# legibility, not necessity, in either direction: every verb each file calls of the other resolves
|
||||
# at call time, `KEYRING_CONTAINER` included (no top-level read of it exists in ContainerFrame.lua).
|
||||
#
|
||||
@@ -605,17 +621,6 @@ Interface\FrameXML\StackSplitFrame.xml
|
||||
# detour benilla answers differently (0765). Must load AFTER them, since it captures at load the
|
||||
# body it wraps; its own header says what it stopped needing to do when the XML became sourceable.
|
||||
ContainerFrameAdapters.xml
|
||||
# The character window (decision 0208 phase 1a): the C-key paper doll. Loaded after BagFrame
|
||||
# (needs nothing from it, but keeps player-state windows grouped) and before the NPC session
|
||||
# windows (Gossip/Merchant/Quest*) — it depends only on Fonts/UiPanels/GameTooltip, all
|
||||
# already loaded above.
|
||||
CharacterFrame.xml
|
||||
# The Pet tab (decision 1057): the same separate-top-level-file shape as SkillFrame below,
|
||||
# and ahead of it because it is the earlier tab. It leans on CharacterFrame.xml for real —
|
||||
# the `(unit, prefix)` stat setters, the `BenillaStatRow`/`BenillaMagicResistanceFrame`
|
||||
# templates and the tab row it raises tab 2 of — all of which only exist once that file has
|
||||
# loaded, so this order is a requirement here, not just tidiness.
|
||||
PetPaperDollFrame.xml
|
||||
# The Reputation tab: the same separate-top-level-file shape as the two pages around it, seated
|
||||
# BETWEEN them because that is the real client's tab order (Character 1, Pet 2, Reputation 3,
|
||||
# Skills 4 — ref CharacterFrame.xml:79-168), and the page's `id=` is that slot. Its hard
|
||||
@@ -770,9 +775,10 @@ TaxiFrame.xml
|
||||
# windows grouped" cluster beside Trainer/Bank/Taxi. Two of its dependencies are resolved at
|
||||
# LOAD and so must sit above it: MoneyFrame (its purse and cost displays inherit
|
||||
# SmallMoneyFrameTemplate) and UIPanelTemplates (UIPanelButtonTemplate / UIPanelCloseButton).
|
||||
# A third is resolved at load too, and is the reason this cannot sit up with the other NPC
|
||||
# windows' dependencies: the model pane's own <OnLoad> calls CharacterFrame.xml's shared
|
||||
# `BenillaPaperDollModel_OnLoad`, so that file must already have run. The rest
|
||||
# A third is resolved at load too: the model pane is a `<PlayerModel>` whose own <OnLoad> calls
|
||||
# the reference's `Model_OnLoad` (UIParent.xml, far above). That used to be CharacterFrame.xml's
|
||||
# `BenillaPaperDollModel_OnLoad`, which is why this entry sat below the character window; it no
|
||||
# longer has to, and stays here only because the NPC-session cluster is where it belongs. The rest
|
||||
# (ItemButtonTemplate's SetItemButtonTexture, GameTooltip, UiPanels' ShowUIPanel/HideUIPanel,
|
||||
# UIParent's BuildListString) are resolved when the window repaints, so their order is free.
|
||||
PetStable.xml
|
||||
|
||||
@@ -142,6 +142,44 @@ const PETDOLL_SLOT: &str = "petdoll";
|
||||
/// [`StableBooth::unit`] at the pet entity ([`PETDOLL_SLOT`]'s case exactly), the cached one points
|
||||
/// [`StableBooth::display_id`] at the display and the mirror comes off a [`PortraitStandIn`].
|
||||
const STABLE_SLOT: &str = "stable";
|
||||
|
||||
/// **Which booth a `<Model>`/`<PlayerModel>` pane samples** — the one join between the reference's
|
||||
/// own frame names and this module's body-bake slots.
|
||||
///
|
||||
/// The reference draws a body pane by rendering the unit into the widget live; benilla renders it
|
||||
/// into an off-screen bake this module keeps *per window* (the doc above: framing, resolution and
|
||||
/// yaw differ per pane, so they are separate booths). Which bake a pane samples is therefore a
|
||||
/// fact about the window, not about the widget — and the pane's global name is what identifies the
|
||||
/// window, because since decision 1751 that name is the reference's own.
|
||||
///
|
||||
/// This table replaces `BenillaSetBoothTexture(region, token)`, the Lua binding that used to carry
|
||||
/// the same join: a file of ours put a Texture inside the pane and named the slot. A migrated
|
||||
/// window's file is the reference's, so the join cannot live in Lua any more — and it should not,
|
||||
/// because a stock addon creating a `<PlayerModel>` never called it either. A pane that is not in
|
||||
/// this table draws nothing, exactly as it does today.
|
||||
///
|
||||
/// The names, and where each is declared: `CharacterModelFrame` (stock `PaperDollFrame.xml`),
|
||||
/// `PetModelFrame` (stock `PetPaperDollFrame.xml`), `DressUpModel` (stock `DressUpFrame.xml`'s
|
||||
/// name, our file), `PetStableModel` (stock `PetStable.xml`'s name, our file), and
|
||||
/// `BenillaInspectModelFrame` — the one that is NOT the reference's, because 1.12's inspect window
|
||||
/// is a LoadOnDemand `Blizzard_InspectUI` the install ships packed, so we have no stock file to
|
||||
/// take the name from.
|
||||
const MODEL_PANE_BOOTHS: [(&str, &str); 5] = [
|
||||
("CharacterModelFrame", PAPERDOLL_SLOT),
|
||||
("PetModelFrame", PETDOLL_SLOT),
|
||||
("BenillaInspectModelFrame", INSPECT_SLOT),
|
||||
("PetStableModel", STABLE_SLOT),
|
||||
("DressUpModel", dressup::DRESSUP_SLOT),
|
||||
];
|
||||
|
||||
/// The booth a named model pane samples, or `None` for a pane no window has claimed.
|
||||
pub(crate) fn model_pane_booth(name: &str) -> Option<&'static str> {
|
||||
MODEL_PANE_BOOTHS
|
||||
.iter()
|
||||
.find(|(pane, _)| *pane == name)
|
||||
.map(|(_, slot)| *slot)
|
||||
}
|
||||
|
||||
/// World is layer 0, the UI quad pass layer 1; portraits sit on their own high layers so nothing in the
|
||||
/// world leaks into a booth and vice-versa (one layer per slot: base, base+1, …).
|
||||
const PORTRAIT_LAYER_BASE: usize = 2;
|
||||
|
||||
@@ -27,9 +27,10 @@
|
||||
//! `locked` (decision 0208 phase 1b — the doll twin of `ui_items::feed_containers`'s bag-slot
|
||||
//! `.locked`), and its resolved `equip_slots` (`ui_items::find_equip_slot` over the item
|
||||
//! template's `inventoryType` — the cursor arc's "fit rule", decision 0216/0218).
|
||||
//! - **The paper-doll booth yaw**: the pane's `Model:SetRotation` transcription writes the VM-side
|
||||
//! value ([`UiScript::paperdoll_yaw`]); the feed mirrors it onto [`PaperDollBooth`] so the booth
|
||||
//! re-bakes at the new facing (decision 0208 §5).
|
||||
//! - **The paper-doll booth yaw**: the stock pane's rotate buttons call the reference's own
|
||||
//! `Model_RotateLeft`/`_RotateRight`, which write `CharacterModelFrame:SetRotation`; the feed
|
||||
//! mirrors that pane's facing onto [`PaperDollBooth`] so the booth re-bakes at the new angle
|
||||
//! (decision 0208 §5, off the pane itself since 1751).
|
||||
//!
|
||||
//! Events, fired on snapshot transitions (grouped by the ref's own registration set,
|
||||
//! `PaperDollFrame.lua:14-28` — arg1 `"player"`): `UNIT_STATS`, `UNIT_RESISTANCES`, `UNIT_DAMAGE`,
|
||||
@@ -505,6 +506,7 @@ pub(crate) fn unit_combat_stats(store: &ObjectStore) -> UnitCombatStats {
|
||||
has_offhand: false,
|
||||
has_wand: false,
|
||||
main_weapon_skill: (0, 0),
|
||||
offhand_weapon_skill: (0, 0),
|
||||
ranged_weapon_skill: (0, 0),
|
||||
defense_skill: (0, 0),
|
||||
}
|
||||
@@ -524,6 +526,7 @@ fn combat_stats(store: &ObjectStore, items: &mut Items, commands: &NetCommands)
|
||||
.is_some_and(|t| t.class == ITEM_CLASS_WEAPON && t.subclass == SUBCLASS_WAND);
|
||||
|
||||
let main_skill = weapon_skill_id(store, items, commands, SLOT_MAIN_HAND);
|
||||
let offhand_skill = weapon_skill_id(store, items, commands, SLOT_OFF_HAND);
|
||||
let ranged_skill_id = slot_entry(store, items, SLOT_RANGED)
|
||||
.and_then(|e| items.template(e, 0, commands))
|
||||
.filter(|t| t.class == ITEM_CLASS_WEAPON)
|
||||
@@ -533,6 +536,10 @@ fn combat_stats(store: &ObjectStore, items: &mut Items, commands: &NetCommands)
|
||||
has_offhand,
|
||||
has_wand,
|
||||
main_weapon_skill: skill_pair(store, main_skill),
|
||||
// `UnitAttackBothHands`'s SECOND pair (decision 1810) — the binding pushes one per hand,
|
||||
// and `weapon_skill_id` already answers Unarmed for an empty or non-weapon off hand, which
|
||||
// is what a shield or an empty hand reads as.
|
||||
offhand_weapon_skill: skill_pair(store, offhand_skill),
|
||||
ranged_weapon_skill: ranged_skill_id.map_or((0, 0), |id| skill_pair(store, id)),
|
||||
// `UnitDefense`'s pair. Its repaint wire is `SKILL_LINES_CHANGED` (which the ref's
|
||||
// `PaperDollFrame` registers, l.28, and `watch_skill_ups` already fires), NOT a
|
||||
@@ -979,9 +986,13 @@ pub(crate) fn feed_char(
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
};
|
||||
// The pane's Model:SetRotation transcription owns the yaw; the booth mirrors it (0208 §5).
|
||||
// The pane owns the yaw and the booth mirrors it (0208 §5). Read off the widget's own
|
||||
// `SetRotation` state since decision 1751 put the reference's `PaperDollFrame.xml` on the
|
||||
// chain: the stock file declares a real `<PlayerModel name="CharacterModelFrame">` and drives
|
||||
// it with `Model_OnLoad`/`Model_Rotate*`/`Model_OnUpdate`, where ours used to call a
|
||||
// benilla-named `BenillaPaperDollModel_SetFacing` into a scalar on the VM.
|
||||
// Write-if-different: an unconditional write would mark the booth changed every frame.
|
||||
let yaw = script.paperdoll_yaw();
|
||||
let yaw = script.model_pane_facing("CharacterModelFrame");
|
||||
if booth.yaw != yaw {
|
||||
booth.yaw = yaw;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
//! `PetPaperDollFrame_Update` calls `PaperDollFrame_SetDamage/_SetAttackPower/_SetArmor/…` with
|
||||
//! `unit = "pet"` (ref `PetPaperDollFrame.lua:73-81`) and its own two setters read
|
||||
//! `UnitStat("pet", i)` / `UnitResistance("pet", id)`. Nothing here is a parallel API.
|
||||
//! - **Pointing the body booth**: [`PetDollBooth`]'s `unit` gets the resolved pet entity and its
|
||||
//! `yaw` the VM-side value the pane's rotate buttons wrote
|
||||
//! ([`UiScript::pet_paperdoll_yaw`]) — the `crate::ui_inspect` pane's arrangement exactly.
|
||||
//! - **Pointing the body booth**: [`PetDollBooth`]'s `unit` gets the resolved pet entity, and its
|
||||
//! `yaw` the facing the stock pane's rotate buttons wrote onto `PetModelFrame` itself
|
||||
//! (`UiScript::model_pane_facing` — decision 1751).
|
||||
//!
|
||||
//! **Why a module of its own rather than more of [`crate::ui_pet_stats`]**, which already resolves
|
||||
//! the same pet: these are the *shared* paper-doll surface (every value passes through a binding
|
||||
@@ -62,7 +62,7 @@ fn feed_pet_doll(
|
||||
// The pane's rotate buttons own the yaw; the booth mirrors it (the inspect pane's arrangement,
|
||||
// decision 0631 §4). Written every frame, pet or no pet — a stale yaw would snap the model the
|
||||
// moment one is summoned.
|
||||
booth.yaw = script.pet_paperdoll_yaw();
|
||||
booth.yaw = script.model_pane_facing("PetModelFrame");
|
||||
|
||||
let pet_guid = bar.spells.pet_guid;
|
||||
let store = (pet_guid != 0).then(|| pet.store(pet_guid)).flatten();
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
//! The shipped **character window** driven end-to-end, engine-only (no Bevy): the real
|
||||
//! `assets/ui/CharacterFrame.xml` loaded behind `Fonts.xml`/`UiPanels.xml`/`GameTooltip.xml` and fed
|
||||
//! a synthetic player snapshot + combat-stats + one equipped item — mirroring `questlog_tests.rs`'s
|
||||
//! harness for the paper-doll slice (decision 0208 phase 1a).
|
||||
//! The shipped **character window** driven end-to-end, engine-only (no Bevy) — and since decision
|
||||
//! 1751's character swap, "shipped" means the **reference's own**
|
||||
//! `Interface\FrameXML\CharacterFrame.xml`, `PaperDollFrame.xml` and `PetPaperDollFrame.xml`, read
|
||||
//! off the player's patch chain. Our `assets/ui/CharacterFrame.xml` and `PetPaperDollFrame.xml` are
|
||||
//! deleted; [`super::test_ui::CHARACTER_UI`] is the load list, in `benilla.toc`'s own order, and
|
||||
//! every test here opens with `wow_data_or_skip!()` because a chain entry needs the install.
|
||||
//!
|
||||
//! NOTE (session state, not a property of this file): at the time this test was written, the
|
||||
//! `benilla` crate's non-test code (`ui_unit.rs`, `ui_script/mod.rs`'s `demo_unit_feed`) had not yet
|
||||
//! been updated for `UnitState`'s new `race`/`class`/`class_file`/`race_file`/`sex` fields (a
|
||||
//! concurrent, unrelated change landing in `benilla-ui/src/script/unit.rs` this same session) — so
|
||||
//! `cargo test -p benilla` could not compile AT ALL, for reasons entirely outside this file. Every
|
||||
//! assertion below was independently cross-verified against the real `benilla-ui` engine through a
|
||||
//! throwaway external harness (a scratch Cargo project depending on `benilla-ui` by path) before
|
||||
//! landing here, so its correctness doesn't rest on `cargo test -p benilla` having run — but that
|
||||
//! command itself is still owed once the unrelated breakage clears, per this crate's own gates.
|
||||
//! The fixtures are unchanged (a synthetic player snapshot + combat stats + one equipped item), but
|
||||
//! the behaviour under them is the stock file's, not ours. Where the two differed the divergence is
|
||||
//! named at the test that used to pin it, with the stock file and line that replaces it — the
|
||||
//! per-window re-examination 1751 asks for, done in the open rather than silently.
|
||||
//!
|
||||
//! **Handlers are driven through the mouse, never called by name.** The reference's slot handlers
|
||||
//! read `this` (`PaperDollItemSlotButton_OnClick(button, ignoreModifiers)` takes the MOUSE button
|
||||
//! and gets the frame from `this`, `PaperDollFrame.lua:647`), and only the engine sets `this`.
|
||||
|
||||
use benilla_ui::script::{
|
||||
ExtractedQuad, InvSlotView, InventorySlots, QuadContent, ScriptValue, SoundRequest, UiScript,
|
||||
@@ -117,18 +118,32 @@ fn backpack_with_fitting_helm() -> benilla_ui::script::ContainerState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The loader itself: every file the window depends on parses and materializes with no errors —
|
||||
/// 59 frames (the container + tab + paper-doll page: 19 slots + ammo + 5 attribute rows + armor/
|
||||
/// attack/damage/ranged rows + 5 resistance frames + the model pane + its 2 rotate buttons + chrome).
|
||||
/// The loader itself: every file the window depends on parses and materializes with no errors, and
|
||||
/// the two chain files this module owns each land their own frame count.
|
||||
///
|
||||
/// **A count is a fingerprint of a file, not a target** (decision 1800's closing note). These two
|
||||
/// are the reference's — `CharacterFrame.xml`'s container + close button + name frame + five tabs,
|
||||
/// and `PaperDollFrame.xml`'s 19 slots + ammo + their cooldown children + the stat/resistance rows
|
||||
/// + the model pane and its two rotate buttons. They change only when the player's own file does.
|
||||
#[test]
|
||||
fn shipped_character_frame_loads_clean() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = UiScript::new().unwrap();
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
counts.insert(*f, super::test_ui::load_ui_strict(&s, f));
|
||||
}
|
||||
assert!(s.errors().is_empty(), "load errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
counts.get("Interface\\FrameXML\\CharacterFrame.xml"),
|
||||
Some(&8),
|
||||
"the reference's own CharacterFrame.xml"
|
||||
);
|
||||
assert_eq!(
|
||||
counts.get("Interface\\FrameXML\\PaperDollFrame.xml"),
|
||||
Some(&75),
|
||||
"the reference's own PaperDollFrame.xml"
|
||||
);
|
||||
}
|
||||
|
||||
/// The whole contract in one end-to-end drive: `ToggleCharacter` (the 'C' binding's entry point)
|
||||
@@ -142,11 +157,9 @@ fn shipped_character_frame_drives_end_to_end() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
@@ -322,13 +335,15 @@ fn shipped_character_frame_drives_end_to_end() {
|
||||
);
|
||||
|
||||
// Rotating the model pane advances the booth yaw from the ref's default 0.61 by +0.03/click and
|
||||
// plays the rotate kit (ref UIParent.lua:1421-1442).
|
||||
s.run("BenillaPaperDollModel_RotateRight(CharacterModelFrame)")
|
||||
.unwrap();
|
||||
// plays the rotate kit. The pane's own rotate button calls `Model_RotateRight(this:GetParent())`
|
||||
// (stock `PaperDollFrame.xml:265`), the reference's shared turntable out of `UIParent.lua:1421`,
|
||||
// declared in our `UIParent.xml` — our `BenillaPaperDollModel_*` pair is gone with the file
|
||||
// that used it (decision 1751).
|
||||
s.run("Model_RotateRight(CharacterModelFrame)").unwrap();
|
||||
assert!(
|
||||
(s.paperdoll_yaw() - 0.64).abs() < 0.001,
|
||||
(s.model_pane_facing("CharacterModelFrame") - 0.64).abs() < 0.001,
|
||||
"yaw = {}",
|
||||
s.paperdoll_yaw()
|
||||
s.model_pane_facing("CharacterModelFrame")
|
||||
);
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
@@ -358,11 +373,9 @@ fn close_button_draws_above_the_paper_doll_page() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
// The window must be SHOWN for extract() to emit quads (hidden="true" by default).
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
@@ -395,54 +408,100 @@ fn close_button_draws_above_the_paper_doll_page() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Before the app's first player snapshot lands, `UnitRace`/`UnitClass` answer nil,nil (this
|
||||
/// engine's own timing — unlike the real client's synchronous local data, this codebase's race/
|
||||
/// class stream in). The level line's "?" placeholders (CharacterFrame.xml's own `SetLevel`
|
||||
/// comment) keep `format()` from erroring outright, rather than the window failing to open at all.
|
||||
/// **The level/race/class line, and the one event that repaints it.** `PaperDollFrame_SetLevel`
|
||||
/// formats `PLAYER_LEVEL` ("Level %d %s %s", `GlobalStrings.lua:3075`) from `UnitLevel`/`UnitRace`/
|
||||
/// `UnitClass` and writes it to BOTH `CharacterLevelText` and the honor page's `HonorLevelText`
|
||||
/// "while we at it" (stock `PaperDollFrame.lua:100-103`); `UNIT_LEVEL` for `"player"` is the only
|
||||
/// event that calls it again (`PaperDollFrame.lua:48-49`), and only while the page is visible
|
||||
/// (`:42-44`).
|
||||
///
|
||||
/// **Retired divergence (decision 1751's character swap).** This test used to be
|
||||
/// `level_line_survives_no_player_snapshot_yet`: it opened the window with NO player snapshot and
|
||||
/// pinned `"Level 0 ? ?"`, because our deleted `assets/ui/CharacterFrame.xml` carried `or "?"`
|
||||
/// placeholders around the two nils. The reference has no such guard — `PaperDollFrame.lua:101`
|
||||
/// passes `UnitRace("player")` straight into `format`, which raises `bad argument #3 to 'format'`
|
||||
/// on a nil — so on the stock file that state is not "survived", it is a hard error, and pinning
|
||||
/// our softening would have been pinning the thing the swap deleted.
|
||||
///
|
||||
/// It is also **unreachable in the app**, which is why nothing replaces the guard rather than the
|
||||
/// expectation moving to "it raises": `ui_unit::feed_units` pushes `set_unit("player", …)` earlier
|
||||
/// in the same system than it fires `PLAYER_ENTERING_WORLD`, and that fire is gated on our avatar's
|
||||
/// descriptor existing (decisions 1087/1094) — while the window can only be opened in-world at all.
|
||||
/// So the state under test is the one below: a snapshot is always there first.
|
||||
#[test]
|
||||
fn level_line_survives_no_player_snapshot_yet() {
|
||||
fn level_line_reads_the_snapshot_and_repaints_on_unit_level() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
|
||||
// No set_unit("player", ...) call at all — UnitLevel/UnitRace/UnitClass answer their absent
|
||||
// shapes (0 / nil,nil / nil,nil, `unit.rs`'s own contract).
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
assert!(s.errors().is_empty(), "open errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return CharacterLevelText:GetText()")
|
||||
.unwrap(),
|
||||
"Level 0 ? ?"
|
||||
"Level 12 Night Elf Warrior"
|
||||
);
|
||||
// The honor page's twin, written by the same call — the reason `HonorFrame.xml` is in
|
||||
// CHARACTER_UI at all (its absence loads clean and raises on the first SHOW).
|
||||
assert_eq!(
|
||||
s.eval::<String>("return HonorLevelText:GetText()").unwrap(),
|
||||
"Level 12 Night Elf Warrior"
|
||||
);
|
||||
|
||||
// A stray extractor's frame-count sanity check: some frame renders (the window itself), no
|
||||
// panics anywhere in the drive.
|
||||
let quads: Vec<ExtractedQuad> = {
|
||||
s.resolve();
|
||||
s.extract()
|
||||
};
|
||||
assert!(!quads.is_empty());
|
||||
// Ding: a new snapshot plus `UNIT_LEVEL` for the player, the app's own sequencing.
|
||||
let mut leveled = player_unit();
|
||||
leveled.level = 13;
|
||||
s.set_unit("player", Some(leveled));
|
||||
s.fire_event("UNIT_LEVEL", vec![ScriptValue::Str("player".into())]);
|
||||
assert!(s.errors().is_empty(), "level-up errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return CharacterLevelText:GetText()")
|
||||
.unwrap(),
|
||||
"Level 13 Night Elf Warrior",
|
||||
"UNIT_LEVEL repaints the line (PaperDollFrame.lua:48-49)"
|
||||
);
|
||||
|
||||
// …and it is really on screen, not merely in the FontString.
|
||||
s.resolve();
|
||||
let quads: Vec<ExtractedQuad> = s.extract();
|
||||
assert!(
|
||||
quads.iter().any(|q| matches!(&q.content,
|
||||
QuadContent::Text { text: Some(t), .. } if t == "Level 13 Night Elf Warrior")),
|
||||
"the level line renders"
|
||||
);
|
||||
}
|
||||
|
||||
/// Decision 0208 phase 1b, end to end: clicking an OCCUPIED doll slot picks the item up onto the
|
||||
/// cursor (the SAME payload `GetCursorInfo`/`CursorHasItem` read) and dims the slot's icon
|
||||
/// (`IsInventoryItemLocked` — the held-here derivation, no server round-trip). A second click on
|
||||
/// the SAME slot cancels: the cursor empties and the dim clears.
|
||||
///
|
||||
/// **Driven through the mouse since decision 1751's character swap**, not `s.run("Handler(frame)")`:
|
||||
/// the stock handler is `PaperDollItemSlotButton_OnClick(button, ignoreModifiers)`
|
||||
/// (`PaperDollFrame.lua:647`), which takes the MOUSE button as its first argument and reads the
|
||||
/// frame off `this` — and only the engine sets `this`. That also puts the template's own
|
||||
/// `RegisterForClicks("LeftButtonUp", "RightButtonUp")` (`PaperDollFrame.lua:86`) and its
|
||||
/// `<OnClick>PaperDollItemSlotButton_OnClick(arg1)</OnClick>` (`PaperDollFrame.xml:15-17`) under
|
||||
/// test, which the old by-name call skipped.
|
||||
///
|
||||
/// **Retired divergence:** the dim used to be asserted as "darker than 0.5". The reference's exact
|
||||
/// value is 0.5 — `PaperDollItemSlotButton_UpdateLock` calls
|
||||
/// `SetItemButtonDesaturated(this, 1, 0.5, 0.5, 0.5)` (`PaperDollFrame.lua:729-737`), and
|
||||
/// `ItemButtonTemplate.lua:61-82` both desaturates the icon and vertex-colours it with exactly
|
||||
/// those numbers. So the assertion is the pair, `desaturated` included.
|
||||
#[test]
|
||||
fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
s.set_inventory_slots(inventory_with_head_item());
|
||||
@@ -455,8 +514,13 @@ fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() {
|
||||
assert!(!s.eval::<bool>("return IsInventoryItemLocked(1)").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
|
||||
s.run(r#"BenillaPaperDollSlot_OnClick(CharacterHeadSlot, "LeftButton")"#)
|
||||
.unwrap();
|
||||
super::test_ui::click(&mut s, "CharacterHeadSlot", "LeftButton");
|
||||
// The repaint is one frame behind the pickup, and that is the reference's shape, not a
|
||||
// convenience: `PickupInventoryItem` locks the slot and QUEUES `ITEM_LOCK_CHANGED(-1, id)`
|
||||
// (`benilla-ui/src/script/cursor/doll.rs:78`, the one transition seam of decision 0216), and
|
||||
// that event is what runs `PaperDollItemSlotButton_UpdateLock` (stock
|
||||
// `PaperDollFrame.lua:601-604`). The app ticks every frame; a test has to say so.
|
||||
s.tick(0.0);
|
||||
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
|
||||
|
||||
assert!(s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
@@ -468,63 +532,91 @@ fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() {
|
||||
s.eval::<bool>("return IsInventoryItemLocked(1)").unwrap(),
|
||||
"the picked slot locks"
|
||||
);
|
||||
// The icon vertex-dims (the bag slots' own convention, this file's own Update comment).
|
||||
// The icon desaturates and vertex-dims to the reference's own 0.5 triple.
|
||||
s.resolve();
|
||||
let head_icon_dim = s.extract().iter().any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), color: Some(c), .. }
|
||||
if p.contains("INV_Helmet_01") && c[0] < 0.5)
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), color: Some(c), desaturated, .. }
|
||||
if p.contains("INV_Helmet_01") && *desaturated && c[0] == 0.5 && c[1] == 0.5 && c[2] == 0.5)
|
||||
});
|
||||
assert!(head_icon_dim, "the picked slot's icon dims");
|
||||
|
||||
// Clicking the SAME slot again cancels — mirrors the bag's own same-slot-cancel contract.
|
||||
s.run(r#"BenillaPaperDollSlot_OnClick(CharacterHeadSlot, "LeftButton")"#)
|
||||
.unwrap();
|
||||
super::test_ui::click(&mut s, "CharacterHeadSlot", "LeftButton");
|
||||
s.tick(0.0);
|
||||
assert!(!s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(!s.eval::<bool>("return IsInventoryItemLocked(1)").unwrap());
|
||||
s.resolve();
|
||||
let head_icon_lit = s.extract().iter().any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), color: Some(c), desaturated, .. }
|
||||
if p.contains("INV_Helmet_01") && !*desaturated && c[0] == 1.0)
|
||||
});
|
||||
assert!(head_icon_lit, "cancelling clears the dim");
|
||||
}
|
||||
|
||||
/// Decision 0208 phase 1b's `CURSOR_UPDATE` highlight: holding a BAG item that fits ONLY the
|
||||
/// head slot locks the head slot's highlight and leaves every other slot unlocked (the neck slot
|
||||
/// checked here) — the wiring test for `BenillaPaperDollSlot_LockHighlight`/`_UnlockHighlight`
|
||||
/// (a Lua-level spy, since no `GetVertexColor` getter exists to read the emulated tint back —
|
||||
/// this file's own header comment on the emulation).
|
||||
/// head slot lights the head slot's highlight ring and leaves every other slot dark (the neck slot
|
||||
/// checked here). Stock `PaperDollItemSlotButton_OnEvent`'s `CURSOR_UPDATE` arm is
|
||||
/// `if ( CursorCanGoInSlot(this:GetID()) ) then this:LockHighlight() else this:UnlockHighlight()`
|
||||
/// (`PaperDollFrame.lua:609-616`).
|
||||
///
|
||||
/// **Retired divergence (decision 1751's character swap).** This used to spy on a pair of our own
|
||||
/// `BenillaPaperDollSlot_LockHighlight`/`_UnlockHighlight` wrappers, which vertex-tinted the
|
||||
/// Quickslot2 ring because the engine had no `LockHighlight`. It has both verbs now
|
||||
/// (`benilla-ui/src/script/button.rs`), the stock file calls them, and the emulation is gone with
|
||||
/// the file that held it — so the assertion is the real locked-highlight state, read where a player
|
||||
/// reads it: `ItemButtonTemplate`'s own `HighlightTexture`
|
||||
/// (`Interface\Buttons\ButtonHilight-Square`, `ItemButtonTemplate.xml:44`) is emitted for a locked
|
||||
/// button and not for an unlocked, unhovered one (`ButtonState::region_visible`). The mouse is
|
||||
/// parked clear of the doll first so nothing is hovered and the lock is the only thing that can
|
||||
/// light a ring.
|
||||
#[test]
|
||||
fn cursor_update_highlights_fitting_doll_slots_while_holding_a_bag_item() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
// The window must be SHOWN for extract() to emit the slots' quads at all.
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
super::test_ui::unhover(&mut s);
|
||||
s.resolve();
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
lockHighlighted, unlockHighlighted = {}, {}
|
||||
local origLock, origUnlock = BenillaPaperDollSlot_LockHighlight, BenillaPaperDollSlot_UnlockHighlight
|
||||
BenillaPaperDollSlot_LockHighlight = function(b) lockHighlighted[b:GetName()] = true; origLock(b) end
|
||||
BenillaPaperDollSlot_UnlockHighlight = function(b) unlockHighlighted[b:GetName()] = true; origUnlock(b) end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
/// Is `slot`'s highlight ring in the render list? (`None` for "no such owner drew anything".)
|
||||
fn ring_lit(s: &mut UiScript, slot: &str) -> bool {
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
quads.iter().any(|q| {
|
||||
s.quad_owner_name(q.target).as_deref() == Some(slot)
|
||||
&& matches!(&q.content,
|
||||
QuadContent::Texture { path: Some(p), .. } if p.contains("ButtonHilight-Square"))
|
||||
})
|
||||
}
|
||||
|
||||
assert!(
|
||||
!ring_lit(&mut s, "CharacterHeadSlot"),
|
||||
"nothing is held yet, so no ring is lit"
|
||||
);
|
||||
|
||||
s.set_container(0, Some(backpack_with_fitting_helm()));
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
s.tick(0.0); // dispatches the queued CURSOR_UPDATE to every registered doll slot
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
|
||||
// The engine's own predicate agrees with the fixture's `equip_slots: vec![1]` — the input the
|
||||
// reference's arm branches on, pinned beside the effect so a wrong answer here can't read as a
|
||||
// wiring failure there.
|
||||
assert!(s.eval::<bool>("return CursorCanGoInSlot(1)").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorCanGoInSlot(2)").unwrap());
|
||||
|
||||
assert!(
|
||||
s.eval::<bool>("return lockHighlighted['CharacterHeadSlot'] == true")
|
||||
.unwrap(),
|
||||
ring_lit(&mut s, "CharacterHeadSlot"),
|
||||
"the fitting slot locks its highlight"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return unlockHighlighted['CharacterNeckSlot'] == true")
|
||||
.unwrap(),
|
||||
!ring_lit(&mut s, "CharacterNeckSlot"),
|
||||
"a non-fitting slot stays unhighlighted"
|
||||
);
|
||||
}
|
||||
@@ -537,11 +629,9 @@ fn model_pane_click_auto_equips_a_held_bag_item() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
@@ -567,19 +657,33 @@ fn model_pane_click_auto_equips_a_held_bag_item() {
|
||||
assert_eq!(s.take_container_autoequips(), vec![(0, 1)]);
|
||||
}
|
||||
|
||||
/// A BROKEN equipped item (durability 0) paints its doll slot red — the icon AND the slot ring,
|
||||
/// both the ref's 0.9,0,0 (PaperDollItemSlotButton_Update l.670-676; director-directed with the
|
||||
/// armor guy). Repairing it restores both to white.
|
||||
/// A BROKEN equipped item (durability 0) paints its doll slot's **ring** red — the reference's own
|
||||
/// 0.9,0,0 on that slot's `NormalTexture` (`Interface\Buttons\UI-Quickslot2`,
|
||||
/// `ItemButtonTemplate.xml:31`). Repairing it restores the ring to white, and no neighbouring slot
|
||||
/// is ever tinted. Director-directed with the armor guy.
|
||||
///
|
||||
/// **Retired divergence (decision 1751's character swap): the ICON does not go red, and in the
|
||||
/// reference it never did.** Stock `PaperDollItemSlotButton_Update` really does call BOTH setters
|
||||
/// on a broken item — `SetItemButtonTextureVertexColor(this, 0.9, 0, 0)` and
|
||||
/// `SetItemButtonNormalTextureVertexColor(this, 0.9, 0, 0)` (`PaperDollFrame.lua:670-672`) — and
|
||||
/// `ItemButtonTemplate.lua` paints `$parentIconTexture` from the first and `$parentNormalTexture`
|
||||
/// from the second (`:53-59` / `:84-90`). But the same `Update` ends with
|
||||
/// `PaperDollItemSlotButton_UpdateLock()` (`:714`), whose unlocked arm is
|
||||
/// `SetItemButtonDesaturated(this, nil)` (`:735`), and that function's `if ( not desaturated )`
|
||||
/// branch overwrites `r,g,b` with 1,1,1 and re-`SetVertexColor`s **the icon**
|
||||
/// (`ItemButtonTemplate.lua:71-81`). Forty-four lines later in the same call, the icon's red is
|
||||
/// gone; only the ring survives. Our deleted `assets/ui/CharacterFrame.xml` painted both and made
|
||||
/// them stick, which is why this test asserted the icon — that half is the transcription's, not the
|
||||
/// reference's, and it goes with the file. Verified on a real run: the head slot's `INV_Helmet_01`
|
||||
/// quad extracts white while its `UI-Quickslot2` quad extracts [0.9, 0, 0, 1].
|
||||
#[test]
|
||||
fn broken_equipped_item_tints_its_doll_slot_red() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
|
||||
@@ -593,32 +697,42 @@ fn broken_equipped_item_tints_its_doll_slot_red() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "update errors: {:?}", s.errors());
|
||||
s.resolve();
|
||||
let color_of = |s: &mut UiScript, needle: &str| {
|
||||
s.extract().iter().find_map(|q| match &q.content {
|
||||
/// The colour of `slot`'s own quad whose texture path contains `needle` — asked per OWNER, so
|
||||
/// the nineteen slots that share the ring art can be told apart.
|
||||
fn slot_color(s: &mut UiScript, slot: &str, needle: &str) -> Option<[f32; 4]> {
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
quads.iter().find_map(|q| match &q.content {
|
||||
QuadContent::Texture {
|
||||
path: Some(p),
|
||||
color,
|
||||
..
|
||||
} if p.contains(needle) => Some(color.unwrap_or([1.0, 1.0, 1.0, 1.0])),
|
||||
} if p.contains(needle) && s.quad_owner_name(q.target).as_deref() == Some(slot) => {
|
||||
Some(color.unwrap_or([1.0, 1.0, 1.0, 1.0]))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
}
|
||||
let red = [0.9, 0.0, 0.0, 1.0];
|
||||
let white = [1.0, 1.0, 1.0, 1.0];
|
||||
assert_eq!(
|
||||
color_of(&mut s, "INV_Helmet_01"),
|
||||
slot_color(&mut s, "CharacterHeadSlot", "Quickslot2"),
|
||||
Some(red),
|
||||
"the broken helm's icon paints red"
|
||||
"the broken slot's ring paints red"
|
||||
);
|
||||
assert_eq!(
|
||||
slot_color(&mut s, "CharacterNeckSlot", "Quickslot2"),
|
||||
Some(white),
|
||||
"…and only that slot's — its neighbours share the art and rest white"
|
||||
);
|
||||
// The icon stays white: see the retired divergence in this test's doc comment.
|
||||
assert_eq!(
|
||||
slot_color(&mut s, "CharacterHeadSlot", "INV_Helmet_01"),
|
||||
Some(white),
|
||||
"the icon's red is overwritten by UpdateLock's SetItemButtonDesaturated(this, nil)"
|
||||
);
|
||||
// The head slot's ring (its NormalTexture Quickslot2 art) paints red too. Other doll slots
|
||||
// share the art but rest white — assert the red one exists among them.
|
||||
let ring_red = s.extract().iter().any(|q| {
|
||||
matches!(&q.content,
|
||||
QuadContent::Texture { path: Some(p), color: Some(c), .. }
|
||||
if p.contains("Quickslot2") && (c[0] - 0.9).abs() < 1e-5 && c[1] == 0.0)
|
||||
});
|
||||
assert!(ring_red, "the broken slot's ring paints red");
|
||||
|
||||
// Repaired: both restore to white.
|
||||
// Repaired: the ring restores to white.
|
||||
let mut inv = inventory_with_head_item();
|
||||
inv[1].as_mut().unwrap().durability = Some((40, 40));
|
||||
s.set_inventory_slots(inv);
|
||||
@@ -628,16 +742,10 @@ fn broken_equipped_item_tints_its_doll_slot_red() {
|
||||
);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
color_of(&mut s, "INV_Helmet_01"),
|
||||
Some([1.0, 1.0, 1.0, 1.0]),
|
||||
"repair restores the icon"
|
||||
slot_color(&mut s, "CharacterHeadSlot", "Quickslot2"),
|
||||
Some(white),
|
||||
"repair restores the ring"
|
||||
);
|
||||
let ring_red = s.extract().iter().any(|q| {
|
||||
matches!(&q.content,
|
||||
QuadContent::Texture { path: Some(p), color: Some(c), .. }
|
||||
if p.contains("Quickslot2") && (c[0] - 0.9).abs() < 1e-5 && c[1] == 0.0)
|
||||
});
|
||||
assert!(!ring_red, "repair restores the ring");
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -654,17 +762,16 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
// Not optional, though this list ran without it: a MISSING template is a loader *warning*, not
|
||||
// an error, so an under-loaded list passes load_xml's assert and then fails later on geometry
|
||||
// that silently never got built. SkillDetailScrollFrame inherits UIPanelScrollFrameTemplate.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
load_xml(&s, "SkillFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
// `ScrollTemplates.xml` and `SkillFrame.xml` used to be loaded again here, because the old
|
||||
// per-file preamble did not carry them and a MISSING template is a loader *warning*, not an
|
||||
// error (`SkillDetailScrollFrame` inherits `UIPanelScrollFrameTemplate`, so it would have
|
||||
// loaded clean with no trough and failed later on geometry that silently never got built).
|
||||
// `CHARACTER_UI` carries both now — `CharacterFrame_ShowSubFrame` hides all five pages by name
|
||||
// on the first `ToggleCharacter` — and `load_ui_strict` fails on the warning, so a second load
|
||||
// would only re-declare frames the list already built.
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_skills(SkillsState {
|
||||
entries: vec![
|
||||
@@ -744,8 +851,8 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
|
||||
assert!(shown(&mut s, "PaperDollFrame"), "opens on the doll page");
|
||||
|
||||
// Tab to Skills — by point, the director's gesture.
|
||||
let tab2 = text_center(&mut s, "Skills");
|
||||
click(&mut s, tab2);
|
||||
let skills_tab = text_center(&mut s, "Skills");
|
||||
click(&mut s, skills_tab);
|
||||
assert!(shown(&mut s, "SkillFrame"), "Skills tab shows the page");
|
||||
assert!(!shown(&mut s, "PaperDollFrame"), "doll page yields");
|
||||
|
||||
@@ -808,13 +915,13 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
|
||||
click(&mut s, row);
|
||||
|
||||
// Tab BACK to Character — the reported failure.
|
||||
let tab1 = text_center(&mut s, "Character");
|
||||
let character_tab = text_center(&mut s, "Character");
|
||||
assert_eq!(
|
||||
s.hit_test_name(tab1.0, tab1.1).as_deref(),
|
||||
s.hit_test_name(character_tab.0, character_tab.1).as_deref(),
|
||||
Some("CharacterFrameTab1"),
|
||||
"the Character tab OWNS its point while Skills is up (the wheel catcher must not)"
|
||||
);
|
||||
click(&mut s, tab1);
|
||||
click(&mut s, character_tab);
|
||||
assert!(
|
||||
shown(&mut s, "PaperDollFrame"),
|
||||
"the Character tab switches back (the 2026-07-17 report)"
|
||||
@@ -822,11 +929,11 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
|
||||
assert!(!shown(&mut s, "SkillFrame"), "Skills page yields");
|
||||
|
||||
// Once more around — "flaky" only shows up on repetition.
|
||||
let tab2 = text_center(&mut s, "Skills");
|
||||
click(&mut s, tab2);
|
||||
let skills_tab = text_center(&mut s, "Skills");
|
||||
click(&mut s, skills_tab);
|
||||
assert!(shown(&mut s, "SkillFrame"), "second trip to Skills");
|
||||
let tab1 = text_center(&mut s, "Character");
|
||||
click(&mut s, tab1);
|
||||
let character_tab = text_center(&mut s, "Character");
|
||||
click(&mut s, character_tab);
|
||||
assert!(shown(&mut s, "PaperDollFrame"), "second trip back");
|
||||
|
||||
assert!(
|
||||
@@ -853,13 +960,9 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
// ROTATIONS_PER_SECOND lives here (the ref's UIParent.lua:2), like it does in the real client.
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
@@ -878,7 +981,7 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
|
||||
// Press and HOLD. The press edge is itself a click (−0.03 off the 0.61 default).
|
||||
s.mouse_move(bx, by);
|
||||
s.mouse_button(bx, by, "LeftButton", true);
|
||||
let pressed = s.paperdoll_yaw();
|
||||
let pressed = s.model_pane_facing("CharacterModelFrame");
|
||||
assert!(
|
||||
(pressed - 0.58).abs() < 1e-4,
|
||||
"the press edge nudges once: {pressed}"
|
||||
@@ -887,7 +990,7 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
|
||||
// Held for half a second: half of half a turn = +π/2 (the held-LEFT branch ADDS).
|
||||
s.tick(0.5);
|
||||
assert!(s.errors().is_empty(), "OnUpdate errors: {:?}", s.errors());
|
||||
let spun = s.paperdoll_yaw();
|
||||
let spun = s.model_pane_facing("CharacterModelFrame");
|
||||
assert!(
|
||||
(spun - (pressed + std::f32::consts::FRAC_PI_2)).abs() < 1e-3,
|
||||
"half a second held spins half of half a turn: {pressed} → {spun}"
|
||||
@@ -895,14 +998,14 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
|
||||
|
||||
// Release: the second click edge (another −0.03), and the spin stops dead.
|
||||
s.mouse_button(bx, by, "LeftButton", false);
|
||||
let released = s.paperdoll_yaw();
|
||||
let released = s.model_pane_facing("CharacterModelFrame");
|
||||
assert!(
|
||||
(released - (spun - 0.03)).abs() < 1e-4,
|
||||
"the release edge nudges again: {spun} → {released}"
|
||||
);
|
||||
s.tick(0.5);
|
||||
assert!(
|
||||
(s.paperdoll_yaw() - released).abs() < 1e-6,
|
||||
(s.model_pane_facing("CharacterModelFrame") - released).abs() < 1e-6,
|
||||
"a released button does not keep spinning"
|
||||
);
|
||||
|
||||
@@ -917,9 +1020,9 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
|
||||
};
|
||||
s.mouse_move(rx, ry);
|
||||
s.mouse_button(rx, ry, "LeftButton", true);
|
||||
let before = s.paperdoll_yaw();
|
||||
let before = s.model_pane_facing("CharacterModelFrame");
|
||||
s.tick(0.25);
|
||||
let after = s.paperdoll_yaw();
|
||||
let after = s.model_pane_facing("CharacterModelFrame");
|
||||
assert!(
|
||||
(after - (before - std::f32::consts::FRAC_PI_4)).abs() < 1e-3,
|
||||
"the right arrow spins the other way: {before} → {after}"
|
||||
@@ -947,39 +1050,60 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "OptionsFrameTemplates.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
load_xml(&s, "PetPaperDollFrame.xml");
|
||||
load_xml(&s, "ReputationFrame.xml");
|
||||
load_xml(&s, "SkillFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
|
||||
// THE INVARIANT the fix rests on: a page's `id=` is its slot in this window's own tab row, and
|
||||
// `BENILLA_CHARACTERFRAME_SUBFRAMES` is the same 1:1 mapping written the other way round. It
|
||||
// did its job: this loop is what made the Reputation page's arrival LOUD, since Skills had to
|
||||
// move from 3 to 4 in the same breath. All five slots are the reference's own now
|
||||
// (Character/Pet/Reputation/Skills/Honor, ref `CharacterFrame.xml:79-168`), so the loop covers
|
||||
// the whole row rather than a prefix of it — there is nothing left past the end for a page to
|
||||
// arrive into unnoticed.
|
||||
for i in 1..=5 {
|
||||
let id: i64 = s
|
||||
.eval(&format!(
|
||||
"return getglobal(BENILLA_CHARACTERFRAME_SUBFRAMES[{i}]):GetID()"
|
||||
))
|
||||
.unwrap();
|
||||
// THE INVARIANT the fix rests on: a page's `id=` is the slot in this window's tab row that the
|
||||
// tab row's own dispatch sends to that page. `ToggleCharacter` selects the row with
|
||||
// `PanelTemplates_SetTab(CharacterFrame, subFrame:GetID())` (stock `CharacterFrame.lua:10`), so
|
||||
// if a page's id and its tab's id ever disagree, a keybind lights the wrong tab.
|
||||
//
|
||||
// **Retired divergence (decision 1751's character swap).** This used to loop over
|
||||
// `BENILLA_CHARACTERFRAME_SUBFRAMES` asserting `page i must carry id=i`, because our deleted
|
||||
// `assets/ui/CharacterFrame.xml` listed the pages in tab order. The reference does not:
|
||||
// `CHARACTERFRAME_SUBFRAMES` is `{PaperDoll, PetPaperDoll, Skill, Reputation, Honor}`
|
||||
// (`CharacterFrame.lua:1`) while the tab row is Character/Pet/**Reputation**/**Skills**/Honor
|
||||
// (`CharacterFrame.xml:79-168`) — index 3 is SkillFrame with id 4, index 4 is ReputationFrame
|
||||
// with id 3. That list is a set to iterate for show/hide, not an ordering, so the ordering
|
||||
// assertion moves to where the reference really states it: `CharacterFrameTab_OnClick`'s five
|
||||
// name branches (`CharacterFrame.lua:35-48`).
|
||||
let row: [(i64, &str); 5] = [
|
||||
(1, "PaperDollFrame"),
|
||||
(2, "PetPaperDollFrame"),
|
||||
(3, "ReputationFrame"),
|
||||
(4, "SkillFrame"),
|
||||
(5, "HonorFrame"),
|
||||
];
|
||||
for (id, page) in row {
|
||||
assert_eq!(
|
||||
id, i,
|
||||
"page {i} of BENILLA_CHARACTERFRAME_SUBFRAMES must carry id={i}"
|
||||
s.eval::<i64>(&format!("return {page}:GetID()")).unwrap(),
|
||||
id,
|
||||
"{page} is the page CharacterFrameTab{id} toggles, so it must carry id={id}"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(&format!("return CharacterFrameTab{id}:GetID()"))
|
||||
.unwrap(),
|
||||
id,
|
||||
);
|
||||
// …and it is one of the five the show/hide sweep knows about, so a page cannot arrive with
|
||||
// a tab and stay invisible to `CharacterFrame_ShowSubFrame` (`CharacterFrame.lua:25-33`).
|
||||
assert!(
|
||||
s.eval::<bool>(&format!(
|
||||
"for _, v in CHARACTERFRAME_SUBFRAMES do if v == \"{page}\" then return true end \
|
||||
end return false"
|
||||
))
|
||||
.unwrap(),
|
||||
"{page} must be in CHARACTERFRAME_SUBFRAMES"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return getn(CHARACTERFRAME_SUBFRAMES)")
|
||||
.unwrap(),
|
||||
5,
|
||||
"…and there is nothing past the end for a sixth page to arrive into unnoticed"
|
||||
);
|
||||
|
||||
let selected = |s: &mut UiScript| {
|
||||
s.eval::<i64>("return PanelTemplates_GetSelectedTab(CharacterFrame)")
|
||||
@@ -1025,6 +1149,22 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
|
||||
// Character tab is inert while the Character page is up. The window only closed because the row
|
||||
// still said Skills: tab 1 was left enabled, the click reached `ToggleCharacter`, and the page
|
||||
// it named was already visible — the HideUIPanel arm.
|
||||
//
|
||||
// The MECHANISM is asserted first and the click second, deliberately: a tab whose OnClick never
|
||||
// ran would sail through the click assertions below for the wrong reason, and this is the one
|
||||
// fact that cannot.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CharacterFrameTab1:IsEnabled()")
|
||||
.unwrap(),
|
||||
0,
|
||||
"the selected tab is DISABLED — that is what makes the click below inert"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CharacterFrameTab4:IsEnabled()")
|
||||
.unwrap(),
|
||||
1,
|
||||
"…while an unselected one stays clickable"
|
||||
);
|
||||
s.run("CharacterFrameTab1:Click()").unwrap();
|
||||
assert!(
|
||||
shown(&mut s, "CharacterFrame"),
|
||||
@@ -1041,14 +1181,13 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
|
||||
/// rest; this client had every other member of the kit and not that one, because our own windows
|
||||
/// wire their tabs to their own handlers and never reached for the generic entry point.
|
||||
///
|
||||
/// **It is built on a row of this test's own, and that is the finding, not a convenience.** The
|
||||
/// reference's tab buttons carry `id="1".."4"` (ref `CharacterFrame.xml:79-133`) and ours carry
|
||||
/// none: our row is id-based through `BenillaCharacterFrameTab_OnClick(id)`, which closes over the
|
||||
/// number instead of reading it off the widget. So `CharacterFrameTab2:GetID()` is **0** here, and
|
||||
/// driving OUR row through the generic entry point would select tab 0 — correct code meeting a
|
||||
/// window that does not obey the contract it reads. An addon's own tabs do carry ids, which is why
|
||||
/// the four callers above work; this builds a row that obeys the reference's contract and drives
|
||||
/// that.
|
||||
/// **It is built on a row of this test's own, and that is deliberate.** When it was written our
|
||||
/// character row carried no `id=` at all (it was id-based through a `BenillaCharacterFrameTab_OnClick(id)`
|
||||
/// closure), so driving it through the generic entry point would have selected tab 0. Decision
|
||||
/// 1751's swap fixed that from the other end — the row is the reference's own now and every tab
|
||||
/// carries its id (`CharacterFrame.xml:79-168`) — but the standalone row stays: this test is about
|
||||
/// the KIT, and an addon's window is exactly what it builds, so it must not need a shipped window
|
||||
/// loaded to run.
|
||||
///
|
||||
/// The `this`/frame split is the whole reason the function exists: `this` is the clicked TAB and
|
||||
/// the owning FRAME is a separate argument, which is the shape `SimpleActionSets.xml:342` relies on
|
||||
|
||||
@@ -12,15 +12,20 @@
|
||||
//! ignoreModifiers)` — which takes the MOUSE button as its first argument and reads the frame from
|
||||
//! `this`, so a test cannot call it directly any more. These drive the mouse instead
|
||||
//! ([`super::test_ui::click`]), which is the stronger test regardless: it puts the template's own
|
||||
//! `RegisterForClicks("LeftButtonUp", "RightButtonUp")` and script wiring under test too. The doll
|
||||
//! slots below are still ours (`BenillaPaperDollSlot_OnClick`) and are still called directly.
|
||||
//! `RegisterForClicks("LeftButtonUp", "RightButtonUp")` and script wiring under test too.
|
||||
//!
|
||||
//! **The doll slots went the same way.** `CharacterFrame.xml` and `PaperDollFrame.xml` are the
|
||||
//! reference's own too now, so `BenillaPaperDollSlot_OnClick` is gone and the fork is stock
|
||||
//! `PaperDollItemSlotButton_OnClick(button, ignoreModifiers)` (`PaperDollFrame.lua:647-662`) —
|
||||
//! which reads `this` exactly as the bag handler does. Those tests drive the mouse as well, over a
|
||||
//! window that is genuinely OPEN, which is the only state a player can click a doll slot from.
|
||||
|
||||
use benilla_ui::script::{
|
||||
ContainerSlot, ContainerState, DressUpIntent, InvSlotView, InventorySlots, SoundRequest,
|
||||
UiScript,
|
||||
UiScript, UnitState,
|
||||
};
|
||||
|
||||
use super::test_ui::{bag_slot_button, click, load_ui as load_xml, BAG_UI};
|
||||
use super::test_ui::{bag_slot_button, click, load_ui as load_xml, BAG_UI, CHARACTER_UI};
|
||||
|
||||
/// The jerky stack every bag fixture here uses — a real 1.12 link, quality white.
|
||||
const JERKY_LINK: &str = "|cffffffff|Hitem:117|h[Tough Jerky]|h|r";
|
||||
@@ -75,6 +80,41 @@ fn load_room_with_bags(s: &UiScript) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The character window **open on its paper doll**, with the room and a chat edit box beside it —
|
||||
/// the two files [`CHARACTER_UI`] does not already carry. (`UIDropDownMenu.xml`,
|
||||
/// `Interface\FrameXML\UIMenu.xml` and `UnitPopup.xml` are in both lists; loading either twice
|
||||
/// would redeclare its frames.)
|
||||
///
|
||||
/// The player behind it carries **both** halves of the race and class pairs: `UnitRace`/`UnitClass`
|
||||
/// answer `(localized, file)` or `nil, nil` — the binding `zip`s them — and stock
|
||||
/// `PaperDollFrame_SetLevel` formats all three into `CharacterLevelText` unguarded
|
||||
/// (`PaperDollFrame.lua:100-104`) on every show.
|
||||
///
|
||||
/// **Needs client data**, like [`load_room_with_bags`].
|
||||
fn shown_paper_doll() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, file);
|
||||
}
|
||||
for file in ["DressUpFrame.xml", "ChatFrame.xml"] {
|
||||
super::test_ui::load_ui_strict(&s, file);
|
||||
}
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
level: 60,
|
||||
race: Some("Human".into()),
|
||||
race_file: Some("Human".into()),
|
||||
class: Some("Warrior".into()),
|
||||
class_file: Some("WARRIOR".into()),
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
/// A backpack holding a 5-stack of Tough Jerky in slot 1, opened; returns the NAME of the button
|
||||
/// showing that slot.
|
||||
///
|
||||
@@ -230,26 +270,17 @@ fn ctrl_clicking_a_chat_link_previews_it() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The paper doll's own slots (ref PaperDollFrame.lua:647-655): ctrl previews what you are
|
||||
/// wearing, shift posts its link. Both read the unit-keyed `GetInventoryItemLink`, the binding this
|
||||
/// arc added — so this also pins that the getter answers for `"player"`.
|
||||
/// The paper doll's own slots (`PaperDollFrame.lua:647-662`): ctrl previews what you are wearing,
|
||||
/// shift posts its link. Both read the unit-keyed `GetInventoryItemLink`, the binding this arc
|
||||
/// added — so this also pins that the getter answers for `"player"`.
|
||||
///
|
||||
/// Since 1751 that handler is the reference's own `PaperDollItemSlotButton_OnClick(button,
|
||||
/// ignoreModifiers)`, which reads the frame from `this` — so this drives the mouse over a window
|
||||
/// that is really open, exactly as the bag tests above do.
|
||||
#[test]
|
||||
fn the_paper_doll_slots_preview_and_post_what_you_wear() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"CharacterFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_paper_doll();
|
||||
let mut slots = InventorySlots::default();
|
||||
slots[1] = Some(InvSlotView {
|
||||
item_id: 1234,
|
||||
@@ -267,11 +298,13 @@ fn the_paper_doll_slots_preview_and_post_what_you_wear() {
|
||||
.unwrap(),
|
||||
"|cff1eff00|Hitem:1234:0:0:0|h[Test Helm]|h|r"
|
||||
);
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
let _ = s.take_dressup_intents();
|
||||
s.resolve();
|
||||
|
||||
// CTRL on the head slot → the room, wearing the helm.
|
||||
s.set_modifiers(false, true, false);
|
||||
s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
|
||||
.unwrap();
|
||||
click(&mut s, "CharacterHeadSlot", "LeftButton");
|
||||
s.set_modifiers(false, false, false);
|
||||
assert_eq!(
|
||||
s.take_dressup_intents(),
|
||||
@@ -281,8 +314,7 @@ fn the_paper_doll_slots_preview_and_post_what_you_wear() {
|
||||
// SHIFT with chat open → the link; the item is never picked up either way.
|
||||
assert!(s.focus_editbox("ChatFrameEditBox"));
|
||||
s.set_modifiers(true, false, false);
|
||||
s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
|
||||
.unwrap();
|
||||
click(&mut s, "CharacterHeadSlot", "LeftButton");
|
||||
s.set_modifiers(false, false, false);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||||
@@ -297,26 +329,17 @@ fn the_paper_doll_slots_preview_and_post_what_you_wear() {
|
||||
}
|
||||
|
||||
/// A slot whose item template has not answered yet has **no link** — a state the real client never
|
||||
/// has (its item cache is synchronous), so the reference carries no guard and `EditBox:Insert`
|
||||
/// takes a string. Shift-clicking such a slot must post nothing and raise nothing; the click after
|
||||
/// the answer lands works normally.
|
||||
/// has (its item cache is synchronous), so the reference carries no guard: stock
|
||||
/// `PaperDollItemSlotButton_OnClick` shift-inserts `GetInventoryItemLink(...)` straight
|
||||
/// (`PaperDollFrame.lua:653`). Shift-clicking such a slot must post nothing and raise nothing.
|
||||
///
|
||||
/// That is now the ENGINE's promise rather than a transcription's guard: `EditBox:Insert(nil)` is a
|
||||
/// no-op because the reference reads the argument through `lua_tostring` (decision 1800), and this
|
||||
/// test is what holds the stock line above harmless over an in-flight item.
|
||||
#[test]
|
||||
fn shift_clicking_an_unresolved_slot_posts_nothing_and_never_raises() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"CharacterFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_paper_doll();
|
||||
// The slot is occupied but unresolved: an item id with no name/quality yet, so no link.
|
||||
let mut slots = InventorySlots::default();
|
||||
slots[1] = Some(InvSlotView {
|
||||
@@ -325,11 +348,13 @@ fn shift_clicking_an_unresolved_slot_posts_nothing_and_never_raises() {
|
||||
..Default::default()
|
||||
});
|
||||
s.set_inventory_slots(slots);
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
let _ = s.take_dressup_intents();
|
||||
s.resolve();
|
||||
assert!(s.focus_editbox("ChatFrameEditBox"));
|
||||
|
||||
s.set_modifiers(true, false, false);
|
||||
s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
|
||||
.unwrap();
|
||||
click(&mut s, "CharacterHeadSlot", "LeftButton");
|
||||
s.set_modifiers(false, false, false);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||||
@@ -341,8 +366,7 @@ fn shift_clicking_an_unresolved_slot_posts_nothing_and_never_raises() {
|
||||
|
||||
// And ctrl on the same unresolved slot is inert too (DressUpItemLink's own nil guard).
|
||||
s.set_modifiers(false, true, false);
|
||||
s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
|
||||
.unwrap();
|
||||
click(&mut s, "CharacterHeadSlot", "LeftButton");
|
||||
s.set_modifiers(false, false, false);
|
||||
assert!(
|
||||
s.take_dressup_intents().is_empty(),
|
||||
|
||||
@@ -119,6 +119,7 @@ fn content_kind(c: &QuadContent) -> &'static str {
|
||||
QuadContent::Frame => "frame",
|
||||
QuadContent::Minimap { .. } => "minimap",
|
||||
QuadContent::Cooldown { .. } => "cooldown",
|
||||
QuadContent::ModelPane { .. } => "modelpane",
|
||||
QuadContent::Texture { .. } => "texture",
|
||||
QuadContent::ColorWheel => "colorwheel",
|
||||
QuadContent::ColorValue { .. } => "colorvalue",
|
||||
@@ -1312,6 +1313,56 @@ fn convert_entry(
|
||||
rect, eq.z, eq.alpha, fraction, flash, clip, assets, images, out,
|
||||
);
|
||||
}
|
||||
// A `<Model>`/`<PlayerModel>` pane's content: the off-screen body bake its window keeps,
|
||||
// sampled square edge to edge. The pane→booth join is
|
||||
// [`crate::portrait::model_pane_booth`], whose doc says why it is a table of frame names.
|
||||
//
|
||||
// This is the same draw the `portrait_unit` arm below makes for a Texture region, and it
|
||||
// used to BE that draw: until decision 1751 a file of ours put a Texture inside each pane
|
||||
// and named the slot with `BenillaSetBoothTexture`. A migrated window runs the reference's
|
||||
// own file, which declares a bare `<PlayerModel>` and no Texture at all — so the widget
|
||||
// itself has to draw, or the character sheet's paper doll is an empty rectangle.
|
||||
//
|
||||
// A pane with no name, or one no window claims, draws nothing (`SetModel` panes included:
|
||||
// this engine holds their scene and renders no M2 into it). Nothing is stubbed white.
|
||||
QuadContent::ModelPane { name } => {
|
||||
use crate::portrait::PortraitSource;
|
||||
let Some(slot) = name.as_deref().and_then(crate::portrait::model_pane_booth) else {
|
||||
return;
|
||||
};
|
||||
// The aspect the bake must render at, and the fact that it is on screen at all
|
||||
// (decision 1069) — published before the readiness check below for the same reason the
|
||||
// `portrait_unit` arm does it there: a pane whose bake has not landed yet is still a
|
||||
// pane being drawn, and gating the publish on the image would be a standoff.
|
||||
if rect.height() > 0.0 {
|
||||
booths
|
||||
.panes
|
||||
.0
|
||||
.insert(slot.to_string(), rect.width() / rect.height());
|
||||
}
|
||||
// The bake is a render target and carries PREMULTIPLIED colour; the 2D stand-in the
|
||||
// slot shows while a model streams is an ordinary straight-alpha BLP.
|
||||
let (handle, premultiplied) = match booths.images.0.get(slot) {
|
||||
Some(PortraitSource::Live(h)) => (Some(h.clone()), true),
|
||||
Some(PortraitSource::File(p)) => (
|
||||
assets.as_mut().and_then(|a| a.sprite_texture(p, images)),
|
||||
false,
|
||||
),
|
||||
None => (None, false),
|
||||
};
|
||||
let Some(handle) = handle else {
|
||||
return;
|
||||
};
|
||||
out.push(UiQuad {
|
||||
rect,
|
||||
z_key: eq.z,
|
||||
texture: Some(handle),
|
||||
color: [1.0, 1.0, 1.0, eq.alpha],
|
||||
premultiplied,
|
||||
clip,
|
||||
..default()
|
||||
});
|
||||
}
|
||||
QuadContent::Texture {
|
||||
path,
|
||||
color,
|
||||
@@ -2199,6 +2250,72 @@ mod extract_gate_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// **A `<PlayerModel>` pane draws its window's booth, and an unclaimed one draws nothing**
|
||||
/// (decision 1810).
|
||||
///
|
||||
/// The engine verb the stock character sheet needed. `PaperDollFrame.xml` declares a bare
|
||||
/// `<PlayerModel name="CharacterModelFrame">` with no texture in it at all — our own deleted
|
||||
/// file put a `<Texture>` there and named the booth with `BenillaSetBoothTexture` — so without
|
||||
/// this the migrated window's paper doll is an empty rectangle, which no loader error and no
|
||||
/// missing global would have said a word about.
|
||||
///
|
||||
/// Both halves are asserted because the second is what keeps the first honest: the join is a
|
||||
/// table of frame names ([`crate::portrait::model_pane_booth`]), and a pane no window claims —
|
||||
/// pfUI's anonymous `CreateFrame("Model")` autocast shine is the corpus case — must draw
|
||||
/// nothing rather than borrow somebody's bake.
|
||||
#[test]
|
||||
fn a_named_model_pane_samples_its_booth_and_an_unclaimed_one_draws_nothing() {
|
||||
let mut app = app_from_script(
|
||||
r#"
|
||||
local doll = CreateFrame("PlayerModel", "CharacterModelFrame")
|
||||
doll:SetPoint("TOPLEFT", 0, 0)
|
||||
doll:SetSize(233, 224)
|
||||
doll:SetUnit("player")
|
||||
local stray = CreateFrame("PlayerModel", "SomeAddonsModelPane")
|
||||
stray:SetPoint("TOPLEFT", 300, 0)
|
||||
stray:SetSize(233, 224)
|
||||
stray:SetUnit("player")
|
||||
"#,
|
||||
);
|
||||
let bake = app
|
||||
.world_mut()
|
||||
.resource_mut::<Assets<Image>>()
|
||||
.add(Image::default());
|
||||
app.world_mut().resource_mut::<PortraitImages>().0.insert(
|
||||
"paperdoll".to_string(),
|
||||
crate::portrait::PortraitSource::Live(bake.clone()),
|
||||
);
|
||||
app.update();
|
||||
|
||||
let quads = &app.world().resource::<UiQuads>().quads;
|
||||
let panes: Vec<_> = quads
|
||||
.iter()
|
||||
.filter(|q| q.texture.as_ref() == Some(&bake))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
panes.len(),
|
||||
1,
|
||||
"exactly the claimed pane draws the paper doll's bake"
|
||||
);
|
||||
assert!(
|
||||
panes[0].premultiplied,
|
||||
"a render-target bake carries premultiplied colour"
|
||||
);
|
||||
// …and the pane published its aspect, which is what lets the booth render at the shape it
|
||||
// will be stretched into (decision 1069) and what tells it it is on screen at all.
|
||||
let aspect = app
|
||||
.world()
|
||||
.resource::<crate::portrait::BoothPanes>()
|
||||
.0
|
||||
.get("paperdoll")
|
||||
.copied()
|
||||
.expect("the pane publishes its aspect");
|
||||
assert!(
|
||||
(aspect - 233.0 / 224.0).abs() < 0.01,
|
||||
"the pane's own rect is the aspect, got {aspect}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **A booth bake reaches the quad pass flagged PREMULTIPLIED, an ordinary region does not**
|
||||
/// (decision 1347). This is the wiring half of the paper-doll/dressing-room fix, and it is the
|
||||
/// half that can silently rot: the shader's `select(a, k, premultiplied)` is only correct if
|
||||
|
||||
@@ -169,13 +169,12 @@ const KNOWN: &[Known] = &[
|
||||
// we have not built, and the interaction the player would notice is the TOOLTIP those handlers
|
||||
// show — not the click-blocking the flag gives. Declaring `enableMouse="true"` here would make
|
||||
// the frame swallow the click and hand back nothing, which is worse than the gap.
|
||||
Known {
|
||||
frame: "PetPaperDollFrameExpBar",
|
||||
flag: Flag::Mouse,
|
||||
why:
|
||||
"the reference bar inherits TextStatusBar, whose OnEnter/OnLeave show the value text; \
|
||||
ours is a plain StatusBar. Wants the status-text arc, not the flag.",
|
||||
},
|
||||
// `PetPaperDollFrameExpBar` RETIRED here (decision 1751's character-sheet window). It read
|
||||
// "the reference bar inherits TextStatusBar, whose OnEnter/OnLeave show the value text; ours is
|
||||
// a plain StatusBar" — true of OUR `PetPaperDollFrame.xml`, which is deleted. The pet page is
|
||||
// the reference's own file now, so its XP bar inherits `TextStatusBar` because it IS the
|
||||
// reference's declaration, and the divergence has nothing left to describe.
|
||||
//
|
||||
// Seven `*ScrollFrame` mouse entries RETIRED here (1795). They read "our faux scroll pane is a
|
||||
// Frame with an explicit `<OnMouseWheel>`, not a ScrollFrame" — a divergence that existed only
|
||||
// because OUR `ScrollFrame` ctor took the mouse and the reference's does not. Correcting the
|
||||
@@ -275,14 +274,16 @@ const KNOWN: &[Known] = &[
|
||||
Known {
|
||||
frame: "TargetofTargetHealthBar",
|
||||
flag: Flag::Mouse,
|
||||
why: "same as PetPaperDollFrameExpBar — a plain StatusBar where the reference inherits \
|
||||
TextStatusBar.",
|
||||
why: "a plain StatusBar where the reference inherits TextStatusBar, whose OnEnter/OnLeave \
|
||||
show the value text. Wants the status-text arc, not the flag. (The pet page's XP bar \
|
||||
was this entry's twin until its window became the reference's own.)",
|
||||
},
|
||||
Known {
|
||||
frame: "TargetofTargetManaBar",
|
||||
flag: Flag::Mouse,
|
||||
why: "same as PetPaperDollFrameExpBar — a plain StatusBar where the reference inherits \
|
||||
TextStatusBar.",
|
||||
why: "a plain StatusBar where the reference inherits TextStatusBar, whose OnEnter/OnLeave \
|
||||
show the value text. Wants the status-text arc, not the flag. (The pet page's XP bar \
|
||||
was this entry's twin until its window became the reference's own.)",
|
||||
},
|
||||
Known {
|
||||
frame: "TradePlayerItem7",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The shipped **Honor tab** (`assets/ui/HonorFrame.xml`) driven end-to-end, engine-only (no
|
||||
//! Bevy) — the per-window test module the skills/reputation files establish (decision 1512).
|
||||
//! The shipped **Honor tab** (`Interface\\FrameXML\\HonorFrame.xml`, the reference's own) driven
|
||||
//! end-to-end, engine-only (no Bevy) — the per-window test module the skills/reputation files
|
||||
//! establish (decision 1512).
|
||||
//!
|
||||
//! What it pins is the PAINT law, the half `benilla-ui`'s own `script::pvp::tests` structurally
|
||||
//! cannot reach: that module drives the thirteen globals and asserts their tuples, and stops at the
|
||||
@@ -51,9 +52,13 @@ fn state() -> HonorState {
|
||||
}
|
||||
|
||||
/// The eight rank titles this fixture can ask for, defined as the install's `GlobalStrings.lua`
|
||||
/// defines them. Set by hand rather than read off the chain so the pane's law is pinned on a
|
||||
/// machine with no client data; [`the_real_global_strings_name_the_rank`] is the same assertion
|
||||
/// against the player's actual file.
|
||||
/// defines them — verbatim (`GlobalStrings.lua:2795`, `:3075`, `:3150-3161`, `:3297`).
|
||||
///
|
||||
/// **The inspect page's, since 1751.** The character page loads the player's own file as its first
|
||||
/// [`super::test_ui::CHARACTER_UI`] entry, because stock `PaperDollFrame_OnLoad` sets seven labels
|
||||
/// from it; [`shown_inspect_honor_page`] loads no such thing, so these are what its titles read.
|
||||
/// [`the_real_global_strings_name_the_rank`] is the same assertion against the player's actual
|
||||
/// file, on both sides.
|
||||
const RANK_GLOBALS: &str = r#"
|
||||
NONE = "None"
|
||||
RANK = "Rank"
|
||||
@@ -64,27 +69,27 @@ const RANK_GLOBALS: &str = r#"
|
||||
PVP_RANK_14_0 = "Champion"
|
||||
"#;
|
||||
|
||||
/// The character window, whose fifth tab this page is. Every file it needs — including
|
||||
/// `Interface\\FrameXML\\HonorFrame.xml` itself, which the shared list carries because
|
||||
/// `PaperDollFrame_SetLevel` writes `HonorLevelText` on every show
|
||||
/// (`PaperDollFrame.lua:103`, and `_SetGuild` writes `HonorGuildText` at `:123`) — is
|
||||
/// [`super::test_ui::CHARACTER_UI`]; this page adds nothing of its own. It was a hand-copied list until the window became the reference's (decision 1751), which
|
||||
/// is when the list stopped being short enough for a copy to stay honest.
|
||||
fn load_page(s: &UiScript) {
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"OptionsFrameTemplates.xml",
|
||||
"CharacterFrame.xml",
|
||||
"Interface\\FrameXML\\HonorFrame.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
for file in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(s, file);
|
||||
}
|
||||
}
|
||||
|
||||
/// An Alliance, male, level-60 player — the two facts the rank title's GlobalString key is built
|
||||
/// from (side → the team digit, sex → the `_FEMALE` twin), plus the level line's input.
|
||||
///
|
||||
/// **Both halves of the race and class pairs, since 1751.** `UnitRace`/`UnitClass` answer
|
||||
/// `(localized, file)` or `nil, nil` — the binding `zip`s the two, so a snapshot carrying only the
|
||||
/// localized name reports the unit as raceless. Stock `PaperDollFrame_SetLevel` writes
|
||||
/// `HonorLevelText` off `format(TEXT(PLAYER_LEVEL), UnitLevel, UnitRace, UnitClass)` — unguarded,
|
||||
/// on every show of this page (`PaperDollFrame.lua:100-104`) — so a half-filled snapshot raises
|
||||
/// there. Ours never called it from the honor tab, which is why the gap sat unseen.
|
||||
fn alliance_player() -> UnitState {
|
||||
UnitState {
|
||||
exists: true,
|
||||
@@ -92,7 +97,9 @@ fn alliance_player() -> UnitState {
|
||||
sex: 2,
|
||||
faction_group: Some("Alliance".into()),
|
||||
race: Some("Human".into()),
|
||||
race_file: Some("Human".into()),
|
||||
class: Some("Warrior".into()),
|
||||
class_file: Some("WARRIOR".into()),
|
||||
pvp_rank: 12,
|
||||
..UnitState::default()
|
||||
}
|
||||
@@ -103,7 +110,6 @@ fn alliance_player() -> UnitState {
|
||||
fn shown_honor_page() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(RANK_GLOBALS).unwrap();
|
||||
load_page(&s);
|
||||
s.set_unit("player", Some(alliance_player()));
|
||||
s.set_honor(Some(state()));
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
//! The shipped **pet paper doll** driven end-to-end, engine-only (decision 1057): the real
|
||||
//! `assets/ui/PetPaperDollFrame.xml` behind `CharacterFrame.xml`, fed synthetic pet snapshots.
|
||||
//! The shipped **pet paper doll** driven end-to-end, engine-only (decision 1057) — and since
|
||||
//! decision 1751's character swap, "shipped" is the reference's own
|
||||
//! `Interface\FrameXML\PetPaperDollFrame.xml`, read off the player's patch chain behind the
|
||||
//! reference's `CharacterFrame.xml` and `PaperDollFrame.xml`. Our
|
||||
//! `assets/ui/PetPaperDollFrame.xml` is deleted; [`super::test_ui::CHARACTER_UI`] is the load
|
||||
//! list, and every test here opens with `wow_data_or_skip!()` because a chain entry needs the
|
||||
//! install.
|
||||
//!
|
||||
//! The page's rows are the character page's own code with a different unit token, and
|
||||
//! `character_tests.rs` already pins those. What is genuinely new — and what these test — is the
|
||||
@@ -11,23 +16,20 @@ use benilla_ui::script::{
|
||||
PetStats, QuadContent, ScriptValue, UiScript, UnitCombatStats, UnitState,
|
||||
};
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The page's production load prefix — it leans on `CharacterFrame.xml` for the shared setters,
|
||||
/// the row templates and the tab row itself, exactly as the manifest orders them.
|
||||
/// The page's production load prefix. [`super::test_ui::CHARACTER_UI`] is the whole character
|
||||
/// block in `benilla.toc`'s order — the pet page cannot stand on its own file, because
|
||||
/// `PetPaperDollFrame_Update` calls seven `PaperDollFrame_Set*` setters with a `"pet"` unit
|
||||
/// token (stock `PetPaperDollFrame.lua:75-81`) and `PetTab_Update` moves a tab that
|
||||
/// `CharacterFrame.xml` declares.
|
||||
///
|
||||
/// **Callers must open with `benilla_formats::wow_data_or_skip!()` themselves** — the macro
|
||||
/// `return`s from the function it is written in, so a helper cannot hold the guard for its caller.
|
||||
fn load_pet_page() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// `UIParent.xml` is the manifest's third file and this page needs one thing out of it:
|
||||
// `BuildListString`, the reference's own `UIParent.lua:1051` joiner behind the diet tooltip
|
||||
// (decision 1062). Loaded in manifest order rather than bolted on at the end.
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "CharacterFrame.xml");
|
||||
load_xml(&s, "PetPaperDollFrame.xml");
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit("player", Some(player_unit()));
|
||||
s
|
||||
}
|
||||
@@ -130,23 +132,48 @@ fn take_pet(s: &mut UiScript) {
|
||||
s.fire_event("UNIT_PET", vec![ScriptValue::Str("player".into())]);
|
||||
}
|
||||
|
||||
/// The loader itself, plus the page's own frame count.
|
||||
///
|
||||
/// **A count is a fingerprint of a file, not a target** (decision 1800's closing note): this one
|
||||
/// is the reference's `PetPaperDollFrame.xml` — the page, the five stat rows and five resistance
|
||||
/// frames, the XP bar, the model pane with its two rotate buttons, the diet icon and the close
|
||||
/// button — and it moves only when the player's own file does. It is new with decision 1751's
|
||||
/// character swap: this test asserted no count while the file it loaded was ours, because a count
|
||||
/// of our own transcription fingerprints nothing but the transcription.
|
||||
#[test]
|
||||
fn shipped_pet_page_loads_clean() {
|
||||
let s = load_pet_page();
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = UiScript::new().unwrap();
|
||||
let mut pet_frames = 0;
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
let n = super::test_ui::load_ui_strict(&s, f);
|
||||
if *f == "Interface\\FrameXML\\PetPaperDollFrame.xml" {
|
||||
pet_frames = n;
|
||||
}
|
||||
}
|
||||
assert!(s.errors().is_empty(), "load errors: {:?}", s.errors());
|
||||
assert_eq!(pet_frames, 34, "the reference's own PetPaperDollFrame.xml");
|
||||
// The page itself exists and starts down, like every other subframe.
|
||||
assert!(!s
|
||||
.eval::<bool>("return PetPaperDollFrame:IsVisible()")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// **The conditional tab, both ways.** With no pet the Pet tab is down and Skills slides onto its
|
||||
/// spot; with a pet the Pet tab comes up *at that very spot* and Skills moves out past it. Pinning
|
||||
/// "Skills-when-closed sits exactly where Pet-when-open does" is what proves the re-anchor really
|
||||
/// ran — a tab merely hidden, with the row left as-authored, would leave a gap and pass a weaker
|
||||
/// check.
|
||||
/// **The conditional tab, both ways.** With no pet the Pet tab is down and the tab beside it slides
|
||||
/// onto its spot; with a pet the Pet tab comes up *at that very spot* and its neighbour moves out
|
||||
/// past it. Pinning "tab-3-when-closed sits exactly where tab 2 stands when open" is what proves
|
||||
/// the re-anchor really ran — a tab merely hidden, with the row left as-authored, would leave a gap
|
||||
/// and pass a weaker check. `PetTab_Update` is the whole mechanism, and it is four lines
|
||||
/// (stock `PetPaperDollFrame.lua:189-198`).
|
||||
///
|
||||
/// **The name was stale, not the test.** It said "Skills closes the gap" from when Skills WAS tab
|
||||
/// 3; the Reputation page moved it to 4 long before this file went to the chain, and the tab
|
||||
/// `PetTab_Update` re-anchors is and always was `CharacterFrameTab3` — REPUTATION in the
|
||||
/// reference's row (`CharacterFrame.xml:115`) and in our deleted copy alike. Renamed so it says
|
||||
/// what it checks; the assertions are untouched.
|
||||
#[test]
|
||||
fn the_pet_tab_rises_and_falls_with_the_pet_and_skills_closes_the_gap() {
|
||||
fn the_pet_tab_rises_and_falls_with_the_pet_and_the_next_tab_closes_the_gap() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_pet_page();
|
||||
// The rects only exist while something lays the window out.
|
||||
s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
|
||||
@@ -168,11 +195,11 @@ fn the_pet_tab_rises_and_falls_with_the_pet_and_skills_closes_the_gap() {
|
||||
let open3 = tab_left(&mut s, 3);
|
||||
assert_eq!(
|
||||
open2, closed3,
|
||||
"with no pet, Skills stood exactly where the Pet tab stands with one"
|
||||
"with no pet, tab 3 stood exactly where the Pet tab stands with one"
|
||||
);
|
||||
assert!(
|
||||
open3 > open2,
|
||||
"…and a pet pushes Skills out past it ({open3} vs {open2})"
|
||||
"…and a pet pushes tab 3 out past it ({open3} vs {open2})"
|
||||
);
|
||||
|
||||
take_pet(&mut s);
|
||||
@@ -185,11 +212,12 @@ fn the_pet_tab_rises_and_falls_with_the_pet_and_skills_closes_the_gap() {
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The reference's `ToggleCharacter` guard (ref `CharacterFrame.lua:4-6`): asking for the pet page
|
||||
/// The reference's `ToggleCharacter` guard (stock `CharacterFrame.lua:4-6`): asking for the pet page
|
||||
/// with no pet does **nothing at all** — it does not open the window on some other tab, and it does
|
||||
/// not close an already-open one.
|
||||
#[test]
|
||||
fn asking_for_the_pet_page_without_a_pet_does_nothing() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = load_pet_page();
|
||||
s.run(r#"ToggleCharacter("PetPaperDollFrame")"#).unwrap();
|
||||
assert!(
|
||||
@@ -208,10 +236,11 @@ fn asking_for_the_pet_page_without_a_pet_does_nothing() {
|
||||
}
|
||||
|
||||
/// The page paints the **pet's** numbers, not the player's — the whole point of un-gating the stat
|
||||
/// bindings. The player's own snapshot is deliberately absent here: anything that leaked through
|
||||
/// the `"player"` path would read zeros and this would fail.
|
||||
/// bindings. No player COMBAT-STATS snapshot is pushed here (only the unit one the window needs to
|
||||
/// open), so anything that leaked through the `"player"` path would read zeros and this would fail.
|
||||
#[test]
|
||||
fn the_page_reads_the_pets_own_snapshot() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_pet_page();
|
||||
give_pet(&mut s);
|
||||
s.run(r#"ToggleCharacter("PetPaperDollFrame")"#).unwrap();
|
||||
@@ -226,7 +255,8 @@ fn the_page_reads_the_pets_own_snapshot() {
|
||||
"expected {want:?} on the page; drew {drawn:?}"
|
||||
);
|
||||
}
|
||||
// Unspent training points = total - spent (ref l.85-86), not either number by itself.
|
||||
// Unspent training points = total - spent (stock `PetPaperDollFrame.lua:85-86`), not either
|
||||
// number by itself.
|
||||
assert!(
|
||||
drawn.iter().any(|t| t == "40"),
|
||||
"unspent training points (170-130); drew {drawn:?}"
|
||||
@@ -234,11 +264,13 @@ fn the_page_reads_the_pets_own_snapshot() {
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The window has ONE name line and the two pages take turns holding it (ref l.50-60) — the pet's
|
||||
/// The window has ONE name line and the two pages take turns holding it (stock
|
||||
/// `PetPaperDollFrame.lua:50-60`) — the pet's
|
||||
/// name replaces the player's on show and hands it back on hide. A page that forgot the hand-back
|
||||
/// would leave the character sheet nameless.
|
||||
#[test]
|
||||
fn the_page_borrows_the_windows_name_line_and_gives_it_back() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_pet_page();
|
||||
give_pet(&mut s);
|
||||
|
||||
@@ -258,11 +290,13 @@ fn the_page_borrows_the_windows_name_line_and_gives_it_back() {
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A pet dismissed or killed while its own page is open **closes the window** (ref l.32-37). This
|
||||
/// A pet dismissed or killed while its own page is open **closes the window** (stock
|
||||
/// `PetPaperDollFrame.lua:32-37`). This
|
||||
/// is the one arm that must run while the page is visible and the pet is already gone, so it is
|
||||
/// also the arm that would blow up on a page that repainted before checking.
|
||||
#[test]
|
||||
fn the_pet_leaving_closes_the_page_under_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_pet_page();
|
||||
give_pet(&mut s);
|
||||
s.run(r#"ToggleCharacter("PetPaperDollFrame")"#).unwrap();
|
||||
@@ -277,9 +311,11 @@ fn the_pet_leaving_closes_the_page_under_it() {
|
||||
}
|
||||
|
||||
/// A warlock's minion: `HasPetUI` says yes, its second return says no. The page opens in full and
|
||||
/// the three hunter-only pieces stay down (1005's class gate, seen from the page's side).
|
||||
/// the three hunter-only pieces stay down (stock `PetPaperDollFrame.lua:83-93`; 1005's class gate,
|
||||
/// seen from the page's side).
|
||||
#[test]
|
||||
fn a_minion_gets_the_page_without_the_hunter_furniture() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_pet_page();
|
||||
s.set_unit("pet", Some(pet_unit()));
|
||||
s.set_pet_stats(true, PetStats::default()); // has_ui, but hunter_pet == false
|
||||
@@ -310,16 +346,16 @@ fn a_minion_gets_the_page_without_the_hunter_furniture() {
|
||||
}
|
||||
|
||||
/// **The level line, both ways** (decision 1062). The reference guards its whole `SetText` on
|
||||
/// `UnitCreatureFamily("pet")` (ref `PetPaperDollFrame.lua:68-70`), so the family is not decoration
|
||||
/// on an existing line — it is the *condition* for the line existing at all. With a family the row
|
||||
/// reads "Level 58 Imp"; without one the page shows no level at all, which is the state 1057
|
||||
/// shipped in and the hole this closes.
|
||||
/// `UnitCreatureFamily("pet")` (stock `PetPaperDollFrame.lua:68-70`), so the family is not
|
||||
/// decoration on an existing line — it is the *condition* for the line existing at all. With a
|
||||
/// family the row reads "Level 58 Imp"; without one nothing about the pet reaches the line, which
|
||||
/// is the state 1057 shipped in and the hole this closes.
|
||||
///
|
||||
/// The minion is deliberate: a warlock's Imp fails `HasPetUI`'s second return, so this also pins
|
||||
/// that the family word rides OUTSIDE 1005's hunter gate. Gating it would blank this line for every
|
||||
/// warlock in the game.
|
||||
///
|
||||
/// **The two halves are two separate page loads on purpose.** The ref's guard skips the `SetText`;
|
||||
/// **The two halves are two separate page loads on purpose.** The guard skips the `SetText`;
|
||||
/// it does not clear the FontString — so pushing a family and then taking it away leaves the old
|
||||
/// word on screen (this test found that by asserting the opposite first). That is the reference's
|
||||
/// own shape and it is unreachable in play: a family is a property of the pet's *template*, it
|
||||
@@ -327,7 +363,8 @@ fn a_minion_gets_the_page_without_the_hunter_furniture() {
|
||||
/// reachable is "the page painted before the creature query answered", which is a page that never
|
||||
/// had a family — the second half here.
|
||||
#[test]
|
||||
fn the_level_line_names_the_family_and_is_blank_without_one() {
|
||||
fn the_level_line_names_the_family_and_is_untouched_without_one() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let with_family = |family: Option<String>| {
|
||||
let mut s = load_pet_page();
|
||||
s.set_unit("pet", Some(pet_unit()));
|
||||
@@ -352,25 +389,38 @@ fn the_level_line_names_the_family_and_is_blank_without_one() {
|
||||
"UNIT_LEVEL_TEMPLATE + the family word; drew {drawn:?}"
|
||||
);
|
||||
|
||||
// No family — the creature query has not answered, or the template has none. The line is
|
||||
// absent, not "Level 58" and not "Level 58 " with a trailing space: the ref's guard skips the
|
||||
// whole SetText, which is why nil (never "") is the binding's contract.
|
||||
// No family — the creature query has not answered, or the template has none. The guard skips
|
||||
// the whole `SetText`, so the FontString keeps whatever it already held: nothing derived from
|
||||
// the pet is written, which is why nil (never "") is the binding's contract.
|
||||
//
|
||||
// **Retired divergence (decision 1751's character swap).** This used to read "no family ⇒ no
|
||||
// level line at all", because our deleted `assets/ui/PetPaperDollFrame.xml` declared
|
||||
// `PetLevelText` with no `text=` and it started empty. The reference declares it
|
||||
// `text="Level level race class"` (stock `PetPaperDollFrame.xml:70`) — a design-time
|
||||
// placeholder, and not a GlobalStrings key, so the loader's `text=` lookup falls through to the
|
||||
// literal and that literal is what a player sees in this state, un-replaced. Both spellings say
|
||||
// the same thing about the guard; this one says it about the file that ships.
|
||||
let drawn = with_family(None);
|
||||
assert!(
|
||||
!drawn.iter().any(|t| t.starts_with("Level")),
|
||||
"no family ⇒ no level line at all; drew {drawn:?}"
|
||||
drawn.iter().any(|t| t == "Level level race class"),
|
||||
"no family ⇒ the XML's own design-time placeholder is left standing; drew {drawn:?}"
|
||||
);
|
||||
assert!(
|
||||
!drawn.iter().any(|t| t.starts_with("Level 58")),
|
||||
"…and nothing derived from the pet reaches the line; drew {drawn:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The diet tooltip** (decision 1062): the happiness-art icon under the rotate buttons is the
|
||||
/// pet's DIET affordance, and its hover runs the reference's own
|
||||
/// `format(PET_DIET_TEMPLATE, BuildListString(GetPetFoodTypes()))` (ref
|
||||
/// `PetPaperDollFrame.xml:269`) — the vararg binding feeding the `UIParent.lua` joiner.
|
||||
/// `format(PET_DIET_TEMPLATE, BuildListString(GetPetFoodTypes()))` (stock
|
||||
/// `PetPaperDollFrame.xml:267-270`) — the vararg binding feeding the `UIParent.lua` joiner.
|
||||
///
|
||||
/// Driven through the frame's real `OnEnter`, not by re-typing the expression, so a guard left in
|
||||
/// the XML or a mis-anchored `GetScript` slot fails here.
|
||||
/// Driven through the pointer, not by re-typing the expression, so a guard left in the XML or a
|
||||
/// frame that has stopped taking the mouse fails here.
|
||||
#[test]
|
||||
fn hovering_the_diet_icon_lists_what_the_pet_eats() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_pet_page();
|
||||
give_pet(&mut s);
|
||||
s.run(r#"ToggleCharacter("PetPaperDollFrame")"#).unwrap();
|
||||
@@ -396,28 +446,38 @@ fn hovering_the_diet_icon_lists_what_the_pet_eats() {
|
||||
let rect = icon.rect.expect("…with a resolved rect");
|
||||
assert!(
|
||||
(rect.right - rect.left - 24.0).abs() < 0.5 && (rect.top - rect.bottom - 23.0).abs() < 0.5,
|
||||
"24x23 (ref PetPaperDollPetInfo), got {}x{}",
|
||||
"24x23 (stock `PetPaperDollFrame.xml:245-248`), got {}x{}",
|
||||
rect.right - rect.left,
|
||||
rect.top - rect.bottom
|
||||
);
|
||||
// …and it draws ON TOP of the model pane it sits inside — the actual regression. The pane's
|
||||
// booth quad is OPAQUE and the icon's rect is wholly within it, so being in the render list is
|
||||
// not the same as being seen. The pane rides BACKGROUND for exactly this reason (decision
|
||||
// 1070): at ARTWORK the draw layer, which is bucket-wide and outranks the frame (0884), buried
|
||||
// the icon no matter which frame was declared later.
|
||||
// …and it draws ON TOP of the model pane it sits inside — the actual regression. The pane is
|
||||
// opaque and the icon's rect is wholly within it, so being in the render list is not the same
|
||||
// as being seen. The pane rides BACKGROUND for exactly this reason (decision 1070): at ARTWORK
|
||||
// the draw layer, which is bucket-wide and outranks the frame (0884), buried the icon no
|
||||
// matter which frame was declared later.
|
||||
//
|
||||
// The pane is found by NAME rather than by "a texture quad with an empty path", which is what
|
||||
// this used to look for. Our deleted `assets/ui/PetPaperDollFrame.xml` declared `PetModelFrame`
|
||||
// as a plain `<Frame>` carrying one opaque BACKGROUND booth texture standing in for a widget
|
||||
// this engine did not draw; the reference declares it `<PlayerModel>` (stock
|
||||
// `PetPaperDollFrame.xml:177`), which is a real widget here now and extracts as
|
||||
// `QuadContent::ModelPane`. The old needle matches nothing against it — this asserts the
|
||||
// containment itself rather than folding it into a `find`, so a pane that stopped covering the
|
||||
// icon fails loudly instead of dropping out of the search.
|
||||
let pane = quads
|
||||
.iter()
|
||||
.find(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path, .. }
|
||||
if path.as_deref().unwrap_or_default().is_empty())
|
||||
&& q.rect.is_some_and(|r| {
|
||||
r.left <= rect.left
|
||||
&& r.right >= rect.right
|
||||
&& r.bottom <= rect.bottom
|
||||
&& r.top >= rect.top
|
||||
})
|
||||
matches!(&q.content, QuadContent::ModelPane { name: Some(n) } if n == "PetModelFrame")
|
||||
})
|
||||
.expect("the pet model pane's booth quad covers the icon's rect");
|
||||
.expect("the pet model pane is in the render list");
|
||||
let pane_rect = pane.rect.expect("…with a resolved rect");
|
||||
assert!(
|
||||
pane_rect.left <= rect.left
|
||||
&& pane_rect.right >= rect.right
|
||||
&& pane_rect.bottom <= rect.bottom
|
||||
&& pane_rect.top >= rect.top,
|
||||
"the icon sits wholly inside the pane, which is what makes the z-order matter"
|
||||
);
|
||||
assert!(
|
||||
icon.z > pane.z,
|
||||
"the diet icon must paint over the pane it sits in (icon z={:#x}, pane z={:#x})",
|
||||
@@ -425,11 +485,18 @@ fn hovering_the_diet_icon_lists_what_the_pet_eats() {
|
||||
pane.z
|
||||
);
|
||||
|
||||
s.run(
|
||||
"local f = PetPaperDollPetInfo \
|
||||
f:GetScript(\"OnEnter\")(f)",
|
||||
)
|
||||
.unwrap();
|
||||
let centre = super::test_ui::centre_of(&mut s, "PetPaperDollPetInfo");
|
||||
// Through the MOUSE, not `GetScript("OnEnter")(f)`: the reference's handler is inline XML that
|
||||
// opens `GameTooltip:SetOwner(this, "ANCHOR_RIGHT")` (stock `PetPaperDollFrame.xml:268`), and
|
||||
// only the engine sets `this` — calling the script by hand passes nil and raises. Driving the
|
||||
// pointer also puts the frame's `enableMouse` and the hit test under test, which is the half
|
||||
// that was never covered.
|
||||
super::test_ui::hover(&mut s, "PetPaperDollPetInfo");
|
||||
assert_eq!(
|
||||
s.hit_test_name(centre.0, centre.1).as_deref(),
|
||||
Some("PetPaperDollPetInfo"),
|
||||
"the diet icon owns its own point inside the model pane"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||||
.unwrap(),
|
||||
@@ -445,6 +512,7 @@ fn hovering_the_diet_icon_lists_what_the_pet_eats() {
|
||||
/// is hunter-gated. Quietly returning `""` here would hide that coupling.
|
||||
#[test]
|
||||
fn build_list_string_is_a_plain_comma_join_that_nils_on_nothing() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = load_pet_page();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BuildListString('Meat', 'Fish', 'Cheese')")
|
||||
|
||||
@@ -36,10 +36,14 @@
|
||||
//!
|
||||
//! **Order decides, and the manifest is the order.** A name defined by both goes to whichever line
|
||||
//! is later. That is the whole rule; there is no precedence machinery. A file sourced *before* our
|
||||
//! own (`PaperDollFrame.lua`, which is there for one frame-agnostic button family and collides on
|
||||
//! eighteen names we drive ourselves) has its collisions overwritten by ours; a file sourced at the
|
||||
//! position of the window it replaces owns its names outright, which is what migrating a window
|
||||
//! means.
|
||||
//! own has its collisions overwritten by ours; a file sourced at the position of the window it
|
||||
//! replaces owns its names outright, which is what migrating a window means.
|
||||
//!
|
||||
//! The load-bearing example was `PaperDollFrame.lua`, sourced far above everything for one
|
||||
//! frame-agnostic button family while our own `CharacterFrame.xml` won the eighteen names it
|
||||
//! collided on. Decision 1751 migrated that window, so the file arrives at its own position now
|
||||
//! and there is nothing left for it to collide with — which is what a finished migration looks
|
||||
//! like.
|
||||
//!
|
||||
//! **Nothing is stubbed silently.** A reference body that reaches for something this client does
|
||||
//! not have raises, naming it — which is loud, correct, and strictly better than a no-op that
|
||||
@@ -251,6 +255,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Strip what is not code, before any call census over a FrameXML file.
|
||||
///
|
||||
/// Both `Name(` and `:Name(` counted calls inside comments until decision 1800: the round that
|
||||
/// built `PickupMerchantItem` also asked wow-re for `ShowInventorySellCursor`, which
|
||||
/// [`chain_gap_report`] had named as `PaperDollFrame.xml`'s last engine gap — and the answer
|
||||
/// was that stock `PaperDollFrame.lua:754-756` has the call **commented out**, all three
|
||||
/// lines. A real binding, never called, blocking a window that was not blocked.
|
||||
///
|
||||
/// Line-based and deliberately simple: Lua `--` to end of line (but not `--[[`, which opens a
|
||||
/// block), Lua `--[[ … ]]` blocks, and XML `<!-- … -->` blocks. It does not track string
|
||||
/// literals, so a `"--"` inside a string truncates that line — which can only ever cause an
|
||||
/// UNDER-report, the safe direction for every reader of it.
|
||||
fn strip_comments(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut in_xml = false;
|
||||
let mut in_lua_block = false;
|
||||
for line in text.lines() {
|
||||
let mut rest = line;
|
||||
let mut kept = String::new();
|
||||
loop {
|
||||
if in_xml {
|
||||
match rest.find("-->") {
|
||||
Some(i) => {
|
||||
in_xml = false;
|
||||
rest = &rest[i + 3..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else if in_lua_block {
|
||||
match rest.find("]]") {
|
||||
Some(i) => {
|
||||
in_lua_block = false;
|
||||
rest = &rest[i + 2..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else {
|
||||
let xml = rest.find("<!--");
|
||||
let lua = rest.find("--");
|
||||
match (xml, lua) {
|
||||
(Some(x), Some(l)) if x <= l => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(_, Some(l)) => {
|
||||
kept.push_str(&rest[..l]);
|
||||
if rest[l..].starts_with("--[[") {
|
||||
in_lua_block = true;
|
||||
rest = &rest[l + 4..];
|
||||
} else {
|
||||
// A plain `--` comment runs to end of line.
|
||||
rest = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(x), None) => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
if !in_xml && !in_lua_block {
|
||||
kept.push_str(rest);
|
||||
}
|
||||
out.push_str(&kept);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// **A chain entry really loads off the player's install, and order really decides.**
|
||||
///
|
||||
/// The two halves of this module's rule, asserted rather than described, because nothing else
|
||||
@@ -286,20 +364,41 @@ mod tests {
|
||||
// …and its constants, which the corpus reads directly.
|
||||
assert_eq!(s.eval::<i64>("return NUM_BAG_FRAMES").unwrap(), 4);
|
||||
assert_eq!(s.eval::<i64>("return NUM_CONTAINER_FRAMES").unwrap(), 12);
|
||||
// The `PaperDollFrame.lua` line's own reason to exist.
|
||||
// `PaperDollFrame.lua` used to be a manifest line of its own, sourced far above everything
|
||||
// for exactly this family. It arrives through stock `PaperDollFrame.xml`'s own
|
||||
// `<Script file=>` now, at the character window's position (decision 1751) — so this
|
||||
// assertion also proves a chain `.xml` really brings its `.lua`.
|
||||
assert!(s
|
||||
.eval::<bool>("return type(PaperDollItemSlotButton_OnLoad) == \"function\"")
|
||||
.unwrap());
|
||||
|
||||
// Order decides: `PaperDollFrame.lua` is sourced ABOVE CharacterFrame.xml, so OUR body of
|
||||
// a colliding name is the live one. `PaperDollFrame_SetLevel` is in the 18-name overlap.
|
||||
// **The 18-name overlap this test used to assert the winner of is GONE.** Our
|
||||
// `CharacterFrame.xml` redefined 18 of `PaperDollFrame.lua`'s 29 functions and won them all
|
||||
// by loading later; that file is deleted and the reference's own bodies are the only ones.
|
||||
// The check that replaces it is the swap's, not the collision's: the live bodies are the
|
||||
// reference's, and ours are not merely shadowed but absent.
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
"return type(PaperDollFrame_SetLevel) == \"function\" \
|
||||
and BenillaPaperDollSlot_OnLoad ~= nil"
|
||||
and type(CHARACTERFRAME_SUBFRAMES) == \"table\" \
|
||||
and table.getn(CHARACTERFRAME_SUBFRAMES) == 5 \
|
||||
and BenillaPaperDollSlot_OnLoad == nil"
|
||||
)
|
||||
.unwrap(),
|
||||
"our character sheet's own bodies must still be the live ones"
|
||||
"the character sheet's bodies must be the reference's own now"
|
||||
);
|
||||
|
||||
// Order still decides, and it is still the whole rule — so it is asserted directly rather
|
||||
// than through whichever window happens to collide this month. Two chunks, the same name,
|
||||
// and the later one stands; `publish_global`'s non-overwriting rule (RF-0023) applies to
|
||||
// FRAMES, never to a plain Lua global, and confusing the two has produced confident wrong
|
||||
// diagnoses before.
|
||||
s.run("function _order_probe() return 1 end").unwrap();
|
||||
s.run("function _order_probe() return 2 end").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return _order_probe()").unwrap(),
|
||||
2,
|
||||
"the later definition of a colliding name is the live one"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:#?}", s.errors());
|
||||
}
|
||||
@@ -561,79 +660,6 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// Strip what is not code before either scan. Both `Name(` and `:Name(` counted calls
|
||||
// inside comments until 1800: the round that built `PickupMerchantItem` also asked wow-re
|
||||
// for `ShowInventorySellCursor`, which this report had named as `PaperDollFrame.xml`'s
|
||||
// last engine gap — and the answer was that stock `PaperDollFrame.lua:754-756` has the
|
||||
// call **commented out**, all three lines. A real binding, never called, blocking a window
|
||||
// that was not blocked.
|
||||
//
|
||||
// Line-based and deliberately simple: Lua `--` to end of line (but not `--[[`, which opens
|
||||
// a block), Lua `--[[ … ]]` blocks, and XML `<!-- … -->` blocks. It does not track string
|
||||
// literals, so a `"--"` inside a string truncates that line — which can only ever cause an
|
||||
// UNDER-report, the safe direction for an instrument that ranks work.
|
||||
let strip_comments = |text: &str| -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut in_xml = false;
|
||||
let mut in_lua_block = false;
|
||||
for line in text.lines() {
|
||||
let mut rest = line;
|
||||
let mut kept = String::new();
|
||||
loop {
|
||||
if in_xml {
|
||||
match rest.find("-->") {
|
||||
Some(i) => {
|
||||
in_xml = false;
|
||||
rest = &rest[i + 3..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else if in_lua_block {
|
||||
match rest.find("]]") {
|
||||
Some(i) => {
|
||||
in_lua_block = false;
|
||||
rest = &rest[i + 2..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else {
|
||||
let xml = rest.find("<!--");
|
||||
let lua = rest.find("--");
|
||||
match (xml, lua) {
|
||||
(Some(x), Some(l)) if x <= l => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(_, Some(l)) => {
|
||||
kept.push_str(&rest[..l]);
|
||||
if rest[l..].starts_with("--[[") {
|
||||
in_lua_block = true;
|
||||
rest = &rest[l + 4..];
|
||||
} else {
|
||||
// A plain `--` comment runs to end of line.
|
||||
rest = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(x), None) => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
if !in_xml && !in_lua_block {
|
||||
kept.push_str(rest);
|
||||
}
|
||||
out.push_str(&kept);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
// `:Name(` — a method call, receiver unknown.
|
||||
let called_methods = |text: &str| -> std::collections::HashSet<String> {
|
||||
let b: Vec<char> = text.chars().collect();
|
||||
@@ -1322,6 +1348,208 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **Every global a MIGRATED window calls is answered by the loaded interface.**
|
||||
///
|
||||
/// The gate the loot window and the character sheet both wanted and neither had.
|
||||
/// [`chain_readiness_report`] asks "does the stock file LOAD"; [`chain_gap_report`] asks "what
|
||||
/// would I have to BUILD before migrating it". Neither asks the question that actually bites
|
||||
/// after a swap: *the file is on the chain now — does everything it calls exist?* A missing
|
||||
/// FrameXML function is invisible to both, because loading a file never runs the body that
|
||||
/// calls it: `LootFrame.xml` shipped load-clean and raised at `LootFrame.lua:85` the first
|
||||
/// time it met real data, and stock `CharacterFrame.xml` would have raised on the first tab
|
||||
/// HOVER, because `MicroButtonTooltipText` did not exist here (it does now — `MicroMenu.xml`,
|
||||
/// our `MainMenuBarMicroButtons.xml` counterpart, is where the reference declares it).
|
||||
///
|
||||
/// So: for every chain `.xml` in the manifest, census the bare `Name(` call sites across it
|
||||
/// and its `.lua`, subtract what those two define themselves, keep the names the reference's
|
||||
/// own `_G` carries (`reference/1.12-globals.tsv` — anything else is a local, a widget method,
|
||||
/// or a table field, and 1.12's widget methods are not globals), and require every survivor to
|
||||
/// be non-nil in a VM with the whole shipped manifest up.
|
||||
///
|
||||
/// **A gate rather than an instrument** (the `assert` is the point): for a window we have
|
||||
/// already migrated, the answer must be zero, and a missing name is a raise waiting for a
|
||||
/// player's first click on it.
|
||||
///
|
||||
/// Three limits, stated because they decide what a green run is worth. It cannot see a name
|
||||
/// reached through `getglobal` (both censuses share that blind spot). It cannot see a WIDGET
|
||||
/// method — those are not in `_G`, which is exactly why `chain_gap_report` grew a separate
|
||||
/// `method=` column (1798). And "the name exists" is not "the body is right": arity and
|
||||
/// semantics are decision 1793's problem and the window's own tests', not this one's.
|
||||
#[test]
|
||||
fn every_global_a_migrated_window_calls_is_answered() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
|
||||
// The reference's own global table — the filter that keeps this from drowning in locals.
|
||||
let tsv = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../reference/1.12-globals.tsv"
|
||||
);
|
||||
let text = std::fs::read_to_string(tsv).expect("the reference surface");
|
||||
let reference: std::collections::HashSet<&str> = text
|
||||
.lines()
|
||||
.filter(|l| !l.starts_with('#'))
|
||||
.filter_map(|l| l.split('\t').next())
|
||||
.collect();
|
||||
|
||||
let mut s = UiScript::new().expect("VM");
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let failures = super::super::manifest::load_default_ui(&s);
|
||||
assert!(failures.is_empty(), "the shipped manifest: {failures:#?}");
|
||||
let have: std::collections::HashSet<String> = s
|
||||
.eval::<Vec<String>>(
|
||||
"local t = {} for k in pairs(_G) do table.insert(t, k) end return t",
|
||||
)
|
||||
.expect("dump _G")
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// **The gaps this gate found on the day it was written, each still open, each named with
|
||||
// the window that reaches it and the click that would.** They are listed rather than
|
||||
// silenced: a KNOWN entry here is a defect we have and have not fixed, not a tolerance —
|
||||
// and the assertion below refuses an entry that no longer describes one, so fixing a gap
|
||||
// forces its line out (`frame_flag_gate`'s rule, applied to a second gate).
|
||||
//
|
||||
// None of them belongs to the character sheet, which is the window that prompted this and
|
||||
// reads clean. Each belongs to whichever window's migration left it, and each is one
|
||||
// binding or one sourced file away.
|
||||
const KNOWN: &[(&str, &str, &str)] = &[
|
||||
(
|
||||
"ContainerFrame.xml",
|
||||
"KeyRingButtonIDToInvSlotID",
|
||||
"an engine binding (`1.12-globals.tsv`). `ContainerFrame.lua:617` hovers a KEYRING \
|
||||
slot with it, so the raise needs the keyring open and a key hovered. Ours drives \
|
||||
keyring tooltips through `ContainerFrameAdapters.xml`'s wrapper (0765), which is \
|
||||
why nothing has hit it — the wrapper answers first for our own rows.",
|
||||
),
|
||||
(
|
||||
"DurabilityFrame.xml",
|
||||
"UpdateInventoryAlertStatus",
|
||||
"an engine binding. `DurabilityFrame.lua:81` calls it from the armor guy's own \
|
||||
update; our `inventory_alerts` snapshot is recomputed on every inventory push \
|
||||
instead, so the recompute exists and only the Lua verb that forces one does not.",
|
||||
),
|
||||
(
|
||||
"QuestTimerFrame.xml",
|
||||
"QuestLog_SetSelection",
|
||||
"a FrameXML function, declared in stock `QuestLogFrame.lua:308` — which we do not \
|
||||
source, because our own `QuestLogFrame.xml` is that window. Clicking a quest \
|
||||
TIMER raises (`QuestTimerFrame.lua:43`). It arrives with the quest log's own \
|
||||
migration and not before.",
|
||||
),
|
||||
(
|
||||
"QuestTimerFrame.xml",
|
||||
"QuestLog_Update",
|
||||
"the twin of the line above, `QuestLogFrame.lua:107`, called on the next line \
|
||||
(`QuestTimerFrame.lua:44`).",
|
||||
),
|
||||
(
|
||||
"UnitFrame.xml",
|
||||
"UnitPlayerControlled",
|
||||
"an engine binding, and the most reachable gap here: `UnitFrame_OnEnter` \
|
||||
(`UnitFrame.lua:62`) calls it while HOVERING any unit frame, behind \
|
||||
`SHOW_NEWBIE_TIPS == \"1\"` — which is the shipped default. wow-re has it carved \
|
||||
(`0x516410`, VERIFIED, `scratch/unit-verbs-controlled-charmed-creaturetype.md`): \
|
||||
one return, `UNIT_FIELD_FLAGS` bit 3, the DOUBLE 1.0 when set and nil when clear, \
|
||||
raising on an unrecognised token. `UnitState` carries no such flag yet, so it is \
|
||||
a field + a push + a binding — the unit frames' own slice.",
|
||||
),
|
||||
];
|
||||
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
let mut missing: Vec<(String, String)> = Vec::new();
|
||||
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
|
||||
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or(entry);
|
||||
let mut text = String::new();
|
||||
let mut cands = vec![entry.replace('\\', "/")];
|
||||
if let Some(stem) = entry.strip_suffix(".xml") {
|
||||
cands.push(format!("{stem}.lua").replace('\\', "/"));
|
||||
}
|
||||
for cand in &cands {
|
||||
if let Some(b) = super::read(cand) {
|
||||
text.push_str(&String::from_utf8_lossy(&b));
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
let text = strip_comments(&text);
|
||||
let defines: std::collections::HashSet<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
.map(|r| r.chars().take_while(|c| super::is_word(*c)).collect())
|
||||
.collect();
|
||||
|
||||
// `name(` at a call position: any identifier not preceded by `.` or `:` (a field or a
|
||||
// method) and followed by `(`.
|
||||
//
|
||||
// **Not capitalised-only.** An earlier cut of this filtered on a leading capital, on
|
||||
// the reasoning that 1.12's globals are named that way — most are, and the ones that
|
||||
// are not are the ones a stat tooltip is built out of: `strupper`, `strsub`, `abs`,
|
||||
// `max`, `floor`, `format`, `getglobal`. `reference/1.12-globals.tsv` is the filter
|
||||
// that actually belongs here, and it does not care about case.
|
||||
let b: Vec<char> = text.chars().collect();
|
||||
let mut called: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
if (b[i].is_ascii_alphabetic() || b[i] == '_')
|
||||
&& (i == 0 || !super::is_word(b[i - 1]))
|
||||
&& (i == 0 || (b[i - 1] != '.' && b[i - 1] != ':'))
|
||||
{
|
||||
let mut j = i;
|
||||
while j < b.len() && super::is_word(b[j]) {
|
||||
j += 1;
|
||||
}
|
||||
let mut k = j;
|
||||
while k < b.len() && b[k].is_whitespace() {
|
||||
k += 1;
|
||||
}
|
||||
if k < b.len() && b[k] == '(' {
|
||||
called.insert(b[i..j].iter().collect());
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let mut gaps: Vec<&String> = called
|
||||
.iter()
|
||||
.filter(|n| !defines.contains(*n))
|
||||
.filter(|n| reference.contains(n.as_str()))
|
||||
.filter(|n| !have.contains(*n))
|
||||
.collect();
|
||||
gaps.sort();
|
||||
for n in gaps {
|
||||
missing.push((leaf.to_string(), n.clone()));
|
||||
}
|
||||
}
|
||||
missing.sort();
|
||||
|
||||
let news: Vec<String> = missing
|
||||
.iter()
|
||||
.filter(|(f, n)| !KNOWN.iter().any(|(kf, kn, _)| kf == f && kn == n))
|
||||
.map(|(f, n)| format!("{f} calls {n}, which nothing answers to"))
|
||||
.collect();
|
||||
assert!(
|
||||
news.is_empty(),
|
||||
"a MIGRATED window calls a global this client does not have — load-clean and dead on \
|
||||
the first click that reaches it:\n {}",
|
||||
news.join("\n ")
|
||||
);
|
||||
|
||||
// …and the other direction: a KNOWN entry whose gap has been closed is documentation
|
||||
// claiming a defect we do not have, so it must go with the fix.
|
||||
let stale: Vec<String> = KNOWN
|
||||
.iter()
|
||||
.filter(|(kf, kn, _)| !missing.iter().any(|(f, n)| f == kf && n == kn))
|
||||
.map(|(kf, kn, why)| format!("{kf} / {kn} — claimed: {why}"))
|
||||
.collect();
|
||||
assert!(
|
||||
stale.is_empty(),
|
||||
"{} KNOWN entr(y/ies) name a gap that is closed — delete them:\n {}",
|
||||
stale.len(),
|
||||
stale.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
/// **A chain `.xml` that does not source its own `.lua` needs TWO manifest lines**, and
|
||||
/// getting it wrong is silent.
|
||||
///
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
//! The fixture deliberately mirrors the engine module's own (Alliance over Ironforge + Stormwind,
|
||||
//! Steamwheedle over Booty Bay, the parentless bucket last) so a failure here reads against a shape
|
||||
//! that is already pinned one layer down, and the two can be compared line for line.
|
||||
//!
|
||||
//! **The window around the page is the reference's own since 1751** — `CharacterFrame.xml` and
|
||||
//! `PaperDollFrame.xml` off the player's chain — so every test here opens with
|
||||
//! `wow_data_or_skip!()` and loads [`super::test_ui::CHARACTER_UI`]. The page itself
|
||||
//! (`ReputationFrame.xml`) and the watch bar (`ActionBar.xml`) are still ours.
|
||||
|
||||
use benilla_ui::script::{FactionEntry, ReputationState, UiScript, UnitState};
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// An ordinary bar row: visible, not a header, `standing_id` 5 ("Friendly") sitting 1000 into a
|
||||
/// 6000-wide rank window. The same numbers `benilla-ui`'s own fixture uses.
|
||||
fn entry(faction_id: u32, rep_list_id: u32, parent_id: u32, name: &str) -> FactionEntry {
|
||||
@@ -76,28 +79,36 @@ fn state() -> ReputationState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The manifest prefix this page really needs, in manifest order: the fonts, the panel manager, the
|
||||
/// managed-position pass, the tooltip, the numerals machinery + Cooldown + ActionBar (the watch bar
|
||||
/// and every frame it drives live there), the two scroll/shared-widget kits the page's list and
|
||||
/// popup inherit from, then the window and the page.
|
||||
/// The character window, whose third tab this page is — [`super::test_ui::CHARACTER_UI`], which
|
||||
/// already carries `ReputationFrame.xml` (stock `CharacterFrame_ShowSubFrame` hides all five pages
|
||||
/// by name, unguarded) and `ActionBar.xml` (the watch bar, and the show/hide pair's
|
||||
/// `ShowWatchedReputationBarText`). This page adds nothing of its own.
|
||||
///
|
||||
/// It was a hand-copied manifest prefix until the window became the reference's (decision 1751):
|
||||
/// stock `CharacterFrame_OnLoad` reaches into four other files before it does anything else, so the
|
||||
/// list stopped being short enough for a per-module copy to stay honest.
|
||||
fn load_page(s: &UiScript) {
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"OptionsFrameTemplates.xml",
|
||||
"CharacterFrame.xml",
|
||||
"ReputationFrame.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
for file in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(s, file);
|
||||
}
|
||||
}
|
||||
|
||||
/// A level-`level` player behind the window — enough of one that opening it is quiet.
|
||||
///
|
||||
/// **Race and class carry both halves of their pairs**, which is not decoration since 1751:
|
||||
/// `UnitRace`/`UnitClass` answer `(localized, file)` or `nil, nil` — the binding `zip`s the two —
|
||||
/// and stock `PaperDollFrame_SetLevel` formats level, race and class into `CharacterLevelText`
|
||||
/// unguarded (`PaperDollFrame.lua:100-104`), on every show of the window this page is a tab of.
|
||||
/// A `UnitState::default()` player therefore raises the moment `ToggleCharacter` runs.
|
||||
fn player(level: u32) -> UnitState {
|
||||
UnitState {
|
||||
exists: true,
|
||||
level,
|
||||
race: Some("Human".into()),
|
||||
race_file: Some("Human".into()),
|
||||
class: Some("Warrior".into()),
|
||||
class_file: Some("WARRIOR".into()),
|
||||
..UnitState::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,14 +117,7 @@ fn shown_reputation_page() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_page(&s);
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
level: 40,
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s.set_unit("player", Some(player(40)));
|
||||
s.set_reputation(state());
|
||||
s.run(r#"ToggleCharacter("ReputationFrame")"#).unwrap();
|
||||
s.resolve();
|
||||
@@ -151,6 +155,7 @@ fn shown(s: &mut UiScript, name: &str) -> bool {
|
||||
/// wrong tab. Skills moved to 4 in the same breath and is asserted here for the same reason.
|
||||
#[test]
|
||||
fn the_reputation_page_opens_on_the_windows_third_tab() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_reputation_page();
|
||||
assert!(
|
||||
s.eval::<bool>("return ReputationFrame:IsVisible()")
|
||||
@@ -163,21 +168,36 @@ fn the_reputation_page_opens_on_the_windows_third_tab() {
|
||||
3,
|
||||
"and the tab row selects 3 — the reference's own slot for Reputation"
|
||||
);
|
||||
// **The id IS the tab slot**, and that is the whole coupling: `ToggleCharacter` selects the tab
|
||||
// with `PanelTemplates_SetTab(CharacterFrame, subFrame:GetID())`
|
||||
// (`CharacterFrame.lua:10`), while `CharacterFrameTab_OnClick` hardcodes which page each tab
|
||||
// opens (`:41-43` — tab 3 → Reputation, tab 4 → Skills). A page whose `id=` disagreed would
|
||||
// open on click and light the wrong tab.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ReputationFrame:GetID()").unwrap(),
|
||||
3,
|
||||
"the id IS the tab slot; BENILLA_CHARACTERFRAME_SUBFRAMES[3] must name this page"
|
||||
"the id IS the tab slot the reference's tab 3 opens"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BENILLA_CHARACTERFRAME_SUBFRAMES[3]")
|
||||
.unwrap(),
|
||||
"ReputationFrame"
|
||||
s.eval::<i64>("return SkillFrame:GetID()").unwrap(),
|
||||
4,
|
||||
"and Skills is the reference's 4 beside it"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BENILLA_CHARACTERFRAME_SUBFRAMES[4]")
|
||||
.unwrap(),
|
||||
"SkillFrame",
|
||||
"and Skills moved to the reference's 4 with it"
|
||||
// **`CHARACTERFRAME_SUBFRAMES` is the hide-all list, not the tab map** — re-pointed here at
|
||||
// 1751, because our deleted `CharacterFrame.xml` wrote the two as one thing and the
|
||||
// reference's does not. Stock line 1 is
|
||||
// `{ "PaperDollFrame", "PetPaperDollFrame", "SkillFrame", "ReputationFrame", "HonorFrame" }` —
|
||||
// Skills THIRD and Reputation FOURTH, the opposite of their tab slots — and it is only ever
|
||||
// walked by `CharacterFrame_ShowSubFrame`, which shows the one it was named and hides the rest
|
||||
// (`CharacterFrame.lua:25-33`). So what this page needs from the list is membership: a page
|
||||
// missing from it is a page nothing ever hides, which is the "one page at a time" assertion at
|
||||
// the end of this test.
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
"for _, v in CHARACTERFRAME_SUBFRAMES do if v == \"ReputationFrame\" then return true end end return false"
|
||||
)
|
||||
.unwrap(),
|
||||
"the hide-all list names this page"
|
||||
);
|
||||
|
||||
// Eight rows in fifteen slots: the tail goes dark on BOTH twins, and a list that fits raises
|
||||
@@ -205,6 +225,7 @@ fn the_reputation_page_opens_on_the_windows_third_tab() {
|
||||
/// global leaves the folded rows on screen (ReputationFrame.xml's deviation 3).
|
||||
#[test]
|
||||
fn a_header_row_paints_its_fold_icon_and_folds_on_click() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_reputation_page();
|
||||
|
||||
assert_eq!(text_of(&mut s, "ReputationHeader1"), "Alliance");
|
||||
@@ -259,6 +280,7 @@ fn a_header_row_paints_its_fold_icon_and_folds_on_click() {
|
||||
/// absolute.
|
||||
#[test]
|
||||
fn a_bar_row_paints_name_standing_and_the_faction_bar_colour() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_reputation_page();
|
||||
|
||||
assert_eq!(text_of(&mut s, "ReputationBar2FactionName"), "Ironforge");
|
||||
@@ -325,10 +347,11 @@ fn a_bar_row_paints_name_standing_and_the_faction_bar_colour() {
|
||||
/// 0251), so this is the assertion that the slots really are re-bound and not just clipped.
|
||||
#[test]
|
||||
fn the_scroll_offset_rebinds_the_fixed_row_slots() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_page(&s);
|
||||
s.set_unit("player", Some(UnitState::default()));
|
||||
s.set_unit("player", Some(player(40)));
|
||||
|
||||
// One header over twenty children: 21 visible rows against 15 slots.
|
||||
let mut entries = vec![header(469, 11, "Alliance")];
|
||||
@@ -403,10 +426,11 @@ fn the_scroll_offset_rebinds_the_fixed_row_slots() {
|
||||
/// bar again closes it, which is the reference's own toggle (l.152-153).
|
||||
#[test]
|
||||
fn clicking_a_bar_opens_the_detail_popup_on_that_faction() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_page(&s);
|
||||
s.set_unit("player", Some(UnitState::default()));
|
||||
s.set_unit("player", Some(player(40)));
|
||||
// Ironforge at war and peace-forced — the two flags the popup reads back off the row.
|
||||
let mut st = state();
|
||||
for e in &mut st.entries {
|
||||
@@ -512,17 +536,11 @@ fn clicking_a_bar_opens_the_detail_popup_on_that_faction() {
|
||||
/// `ExhaustionTick_Update` as unreachable while this bar could never show.
|
||||
#[test]
|
||||
fn the_watch_bar_shows_the_watched_factions_progress_and_swaps_at_max_level() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_page(&s);
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
level: 40,
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s.set_unit("player", Some(player(40)));
|
||||
assert!(
|
||||
!shown(&mut s, "ReputationWatchBar"),
|
||||
"nothing watched: the bar stays down, as it has since it was a stub"
|
||||
@@ -587,14 +605,7 @@ fn the_watch_bar_shows_the_watched_factions_progress_and_swaps_at_max_level() {
|
||||
.unwrap();
|
||||
|
||||
// Ding to 60: the reputation strip takes the XP bar's place, art and all.
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
level: 60,
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s.set_unit("player", Some(player(60)));
|
||||
s.fire_event(
|
||||
"PLAYER_LEVEL_UP",
|
||||
vec![benilla_ui::script::ScriptValue::Int(60)],
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
//! descriptor is (`benilla-ui`'s `SkillEntry::mono`; wow-re `0x4d3610`'s `4d38b1` branch). A
|
||||
//! hunter's `Beast Mastery` on vmangos arrives as `300/300` and must still read gray and
|
||||
//! numberless, exactly as it does in the real client.
|
||||
//!
|
||||
//! **The window around the page is the reference's own since 1751** — `CharacterFrame.xml` and
|
||||
//! `PaperDollFrame.xml` off the player's chain — so every test that opens the page loads
|
||||
//! [`super::test_ui::CHARACTER_UI`] and opens with `wow_data_or_skip!()`. The page itself
|
||||
//! (`assets/ui/SkillFrame.xml`) is still ours.
|
||||
|
||||
use benilla_ui::script::{QuadContent, SkillEntry, SkillsState, UiScript};
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
use benilla_ui::script::{QuadContent, SkillEntry, SkillsState, UiScript, UnitState};
|
||||
|
||||
fn skill(
|
||||
skill_id: u32,
|
||||
@@ -45,25 +48,37 @@ fn skill(
|
||||
/// The Skills page, shown, with one line of each shape — a hunter's real vmangos numbers:
|
||||
/// `Beast Mastery 300/300` (single-rank, `SkillRaceClassInfo` 0x410), `Defense 12/60` (a normal
|
||||
/// weapon line), `Cloth 1/1` (an armor proficiency the server itself caps).
|
||||
///
|
||||
/// The window around the page is the reference's own since 1751, so the file list is
|
||||
/// [`super::test_ui::CHARACTER_UI`] — which already carries `SkillFrame.xml` (stock
|
||||
/// `CharacterFrame_ShowSubFrame` hides all five pages by name, unguarded) and the
|
||||
/// `UIPanelTemplates.xml` this list learned by hand a year ago: a MISSING template is a loader
|
||||
/// *warning*, not an error, so an under-loaded list passes and then fails later on geometry that
|
||||
/// 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 {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// UIPanelTemplates.xml is NOT optional even though this list ran without it for a year: a
|
||||
// MISSING template is a loader *warning*, not an error, so an under-loaded list passes the
|
||||
// assert in load_xml and then fails later on geometry that silently never got built.
|
||||
// SkillDetailScrollFrame inherits UIPanelScrollFrameTemplate from that file.
|
||||
for f in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"CharacterFrame.xml",
|
||||
"SkillFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
// A player behind the window, carrying BOTH halves of the race and class pairs:
|
||||
// `UnitRace`/`UnitClass` answer `(localized, file)` or `nil, nil` — the binding `zip`s them —
|
||||
// and stock `PaperDollFrame_SetLevel` formats level, race and class into `CharacterLevelText`
|
||||
// unguarded (`PaperDollFrame.lua:100-104`) every time this window is shown. Ours never did,
|
||||
// which is why this page could be opened with no player at all.
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
level: 60,
|
||||
race: Some("Tauren".into()),
|
||||
race_file: Some("Tauren".into()),
|
||||
class: Some("Hunter".into()),
|
||||
class_file: Some("HUNTER".into()),
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s.set_skills(SkillsState {
|
||||
entries: vec![
|
||||
skill(50, "Beast Mastery", 300, 300, true, (7, "Class Skills", 2)),
|
||||
@@ -89,6 +104,24 @@ fn row(s: &mut UiScript, i: u32) -> (String, [f32; 4]) {
|
||||
(text, [c.0, c.1, c.2, c.3])
|
||||
}
|
||||
|
||||
/// The quads **this page** draws — `s.extract()` narrowed to targets whose nearest named owner is
|
||||
/// one of the page's own frames.
|
||||
///
|
||||
/// Not a convenience: since 1751 the window around the page is the reference's own, so a test VM
|
||||
/// carries the whole [`super::test_ui::CHARACTER_UI`] — the action bar, the micro menu and the unit
|
||||
/// frames are loaded and *visible*, and their quads land in the same extract. An unfiltered
|
||||
/// "nothing draws at alpha 0.5" over that list asserts something about the action bar, not about
|
||||
/// this page's troughs.
|
||||
fn page_quads(s: &UiScript) -> Vec<benilla_ui::script::ExtractedQuad> {
|
||||
s.extract()
|
||||
.into_iter()
|
||||
.filter(|q| {
|
||||
s.target_owner_name(q.target)
|
||||
.is_some_and(|n| n.starts_with("Skill"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Row slot `i`'s **trough** colour — the `$parentBackground` texture behind the fill, which the
|
||||
/// ref recolours per branch alongside the fill (`SkillFrame.lua:158` normal / `:167` proficiency).
|
||||
fn row_bg(s: &mut UiScript, i: u32) -> [f32; 4] {
|
||||
@@ -102,6 +135,7 @@ fn row_bg(s: &mut UiScript, i: u32) -> [f32; 4] {
|
||||
|
||||
#[test]
|
||||
fn a_single_rank_line_paints_gray_with_no_rank_text() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_skills_page();
|
||||
assert!(
|
||||
s.eval::<bool>("return SkillFrame:IsVisible()").unwrap(),
|
||||
@@ -142,8 +176,7 @@ fn a_single_rank_line_paints_gray_with_no_rank_text() {
|
||||
// — a real texel — and the vertex colour MULTIPLIES it, alpha included (the composition law,
|
||||
// `benilla-ui` `script::tests::regions`): the proficiency's white reaches the screen at
|
||||
// `0.2 x 0.5 = 0.1`, not 0.5. Every solid-colour quad this page emits is one of these troughs.
|
||||
let solids: Vec<[f32; 4]> = s
|
||||
.extract()
|
||||
let solids: Vec<[f32; 4]> = page_quads(&s)
|
||||
.iter()
|
||||
.filter_map(|q| match &q.content {
|
||||
QuadContent::Texture {
|
||||
@@ -295,6 +328,7 @@ fn a_real_hunters_block_lists_exactly_what_the_reference_client_lists() {
|
||||
/// (80x22 centred on the page's TOPLEFT + (305,-422)), and that it closes the window.
|
||||
#[test]
|
||||
fn the_pages_close_button_sits_where_the_reference_seats_it_and_closes_the_window() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_skills_page();
|
||||
|
||||
assert!(
|
||||
@@ -330,18 +364,24 @@ fn the_pages_close_button_sits_where_the_reference_seats_it_and_closes_the_windo
|
||||
);
|
||||
|
||||
// Its label is the CLOSE global string's seat, in the panel-button gold.
|
||||
let label = s
|
||||
.extract()
|
||||
// **"Close", not "CLOSE".** An XML `text=` attribute is a GlobalStrings LOOKUP, not a literal
|
||||
// (`loader::Loader::resolve_text`, wow-re `rf28-typed-widget-loadxml.md` l.36 —
|
||||
// `FrameScript_GetText 0x703bf0`), and `CLOSE = "Close"` (`GlobalStrings.lua:760`). This test
|
||||
// read "CLOSE" while the page's list carried no `GlobalStrings.lua`: it was asserting our
|
||||
// loader's miss-fallback (the LITERAL, a deliberate divergence for benilla-authored files),
|
||||
// not the label the reference client draws. [`super::test_ui::CHARACTER_UI`] loads the
|
||||
// player's own strings first, as the app does, so this is the real label now.
|
||||
let label = page_quads(&s)
|
||||
.iter()
|
||||
.find_map(|q| match &q.content {
|
||||
QuadContent::Text {
|
||||
text: Some(t),
|
||||
color,
|
||||
..
|
||||
} if t == "CLOSE" => Some(*color),
|
||||
} if t == "Close" => Some(*color),
|
||||
_ => None,
|
||||
})
|
||||
.expect("the CLOSE label draws");
|
||||
.expect("the Close label draws");
|
||||
assert_eq!(
|
||||
label,
|
||||
Some([1.0, 0.82, 0.0, 1.0]),
|
||||
@@ -368,10 +408,12 @@ fn the_pages_close_button_sits_where_the_reference_seats_it_and_closes_the_windo
|
||||
/// cap), not 3px above it, which is where the offset copied from `TrainerFrame.xml` put it.
|
||||
#[test]
|
||||
fn the_collapse_all_fold_wears_the_row_font_and_the_references_seat() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = shown_skills_page();
|
||||
|
||||
let label = s
|
||||
.extract()
|
||||
// "All", not "ALL" — the same GlobalStrings lookup as the CLOSE button above
|
||||
// (`ALL = "All"`, `GlobalStrings.lua:45`). The face is what this test is about, not the word.
|
||||
let label = page_quads(&s)
|
||||
.iter()
|
||||
.find_map(|q| match &q.content {
|
||||
QuadContent::Text {
|
||||
@@ -379,10 +421,10 @@ fn the_collapse_all_fold_wears_the_row_font_and_the_references_seat() {
|
||||
color,
|
||||
font_height,
|
||||
..
|
||||
} if t == "ALL" => Some((*color, *font_height)),
|
||||
} if t == "All" => Some((*color, *font_height)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("the ALL label draws");
|
||||
.expect("the All label draws");
|
||||
assert_eq!(
|
||||
label,
|
||||
(Some([1.0, 1.0, 1.0, 1.0]), Some(12.0)),
|
||||
@@ -423,6 +465,7 @@ fn the_collapse_all_fold_wears_the_row_font_and_the_references_seat() {
|
||||
/// reference's, and only the first is what a cold load sees.
|
||||
#[test]
|
||||
fn the_expand_tab_fits_its_label_once_a_measure_answers() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_skills_page();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return SkillFrameExpandButtonFrame:GetWidth()")
|
||||
|
||||
@@ -144,6 +144,80 @@ pub(super) const LOOT_UI: &[&str] = &[
|
||||
"PartyFrame.xml",
|
||||
];
|
||||
|
||||
/// What the **character window** needs before `Interface\FrameXML\CharacterFrame.xml`,
|
||||
/// `PaperDollFrame.xml` and `PetPaperDollFrame.xml` will load and behave — the same shape as
|
||||
/// [`BAG_UI`] / [`LOOT_UI`] / [`MERCHANT_UI`], and grown the same way (decision 1751).
|
||||
///
|
||||
/// This one is the longest of the four, and the reason is `CharacterFrame_OnLoad`: it is the only
|
||||
/// migrated window whose LOAD-time body reaches outside its own file, and it reaches into four
|
||||
/// others at once —
|
||||
///
|
||||
/// * **`Interface\FrameXML\TextStatusBar.lua`** for `SetTextStatusBarTextPrefix`, which it calls
|
||||
/// three times before doing anything else. Its `.xml` twin comes too, because the pet page's XP
|
||||
/// bar inherits the `TextStatusBar` template it declares and a missing template is a loader
|
||||
/// *warning* — the bar would load with no art and no text region and read as a pass.
|
||||
/// * **`UnitFrames.xml`** for `PlayerFrameHealthBar` / `PlayerFrameManaBar`, the frames those three
|
||||
/// calls name, plus `PetFrameHealthBar` / `PetFrameManaBar` which `CharacterFrame_OnShow` shows
|
||||
/// the text on. (Ours: the unit frames are not migrated. When they are, this entry becomes the
|
||||
/// stock `PlayerFrame.xml` / `PetFrame.xml` pair.)
|
||||
/// * **`ActionBar.xml`** for `MainMenuExpBar` — the third bar of that prefix call — and for
|
||||
/// `ShowWatchedReputationBarText` / `HideWatchedReputationBarText`, which the window's
|
||||
/// show/hide pair calls.
|
||||
/// * **`UIPanelTemplates.xml`** for `PanelTemplates_SetNumTabs`/`_SetTab`, the last two lines of
|
||||
/// that same OnLoad.
|
||||
///
|
||||
/// And two more that only bite later, which is exactly why they are written down:
|
||||
///
|
||||
/// * **`Interface\FrameXML\HonorFrame.xml`** — `PaperDollFrame_SetLevel` and `_SetGuild` write
|
||||
/// `HonorLevelText` and `HonorGuildText` "while we're at it" (`PaperDollFrame.lua:100`/`:120`).
|
||||
/// Nothing touches them at load, so leaving this out loads clean and then raises the first time
|
||||
/// the window is SHOWN. It sits below the character block in the manifest for the same reason.
|
||||
/// * **`MicroMenu.xml`** — `UpdateMicroButtons` (called by both `CharacterFrame_OnShow` and
|
||||
/// `_OnHide`) and `MicroButtonTooltipText` (all five tab hovers). A tab hover is the only thing
|
||||
/// that reaches the second one, so its absence is invisible until a test hovers a tab.
|
||||
///
|
||||
/// Needs client data, like its three siblings: open with `benilla_formats::wow_data_or_skip!()`.
|
||||
pub(super) const CHARACTER_UI: &[&str] = &[
|
||||
// The reference's own localized strings. The stock file has no `X = X or "…"` fallbacks —
|
||||
// `PaperDollFrame_OnLoad` sets seven labels from them at LOAD, and `PaperDollFrame_SetStats`
|
||||
// concatenates `SPELL_STAT0_NAME`..`4` on every repaint.
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml",
|
||||
"BasicControls.xml", // TEXT(), which every one of those label sets goes through
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml", // PaperDollItemSlotButtonTemplate's base
|
||||
"MoneyFrame.xml",
|
||||
"UIParent.xml", // Model_OnLoad/_Rotate*/_OnUpdate — the model panes' turntable
|
||||
"UiPanels.xml", // CharacterFrameTabButtonTemplate, UIPanelWindows, Show/HideUIPanel
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml", // CooldownFrameTemplate + CooldownFrame_SetTimer, per equipment slot
|
||||
"UIPanelTemplates.xml",
|
||||
// The unit frames' four right-click dropdowns call `UIDropDownMenu_Initialize` at LOAD, so the
|
||||
// kit and the menu table it initialises from both precede them — the manifest's own order
|
||||
// (175 → 189 → 193 → 263).
|
||||
"UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml",
|
||||
"UnitPopup.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"UnitFrames.xml",
|
||||
"ActionBar.xml",
|
||||
"MicroMenu.xml",
|
||||
// The two page files these three need before the window can be OPENED, which is not the same
|
||||
// as before it can load: `CHARACTERFRAME_SUBFRAMES` lists all five pages by name and
|
||||
// `CharacterFrame_ShowSubFrame` calls `getglobal(value):Hide()` on each one it is not showing
|
||||
// (`CharacterFrame.lua:25-32`), unguarded. A missing page is `attempt to index a nil value` on
|
||||
// the very first `ToggleCharacter` — load-clean, then dead on the first click. Their own
|
||||
// template dependencies come with them.
|
||||
"ScrollTemplates.xml", // SkillFrame's faux list + trough
|
||||
"OptionsFrameTemplates.xml", // ReputationFrame's detail check boxes
|
||||
"Interface\\FrameXML\\CharacterFrame.xml",
|
||||
"Interface\\FrameXML\\PaperDollFrame.xml",
|
||||
"Interface\\FrameXML\\PetPaperDollFrame.xml",
|
||||
"ReputationFrame.xml",
|
||||
"SkillFrame.xml",
|
||||
"Interface\\FrameXML\\HonorFrame.xml",
|
||||
];
|
||||
|
||||
pub(super) const BAG_UI: &[&str] = &[
|
||||
// The reference's own localized strings — `BACKPACK_TOOLTIP`, `EQUIP_CONTAINER`, `KEYRING`,
|
||||
// the `*_FONT_COLOR_CODE` pair. The app loads this at VM setup, ahead of the manifest
|
||||
@@ -154,11 +228,6 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
// is the better answer, and these tests already gate on the install.
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml",
|
||||
// The reference's own `PaperDollItemSlotButton_*` family, sourced at manifest entry 2. Not
|
||||
// optional since 1751's third window: stock `BagSlotButtonTemplate`'s OnLoad *is*
|
||||
// `PaperDollItemSlotButton_OnLoad()`, which is what gives each bag button its inventory-slot
|
||||
// id (20..23 via `GetInventorySlotInfo`), its six event registrations and its first paint.
|
||||
"Interface\\FrameXML\\PaperDollFrame.lua",
|
||||
// `TEXT()` — the reference's own identity-function wrapper, which stock
|
||||
// `MainMenuBarBackpackButton`'s OnEnter calls (`GameTooltip:SetText(TEXT(BACKPACK_TOOLTIP)…)`)
|
||||
// and `BagSlotButton_OnEnter` calls for `EQUIP_CONTAINER`. Manifest entry 3, and not optional
|
||||
@@ -183,9 +252,20 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
"MicroMenu.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\ContainerFrame.xml",
|
||||
// `PaperDollItemSlotButtonTemplate`, which every bag button inherits — resolved at load, so it
|
||||
// has to precede the bar exactly as it does in the manifest.
|
||||
"ItemSlotButtonTemplates.xml",
|
||||
// `PaperDollItemSlotButtonTemplate` and the `PaperDollItemSlotButton_*` family behind it,
|
||||
// which every bag button inherits and runs — resolved at load, so this has to precede the bar
|
||||
// exactly as it does in the manifest. Stock `BagSlotButtonTemplate`'s OnLoad *is*
|
||||
// `PaperDollItemSlotButton_OnLoad()`, which gives each bag button its inventory-slot id
|
||||
// (20..23 via `GetInventorySlotInfo`), its six event registrations and its first paint.
|
||||
//
|
||||
// The whole paper-doll file, because that is where the reference declares both — our
|
||||
// `ItemSlotButtonTemplates.xml` held a transcribed copy of the template only because our own
|
||||
// character window loaded too late to declare it, and it is deleted (decision 1751). Its
|
||||
// companion `Interface\\FrameXML\\CharacterFrame.xml` is deliberately NOT here: this list is
|
||||
// the bags, `PaperDollFrame` only names `CharacterFrame` in `parent=` (a missing parent is a
|
||||
// loader warning, not an error), and `CharacterFrame_OnLoad` would drag in the unit frames,
|
||||
// the XP bar and the text-status-bar file for a window no bag test opens.
|
||||
"Interface\\FrameXML\\PaperDollFrame.xml",
|
||||
// The bag BAR itself, the reference's own since 1751's third window: MainMenuBarBackpackButton,
|
||||
// CharacterBag0..3Slot, KeyRingButton, `BagSlotButtonTemplate`, and `KEYRING_CONTAINER`.
|
||||
"Interface\\FrameXML\\MainMenuBarBagButtons.xml",
|
||||
|
||||
@@ -9,60 +9,90 @@
|
||||
//! `ContainerFrameItemButton_OnEnter` on one of its recycled `ContainerFrame1..12` now. Same two
|
||||
//! calls arm the compare (`GameTooltip:SetOwner` + `SetBagItem`), so what these tests pin is
|
||||
//! unchanged — but the handler reads `this`, so they drive the mouse
|
||||
//! ([`super::test_ui::hover`]) instead of calling it. The doll end is still ours
|
||||
//! (`BenillaPaperDollSlot_OnEnter`) and is still called directly.
|
||||
//! ([`super::test_ui::hover`]) instead of calling it.
|
||||
//!
|
||||
//! **The doll end went the same way.** `CharacterFrame.xml` and `PaperDollFrame.xml` are the
|
||||
//! reference's own too now, so `BenillaPaperDollSlot_OnEnter` is gone and the hover is stock
|
||||
//! `PaperDollItemSlotButton_OnEnter` (`PaperDollFrame.lua:739-764`), which reads `this` as well.
|
||||
//! The doll-slot names (`CharacterHeadSlot` and kin) are unchanged; the mouse is what reaches them.
|
||||
|
||||
use benilla_ui::script::{
|
||||
ContainerSlot, ContainerState, InvSlotView, InventorySlots, ItemTemplateView, UiScript,
|
||||
UnitState,
|
||||
};
|
||||
|
||||
use super::test_ui::{bag_slot_button, hover, load_ui as load_xml, BAG_UI};
|
||||
use super::test_ui::{bag_slot_button, hover, BAG_UI, CHARACTER_UI};
|
||||
|
||||
/// The window set the compare flow crosses **without a bag window**: fonts, UIParent, panels, both
|
||||
/// tooltip files and the character window (the listener's doll slots). Deliberately install-free,
|
||||
/// so the chat-link test still runs on a bare checkout.
|
||||
/// The two shared lists overlap heavily — `GlobalStrings`, the fonts, `UIParent`, the tooltip,
|
||||
/// `Cooldown`, the action bar and `Interface\FrameXML\PaperDollFrame.xml` are in both — and
|
||||
/// loading one file twice redeclares its frames. So a list is walked once and anything already
|
||||
/// loaded is skipped, which also keeps the ORDER the first list asked for.
|
||||
fn load_once(s: &UiScript, seen: &mut Vec<&'static str>, files: &[&'static str]) {
|
||||
for f in files {
|
||||
if seen.contains(f) {
|
||||
continue;
|
||||
}
|
||||
seen.push(f);
|
||||
super::test_ui::load_ui_strict(s, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The window set the compare flow crosses **without a bag window**: the whole character window
|
||||
/// (the listener's doll slots), plus [`ROUTER_UI`].
|
||||
///
|
||||
/// **Needs client data.** It always did — `Interface\FrameXML\ItemRef.xml` has been a chain entry
|
||||
/// since it was pointed there — but the comment here claimed "deliberately install-free" until
|
||||
/// 1751 made the claim impossible to miss. Callers open with `wow_data_or_skip!`.
|
||||
fn harness() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"CharacterFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
let mut seen = Vec::new();
|
||||
load_once(&s, &mut seen, CHARACTER_UI);
|
||||
load_once(&s, &mut seen, &ROUTER_UI);
|
||||
s.set_money(0);
|
||||
s.set_unit("player", Some(player()));
|
||||
s
|
||||
}
|
||||
|
||||
/// The two reference files this harness carries beyond the character window: `MerchantFrame.xml`,
|
||||
/// the other window the compare flow crosses, and `ItemRef.xml`, which declares the chat-link
|
||||
/// router's own `ItemRefTooltip`. Both were in this harness's hand-copied list before 1751; the
|
||||
/// manifest's order is the one kept (`FrameXML.toc` 63 → 77).
|
||||
const ROUTER_UI: [&str; 2] = [
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
];
|
||||
|
||||
/// A level-60 player carrying **both** halves of the race and class pairs: `UnitRace`/`UnitClass`
|
||||
/// answer `(localized, file)` or `nil, nil` — the binding `zip`s them — and stock
|
||||
/// `PaperDollFrame_SetLevel` formats all three into `CharacterLevelText` unguarded
|
||||
/// (`PaperDollFrame.lua:100-104`) on every show of the character window.
|
||||
fn player() -> UnitState {
|
||||
UnitState {
|
||||
exists: true,
|
||||
level: 60,
|
||||
race: Some("Human".into()),
|
||||
race_file: Some("Human".into()),
|
||||
class: Some("Warrior".into()),
|
||||
class_file: Some("WARRIOR".into()),
|
||||
..UnitState::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// [`harness`] with the REFERENCE's bag windows (1751) — the hover source. `BAG_UI` is the ordered
|
||||
/// set a test needs before it can open one and already carries the fonts, panels, tooltip,
|
||||
/// item-button template and bag bar; `UIParent.xml` leads because it inherits nothing and every
|
||||
/// window below parents to it.
|
||||
/// set a test needs before it can open one; `CHARACTER_UI` leads because the manifest seats the
|
||||
/// character block above the containers (`FrameXML.toc` 53-58 → 65) and because it carries
|
||||
/// `PaperDollFrame.xml`, which `MainMenuBarBagButtons` inherits its `BagSlotButtonTemplate` body
|
||||
/// from.
|
||||
///
|
||||
/// **Needs client data**: `BAG_UI` names a chain entry, so its callers open with
|
||||
/// `wow_data_or_skip!`.
|
||||
/// **Needs client data**: both lists name chain entries, so callers open with `wow_data_or_skip!`.
|
||||
fn harness_with_bags() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "UIParent.xml");
|
||||
for f in BAG_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
for f in [
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"CharacterFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
let mut seen = Vec::new();
|
||||
load_once(&s, &mut seen, CHARACTER_UI);
|
||||
load_once(&s, &mut seen, BAG_UI);
|
||||
load_once(&s, &mut seen, &ROUTER_UI);
|
||||
s.set_money(0);
|
||||
s
|
||||
}
|
||||
@@ -156,7 +186,7 @@ fn seed_items(s: &mut UiScript) {
|
||||
fn shift_compare_over_a_bag_item_seats_on_the_doll_slot() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness_with_bags();
|
||||
s.set_unit("player", Some(UnitState::default()));
|
||||
s.set_unit("player", Some(player()));
|
||||
seed_items(&mut s);
|
||||
|
||||
// Open the bag and hover the helm slot. The button is ASKED for by its own `GetID()` — the
|
||||
@@ -236,6 +266,7 @@ fn shift_compare_over_a_bag_item_seats_on_the_doll_slot() {
|
||||
/// close button hides it.
|
||||
#[test]
|
||||
fn item_ref_tooltip_renders_a_chat_link() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
seed_items(&mut s);
|
||||
s.run(
|
||||
@@ -272,7 +303,7 @@ fn item_ref_tooltip_renders_a_chat_link() {
|
||||
fn doll_hover_renders_the_live_instance_and_never_self_compares() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness_with_bags();
|
||||
s.set_unit("player", Some(UnitState::default()));
|
||||
s.set_unit("player", Some(player()));
|
||||
seed_items(&mut s);
|
||||
// Break the equipped helm: instance pair (0, 40); the template stays authored-full.
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
@@ -326,9 +357,10 @@ fn doll_hover_renders_the_live_instance_and_never_self_compares() {
|
||||
);
|
||||
s.set_modifiers(false, false, false);
|
||||
|
||||
// The doll hover: the live pair, not the template's 40/40.
|
||||
s.run("BenillaPaperDollSlot_OnEnter(CharacterHeadSlot)")
|
||||
.unwrap();
|
||||
// The doll hover: the live pair, not the template's 40/40. Stock
|
||||
// `PaperDollItemSlotButton_OnEnter` reads `this`, so the mouse is what reaches it — and moving
|
||||
// OFF the bag button is part of what this test needs anyway.
|
||||
hover(&mut s, "CharacterHeadSlot");
|
||||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||||
let found: String = s
|
||||
.eval(
|
||||
|
||||
@@ -366,7 +366,7 @@ fn feed_stable_booth(
|
||||
};
|
||||
// Written every frame, selection or none — a stale yaw would snap the model the moment one is
|
||||
// picked (the pet paper doll's arrangement).
|
||||
booth.yaw = script.stable_yaw();
|
||||
booth.yaw = script.model_pane_facing("PetStableModel");
|
||||
|
||||
// `GetSelectedStablePet()`'s own answer: `0` = the summoned pet, `1..=2` a bought stable slot,
|
||||
// `-1` nothing. Reading the binding's translation rather than the raw petNumber is deliberate —
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Drives the REAL `assets/ui/CharacterFrame.xml` stat rows through the engine — the first test
|
||||
//! that executes the paper doll's own `PaperDollFrame_Set*` Lua at all.
|
||||
//! Drives the REAL `Interface\FrameXML\PaperDollFrame.xml` stat rows through the engine — the
|
||||
//! first test that executes the paper doll's own `PaperDollFrame_Set*` Lua at all, and since
|
||||
//! decision 1751 that Lua is the reference's own rather than a transcription of it.
|
||||
//!
|
||||
//! That gap is why this file exists. The character sheet's whole buff-decomposition surface — the
|
||||
//! green number, the red number, the `(base +x -y)` tooltip — sat broken from the day it shipped
|
||||
@@ -19,17 +20,38 @@ mod common;
|
||||
|
||||
use benilla_ui::script::{UiScript, UnitCombatStats, UnitState};
|
||||
|
||||
/// The character window's load prefix, in `assets/ui/benilla.toc` order. The .toc's own entry says
|
||||
/// it "depends only on Fonts/UiPanels/GameTooltip"; `ItemButtonTemplate.xml` joins them because the
|
||||
/// doll's equipment slots are `SetItemButton*` buttons.
|
||||
const FILES: [&str; 6] = [
|
||||
/// The paper doll's load prefix, in `assets/ui/benilla.toc` order.
|
||||
///
|
||||
/// **`CharacterFrame.xml` is deliberately not here.** These tests repaint the stat rows directly;
|
||||
/// they never open the window, and `PaperDollFrame`'s only tie to its container is a `parent=`
|
||||
/// name, which the loader warns about and falls back from. Pulling the container in would drag
|
||||
/// `CharacterFrame_OnLoad`'s four external dependencies (the unit frames, the XP bar,
|
||||
/// `TextStatusBar.lua`, the panel-tab kit — see `ui_script::test_ui::CHARACTER_UI`) into a test
|
||||
/// about arithmetic.
|
||||
///
|
||||
/// `GlobalStrings.lua` and `BasicControls.xml` are not scenery either: the stock file has no
|
||||
/// `X = X or "…"` fallbacks, and every row here formats a real string — `SPELL_STAT0_NAME`..`4`
|
||||
/// through `TEXT()` (`PaperDollFrame.lua:143`), `RESISTANCE<n>_NAME` and
|
||||
/// `RESISTANCE_TOOLTIP_SUBTEXT` (`:184`/`:224`), `ARMOR` and `ARMOR_TOOLTIP` (`:236`/`:249`).
|
||||
const FILES: [&str; 10] = [
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml",
|
||||
"BasicControls.xml",
|
||||
// The reference's own since 1751 window 24 — `common::load_ui` speaks both stores.
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
// `Model_OnLoad` — the model pane's `<OnLoad>` calls it, so this is a LOAD-time dependency of
|
||||
// the paper doll, not of the window. The reference declares it in `UIParent.lua`; ours lives
|
||||
// in our counterpart of that file.
|
||||
"UIParent.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
"CharacterFrame.xml",
|
||||
// `CooldownFrameTemplate` and `CooldownFrame_SetTimer`. Not scenery: every one of the 20 slot
|
||||
// buttons runs `PaperDollItemSlotButton_Update` from its own OnLoad, which calls
|
||||
// `CooldownFrame_SetTimer` on `$parentCooldown` unconditionally (`PaperDollFrame.lua:692`) —
|
||||
// so leaving this out is 20 loader errors before the first assertion.
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\PaperDollFrame.xml",
|
||||
];
|
||||
|
||||
fn load_ui(script: &UiScript) {
|
||||
@@ -86,6 +108,12 @@ fn text_of(script: &mut UiScript, region: &str) -> String {
|
||||
.unwrap_or_else(|e| panic!("reading {region}: {e}"))
|
||||
}
|
||||
|
||||
fn tooltip_of_field(script: &mut UiScript, frame: &str, field: &str) -> String {
|
||||
script
|
||||
.eval::<String>(&format!(r#"return getglobal("{frame}").{field}"#))
|
||||
.unwrap_or_else(|e| panic!("reading {frame}.{field}: {e}"))
|
||||
}
|
||||
|
||||
fn tooltip_of(script: &mut UiScript, frame: &str) -> String {
|
||||
script
|
||||
.eval::<String>(&format!(r#"return getglobal("{frame}").tooltip"#))
|
||||
@@ -163,13 +191,16 @@ fn resistance_rows_colour_by_which_half_is_bigger() {
|
||||
let mut s = seated();
|
||||
painted(&mut s);
|
||||
|
||||
// The five magic rows are the sheet's own school order (BENILLA_MAGICRES_IDS); resolve each
|
||||
// row's school from the frame rather than assuming, so a row-order change cannot silently
|
||||
// re-point these assertions.
|
||||
// Resolve each row's school from the FRAME's own `id=`, never from a table: that is exactly
|
||||
// what the reference's `PaperDollFrame_SetResistances` does (`UnitResistance("player",
|
||||
// frame:GetID())`), and it means a row-order change cannot silently re-point these assertions.
|
||||
// It used to read our deleted file's `BENILLA_MAGICRES_IDS`; the stock XML carries the same
|
||||
// mapping on the frames themselves (`MagicResFrame1 id="6"` arcane, then 2..5 fire/nature/
|
||||
// frost/shadow — `PaperDollFrame.xml:654-758`).
|
||||
let row_of = |s: &mut UiScript, school: i64| -> usize {
|
||||
s.eval::<i64>(&format!(
|
||||
"for i = 1, NUM_RESISTANCE_TYPES do \
|
||||
if BENILLA_MAGICRES_IDS[i] == {school} then return i end \
|
||||
if getglobal(\"MagicResFrame\"..i):GetID() == {school} then return i end \
|
||||
end return 0"
|
||||
))
|
||||
.expect("the school→row map") as usize
|
||||
@@ -191,6 +222,124 @@ fn resistance_rows_colour_by_which_half_is_bigger() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **The melee block: the seven-value `UnitDamage` unpacked positionally by the reference's own
|
||||
/// Lua**, and the arithmetic it does with the last three.
|
||||
///
|
||||
/// The stat rows above pin `UnitStat`/`UnitResistance`/`UnitArmor`. This one pins the family
|
||||
/// decision 1793 warns about hardest, because its consumer is arithmetic rather than a lookup:
|
||||
/// `PaperDollFrame_SetDamage` destructures **seven** values and immediately divides by the
|
||||
/// seventh. One value short and `percent` is nil — `(minDamage / nil)` raises; one value long and
|
||||
/// every term after it is off by a slot and the numbers come out plausible and wrong. Asserting
|
||||
/// the rendered strings is what tells those two apart from correct.
|
||||
///
|
||||
/// The numbers below are the reference's own formula, not ours: `displayMin`/`displayMax` are the
|
||||
/// raw range floored/ceiled, while the TOOLTIP range is the same numbers with the multiplier
|
||||
/// divided out and the flat bonuses subtracted (`PaperDollFrame.lua:279-283`) — which is why
|
||||
/// "40 - 62" and "27 - 47" are both right and different.
|
||||
#[test]
|
||||
fn the_melee_block_unpacks_seven_values_and_does_the_references_arithmetic() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = seated();
|
||||
// 40.5-61.5 at 2.6 s, +12/-3 flat and a x1.1 multiplier: every one of the seven slots is
|
||||
// distinct and non-zero, so a slot read from the wrong position cannot coincide with the
|
||||
// right answer.
|
||||
s.set_player_combat_stats(Some(UnitCombatStats {
|
||||
main_attack_time_ms: 2600,
|
||||
min_damage: 40.5,
|
||||
max_damage: 61.5,
|
||||
physical_bonus_pos: 12,
|
||||
physical_bonus_neg: -3,
|
||||
damage_percent: 1.1,
|
||||
attack_power: 780,
|
||||
attack_power_pos: 30,
|
||||
attack_power_neg: -10,
|
||||
..stats()
|
||||
}));
|
||||
s.run("PaperDollFrame_SetDamage() PaperDollFrame_SetAttackPower()")
|
||||
.expect("the melee rows repaint");
|
||||
|
||||
// The ROW: the raw range, green because the bonuses are net positive.
|
||||
assert_eq!(
|
||||
text_of(&mut s, "CharacterDamageFrameStatText"),
|
||||
format!("{GREEN}40 - 62|r")
|
||||
);
|
||||
// The TOOLTIP: the same range with the multiplier divided out and the flat bonuses removed,
|
||||
// then each modifier appended in the reference's own order and colours.
|
||||
assert_eq!(
|
||||
tooltip_of_field(&mut s, "CharacterDamageFrame", "damage"),
|
||||
format!("27 - 47{GREEN} +12|r{RED} -3|r{GREEN} x110%|r")
|
||||
);
|
||||
let speed: f64 = s
|
||||
.eval("return CharacterDamageFrame.attackSpeed")
|
||||
.expect("the hover's attack speed");
|
||||
assert!((speed - 2.6).abs() < 1e-6, "got {speed}");
|
||||
// dps = fullDamage / speed, fullDamage = (base + pos + neg) * percent = 51.0.
|
||||
let dps: f64 = s.eval("return CharacterDamageFrame.dps").expect("the dps");
|
||||
assert!((dps - 51.0 / 2.6).abs() < 0.01, "got {dps}");
|
||||
// No offhand weapon: the reference NILS the offhand speed, which is what its hover branches on.
|
||||
assert!(s
|
||||
.eval::<Option<f64>>("return CharacterDamageFrame.offhandAttackSpeed")
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// Attack power goes through `PaperDollFormatStat`, whose colour law is "red if anything is
|
||||
// negative, green otherwise" — so a mixed pair is RED even though the net is positive.
|
||||
assert_eq!(
|
||||
text_of(&mut s, "CharacterAttackPowerFrameStatText"),
|
||||
format!("{RED}800|r")
|
||||
);
|
||||
let ap_tip = tooltip_of(&mut s, "CharacterAttackPowerFrame");
|
||||
assert!(
|
||||
ap_tip.contains("800") && ap_tip.contains("(780") && ap_tip.contains("+30"),
|
||||
"the AP tooltip names the effective, the base and the buff, got {ap_tip:?}"
|
||||
);
|
||||
|
||||
// The plain leg: no bonuses and no multiplier, so `totalBonus == 0` and the row carries no
|
||||
// colour escape at all — the branch a wrong `percent` would silently take.
|
||||
s.set_player_combat_stats(Some(UnitCombatStats {
|
||||
main_attack_time_ms: 2600,
|
||||
min_damage: 40.5,
|
||||
max_damage: 61.5,
|
||||
damage_percent: 1.0,
|
||||
..stats()
|
||||
}));
|
||||
s.run("PaperDollFrame_SetDamage()").unwrap();
|
||||
assert_eq!(text_of(&mut s, "CharacterDamageFrameStatText"), "40 - 62");
|
||||
}
|
||||
|
||||
/// **The ranged block with nothing in the ranged slot** — the `NOT_APPLICABLE` fallback all three
|
||||
/// ranged rows share, and the `PaperDollFrame.noRanged` latch that carries it between them.
|
||||
///
|
||||
/// It is the reachable state for eight of nine classes at level 1, and it is the branch that
|
||||
/// decides whether `PaperDollFrame_SetRangedDamage` unpacks `UnitRangedDamage`'s six values at
|
||||
/// all — so it has to be pinned before the numbers are worth anything.
|
||||
#[test]
|
||||
fn the_ranged_rows_fall_back_to_not_applicable_with_an_empty_ranged_slot() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = seated();
|
||||
s.run(
|
||||
"PaperDollFrame_SetRangedAttack() PaperDollFrame_SetRangedDamage() PaperDollFrame_SetRangedAttackPower()",
|
||||
)
|
||||
.expect("the ranged rows repaint");
|
||||
|
||||
let na: String = s
|
||||
.eval("return NOT_APPLICABLE")
|
||||
.expect("the ref's own string");
|
||||
for row in [
|
||||
"CharacterRangedAttackFrameStatText",
|
||||
"CharacterRangedDamageFrameStatText",
|
||||
"CharacterRangedAttackPowerFrameStatText",
|
||||
] {
|
||||
assert_eq!(text_of(&mut s, row), na, "{row}");
|
||||
}
|
||||
// …and the hover has nothing to show, which is the guard
|
||||
// `CharacterRangedDamageFrame_OnEnter` opens with.
|
||||
assert!(s
|
||||
.eval::<Option<String>>("return CharacterRangedDamageFrame.damage")
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
/// Armor is the one row of this family the local server never decomposes — item armor lands in
|
||||
/// `UNIT_FIELD_RESISTANCES[0]` itself, not in the buff split — so its plain-number leg is the live
|
||||
/// shape and worth pinning as such.
|
||||
|
||||
@@ -154,6 +154,10 @@ pub struct UnitCombatStats {
|
||||
/// WHICH skill via [`weapon_subclass_skill`] (unarmed = [`SKILL_UNARMED`]) and reads the pair
|
||||
/// from `PLAYER_SKILL_INFO`; `UnitAttackBothHands` serves it verbatim.
|
||||
pub main_weapon_skill: (u32, i32),
|
||||
/// The OFF hand's, read the same way. `UnitAttackBothHands` pushes both hands (§4 of wow-re's
|
||||
/// `pet-paperdoll-stat-api.md`: `0x518810` calls `[vtbl+0xb0]` twice, hand 0 then hand 1), and
|
||||
/// an empty or non-weapon off hand is Unarmed exactly as the main hand is.
|
||||
pub offhand_weapon_skill: (u32, i32),
|
||||
/// The ranged weapon's skill pair (`UnitRangedAttack`).
|
||||
pub ranged_weapon_skill: (u32, i32),
|
||||
/// The [`SKILL_DEFENSE`] line's `(value, temp+perm bonus)` pair, read out of `PLAYER_SKILL_INFO`
|
||||
@@ -200,6 +204,7 @@ impl Default for UnitCombatStats {
|
||||
ranged_min_damage: 0.0,
|
||||
ranged_max_damage: 0.0,
|
||||
main_weapon_skill: (0, 0),
|
||||
offhand_weapon_skill: (0, 0),
|
||||
ranged_weapon_skill: (0, 0),
|
||||
defense_skill: (0, 0),
|
||||
has_wand: false,
|
||||
@@ -466,21 +471,6 @@ impl super::UiScript {
|
||||
pub fn take_inventory_uses(&mut self) -> Vec<u32> {
|
||||
std::mem::take(&mut self.model_mut().inventory_uses)
|
||||
}
|
||||
|
||||
/// The paper-doll model pane's yaw in radians (`BenillaPaperDollModel_SetFacing` wrote it) —
|
||||
/// a persistent value the app samples each frame to pose the booth's body bake (decision 0208
|
||||
/// §5), not a drain. `0.0` until Lua sets it (the ref's own default, 0.61, is authored in the
|
||||
/// window's OnLoad).
|
||||
pub fn paperdoll_yaw(&self) -> f32 {
|
||||
self.model_ref().paperdoll_yaw
|
||||
}
|
||||
|
||||
/// The **pet** paper doll pane's yaw (`BenillaPetPaperDollModel_SetFacing` wrote it) — the
|
||||
/// exact twin of [`Self::paperdoll_yaw`] and of `UiScript::inspect_yaw`, sampled each frame by
|
||||
/// `crate::ui_pet_doll` onto its booth (decision 1057).
|
||||
pub fn pet_paperdoll_yaw(&self) -> f32 {
|
||||
self.model_ref().pet_paperdoll_yaw
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a unit's combat-stats snapshot under a short model borrow, routed by token: `"player"` and
|
||||
@@ -502,6 +492,16 @@ fn with_unit_stats<T>(
|
||||
f(pushed.unwrap_or(&absent))
|
||||
}
|
||||
|
||||
/// The post-processing both skill-shaped bindings share: **`if (mod + base) < 0 then mod = -base`**
|
||||
/// — so the pair can never sum below zero (`0x519298` for `UnitDefense`, `0x5188a7` per hand for
|
||||
/// `UnitAttackBothHands`; wow-re `ui/scratch/pet-paperdoll-stat-api.md` §4). A debuff deeper than
|
||||
/// the skill itself reads as "reduced to 0", never as a negative total.
|
||||
fn skill_clamped((base, modifier): (u32, i32)) -> (i64, i64) {
|
||||
let base = i64::from(base);
|
||||
let modifier = i64::from(modifier);
|
||||
(base, if modifier + base < 0 { -base } else { modifier })
|
||||
}
|
||||
|
||||
/// A **non-player** unit's answer to both skill-shaped questions — `UnitDefense` and
|
||||
/// `UnitAttackBothHands` — which is `UNIT_FIELD_LEVEL * 5`, flat.
|
||||
///
|
||||
@@ -839,27 +839,39 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// UnitAttackBothHands("player") → (base, modifier) — the main-hand weapon-skill line: skill
|
||||
// value + its temp+perm bonus, resolved app-side via the weapon_subclass_skill table (unarmed
|
||||
// = SKILL_UNARMED when nothing's equipped).
|
||||
// UnitAttackBothHands("player") → (mainBase, mainMod, offBase, offMod) — **FOUR** returns, one
|
||||
// pair per hand: each hand's weapon-skill line, value + its temp+perm bonus, resolved app-side
|
||||
// via the weapon_subclass_skill table (unarmed = SKILL_UNARMED for an empty or non-weapon
|
||||
// hand).
|
||||
//
|
||||
// A non-player unit takes the same virtual fork `UnitDefense` does (`0x6136b0`, wow-re
|
||||
// `ui/scratch/pet-paperdoll-stat-api.md`) and lands on the identical `level * 5` — and that
|
||||
// one **ignores its hand index outright**, so both hands answer the same pair. The pet sheet's
|
||||
// Attack row therefore reads 300 at level 60, exactly like its Defense row.
|
||||
// **It answered two until decision 1810**, which is 1793's shape at its quietest: `0x518810`
|
||||
// calls `[vtbl+0xb0]` TWICE, hand 0 then hand 1, and pushes `(base0, mod0, base1, mod1)`
|
||||
// (VERIFIED, wow-re `ui/scratch/pet-paperdoll-stat-api.md` §4). The reference's own
|
||||
// `PaperDollFrame_SetAttackBothHands` destructures only the first two — its next line is
|
||||
// `-- FIXME: The offhand stats aren't displayed yet.` — so putting the stock character sheet on
|
||||
// the chain could never have exposed this. An addon reading four gets two nils.
|
||||
//
|
||||
// A non-player unit takes the same virtual fork `UnitDefense` does (`0x6136b0`) and lands on
|
||||
// the identical `level * 5` — and that one **ignores its hand index outright**, so both hands
|
||||
// answer the same pair. The pet sheet's Attack row reads 300 at level 60, like its Defense row.
|
||||
g.set(
|
||||
"UnitAttackBothHands",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(match token.as_deref() {
|
||||
Some("player") => model.player_combat_stats.as_ref().map_or((0, 0), |s| {
|
||||
(
|
||||
i64::from(s.main_weapon_skill.0),
|
||||
i64::from(s.main_weapon_skill.1),
|
||||
)
|
||||
}),
|
||||
Some("pet") => (cgunit_skill(&model, "pet"), 0),
|
||||
_ => (0, 0),
|
||||
Some("player") => model
|
||||
.player_combat_stats
|
||||
.as_ref()
|
||||
.map_or((0, 0, 0, 0), |s| {
|
||||
let (mb, mm) = skill_clamped(s.main_weapon_skill);
|
||||
let (ob, om) = skill_clamped(s.offhand_weapon_skill);
|
||||
(mb, mm, ob, om)
|
||||
}),
|
||||
Some("pet") => {
|
||||
let base = cgunit_skill(&model, "pet");
|
||||
(base, 0, base, 0)
|
||||
}
|
||||
_ => (0, 0, 0, 0),
|
||||
})
|
||||
})?,
|
||||
)?;
|
||||
@@ -895,9 +907,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(match token.as_deref() {
|
||||
Some("player") => model.player_combat_stats.as_ref().map_or((0, 0), |s| {
|
||||
(i64::from(s.defense_skill.0), i64::from(s.defense_skill.1))
|
||||
}),
|
||||
Some("player") => model
|
||||
.player_combat_stats
|
||||
.as_ref()
|
||||
.map_or((0, 0), |s| skill_clamped(s.defense_skill)),
|
||||
Some("pet") => (cgunit_skill(&model, "pet"), 0),
|
||||
_ => (0, 0),
|
||||
})
|
||||
@@ -1174,33 +1187,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// BenillaPaperDollModel_SetFacing(radians) — the model pane's rotate buttons write the bake
|
||||
// yaw here (persistent; the app samples UiScript::paperdoll_yaw each frame — decision 0208
|
||||
// §5's rotate-adjusts-the-bake). Benilla-named: the real client's Model:SetFacing is a widget
|
||||
// method on a live 3D pane; our doctrine-consistent still carries one scalar.
|
||||
g.set(
|
||||
"BenillaPaperDollModel_SetFacing",
|
||||
lua.create_function(|lua, radians: f32| {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.paperdoll_yaw = radians;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// BenillaPetPaperDollModel_SetFacing(radians) — the PET pane's own bake yaw (decision 1057),
|
||||
// the exact twin of `BenillaInspectModel_SetFacing`: a third scalar, because tab 1 and tab 2
|
||||
// are two panes that can sit at two different facings.
|
||||
g.set(
|
||||
"BenillaPetPaperDollModel_SetFacing",
|
||||
lua.create_function(|lua, radians: f32| {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.pet_paperdoll_yaw = radians;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1240,6 +1226,7 @@ mod tests {
|
||||
ranged_min_damage: 31.0,
|
||||
ranged_max_damage: 47.0,
|
||||
main_weapon_skill: (25, 2),
|
||||
offhand_weapon_skill: (20, 0),
|
||||
ranged_weapon_skill: (18, 0),
|
||||
defense_skill: (55, 4),
|
||||
has_wand: false,
|
||||
@@ -1488,9 +1475,10 @@ mod tests {
|
||||
(52, 0, -3)
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitAttackBothHands("player")"#)
|
||||
s.eval::<(i64, i64, i64, i64)>(r#"return UnitAttackBothHands("player")"#)
|
||||
.unwrap(),
|
||||
(25, 2)
|
||||
(25, 2, 20, 0),
|
||||
"FOUR values, one pair per hand"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitRangedAttack("player")"#)
|
||||
@@ -1509,9 +1497,57 @@ mod tests {
|
||||
(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitAttackBothHands("target")"#)
|
||||
s.eval::<(i64, i64, i64, i64)>(r#"return UnitAttackBothHands("target")"#)
|
||||
.unwrap(),
|
||||
(0, 0)
|
||||
(0, 0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
/// **`UnitAttackBothHands` pushes FOUR values, one pair per hand — and it pushed two until
|
||||
/// decision 1810.**
|
||||
///
|
||||
/// The quietest shape decision 1793 describes: `0x518810` calls `[vtbl+0xb0]` twice, hand 0
|
||||
/// then hand 1, and pushes `(base0, mod0, base1, mod1)` (VERIFIED, wow-re
|
||||
/// `ui/scratch/pet-paperdoll-stat-api.md` §4). The reference's own
|
||||
/// `PaperDollFrame_SetAttackBothHands` destructures only the first two — its very next line is
|
||||
/// `-- FIXME: The offhand stats aren't displayed yet.` — so no amount of running the stock
|
||||
/// character sheet could have exposed it. An addon reading four got two nils.
|
||||
///
|
||||
/// The pet leg is here too because it is the same call: `CGUnit_C 0x6136b0` takes a hand index
|
||||
/// and **never reads it**, so both hands answer the identical `level * 5`.
|
||||
#[test]
|
||||
fn attack_both_hands_answers_a_pair_per_hand() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_player_combat_stats(Some(UnitCombatStats {
|
||||
main_weapon_skill: (300, 5),
|
||||
offhand_weapon_skill: (275, -12),
|
||||
..stats()
|
||||
}));
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64, i64, i64)>(r#"return UnitAttackBothHands("player")"#)
|
||||
.unwrap(),
|
||||
(300, 5, 275, -12),
|
||||
"each hand's own skill line, in hand order"
|
||||
);
|
||||
// …and the per-hand clamp the binding applies after each virtual call
|
||||
// (`0x5188a7`: `if (out2 + out1 < 0) out2 = -out1`), which `UnitDefense` shares.
|
||||
s.set_player_combat_stats(Some(UnitCombatStats {
|
||||
main_weapon_skill: (40, -100),
|
||||
offhand_weapon_skill: (40, -40),
|
||||
defense_skill: (30, -90),
|
||||
..stats()
|
||||
}));
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64, i64, i64)>(r#"return UnitAttackBothHands("player")"#)
|
||||
.unwrap(),
|
||||
(40, -40, 40, -40),
|
||||
"a debuff deeper than the skill reads as reduced-to-0, never a negative total"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitDefense("player")"#)
|
||||
.unwrap(),
|
||||
(30, -30),
|
||||
"the same clamp, the same law"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1650,10 +1686,10 @@ mod tests {
|
||||
"a level-60 pet: level * 5, modifier flat 0"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitAttackBothHands("pet")"#)
|
||||
s.eval::<(i64, i64, i64, i64)>(r#"return UnitAttackBothHands("pet")"#)
|
||||
.unwrap(),
|
||||
(300, 0),
|
||||
"the Attack row takes the same fork — and ignores its hand index"
|
||||
(300, 0, 300, 0),
|
||||
"the Attack row takes the same fork — and ignores its hand index, so both pairs match"
|
||||
);
|
||||
// The pet's snapshot carries neither pair, which is the whole point: the numbers above
|
||||
// cannot have come from it.
|
||||
@@ -1949,29 +1985,57 @@ mod tests {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
/// The character and pet paper dolls' facings are **the panes' own `SetRotation` state**, read
|
||||
/// back by name — not the two benilla-named scalars they were until decision 1751.
|
||||
///
|
||||
/// The two tabs are two `<PlayerModel>`s that can sit at different facings, which is the whole
|
||||
/// reason this is per-pane at all: turning one must leave the other alone. It used to be
|
||||
/// guaranteed by there being two fields on `Model`; now it is guaranteed by there being two
|
||||
/// frames, which is also what the reference relies on (`Model_RotateLeft(model)` takes the
|
||||
/// pane).
|
||||
#[test]
|
||||
fn paperdoll_facing_persists_for_the_app_to_sample() {
|
||||
fn each_model_pane_carries_its_own_facing_for_the_app_to_sample() {
|
||||
let s = UiScript::new().unwrap();
|
||||
assert_eq!(s.paperdoll_yaw(), 0.0, "unset default");
|
||||
s.run("BenillaPaperDollModel_SetFacing(0.61)").unwrap();
|
||||
assert_eq!(s.paperdoll_yaw(), 0.61);
|
||||
// Persistent, not a drain — two reads see the same value; a later write replaces it.
|
||||
assert_eq!(s.paperdoll_yaw(), 0.61);
|
||||
s.run("BenillaPaperDollModel_SetFacing(-0.5)").unwrap();
|
||||
assert_eq!(s.paperdoll_yaw(), -0.5);
|
||||
}
|
||||
assert_eq!(
|
||||
s.model_pane_facing("CharacterModelFrame"),
|
||||
0.0,
|
||||
"a pane whose file has not loaded"
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
CreateFrame("PlayerModel", "CharacterModelFrame")
|
||||
CreateFrame("PlayerModel", "PetModelFrame")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// The reference's own default, as `Model_OnLoad` writes it.
|
||||
s.run("CharacterModelFrame:SetRotation(0.61)").unwrap();
|
||||
assert_eq!(s.model_pane_facing("CharacterModelFrame"), 0.61);
|
||||
// Persistent, not a drain — two reads see the same value.
|
||||
assert_eq!(s.model_pane_facing("CharacterModelFrame"), 0.61);
|
||||
assert_eq!(
|
||||
s.model_pane_facing("PetModelFrame"),
|
||||
0.0,
|
||||
"the pet pane did not move"
|
||||
);
|
||||
|
||||
/// The pet pane's yaw is a THIRD scalar, not a share of the character pane's: the two tabs can
|
||||
/// sit at different facings, so a write to either must leave the other alone.
|
||||
#[test]
|
||||
fn pet_paperdoll_facing_is_independent_of_the_character_panes() {
|
||||
let s = UiScript::new().unwrap();
|
||||
assert_eq!(s.pet_paperdoll_yaw(), 0.0, "unset default");
|
||||
s.run("BenillaPetPaperDollModel_SetFacing(1.2)").unwrap();
|
||||
assert_eq!(s.pet_paperdoll_yaw(), 1.2);
|
||||
assert_eq!(s.paperdoll_yaw(), 0.0, "the character pane did not move");
|
||||
s.run("BenillaPaperDollModel_SetFacing(0.61)").unwrap();
|
||||
assert_eq!(s.pet_paperdoll_yaw(), 1.2, "…and neither did the pet pane");
|
||||
assert_eq!(s.paperdoll_yaw(), 0.61);
|
||||
s.run("PetModelFrame:SetRotation(1.2)").unwrap();
|
||||
assert_eq!(s.model_pane_facing("PetModelFrame"), 1.2);
|
||||
assert_eq!(
|
||||
s.model_pane_facing("CharacterModelFrame"),
|
||||
0.61,
|
||||
"…and neither did the character pane"
|
||||
);
|
||||
|
||||
s.run("CharacterModelFrame:SetRotation(-0.5)").unwrap();
|
||||
assert_eq!(s.model_pane_facing("CharacterModelFrame"), -0.5);
|
||||
|
||||
// A name nothing declares reads 0.0 rather than raising — the app samples this every
|
||||
// frame, including before the window's file has loaded.
|
||||
assert_eq!(s.model_pane_facing("NoSuchModelFrame"), 0.0);
|
||||
// …and so does a name that IS declared but is not a model pane, which is the shape a
|
||||
// renamed window would take.
|
||||
s.run(r#"CreateFrame("Frame", "NotAPane")"#).unwrap();
|
||||
assert!(s.model_pane("NotAPane").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,14 @@ impl UiScript {
|
||||
});
|
||||
QuadContent::Cooldown { fraction, flash }
|
||||
}
|
||||
// A `<Model>`/`<PlayerModel>` pane's content hole, carrying the pane's own
|
||||
// name so the app can join it to the bake that window keeps (see
|
||||
// [`QuadContent::ModelPane`]). Both widget kinds share `KindState::Model`
|
||||
// because the client's `CGCharacterModelBase` extends `CSimpleModel`, and
|
||||
// both draw the same way here.
|
||||
Some(crate::widget::KindState::Model(_)) => QuadContent::ModelPane {
|
||||
name: frame.and_then(|f| f.name.clone()),
|
||||
},
|
||||
_ => QuadContent::Frame,
|
||||
};
|
||||
(
|
||||
|
||||
@@ -439,16 +439,26 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// nil. The binding pushes 3 on both legs (`mov eax,3`), so a caller destructuring the third
|
||||
// gets `0`. Returning nil there would be a different contract.
|
||||
//
|
||||
// A unit token the resolver does not recognise **raises**, rather than answering nils — that
|
||||
// is the binding's own behaviour, and swallowing it would hide a typo'd token forever.
|
||||
// **RECOGNISED and UNRESOLVABLE are two different things**, and this binding conflated them
|
||||
// until the stock character sheet came off the chain (decision 1751). It resolves its argument
|
||||
// through the GENERAL unit-token resolver `0x515940`, so an *unrecognised* token raises via the
|
||||
// L-less `luaL_error 0x7040e0` at `0x515c1a` — but "a recognised-but-unresolvable token, a
|
||||
// non-player, a guildless player and a not-yet-arrived cache record ALL return (nil, nil, 0)"
|
||||
// (wow-re `system/ui` ledger, `0x4c9330`, VERIFIED, `scratch/guild-api-carve.md`).
|
||||
//
|
||||
// Ours raised on any token the model held no unit for, which includes `"player"` before the
|
||||
// app has pushed the first snapshot. Stock `PaperDollFrame_SetGuild` calls
|
||||
// `GetGuildInfo("player")` UNGUARDED out of `PaperDollFrame_OnShow` (`PaperDollFrame.lua:117`,
|
||||
// reached from `:569`), so opening the character window in the frames before that push raised
|
||||
// where the reference prints an empty guild line. [`check_unit_token`] is the resolver's own
|
||||
// three-way split and every other unit binding already goes through it.
|
||||
g.set(
|
||||
"GetGuildInfo",
|
||||
lua.create_function(|lua, unit: String| {
|
||||
lua.create_function(|lua, unit: Option<String>| {
|
||||
crate::script::unit::check_unit_token(&unit)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let Some(state) = model.unit(&unit) else {
|
||||
return Err(mlua::Error::RuntimeError(format!(
|
||||
"GetGuildInfo: unknown unit '{unit}'"
|
||||
)));
|
||||
let Some(state) = unit.as_deref().and_then(|u| model.unit(u)) else {
|
||||
return Ok((Value::Nil, Value::Nil, Value::Integer(0)));
|
||||
};
|
||||
let Some(guild) = state.guild.as_ref() else {
|
||||
return Ok((Value::Nil, Value::Nil, Value::Integer(0)));
|
||||
@@ -917,6 +927,33 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// **`GetGuildInfo` splits three ways, not two** — wow-re `0x4c9330` (VERIFIED,
|
||||
/// `scratch/guild-api-carve.md`): only an *unrecognised* token raises; a **recognised**
|
||||
/// token that resolves to nothing answers `(nil, nil, 0)` like a guildless player does.
|
||||
///
|
||||
/// The distinction is not academic. Stock `PaperDollFrame_SetGuild` calls
|
||||
/// `GetGuildInfo("player")` unguarded from `PaperDollFrame_OnShow`, so before this split
|
||||
/// existed, opening the character window in the frames before the app pushed its first player
|
||||
/// snapshot raised — found by putting the reference's own `PaperDollFrame.xml` on the chain
|
||||
/// (decision 1751). `"player"` with no unit behind it is the whole test.
|
||||
#[test]
|
||||
fn a_recognised_but_unresolved_unit_answers_nils_and_only_an_unknown_token_raises() {
|
||||
let s = crate::script::UiScript::new().unwrap();
|
||||
// Recognised, resolves to nothing — the paper doll's own case at world entry.
|
||||
let (name, rank, idx): (Option<String>, Option<String>, i64) = s
|
||||
.eval(r#"return GetGuildInfo("player")"#)
|
||||
.expect("a recognised token must not raise");
|
||||
assert_eq!((name, rank, idx), (None, None, 0));
|
||||
// …and the third value is the NUMBER zero on that leg, not nil (`mov eax,3` on both).
|
||||
assert!(s
|
||||
.eval::<bool>(r#"local a,b,c = GetGuildInfo("party3") return c == 0"#)
|
||||
.unwrap());
|
||||
// Unrecognised: the resolver's own raise, unchanged.
|
||||
assert!(s
|
||||
.eval::<i64>(r#"return GetGuildInfo("nosuchunit")"#)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
/// The thirteen-bit table is the API contract, and its non-monotonicity is the trap: Promote
|
||||
/// and Demote sit at indices 5 and 6 with bits ABOVE Invite and Remove at 7 and 8. A
|
||||
/// `1 << (i - 1)` implementation passes indices 1-4 and fails the other nine, so this asserts
|
||||
|
||||
@@ -1272,7 +1272,6 @@ pub(crate) struct Model {
|
||||
/// 12th) — recomputed on every inventory push, which fires `UPDATE_INVENTORY_ALERTS`
|
||||
/// unconditionally, the client's own shape (see [`char_stats`]).
|
||||
pub(crate) inventory_alerts: [u8; 12],
|
||||
pub(crate) paperdoll_yaw: f32,
|
||||
/// Inventory-slot ids queued by `UseInventoryItem` (decision 0208 phase 1b, `cursor`'s `doll`
|
||||
/// submodule) — drained by the app into `CMSG_USE_ITEM` against the equipped position.
|
||||
pub(crate) inventory_uses: Vec<u32>,
|
||||
@@ -1291,12 +1290,11 @@ pub(crate) struct Model {
|
||||
pub(crate) inspect_notifies: Vec<String>,
|
||||
/// `ClearInspectPlayer` was called — drained by the app, which drops its inspect target.
|
||||
pub(crate) inspect_clear: bool,
|
||||
/// The inspect model pane's bake yaw, the twin of [`Self::paperdoll_yaw`].
|
||||
/// The inspect model pane's bake yaw — the last of the benilla-named pane scalars beside
|
||||
/// [`Self::dressup_yaw`]. A migrated window's pane carries its own facing in `ModelState`
|
||||
/// (`UiScript::model_pane_facing`, decision 1751); these two remain because their windows are
|
||||
/// still ours, and each retires with its file.
|
||||
pub(crate) inspect_yaw: f32,
|
||||
/// The **pet** paper doll's model-pane bake yaw (decision 1057) — a third scalar for the same
|
||||
/// reason the inspect pane got a second: character tab 1 and tab 2 are two panes that can sit
|
||||
/// at two different facings, and the ref carries a `rotation` per `<PlayerModel>`.
|
||||
pub(crate) pet_paperdoll_yaw: f32,
|
||||
/// The dressing room's queued intents (decision 1060) — `BenillaDressUpModel_Dress/TryOn/Close`,
|
||||
/// drained by the app in order (see [`super::dressup`] on why order matters).
|
||||
pub(crate) dressup_intents: Vec<super::dressup::DressUpIntent>,
|
||||
@@ -1821,14 +1819,12 @@ impl Model {
|
||||
inventory_slots: Default::default(),
|
||||
bank_bag_slots: Default::default(),
|
||||
inventory_alerts: [0; 12],
|
||||
paperdoll_yaw: 0.0,
|
||||
inventory_uses: Vec::new(),
|
||||
weapon_enchants: [None; 2],
|
||||
inspect: None,
|
||||
inspect_notifies: Vec::new(),
|
||||
inspect_clear: false,
|
||||
inspect_yaw: 0.0,
|
||||
pet_paperdoll_yaw: 0.0,
|
||||
dressup_intents: Vec::new(),
|
||||
dressup_yaw: 0.0,
|
||||
unit_reach: HashMap::new(),
|
||||
|
||||
@@ -432,3 +432,42 @@ fn playermodel_install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
lua.set_named_registry_value(REG_PLAYERMODEL_METHODS, m)
|
||||
}
|
||||
|
||||
impl crate::script::UiScript {
|
||||
/// A named model pane's scene state, or `None` if no live frame carries that name as a
|
||||
/// `Model`/`PlayerModel`.
|
||||
///
|
||||
/// **The read side of the pane, for a host that draws it.** The app keeps one off-screen body
|
||||
/// bake per window (this module's header: the scene is ours, the pixels are the host's), and
|
||||
/// every frame it has to ask the pane the reference drives which way it is turned. Before
|
||||
/// decision 1751 that came from benilla-named globals a file of ours called
|
||||
/// (`BenillaPaperDollModel_SetFacing` and its four siblings, one scalar per window on
|
||||
/// [`Model`]); the stock FrameXML instead calls
|
||||
/// `Model_OnLoad`/`Model_RotateLeft`/`Model_RotateRight`/`Model_OnUpdate`, which write
|
||||
/// `PlayerModel:SetRotation` — i.e. this state, on the pane itself. So the host reads the pane.
|
||||
///
|
||||
/// By NAME rather than by handle because that is what the host knows: the character sheet's
|
||||
/// bake belongs to `CharacterModelFrame`, and since 1751 that name is the reference's own. A
|
||||
/// linear scan, because a name index would be a second store to keep true and a loaded
|
||||
/// interface holds single-digit model panes.
|
||||
pub fn model_pane(&self, name: &str) -> Option<ModelState> {
|
||||
self.model_ref()
|
||||
.arena
|
||||
.iter_frames()
|
||||
.find_map(|(_, f)| match (&f.name, &f.kind_state) {
|
||||
(Some(n), KindState::Model(m)) if n == name => Some(m.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A named model pane's yaw in radians — [`Self::model_pane`]'s one hot field, and `0.0` for a
|
||||
/// pane that does not exist yet (its window's file not loaded, or its `OnLoad` not run).
|
||||
///
|
||||
/// `0.0` rather than an `Option` because every caller is a per-frame booth mirror whose only
|
||||
/// other answer would be "keep the last one", and a pane that has not loaded has no last one.
|
||||
/// The reference's own default, 0.61, is authored in `Model_OnLoad`, so a real pane only reads
|
||||
/// 0.0 before it loads.
|
||||
pub fn model_pane_facing(&self, name: &str) -> f32 {
|
||||
self.model_pane(name).map_or(0.0, |m| m.facing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,10 +168,15 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// BenillaSetBoothTexture(textureRegion, slotToken) — the **square** twin of
|
||||
// SetPortraitTexture (decision 0208 §5): the same `portrait_unit` booth-image binding
|
||||
// WITHOUT the circular mask, for the paper doll's rectangular model pane (its texture region
|
||||
// samples the booth's body bake edge to edge — no frame ring to mask for). Benilla-named:
|
||||
// the real client's pane is a live 3D `<PlayerModel>`; ours is the doctrine-consistent
|
||||
// still (0105/0118), so the binding is ours, not the live API's.
|
||||
// WITHOUT the circular mask, for a body pane whose texture region samples the booth's bake
|
||||
// edge to edge (no frame ring to mask for). Benilla-named because it is not the live API: it
|
||||
// is the pane→booth join written in Lua.
|
||||
//
|
||||
// **RETIRING.** A migrated window's file is the reference's, which declares a bare
|
||||
// `<PlayerModel>` and no texture at all, so the join moved app-side
|
||||
// (`portrait::MODEL_PANE_BOOTHS`) and the widget draws itself (decision 1810). Two callers are
|
||||
// left — the dressing room and the inspect window, both still our own files — and this goes
|
||||
// with the later of them.
|
||||
lua.globals().set(
|
||||
"BenillaSetBoothTexture",
|
||||
lua.create_function(|lua, (region, token): (Table, String)| {
|
||||
|
||||
@@ -145,13 +145,6 @@ pub(crate) struct StableModel {
|
||||
pub(crate) selected: i32,
|
||||
pub(crate) intents: Vec<StableIntent>,
|
||||
pub(crate) close: bool,
|
||||
/// The model pane's bake yaw, in radians — the fifth of the pane scalars beside
|
||||
/// `paperdoll_yaw` / `inspect_yaw` / `pet_paperdoll_yaw` / `dressup_yaw`, written by
|
||||
/// `BenillaStableModel_SetFacing` and sampled each frame by the app onto the `"stable"` booth
|
||||
/// slot. It lives on the window's own transient state rather than on [`super::Model`] because
|
||||
/// that is what it is: a pane facing that must survive the snapshot replacement every list
|
||||
/// packet performs, exactly like [`Self::selected`].
|
||||
pub(crate) yaw: f32,
|
||||
}
|
||||
|
||||
/// The zero state is the client's `0` — "nothing selected" — not `-1`, which is its encoding for
|
||||
@@ -166,9 +159,6 @@ impl Default for StableModel {
|
||||
selected: 0,
|
||||
intents: Vec::new(),
|
||||
close: false,
|
||||
// `0.0` like every other pane scalar: `BenillaPaperDollModel_OnLoad` seeds the
|
||||
// reference's own 0.61 default through `frame.setFacing` when the window loads.
|
||||
yaw: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,13 +201,6 @@ impl super::UiScript {
|
||||
let model = self.model_mut();
|
||||
selected_slot(&model.stable)
|
||||
}
|
||||
|
||||
/// The stable model pane's bake yaw (`BenillaStableModel_SetFacing` wrote it) — the exact twin
|
||||
/// of [`Self::paperdoll_yaw`] / [`Self::pet_paperdoll_yaw`], sampled each frame by the app onto
|
||||
/// the `"stable"` booth slot.
|
||||
pub fn stable_yaw(&self) -> f32 {
|
||||
self.model_ref().stable.yaw
|
||||
}
|
||||
}
|
||||
|
||||
/// The petNumber→slot translation behind `GetSelectedStablePet` (§7.1).
|
||||
@@ -577,22 +560,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// BenillaStableModel_SetFacing(radians) — the stable pane's own bake yaw, the exact twin of
|
||||
// `BenillaPaperDollModel_SetFacing` / `BenillaPetPaperDollModel_SetFacing`. Benilla-named for
|
||||
// the same reason they are: the real client's pane is a live `<PlayerModel>` widget whose
|
||||
// `SetRotation` is a widget method, and our booth still carries one scalar (decision 0208 §5).
|
||||
// `PetStable.xml`'s `BenillaPetStableModel_SetFacing` shim forwards to this.
|
||||
g.set(
|
||||
"BenillaStableModel_SetFacing",
|
||||
lua.create_function(|lua, radians: f32| {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.stable
|
||||
.yaw = radians;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// ClosePetStables() — client-side close, no packet exists (vmangos has no close opcode): flag
|
||||
// the app to clear its session.
|
||||
g.set(
|
||||
|
||||
@@ -94,6 +94,24 @@ pub enum QuadContent {
|
||||
/// hold to the half, 1→0 over the back half) — applied app-side.
|
||||
flash: Option<f32>,
|
||||
},
|
||||
/// A `Model` / `PlayerModel` widget's own draw slot: the 3D pane's content hole. The engine
|
||||
/// core carries the resolved rect and the pane's identity; the app renderer puts pixels in it
|
||||
/// — the same division of labour [`QuadContent::Minimap`] and [`QuadContent::Cooldown`] use,
|
||||
/// and for the same reason (the scene state is [`crate::widget::ModelState`]; the render is
|
||||
/// not this crate's).
|
||||
///
|
||||
/// **Why the NAME travels rather than the scene.** benilla draws a body pane by sampling an
|
||||
/// off-screen bake the app already keeps per *window* (the paper doll's, the inspect window's,
|
||||
/// the pet page's), and which bake a pane samples is a fact about that window, not about the
|
||||
/// widget. So the seam carries what the app needs to make the join — the pane's global frame
|
||||
/// name, which since decision 1751 is the reference's own — and nothing it would have to
|
||||
/// invent a meaning for. A pane with no name, or one no window has claimed, draws nothing;
|
||||
/// that is also what a `SetModel` pane does today, and it is honest rather than a white slab.
|
||||
ModelPane {
|
||||
/// The pane's global frame name (`$parent`-expanded), or `None` for an anonymous
|
||||
/// `CreateFrame("Model")` — pfUI's autocast shine is the corpus example of the latter.
|
||||
name: Option<String>,
|
||||
},
|
||||
/// A `Texture` region: a BLP path *or* a solid/vertex color (or both — a tinted texture).
|
||||
Texture {
|
||||
path: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user