diff --git a/crates/benilla-app/assets/ui/ActionBar.xml b/crates/benilla-app/assets/ui/ActionBar.xml
index 8ba4802f..14d5fc86 100644
--- a/crates/benilla-app/assets/ui/ActionBar.xml
+++ b/crates/benilla-app/assets/ui/ActionBar.xml
@@ -8,13 +8,13 @@
ACTIONBAR_SLOT_CHANGED repaint, HasAction/GetActionTexture in, UseAction out.
Structure mirrors the real hierarchy so the draw order falls out of the frame tree:
- BenillaActionBar (1024x53, BOTTOM of screen)
- ├─ BenillaExpBar (StatusBar, TOP) — drawn first ⇒ lowest
- │ ├─ BenillaExhaustionLevelFillBar (BORDER) — the pale rested wash up to the tick
- │ └─ BenillaExpBarOverlayFrame (DIALOG) — the hover "curr / max" numerals
- ├─ BenillaActionBarArtFrame (fills the bar) — dwarf strips + end caps over the XP bar
+ MainMenuBar (1024x53, BOTTOM of screen)
+ ├─ MainMenuExpBar (StatusBar, TOP) — drawn first ⇒ lowest
+ │ ├─ ExhaustionLevelFillBar (BORDER) — the pale rested wash up to the tick
+ │ └─ MainMenuBarOverlayFrame (DIALOG) — the hover "curr / max" numerals
+ ├─ MainMenuBarArtFrame (fills the bar) — dwarf strips + end caps over the XP bar
│ └─ 12 buttons + page up/down — child frames ⇒ over the art
- └─ BenillaExhaustionTick (DIALOG) — the rested-boundary marker on the strip
+ └─ ExhaustionTick (DIALOG) — the rested-boundary marker on the strip
(The real client gets the same order via RaiseFrameLevel(MainMenuBarArtFrame); our loader's
later-sibling-is-higher + child-over-parent rules reproduce it without the explicit raise.)
@@ -47,12 +47,12 @@
- Exhaustion tick + rest fill, XP text (hover/cvar-gated), MaxLevelBar: OUT. The bar's
right-hand cluster is otherwise complete — the backpack toggle + four bag slots live in
BagFrame.xml and the eight micro buttons in MicroMenu.xml (both anchored into
- BenillaActionBarArtFrame and seated over the art by BenillaActionBarArt_SeatAbove below),
+ MainMenuBarArtFrame and seated over the art by BenillaActionBarArt_SeatAbove below),
and the performance meter is at the foot of this file. The KEYRING landed with decision
0765: its button and container live in BagFrame.xml (the ref's own split —
MainMenuBarBagButtons.xml / ContainerFrame.lua), and the bar's half of it — swapping the
two right-hand strips to the keyring art and sliding the performance meter over to make
- room — is BenillaMainMenuBar_UpdateKeyRing below, exactly where the ref keeps it
+ room — is MainMenuBar_UpdateKeyRing below, exactly where the ref keeps it
(MainMenuBar.lua l.174-183).
-->
@@ -982,14 +991,14 @@
BenillaActionButton_OnClick(self)BenillaActionButton_OnDragStart(self)BenillaActionButton_OnReceiveDrag(self)
- BenillaActionButton_OnUpdate(self, arg1)
+ ActionButton_OnUpdate(arg1)BenillaActionButton_OnEnter(self)if GameTooltip then GameTooltip:Hide() end
-
+
@@ -1005,7 +1014,7 @@
-->
-
+
@@ -1019,33 +1028,33 @@
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1057,16 +1066,16 @@
a DIALOG-strata child so the "1234 / 5678" text draws over the dwarf-notch
art and the buttons; its own OnLoad wires the FontString to the bar
(BenillaExpBarOverlay_OnLoad's note). -->
-
+
-
-
+
+
-
+
-
+
@@ -1083,7 +1092,7 @@
BenillaExpBar_OnEvent(self)BenillaExpBar_OnEnter(self)BenillaExpBar_OnLeave(self)
- BenillaExhaustionTick_OnUpdate(arg1)
+ ExhaustionTick_OnUpdate(arg1)
if this:IsShown() and TextStatusBar_OnValueChanged then
TextStatusBar_OnValueChanged()
@@ -1095,23 +1104,23 @@
-
@@ -649,7 +649,7 @@
- BenillaFriendsFrameIgnoreButton_OnClick(); PlaySound("igMainMenuOptionCheckBoxOn")
+ FriendsFrameIgnoreButton_OnClick(); PlaySound("igMainMenuOptionCheckBoxOn")
@@ -679,7 +679,7 @@
this:RegisterForClicks("LeftButtonUp", "RightButtonUp")
- BenillaFriendsFrameWhoButton_OnClick(arg1); PlaySound("igMainMenuOptionCheckBoxOn")
+ FriendsFrameWhoButton_OnClick(arg1); PlaySound("igMainMenuOptionCheckBoxOn")
@@ -727,7 +727,7 @@
-
+
@@ -739,25 +739,25 @@
-
+
-
+
-
+
-
+
-
+
@@ -771,7 +771,7 @@
-
+
@@ -779,32 +779,32 @@
self:SetFrameLevel(self:GetFrameLevel() + 1)
- HideUIPanel(BenillaFriendsFrame)
+ HideUIPanel(FriendsFrame)
-
+
-
-
+
+ PanelTemplates_SelectTab(this)
-
-
+
+ PanelTemplates_DeselectTab(this)
- BenillaFriendsFrame.showFriendsList = nil
- BenillaFriendsFrame_Update()
+ FriendsFrame.showFriendsList = nil
+ FriendsFrame_Update()
PlaySound("igCharacterInfoTab")
-
+
-
+
@@ -819,29 +819,29 @@
GameTooltip:Hide()
-
+
-
+
- BenillaFriendsFrame_RemoveFriend()
+ FriendsFrame_RemoveFriend()GameTooltip_AddNewbieTip(self, REMOVE_FRIEND, 1.0, 1.0, 1.0, NEWBIE_TOOLTIP_REMOVEFRIEND, 1)GameTooltip:Hide()
-
+
-
+
- BenillaFriendsFrame_SendMessage()
+ FriendsFrame_SendMessage()GameTooltip_AddNewbieTip(self, SEND_MESSAGE, 1.0, 1.0, 1.0, NEWBIE_TOOLTIP_SENDMESSAGE, 1)GameTooltip:Hide()
-
+
-
+
- BenillaFriendsFrame_GroupInvite()
+ FriendsFrame_GroupInvite()GameTooltip_AddNewbieTip(self, GROUP_INVITE, 1.0, 1.0, 1.0, NEWBIE_TOOLTIP_GROUPINVITE, 1)GameTooltip:Hide()
@@ -849,289 +849,289 @@
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
- self.updateFunc = BenillaFriendsList_Update
+
+ self.updateFunc = FriendsList_Update
- BenillaFauxScrollFrame_OnMouseWheel(BenillaFriendsFrameFriendsScrollFrame, arg1)
+ BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameFriendsScrollFrame, arg1)
-
+
-
-
+
+ PanelTemplates_DeselectTab(this)
- BenillaFriendsFrame.showFriendsList = 1
- BenillaFriendsFrame_Update()
+ FriendsFrame.showFriendsList = 1
+ FriendsFrame_Update()
PlaySound("igCharacterInfoTab")
-
-
+
+ PanelTemplates_SelectTab(this)
-
+
-
+ StaticPopup_Show("ADD_IGNORE")
-
+
-
+
- BenillaFriendsFrame_UnIgnore()
+ FriendsFrame_UnIgnore()
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
- self.updateFunc = BenillaIgnoreList_Update
+
+ self.updateFunc = IgnoreList_Update
- BenillaFauxScrollFrame_OnMouseWheel(BenillaFriendsFrameIgnoreScrollFrame, arg1)
+ BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameIgnoreScrollFrame, arg1)
-
+
-
+
-
+
- BenillaWhoFrameColumn_SetWidth(83, this); this.sortType = "name"
+ WhoFrameColumn_SetWidth(83, this); this.sortType = "name"
-
-
+
+
-
+
- BenillaWhoFrameDropDown_OnLoad()
+ WhoFrameDropDown_OnLoad()
- UIDropDownMenu_Initialize(BenillaWhoFrameDropDown, BenillaWhoFrameDropDown_Initialize)
- UIDropDownMenu_SetSelectedID(BenillaWhoFrameDropDown, 1)
+ UIDropDownMenu_Initialize(WhoFrameDropDown, WhoFrameDropDown_Initialize)
+ UIDropDownMenu_SetSelectedID(WhoFrameDropDown, 1)
- BenillaWhoFrameColumn_SetWidth(105, this)
+ WhoFrameColumn_SetWidth(105, this)
-
-
- BenillaWhoFrameColumn_SetWidth(32, this); this.sortType = "level"
+
+
+ WhoFrameColumn_SetWidth(32, this); this.sortType = "level"
-
-
- BenillaWhoFrameColumn_SetWidth(92, this); this.sortType = "class"
+
+
+ WhoFrameColumn_SetWidth(92, this); this.sortType = "class"
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
- InviteByName(BenillaWhoFrame.selectedName)
+ InviteByName(WhoFrame.selectedName)
-
+
-
- AddFriend(BenillaWhoFrame.selectedName)
+
+ AddFriend(WhoFrame.selectedName)
-
+
-
+
- BenillaWhoFrameEditBox_OnEnterPressed()
- BenillaWhoFrame.selectedWho = nil
+ WhoFrameEditBox_OnEnterPressed()
+ WhoFrame.selectedWho = nil
-
+ this:ClearFocus()
- BenillaWhoFrameEditBox_OnEnterPressed()
+ WhoFrameEditBox_OnEnterPressed()this:ClearFocus()
-
+
-
- self.updateFunc = BenillaWhoList_Update
+
+ self.updateFunc = WhoList_Update
@@ -1140,41 +1140,41 @@
SetWhoToUI(0)SetWhoToUI(1)SetWhoToUI(0)
- BenillaFauxScrollFrame_OnMouseWheel(BenillaWhoListScrollFrame, arg1)
+ BenillaFauxScrollFrame_OnMouseWheel(WhoListScrollFrame, arg1)
-
+
- PanelTemplates_SetTab(BenillaFriendsFrame, this:GetID())
- BenillaFriendsFrame_OnShow()
+ PanelTemplates_SetTab(FriendsFrame, this:GetID())
+ FriendsFrame_OnShow()
PlaySound("igCharacterInfoTab")
-
-
+
+
- PanelTemplates_SetTab(BenillaFriendsFrame, this:GetID())
- BenillaFriendsFrame_OnShow()
+ PanelTemplates_SetTab(FriendsFrame, this:GetID())
+ FriendsFrame_OnShow()
PlaySound("igCharacterInfoTab")
-
-
+
+
- BenillaFriendsFrame_OnLoad()
- BenillaFriendsFrame_OnShow()
- BenillaFriendsFrame_OnHide()
- BenillaFriendsFrame_OnEvent()
+ FriendsFrame_OnLoad()
+ FriendsFrame_OnShow()
+ FriendsFrame_OnHide()
+ FriendsFrame_OnEvent()
diff --git a/crates/benilla-app/assets/ui/GameMenuFrame.xml b/crates/benilla-app/assets/ui/GameMenuFrame.xml
index 8aefafd2..05a1b225 100644
--- a/crates/benilla-app/assets/ui/GameMenuFrame.xml
+++ b/crates/benilla-app/assets/ui/GameMenuFrame.xml
@@ -81,9 +81,14 @@
- The Options label stays a literal "Options": 1.12 GlobalStrings has no era
GAMEMENU_OPTIONS (checked; its OPTIONS_MENU is "Options Menu", a different string).
- The ref frame is `toplevel="true" movable="true" parent="UIParent"`. Dropped: this engine has
- no cross-file `parent=` attachment (SkillFrame.xml's header comment), no frame-level raise,
- and `movable` is inert in the ref too (GameMenuFrame defines no drag scripts, so it never
- actually moves). `frameStrata="DIALOG"` is what puts it over the world UI, and that is kept.
+ no cross-file `parent=` attachment (SkillFrame.xml's header comment), and `movable` is inert
+ in the ref too (GameMenuFrame defines no drag scripts, so it never actually moves).
+ `frameStrata="DIALOG"` is what puts it over the world UI, and that is kept.
+ **The "no frame-level raise" half of that reason is gone**: the raise law is built
+ (`script::object::toplevel`, wow-re `ui/scratch/toplevel-raise.md`) and the loader honours
+ `toplevel="true"`. Re-adding it here is a one-word change and would make the Esc menu come
+ to the front over any other DIALOG-strata frame it overlaps; left off pending the
+ director's call on the look, not on the mechanism.
-->
-
-
-
-
+
+
@@ -255,6 +306,46 @@
+
+
+
+
+
+
+
+
+
+ HealthBar_OnValueChanged(arg1)
+
+
+
+
+
+
+ GameTooltip_OnLoad(self)
+ GameTooltip_OnHide(this)
+
+
+
+
+
+
-
-
-
-
-
-
-
- HealthBar_OnValueChanged(arg1)
-
-
-
@@ -363,47 +439,25 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- GameTooltip_OnLoad(self)
-
-
-
+
+
@@ -435,4 +489,10 @@
GameTooltip_OnLoad(self)
+
+
+
+
diff --git a/crates/benilla-app/assets/ui/GossipFrame.xml b/crates/benilla-app/assets/ui/GossipFrame.xml
index ffd800d6..8bd92818 100644
--- a/crates/benilla-app/assets/ui/GossipFrame.xml
+++ b/crates/benilla-app/assets/ui/GossipFrame.xml
@@ -3,8 +3,8 @@
benilla's gossip dialog (decision 0081 phase 3), RE-SKINNED to the real 1.12 gossip/quest
greeting window (dressing arc, decision 0084): the geometry and art below are QUOTED from the
real `GossipFrame.xml` (scratchpad `ref-GossipFrame.xml`; line citations inline), turned onto
- our own feeds/intents. The name `BenillaGossipFrame`, its panel registration
- (UiPanels.xml's UIPanelWindows["BenillaGossipFrame"], area="left", pushable=0), the Era gossip
+ our own feeds/intents. The name `GossipFrame`, its panel registration
+ (UiPanels.xml's UIPanelWindows["GossipFrame"], area="left", pushable=0), the Era gossip
feeds (GetGossipText/GetGossipOptions/IsGossipOptionCoded/SelectGossipOption/CloseGossip/
GetNumGossipQuests/GetGossipQuestInfo/SelectGossipQuest), and the intent functions are
UNCHANGED — only the window's dressing is real now.
@@ -24,7 +24,7 @@
over our OWN flatter seam (`benilla-ui` `script/gossip.rs`): the app already resolves the wire's
available/active split and delivers ONE ordered `GetNumGossipQuests()`/`GetGossipQuestInfo(i)`
list (quest rows first, in wire order), so `BenillaGossipFrame_Update` below runs a single loop
- over the shared `BenillaGossipRow1..8` pool — quest rows fill from i=1, option rows fill after
+ over the shared `GossipRow1..8` pool — quest rows fill from i=1, option rows fill after
them — instead of the ref's three separate counters; a row's `.kind` ("quest"/"option") plus its
`.itemIndex` (the row's 1-based position within ITS OWN list) is all `BenillaGossipRow_OnClick`
needs to call the right one of `SelectGossipQuest`/`SelectGossipOption` (ref's three intents
@@ -62,7 +62,7 @@
-->
@@ -274,7 +276,7 @@
-
+
@@ -322,7 +324,7 @@
on the window at TOP -23. The app delivers the name as arg1 of GOSSIP_SHOW; blank
until it lands. CENTER + the single-point (~zero-width) rect keeps the name centred
regardless of length, exactly like the merchant's title. -->
-
+
@@ -348,7 +350,7 @@
re-anchored per update by GossipFrame.lua l.39-44); ours is the shared kit's
BENILLA_SCROLL_PAD, the same breathing room by a plainer mechanism.
-->
-
+
@@ -378,7 +380,7 @@
-
+
@@ -392,38 +394,38 @@
the rest off the previous row's BOTTOMLEFT (ref l.256-269) — one static
chain, so rows re-flow off each other's measured heights with no runtime
repositioning. -->
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
- BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
+ BenillaGossipRow_OnLoad(self)
@@ -431,7 +433,7 @@
-
+
@@ -451,7 +453,7 @@
(GlobalStrings GOODBYE = "Goodbye") is an OVERLAY FontString centred on the button (the
real ButtonText draws in the OVERLAY layer). Wired to the same close path as the close
button (CloseGossip → HideUIPanel), mirroring the ref's OnClick HideUIPanel(GossipFrame). -->
-
+
@@ -487,8 +489,8 @@
- BenillaGossipFrame_OnLoad(self)
- BenillaGossipFrame_OnEvent(self)
+ GossipFrame_OnLoad()
+ GossipFrame_OnEvent()
diff --git a/crates/benilla-app/assets/ui/GroupLootFrame.xml b/crates/benilla-app/assets/ui/GroupLootFrame.xml
index a1e0f576..c5adc1f0 100644
--- a/crates/benilla-app/assets/ui/GroupLootFrame.xml
+++ b/crates/benilla-app/assets/ui/GroupLootFrame.xml
@@ -62,12 +62,24 @@
may be `nil` while the item template is still in flight). `GetLootRollItemLink` DOES exist now
(decision 1059) but is the click's, not the hover's: the tooltip wants the id, not the markup.
- `toplevel="true"` (ref l.243) IS OMITTED — `SetToplevel` isn't in the v1 object model yet
- (`loader/geometry.rs`'s `apply_attrs`: a warn-once "gap", same as `movable`/`resizable`); other
- benilla toplevel popups (BuffFrame.xml, CastingBar.xml) carry the attribute anyway and simply
- eat the warning, but it has ZERO engine effect either way today, so keeping this file's own
- load warning-free (this assignment's own bar) costs nothing real — trivially re-added the day
- `SetToplevel` lands.
+ `toplevel="true"` (ref l.243) IS STILL OMITTED, but for a NEW reason — the old one ("`SetToplevel`
+ isn't in the v1 object model yet ... trivially re-added the day `SetToplevel` lands") has expired.
+ `SetToplevel` landed: the loader calls the real method and the raise law — occlusion-gated,
+ confined to this frame's own stratum — is `script::object::toplevel` (wow-re
+ `ui/scratch/toplevel-raise.md`).
+
+ What adding it back does, measured rather than guessed: `GroupLootFrame1`'s rect
+ (390.5,60)-(633.5,144) overlaps `ExhaustionTick` — `ActionBar.xml` l.1111, `frameStrata="DIALOG"`
+ transcribed verbatim from ref-MainMenuBar.xml l.415-442 — in a ~2.5 px band, so showing roll
+ frame 1 raises its whole subtree to DIALOG level 3 while frame 2 (which overlaps nothing, the
+ four stack with a 15 px gap) stays at level 0. That is exactly what the reference does, and the
+ only pixels it moves are the sliver where the roll frame meets the tick. It does, however, break
+ `group_loot_tests::start_loot_roll_claims_frames_in_order_and_paints_the_roll`, whose
+ `quad_center("UI-GroupLoot-Dice-Up")` picks the FIRST dice quad in painter order as a stand-in
+ for "frame 1's Need button" — an unstated assumption that declaration order is draw order among
+ the four, which the raise legitimately ends. That is a fixture proxy, not the test's assertion
+ (the BoP-confirm gate it proves is untouched), so re-adding the attribute is one word plus a
+ two-line fix to that helper — a director's call on the look, not a blocked mechanism.
NAMED FONT OBJECTS ARE SUPPORTED on this branch (the loader's top-level `` handling,
`loader/mod.rs`'s `do_font`, plus the full `Fonts.xml` transcription, both landed since
@@ -75,9 +87,9 @@
`inherits="GameFontNormalSmall"` below resolves for real; no stale caveat needed here.
IN-FLIGHT ITEMS: `GetLootRollItemInfo`'s `name`/`texture`/`quality` are `nil` while the item
- template query hasn't landed yet (`loot_roll.rs`). `BenillaGroupLootFrame_OnShow` falls back to
+ template query hasn't landed yet (`loot_roll.rs`). `GroupLootFrame_OnShow` falls back to
`BENILLA_FALLBACK_ICON` / an empty name / common (quality 1) — the same fallback shape
- `BenillaLootFrame_Update` uses for an in-flight loot row.
+ `LootFrame_Update` uses for an in-flight loot row.
The Era API this drives (`benilla-ui/src/script/loot_roll.rs`, decision 0591):
`GetLootRollItemInfo(rollID)` -> texture,name,count,quality,bindOnPickUp,itemID;
@@ -125,7 +137,7 @@
-- of the four frames for a freshly opened roll, seed its rollID/rollTime, and prime the
-- Timer StatusBar's max before showing it (fields set BEFORE Show(), so OnShow below can
-- already read frame.rollID).
- function BenillaGroupLootFrame_OpenNewFrame(rollId, rollTime)
+ function GroupLootFrame_OpenNewFrame(rollId, rollTime)
for i = 1, BENILLA_NUM_GROUP_LOOT_FRAMES do
local frame = getglobal("GroupLootFrame" .. i)
if frame and not frame:IsVisible() then
@@ -143,7 +155,7 @@
-- GroupLootFrame_OnShow's body (ref l.260-277): resolve the roll's display fields and paint
-- the icon/name/backdrop. name/texture/quality may be nil while the item template query is
-- still in flight (loot_roll.rs) — fall back to the fallback icon / blank name / common (1)
- -- quality rather than erroring, the same fallback shape BenillaLootFrame_Update uses for an
+ -- quality rather than erroring, the same fallback shape LootFrame_Update uses for an
-- in-flight loot row (LootFrame.xml).
--
-- SPLIT OUT OF OnShow, unlike the reference, because it must be re-runnable: the ref's
@@ -182,7 +194,8 @@
nameText:SetVertexColor(color.r, color.g, color.b)
end
- function BenillaGroupLootFrame_OnShow(frame)
+ function GroupLootFrame_OnShow()
+ local frame = this
BenillaGroupLootFrame_Paint(frame)
end
@@ -195,7 +208,8 @@
frame:RegisterEvent("UPDATE_LOOT_ROLL")
end
- function BenillaGroupLootFrame_OnEvent(frame)
+ function GroupLootFrame_OnEvent()
+ local frame = this
if arg1 ~= frame.rollID then
return
end
@@ -320,7 +334,7 @@
+ bind-on-pickup roll (GroupLootFrame_OnShow). -->
@@ -363,7 +377,7 @@
@@ -448,8 +462,8 @@
BenillaGroupLootFrame_OnLoad(self)
- BenillaGroupLootFrame_OnShow(self)
- BenillaGroupLootFrame_OnEvent(self)
+ GroupLootFrame_OnShow()
+ GroupLootFrame_OnEvent()
@@ -495,7 +509,7 @@
if event == "START_LOOT_ROLL" then
- BenillaGroupLootFrame_OpenNewFrame(arg1, arg2)
+ GroupLootFrame_OpenNewFrame(arg1, arg2)
elseif event == "CONFIRM_LOOT_ROLL" then
-- Ref UIParent.lua:517-524 verbatim, minus the central router: a Need/Greed on a
-- BIND-ON-PICKUP roll sends nothing and asks first (decision 0594). arg1 = rollID,
diff --git a/crates/benilla-app/assets/ui/InspectFrame.xml b/crates/benilla-app/assets/ui/InspectFrame.xml
index 92c6bba3..ea734a93 100644
--- a/crates/benilla-app/assets/ui/InspectFrame.xml
+++ b/crates/benilla-app/assets/ui/InspectFrame.xml
@@ -136,7 +136,7 @@
-- ref InspectFrame_OnHide (l.53-59): drop the token, play the close sound, and tell the
-- engine to stop inspecting (which stops the app's per-frame slot resolve). No subframe
- -- hide-loop is needed (unlike BenillaCharacterFrame_OnHide's): the one page is a true
+ -- hide-loop is needed (unlike CharacterFrame_OnHide's): the one page is a true
-- engine child of this frame, so it hides on the parent's cascade.
function BenillaInspectFrame_OnHide(frame)
frame.unit = nil
diff --git a/crates/benilla-app/assets/ui/ItemRef.xml b/crates/benilla-app/assets/ui/ItemRef.xml
index 10f62acd..a3d35045 100644
--- a/crates/benilla-app/assets/ui/ItemRef.xml
+++ b/crates/benilla-app/assets/ui/ItemRef.xml
@@ -2,11 +2,15 @@
@@ -125,11 +113,15 @@
+
GameTooltip_OnLoad(self)
self:SetPadding(16)
- GameTooltip_OnHide(this)
diff --git a/crates/benilla-app/assets/ui/ItemTextFrame.xml b/crates/benilla-app/assets/ui/ItemTextFrame.xml
index ac9d1b0e..f4d2b416 100644
--- a/crates/benilla-app/assets/ui/ItemTextFrame.xml
+++ b/crates/benilla-app/assets/ui/ItemTextFrame.xml
@@ -2,7 +2,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -213,7 +213,7 @@
-
+
@@ -222,28 +222,28 @@
SetPoint time — a target declared later falls back to the parent (and
the black track lands on the parchment). -->
-
+
-
+
-
+
-
+
-
+
@@ -252,9 +252,9 @@
-
+
-
+
@@ -270,7 +270,7 @@
-
+
@@ -291,7 +291,7 @@
-
+
@@ -312,11 +312,11 @@
-
+
- HideUIPanel(BenillaItemTextFrame)
+ HideUIPanel(ItemTextFrame)
@@ -324,9 +324,9 @@
- BenillaItemTextFrame_OnLoad()
- BenillaItemTextFrame_OnEvent(event)
- BenillaItemTextFrame_OnUpdate(arg1)
+ ItemTextFrame_OnLoad()
+ ItemTextFrame_OnEvent(event)
+ ItemTextFrame_OnUpdate(arg1)PlaySound("igMainMenuOpen")
CloseItemText()
diff --git a/crates/benilla-app/assets/ui/KeyBindingsPage.xml b/crates/benilla-app/assets/ui/KeyBindingsPage.xml
index e9bd72c5..ec5876b1 100644
--- a/crates/benilla-app/assets/ui/KeyBindingsPage.xml
+++ b/crates/benilla-app/assets/ui/KeyBindingsPage.xml
@@ -302,7 +302,7 @@
function KeyBindingsPage_Update()
local page = KeyBindingsPage
local scroll = getglobal("OptionsFrameContainerBodyKeybindingsScrollFrame")
- local offset = BenillaFauxScrollFrame_GetOffset(scroll)
+ local offset = FauxScrollFrame_GetOffset(scroll)
for i = 1, KEYBINDINGS_DISPLAYED do
local name = KEYBINDINGS_ROW_BASE .. i
local row = getglobal(name)
@@ -317,7 +317,7 @@
row:Hide()
end
end
- BenillaFauxScrollFrame_Update(scroll, table.getn(page.items),
+ FauxScrollFrame_Update(scroll, table.getn(page.items),
KEYBINDINGS_DISPLAYED, KEYBINDINGS_ROW_HEIGHT)
KeyBindings_UpdateUnbindKey()
end
diff --git a/crates/benilla-app/assets/ui/LootFrame.xml b/crates/benilla-app/assets/ui/LootFrame.xml
index e15a5192..47d007d3 100644
--- a/crates/benilla-app/assets/ui/LootFrame.xml
+++ b/crates/benilla-app/assets/ui/LootFrame.xml
@@ -2,7 +2,7 @@
@@ -364,7 +366,7 @@
OnShow resets it to the skull and swaps a fishing loot to FishingLoot-Icon
(decision 1086), exactly as the ref's OnShow does (l.133-140). -->
-
+
@@ -376,7 +378,7 @@
-
+
@@ -386,27 +388,27 @@
-
+ BenillaLootItem_OnLoad(self, 1)
-
-
+
+ BenillaLootItem_OnLoad(self, 2)
-
-
+
+ BenillaLootItem_OnLoad(self, 3)
-
-
+
+ BenillaLootItem_OnLoad(self, 4)
-
+
@@ -415,24 +417,24 @@
- BenillaLootFrame_PageUp()
+ LootFrame_PageUp()
-
+
-
+
- BenillaLootFrame_PageDown()
+ LootFrame_PageDown()
-
+
@@ -444,10 +446,10 @@
- BenillaLootFrame_OnLoad(self)
- BenillaLootFrame_OnEvent(self)
- BenillaLootFrame_OnShow()
- BenillaLootFrame_OnHide()
+ LootFrame_OnLoad()
+ LootFrame_OnEvent(event)
+ LootFrame_OnShow()
+ LootFrame_OnHide()
diff --git a/crates/benilla-app/assets/ui/MacroFrame.xml b/crates/benilla-app/assets/ui/MacroFrame.xml
index cf26f2c0..5222bb85 100644
--- a/crates/benilla-app/assets/ui/MacroFrame.xml
+++ b/crates/benilla-app/assets/ui/MacroFrame.xml
@@ -350,7 +350,7 @@
local popup = getglobal("BenillaMacroPopupFrame")
local scrollFrame = getglobal("BenillaMacroPopupScrollFrame")
local numMacroIcons = GetNumMacroIcons()
- local macroPopupOffset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
+ local macroPopupOffset = FauxScrollFrame_GetOffset(scrollFrame)
if resetName then
if popup.mode == "new" then
@@ -379,7 +379,7 @@
end
end
- BenillaFauxScrollFrame_Update(scrollFrame,
+ FauxScrollFrame_Update(scrollFrame,
math.ceil(numMacroIcons / NUM_ICONS_PER_ROW), NUM_ICON_ROWS, MACRO_ICON_ROW_HEIGHT)
end
@@ -404,7 +404,7 @@
local popup = getglobal("BenillaMacroPopupFrame")
local scrollFrame = getglobal("BenillaMacroPopupScrollFrame")
popup.selectedIcon = button:GetID()
- + (BenillaFauxScrollFrame_GetOffset(scrollFrame) * NUM_ICONS_PER_ROW)
+ + (FauxScrollFrame_GetOffset(scrollFrame) * NUM_ICONS_PER_ROW)
getglobal("BenillaMacroFrameSelectedMacroButtonIcon")
:SetTexture(GetMacroIconInfo(popup.selectedIcon))
BenillaMacroPopupOkayButton_Update()
@@ -892,13 +892,13 @@
-
+
@@ -907,7 +907,7 @@
- BenillaFauxScrollFrame_OnLoad(self)
+ ScrollFrame_OnLoad()
self.updateFunc = BenillaMacroPopupFrame_Update
diff --git a/crates/benilla-app/assets/ui/MailFrame.xml b/crates/benilla-app/assets/ui/MailFrame.xml
index 15f1b9a5..e7253a4e 100644
--- a/crates/benilla-app/assets/ui/MailFrame.xml
+++ b/crates/benilla-app/assets/ui/MailFrame.xml
@@ -40,7 +40,7 @@
BENILLA_STATIONERY_PATH = "Interface\\Stationery\\" -- STATIONERY_PATH (ref l.5)
-- The money entry uses the shared BenillaMoneyInput_* kit (MerchantFrame.xml, loaded first) —
- -- the send tab's three numeric edit boxes are named BenillaSendMailMoneyGold/Silver/Copper.
+ -- the send tab's three numeric edit boxes are named SendMailMoneyGold/Silver/Copper.
-- ── Load / event dispatch (ref MailFrame_Onload l.8-24 / MailFrame_OnEvent l.26-62) ──────
function BenillaMailFrame_OnLoad(frame)
@@ -57,7 +57,8 @@
frame:RegisterEvent("PLAYER_MONEY")
end
- function BenillaMailFrame_OnEvent(frame)
+ function MailFrame_OnEvent()
+ local frame = this
if event == "MAIL_SHOW" then
ShowUIPanel(frame)
if not frame:IsVisible() then
@@ -65,96 +66,96 @@
return
end
OpenBackpack()
- BenillaSendMailFrame_Update()
- BenillaMailFrameTab_OnClick(1)
+ SendMailFrame_Update()
+ MailFrameTab_OnClick(1)
CheckInbox() -- drains to CMSG_GET_MAIL_LIST (throttled app-side, wow-re §5)
elseif event == "MAIL_INBOX_UPDATE" then
- BenillaInboxFrame_Update()
- BenillaOpenMail_Update()
+ InboxFrame_Update()
+ OpenMail_Update()
elseif event == "MAIL_SEND_SUCCESS" then
-- Fires on a successful send AND, byte-verified, on window open (the compose-tab
-- reset side effect — wow-re §5); either way reset the form + play the page turn.
- BenillaSendMailFrame_Reset()
+ SendMailFrame_Reset()
PlaySound("igAbiliityPageTurn")
- if BenillaSendMailFrame.sendMode == "reply" then
- BenillaMailFrameTab_OnClick(1)
+ if SendMailFrame.sendMode == "reply" then
+ MailFrameTab_OnClick(1)
end
elseif event == "MAIL_FAILED" then
-- Fires on EVERY SMSG_SEND_MAIL_RESULT for a send (success included, wow-re §5): its
-- only job is to re-enable the button so the UI never stays blocked.
- BenillaSendMailMailButton:Enable()
+ SendMailMailButton:Enable()
elseif event == "MAIL_CLOSED" then
HideUIPanel(frame)
elseif event == "PLAYER_MONEY" then
-- Repaints the purse AND recolors the postage cost (ref: both money frames
-- self-refresh on this event).
- BenillaSendMailFrame_Update()
+ SendMailFrame_Update()
end
end
-- The tab pair + the four corner-texture swaps (ref MailFrameTab_OnClick l.64-93).
- function BenillaMailFrameTab_OnClick(tab)
+ function MailFrameTab_OnClick(tab)
if not tab then tab = this:GetID() end
- PanelTemplates_SetTab(BenillaMailFrame, tab)
+ PanelTemplates_SetTab(MailFrame, tab)
if tab == 1 then
- BenillaInboxFrame:Show()
- BenillaSendMailFrame:Hide()
- BenillaMailFrameTopLeft:SetTexture("Interface\\ItemTextFrame\\UI-ItemText-TopLeft")
- BenillaMailFrameTopRight:SetTexture("Interface\\Spellbook\\UI-SpellbookPanel-TopRight")
- BenillaMailFrameBotLeft:SetTexture("Interface\\ItemTextFrame\\UI-ItemText-BotLeft")
- BenillaMailFrameBotRight:SetTexture("Interface\\Spellbook\\UI-SpellbookPanel-BotRight")
- BenillaMailFrameTopLeft:SetPoint("TOPLEFT", "BenillaMailFrame", "TOPLEFT", 0, 0)
+ InboxFrame:Show()
+ SendMailFrame:Hide()
+ MailFrameTopLeft:SetTexture("Interface\\ItemTextFrame\\UI-ItemText-TopLeft")
+ MailFrameTopRight:SetTexture("Interface\\Spellbook\\UI-SpellbookPanel-TopRight")
+ MailFrameBotLeft:SetTexture("Interface\\ItemTextFrame\\UI-ItemText-BotLeft")
+ MailFrameBotRight:SetTexture("Interface\\Spellbook\\UI-SpellbookPanel-BotRight")
+ MailFrameTopLeft:SetPoint("TOPLEFT", "MailFrame", "TOPLEFT", 0, 0)
else
- BenillaInboxFrame:Hide()
- BenillaSendMailFrame:Show()
- BenillaMailFrameTopLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft")
- BenillaMailFrameTopRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight")
- BenillaMailFrameBotLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotLeft")
- BenillaMailFrameBotRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight")
- BenillaMailFrameTopLeft:SetPoint("TOPLEFT", "BenillaMailFrame", "TOPLEFT", 2, -1)
- BenillaSendMailFrame_Update()
- BenillaSendMailFrame.sendMode = "send"
+ InboxFrame:Hide()
+ SendMailFrame:Show()
+ MailFrameTopLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft")
+ MailFrameTopRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight")
+ MailFrameBotLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotLeft")
+ MailFrameBotRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight")
+ MailFrameTopLeft:SetPoint("TOPLEFT", "MailFrame", "TOPLEFT", 2, -1)
+ SendMailFrame_Update()
+ SendMailFrame.sendMode = "send"
end
PlaySound("igSpellBookOpen")
end
-- ── Inbox (ref InboxFrame_Update l.97-199): the row's clickable child is $parentButton, so
-- every getglobal below reads MailItemButton* / MailItemSender/Subject/ExpireTime. ─
- function BenillaInboxFrame_Update()
+ function InboxFrame_Update()
local numItems = GetInboxNumItems()
- local index = ((BenillaMailFrame.pageNum - 1) * BENILLA_INBOXITEMS_TO_DISPLAY) + 1
+ local index = ((MailFrame.pageNum - 1) * BENILLA_INBOXITEMS_TO_DISPLAY) + 1
for i = 1, BENILLA_INBOXITEMS_TO_DISPLAY do
if index <= numItems then
local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, x, y, z, isGM = GetInboxHeaderInfo(index)
local icon
if packageIcon and not isGM then icon = packageIcon else icon = stationeryIcon end
if not sender then sender = "Unknown" end -- UNKNOWN
- local button = getglobal("BenillaMailItem"..i.."Button")
+ local button = getglobal("MailItem"..i.."Button")
button:Show()
button.index = index
button.hasItem = hasItem
- getglobal("BenillaMailItem"..i.."ButtonIcon"):SetTexture(icon)
- local subjectText = getglobal("BenillaMailItem"..i.."Subject")
+ getglobal("MailItem"..i.."ButtonIcon"):SetTexture(icon)
+ local subjectText = getglobal("MailItem"..i.."Subject")
subjectText:SetText(subject)
- local senderText = getglobal("BenillaMailItem"..i.."Sender")
+ local senderText = getglobal("MailItem"..i.."Sender")
senderText:SetText(sender)
-- Unread gold / read grey (ref l.130-141; the read icon's SetDesaturation is our
-- vertex-grey stand-in — the engine has no desaturation).
if wasRead then
senderText:SetTextColor(0.75, 0.75, 0.75)
subjectText:SetTextColor(0.75, 0.75, 0.75)
- getglobal("BenillaMailItem"..i.."ButtonSlot"):SetVertexColor(0.5, 0.5, 0.5)
- getglobal("BenillaMailItem"..i.."ButtonIcon"):SetVertexColor(0.5, 0.5, 0.5)
+ getglobal("MailItem"..i.."ButtonSlot"):SetVertexColor(0.5, 0.5, 0.5)
+ getglobal("MailItem"..i.."ButtonIcon"):SetVertexColor(0.5, 0.5, 0.5)
else
senderText:SetTextColor(1.0, 0.82, 0) -- NORMAL_FONT_COLOR
subjectText:SetTextColor(1.0, 1.0, 1.0) -- HIGHLIGHT_FONT_COLOR
- getglobal("BenillaMailItem"..i.."ButtonSlot"):SetVertexColor(1.0, 0.82, 0)
- getglobal("BenillaMailItem"..i.."ButtonIcon"):SetVertexColor(1, 1, 1)
+ getglobal("MailItem"..i.."ButtonSlot"):SetVertexColor(1.0, 0.82, 0)
+ getglobal("MailItem"..i.."ButtonIcon"):SetVertexColor(1, 1, 1)
end
-- Expiry: green whole days, red under a day (ref l.142-149: GetText("DAYS_ABBR")
-- pluralizes — DAYS_ABBR "Day" / DAYS_ABBR_P1 "Days", GlobalStrings l.895-896;
-- the sub-day SecondsToTime is approximated as "< 1 day").
- local expireTime = getglobal("BenillaMailItem"..i.."ExpireTime")
+ local expireTime = getglobal("MailItem"..i.."ExpireTime")
if daysLeft >= 1 then
local days = math.floor(daysLeft)
expireTime:SetText(string.format("|cff20ff20%d %s|r", days, days == 1 and "Day" or "Days"))
@@ -170,58 +171,59 @@
expireTime:Show()
-- COD tag (ref l.157-164) + money field.
if CODAmount > 0 then
- getglobal("BenillaMailItem"..i.."ButtonCOD"):Show()
+ getglobal("MailItem"..i.."ButtonCOD"):Show()
button.cod = CODAmount
else
- getglobal("BenillaMailItem"..i.."ButtonCOD"):Hide()
+ getglobal("MailItem"..i.."ButtonCOD"):Hide()
button.cod = nil
end
if money > 0 then button.money = money else button.money = nil end
- if BenillaInboxFrame.openMailID == index then
+ if InboxFrame.openMailID == index then
button:SetChecked(1)
else
button:SetChecked(nil)
end
else
- getglobal("BenillaMailItem"..i.."Button"):Hide()
- getglobal("BenillaMailItem"..i.."Sender"):SetText("")
- getglobal("BenillaMailItem"..i.."Subject"):SetText("")
- getglobal("BenillaMailItem"..i.."ExpireTime"):Hide()
+ getglobal("MailItem"..i.."Button"):Hide()
+ getglobal("MailItem"..i.."Sender"):SetText("")
+ getglobal("MailItem"..i.."Subject"):SetText("")
+ getglobal("MailItem"..i.."ExpireTime"):Hide()
end
index = index + 1
end
-- Page arrows (ref l.188-198).
- if BenillaMailFrame.pageNum == 1 then
- BenillaInboxPrevPageButton:Disable()
+ if MailFrame.pageNum == 1 then
+ InboxPrevPageButton:Disable()
else
- BenillaInboxPrevPageButton:Enable()
+ InboxPrevPageButton:Enable()
end
- if (BenillaMailFrame.pageNum * BENILLA_INBOXITEMS_TO_DISPLAY) < numItems then
- BenillaInboxNextPageButton:Enable()
+ if (MailFrame.pageNum * BENILLA_INBOXITEMS_TO_DISPLAY) < numItems then
+ InboxNextPageButton:Enable()
else
- BenillaInboxNextPageButton:Disable()
+ InboxNextPageButton:Disable()
end
-- InboxCurrentPage is declared in the ref XML (l.329) but NO reference Lua ever writes
-- it — the real 1.12 window shows nothing there, so neither do we.
end
-- Row click: open / close the letter (ref InboxFrame_OnClick l.201-213; this = the button).
- function BenillaInboxFrame_OnClick(index)
+ function InboxFrame_OnClick(index)
if this:GetChecked() then
- BenillaInboxFrame.openMailID = index
- BenillaOpenMail_Update()
- ShowUIPanel(BenillaOpenMailFrame)
+ InboxFrame.openMailID = index
+ OpenMail_Update()
+ ShowUIPanel(OpenMailFrame)
PlaySound("igSpellBookOpen")
else
- BenillaInboxFrame.openMailID = 0
- HideUIPanel(BenillaOpenMailFrame)
+ InboxFrame.openMailID = 0
+ HideUIPanel(OpenMailFrame)
end
- BenillaInboxFrame_Update()
+ InboxFrame_Update()
end
-- Row hover: the enclosed item (SetInboxItem) + the enclosed-money / COD lines (ref
-- InboxFrameItem_OnEnter l.215-240; this = the button).
- function BenillaInboxFrameItem_OnEnter(button)
+ function InboxFrameItem_OnEnter()
+ local button = this
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
if button.hasItem then
GameTooltip:SetInboxItem(button.index)
@@ -238,139 +240,139 @@
GameTooltip:Show()
end
- function BenillaInboxNextPage()
+ function InboxNextPage()
PlaySound("igMainMenuOptionCheckBoxOn")
- BenillaMailFrame.pageNum = BenillaMailFrame.pageNum + 1
- BenillaInboxFrame_Update()
+ MailFrame.pageNum = MailFrame.pageNum + 1
+ InboxFrame_Update()
end
- function BenillaInboxPrevPage()
+ function InboxPrevPage()
PlaySound("igMainMenuOptionCheckBoxOn")
- BenillaMailFrame.pageNum = BenillaMailFrame.pageNum - 1
- BenillaInboxFrame_Update()
+ MailFrame.pageNum = MailFrame.pageNum - 1
+ InboxFrame_Update()
end
-- ── Open letter (ref OpenMailFrame_OnHide l.256-272 / OpenMail_Update l.274-432) ─────────
- function BenillaOpenMailFrame_OnHide()
+ function OpenMailFrame_OnHide()
StaticPopup_Hide("DELETE_MAIL")
- if not BenillaInboxFrame.openMailID or BenillaInboxFrame.openMailID == 0 then
- BenillaInboxFrame_Update()
+ if not InboxFrame.openMailID or InboxFrame.openMailID == 0 then
+ InboxFrame_Update()
PlaySound("igSpellBookClose")
return
end
- local pkg, sta, sender, subject, money, cod, days, itemID, wasRead, wasReturned, textCreated = GetInboxHeaderInfo(BenillaInboxFrame.openMailID)
+ local pkg, sta, sender, subject, money, cod, days, itemID, wasRead, wasReturned, textCreated = GetInboxHeaderInfo(InboxFrame.openMailID)
if money == 0 and not itemID and textCreated then
- DeleteInboxItem(BenillaInboxFrame.openMailID)
+ DeleteInboxItem(InboxFrame.openMailID)
end
- BenillaInboxFrame.openMailID = 0
- BenillaInboxFrame_Update()
+ InboxFrame.openMailID = 0
+ InboxFrame_Update()
PlaySound("igSpellBookClose")
end
- function BenillaOpenMail_Update()
- local id = BenillaInboxFrame.openMailID
+ function OpenMail_Update()
+ local id = InboxFrame.openMailID
if not id or id == 0 then return end
local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply = GetInboxHeaderInfo(id)
if not sender or not canReply then
- BenillaOpenMailReplyButton:Disable()
+ OpenMailReplyButton:Disable()
else
- BenillaOpenMailReplyButton:Enable()
+ OpenMailReplyButton:Enable()
end
if not sender then sender = "Unknown" end
- BenillaOpenMailSender:SetText(sender)
- BenillaOpenMailSubject:SetText(subject)
- BenillaOpenMailFrameIcon:SetTexture(stationeryIcon) -- ref SetPortraitToTexture l.174
+ OpenMailSender:SetText(sender)
+ OpenMailSubject:SetText(subject)
+ OpenMailFrameIcon:SetTexture(stationeryIcon) -- ref SetPortraitToTexture l.174
local bodyText, texture, isTakeable, isInvoice = GetInboxText(id)
- BenillaOpenMailBodyText:SetText(bodyText)
+ OpenMailBodyText:SetText(bodyText)
if texture then
- BenillaOpenStationeryBackgroundLeft:SetTexture(BENILLA_STATIONERY_PATH..texture.."1")
- BenillaOpenStationeryBackgroundRight:SetTexture(BENILLA_STATIONERY_PATH..texture.."2")
+ OpenStationeryBackgroundLeft:SetTexture(BENILLA_STATIONERY_PATH..texture.."1")
+ OpenStationeryBackgroundRight:SetTexture(BENILLA_STATIONERY_PATH..texture.."2")
end
local hasAttachments
-- The letter button — "make a permanent copy" (ref l.364-376): a takeable body not yet
-- copied shows the stationery icon; the package button chains off its RIGHT, else
-- collapses onto its LEFT so the row stays packed.
if isTakeable and not textCreated then
- getglobal("BenillaOpenMailLetterButtonIcon"):SetTexture(stationeryIcon)
- BenillaOpenMailLetterButton:Enable()
- BenillaOpenMailLetterButton:Show()
- BenillaOpenMailPackageButton:SetPoint("LEFT", "BenillaOpenMailLetterButton", "RIGHT", 10, 0)
+ getglobal("OpenMailLetterButtonIcon"):SetTexture(stationeryIcon)
+ OpenMailLetterButton:Enable()
+ OpenMailLetterButton:Show()
+ OpenMailPackageButton:SetPoint("LEFT", "OpenMailLetterButton", "RIGHT", 10, 0)
hasAttachments = 1
else
- BenillaOpenMailLetterButton:Hide()
- BenillaOpenMailPackageButton:SetPoint("LEFT", "BenillaOpenMailLetterButton", "LEFT", 0, 0)
+ OpenMailLetterButton:Hide()
+ OpenMailPackageButton:SetPoint("LEFT", "OpenMailLetterButton", "LEFT", 0, 0)
end
local name, itemTexture, count, quality, canUse = GetInboxItem(id)
if name then
- BenillaOpenMailFrame.itemName = name
- getglobal("BenillaOpenMailPackageButtonIcon"):SetTexture(itemTexture)
+ OpenMailFrame.itemName = name
+ getglobal("OpenMailPackageButtonIcon"):SetTexture(itemTexture)
if (count or 1) > 1 then
- getglobal("BenillaOpenMailPackageButtonCount"):SetText(count)
+ getglobal("OpenMailPackageButtonCount"):SetText(count)
else
- getglobal("BenillaOpenMailPackageButtonCount"):SetText("")
+ getglobal("OpenMailPackageButtonCount"):SetText("")
end
if canUse then
- getglobal("BenillaOpenMailPackageButtonIcon"):SetVertexColor(1, 1, 1)
+ getglobal("OpenMailPackageButtonIcon"):SetVertexColor(1, 1, 1)
else
- getglobal("BenillaOpenMailPackageButtonIcon"):SetVertexColor(1.0, 0.1, 0.1)
+ getglobal("OpenMailPackageButtonIcon"):SetVertexColor(1.0, 0.1, 0.1)
end
- BenillaOpenMailPackageButton:Enable()
- BenillaOpenMailPackageButton:Show()
- BenillaOpenMailMoneyButton:SetPoint("LEFT", "BenillaOpenMailPackageButton", "RIGHT", 10, 0)
+ OpenMailPackageButton:Enable()
+ OpenMailPackageButton:Show()
+ OpenMailMoneyButton:SetPoint("LEFT", "OpenMailPackageButton", "RIGHT", 10, 0)
hasAttachments = 1
else
- BenillaOpenMailFrame.itemName = nil
- BenillaOpenMailPackageButton:Hide()
- BenillaOpenMailMoneyButton:SetPoint("LEFT", "BenillaOpenMailPackageButton", "LEFT", 0, 0)
+ OpenMailFrame.itemName = nil
+ OpenMailPackageButton:Hide()
+ OpenMailMoneyButton:SetPoint("LEFT", "OpenMailPackageButton", "LEFT", 0, 0)
end
- if CODAmount > 0 then BenillaOpenMailFrame.cod = CODAmount else BenillaOpenMailFrame.cod = nil end
+ if CODAmount > 0 then OpenMailFrame.cod = CODAmount else OpenMailFrame.cod = nil end
if money == 0 then
- BenillaOpenMailMoneyButton:Hide()
- BenillaOpenMailFrame.money = nil
+ OpenMailMoneyButton:Hide()
+ OpenMailFrame.money = nil
else
- getglobal("BenillaOpenMailMoneyButtonIcon"):SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
- BenillaOpenMailMoneyButton:Enable()
- BenillaOpenMailMoneyButton:Show()
- BenillaOpenMailFrame.money = money
+ getglobal("OpenMailMoneyButtonIcon"):SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
+ OpenMailMoneyButton:Enable()
+ OpenMailMoneyButton:Show()
+ OpenMailFrame.money = money
hasAttachments = 1
end
if InboxItemCanDelete(id) then
- BenillaOpenMailDeleteButton:SetText("Delete") -- DELETE
+ OpenMailDeleteButton:SetText("Delete") -- DELETE
else
- BenillaOpenMailDeleteButton:SetText("Return") -- MAIL_RETURN
+ OpenMailDeleteButton:SetText("Return") -- MAIL_RETURN
end
-- The caption + its two positions (ref l.422-431: BOTTOM -68,123 attachments / -5,123 none).
if hasAttachments then
- BenillaOpenMailAttachmentText:SetText("Take Attachments:") -- TAKE_ATTACHMENTS
- BenillaOpenMailAttachmentText:SetTextColor(1, 1, 1)
- BenillaOpenMailAttachmentText:SetPoint("BOTTOM", "BenillaOpenMailFrame", "BOTTOM", -68, 123)
+ OpenMailAttachmentText:SetText("Take Attachments:") -- TAKE_ATTACHMENTS
+ OpenMailAttachmentText:SetTextColor(1, 1, 1)
+ OpenMailAttachmentText:SetPoint("BOTTOM", "OpenMailFrame", "BOTTOM", -68, 123)
else
- BenillaOpenMailAttachmentText:SetText("No Attachments") -- NO_ATTACHMENTS
- BenillaOpenMailAttachmentText:SetTextColor(0.5, 0.5, 0.5)
- BenillaOpenMailAttachmentText:SetPoint("BOTTOM", "BenillaOpenMailFrame", "BOTTOM", -5, 123)
+ OpenMailAttachmentText:SetText("No Attachments") -- NO_ATTACHMENTS
+ OpenMailAttachmentText:SetTextColor(0.5, 0.5, 0.5)
+ OpenMailAttachmentText:SetPoint("BOTTOM", "OpenMailFrame", "BOTTOM", -5, 123)
end
end
-- Reply → Send tab, prefilled "RE: " (ref OpenMail_Reply l.434-447).
- function BenillaOpenMail_Reply()
- BenillaMailFrameTab_OnClick(2)
- BenillaSendMailNameEditBox:SetText(BenillaOpenMailSender:GetText())
- local subject = BenillaOpenMailSubject:GetText()
+ function OpenMail_Reply()
+ MailFrameTab_OnClick(2)
+ SendMailNameEditBox:SetText(OpenMailSender:GetText())
+ local subject = OpenMailSubject:GetText()
local prefix = "RE: " -- MAIL_REPLY_PREFIX.." "
if string.sub(subject, 1, string.len(prefix)) ~= prefix then
subject = prefix..subject
end
- BenillaSendMailSubjectEditBox:SetText(subject)
- BenillaSendMailBodyEditBox:SetFocus()
- BenillaSendMailFrame.sendMode = "reply"
+ SendMailSubjectEditBox:SetText(subject)
+ SendMailBodyEditBox:SetFocus()
+ SendMailFrame.sendMode = "reply"
end
- function BenillaOpenMail_Delete()
- local id = BenillaInboxFrame.openMailID
+ function OpenMail_Delete()
+ local id = InboxFrame.openMailID
if InboxItemCanDelete(id) then
- if BenillaOpenMailFrame.itemName then
- StaticPopup_Show("DELETE_MAIL", BenillaOpenMailFrame.itemName)
+ if OpenMailFrame.itemName then
+ StaticPopup_Show("DELETE_MAIL", OpenMailFrame.itemName)
return
- elseif BenillaOpenMailFrame.money then
+ elseif OpenMailFrame.money then
StaticPopup_Show("DELETE_MONEY")
return
else
@@ -380,19 +382,19 @@
ReturnInboxItem(id)
StaticPopup_Hide("COD_CONFIRMATION")
end
- BenillaInboxFrame.openMailID = 0
- HideUIPanel(BenillaOpenMailFrame)
+ InboxFrame.openMailID = 0
+ HideUIPanel(OpenMailFrame)
end
function BenillaOpenMailMoneyButton_OnClick()
- TakeInboxMoney(BenillaInboxFrame.openMailID)
+ TakeInboxMoney(InboxFrame.openMailID)
end
-- Hovering the coins shows the plain money tooltip (ref OpenMailMoneyButton OnEnter
-- l.1823-1829: no header line, just the amount).
function BenillaOpenMailMoneyButton_OnEnter(button)
- if BenillaOpenMailFrame.money then
+ if OpenMailFrame.money then
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
- SetTooltipMoney(GameTooltip, BenillaOpenMailFrame.money)
+ SetTooltipMoney(GameTooltip, OpenMailFrame.money)
GameTooltip:Show()
end
end
@@ -400,34 +402,35 @@
-- l.1744-1747 → CMSG_MAIL_CREATE_TEXT_ITEM; the OK result stamps COPIED server-side, and
-- the list re-sync hides the button via textCreated).
function BenillaOpenMailLetterButton_OnClick()
- TakeInboxTextItem(BenillaInboxFrame.openMailID)
+ TakeInboxTextItem(InboxFrame.openMailID)
PlaySound("igMainMenuOptionCheckBoxOn")
end
-- Take the item; a COD parcel confirms first (ref OpenMailPackageButton OnClick l.1778-1787).
function BenillaOpenMailPackageButton_OnClick()
- if BenillaOpenMailFrame.cod then
+ if OpenMailFrame.cod then
StaticPopup_Show("COD_CONFIRMATION")
else
- TakeInboxItem(BenillaInboxFrame.openMailID)
+ TakeInboxItem(InboxFrame.openMailID)
end
PlaySound("igMainMenuOptionCheckBoxOn")
end
- function BenillaOpenMailPackage_OnEnter(button)
+ function OpenMailPackage_OnEnter()
+ local button = this
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
- GameTooltip:SetInboxItem(BenillaInboxFrame.openMailID)
- if BenillaOpenMailFrame.cod then
- SetTooltipMoney(GameTooltip, BenillaOpenMailFrame.cod)
+ GameTooltip:SetInboxItem(InboxFrame.openMailID)
+ if OpenMailFrame.cod then
+ SetTooltipMoney(GameTooltip, OpenMailFrame.cod)
end
GameTooltip:Show()
end
-- ── Send tab (ref SendMail* l.484-611 / l.936-960) ──────────────────────────────────────
- function BenillaSendMailMailButton_OnClick()
- local copper = BenillaMoneyInput_GetCopper("BenillaSendMailMoney")
- if BenillaSendMailSendMoneyButton:GetChecked() then
+ function SendMailMailButton_OnClick()
+ local copper = BenillaMoneyInput_GetCopper("SendMailMoney")
+ if SendMailSendMoneyButton:GetChecked() then
SetSendMailCOD(0)
if copper > 0 then
- StaticPopup_Show("SEND_MONEY", BenillaSendMailNameEditBox:GetText())
+ StaticPopup_Show("SEND_MONEY", SendMailNameEditBox:GetText())
this:Disable()
return
end
@@ -436,102 +439,102 @@
SetSendMailMoney(0)
SetSendMailCOD(copper)
end
- BenillaSendMailFrame_SendMail()
+ SendMailFrame_SendMail()
this:Disable()
end
- function BenillaSendMailFrame_SendMail()
- SendMail(BenillaSendMailNameEditBox:GetText(), BenillaSendMailSubjectEditBox:GetText(), BenillaSendMailBodyEditBox:GetText())
+ function SendMailFrame_SendMail()
+ SendMail(SendMailNameEditBox:GetText(), SendMailSubjectEditBox:GetText(), SendMailBodyEditBox:GetText())
end
- function BenillaSendMailFrame_Update()
+ function SendMailFrame_Update()
local itemName, itemTexture, stackCount, quality = GetSendMailItem()
- getglobal("BenillaSendMailPackageButtonIcon"):SetTexture(itemTexture)
+ getglobal("SendMailPackageButtonIcon"):SetTexture(itemTexture)
if (stackCount or 1) <= 1 then stackCount = "" end
if itemName then
- BenillaSendMailCODButton:Enable()
- BenillaSendMailCODButtonText:SetTextColor(1.0, 0.82, 0)
- if BenillaSendMailSubjectEditBox:GetText() == "" or BenillaSendMailSubjectEditBox:GetText() == BenillaSendMailFrame.previousItem then
- BenillaSendMailSubjectEditBox:SetText(itemName)
- BenillaSendMailFrame.previousItem = itemName
+ SendMailCODButton:Enable()
+ SendMailCODButtonText:SetTextColor(1.0, 0.82, 0)
+ if SendMailSubjectEditBox:GetText() == "" or SendMailSubjectEditBox:GetText() == SendMailFrame.previousItem then
+ SendMailSubjectEditBox:SetText(itemName)
+ SendMailFrame.previousItem = itemName
end
else
- if BenillaSendMailSubjectEditBox:GetText() == BenillaSendMailFrame.previousItem then
- BenillaSendMailSubjectEditBox:SetText("")
+ if SendMailSubjectEditBox:GetText() == SendMailFrame.previousItem then
+ SendMailSubjectEditBox:SetText("")
end
- BenillaSendMailFrame.previousItem = ""
- BenillaSendMailRadioButton_OnClick(1)
- BenillaSendMailCODButton:Disable()
- BenillaSendMailCODButtonText:SetTextColor(0.5, 0.5, 0.5)
+ SendMailFrame.previousItem = ""
+ SendMailRadioButton_OnClick(1)
+ SendMailCODButton:Disable()
+ SendMailCODButtonText:SetTextColor(0.5, 0.5, 0.5)
end
- getglobal("BenillaSendMailPackageButtonCount"):SetText(stackCount)
- BenillaMoney_Set("BenillaSendMailMoneyFrame", GetMoney())
+ getglobal("SendMailPackageButtonCount"):SetText(stackCount)
+ BenillaMoney_Set("SendMailMoneyFrame", GetMoney())
-- The 30c postage (ref l.542-547); red when it exceeds the purse.
- BenillaMoney_Set("BenillaSendMailCost", GetSendMailPrice())
+ BenillaMoney_Set("SendMailCost", GetSendMailPrice())
if GetSendMailPrice() > GetMoney() then
- BenillaMoney_SetColor("BenillaSendMailCost", 1.0, 0.1, 0.1)
+ BenillaMoney_SetColor("SendMailCost", 1.0, 0.1, 0.1)
else
- BenillaMoney_SetColor("BenillaSendMailCost", 1.0, 1.0, 1.0)
+ BenillaMoney_SetColor("SendMailCost", 1.0, 1.0, 1.0)
end
- BenillaSendMailFrame_CanSend()
+ SendMailFrame_CanSend()
end
- function BenillaSendMailFrame_Reset()
- BenillaSendMailNameEditBox:SetText("")
- BenillaSendMailSubjectEditBox:SetText("")
- BenillaSendMailBodyEditBox:SetText("")
- BenillaMoneyInput_ResetMoney("BenillaSendMailMoney")
- BenillaSendMailRadioButton_OnClick(1)
- BenillaSendMailFrame_Update()
+ function SendMailFrame_Reset()
+ SendMailNameEditBox:SetText("")
+ SendMailSubjectEditBox:SetText("")
+ SendMailBodyEditBox:SetText("")
+ BenillaMoneyInput_ResetMoney("SendMailMoney")
+ SendMailRadioButton_OnClick(1)
+ SendMailFrame_Update()
end
-- SendMailFrame_CanSend (ref l.564-598): name + subject required (stationery is always the
-- default here — the picker is omitted); a COD send additionally caps at 10,000g.
- function BenillaSendMailFrame_CanSend()
+ function SendMailFrame_CanSend()
local checks = 0
local checksRequired = 2
- if string.len(BenillaSendMailNameEditBox:GetText()) > 0 then checks = checks + 1 end
- if string.len(BenillaSendMailSubjectEditBox:GetText()) > 0 then checks = checks + 1 end
- if BenillaSendMailCODButton:GetChecked() then
+ if string.len(SendMailNameEditBox:GetText()) > 0 then checks = checks + 1 end
+ if string.len(SendMailSubjectEditBox:GetText()) > 0 then checks = checks + 1 end
+ if SendMailCODButton:GetChecked() then
checksRequired = 3
- if BenillaMoneyInput_GetCopper("BenillaSendMailMoney") > BENILLA_MAX_COD_AMOUNT * 10000 then
- BenillaSendMailErrorText:Show()
- BenillaSendMailErrorCoin:Show()
+ if BenillaMoneyInput_GetCopper("SendMailMoney") > BENILLA_MAX_COD_AMOUNT * 10000 then
+ SendMailErrorText:Show()
+ SendMailErrorCoin:Show()
else
- BenillaSendMailErrorText:Hide()
- BenillaSendMailErrorCoin:Hide()
+ SendMailErrorText:Hide()
+ SendMailErrorCoin:Hide()
checks = checks + 1
end
else
- BenillaSendMailErrorText:Hide()
- BenillaSendMailErrorCoin:Hide()
+ SendMailErrorText:Hide()
+ SendMailErrorCoin:Hide()
end
if checks == checksRequired then
- BenillaSendMailMailButton:Enable()
+ SendMailMailButton:Enable()
else
- BenillaSendMailMailButton:Disable()
+ SendMailMailButton:Disable()
end
end
- function BenillaSendMailRadioButton_OnClick(index)
+ function SendMailRadioButton_OnClick(index)
if index == 1 then
- BenillaSendMailSendMoneyButton:SetChecked(1)
- BenillaSendMailCODButton:SetChecked(nil)
- BenillaSendMailMoneyText:SetText("Amount to send:") -- AMOUNT_TO_SEND
+ SendMailSendMoneyButton:SetChecked(1)
+ SendMailCODButton:SetChecked(nil)
+ SendMailMoneyText:SetText("Amount to send:") -- AMOUNT_TO_SEND
else
- BenillaSendMailSendMoneyButton:SetChecked(nil)
- BenillaSendMailCODButton:SetChecked(1)
- BenillaSendMailMoneyText:SetText("Cash on Delivery Amount:") -- COD_AMOUNT
+ SendMailSendMoneyButton:SetChecked(nil)
+ SendMailCODButton:SetChecked(1)
+ SendMailMoneyText:SetText("Cash on Delivery Amount:") -- COD_AMOUNT
end
PlaySound("igMainMenuOptionCheckBoxOn")
- BenillaSendMailFrame_CanSend()
+ SendMailFrame_CanSend()
end
-- The send item slot: a cursor drop (ref SendMailPackageButton_OnClick l.709-721; the cursor
-- MONEY drop path is omitted with the cursor-money system).
- function BenillaSendMailPackageButton_OnClick()
+ function SendMailPackageButton_OnClick()
ClickSendMailItemButton()
- BenillaSendMailFrame_Update()
+ SendMailFrame_Update()
end
-- ── The window-owned StaticPopups (ref StaticPopup.lua l.205/220/234/252; the DeathFrame.xml
@@ -540,7 +543,7 @@
text = "Accepting this item will cost:", -- COD_CONFIRMATION
button1 = "Accept",
button2 = "Cancel",
- OnAccept = function() TakeInboxItem(BenillaInboxFrame.openMailID) end,
+ OnAccept = function() TakeInboxItem(InboxFrame.openMailID) end,
timeout = 0,
hideOnEscape = 1,
}
@@ -549,9 +552,9 @@
button1 = "Accept",
button2 = "Cancel",
OnAccept = function()
- DeleteInboxItem(BenillaInboxFrame.openMailID)
- BenillaInboxFrame.openMailID = 0
- HideUIPanel(BenillaOpenMailFrame)
+ DeleteInboxItem(InboxFrame.openMailID)
+ InboxFrame.openMailID = 0
+ HideUIPanel(OpenMailFrame)
end,
showAlert = 1,
timeout = 0,
@@ -562,9 +565,9 @@
button1 = "Accept",
button2 = "Cancel",
OnAccept = function()
- DeleteInboxItem(BenillaInboxFrame.openMailID)
- BenillaInboxFrame.openMailID = 0
- HideUIPanel(BenillaOpenMailFrame)
+ DeleteInboxItem(InboxFrame.openMailID)
+ InboxFrame.openMailID = 0
+ HideUIPanel(OpenMailFrame)
end,
showAlert = 1,
timeout = 0,
@@ -575,11 +578,11 @@
button1 = "Accept",
button2 = "Cancel",
OnAccept = function()
- if SetSendMailMoney(BenillaMoneyInput_GetCopper("BenillaSendMailMoney")) then
- BenillaSendMailFrame_SendMail()
+ if SetSendMailMoney(BenillaMoneyInput_GetCopper("SendMailMoney")) then
+ SendMailFrame_SendMail()
end
end,
- OnCancel = function() BenillaSendMailMailButton:Enable() end,
+ OnCancel = function() SendMailMailButton:Enable() end,
timeout = 0,
hideOnEscape = 1,
}
@@ -657,8 +660,8 @@
this:RegisterForClicks("LeftButtonUp", "RightButtonUp")
- BenillaInboxFrame_OnClick(this.index)
- BenillaInboxFrameItem_OnEnter(self)
+ InboxFrame_OnClick(this.index)
+ InboxFrameItem_OnEnter()GameTooltip:Hide()
@@ -683,7 +686,7 @@
-
+
@@ -718,45 +721,45 @@
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
@@ -768,14 +771,14 @@
this:Disable()
- BenillaInboxPrevPage()
+ InboxPrevPage()
-
+
@@ -787,7 +790,7 @@
this:Disable()
- BenillaInboxNextPage()
+ InboxNextPage()
@@ -798,12 +801,12 @@
-
+
-
+
@@ -812,23 +815,23 @@
OnLoad (ref l.1040), because MoneyText is declared in a LATER child
frame and anchors resolve at SetPoint time (an XML forward reference
silently lands on the parent). Kept verbatim in our OnLoad below. -->
-
+
-
+
-
+
-
+
-
+
@@ -837,18 +840,18 @@
-
+
-
+
-
+
-
+
@@ -890,14 +893,14 @@
-
+
- BenillaSendMailBodyEditBox:SetFocus()
- if ( CursorHasItem() ) then BenillaSendMailPackageButton_OnClick() end
+ SendMailBodyEditBox:SetFocus()
+ if ( CursorHasItem() ) then SendMailPackageButton_OnClick() end
this:ClearFocus()
@@ -906,7 +909,7 @@
-
+
@@ -932,18 +935,18 @@
- BenillaSendMailSubjectEditBox:SetFocus()
+ SendMailSubjectEditBox:SetFocus()this:HighlightText(0, 0)
- BenillaSendMailSubjectEditBox:SetFocus()
- BenillaSendMailNameEditBox:ClearFocus()
- BenillaSendMailFrame_CanSend()
+ SendMailSubjectEditBox:SetFocus()
+ SendMailNameEditBox:ClearFocus()
+ SendMailFrame_CanSend()
-
+
@@ -972,9 +975,9 @@
-
+
-
+
@@ -998,16 +1001,16 @@
- BenillaSendMailBodyEditBox:SetFocus()
- BenillaSendMailBodyEditBox:SetFocus()
- BenillaSendMailSubjectEditBox:ClearFocus()
- BenillaSendMailFrame_CanSend()
+ SendMailBodyEditBox:SetFocus()
+ SendMailBodyEditBox:SetFocus()
+ SendMailSubjectEditBox:ClearFocus()
+ SendMailFrame_CanSend()
-
+
@@ -1018,7 +1021,7 @@
-
+
@@ -1036,7 +1039,7 @@
each box's L slice 8×20 at (-5,0), R slice at RIGHT (gold 0,
silver/copper -10), M stretched between; coin 13×13 LEFT→RIGHT at
+2 (gold) / -8 (silver, copper)). -->
-
+
@@ -1074,7 +1077,7 @@
getglobal(this:GetParent():GetName().."Silver"):SetFocus()getglobal(this:GetParent():GetName().."Silver"):SetFocus()this:ClearFocus()
- BenillaSendMailFrame_CanSend()
+ SendMailFrame_CanSend()this:HighlightText(0, 0)this:HighlightText()
@@ -1114,7 +1117,7 @@
getglobal(this:GetParent():GetName().."Copper"):SetFocus()getglobal(this:GetParent():GetName().."Copper"):SetFocus()this:ClearFocus()
- BenillaSendMailFrame_CanSend()
+ SendMailFrame_CanSend()this:HighlightText(0, 0)this:HighlightText()
@@ -1152,7 +1155,7 @@
this:ClearFocus()
- BenillaSendMailFrame_CanSend()
+ SendMailFrame_CanSend()this:HighlightText(0, 0)this:HighlightText()
@@ -1161,24 +1164,24 @@
-
+ getglobal(this:GetName().."Text"):SetText("Send Money")
- BenillaSendMailRadioButton_OnClick(1)
+ SendMailRadioButton_OnClick(1)
-
-
+
+ getglobal(this:GetName().."Text"):SetText("C.O.D.")
- BenillaSendMailRadioButton_OnClick(2)
+ SendMailRadioButton_OnClick(2)
- BenillaSendMailPackageButton_OnClick()
- BenillaSendMailPackageButton_OnClick()
+ SendMailPackageButton_OnClick()
+ SendMailPackageButton_OnClick()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
if ( GetSendMailItem() ) then
@@ -1195,8 +1198,8 @@
BOTTOMRIGHT→BOTTOMLEFT 183,84) — it fills the recess authored into
UI-ClassTrainer-BotLeft left of the Send button (without it the recess reads
as a broken empty input box). Inlined as the BenillaMoney coin trio; fed
- GetMoney() by BenillaSendMailFrame_Update + the PLAYER_MONEY event. -->
-
+ GetMoney() by SendMailFrame_Update + the PLAYER_MONEY event. -->
+
@@ -1219,7 +1222,7 @@
-
+
@@ -1227,18 +1230,18 @@
- HideUIPanel(BenillaMailFrame)
+ HideUIPanel(MailFrame)
-
+
-
+
- BenillaSendMailMailButton_OnClick()
+ SendMailMailButton_OnClick()
@@ -1246,7 +1249,7 @@
because its target lives in a later-declared child frame (the OnLoad's
SetText sibling is inlined as the XML text= literal). -->
- BenillaSendMailErrorText:SetPoint("BOTTOMLEFT", "BenillaSendMailMoneyText", "TOPLEFT", 0, 2)
+ SendMailErrorText:SetPoint("BOTTOMLEFT", "SendMailMoneyText", "TOPLEFT", 0, 2)
- BenillaStationeryBackgroundLeft:SetTexture(BENILLA_STATIONERY_PATH.."STATIONERYTEST1")
- BenillaStationeryBackgroundRight:SetTexture(BENILLA_STATIONERY_PATH.."STATIONERYTEST2")
- BenillaSendMailNameEditBox:SetFocus()
- BenillaSendMailRadioButton_OnClick(1)
+ StationeryBackgroundLeft:SetTexture(BENILLA_STATIONERY_PATH.."STATIONERYTEST1")
+ StationeryBackgroundRight:SetTexture(BENILLA_STATIONERY_PATH.."STATIONERYTEST2")
+ SendMailNameEditBox:SetFocus()
+ SendMailRadioButton_OnClick(1)
- if ( CursorHasItem() ) then BenillaSendMailPackageButton_OnClick() end
+ if ( CursorHasItem() ) then SendMailPackageButton_OnClick() end
-
+
- HideUIPanel(BenillaMailFrame)
+ HideUIPanel(MailFrame)
- BenillaMailFrameTab_OnClick(1)
+ MailFrameTab_OnClick(1)
- BenillaMailFrameTab_OnClick(2)
+ MailFrameTab_OnClick(2)BenillaMailFrame_OnLoad(self)
- BenillaMailFrame_OnEvent(self)
+ MailFrame_OnEvent()OpenBackpack(); PlaySound("igCharacterInfoOpen")
CloseMail()
CloseBackpack()
- HideUIPanel(BenillaOpenMailFrame)
- BenillaSendMailBodyEditBox:SetText("")
- BenillaSendMailNameEditBox:SetText("")
- BenillaSendMailSubjectEditBox:SetText("")
+ HideUIPanel(OpenMailFrame)
+ SendMailBodyEditBox:SetText("")
+ SendMailNameEditBox:SetText("")
+ SendMailSubjectEditBox:SetText("")
PlaySound("igCharacterInfoClose")
-
+
-
+
-
+ at runtime by OpenMail_Update). -->
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1383,7 +1386,7 @@
-
+
@@ -1418,16 +1421,16 @@
-
+
-
+
-
+
-
+
@@ -1438,9 +1441,9 @@
OpenMailMoneyButton l.1797-1835 (ItemButtonTemplate; inlined 37px icon + slot +
count). The row chains letter → package → money off the attachment caption;
OpenMail_Update re-points the two followers as their predecessors show/hide. -->
-
+
-
+
@@ -1453,9 +1456,9 @@
GameTooltip:Hide()
-
+
-
+
@@ -1465,13 +1468,13 @@
this:RegisterForClicks("LeftButtonUp", "RightButtonUp")BenillaOpenMailPackageButton_OnClick()
- BenillaOpenMailPackage_OnEnter(self)
+ OpenMailPackage_OnEnter()GameTooltip:Hide()
-
+
-
+
@@ -1486,7 +1489,7 @@
-
+
@@ -1496,40 +1499,40 @@
HideUIPanel(this:GetParent())
-
+
-
+
- BenillaOpenMail_Delete()
+ OpenMail_Delete()
-
+
-
+
- BenillaOpenMail_Reply()
+ OpenMail_Reply()
-
+
- HideUIPanel(BenillaOpenMailFrame)
+ HideUIPanel(OpenMailFrame)
- BenillaOpenMailFrame_OnHide()
+ OpenMailFrame_OnHide()
diff --git a/crates/benilla-app/assets/ui/MerchantFrame.xml b/crates/benilla-app/assets/ui/MerchantFrame.xml
index 294b0fd5..1392ef2e 100644
--- a/crates/benilla-app/assets/ui/MerchantFrame.xml
+++ b/crates/benilla-app/assets/ui/MerchantFrame.xml
@@ -3,8 +3,8 @@
benilla's vendor window (decision 0081 phase 4), RE-SKINNED to the real 1.12 merchant window
(dressing arc, decision 0084): the geometry and art below are QUOTED from the real
`MerchantFrame.xml` (scratchpad `ref-MerchantFrame.xml`; line citations inline), turned onto our
- own feeds/intents. The name `BenillaMerchantFrame`, its panel registration
- (UiPanels.xml's UIPanelWindows["BenillaMerchantFrame"], area="left"), the Era vendor feeds
+ own feeds/intents. The name `MerchantFrame`, its panel registration
+ (UiPanels.xml's UIPanelWindows["MerchantFrame"], area="left"), the Era vendor feeds
(GetMerchantNumItems/GetMerchantItemInfo/BuyMerchantItem/CloseMerchant, GetMoney), and the
intent functions are UNCHANGED — only the window's dressing is real now.
@@ -199,14 +199,14 @@
-- The merchant-list index a row button shows on the current page (ref index arithmetic,
-- MerchantFrame.lua l.74).
function BenillaMerchantFrame_ItemIndex(button)
- return ((BenillaMerchantFrame.page or 1) - 1) * BENILLA_MERCHANT_MAX + button.merchantIndex
+ return ((MerchantFrame.page or 1) - 1) * BENILLA_MERCHANT_MAX + button.merchantIndex
end
function BenillaMerchantItem_OnClick(button, mouseButton)
-- Buyback tab: either click buys the slot back (ref MerchantItemButton_OnClick's else
-- arm, l.358-361) — and, as there, with NO modifier branch of any kind: 1.12 ships no
-- GetBuybackItemLink, so a buyback row carries no link at all (ui_merchant.rs).
- if BenillaMerchantFrame.selectedTab == 2 then
+ if MerchantFrame.selectedTab == 2 then
BuybackItem(button.merchantIndex)
return
end
@@ -254,7 +254,7 @@
-- offset back to the icon's TOPRIGHT (37 − 153 = −116). No buy-price line — the price is
-- on the row, and the real vendor tooltip doesn't repeat it.
function BenillaMerchantItem_OnEnter(button)
- if BenillaMerchantFrame.selectedTab == 2 then
+ if MerchantFrame.selectedTab == 2 then
if not GetBuybackItemInfo(button.merchantIndex) then return end
GameTooltip:SetOwner(button, "ANCHOR_RIGHT", -116, 0)
GameTooltip:SetBuybackItem(button.merchantIndex)
@@ -272,14 +272,14 @@
-- Buy tab: remember the row so the frame's OnUpdate arms the coin every frame — Buy
-- vs the grayed UnableBuy by affordability, or the Inspect magnifier while Ctrl is held
-- (ref MerchantFrame.itemHover + OnUpdate l.82/725-731).
- BenillaMerchantFrame.itemHover = index
+ MerchantFrame.itemHover = index
end
function BenillaMerchantItem_OnLeave(button)
GameTooltip:Hide()
-- Drop the coin/inspect override and stop the OnUpdate arming (ref OnLeave l.87-90).
ResetCursor()
- BenillaMerchantFrame.itemHover = nil
+ MerchantFrame.itemHover = nil
end
-- One row's item fill — the shape both pages share (ref fills name/texture/count/money
@@ -337,13 +337,13 @@
-- The merchant page (ref MerchantFrame_UpdateMerchantInfo l.66-193): the paged 10 rows,
-- the repair pair, the single most-recent buyback slot, the page buttons, the merchant
-- bottom border — and the buyback page's pieces hidden.
- function BenillaMerchantFrame_UpdateMerchantInfo()
- local frame = BenillaMerchantFrame
+ function MerchantFrame_UpdateMerchantInfo()
+ local frame = MerchantFrame
local page = frame.page or 1
local numItems = GetMerchantNumItems()
for i = 1, BENILLA_MERCHANT_MAX do
- local button = getglobal("BenillaMerchantItem" .. i)
+ local button = getglobal("MerchantItem" .. i)
local index = (page - 1) * BENILLA_MERCHANT_MAX + i
if index <= numItems then
local name, texture, price, quantity, numAvailable, isUsable = GetMerchantItemInfo(index)
@@ -355,32 +355,32 @@
-- Rows back to the merchant spacing (ref l.188-192; the buyback page spreads them to
-- -15). SetPoint replaces the same-point anchor, the real client's semantics.
- getglobal("BenillaMerchantItem3"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem1"), "BOTTOMLEFT", 0, -8)
- getglobal("BenillaMerchantItem5"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem3"), "BOTTOMLEFT", 0, -8)
- getglobal("BenillaMerchantItem7"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem5"), "BOTTOMLEFT", 0, -8)
- getglobal("BenillaMerchantItem9"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem7"), "BOTTOMLEFT", 0, -8)
+ getglobal("MerchantItem3"):SetPoint("TOPLEFT", getglobal("MerchantItem1"), "BOTTOMLEFT", 0, -8)
+ getglobal("MerchantItem5"):SetPoint("TOPLEFT", getglobal("MerchantItem3"), "BOTTOMLEFT", 0, -8)
+ getglobal("MerchantItem7"):SetPoint("TOPLEFT", getglobal("MerchantItem5"), "BOTTOMLEFT", 0, -8)
+ getglobal("MerchantItem9"):SetPoint("TOPLEFT", getglobal("MerchantItem7"), "BOTTOMLEFT", 0, -8)
-- Buyback-page-only pieces off (ref l.180-186), merchant-only pieces on (ref l.175-178).
- getglobal("BenillaMerchantItem11"):Hide()
- getglobal("BenillaMerchantItem12"):Hide()
- getglobal("BenillaBuybackFrameTopLeft"):Hide()
- getglobal("BenillaBuybackFrameTopRight"):Hide()
- getglobal("BenillaBuybackFrameBotLeft"):Hide()
- getglobal("BenillaBuybackFrameBotRight"):Hide()
- getglobal("BenillaMerchantFrameBottomLeftBorder"):Show()
- getglobal("BenillaMerchantFrameBottomRightBorder"):Show()
+ getglobal("MerchantItem11"):Hide()
+ getglobal("MerchantItem12"):Hide()
+ getglobal("BuybackFrameTopLeft"):Hide()
+ getglobal("BuybackFrameTopRight"):Hide()
+ getglobal("BuybackFrameBotLeft"):Hide()
+ getglobal("BuybackFrameBotRight"):Hide()
+ getglobal("MerchantFrameBottomLeftBorder"):Show()
+ getglobal("MerchantFrameBottomRightBorder"):Show()
-- The repair pair shows iff this vendor repairs (ref l.121-130); the all-button
-- enables iff there is damage to pay for (ref MerchantFrame_OnShow l.37-46 — our
-- vertex-color gray stands in for its SetDesaturation, which the engine lacks).
if CanMerchantRepair() then
- getglobal("BenillaMerchantRepairText"):Show()
- local allButton = getglobal("BenillaMerchantRepairAllButton")
- local itemButton = getglobal("BenillaMerchantRepairItemButton")
+ getglobal("MerchantRepairText"):Show()
+ local allButton = getglobal("MerchantRepairAllButton")
+ local itemButton = getglobal("MerchantRepairItemButton")
allButton:Show()
itemButton:Show()
local repairAllCost, canRepair = GetRepairAllCost()
- local icon = getglobal("BenillaMerchantRepairAllIcon")
+ local icon = getglobal("MerchantRepairAllIcon")
if canRepair then
icon:SetVertexColor(1, 1, 1)
allButton:Enable()
@@ -389,14 +389,14 @@
allButton:Disable()
end
else
- getglobal("BenillaMerchantRepairText"):Hide()
- getglobal("BenillaMerchantRepairAllButton"):Hide()
- getglobal("BenillaMerchantRepairItemButton"):Hide()
+ getglobal("MerchantRepairText"):Hide()
+ getglobal("MerchantRepairAllButton"):Hide()
+ getglobal("MerchantRepairItemButton"):Hide()
end
-- The single buyback slot: the MOST RECENT sale (ref l.132-152 —
-- GetBuybackItemInfo(GetNumBuybackItems())).
- local slot = getglobal("BenillaMerchantBuyBackItem")
+ local slot = getglobal("MerchantBuyBackItem")
local bbName, bbTexture, bbPrice, bbQuantity = GetBuybackItemInfo(GetNumBuybackItems())
if bbName then
slot.icon:SetTexture(bbTexture or BENILLA_FALLBACK_ICON)
@@ -406,13 +406,13 @@
else
slot.count:SetText("")
end
- BenillaMoney_Set("BenillaMerchantBuyBackItemPrice", bbPrice, true)
+ BenillaMoney_Set("MerchantBuyBackItemPrice", bbPrice, true)
slot.slot:SetVertexColor(1, 1, 1)
else
slot.icon:SetTexture(nil)
slot.name:SetText("")
slot.count:SetText("")
- BenillaMoney_Clear("BenillaMerchantBuyBackItemPrice")
+ BenillaMoney_Clear("MerchantBuyBackItemPrice")
slot.slot:SetVertexColor(0.4, 0.4, 0.4)
-- Hide the tooltip upon sale of the last slot (ref l.148-151).
if GameTooltip:IsOwned(slot) then
@@ -422,9 +422,9 @@
slot:Show()
-- Paging (ref l.154-173): buttons + label only when the stock overflows one page.
- local prevButton = getglobal("BenillaMerchantPrevPageButton")
- local nextButton = getglobal("BenillaMerchantNextPageButton")
- local pageText = getglobal("BenillaMerchantPageText")
+ local prevButton = getglobal("MerchantPrevPageButton")
+ local nextButton = getglobal("MerchantNextPageButton")
+ local pageText = getglobal("MerchantPageText")
if numItems > BENILLA_MERCHANT_MAX then
pageText:SetText(string.format("Page %d", page)) -- PAGE_NUMBER
if page == 1 then prevButton:Disable() else prevButton:Enable() end
@@ -445,34 +445,34 @@
-- Title + portrait back to the vendor (ref l.67-68): re-bind the "npc" model bake the
-- buyback page's raw SetTexture(UI-BuyBack-Icon) dropped.
BenillaMerchantFrame_SetTitle(frame.vendorName)
- SetPortraitTexture(getglobal("BenillaMerchantFramePortrait"), "npc")
+ SetPortraitTexture(getglobal("MerchantFramePortrait"), "npc")
end
-- The buyback page (ref MerchantFrame_UpdateBuybackInfo l.195-273): 12 rows over the
-- buyback slots, the wider -15 row spacing, the buyback corner art — and every
-- merchant-only piece hidden.
- function BenillaMerchantFrame_UpdateBuybackInfo()
- getglobal("BenillaMerchantNameText"):SetText("Merchant Buyback") -- MERCHANT_BUYBACK
+ function MerchantFrame_UpdateBuybackInfo()
+ getglobal("MerchantNameText"):SetText("Merchant Buyback") -- MERCHANT_BUYBACK
-- The buyback icon is drawn for the portrait ring — the ref sets it RAW (SetTexture,
-- l.197), not through the portrait mask.
- getglobal("BenillaMerchantFramePortrait"):SetTexture("Interface\\MerchantFrame\\UI-BuyBack-Icon")
+ getglobal("MerchantFramePortrait"):SetTexture("Interface\\MerchantFrame\\UI-BuyBack-Icon")
- getglobal("BenillaMerchantItem11"):Show()
- getglobal("BenillaMerchantItem12"):Show()
- getglobal("BenillaBuybackFrameTopLeft"):Show()
- getglobal("BenillaBuybackFrameTopRight"):Show()
- getglobal("BenillaBuybackFrameBotLeft"):Show()
- getglobal("BenillaBuybackFrameBotRight"):Show()
+ getglobal("MerchantItem11"):Show()
+ getglobal("MerchantItem12"):Show()
+ getglobal("BuybackFrameTopLeft"):Show()
+ getglobal("BuybackFrameTopRight"):Show()
+ getglobal("BuybackFrameBotLeft"):Show()
+ getglobal("BuybackFrameBotRight"):Show()
-- The wider buyback row spacing (ref l.208-211).
- getglobal("BenillaMerchantItem3"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem1"), "BOTTOMLEFT", 0, -15)
- getglobal("BenillaMerchantItem5"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem3"), "BOTTOMLEFT", 0, -15)
- getglobal("BenillaMerchantItem7"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem5"), "BOTTOMLEFT", 0, -15)
- getglobal("BenillaMerchantItem9"):SetPoint("TOPLEFT", getglobal("BenillaMerchantItem7"), "BOTTOMLEFT", 0, -15)
+ getglobal("MerchantItem3"):SetPoint("TOPLEFT", getglobal("MerchantItem1"), "BOTTOMLEFT", 0, -15)
+ getglobal("MerchantItem5"):SetPoint("TOPLEFT", getglobal("MerchantItem3"), "BOTTOMLEFT", 0, -15)
+ getglobal("MerchantItem7"):SetPoint("TOPLEFT", getglobal("MerchantItem5"), "BOTTOMLEFT", 0, -15)
+ getglobal("MerchantItem9"):SetPoint("TOPLEFT", getglobal("MerchantItem7"), "BOTTOMLEFT", 0, -15)
local numBuyback = GetNumBuybackItems()
for i = 1, BENILLA_BUYBACK_MAX do
- local button = getglobal("BenillaMerchantItem" .. i)
+ local button = getglobal("MerchantItem" .. i)
if i <= numBuyback then
local name, texture, price, quantity, numAvailable, isUsable = GetBuybackItemInfo(i)
-- Never soldOut here: the ref's buyback sold-out leg tests its merchant-page
@@ -485,45 +485,45 @@
end
-- Merchant-only pieces off (ref l.263-272).
- getglobal("BenillaMerchantRepairText"):Hide()
- getglobal("BenillaMerchantRepairAllButton"):Hide()
- getglobal("BenillaMerchantRepairItemButton"):Hide()
- getglobal("BenillaMerchantBuyBackItem"):Hide()
- getglobal("BenillaMerchantPrevPageButton"):Hide()
- getglobal("BenillaMerchantNextPageButton"):Hide()
- getglobal("BenillaMerchantPageText"):Hide()
- getglobal("BenillaMerchantFrameBottomLeftBorder"):Hide()
- getglobal("BenillaMerchantFrameBottomRightBorder"):Hide()
+ getglobal("MerchantRepairText"):Hide()
+ getglobal("MerchantRepairAllButton"):Hide()
+ getglobal("MerchantRepairItemButton"):Hide()
+ getglobal("MerchantBuyBackItem"):Hide()
+ getglobal("MerchantPrevPageButton"):Hide()
+ getglobal("MerchantNextPageButton"):Hide()
+ getglobal("MerchantPageText"):Hide()
+ getglobal("MerchantFrameBottomLeftBorder"):Hide()
+ getglobal("MerchantFrameBottomRightBorder"):Hide()
end
- function BenillaMerchantFrame_Update()
+ function MerchantFrame_Update()
-- The tab dispatch (ref MerchantFrame_Update l.57-64) + the purse (the ref's money
-- frame updates itself on PLAYER_MONEY; ours repaints with the window).
- if BenillaMerchantFrame.selectedTab == 2 then
- BenillaMerchantFrame_UpdateBuybackInfo()
+ if MerchantFrame.selectedTab == 2 then
+ MerchantFrame_UpdateBuybackInfo()
else
- BenillaMerchantFrame_UpdateMerchantInfo()
+ MerchantFrame_UpdateMerchantInfo()
end
- BenillaMoney_Set("BenillaMerchantMoney", GetMoney())
+ BenillaMoney_Set("MerchantMoney", GetMoney())
end
-- The tab pair (ref MerchantFrameTab1/2 OnClick l.682-685/704-707): select + repaint.
function BenillaMerchantFrameTab_OnClick(id)
- PanelTemplates_SetTab(BenillaMerchantFrame, id)
- BenillaMerchantFrame_Update()
+ PanelTemplates_SetTab(MerchantFrame, id)
+ MerchantFrame_Update()
end
-- Paging clicks (ref MerchantPrevPageButton_OnClick/Next l.275-285).
- function BenillaMerchantPrevPageButton_OnClick()
+ function MerchantPrevPageButton_OnClick()
PlaySound("igMainMenuOptionCheckBoxOn")
- BenillaMerchantFrame.page = (BenillaMerchantFrame.page or 1) - 1
- BenillaMerchantFrame_Update()
+ MerchantFrame.page = (MerchantFrame.page or 1) - 1
+ MerchantFrame_Update()
end
- function BenillaMerchantNextPageButton_OnClick()
+ function MerchantNextPageButton_OnClick()
PlaySound("igMainMenuOptionCheckBoxOn")
- BenillaMerchantFrame.page = (BenillaMerchantFrame.page or 1) + 1
- BenillaMerchantFrame_Update()
+ MerchantFrame.page = (MerchantFrame.page or 1) + 1
+ MerchantFrame_Update()
end
-- The single buyback slot's handlers (ref MerchantBuyBackItemItemButton l.516-541): a
@@ -558,7 +558,8 @@
end
end
- function BenillaMerchantFrame_OnLoad(frame)
+ function MerchantFrame_OnLoad()
+ local frame = this
-- The NPC portrait: the "npc" unit token → the vendor's live model bake
-- (crates/benilla/src/portrait, InteractNpc). The buyback page overrides this ring with a
-- raw SetTexture (which drops the binding); the vendor page restores it (see below).
@@ -582,26 +583,27 @@
if name == nil or name == "" then
name = "Merchant"
end
- getglobal("BenillaMerchantNameText"):SetText(name)
+ getglobal("MerchantNameText"):SetText(name)
end
- function BenillaMerchantFrame_OnEvent(frame)
+ function MerchantFrame_OnEvent()
+ local frame = this
if event == "PLAYER_MONEY" then
- BenillaMoney_Set("BenillaMerchantMoney", GetMoney())
+ BenillaMoney_Set("MerchantMoney", GetMoney())
elseif event == "MERCHANT_SHOW" then
-- Fresh session: first page, merchant tab (ref OnEvent l.29 / OnShow l.47).
frame.page = 1
frame.vendorName = arg1
PanelTemplates_SetTab(frame, 1)
BenillaMerchantFrame_SetTitle(arg1)
- BenillaMerchantFrame_Update()
+ MerchantFrame_Update()
ShowUIPanel(frame)
elseif event == "MERCHANT_UPDATE" then
frame.vendorName = arg1
if frame.selectedTab ~= 2 then
BenillaMerchantFrame_SetTitle(arg1)
end
- BenillaMerchantFrame_Update()
+ MerchantFrame_Update()
elseif event == "MERCHANT_CLOSED" then
HideUIPanel(frame)
end
@@ -611,7 +613,7 @@
-- vanilla's client-side close sends no packet; the app clears the merchant on this intent
-- (which fires MERCHANT_CLOSED), and we hide immediately so there's no one-frame lag.
CloseMerchant()
- HideUIPanel(getglobal("BenillaMerchantFrame"))
+ HideUIPanel(getglobal("MerchantFrame"))
end
]]>
@@ -622,7 +624,7 @@
the icon, and the dark label plate $parentNameFrame (Interface\MerchantFrame\UI-Merchant-
LabelSlots, 128×78, LEFT of the slot -9,-18 — ref l.22-33) behind the name+price. Every one of
the 10 slots always renders these; an empty slot dims the socket to 0.4 and clears its item
- (BenillaMerchantFrame_Update, ref MerchantFrame.lua l.112-118). The name (GameFontNormalSmall,
+ (MerchantFrame_Update, ref MerchantFrame.lua l.112-118). The name (GameFontNormalSmall,
LEFT — ref l.34) sits above the price coins, the real name-over-money stack. The pushed-state
depress ring the ref repair buttons use (l.419) is a square-button texture that would smear
across our wide whole-row button, so it is OMITTED; the highlight is the square ADD glow.
@@ -786,7 +788,7 @@
-
+
@@ -830,20 +832,20 @@
OnEvent handler sets it here, falling back to "Merchant" until it lands. CENTER +
the (unsized, single-point) ~zero-width rect keeps the name centered on the
window regardless of length. -->
-
+
-
+
-
+
@@ -853,34 +855,34 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -892,8 +894,8 @@
starts at iconWidth — MoneyFrame.lua l.219), so the COIN edge the ref frame anchor
implies is -40-13 = -53, bottom 67. Three position-slots chained right-to-left with
the real 4px spacing, shrunk to content by the same BenillaMoney_Set fill as the row
- prices (prefix "BenillaMerchantMoney") — copper-first, right-flush off Coin1. -->
-
+ prices (prefix "MerchantMoney") — copper-first, right-flush off Coin1. -->
+
@@ -912,10 +914,10 @@
-
+
-
+
@@ -931,10 +933,10 @@
-
+
-
+
@@ -953,54 +955,54 @@
-
+ BenillaMerchantItem_OnLoad(self, 1)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 2)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 3)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 4)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 5)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 6)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 7)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 8)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 9)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 10)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 11)
-
-
+
+ BenillaMerchantItem_OnLoad(self, 12)
-
+
-
+
@@ -1036,10 +1038,10 @@
GameTooltip:Hide()
-
+
-
+
@@ -1064,10 +1066,10 @@
MerchantBuyBackItem l.460-559): the same socket+plate row shape at 153×37, the
shorter 64-tall label plate, anchored under row 10 (0,-53). One wide click
button, our whole-row model. -->
-
+
-
+
@@ -1190,7 +1192,7 @@
-
+
@@ -1211,10 +1213,10 @@
- BenillaMerchantPrevPageButton_OnClick()
+ MerchantPrevPageButton_OnClick()
-
+
@@ -1235,7 +1237,7 @@
- BenillaMerchantNextPageButton_OnClick()
+ MerchantNextPageButton_OnClick()
-
+
@@ -1273,19 +1275,19 @@
- BenillaMerchantFrame_OnLoad(self)
- BenillaMerchantFrame_OnEvent(self)
+ MerchantFrame_OnLoad()
+ MerchantFrame_OnEvent()
- if BenillaMerchantFrame.itemHover then
+ if MerchantFrame.itemHover then
if IsControlKeyDown() then
ShowInspectCursor()
else
- ShowMerchantSellCursor(BenillaMerchantFrame.itemHover)
+ ShowMerchantSellCursor(MerchantFrame.itemHover)
end
end
@@ -1307,7 +1309,7 @@
OpenBackpack(); PlaySound("igCharacterInfoOpen")
- CloseMerchant(); CloseBackpack(); HideRepairCursor(); ResetCursor(); BenillaMerchantFrame.itemHover = nil; PlaySound("igCharacterInfoClose")
+ CloseMerchant(); CloseBackpack(); HideRepairCursor(); ResetCursor(); MerchantFrame.itemHover = nil; PlaySound("igCharacterInfoClose")
diff --git a/crates/benilla-app/assets/ui/MicroMenu.xml b/crates/benilla-app/assets/ui/MicroMenu.xml
index f193babb..ae0f5bca 100644
--- a/crates/benilla-app/assets/ui/MicroMenu.xml
+++ b/crates/benilla-app/assets/ui/MicroMenu.xml
@@ -23,7 +23,7 @@
the action bar behind it, and an unguarded call would abort the rest of that OnShow. Same
posture as UiPanels.xml's `CloseMenus and CloseMenus()` and the `if GameTooltip` guards.
- Structure: the buttons are TOP-LEVEL frames anchored by name into BenillaActionBarArtFrame
+ Structure: the buttons are TOP-LEVEL frames anchored by name into MainMenuBarArtFrame
(this engine has no cross-file `parent=` attachment — SkillFrame.xml's header comment), then
seated one frame level above the art by BenillaActionBarArt_SeatAbove (ActionBar.xml) so the
bar plate doesn't paint over them. Same shape the bag bar next door already uses.
@@ -153,10 +153,10 @@
-- reference lists first. Its keyring arm has no frame here; the still-unwired Help button
-- simply never pushes.
function UpdateMicroButtons()
- BenillaMicroButton_SetPushed(CharacterMicroButton, BenillaMicro_PanelShown("BenillaCharacterFrame"))
- BenillaMicroButton_SetPushed(SpellbookMicroButton, BenillaMicro_PanelShown("BenillaSpellBookFrame"))
+ BenillaMicroButton_SetPushed(CharacterMicroButton, BenillaMicro_PanelShown("CharacterFrame"))
+ BenillaMicroButton_SetPushed(SpellbookMicroButton, BenillaMicro_PanelShown("SpellBookFrame"))
BenillaMicroButton_SetPushed(TalentMicroButton, BenillaMicro_PanelShown("BenillaTalentFrame"))
- BenillaMicroButton_SetPushed(QuestLogMicroButton, BenillaMicro_PanelShown("BenillaQuestLogFrame"))
+ BenillaMicroButton_SetPushed(QuestLogMicroButton, BenillaMicro_PanelShown("QuestLogFrame"))
BenillaMicroButton_SetPushed(WorldMapMicroButton, BenillaMicro_PanelShown("WorldMapFrame"))
BenillaMicroButton_SetPushed(MainMenuMicroButton, BenillaMicro_PanelShown("GameMenuFrame"))
end
@@ -249,7 +249,7 @@
button's TOP -28. -->
-
+
@@ -262,7 +262,7 @@
CharacterMicroButton_OnLoad(self)
- ToggleCharacter("BenillaPaperDollFrame")
+ ToggleCharacter("PaperDollFrame")CharacterMicroButton_OnUpdate(self)
diff --git a/crates/benilla-app/assets/ui/MoneyFrame.xml b/crates/benilla-app/assets/ui/MoneyFrame.xml
new file mode 100644
index 00000000..404ecdce
--- /dev/null
+++ b/crates/benilla-app/assets/ui/MoneyFrame.xml
@@ -0,0 +1,601 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ if ( OpenCoinPickupFrame ) then
+ OpenCoinPickupFrame(1, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
+ parent.hasPickup = 1
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ if ( OpenCoinPickupFrame ) then
+ OpenCoinPickupFrame(COPPER_PER_SILVER, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
+ parent.hasPickup = 1
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ if ( OpenCoinPickupFrame ) then
+ OpenCoinPickupFrame(COPPER_PER_GOLD, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
+ parent.hasPickup = 1
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MoneyFrame_OnLoad()
+ MoneyFrame_OnEvent()
+ MoneyFrame_UpdateMoney()
+
+ if ( this.hasPickup == 1 and CoinPickupFrame ) then
+ CoinPickupFrame:Hide()
+ this.hasPickup = 0
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ if ( OpenCoinPickupFrame ) then
+ OpenCoinPickupFrame(1, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
+ parent.hasPickup = 1
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ if ( OpenCoinPickupFrame ) then
+ OpenCoinPickupFrame(COPPER_PER_SILVER, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
+ parent.hasPickup = 1
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ if ( OpenCoinPickupFrame ) then
+ OpenCoinPickupFrame(COPPER_PER_GOLD, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
+ parent.hasPickup = 1
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SmallMoneyFrame_OnLoad()
+ MoneyFrame_OnEvent()
+ MoneyFrame_UpdateMoney()
+
+ if ( this.hasPickup == 1 and CoinPickupFrame ) then
+ CoinPickupFrame:Hide()
+ this.hasPickup = 0
+ end
+
+
+
+
diff --git a/crates/benilla-app/assets/ui/MultiBars.xml b/crates/benilla-app/assets/ui/MultiBars.xml
index 09ebac3e..45dd0af0 100644
--- a/crates/benilla-app/assets/ui/MultiBars.xml
+++ b/crates/benilla-app/assets/ui/MultiBars.xml
@@ -15,7 +15,7 @@
as per-button data.
- Buttons reuse BenillaActionButtonTemplate + the whole ActionBar.xml handler set: same
click/drag/keyless-use, same feedback layer (state/usable/cooldown/flash), fanned from
- BenillaActionBar's bar-level events via the BENILLA_ACTION_BUTTONS roster each button
+ MainMenuBar's bar-level events via the BENILLA_ACTION_BUTTONS roster each button
joins at OnLoad.
Ref divergences (deliberate, mirroring ActionBar.xml's SCOPE notes):
@@ -36,24 +36,30 @@
BENILLA_MULTIBAR_BOTTOMLEFT_BASE = 60
BENILLA_MULTIBAR_BOTTOMRIGHT_BASE = 48
- -- The MULTIACTIONBAR keybind pair — ref MultiActionBars.lua:24-45 MultiActionButtonDown/
- -- Up, transcribed onto our frame names (`bar` is "BottomLeft"/"BottomRight", the ref
- -- passes "MultiBarBottomLeft"); the same NORMAL→PUSHED debounce dance as the main bar's
- -- BenillaActionButtonDown/Up (ActionBar.xml — the ref's MacroFrame_SaveMacro hook and
- -- onSelf argument are not modeled there either). Driven by the binding dispatch
- -- (crate::bindings, Kind::EdgeUpDown — decision 1008).
- function BenillaMultiActionButtonDown(bar, id)
- local button = getglobal("BenillaMultiBar" .. bar .. "Button" .. id)
+ -- The MULTIACTIONBAR keybind pair — ref MultiActionBars.lua:24-45 MultiActionButtonDown/Up,
+ -- the same NORMAL→PUSHED debounce dance as the main bar's ActionButtonDown/Up
+ -- (ActionBar.xml — the ref's MacroFrame_SaveMacro hook is not modeled there either).
+ -- Driven by the binding dispatch (crate::bindings, Kind::EdgeUpDown — decision 1008).
+ --
+ -- `bar` IS THE WHOLE FRAME-NAME PREFIX, not a suffix (decision 1199). The reference resolves
+ -- `getglobal(bar.."Button"..id)` and its own Bindings.xml passes "MultiBarBottomLeft"; ours
+ -- used to pass "BottomLeft" and splice "MultiBar" on in here. Same arity, different MEANING
+ -- — the silent kind of wrong shape: zBar calls `MultiActionButtonDown("zBar"..barid, id)`
+ -- and under the old splice built the nonexistent frame name "MultiBarzBar1Button1". The
+ -- caller now names the bar in full, exactly as the reference's binding does, so a third-
+ -- party bar can address these at all. `onSelf` rides to UseAction like ActionButtonUp's.
+ function MultiActionButtonDown(bar, id)
+ local button = getglobal(bar .. "Button" .. id)
if ( button:GetButtonState() == "NORMAL" ) then
button:SetButtonState("PUSHED")
end
end
- function BenillaMultiActionButtonUp(bar, id)
- local button = getglobal("BenillaMultiBar" .. bar .. "Button" .. id)
+ function MultiActionButtonUp(bar, id, onSelf)
+ local button = getglobal(bar .. "Button" .. id)
if ( button:GetButtonState() == "PUSHED" ) then
button:SetButtonState("NORMAL")
- UseAction(BenillaActionButton_Action(button))
+ UseAction(BenillaActionButton_Action(button), 0, onSelf)
-- Ref MultiActionButtonUp:39-43: the checked ring reflects the use immediately.
BenillaActionButton_UpdateState(button)
end
@@ -65,116 +71,116 @@
both sides by design — and both sides carry deep nested button trees, so co-tenanting one
stratum is exactly the level-outranks-insertion collision decision 0597 fixed for the party
frame. HIGH is what keeps the bars above an open bag. -->
-
+
-
+
-
+ BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")
-
+
-
+
-
+ BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
-
-
+
+ BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")
diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index 2fc4abe1..6a0d474f 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -326,7 +326,7 @@
function OptionsSearch_Words(text)
local up = string.upper(text)
local words = { up }
- for w in string.gmatch(up, "([^, ]+)") do
+ for w in string.gfind(up, "([^, ]+)") do
table.insert(words, w)
end
return words
@@ -2373,7 +2373,7 @@
OptionsGlobalRow_OnLoad(self, "SHOW_BUFF_DURATIONS",
"Buff Durations", "OPTION_TOOLTIP_SHOW_BUFF_DURATION")
- self.applyFunc = BenillaBuffButtons_UpdatePositions
+ self.applyFunc = BuffButtons_UpdatePositions
@@ -2685,14 +2685,14 @@
seating). The wheel handler is NOT here: a spin over a row
bubbles up the PARENT chain (pointer.rs), and this frame is
the rows' sibling — it lives on the body below. -->
-
+
- BenillaFauxScrollFrame_OnLoad(this)
+ ScrollFrame_OnLoad()
this.updateFunc = KeyBindingsPage_Update
diff --git a/crates/benilla-app/assets/ui/OptionsFrameTemplates.xml b/crates/benilla-app/assets/ui/OptionsFrameTemplates.xml
new file mode 100644
index 00000000..3ffbeffe
--- /dev/null
+++ b/crates/benilla-app/assets/ui/OptionsFrameTemplates.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ this:SetBackdropBorderColor(0.4, 0.4, 0.4)
+ this:SetBackdropColor(0.5, 0.5, 0.5)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if (this:GetChecked()) then
+ PlaySound("igMainMenuOptionCheckBoxOn")
+ else
+ PlaySound("igMainMenuOptionCheckBoxOff")
+ end
+
+
+ if (this.tooltipText) then
+ GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
+ GameTooltip:SetText(this.tooltipText, nil, nil, nil, nil, 1)
+ end
+ if (this.tooltipRequirement) then
+ GameTooltip:AddLine(this.tooltipRequirement, "", 1.0, 1.0, 1.0)
+ GameTooltip:Show()
+ end
+
+ GameTooltip:Hide()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if (this.tooltipText) then
+ GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
+ GameTooltip:SetText(this.tooltipText, nil, nil, nil, nil, 1)
+ end
+ if (this.tooltipRequirement) then
+ GameTooltip:AddLine(this.tooltipRequirement, "", 1.0, 1.0, 1.0)
+ GameTooltip:Show()
+ end
+
+ GameTooltip:Hide()
+
+
+
+
+
+
diff --git a/crates/benilla-app/assets/ui/PartyFrame.xml b/crates/benilla-app/assets/ui/PartyFrame.xml
index 92002492..7fe515df 100644
--- a/crates/benilla-app/assets/ui/PartyFrame.xml
+++ b/crates/benilla-app/assets/ui/PartyFrame.xml
@@ -144,14 +144,14 @@
frame.healthBar:SetMinMaxValues(0, UnitHealthMax(unit))
frame.healthBar:SetValue(UnitHealth(unit))
- local maxPower = UnitPowerMax(unit)
+ local maxPower = UnitManaMax(unit)
if maxPower > 0 then
frame.manaBar:Show()
local _, token = UnitPowerType(unit)
local c = BENILLA_POWER_COLORS[token] or BENILLA_POWER_COLORS.MANA
frame.manaBar:SetStatusBarColor(c.r, c.g, c.b)
frame.manaBar:SetMinMaxValues(0, maxPower)
- frame.manaBar:SetValue(UnitPower(unit))
+ frame.manaBar:SetValue(UnitMana(unit))
else
frame.manaBar:Hide()
end
@@ -201,7 +201,7 @@
end
local hp, hpMax = UnitHealth(unit), UnitHealthMax(unit)
if hpMax > 0 and hp > 0 and hp / hpMax <= 0.2 then
- local t = math.fmod(frame.flashTimer + elapsed, 1.0)
+ local t = mod(frame.flashTimer + elapsed, 1.0)
frame.flashTimer = t
local a
if t < 0.5 then
diff --git a/crates/benilla-app/assets/ui/PetActionBar.xml b/crates/benilla-app/assets/ui/PetActionBar.xml
index 93d01a80..92060d1c 100644
--- a/crates/benilla-app/assets/ui/PetActionBar.xml
+++ b/crates/benilla-app/assets/ui/PetActionBar.xml
@@ -25,7 +25,7 @@
strips (256x44 at TOPLEFT with TexCoords 0..1 / 0.015625..0.359375, then 184x44 to its
right with 0..0.71875 / 0.375..0.71875); buttons 30x30, button1 at frame BOTTOMLEFT +(36,2),
chained +8 (with the ref's own +7 between 6 and 7); the 54x54 UI-Quickslot2 ring CENTER
- (0,-1). The bar rides `BenillaActionBar` BOTTOMLEFT — our MainMenuBar, the same 1024x53
+ (0,-1). The bar rides `MainMenuBar` BOTTOMLEFT — our MainMenuBar, the same 1024x53
frame at screen BOTTOM the ref anchors to — at the managed PETACTIONBAR_YPOS (UIParent.xml
declares the row already).
@@ -222,13 +222,14 @@
-- and the reference immediately clears that (`this:SetChecked(0)`) — nothing about this bar
-- is locally latched, because the server owns every bit of it. Left = run the slot, unless
-- the pet is already on the attack, in which case it calls off; right = flip autocast.
- function BenillaPetActionButton_OnClick(button, mouseButton)
+ function PetActionButton_OnClick(mouseButton)
+ local button = this
button:SetChecked(0)
-- Shift outranks the button fork entirely (ref l.253-255): a shift-click on the pet bar
-- is a pick-up whichever mouse button it came from, and it never toggles autocast.
if IsShiftKeyDown() then
PickupPetAction(button.index)
- BenillaPetActionBar_Update()
+ PetActionBar_Update()
elseif mouseButton == "RightButton" then
TogglePetAutocast(button.index)
elseif IsPetAttackActive(button.index) then
@@ -255,15 +256,15 @@
-- latch lit, so click and key both simply order the attack. The split is visible on a
-- POSSESS bar (Mind Control, Eye of Kilrogg, Eyes of the Beast): there a click sends
-- PetStopAttack and CTRL-1 re-issues the order instead.
- function BenillaPetActionButtonDown(id)
- local button = getglobal("BenillaPetActionButton" .. id)
+ function PetActionButtonDown(id)
+ local button = getglobal("PetActionButton" .. id)
if ( button:GetButtonState() == "NORMAL" ) then
button:SetButtonState("PUSHED")
end
end
- function BenillaPetActionButtonUp(id)
- local button = getglobal("BenillaPetActionButton" .. id)
+ function PetActionButtonUp(id)
+ local button = getglobal("PetActionButton" .. id)
if ( button:GetButtonState() == "PUSHED" ) then
button:SetButtonState("NORMAL")
CastPetAction(id)
@@ -278,22 +279,25 @@
-- Both also honour the lock (the ref wraps each body in `if LOCK_ACTIONBAR ~= "1" then …`).
-- The shift-click in OnClick above is unguarded on this bar too, exactly as the reference
-- leaves it (l.253-255): the lock stops the drag, shift-click stays the way through.
- function BenillaPetActionButton_OnDragStart(button)
+ function PetActionButton_OnDragStart()
+ local button = this
if LOCK_ACTIONBAR == "1" then return end
PickupPetAction(button.index)
- BenillaPetActionBar_Update()
+ PetActionBar_Update()
end
- function BenillaPetActionButton_OnReceiveDrag(button)
+ function PetActionButton_OnReceiveDrag()
+ local button = this
if LOCK_ACTIONBAR == "1" then return end
PickupPetAction(button.index)
- BenillaPetActionBar_Update()
+ PetActionBar_Update()
end
-- Ref PetActionButton_OnEnter. A token slot has no spell to look up, so its tooltip is
-- built from the two strings the info call already returned (the ref's own `isToken` fork);
-- a spell slot goes through the engine's tooltip channel.
- function BenillaPetActionButton_OnEnter(button)
+ function PetActionButton_OnEnter()
+ local button = this
if not GameTooltip then return end -- engine tests load file subsets
if not button.tooltipName then return end
GameTooltip_SetDefaultAnchor(GameTooltip, button)
@@ -308,7 +312,8 @@
end
end
- function BenillaPetActionButton_OnLeave(button)
+ function PetActionButton_OnLeave()
+ local button = this
if GameTooltip then GameTooltip:Hide() end
end
@@ -318,10 +323,10 @@
-- header). Every branch below is the reference's, including the two that are easy to lose:
-- an unnamed slot HIDES its button, and a slot with no texture swaps the button's ring from
-- the filled UI-Quickslot2 to the empty UI-Quickslot.
- function BenillaPetActionBar_Update()
+ function PetActionBar_Update()
local usable = GetPetActionsUsable()
for i = 1, BENILLA_NUM_PET_ACTION_SLOTS do
- local button = getglobal("BenillaPetActionButton" .. i)
+ local button = getglobal("PetActionButton" .. i)
local name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled = GetPetActionInfo(i)
button.isToken = isToken
button.tooltipSubtext = subtext
@@ -341,7 +346,7 @@
-- An unnamed slot hides — UNLESS the drop grid is up, which is the whole point of
-- the grid: while a pet action rides the cursor every slot shows, so you can see
-- where it may land (ref PetActionBar_Update l.122-128 + PetActionBar_ShowGrid).
- if name or (BenillaPetActionBarFrame.showgrid or 0) > 0 then
+ if name or (PetActionBarFrame.showgrid or 0) > 0 then
button:Show()
else
button:Hide()
@@ -361,7 +366,8 @@
end
end
- function BenillaPetActionBar_OnLoad(frame)
+ function PetActionBar_OnLoad()
+ local frame = this
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PET_BAR_UPDATE")
-- The drop grid (decision 1010). A counter rather than a flag, ref-verbatim
@@ -372,21 +378,26 @@
frame:RegisterEvent("PET_BAR_HIDEGRID")
frame.showgrid = 0
frame.clock = 0
- BenillaPetActionBar_OnEvent(frame)
+ PetActionBar_OnEvent()
end
-- The bar exists exactly while PetHasActionBar() does. Its show/hide re-fires the managed
-- bottom-stack pass (UIParent.xml, decision 0272) — the pet term of that stack is what
-- lifts the cast bar and the chat frame clear of it, which is the ref's own
-- PetActionBarFrame.xml:205-210.
- function BenillaPetActionBar_OnEvent(frame, eventName)
- if eventName == "PET_BAR_SHOWGRID" then
+ -- Declares nothing and reads the ambient `event`, exactly as the reference does
+ -- (PetActionBarFrame.lua:33 / .xml's bare `PetActionBar_OnEvent();`). PetActionBar_OnLoad's
+ -- own tail call therefore lands with `event` nil, which is the no-grid-edge path — the same
+ -- thing the old explicit `eventName` argument did when omitted.
+ function PetActionBar_OnEvent()
+ local frame = this
+ if event == "PET_BAR_SHOWGRID" then
frame.showgrid = frame.showgrid + 1
- elseif eventName == "PET_BAR_HIDEGRID" and frame.showgrid > 0 then
+ elseif event == "PET_BAR_HIDEGRID" and frame.showgrid > 0 then
frame.showgrid = frame.showgrid - 1
end
if PetHasActionBar() then
- BenillaPetActionBar_Update()
+ PetActionBar_Update()
frame:Show()
else
frame:Hide()
@@ -401,7 +412,7 @@
frame.clock = frame.clock - BENILLA_PET_AUTOCAST_PERIOD
end
for i = 1, BENILLA_NUM_PET_ACTION_SLOTS do
- BenillaPetAutocast_Update(getglobal("BenillaPetActionButton" .. i), frame.clock)
+ BenillaPetAutocast_Update(getglobal("PetActionButton" .. i), frame.clock)
end
end
@@ -519,15 +530,15 @@
- BenillaPetActionButton_OnClick(self, arg1)
- BenillaPetActionButton_OnDragStart(self)
- BenillaPetActionButton_OnReceiveDrag(self)
- BenillaPetActionButton_OnEnter(self)
- BenillaPetActionButton_OnLeave(self)
+ PetActionButton_OnClick(arg1)
+ PetActionButton_OnDragStart()
+ PetActionButton_OnReceiveDrag()
+ PetActionButton_OnEnter()
+ PetActionButton_OnLeave()
-
+
-
+
-
+
-
+
-
+
-
+ BenillaPetActionButton_OnLoad(self, 1)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 2)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 3)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 4)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 5)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 6)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 7)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 8)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 9)
-
-
+
+ BenillaPetActionButton_OnLoad(self, 10)
- BenillaPetActionBar_OnLoad(self)
- BenillaPetActionBar_OnEvent(self, event)
+ PetActionBar_OnLoad()
+ PetActionBar_OnEvent()BenillaPetActionBar_OnUpdate(self, arg1)UIParent_ManageFramePositions()UIParent_ManageFramePositions()
diff --git a/crates/benilla-app/assets/ui/PetPaperDollFrame.xml b/crates/benilla-app/assets/ui/PetPaperDollFrame.xml
index 7c36047c..8b50741e 100644
--- a/crates/benilla-app/assets/ui/PetPaperDollFrame.xml
+++ b/crates/benilla-app/assets/ui/PetPaperDollFrame.xml
@@ -7,7 +7,7 @@
**Its own top-level file, loaded straight after `CharacterFrame.xml`** — the SkillFrame.xml
precedent: this engine reads no XML `parent=` attribute, so a page ships as a sibling pinned
- to `BenillaCharacterFrame`'s TOPLEFT by name, and `BENILLA_CHARACTERFRAME_SUBFRAMES` (which
+ to `CharacterFrame`'s TOPLEFT by name, 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
@@ -19,10 +19,10 @@
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 `"BenillaPet"`, so `prefix.."ArmorFrame"` resolves `BenillaPetArmorFrame`,
- exactly as `"BenillaCharacter"` resolves the sibling page's.
+ `prefix` here is `"Pet"`, so `prefix.."ArmorFrame"` resolves `PetArmorFrame`,
+ exactly as `"Character"` resolves the sibling page's.
- **The tab is conditional, and `BenillaPetTab_Update` is the whole mechanism** (ref
+ **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. Ours is the
ref's arithmetic unchanged; only WHICH page tab 3 is differs (Skills here, Reputation there —
@@ -55,7 +55,7 @@
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.
- - `BenillaPetPaperDollFrame_SetResistances` reads the school ids from the shared
+ - `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
@@ -90,12 +90,13 @@
-- 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 BenillaPetPaperDollFrame_OnLoad(frame)
- getglobal("BenillaPetAttackFrameLabel"):SetText(ATTACK_COLON)
- getglobal("BenillaPetDamageFrameLabel"):SetText(DAMAGE_COLON)
- getglobal("BenillaPetAttackPowerFrameLabel"):SetText(ATTACK_POWER_COLON)
- getglobal("BenillaPetDefenseFrameLabel"):SetText(DEFENSE_COLON)
- getglobal("BenillaPetArmorFrameLabel"):SetText(ARMOR_COLON)
+ 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")
@@ -113,55 +114,56 @@
frame:RegisterEvent("UNIT_DEFENSE")
frame:RegisterEvent("UNIT_ATTACK")
frame:RegisterEvent("UNIT_PET_TRAINING_POINTS")
- BenillaPetTab_Update()
+ 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 BenillaPetPaperDollFrame_OnEvent(frame)
+ 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("BenillaPetPaperDollFrame")
+ ToggleCharacter("PetPaperDollFrame")
end
- BenillaPetTab_Update()
- BenillaPetPaperDollFrame_Update()
+ PetTab_Update()
+ PetPaperDollFrame_Update()
elseif event == "PET_UI_CLOSE" then
if frame:IsVisible() then
- ToggleCharacter("BenillaPetPaperDollFrame")
+ ToggleCharacter("PetPaperDollFrame")
end
- BenillaPetTab_Update()
+ PetTab_Update()
elseif event == "UNIT_PET_EXPERIENCE" then
- BenillaPetExpBar_Update()
+ PetExpBar_Update()
elseif arg1 == "pet" then
- BenillaPetPaperDollFrame_Update()
+ 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 BenillaPetPaperDollFrame_OnShow()
- getglobal("BenillaCharacterNameText"):Hide()
- local petName = getglobal("BenillaPetNameText")
+ function PetPaperDollFrame_OnShow()
+ getglobal("CharacterNameText"):Hide()
+ local petName = getglobal("PetNameText")
petName:Show()
petName:SetText(UnitName("pet"))
- BenillaPetPaperDollFrame_Update()
+ PetPaperDollFrame_Update()
end
- function BenillaPetPaperDollFrame_OnHide()
- getglobal("BenillaCharacterNameText"):Show()
- getglobal("BenillaPetNameText"):Hide()
+ 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 BenillaPetPaperDollFrame_Update()
+ 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
@@ -174,34 +176,34 @@
-- the binding twice for one answer.
local family = UnitCreatureFamily("pet")
if family then
- getglobal("BenillaPetLevelText"):SetText(
+ getglobal("PetLevelText"):SetText(
format(UNIT_LEVEL_TEMPLATE, UnitLevel("pet")) .. " " .. family)
end
- getglobal("BenillaPetLoyaltyText"):SetText(GetPetLoyalty())
- BenillaPetExpBar_Update()
- BenillaPetPaperDollFrame_SetResistances()
- BenillaPetPaperDollFrame_SetStats()
- BenillaPaperDollFrame_SetDamage("pet", "BenillaPet")
- BenillaPaperDollFrame_SetRangedDamage("pet", "BenillaPet")
- BenillaPaperDollFrame_SetAttackPower("pet", "BenillaPet")
- BenillaPaperDollFrame_SetRangedAttackPower("pet", "BenillaPet")
- BenillaPaperDollFrame_SetArmor("pet", "BenillaPet")
- BenillaPaperDollFrame_SetAttackBothHands("pet", "BenillaPet")
- BenillaPaperDollFrame_SetDefense("pet", "BenillaPet")
+ getglobal("PetLoyaltyText"):SetText(GetPetLoyalty())
+ PetExpBar_Update()
+ PetPaperDollFrame_SetResistances()
+ PetPaperDollFrame_SetStats()
+ PaperDollFrame_SetDamage("pet", "Pet")
+ PaperDollFrame_SetRangedDamage("pet", "Pet")
+ PaperDollFrame_SetAttackPower("pet", "Pet")
+ PaperDollFrame_SetRangedAttackPower("pet", "Pet")
+ PaperDollFrame_SetArmor("pet", "Pet")
+ PaperDollFrame_SetAttackBothHands("pet", "Pet")
+ PaperDollFrame_SetDefense("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("BenillaPetPaperDollPetInfo"):Show()
+ getglobal("PetPaperDollPetInfo"):Show()
local totalPoints, spent = GetPetTrainingPoints()
- local text = getglobal("BenillaPetTrainingPointText")
+ local text = getglobal("PetTrainingPointText")
text:SetText(totalPoints - spent)
text:Show()
- getglobal("BenillaPetTrainingPointLabel"):Show()
+ getglobal("PetTrainingPointLabel"):Show()
else
- getglobal("BenillaPetPaperDollPetInfo"):Hide()
- getglobal("BenillaPetTrainingPointText"):Hide()
- getglobal("BenillaPetTrainingPointLabel"):Hide()
+ getglobal("PetPaperDollPetInfo"):Hide()
+ getglobal("PetTrainingPointText"):Hide()
+ getglobal("PetTrainingPointLabel"):Hide()
end
end
@@ -209,11 +211,11 @@
-- 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, transcribed rather than unified.
- function BenillaPetPaperDollFrame_SetResistances()
+ function PetPaperDollFrame_SetResistances()
for i = 1, BENILLA_NUM_PET_RESISTANCE_TYPES do
local resistId = BENILLA_MAGICRES_IDS[i]
- local text = getglobal("BenillaPetMagicResText" .. i)
- local frame = getglobal("BenillaPetMagicResFrame" .. 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.
@@ -241,11 +243,11 @@
-- _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 BenillaPetPaperDollFrame_SetStats()
+ function PetPaperDollFrame_SetStats()
for i = 1, BENILLA_NUM_PET_STATS do
- local label = getglobal("BenillaPetStatFrame" .. i .. "Label")
- local text = getglobal("BenillaPetStatFrame" .. i .. "StatText")
- local frame = getglobal("BenillaPetStatFrame" .. i)
+ 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)
@@ -279,9 +281,9 @@
-- 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 BenillaPetExpBar_Update()
+ function PetExpBar_Update()
local currXP, nextXP = GetPetExperience()
- local bar = getglobal("BenillaPetPaperDollFrameExpBar")
+ local bar = getglobal("PetPaperDollFrameExpBar")
bar:SetMinMaxValues(math.min(0, currXP), nextXP)
bar:SetValue(currXP)
end
@@ -289,27 +291,27 @@
-- ref PetTab_Update (l.189-198) — the conditional tab, whole. 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.
- function BenillaPetTab_Update()
- local tab2 = getglobal("BenillaCharacterFrameTab2")
- local tab3 = getglobal("BenillaCharacterFrameTab3")
+ 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", "BenillaCharacterFrameTab2", "LEFT", 0, 0)
+ tab3:SetPoint("LEFT", "CharacterFrameTab2", "LEFT", 0, 0)
else
tab2:Show()
- tab3:SetPoint("LEFT", "BenillaCharacterFrameTab2", "RIGHT", -16, 0)
+ tab3:SetPoint("LEFT", "CharacterFrameTab2", "RIGHT", -16, 0)
end
end
]]>
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -367,19 +369,19 @@
-
+
-
+
-
+
@@ -390,14 +392,14 @@
- BenillaPetExpBar_Update()
+ PetExpBar_Update()
-
+
@@ -423,26 +425,26 @@
-
+
-
+ self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")
- BenillaPaperDollModel_RotateLeft(getglobal("BenillaPetModelFrame"))
+ BenillaPaperDollModel_RotateLeft(getglobal("PetModelFrame"))
-
+
-
+ self:RegisterForClicks("LeftButtonDown", "LeftButtonUp")
- BenillaPaperDollModel_RotateRight(getglobal("BenillaPetModelFrame"))
+ BenillaPaperDollModel_RotateRight(getglobal("PetModelFrame"))
@@ -467,10 +469,10 @@
(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. -->
-
+
-
+
@@ -505,7 +507,7 @@
-
+
@@ -519,7 +521,7 @@
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. -->
- HideUIPanel(getglobal("BenillaCharacterFrame"))
+ HideUIPanel(getglobal("CharacterFrame"))
@@ -527,7 +529,7 @@
(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. -->
-
+
@@ -547,51 +549,51 @@
-
+
self:EnableMouse(true)
- BenillaPaperDollStatTooltip(self, "STRENGTH", "pet")
+ PaperDollStatTooltip("pet", "STRENGTH")GameTooltip:Hide()
-
-
+
+ self:EnableMouse(true)
- BenillaPaperDollStatTooltip(self, "AGILITY", "pet")
+ PaperDollStatTooltip("pet", "AGILITY")GameTooltip:Hide()
-
-
+
+ self:EnableMouse(true)
- BenillaPaperDollStatTooltip(self, "STAMINA", "pet")
+ PaperDollStatTooltip("pet", "STAMINA")GameTooltip:Hide()
-
-
+
+ self:EnableMouse(true)
- BenillaPaperDollStatTooltip(self, "INTELLECT", "pet")
+ PaperDollStatTooltip("pet", "INTELLECT")GameTooltip:Hide()
-
-
+
+ self:EnableMouse(true)
- BenillaPaperDollStatTooltip(self, "SPIRIT", "pet")
+ PaperDollStatTooltip("pet", "SPIRIT")GameTooltip:Hide()
-
-
+
+ self:EnableMouse(true)
- BenillaCharacterDamageFrame_OnEnter(self)
+ CharacterDamageFrame_OnEnter()GameTooltip:Hide()
-
-
+
+ self:EnableMouse(true)
@@ -641,8 +643,8 @@
GameTooltip:Hide()
-
-
+
+ self:EnableMouse(true)
@@ -662,72 +664,72 @@
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). -->
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
@@ -737,10 +739,10 @@
- BenillaPetPaperDollFrame_OnLoad(self)
- BenillaPetPaperDollFrame_OnEvent(self)
- BenillaPetPaperDollFrame_OnShow()
- BenillaPetPaperDollFrame_OnHide()
+ PetPaperDollFrame_OnLoad()
+ PetPaperDollFrame_OnEvent()
+ PetPaperDollFrame_OnShow()
+ PetPaperDollFrame_OnHide()
diff --git a/crates/benilla-app/assets/ui/QuestFrame.xml b/crates/benilla-app/assets/ui/QuestFrame.xml
index e77e29ad..425a83c5 100644
--- a/crates/benilla-app/assets/ui/QuestFrame.xml
+++ b/crates/benilla-app/assets/ui/QuestFrame.xml
@@ -8,13 +8,13 @@
real 1.12 `QuestFrame.xml`/`QuestFrameTemplates.xml` (scratchpad `ref-framexml/QuestFrame.*`;
`file:line` cites below are into those extracted files) structurally, not just its numbers:
- QUEST_GREETING → BenillaQuestGreetingPanel (SMSG_QUESTGIVER_QUEST_LIST): greeting text +
+ QUEST_GREETING → QuestGreetingPanel (SMSG_QUESTGIVER_QUEST_LIST): greeting text +
active/available quest title buttons (SelectActiveQuest/SelectAvailableQuest).
- QUEST_DETAIL → BenillaQuestDetailPanel (SMSG_QUESTGIVER_QUEST_DETAILS): title/description/
+ QUEST_DETAIL → QuestDetailPanel (SMSG_QUESTGIVER_QUEST_DETAILS): title/description/
objectives + rewards on offer, Accept/Decline.
- QUEST_PROGRESS → BenillaQuestProgressPanel (SMSG_QUESTGIVER_REQUEST_ITEMS): title, request
+ QUEST_PROGRESS → QuestProgressPanel (SMSG_QUESTGIVER_REQUEST_ITEMS): title, request
text, required items/money, Continue (enabled iff completable)/Goodbye.
- QUEST_COMPLETE → BenillaQuestRewardPanel (SMSG_QUESTGIVER_OFFER_REWARD): title, reward text,
+ QUEST_COMPLETE → QuestRewardPanel (SMSG_QUESTGIVER_OFFER_REWARD): title, reward text,
choose reward + fixed rewards + money, Complete Quest/Cancel.
QUEST_ITEM_UPDATE → in-place refresh of the visible panel's rows (async name landing).
QUEST_FINISHED → hide the window (turn-in result / interaction end).
@@ -25,7 +25,7 @@
constraint; the resolver now runs to a true change-driven fixpoint and a dedicated test
(`child_frame_layers_regions_render_after_the_fixpoint`, `benilla-ui`) pins that a shown child
frame's own Layers/buttons render correctly. So the ref's actual frame tree — one always-shown
- `BenillaQuestFrame` chrome (portrait/name/close) hosting four independent child panels, each
+ `QuestFrame` chrome (portrait/name/close) hosting four independent child panels, each
shown/hidden by its own event, each carrying its own art + a real ScrollFrame — is expressible
again, and is what ships here. The reward/choice item grids still use a RUNTIME `SetPoint` chain
(not a static XML relativeTo chain) for the SAME reason the quest log's rewards do
@@ -49,7 +49,7 @@
v1 dressing follow-up"); the budget only means the scroll RANGE is generous, never that content
is cut off — everything still resolves, scrolls, and clips correctly at whatever size it is.
- Item row visuals (`BenillaQuestItemTemplate`) are UNCHANGED from before this pass — they were
+ Item row visuals (`QuestItemTemplate`) are UNCHANGED from before this pass — they were
already a capture-verified port of the quest log's own item row shape (ring-slot art), not the
ref's plainer inline `QuestItemTemplate` (`QuestFrameTemplates.xml` l.85-166) — kept for visual
consistency across the quest windows, not a fidelity regression introduced here.
@@ -57,7 +57,7 @@
Docks the left UIPanel slot (`pushable=0`, `UiPanels.xml`) exactly as before — untouched by this
pass. The window-sound convention (decision 0090/0095/0096) and the Lua seam
(`benilla-ui` `script/quest.rs`'s `GetTitleText`/`AcceptQuest`/`GetQuestReward`/… — untouched by
- this pass, see that module's doc) live on the OUTER `BenillaQuestFrame` only, matching the ref's
+ this pass, see that module's doc) live on the OUTER `QuestFrame` only, matching the ref's
own `QuestFrame_OnShow`/`OnHide` (l.284-295) — a panel switch never re-plays a kit, only the
window opening/closing does.
@@ -180,40 +180,40 @@
-- Per-panel resize candidates: every element that can be the LOWEST visible thing.
BENILLA_QUEST_PANEL_WIDGETS = {
- ["BenillaQuestGreetingScroll"] = (function()
- local t = { "BenillaQuestGreetingText", "BenillaQuestActiveHeader", "BenillaQuestAvailableHeader" }
- for i = 1, BENILLA_QUEST_MAX_TITLES do table.insert(t, "BenillaQuestTitleButton" .. i) end
+ ["QuestGreetingScroll"] = (function()
+ local t = { "QuestGreetingText", "QuestActiveHeader", "QuestAvailableHeader" }
+ for i = 1, BENILLA_QUEST_MAX_TITLES do table.insert(t, "QuestTitleButton" .. i) end
return t
end)(),
- ["BenillaQuestDetailScroll"] = (function()
- local t = { "BenillaQuestDetailBody", "BenillaQuestDetailObjective", "BenillaQuestDetailRewardTitleText",
- "BenillaQuestDetailItemChooseText", "BenillaQuestDetailItemReceiveText" }
+ ["QuestDetailScroll"] = (function()
+ local t = { "QuestDetailBody", "QuestDetailObjective", "QuestDetailRewardTitleText",
+ "QuestDetailItemChooseText", "QuestDetailItemReceiveText" }
for i = 1, BENILLA_QUEST_MAX_ITEMS do
- table.insert(t, "BenillaQuestDetailChoice" .. i)
- table.insert(t, "BenillaQuestDetailReward" .. i)
+ table.insert(t, "QuestDetailChoice" .. i)
+ table.insert(t, "QuestDetailReward" .. i)
end
- for i = 1, 3 do table.insert(t, "BenillaQuestDetailMoneyCoin" .. i) end
+ for i = 1, 3 do table.insert(t, "QuestDetailMoneyCoin" .. i) end
return t
end)(),
- ["BenillaQuestProgressScroll"] = { "BenillaQuestProgressBody", "BenillaQuestProgressRequiredText",
- "BenillaQuestProgressRequiredMoneyText",
- "BenillaQuestProgressMoneyCoin1", "BenillaQuestProgressMoneyCoin2",
- "BenillaQuestProgressMoneyCoin3" },
- ["BenillaQuestRewardScroll"] = (function()
- local t = { "BenillaQuestRewardBody", "BenillaQuestRewardRewardTitleText",
- "BenillaQuestRewardItemChooseText", "BenillaQuestRewardItemReceiveText" }
+ ["QuestProgressScroll"] = { "QuestProgressBody", "QuestProgressRequiredText",
+ "QuestProgressRequiredMoneyText",
+ "QuestProgressMoneyCoin1", "QuestProgressMoneyCoin2",
+ "QuestProgressMoneyCoin3" },
+ ["QuestRewardScroll"] = (function()
+ local t = { "QuestRewardBody", "QuestRewardRewardTitleText",
+ "QuestRewardItemChooseText", "QuestRewardItemReceiveText" }
for i = 1, BENILLA_QUEST_MAX_ITEMS do
- table.insert(t, "BenillaQuestRewardChoice" .. i)
- table.insert(t, "BenillaQuestRewardReward" .. i)
+ table.insert(t, "QuestRewardChoice" .. i)
+ table.insert(t, "QuestRewardReward" .. i)
end
- for i = 1, 3 do table.insert(t, "BenillaQuestRewardMoneyCoin" .. i) end
+ for i = 1, 3 do table.insert(t, "QuestRewardMoneyCoin" .. i) end
return t
end)(),
}
BENILLA_QUEST_PANEL_SCROLLS = {
- greeting = "BenillaQuestGreetingScroll", detail = "BenillaQuestDetailScroll",
- progress = "BenillaQuestProgressScroll", reward = "BenillaQuestRewardScroll",
+ greeting = "QuestGreetingScroll", detail = "QuestDetailScroll",
+ progress = "QuestProgressScroll", reward = "QuestRewardScroll",
}
-- The convergence driver: while the window shows, keep re-measuring the ACTIVE panel
@@ -221,11 +221,11 @@
-- The greeting panel's title rows fit FIRST, so the panel sum below already sees their
-- final bottoms in this same pass.
function BenillaQuestFrame_OnUpdate()
- local frame = getglobal("BenillaQuestFrame")
+ local frame = getglobal("QuestFrame")
if not frame:IsVisible() or not frame.panel then return end
if frame.panel == "greeting" then
for i = 1, BENILLA_QUEST_MAX_TITLES do
- local button = getglobal("BenillaQuestTitleButton" .. i)
+ local button = getglobal("QuestTitleButton" .. i)
if button:IsShown() then
BenillaQuestTitleButton_Resize(button)
end
@@ -238,7 +238,8 @@
end
-- ── Greeting panel (ref QuestFrameGreetingPanel_OnShow, QuestFrame.lua:219-282) ──────────────
- function BenillaQuestTitleButton_OnClick(button)
+ function QuestTitleButton_OnClick()
+ local button = this
if button.isActive then SelectActiveQuest(button.titleIndex) else SelectAvailableQuest(button.titleIndex) end
PlaySound("igQuestListSelect") -- ref QuestTitleButton_OnClick, QuestFrame.lua:303
end
@@ -258,15 +259,15 @@
end
function BenillaQuestGreetingPanel_OnShow()
- getglobal("BenillaQuestDetailPanel"):Hide()
- getglobal("BenillaQuestProgressPanel"):Hide()
- getglobal("BenillaQuestRewardPanel"):Hide()
+ getglobal("QuestDetailPanel"):Hide()
+ getglobal("QuestProgressPanel"):Hide()
+ getglobal("QuestRewardPanel"):Hide()
- local greetingText = getglobal("BenillaQuestGreetingText")
+ local greetingText = getglobal("QuestGreetingText")
greetingText:SetText(GetGreetingText() or "")
- local activeHeader = getglobal("BenillaQuestActiveHeader")
- local availableHeader = getglobal("BenillaQuestAvailableHeader")
- local hbreak = getglobal("BenillaQuestGreetingBreak")
+ local activeHeader = getglobal("QuestActiveHeader")
+ local availableHeader = getglobal("QuestAvailableHeader")
+ local hbreak = getglobal("QuestGreetingBreak")
local numActive = math.min(GetNumActiveQuests(), BENILLA_QUEST_MAX_TITLES)
local numAvailable = math.min(GetNumAvailableQuests(), BENILLA_QUEST_MAX_TITLES - numActive)
@@ -277,7 +278,7 @@
activeHeader:SetText("Current Quests")
activeHeader:SetPoint("TOPLEFT", greetingText, "BOTTOMLEFT", 0, -10)
for i = 1, numActive do
- local button = getglobal("BenillaQuestTitleButton" .. i)
+ local button = getglobal("QuestTitleButton" .. i)
button.isActive = true
button.titleIndex = i
button:SetText(GetActiveTitle(i))
@@ -286,7 +287,7 @@
if i == 1 then
button:SetPoint("TOPLEFT", activeHeader, "BOTTOMLEFT", -10, -5)
else
- button:SetPoint("TOPLEFT", getglobal("BenillaQuestTitleButton" .. (i - 1)), "BOTTOMLEFT", 0, 0)
+ button:SetPoint("TOPLEFT", getglobal("QuestTitleButton" .. (i - 1)), "BOTTOMLEFT", 0, 0)
end
end
end
@@ -295,7 +296,7 @@
-- shows only when BOTH lists are non-empty (l.256-260).
if numActive > 0 and numAvailable > 0 then
hbreak:SetTexture("Interface\\QuestFrame\\UI-HorizontalBreak")
- hbreak:SetPoint("TOPLEFT", getglobal("BenillaQuestTitleButton" .. numActive), "BOTTOMLEFT", 22, -10)
+ hbreak:SetPoint("TOPLEFT", getglobal("QuestTitleButton" .. numActive), "BOTTOMLEFT", 22, -10)
else
hbreak:SetTexture(nil)
end
@@ -309,7 +310,7 @@
availableHeader:SetPoint("TOPLEFT", greetingText, "BOTTOMLEFT", 0, -10)
end
for i = numActive + 1, numActive + numAvailable do
- local button = getglobal("BenillaQuestTitleButton" .. i)
+ local button = getglobal("QuestTitleButton" .. i)
button.isActive = false
button.titleIndex = i - numActive
button:SetText(GetAvailableTitle(i - numActive))
@@ -318,35 +319,35 @@
if i == numActive + 1 then
button:SetPoint("TOPLEFT", availableHeader, "BOTTOMLEFT", -10, -5)
else
- button:SetPoint("TOPLEFT", getglobal("BenillaQuestTitleButton" .. (i - 1)), "BOTTOMLEFT", 0, 0)
+ button:SetPoint("TOPLEFT", getglobal("QuestTitleButton" .. (i - 1)), "BOTTOMLEFT", 0, 0)
end
end
end
for i = numActive + numAvailable + 1, BENILLA_QUEST_MAX_TITLES do
- getglobal("BenillaQuestTitleButton" .. i):Hide()
+ getglobal("QuestTitleButton" .. i):Hide()
end
- getglobal("BenillaQuestGreetingScroll"):SetVerticalScroll(0)
+ getglobal("QuestGreetingScroll"):SetVerticalScroll(0)
-- The action button inherits `hidden="true"` from `BenillaQuestPanelButtonTemplate`
-- (a virtual template — never itself drawn — declared hidden so a bare `inherits=` alone
-- doesn't imply "visible"); every panel must :Show() its own buttons explicitly, the same
-- way BenillaQuestItem_Set already does for item/title rows.
- getglobal("BenillaQuestGreetingGoodbyeButton"):Show()
+ getglobal("QuestGreetingGoodbyeButton"):Show()
if QUEST_FADING_DISABLE == "0" then -- ref l.223-226: instant text skips the fade-in
- UIFrameFadeIn(getglobal("BenillaQuestGreetingScrollChild"), QUESTINFO_FADE_IN)
+ UIFrameFadeIn(getglobal("QuestGreetingScrollChild"), QUESTINFO_FADE_IN)
end
end
function BenillaQuestGreetingGoodbyeButton_OnClick()
-- ref l.749-752: no DeclineQuest() call here — the greeting has no in-progress panel to
-- decline yet. The window's own OnHide (CloseQuest()) still ends the giver session.
- HideUIPanel(getglobal("BenillaQuestFrame"))
+ HideUIPanel(getglobal("QuestFrame"))
end
-- ── The shared reward/choice grid (ref's `QuestFrameItems_Update`, QuestFrame.lua:311-522) —
-- ported line-for-line, the SAME idiom `QuestLogFrame.xml`'s `BenillaQuestLogRewards_Update`
-- already proved: 2-per-row, odd index starts a new row under the row two back, even index
- -- sits beside the previous. `prefix` is "BenillaQuestDetail" or "BenillaQuestReward" — both
+ -- sits beside the previous. `prefix` is "QuestDetail" or "QuestReward" — both
-- panels read the SAME giver getters (`GetNumQuestChoices`/`GetQuestItemInfo`/`GetRewardMoney`
-- are giver-global, not panel-specific — `benilla-ui`'s `quest.rs`), only the widget names
-- differ (the ref's own two panels are likewise fed by the one shared function). ──────────────
@@ -425,27 +426,27 @@
-- ── Detail panel (ref QuestFrameDetailPanel_OnShow, QuestFrame.lua:524-552) ──────────────────
function BenillaQuestDetailPanel_OnShow()
- getglobal("BenillaQuestGreetingPanel"):Hide()
- getglobal("BenillaQuestProgressPanel"):Hide()
- getglobal("BenillaQuestRewardPanel"):Hide()
+ getglobal("QuestGreetingPanel"):Hide()
+ getglobal("QuestProgressPanel"):Hide()
+ getglobal("QuestRewardPanel"):Hide()
- getglobal("BenillaQuestDetailTitle"):SetText(GetTitleText() or "")
- getglobal("BenillaQuestDetailBody"):SetText(GetQuestText() or "")
- getglobal("BenillaQuestDetailObjectiveHeader"):SetText("Quest Objectives")
- getglobal("BenillaQuestDetailObjective"):SetText(GetObjectiveText() or "")
- BenillaQuestItems_Update("BenillaQuestDetail")
- getglobal("BenillaQuestDetailScroll"):SetVerticalScroll(0)
- getglobal("BenillaQuestAcceptButton"):Show()
- getglobal("BenillaQuestDeclineButton"):Show()
+ getglobal("QuestDetailTitle"):SetText(GetTitleText() or "")
+ getglobal("QuestDetailBody"):SetText(GetQuestText() or "")
+ getglobal("QuestDetailObjectiveHeader"):SetText("Quest Objectives")
+ getglobal("QuestDetailObjective"):SetText(GetObjectiveText() or "")
+ BenillaQuestItems_Update("QuestDetail")
+ getglobal("QuestDetailScroll"):SetVerticalScroll(0)
+ getglobal("QuestAcceptButton"):Show()
+ getglobal("QuestDeclineButton"):Show()
-- Arm the write-on (ref l.538-551): objectives/rewards invisible and Accept dead
-- until the description has finished writing itself onto the parchment.
- getglobal("BenillaQuestDetailTextAlphaFrame"):SetAlpha(0)
- getglobal("BenillaQuestAcceptButton"):Disable()
- local panel = getglobal("BenillaQuestDetailPanel")
+ getglobal("QuestDetailTextAlphaFrame"):SetAlpha(0)
+ getglobal("QuestAcceptButton"):Disable()
+ local panel = getglobal("QuestDetailPanel")
panel.fading = 1
panel.fadingProgress = 0
- getglobal("BenillaQuestDetailBody"):SetAlphaGradient(0, QUEST_DESCRIPTION_GRADIENT_LENGTH)
+ getglobal("QuestDetailBody"):SetAlphaGradient(0, QUEST_DESCRIPTION_GRADIENT_LENGTH)
-- Instant text (ref l.549-551): the write-on still arms, but the reveal edge starts
-- past any quest text's end — the first OnUpdate tick scratches the quill once, runs
-- the gradient off, and wakes everything on the spot.
@@ -465,30 +466,30 @@
if panel.fading then
panel.fadingProgress = panel.fadingProgress + (elapsed * QUEST_DESCRIPTION_GRADIENT_CPS)
PlaySound("WriteQuest")
- if not getglobal("BenillaQuestDetailBody"):SetAlphaGradient(panel.fadingProgress, QUEST_DESCRIPTION_GRADIENT_LENGTH) then
+ if not getglobal("QuestDetailBody"):SetAlphaGradient(panel.fadingProgress, QUEST_DESCRIPTION_GRADIENT_LENGTH) then
panel.fading = nil
if QUEST_FADING_DISABLE == "0" then
- UIFrameFadeIn(getglobal("BenillaQuestDetailTextAlphaFrame"), QUESTINFO_FADE_IN)
+ UIFrameFadeIn(getglobal("QuestDetailTextAlphaFrame"), QUESTINFO_FADE_IN)
else
-- instant: the block snaps on, no fade (ref l.561-564)
- getglobal("BenillaQuestDetailTextAlphaFrame"):SetAlpha(1)
+ getglobal("QuestDetailTextAlphaFrame"):SetAlpha(1)
end
- getglobal("BenillaQuestAcceptButton"):Enable()
+ getglobal("QuestAcceptButton"):Enable()
end
end
end
- function BenillaQuestDetailAcceptButton_OnClick()
+ function QuestDetailAcceptButton_OnClick()
AcceptQuest()
end
- function BenillaQuestDetailDeclineButton_OnClick()
+ function QuestDetailDeclineButton_OnClick()
-- vanilla's client-side decline sends no packet; hide immediately (no one-frame lag,
-- the same convention every NPC window's close button already uses — e.g.
-- BenillaMerchantCloseButton_OnClick).
DeclineQuest()
PlaySound("igQuestCancel") -- ref QuestDetailDeclineButton_OnClick, QuestFrame.lua:577
- HideUIPanel(getglobal("BenillaQuestFrame"))
+ HideUIPanel(getglobal("QuestFrame"))
end
-- ── Progress panel (ref QuestFrameProgressPanel_OnShow/Items_Update, QuestFrame.lua:143-217)
@@ -498,49 +499,49 @@
-- the resolver's fixpoint keeps correctly re-propagated as item1 moves — no Lua needed for
-- them at all. ─────────────────────────────────────────────────────────────────────────────
function BenillaQuestProgressPanel_OnShow()
- getglobal("BenillaQuestGreetingPanel"):Hide()
- getglobal("BenillaQuestDetailPanel"):Hide()
- getglobal("BenillaQuestRewardPanel"):Hide()
+ getglobal("QuestGreetingPanel"):Hide()
+ getglobal("QuestDetailPanel"):Hide()
+ getglobal("QuestRewardPanel"):Hide()
- getglobal("BenillaQuestProgressTitle"):SetText(GetTitleText() or "")
- getglobal("BenillaQuestProgressBody"):SetText(GetProgressText() or "")
+ getglobal("QuestProgressTitle"):SetText(GetTitleText() or "")
+ getglobal("QuestProgressBody"):SetText(GetProgressText() or "")
local numItems = math.min(GetNumQuestItems(), BENILLA_QUEST_MAX_ITEMS)
local money = GetQuestMoneyToGet()
- local reqText = getglobal("BenillaQuestProgressRequiredText")
- local moneyText = getglobal("BenillaQuestProgressRequiredMoneyText")
- local item1 = getglobal("BenillaQuestProgressRequired1")
+ local reqText = getglobal("QuestProgressRequiredText")
+ local moneyText = getglobal("QuestProgressRequiredMoneyText")
+ local item1 = getglobal("QuestProgressRequired1")
if numItems > 0 or (money and money > 0) then
reqText:SetText("Items Needed:")
if money and money > 0 then
moneyText:SetText("Required Money:")
- BenillaMoney_Set("BenillaQuestProgressMoney", money, true)
+ BenillaMoney_Set("QuestProgressMoney", money, true)
item1:SetPoint("TOPLEFT", moneyText, "BOTTOMLEFT", 0, -10)
else
moneyText:SetText("")
- BenillaMoney_Clear("BenillaQuestProgressMoney")
+ BenillaMoney_Clear("QuestProgressMoney")
item1:SetPoint("TOPLEFT", reqText, "BOTTOMLEFT", -3, -5)
end
else
reqText:SetText("")
moneyText:SetText("")
- BenillaMoney_Clear("BenillaQuestProgressMoney")
+ BenillaMoney_Clear("QuestProgressMoney")
end
for i = 1, BENILLA_QUEST_MAX_ITEMS do
if i <= numItems then
- BenillaQuestItem_Set(getglobal("BenillaQuestProgressRequired" .. i), "required", i)
+ BenillaQuestItem_Set(getglobal("QuestProgressRequired" .. i), "required", i)
else
- getglobal("BenillaQuestProgressRequired" .. i):Hide()
+ getglobal("QuestProgressRequired" .. i):Hide()
end
end
- local continueButton = getglobal("BenillaQuestProgressContinueButton")
+ local continueButton = getglobal("QuestProgressContinueButton")
if IsQuestCompletable() then continueButton:Enable() else continueButton:Disable() end
continueButton:Show()
- getglobal("BenillaQuestProgressGoodbyeButton"):Show()
- getglobal("BenillaQuestProgressScroll"):SetVerticalScroll(0)
+ getglobal("QuestProgressGoodbyeButton"):Show()
+ getglobal("QuestProgressScroll"):SetVerticalScroll(0)
if QUEST_FADING_DISABLE == "0" then -- ref l.159-162: instant text skips the fade-in
- UIFrameFadeIn(getglobal("BenillaQuestProgressScrollChild"), QUESTINFO_FADE_IN)
+ UIFrameFadeIn(getglobal("QuestProgressScrollChild"), QUESTINFO_FADE_IN)
end
end
@@ -551,27 +552,27 @@
function BenillaQuestProgressGoodbyeButton_OnClick()
DeclineQuest()
PlaySound("igQuestCancel") -- ref QuestGoodbyeButton_OnClick, QuestFrame.lua:112
- HideUIPanel(getglobal("BenillaQuestFrame"))
+ HideUIPanel(getglobal("QuestFrame"))
end
-- ── Reward panel (ref QuestFrameRewardPanel_OnShow + reward-item clicks, QuestFrame.lua:72-141)
function BenillaQuestRewardPanel_OnShow()
- getglobal("BenillaQuestGreetingPanel"):Hide()
- getglobal("BenillaQuestDetailPanel"):Hide()
- getglobal("BenillaQuestProgressPanel"):Hide()
+ getglobal("QuestGreetingPanel"):Hide()
+ getglobal("QuestDetailPanel"):Hide()
+ getglobal("QuestProgressPanel"):Hide()
- getglobal("BenillaQuestRewardTitle"):SetText(GetTitleText() or "")
- getglobal("BenillaQuestRewardBody"):SetText(GetRewardText() or "")
- getglobal("BenillaQuestFrame").itemChoice = 0
- getglobal("BenillaQuestRewardChoiceHighlight"):Hide()
- BenillaQuestItems_Update("BenillaQuestReward")
- getglobal("BenillaQuestRewardScroll"):SetVerticalScroll(0)
- getglobal("BenillaQuestRewardCompleteButton"):Show()
- getglobal("BenillaQuestRewardCancelButton"):Show()
+ getglobal("QuestRewardTitle"):SetText(GetTitleText() or "")
+ getglobal("QuestRewardBody"):SetText(GetRewardText() or "")
+ getglobal("QuestFrame").itemChoice = 0
+ getglobal("QuestRewardChoiceHighlight"):Hide()
+ BenillaQuestItems_Update("QuestReward")
+ getglobal("QuestRewardScroll"):SetVerticalScroll(0)
+ getglobal("QuestRewardCompleteButton"):Show()
+ getglobal("QuestRewardCancelButton"):Show()
-- ref l.85-88: the whole parchment content fades in — unless the text is instant.
if QUEST_FADING_DISABLE == "0" then
- getglobal("BenillaQuestRewardScrollChild"):SetAlpha(0)
- UIFrameFadeIn(getglobal("BenillaQuestRewardScrollChild"), QUESTINFO_FADE_IN)
+ getglobal("QuestRewardScrollChild"):SetAlpha(0)
+ UIFrameFadeIn(getglobal("QuestRewardScrollChild"), QUESTINFO_FADE_IN)
end
end
@@ -592,7 +593,8 @@
-- One benilla-only guard: `link` is nil until the ask-once item template lands (a state the
-- reference's synchronous item cache never has), and EditBox:Insert takes a string. The ref
-- already carries the twin of this guard inside DressUpItemLink (ref DressUpFrame.lua:11-13).
- function BenillaQuestItem_OnClick(button)
+ function QuestItem_OnClick()
+ local button = this
if IsControlKeyDown() then
if button.rewardType ~= "spell" then
DressUpItemLink(GetQuestItemLink(button.kind, button.index))
@@ -611,7 +613,8 @@
-- QuestItem_OnClick's verbatim (only the shift arm's spell guard differs — the ref drops it
-- here, l.133); the choice SELECT is the THIRD arm, so a ctrl- or shift-click must NOT also
-- pick the reward. That ordering is the whole point of the fork.
- function BenillaQuestRewardItem_OnClick(button)
+ function QuestRewardItem_OnClick()
+ local button = this
if IsControlKeyDown() then
if button.rewardType ~= "spell" then
DressUpItemLink(GetQuestItemLink(button.kind, button.index))
@@ -623,7 +626,7 @@
elseif button.kind == "choice" then
-- ref l.136's own `this.type == "choice"` test. The ref needs it because its reward
-- panel packs choices AND fixed rewards into ONE QuestRewardItem1..10 pool; ours
- -- keeps two pools and hangs this template only on BenillaQuestRewardChoice*, so the
+ -- keeps two pools and hangs this template only on QuestRewardChoice*, so the
-- test is belt-and-braces — kept because it is the ref's, and because it is what
-- makes the template safe to reuse if the pools are ever merged.
BenillaQuestRewardChoice_OnClick(button)
@@ -632,32 +635,33 @@
-- The third arm alone (ref l.136-139): only a REWARD-panel choice row is selectable — the
-- detail panel's rows and both panels' FIXED reward rows ride the plain
- -- `BenillaQuestItemTemplate`, whose OnClick has no select arm at all, matching the ref's
+ -- `QuestItemTemplate`, whose OnClick has no select arm at all, matching the ref's
-- separate QuestRewardItemTemplate vs QuestItemTemplate.
function BenillaQuestRewardChoice_OnClick(button)
- getglobal("BenillaQuestFrame").itemChoice = button.index
+ getglobal("QuestFrame").itemChoice = button.index
-- Ref QuestRewardItem_OnClick l.137-138 verbatim: the glow frame seats at (-8,7) on
-- the clicked row's TOPLEFT.
- local hl = getglobal("BenillaQuestRewardChoiceHighlight")
+ local hl = getglobal("QuestRewardChoiceHighlight")
hl:SetPoint("TOPLEFT", button, "TOPLEFT", -8, 7)
hl:Show()
end
- function BenillaQuestRewardCompleteButton_OnClick()
- local choice = getglobal("BenillaQuestFrame").itemChoice or 0
+ function QuestRewardCompleteButton_OnClick()
+ local choice = getglobal("QuestFrame").itemChoice or 0
if GetNumQuestChoices() > 0 and choice == 0 then return end -- must pick a choice first
GetQuestReward(math.max(choice - 1, 0)) -- 1-based row -> 0-based wire index
PlaySound("igQuestListComplete") -- ref QuestRewardCompleteButton_OnClick, QuestFrame.lua:101
end
- function BenillaQuestRewardCancelButton_OnClick()
+ function QuestRewardCancelButton_OnClick()
DeclineQuest()
PlaySound("igQuestCancel") -- ref QuestRewardCancelButton_OnClick, QuestFrame.lua:93
- HideUIPanel(getglobal("BenillaQuestFrame"))
+ HideUIPanel(getglobal("QuestFrame"))
end
-- ── Window lifecycle (ref QuestFrame_OnLoad/OnEvent, QuestFrame.lua:8-70) ────────────────────
- function BenillaQuestFrame_OnLoad(frame)
+ function QuestFrame_OnLoad()
+ local frame = this
-- The NPC portrait: the "npc" unit token → the questgiver's live model bake
-- (crates/benilla/src/portrait, InteractNpc) — the same binding MerchantFrame/GossipFrame use.
SetPortraitTexture(getglobal(frame:GetName() .. "Portrait"), "npc")
@@ -683,7 +687,8 @@
elseif panel == "reward" then BenillaQuestRewardPanel_OnShow() end
end
- function BenillaQuestFrame_OnEvent(frame)
+ function QuestFrame_OnEvent(event)
+ local frame = this
if event == "QUEST_FINISHED" then
HideUIPanel(frame)
frame.panel = nil
@@ -693,12 +698,12 @@
-- The in-place refresh also carries the NPC name (arg1) — in live play the ask-once
-- name often lands one frame AFTER the panel opened, arriving exactly here; dropping
-- it left the title bar blank (caught reviewing the ui-quest capture).
- if arg1 and arg1 ~= "" then getglobal("BenillaQuestNpcNameText"):SetText(arg1) end
+ if arg1 and arg1 ~= "" then getglobal("QuestNpcNameText"):SetText(arg1) end
if frame:IsVisible() and frame.panel then BenillaQuestFrame_Repaint(frame.panel) end
return
end
- getglobal("BenillaQuestNpcNameText"):SetText(arg1 or "")
+ getglobal("QuestNpcNameText"):SetText(arg1 or "")
if event == "QUEST_GREETING" then frame.panel = "greeting"
elseif event == "QUEST_DETAIL" then frame.panel = "detail"
elseif event == "QUEST_PROGRESS" then frame.panel = "progress"
@@ -709,8 +714,8 @@
-- (which hides the other three siblings + repaints itself), even on a same-panel refresh
-- (e.g. re-QUERY_QUEST on the same NPC while Detail is already open).
local names = {
- greeting = "BenillaQuestGreetingPanel", detail = "BenillaQuestDetailPanel",
- progress = "BenillaQuestProgressPanel", reward = "BenillaQuestRewardPanel",
+ greeting = "QuestGreetingPanel", detail = "QuestDetailPanel",
+ progress = "QuestProgressPanel", reward = "QuestRewardPanel",
}
local target = getglobal(names[frame.panel])
target:Hide()
@@ -719,7 +724,7 @@
function BenillaQuestCloseButton_OnClick()
CloseQuest()
- HideUIPanel(getglobal("BenillaQuestFrame"))
+ HideUIPanel(getglobal("QuestFrame"))
end
]]>
@@ -732,7 +737,7 @@
`BenillaQuestLogItemTemplate` (per-window test-isolation duplication, that template's own
comment).
-->
-
+
@@ -757,7 +762,7 @@
@@ -777,17 +782,17 @@
- BenillaQuestItem_OnClick(self)
+ QuestItem_OnClick()
-
- BenillaQuestRewardItem_OnClick(self)
+
+ QuestRewardItem_OnClick()
+ `QuestFrame`). -->
@@ -860,7 +865,7 @@
-
+
-
+
-
+
@@ -897,7 +902,7 @@
GossipFrame.xml's own close button uses, since quest and gossip are the same "list"
window family in the real client) on the minimize-button art this engine's windows all
substitute for the real UIPanelCloseButton (that art rides the tooltip/dialog arc). -->
-
+
@@ -907,7 +912,7 @@
-
+
-
+
-
+
@@ -990,7 +995,7 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestGreetingScroll"), -BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestGreetingScroll"), -BENILLA_QUEST_SCROLL_STEP)
@@ -999,13 +1004,13 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestGreetingScroll"), BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestGreetingScroll"), BENILLA_QUEST_SCROLL_STEP)
-
+
-
+
@@ -1020,19 +1025,19 @@
-
+
-
+
- BenillaQuestDetailAcceptButton_OnClick()
+ QuestDetailAcceptButton_OnClick()
-
+
- BenillaQuestDetailDeclineButton_OnClick()
+ QuestDetailDeclineButton_OnClick()
-
+
@@ -1042,18 +1047,18 @@
BenillaQuestScroll_SyncBar(this)
-
+
-
+
-
+
-
+
@@ -1064,50 +1069,50 @@
the child item buttons/coins (wow-re propagation.md,
CORRECTED), so the whole block hides while the text writes
and fades in as one. -->
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
@@ -1117,9 +1122,9 @@
-
+
-
+
@@ -1128,7 +1133,7 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestDetailScroll"), -BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestDetailScroll"), -BENILLA_QUEST_SCROLL_STEP)
@@ -1137,13 +1142,13 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestDetailScroll"), BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestDetailScroll"), BENILLA_QUEST_SCROLL_STEP)
-
+
-
+
@@ -1161,19 +1166,19 @@
-
+
-
+ BenillaQuestProgressContinueButton_OnClick()
-
+ BenillaQuestProgressGoodbyeButton_OnClick()
-
+
@@ -1183,42 +1188,42 @@
BenillaQuestScroll_SyncBar(this)
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -1226,9 +1231,9 @@
-
+
-
+
@@ -1237,7 +1242,7 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestProgressScroll"), -BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestProgressScroll"), -BENILLA_QUEST_SCROLL_STEP)
@@ -1246,13 +1251,13 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestProgressScroll"), BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestProgressScroll"), BENILLA_QUEST_SCROLL_STEP)
-
+
-
+
@@ -1267,19 +1272,19 @@
-
+
-
+
- BenillaQuestRewardCompleteButton_OnClick()
+ QuestRewardCompleteButton_OnClick()
-
+
- BenillaQuestRewardCancelButton_OnClick()
+ QuestRewardCancelButton_OnClick()
-
+
@@ -1289,56 +1294,56 @@
BenillaQuestScroll_SyncBar(this)
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
+
@@ -1355,9 +1360,9 @@
-
+
-
+
@@ -1366,7 +1371,7 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestRewardScroll"), -BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestRewardScroll"), -BENILLA_QUEST_SCROLL_STEP)
@@ -1375,13 +1380,13 @@
- BenillaQuestScroll_Step(getglobal("BenillaQuestRewardScroll"), BENILLA_QUEST_SCROLL_STEP)
+ BenillaQuestScroll_Step(getglobal("QuestRewardScroll"), BENILLA_QUEST_SCROLL_STEP)
-
+
-
+
@@ -1396,8 +1401,8 @@
- BenillaQuestFrame_OnLoad(self)
- BenillaQuestFrame_OnEvent(self)
+ QuestFrame_OnLoad()
+ QuestFrame_OnEvent(event)BenillaQuestFrame_OnUpdate()PlaySound("igQuestListOpen")
- getglobal("BenillaQuestGreetingPanel"):Hide()
- getglobal("BenillaQuestDetailPanel"):Hide()
- getglobal("BenillaQuestProgressPanel"):Hide()
- getglobal("BenillaQuestRewardPanel"):Hide()
+ getglobal("QuestGreetingPanel"):Hide()
+ getglobal("QuestDetailPanel"):Hide()
+ getglobal("QuestProgressPanel"):Hide()
+ getglobal("QuestRewardPanel"):Hide()
CloseQuest()
PlaySound("igQuestListClose")
diff --git a/crates/benilla-app/assets/ui/QuestLogFrame.xml b/crates/benilla-app/assets/ui/QuestLogFrame.xml
index 3754e6d4..ae545df9 100644
--- a/crates/benilla-app/assets/ui/QuestLogFrame.xml
+++ b/crates/benilla-app/assets/ui/QuestLogFrame.xml
@@ -4,7 +4,7 @@
`QuestFrame.xml`'s questgiver window, dressed from the real 1.12 `QuestLogFrame.xml`/
`QuestLogFrame.lua` (scratchpad `ref-framexml/QUESTLOG-PIN.md`; section cites below are into
that pin, `file:line` cites are into the pin's own extracted `QuestLogFrame.xml`/`.lua`). One
- `BenillaQuestLogFrame` carries BOTH the list page and the detail page — FLAT layout (decision
+ `QuestLogFrame` carries BOTH the list page and the detail page — FLAT layout (decision
0088 §2, quoted in `QuestFrame.xml`'s header): a child frame shown at runtime does not render
its own `` FontStrings in this engine, so every FontString/Button here lives directly
on the one always-visible window, never on a nested sub-frame. `enableMouse`/`toplevel`/
@@ -13,7 +13,7 @@
construction regardless (`widget/mod.rs`'s ctor comment).
Docks the left UIPanel slot exactly like Gossip/Merchant/Quest (`UiPanels.xml`
- `UIPanelWindows["BenillaQuestLogFrame"] = { area = "left", pushable = 0 }`, pin §8's
+ `UIPanelWindows["QuestLogFrame"] = { area = "left", pushable = 0 }`, pin §8's
`UIParent.lua l.25` quote) — so it competes for/replaces those windows, never stacks with
them. UNLIKE those windows this is not an NPC session: no `CloseX()` on hide, no range guard;
it is durable player state the app can push at any time (`benilla-ui`'s `quest_log.rs` module
@@ -28,7 +28,7 @@
this window and scrolls. (A bare `` is not a mouse HIT target — only
Button/CheckButton/EditBox/ScrollingMessageFrame are, `widget/mod.rs`'s ctor comment, and
`enableMouse` is a no-op stub — but it is still a valid bubble ANCESTOR: bubbling checks for the
- handler, not mouse-enablement.) The `BenillaQuestLogWheelCatcher` BUTTON, declared FIRST so every
+ handler, not mouse-enablement.) The `QuestLogWheelCatcher` BUTTON, declared FIRST so every
real row out-ranks it in hit-testing (`order::hit_test` walks the draw-order-descending list),
now only gives the list's EMPTY tail a hit-target whose spin also bubbles here.
@@ -36,7 +36,7 @@
- The LIST page still has no real ScrollFrame — `questLogOffset`'s faux-scroll stands in
(this comment's own "Faux-scroll" paragraph above); real real-estate accounting there
would need a genuine header/collapse model this v1 doesn't ship. The DETAIL page now
- DOES sit on a real ScrollFrame (`BenillaQuestLogDetailScroll`, decision 0112's mechanism)
+ DOES sit on a real ScrollFrame (`QuestLogDetailScroll`, decision 0112's mechanism)
— a quest with many objectives/a long description/many rewards scrolls and clips at the
pane's own edge instead of running past the window's bottom (pin §3's doNotScroll
asymmetry is implemented — see `BenillaQuestLogDetail_Update`'s own comment).
@@ -60,7 +60,7 @@
note once named as the missing engine slice (`IsShiftKeyDown` + the app's per-frame
mirror) landed with the cursor arc; the interim right-click substitute is retired.
- **The two ref watch errors (`QUEST_WATCH_NO_OBJECTIVES`/`QUEST_WATCH_TOO_MANY`,
- `UIErrorsFrame:AddMessage`) have no seam to reach `BenillaErrorsFrame` from Lua**: that
+ `UIErrorsFrame:AddMessage`) have no seam to reach `UIErrorsFrame` from Lua**: that
frame only reacts to the `UI_ERROR_MESSAGE` *event*, and firing an event is a method on
`UiScript` (`fire_event`) — there is no Lua-callable "fire this event"/"post this error"
global anywhere in this engine (grepped; every `UI_ERROR_MESSAGE` fire in this codebase is
@@ -218,7 +218,7 @@
-- silently unwatch a re-added MANUAL watch.
if title then BENILLA_QUEST_WATCH_TIMERS[title] = nil end
RemoveQuestWatch(index)
- BenillaQuestWatch_Update()
+ QuestWatch_Update()
elseif GetNumQuestLeaderBoards(index) == 0 then
-- ref QUEST_WATCH_NO_OBJECTIVES, red, straight onto the errors frame
-- (the ref calls UIErrorsFrame:AddMessage the same way).
@@ -231,7 +231,7 @@
-- entry the auto-watch may have armed for it.
if title then BENILLA_QUEST_WATCH_TIMERS[title] = nil end
AddQuestWatch(index)
- BenillaQuestWatch_Update()
+ QuestWatch_Update()
end
end
-- pin §5: every click (shifted/right-clicked or not) ends in select + a full repaint.
@@ -241,8 +241,8 @@
-- The fold-all tab (ref QuestLogCollapseAllButton_OnClick, QuestLogFrame.lua:554-563):
-- index 0 = every header; the tab's own +/- icon tracks the all-state.
- function BenillaQuestLogCollapseAllButton_OnClick()
- local b = getglobal("BenillaQuestLogCollapseAllButton")
+ function QuestLogCollapseAllButton_OnClick()
+ local b = getglobal("QuestLogCollapseAllButton")
if b.allCollapsed then
b.allCollapsed = nil
b:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
@@ -256,7 +256,7 @@
-- pin §2's QuestLog_SetFirstValidSelection/QuestLog_GetFirstSelectableQuest
-- (QuestLogFrame.lua:293-296, :565-582): if nothing is selected, pick the first non-header row.
- function BenillaQuestLog_SetFirstValidSelection()
+ function QuestLog_SetFirstValidSelection()
if GetQuestLogSelection() ~= 0 then return end
local numEntries = GetNumQuestLogEntries()
for i = 1, numEntries do
@@ -275,10 +275,10 @@
-- pill's middle piece then shrinks to the text (ref sizes it every update). The >10
-- guard skips the first-ever paint, where the host hasn't measured the string yet
-- (GetWidth 0 would collapse the pill; the next update — and every one after — sizes it).
- getglobal("BenillaQuestLogCount"):SetText(string.format("Quests: |cffffffff%d/%d|r", numQuests, BENILLA_QUESTLOG_MAX_DISPLAY))
- local countWidth = getglobal("BenillaQuestLogCount"):GetWidth()
+ getglobal("QuestLogCount"):SetText(string.format("Quests: |cffffffff%d/%d|r", numQuests, BENILLA_QUESTLOG_MAX_DISPLAY))
+ local countWidth = getglobal("QuestLogCount"):GetWidth()
if countWidth and countWidth > 10 then
- getglobal("BenillaQuestLogCountMiddle"):SetWidth(countWidth)
+ getglobal("QuestLogCountMiddle"):SetWidth(countWidth)
end
-- v1 empty state (pin §2 drives a whole separate EmptyQuestLogFrame; simplified here to
@@ -288,12 +288,12 @@
-- convention predates region Show/Hide (`region.rs` has the real pair now, and the
-- loader honours hidden= on regions); an empty string draws nothing, so it is equivalent
-- either way and the idiom stays for consistency with the sibling windows.
- local abandonButton = getglobal("BenillaQuestLogAbandonButton")
+ local abandonButton = getglobal("QuestLogAbandonButton")
if numEntries == 0 then
- getglobal("BenillaQuestLogEmptyText"):SetText("Your quest log is empty.")
+ getglobal("QuestLogEmptyText"):SetText("Your quest log is empty.")
abandonButton:Disable()
else
- getglobal("BenillaQuestLogEmptyText"):SetText("")
+ getglobal("QuestLogEmptyText"):SetText("")
abandonButton:Enable()
end
@@ -306,7 +306,7 @@
local selection = GetQuestLogSelection()
for i = 1, BENILLA_QUESTLOG_ROWS do
- local button = getglobal("BenillaQuestLogTitle" .. i)
+ local button = getglobal("QuestLogTitle" .. i)
local index = i + questLogOffset
if index <= numEntries then
local title, level, tag, isHeader, isCollapsed, isComplete = GetQuestLogTitle(index)
@@ -366,17 +366,17 @@
-- scrolls back to it. Ref l.337-339 verbatim: the glow rides at (5,0) on the row and is
-- vertex-colored to the row's own difficulty color (that green bar in the ref shot IS
-- this vertex color on the white ADD gradient).
- local hl = getglobal("BenillaQuestLogSelectHighlight")
+ local hl = getglobal("QuestLogSelectHighlight")
if selection > questLogOffset and selection <= questLogOffset + BENILLA_QUESTLOG_ROWS
and selection <= numEntries then
- local selButton = getglobal("BenillaQuestLogTitle" .. (selection - questLogOffset))
+ local selButton = getglobal("QuestLogTitle" .. (selection - questLogOffset))
local _, selLevel, _, selIsHeader = GetQuestLogTitle(selection)
if selIsHeader then
local c = QuestDifficultyColor["header"]
- getglobal("BenillaQuestLogSkillHighlight"):SetVertexColor(c.r, c.g, c.b)
+ getglobal("QuestLogSkillHighlight"):SetVertexColor(c.r, c.g, c.b)
else
local c = GetDifficultyColor(selLevel)
- getglobal("BenillaQuestLogSkillHighlight"):SetVertexColor(c.r, c.g, c.b)
+ getglobal("QuestLogSkillHighlight"):SetVertexColor(c.r, c.g, c.b)
end
hl:SetPoint("TOPLEFT", selButton, "TOPLEFT", 5, 0)
hl:Show()
@@ -399,9 +399,9 @@
-- ── The detail page ──────────────────────────────────────────────────────────────────────
-- The detail pane's real ScrollFrame (decision 0112's mechanism, landed this session — this
-- window used to be a fixed layout that simply clipped past the window's bottom edge, this
- -- file's header comment). `BenillaQuestLogDetailScroll`'s ScrollChild is
- -- `BenillaQuestLogDetailChild`: every FontString/Button below that used to live flat on
- -- `BenillaQuestLogFrame` now lives on that child instead (the 0088 §2 flat-frame constraint
+ -- file's header comment). `QuestLogDetailScroll`'s ScrollChild is
+ -- `QuestLogDetailChild`: every FontString/Button below that used to live flat on
+ -- `QuestLogFrame` now lives on that child instead (the 0088 §2 flat-frame constraint
-- this file's header comment quoted DIED with the 0112 resolver fixpoint — child frames
-- render their own `` regions again), so its subtree clips at the ScrollFrame's own
-- rect and scrolls independent of the rest of the window.
@@ -447,9 +447,9 @@
function BenillaQuestLogDetail_ResizeChild(objAnchorIndex, rewardSectionHeight)
detailObjAnchorIndex = objAnchorIndex
detailRewardHeight = rewardSectionHeight
- local title = getglobal("BenillaQuestLogQuestTitle")
- local objectivesText = getglobal("BenillaQuestLogObjectivesText")
- local description = getglobal("BenillaQuestLogDescription")
+ local title = getglobal("QuestLogQuestTitle")
+ local objectivesText = getglobal("QuestLogObjectivesText")
+ local description = getglobal("QuestLogDescription")
local objSpan
if objAnchorIndex > 0 then
@@ -483,12 +483,12 @@
if height ~= detailLastAppliedHeight then
detailLastAppliedHeight = height
- getglobal("BenillaQuestLogDetailChild"):SetHeight(height)
- getglobal("BenillaQuestLogDetailScroll"):UpdateScrollChildRect()
+ getglobal("QuestLogDetailChild"):SetHeight(height)
+ getglobal("QuestLogDetailScroll"):UpdateScrollChildRect()
end
end
- -- The re-check `BenillaQuestLogDetailScroll`'s own `` calls every frame the log
+ -- The re-check `QuestLogDetailScroll`'s own `` calls every frame the log
-- window is open (`UiScript::tick`'s OnUpdate gate gates it on effective-visibility, so this
-- costs nothing while the window is closed): re-sums the SAME inputs the last full populate
-- computed (`BenillaQuestLogDetail_ResizeChild`'s own header comment on why a single populate
@@ -505,14 +505,14 @@
-- SecondsToTime's output only changes once a second, so anything faster is a re-measure of
-- an identical string. The flag/accumulator pair is the ref's own (its two fields on
-- QuestLogFrame; ours are file globals because our detail state already lives that way).
- function BenillaQuestLog_OnUpdate(elapsed)
+ function QuestLog_OnUpdate(elapsed)
if not BenillaQuestLogHasTimer then
return
end
BenillaQuestLogTimePassed = (BenillaQuestLogTimePassed or 0) + (elapsed or 0)
if BenillaQuestLogTimePassed > BENILLA_QUESTLOG_UPDATE_DELAY then
local left = GetQuestLogTimeLeft()
- getglobal("BenillaQuestLogTimerText"):SetText(
+ getglobal("QuestLogTimerText"):SetText(
BENILLA_TIME_REMAINING .. " " .. SecondsToTime(left or 0))
BenillaQuestLogTimePassed = 0
end
@@ -522,7 +522,7 @@
-- buttons share this one path; SetVerticalScroll's own clamp (benilla-ui's `scrollframe.rs`)
-- does the range-bounding, this function just supplies the signed step.
function BenillaQuestLogDetailScroll_Step(delta)
- local sf = getglobal("BenillaQuestLogDetailScroll")
+ local sf = getglobal("QuestLogDetailScroll")
sf:SetVerticalScroll(sf:GetVerticalScroll() + delta)
end
@@ -533,52 +533,52 @@
-- either arrow at its own end, mirroring the ref's identical two Enable/Disable calls off a
-- real Slider's min/max (there's no Slider here — this window's own comment on why).
function BenillaQuestLogDetailScroll_SyncBar()
- local sf = getglobal("BenillaQuestLogDetailScroll")
+ local sf = getglobal("QuestLogDetailScroll")
local value = sf:GetVerticalScroll()
local range = sf:GetVerticalScrollRange()
- local bar = getglobal("BenillaQuestLogDetailScrollBar")
+ local bar = getglobal("QuestLogDetailScrollBar")
if range <= 0 then
bar:Hide()
return
end
bar:Show()
if value > 0 then
- getglobal("BenillaQuestLogDetailScrollUp"):Enable()
+ getglobal("QuestLogDetailScrollUp"):Enable()
else
- getglobal("BenillaQuestLogDetailScrollUp"):Disable()
+ getglobal("QuestLogDetailScrollUp"):Disable()
end
if value < range then
- getglobal("BenillaQuestLogDetailScrollDown"):Enable()
+ getglobal("QuestLogDetailScrollDown"):Enable()
else
- getglobal("BenillaQuestLogDetailScrollDown"):Disable()
+ getglobal("QuestLogDetailScrollDown"):Disable()
end
local travel = BENILLA_QUESTLOG_SCROLL_TRACK_H - BENILLA_QUESTLOG_SCROLL_THUMB_H
local frac = value / range
- getglobal("BenillaQuestLogDetailScrollThumb"):SetPoint(
- "TOPLEFT", getglobal("BenillaQuestLogDetailScrollTrack"), "TOPLEFT", 0, -(frac * travel))
+ getglobal("QuestLogDetailScrollThumb"):SetPoint(
+ "TOPLEFT", getglobal("QuestLogDetailScrollTrack"), "TOPLEFT", 0, -(frac * travel))
end
function BenillaQuestLogDetail_Clear()
- getglobal("BenillaQuestLogQuestTitle"):SetText("")
- getglobal("BenillaQuestLogObjectivesText"):SetText("")
+ getglobal("QuestLogQuestTitle"):SetText("")
+ getglobal("QuestLogObjectivesText"):SetText("")
-- The ref clears hasTimer on the empty-log path too (QuestLog_Update, :108) — without
-- it the per-tick repaint keeps writing a countdown into a pane with no selection, and
-- the child height keeps paying for a row that isn't there.
BenillaQuestLogHasTimer = nil
- getglobal("BenillaQuestLogTimerText"):SetText("")
- getglobal("BenillaQuestLogDescriptionTitle"):SetText("")
- getglobal("BenillaQuestLogDescription"):SetText("")
+ getglobal("QuestLogTimerText"):SetText("")
+ getglobal("QuestLogDescriptionTitle"):SetText("")
+ getglobal("QuestLogDescription"):SetText("")
for i = 1, BENILLA_QUESTLOG_MAX_OBJECTIVES do
- getglobal("BenillaQuestLogObjective" .. i):SetText("")
+ getglobal("QuestLogObjective" .. i):SetText("")
end
- getglobal("BenillaQuestLogRewardTitleText"):SetText("")
- getglobal("BenillaQuestLogItemChooseText"):SetText("")
- getglobal("BenillaQuestLogItemReceiveText"):SetText("")
+ getglobal("QuestLogRewardTitleText"):SetText("")
+ getglobal("QuestLogItemChooseText"):SetText("")
+ getglobal("QuestLogItemReceiveText"):SetText("")
for i = 1, BENILLA_QUESTLOG_MAX_ITEMS do
- getglobal("BenillaQuestLogChoice" .. i):Hide()
- getglobal("BenillaQuestLogReward" .. i):Hide()
+ getglobal("QuestLogChoice" .. i):Hide()
+ getglobal("QuestLogReward" .. i):Hide()
end
- BenillaMoney_Clear("BenillaQuestLogMoney")
+ BenillaMoney_Clear("QuestLogMoney")
-- Nothing selected: the header-only span (BenillaQuestLogRewards_Update's own
-- haveAnyReward==false case, 18px) is the right resize input here too.
BenillaQuestLogDetail_ResizeChild(0, 18)
@@ -627,7 +627,8 @@
-- the ask-once item template lands, a state the reference's synchronous item cache never
-- has, and EditBox:Insert takes a string (DressUpItemLink carries the same guard itself —
-- ref DressUpFrame.lua:11-13).
- function BenillaQuestLogRewardItem_OnClick(button)
+ function QuestLogRewardItem_OnClick()
+ local button = this
if IsControlKeyDown() then
if button.rewardType ~= "spell" then
DressUpItemLink(GetQuestLogItemLink(button.kind, button.index))
@@ -659,7 +660,7 @@
-- pin §3 / QuestFrame.lua:343-349: QuestLogRewardTitleText shown only if
-- numRewards+numChoices+money > 0. Text is toggled rather than Show/Hide — see
-- BenillaQuestLogList_Update's comment on this file's blank-text convention.
- local header = getglobal("BenillaQuestLogRewardTitleText")
+ local header = getglobal("QuestLogRewardTitleText")
local haveAnyReward = numChoices > 0 or numRewards > 0 or (money and money > 0)
if haveAnyReward then
header:SetText("Rewards")
@@ -671,7 +672,7 @@
-- anchors below the header text (-3,-5); each following ODD index anchors BOTTOMLEFT of
-- the row two back (0,-2) — starts a new row; each EVEN index anchors TOPRIGHT of the
-- previous (1,0) — sits beside it. ──
- local chooseText = getglobal("BenillaQuestLogItemChooseText")
+ local chooseText = getglobal("QuestLogItemChooseText")
local lastChoice = math.min(numChoices, BENILLA_QUESTLOG_MAX_ITEMS)
if numChoices > 0 then
chooseText:SetText("You may choose one of these rewards:")
@@ -679,15 +680,15 @@
chooseText:SetText("")
end
for i = 1, BENILLA_QUESTLOG_MAX_ITEMS do
- local choice = getglobal("BenillaQuestLogChoice" .. i)
+ local choice = getglobal("QuestLogChoice" .. i)
if i <= lastChoice then
BenillaQuestLogItem_Set(choice, GetQuestLogChoiceInfo, i, "choice")
if i == 1 then
choice:SetPoint("TOPLEFT", chooseText, "BOTTOMLEFT", -3, -5)
elseif i % 2 == 1 then
- choice:SetPoint("TOPLEFT", getglobal("BenillaQuestLogChoice" .. (i - 2)), "BOTTOMLEFT", 0, -2)
+ choice:SetPoint("TOPLEFT", getglobal("QuestLogChoice" .. (i - 2)), "BOTTOMLEFT", 0, -2)
else
- choice:SetPoint("TOPLEFT", getglobal("BenillaQuestLogChoice" .. (i - 1)), "TOPRIGHT", 1, 0)
+ choice:SetPoint("TOPLEFT", getglobal("QuestLogChoice" .. (i - 1)), "TOPRIGHT", 1, 0)
end
else
choice:Hide()
@@ -698,14 +699,14 @@
-- whenever there's a fixed reward OR money, even with zero fixed-reward rows (money-only).
-- Anchored below the LAST CHOICE ROW'S LEFT COLUMN (the shorter of the two columns) when
-- choices exist, else below the Rewards header directly. ──
- local receiveText = getglobal("BenillaQuestLogItemReceiveText")
+ local receiveText = getglobal("QuestLogItemReceiveText")
local haveFixed = numRewards > 0 or (money and money > 0)
if haveFixed then
if numChoices > 0 then
receiveText:SetText("You will also receive:")
local anchorIndex = lastChoice
if anchorIndex % 2 == 0 then anchorIndex = anchorIndex - 1 end
- receiveText:SetPoint("TOPLEFT", getglobal("BenillaQuestLogChoice" .. anchorIndex), "BOTTOMLEFT", 3, -5)
+ receiveText:SetPoint("TOPLEFT", getglobal("QuestLogChoice" .. anchorIndex), "BOTTOMLEFT", 3, -5)
else
receiveText:SetText("You will receive:")
receiveText:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 3, -5)
@@ -718,15 +719,15 @@
-- the same odd/even 2-per-row pattern as the choice rows above, chained off each other. ──
local lastReward = math.min(numRewards, BENILLA_QUESTLOG_MAX_ITEMS)
for i = 1, BENILLA_QUESTLOG_MAX_ITEMS do
- local reward = getglobal("BenillaQuestLogReward" .. i)
+ local reward = getglobal("QuestLogReward" .. i)
if i <= lastReward then
BenillaQuestLogItem_Set(reward, GetQuestLogRewardInfo, i, "reward")
if i == 1 then
reward:SetPoint("TOPLEFT", receiveText, "BOTTOMLEFT", -3, -5)
elseif i % 2 == 1 then
- reward:SetPoint("TOPLEFT", getglobal("BenillaQuestLogReward" .. (i - 2)), "BOTTOMLEFT", 0, -2)
+ reward:SetPoint("TOPLEFT", getglobal("QuestLogReward" .. (i - 2)), "BOTTOMLEFT", 0, -2)
else
- reward:SetPoint("TOPLEFT", getglobal("BenillaQuestLogReward" .. (i - 1)), "TOPRIGHT", 1, 0)
+ reward:SetPoint("TOPLEFT", getglobal("QuestLogReward" .. (i - 1)), "TOPRIGHT", 1, 0)
end
else
reward:Hide()
@@ -737,9 +738,9 @@
-- QuestLogFrame.xml:896-903) tracks receiveText's runtime SetPoint above via the resolver's
-- fixpoint re-propagation; only the coin fill/clear is Lua's job here.
if money and money > 0 then
- BenillaMoney_Set("BenillaQuestLogMoney", money, true)
+ BenillaMoney_Set("QuestLogMoney", money, true)
else
- BenillaMoney_Clear("BenillaQuestLogMoney")
+ BenillaMoney_Clear("QuestLogMoney")
end
-- The section's total height, mirroring the branches above term-for-term (a money-only
@@ -778,7 +779,7 @@
if selection == 0 or selection > GetNumQuestLogEntries() then
BenillaQuestLogDetail_Clear()
if not doNotScroll then
- getglobal("BenillaQuestLogDetailScroll"):SetVerticalScroll(0)
+ getglobal("QuestLogDetailScroll"):SetVerticalScroll(0)
end
return
end
@@ -789,22 +790,22 @@
if IsCurrentQuestFailed() then
title = (title or "") .. " - (Failed)"
end
- getglobal("BenillaQuestLogQuestTitle"):SetText(title or "")
+ getglobal("QuestLogQuestTitle"):SetText(title or "")
-- pin §3: description is GetQuestLogQuestText()'s FIRST return, the objectives FLAVOR
-- paragraph (not the leaderboard lines) is the second.
local description, objectivesText = GetQuestLogQuestText()
- getglobal("BenillaQuestLogObjectivesText"):SetText(objectivesText or "")
+ getglobal("QuestLogObjectivesText"):SetText(objectivesText or "")
-- pin §3, QuestLog_UpdateQuestDetails (QuestLogFrame.lua:364-374) verbatim, minus the
-- Show/Hide our region model has no verb for: a timed quest gets the "Time Remaining:"
-- row and pushes Objective1 below it; an untimed one blanks the row (our stand-in for
-- the ref's :Hide()) and anchors Objective1 straight onto ObjectivesText, so the row
-- costs an untimed quest no space at all. `hasTimer` is the ref's own flag and drives
- -- both the per-tick repaint (BenillaQuestLog_OnUpdate) and the child-height sum.
+ -- both the per-tick repaint (QuestLog_OnUpdate) and the child-height sum.
local questTimer = GetQuestLogTimeLeft()
- local timerText = getglobal("BenillaQuestLogTimerText")
- local objective1 = getglobal("BenillaQuestLogObjective1")
+ local timerText = getglobal("QuestLogTimerText")
+ local objective1 = getglobal("QuestLogObjective1")
if questTimer then
BenillaQuestLogHasTimer = 1
BenillaQuestLogTimePassed = 0
@@ -813,18 +814,18 @@
else
BenillaQuestLogHasTimer = nil
timerText:SetText("")
- objective1:SetPoint("TOPLEFT", getglobal("BenillaQuestLogObjectivesText"), "BOTTOMLEFT", 0, -10)
+ objective1:SetPoint("TOPLEFT", getglobal("QuestLogObjectivesText"), "BOTTOMLEFT", 0, -10)
end
- getglobal("BenillaQuestLogDescription"):SetText(description or "")
+ getglobal("QuestLogDescription"):SetText(description or "")
-- Lua-managed header (ref text=QUEST_DESCRIPTION is static since the ref always has a real
-- selection whenever this pane is even visible; our flat window keeps the same FontString
-- alive with no selection, so THIS function — not a static `text=` — owns "Description",
-- blanked by BenillaQuestLogDetail_Clear on the empty-log path).
- getglobal("BenillaQuestLogDescriptionTitle"):SetText("Description")
+ getglobal("QuestLogDescriptionTitle"):SetText("Description")
local numObjectives = GetNumQuestLeaderBoards()
for i = 1, BENILLA_QUESTLOG_MAX_OBJECTIVES do
- local line = getglobal("BenillaQuestLogObjective" .. i)
+ local line = getglobal("QuestLogObjective" .. i)
if i <= numObjectives then
local text, kind, finished = GetQuestLogLeaderBoard(i)
local display = text
@@ -852,23 +853,23 @@
-- The objective-less TIMED branch (ref :432-437) is transcribed: a timed quest with no
-- leaderboard lines hangs the header off the countdown row, not off ObjectivesText.
local objAnchorIndex = math.min(numObjectives, BENILLA_QUESTLOG_MAX_OBJECTIVES)
- local descTitle = getglobal("BenillaQuestLogDescriptionTitle")
+ local descTitle = getglobal("QuestLogDescriptionTitle")
if objAnchorIndex > 0 then
- descTitle:SetPoint("TOPLEFT", getglobal("BenillaQuestLogObjective" .. objAnchorIndex), "BOTTOMLEFT", 0, -10)
+ descTitle:SetPoint("TOPLEFT", getglobal("QuestLogObjective" .. objAnchorIndex), "BOTTOMLEFT", 0, -10)
elseif questTimer then
- descTitle:SetPoint("TOPLEFT", getglobal("BenillaQuestLogTimerText"), "BOTTOMLEFT", 0, -10)
+ descTitle:SetPoint("TOPLEFT", getglobal("QuestLogTimerText"), "BOTTOMLEFT", 0, -10)
else
- descTitle:SetPoint("TOPLEFT", getglobal("BenillaQuestLogObjectivesText"), "BOTTOMLEFT", 0, -10)
+ descTitle:SetPoint("TOPLEFT", getglobal("QuestLogObjectivesText"), "BOTTOMLEFT", 0, -10)
end
local rewardHeight = BenillaQuestLogRewards_Update()
BenillaQuestLogDetail_ResizeChild(objAnchorIndex, rewardHeight)
if not doNotScroll then
- getglobal("BenillaQuestLogDetailScroll"):SetVerticalScroll(0)
+ getglobal("QuestLogDetailScroll"):SetVerticalScroll(0)
end
end
- -- ── The always-visible quest-watch tracker (BenillaQuestWatchFrame) ─────────────────────────
+ -- ── The always-visible quest-watch tracker (QuestWatchFrame) ─────────────────────────
-- Ref QuestWatch_Update (QuestLogFrame.lua:602-663), transcribed onto the flat 30-line pool
-- (the frame's own XML comment): every paint re-anchors each used line — a quest TITLE drops
-- -5 below the previous line (ref -4 under its 13px boxes; ours are measured 12s, same
@@ -879,7 +880,7 @@
-- window's Track dot, and hides the pool's trailing lines. `GetQuestIndexForWatch(w)`
-- answers each watch's CURRENT log index (it follows the quest across a log shuffle —
-- `quest_log.rs`'s id-keyed watch set), exactly like the ref.
- function BenillaQuestWatch_Update()
+ function QuestWatch_Update()
local watchTextIndex = 1
local maxWidth = 0
for i = 1, GetNumQuestWatches() do
@@ -887,11 +888,11 @@
if questIndex then
local numObjectives = GetNumQuestLeaderBoards(questIndex)
if numObjectives > 0 then
- local titleLine = getglobal("BenillaQuestWatchLine" .. watchTextIndex)
+ local titleLine = getglobal("QuestWatchLine" .. watchTextIndex)
titleLine:SetText(GetQuestLogTitle(questIndex) or "")
if watchTextIndex > 1 then
titleLine:ClearAllPoints()
- titleLine:SetPoint("TOPLEFT", getglobal("BenillaQuestWatchLine" .. (watchTextIndex - 1)), "BOTTOMLEFT", 0, -5)
+ titleLine:SetPoint("TOPLEFT", getglobal("QuestWatchLine" .. (watchTextIndex - 1)), "BOTTOMLEFT", 0, -5)
end
titleLine:Show()
if titleLine:GetStringWidth() > maxWidth then maxWidth = titleLine:GetStringWidth() end
@@ -900,7 +901,7 @@
for j = 1, numObjectives do
local text, kind, finished = GetQuestLogLeaderBoard(j, questIndex)
if not text or text == "" then text = kind or "" end
- local line = getglobal("BenillaQuestWatchLine" .. watchTextIndex)
+ local line = getglobal("QuestWatchLine" .. watchTextIndex)
line:SetText(" - " .. text)
if finished then
line:SetTextColor(1, 1, 1)
@@ -909,7 +910,7 @@
line:SetTextColor(0.8, 0.8, 0.8)
end
line:ClearAllPoints()
- line:SetPoint("TOPLEFT", getglobal("BenillaQuestWatchLine" .. (watchTextIndex - 1)), "BOTTOMLEFT", 0, -1)
+ line:SetPoint("TOPLEFT", getglobal("QuestWatchLine" .. (watchTextIndex - 1)), "BOTTOMLEFT", 0, -1)
line:Show()
if line:GetStringWidth() > maxWidth then maxWidth = line:GetStringWidth() end
watchTextIndex = watchTextIndex + 1
@@ -925,11 +926,11 @@
-- The log window's Track dot (the ref's tracking-indicator tail): green with ≥1
-- watch, red with none.
if GetNumQuestWatches() > 0 then
- getglobal("BenillaQuestLogTrackTracking"):SetVertexColor(0, 1.0, 0)
+ getglobal("QuestLogTrackTracking"):SetVertexColor(0, 1.0, 0)
else
- getglobal("BenillaQuestLogTrackTracking"):SetVertexColor(1.0, 0, 0)
+ getglobal("QuestLogTrackTracking"):SetVertexColor(1.0, 0, 0)
end
- local frame = getglobal("BenillaQuestWatchFrame")
+ local frame = getglobal("QuestWatchFrame")
if watchTextIndex == 1 then
frame:Hide()
return
@@ -942,7 +943,7 @@
frame.used_lines = watchTextIndex - 1
frame.resize_pending = 2
for i = watchTextIndex, BENILLA_QUESTWATCH_LINES do
- getglobal("BenillaQuestWatchLine" .. i):Hide()
+ getglobal("QuestWatchLine" .. i):Hide()
end
end
@@ -993,7 +994,7 @@
end
end
- function BenillaAutoQuestWatch_Update(questIndex)
+ function AutoQuestWatch_Update(questIndex)
if AUTO_QUEST_WATCH ~= "1" or not questIndex then return end
local title = GetQuestLogTitle(questIndex)
if not title then return end
@@ -1017,7 +1018,7 @@
BENILLA_QUEST_WATCH_TIMERS[title] = BENILLA_QUEST_WATCH_TIMER
end
- function BenillaAutoQuestWatch_OnUpdate(elapsed)
+ function AutoQuestWatch_OnUpdate(elapsed)
local expired = nil
for title, timer in pairs(BENILLA_QUEST_WATCH_TIMERS) do
if timer - elapsed <= 0 then
@@ -1032,7 +1033,7 @@
BENILLA_QUEST_WATCH_TIMERS[title] = nil
BenillaQuestWatch_RemoveByTitle(title)
end
- BenillaQuestWatch_Update()
+ QuestWatch_Update()
BenillaQuestLogList_Update() -- the log's row ticks follow (ref: QuestLog_Update)
end
end
@@ -1047,29 +1048,29 @@
frame:RegisterEvent("QUEST_LOG_UPDATE")
frame:RegisterEvent("QUEST_WATCH_UPDATE")
frame:RegisterEvent("BENILLA_QUEST_PROGRESS")
- BenillaQuestWatch_Update()
+ QuestWatch_Update()
end
function BenillaQuestWatchFrame_OnEvent(frame)
if event == "BENILLA_QUEST_PROGRESS" then
- BenillaAutoQuestWatch_Update(arg1)
- BenillaQuestWatch_Update()
+ AutoQuestWatch_Update(arg1)
+ QuestWatch_Update()
elseif event == "QUEST_WATCH_UPDATE" or event == "QUEST_LOG_UPDATE" then
- BenillaQuestWatch_Update()
+ QuestWatch_Update()
end
end
-- The watch frame's OnUpdate carries the ref's auto-watch timer decay (ref
-- QuestLogFrame.xml:1140-1144 — the ALWAYS-VISIBLE frame hosts it, so timers tick with
-- the log closed; hidden-with-no-watches is safe, an empty watch set has no timers) plus
- -- our sizing-convergence tail (BenillaQuestWatch_Update's own comment).
+ -- our sizing-convergence tail (QuestWatch_Update's own comment).
function BenillaQuestWatchFrame_OnUpdate(frame, elapsed)
- BenillaAutoQuestWatch_OnUpdate(elapsed or 0)
+ AutoQuestWatch_OnUpdate(elapsed or 0)
if frame.resize_pending and frame.resize_pending > 0 then
frame.resize_pending = frame.resize_pending - 1
local maxw = 0
for i = 1, (frame.used_lines or 0) do
- local w = getglobal("BenillaQuestWatchLine" .. i):GetStringWidth()
+ local w = getglobal("QuestWatchLine" .. i):GetStringWidth()
if w > maxw then maxw = w end
end
if maxw > 0 then frame:SetWidth(maxw + 10) end
@@ -1081,11 +1082,11 @@
-- absent/falsy for a manual call (a row click, the window's own OnShow); `true` from the
-- QUEST_LOG_UPDATE event handler below.
function BenillaQuestLogFrame_Update(doNotScroll)
- BenillaQuestLog_SetFirstValidSelection()
+ QuestLog_SetFirstValidSelection()
BenillaQuestLogList_Update()
-- pin §5, QuestLog_OnEvent (QuestLogFrame.lua:57-63): the detail pane only repaints while
-- the window is visible — the list rebuild above runs unconditionally either way.
- if getglobal("BenillaQuestLogFrame"):IsVisible() then
+ if getglobal("QuestLogFrame"):IsVisible() then
BenillaQuestLogDetail_Update(doNotScroll)
end
end
@@ -1106,7 +1107,7 @@
-- pin §8, QuestLogFrame.lua:24-30 — the app's 'L' binding already calls this
-- (ui_script/mod.rs).
function ToggleQuestLog()
- local frame = getglobal("BenillaQuestLogFrame")
+ local frame = getglobal("QuestLogFrame")
if frame:IsVisible() then
HideUIPanel(frame)
else
@@ -1115,7 +1116,7 @@
end
function BenillaQuestLogCloseButton_OnClick()
- HideUIPanel(getglobal("BenillaQuestLogFrame"))
+ HideUIPanel(getglobal("QuestLogFrame"))
end
-- ── The abandon two-step (mark on click → confirm on Yes) — the ref's ABANDON_QUEST
@@ -1150,7 +1151,7 @@
- BenillaQuestLogRewardItem_OnClick(self)
+ QuestLogRewardItem_OnClick()
@@ -1225,7 +1226,7 @@
16x16 UI-PlusButton-Hilight at the same spot is the ONLY hover art (quest rows clear it
too — the ref has NO row-wide hover glow), and the text starts at LEFT+20 (the real
indent; quest rows add the ref's own two-space prefix on top). The PERSISTENT selection bar
- is the separate `BenillaQuestLogSelectHighlight` frame `BenillaQuestLogList_Update` moves.
+ is the separate `QuestLogSelectHighlight` frame `BenillaQuestLogList_Update` moves.
`$parentCheck` (pin §2, ref `QuestLogFrame.xml:34-45`) is the per-row watch checkbox — a 16×16
`UI-CheckBox-Check`, no `file=` here (where the ref uses `hidden="true"`): it starts with
@@ -1298,7 +1299,7 @@
-
+
@@ -1318,42 +1319,42 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
-
+
@@ -1376,7 +1377,7 @@
-
+
@@ -1390,31 +1391,31 @@
(70,-48) — its own +/- NormalTexture slot doubles as the all-state icon. The ref's
sibling "Track Quest" checkbox (QuestLogTrack) is a named deferral: our watch
toggle is the row right-click until the track-checkbox arc lands. -->
-
+
-
+
-
+
-
+
-
+
-
+
- BenillaQuestLogCollapseAllButton_OnClick()
+ QuestLogCollapseAllButton_OnClick()
-
+
-
+
@@ -1453,14 +1454,14 @@
-
+
- getglobal("BenillaQuestLogTrackTracking"):SetVertexColor(1.0, 0, 0)
+ getglobal("QuestLogTrackTracking"):SetVertexColor(1.0, 0, 0)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
@@ -1479,11 +1480,11 @@
the title text and washed it out — 0112's capture gate caught the wash, this scrub
found the cause). Moved onto the selected row (at the ref's (5,0)) and vertex-colored
to the row's difficulty color by `BenillaQuestLogList_Update` (ref lua l.337-339). -->
-
+
-
+
@@ -1492,25 +1493,25 @@
-
+
-
-
-
-
-
+
+
+
+
+
-
+
@@ -1526,18 +1527,18 @@
subtree; everything downstream still chains by NAME exactly as before (anchor
resolution is name-keyed, not parent-scoped — unaffected by which frame owns
which region). -->
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
@@ -1645,43 +1646,43 @@
`BenillaQuestLogRewards_Update` SetPoints every shown row every repaint, porting
`QuestFrameItems_Update`'s own SetPoint math verbatim (QuestFrame.lua:311-522,
cited per-line in that function). -->
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1700,11 +1701,11 @@
interactive paths to scroll, which is already the ENTIRE ask (the ref's own
mouse-drag-the-thumb affordance is the one piece missing). Hidden whenever
nothing overflows (`BenillaQuestLogDetailScroll_SyncBar`). -->
-
+
-
+
-
+
@@ -1713,7 +1714,7 @@
BenillaQuestLogDetailScroll_Step(-BENILLA_QUESTLOG_SCROLL_STEP)
-
+
@@ -1726,11 +1727,11 @@
arrow buttons (261 - 16 - 16). The thumb texture is a plain child
region SetPoint'd by BenillaQuestLogDetailScroll_SyncBar off the
CURRENT GetVerticalScroll()/GetVerticalScrollRange() fraction. -->
-
+
-
+
-
+
-
+ BenillaQuestLogAbandonButton_OnClick()
-
+
- HideUIPanel(getglobal("BenillaQuestLogFrame"))
+ HideUIPanel(getglobal("QuestLogFrame"))
-
+
-
+ self:Disable()
@@ -1796,11 +1797,11 @@
the ONLY thing the ref's window OnUpdate does. Gated on hasTimer, so it costs an
untimed selection nothing, and gated on the window being effectively visible by
`UiScript::tick`'s own OnUpdate gate, so it costs a closed window nothing. -->
- BenillaQuestLog_OnUpdate(arg1)
+ QuestLog_OnUpdate(arg1)BenillaQuestLogFrame_OnMouseWheel(arg1)
@@ -1821,7 +1822,7 @@
-
+
@@ -1867,40 +1868,40 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/crates/benilla-app/assets/ui/QuestTimerFrame.xml b/crates/benilla-app/assets/ui/QuestTimerFrame.xml
index 639f5b51..f1ef47ee 100644
--- a/crates/benilla-app/assets/ui/QuestTimerFrame.xml
+++ b/crates/benilla-app/assets/ui/QuestTimerFrame.xml
@@ -24,14 +24,18 @@
getglobal(name), and the ref's own Lua below reaches its rows by
getglobal("QuestTimer"..i.."Text"). Prefixing would silently unseat the frame and break
the row lookup at once.
- - Lua 5.0 `arg`/`arg.n` varargs replaced with select("#", ...)/select(i, ...) —
- LUA_COMPAT_VARARG isn't shipped (TalentFrame.xml / WorldMapFrame.xml's precedent).
+ - Lua 5.0 `arg`/`arg.n` varargs written as select("#", ...)/select(i, ...), the 5.1-native
+ spelling and this tree's house style (TalentFrame.xml / WorldMapFrame.xml's precedent).
- The ref's XML anchor (TOPRIGHT of MinimapCluster +10,0) is transcribed and, like every
managed HUD frame, DEAD ON ARRIVAL: UIParent_ManageFramePositions re-seats it at
-CONTAINER_OFFSET_X on every pass. Kept verbatim anyway so the file reads as the ref's.
- - `toplevel="true"` is dropped: it is the ref's raise-on-click behaviour for movable
- dialogs, which this engine has no verb for and this frame never uses (it is not movable
- and its click opens the quest log rather than raising anything).
+ - `toplevel="true"` is dropped, and the reason given here was half wrong: the raise's real
+ triggers are **Show**, drag-start and mouse-down, not "raise-on-click for movable dialogs"
+ (wow-re `ui/scratch/toplevel-raise.md`). The verb now exists (`script::object::toplevel`),
+ so the standing reason is only the second one — this frame is a managed HUD strip that
+ UIParent_ManageFramePositions re-seats and that overlaps nothing in its own stratum, so
+ the occlusion gate would decline every raise anyway. Re-adding it is one word if that
+ stops being true.
- QuestTimerFrame_OnShow/OnHide call the manage pass, as the ref does — that is what lets
the durability guy and the quest tracker drop below this window when a timer appears.
- The ref's own commented-out QuestTimerFrame_UpdatePosition (its pre-manage-pass
@@ -112,7 +116,7 @@
if not index then
return
end
- ShowUIPanel(getglobal("BenillaQuestLogFrame"))
+ ShowUIPanel(getglobal("QuestLogFrame"))
SelectQuestLogEntry(index)
BenillaQuestLogFrame_Update()
end
diff --git a/crates/benilla-app/assets/ui/ScrollTemplates.xml b/crates/benilla-app/assets/ui/ScrollTemplates.xml
index 1a023928..0cd2e865 100644
--- a/crates/benilla-app/assets/ui/ScrollTemplates.xml
+++ b/crates/benilla-app/assets/ui/ScrollTemplates.xml
@@ -64,6 +64,25 @@
-- repaint calls _Update below, which SetValue()s the thumb back to offset*itemHeight — a value
-- that maps to the SAME offset, so this handler no-ops and the cycle stops after one hop (the
-- Slider's own SetValue change-gate stops the value side, decision 0250).
+ --
+ -- THE SECOND HALF IS FOR ADDONS, and it is the reference's own wiring. The ref's
+ -- `UIPanelScrollBarTemplate` routes its value through the ScrollFrame it rides
+ -- (`this:GetParent():SetVerticalScroll(arg1)`,
+ -- ref-UIPanelTemplates.xml l.156-158), and that frame's `` is where an
+ -- addon puts `FauxScrollFrame_OnVerticalScroll(itemHeight, MyUpdate)`. Our OWN faux frames
+ -- are plain ``s (the 0251 divergence in the header above), so they have no
+ -- SetVerticalScroll and this line never fires for them — but an addon's is declared
+ -- ``, and the instance's own tag wins
+ -- over the template's on inherit (framexml.rs `merge`), so it really is one. Duck-typed for
+ -- exactly that reason.
+ --
+ -- GAP, stated where it bites: `SetVerticalScroll` clamps into `[0, GetVerticalScrollRange()]`
+ -- (scrollframe.rs), and that range comes from the frame's SCROLL CHILD — which the ref's
+ -- `FauxScrollFrameTemplate` declares as ``
+ -- and our XML loader has no `` element for. Until it does, an addon frame's
+ -- range reads 0, so the handler fires with arg1 = 0 and its list stays at the top. The
+ -- offset our own kit computes above is unaffected — it comes straight off the Slider, which
+ -- is the whole point of 0251.
function BenillaFauxScrollBar_OnValueChanged(scrollBar, value)
local frame = scrollBar:GetParent()
local step = scrollBar:GetValueStep()
@@ -74,25 +93,53 @@
if frame.updateFunc then frame.updateFunc() end
end
BenillaScrollBar_UpdateArrows(scrollBar)
+ if frame.SetVerticalScroll then frame:SetVerticalScroll(value) end
end
- -- OnLoad: a fresh faux frame is at the top with an empty range (ref ScrollFrame_OnLoad).
- function BenillaFauxScrollFrame_OnLoad(frame)
- frame.offset = 0
- local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
+ -- OnLoad: a fresh scroll frame is at the top with an empty range and both arrows greyed
+ -- (ref UIPanelTemplates.lua l.244-252, verbatim but for the one added line). The ref's own
+ -- name, because this is the ref's own function: `FauxScrollFrameTemplate` inherits
+ -- `UIPanelScrollFrameTemplate`, whose `` is exactly this.
+ --
+ -- The added line is `SetValueStep(1)`: our bar's step is also its ARROW/WHEEL step
+ -- (BenillaScrollBar_Step), so a bar that has not been through an _Update yet would step by
+ -- nothing. The ref has no such coupling — its arrows move by half the bar's height.
+ function ScrollFrame_OnLoad()
+ getglobal(this:GetName() .. "ScrollBarScrollDownButton"):Disable()
+ getglobal(this:GetName() .. "ScrollBarScrollUpButton"):Disable()
+
+ local scrollBar = getglobal(this:GetName() .. "ScrollBar")
scrollBar:SetMinMaxValues(0, 0)
scrollBar:SetValueStep(1)
scrollBar:SetValue(0)
+ this.offset = 0
end
- -- The offset getters/setters the owner's list code reads (ref FauxScrollFrame_GetOffset /
- -- _SetOffset). _SetOffset syncs the thumb too, so a programmatic reset (top-of-list on open)
- -- moves the bar.
- function BenillaFauxScrollFrame_GetOffset(frame)
+ -- The frame's `` body, as the reference writes it (UIPanelTemplates.lua
+ -- l.228-233, verbatim): the frame scrolled, so seat the bar at the new pixel value, round it
+ -- to a row, and repaint. `this`/`arg1` are the handler globals our dispatcher sets
+ -- (script/event.rs `invoke_with_globals`), so the ref's body runs unchanged.
+ --
+ -- Nothing in benilla's own FrameXML calls this — our six faux lists take the offset straight
+ -- off the Slider (BenillaFauxScrollBar_OnValueChanged above). It is here because it is the
+ -- shape 27 corpus addons write into their own XML, and it is reached through the
+ -- SetVerticalScroll passthrough there, whose one gap is documented at that spot.
+ function FauxScrollFrame_OnVerticalScroll(itemHeight, updateFunction)
+ local scrollbar = getglobal(this:GetName() .. "ScrollBar")
+ scrollbar:SetValue(arg1)
+ this.offset = floor((arg1 / itemHeight) + 0.5)
+ updateFunction()
+ end
+
+ -- The offset getters/setters the owner's list code reads (ref UIPanelTemplates.lua
+ -- l.235-241). _SetOffset diverges by one line, deliberately: the ref writes `frame.offset`
+ -- and leaves the thumb where it was, which is a latent desync until the next _Update; ours
+ -- syncs the bar, so a programmatic reset (top-of-list on open) moves it there and then.
+ function FauxScrollFrame_GetOffset(frame)
return frame.offset or 0
end
- function BenillaFauxScrollFrame_SetOffset(frame, offset)
+ function FauxScrollFrame_SetOffset(frame, offset)
frame.offset = offset
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
scrollBar:SetValue(offset * scrollBar:GetValueStep())
@@ -152,27 +199,93 @@
end
end
- -- Reconcile the bar to a freshly-sized list (ref FauxScrollFrame_Update): clamp the offset into
- -- range, set the pixel range + step, seat the thumb at the current offset, and show the bar only
- -- when the list overflows the visible rows. Called by the owner's list-repaint each pass.
- function BenillaFauxScrollFrame_Update(frame, numItems, numToDisplay, itemHeight)
- local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
+ -- Reconcile the bar to a freshly-sized list — the reference's own `FauxScrollFrame_Update`
+ -- (UIPanelTemplates.lua l.160-226), whose signature and return value are transcribed whole
+ -- because 28 corpus addons call it with all ten arguments and read the result:
+ --
+ -- frame, numItems, numToDisplay, valueStep the list itself (our four)
+ -- button, smallWidth, bigWidth a `1..numToDisplay` row family to
+ -- NARROW when the bar takes its gutter
+ -- highlightFrame, smallHighlightWidth, and the moving highlight over them
+ -- bigHighlightWidth
+ -- → showScrollBar 1 when the bar is up, nil when it is not
+ --
+ -- `valueStep` is what benilla called `itemHeight` before this landed; same number, the ref's
+ -- name. The shrink/widen tail is pure ref and no benilla window passes it — our lists are
+ -- laid out to their with-bar width from the start.
+ --
+ -- TWO DELIBERATE DIVERGENCES from the ref body, both named where they are:
+ --
+ -- 1 · The ref gates its whole middle section on `frame:IsVisible()` right after showing or
+ -- hiding the frame — reading back a state it just wrote. That is a quirk, not a
+ -- mechanism: it is true exactly when `showScrollBar` is, EXCEPT when an ancestor is
+ -- hidden, in which case the range/step/value silently do not get set and the bar is
+ -- stale the next time the window opens. benilla repaints lists while their window is
+ -- closed (a feed arriving is not a click), so we gate on `showScrollBar` — what the ref
+ -- means — and the offset clamp runs unconditionally.
+ -- 2 · The bar is shown/hidden through BenillaScrollBar_SetShown, not by the frame alone:
+ -- ours also carries the TROUGH (see its own note above; the ref has no trough in this
+ -- kit at all). The frame's own Show/Hide is kept as well, because it is what an addon
+ -- reads back — and because for us it is the same pixels either way, the bar being the
+ -- only thing a faux frame contains.
+ function FauxScrollFrame_Update(frame, numItems, numToDisplay, valueStep, button, smallWidth, bigWidth, highlightFrame, smallHighlightWidth, bigHighlightWidth)
+ local frameName = frame:GetName()
+ local scrollBar = getglobal(frameName .. "ScrollBar")
+
local maxOffset = numItems - numToDisplay
if maxOffset < 0 then maxOffset = 0 end
if not frame.offset or frame.offset > maxOffset then frame.offset = maxOffset end
if frame.offset < 0 then frame.offset = 0 end
- scrollBar:SetValueStep(itemHeight)
- scrollBar:SetMinMaxValues(0, maxOffset * itemHeight)
- scrollBar:SetValue(frame.offset * itemHeight)
+ local showScrollBar
+ if numItems > numToDisplay then showScrollBar = 1 end
+
+ scrollBar:SetValueStep(valueStep)
+ scrollBar:SetMinMaxValues(0, maxOffset * valueStep)
+ scrollBar:SetValue(frame.offset * valueStep)
BenillaScrollBar_UpdateArrows(scrollBar)
- BenillaScrollBar_SetShown(scrollBar, numItems > numToDisplay)
+ if showScrollBar then frame:Show() else frame:Hide() end
+ BenillaScrollBar_SetShown(scrollBar, showScrollBar)
+
+ -- The ref sizes the scroll child to the whole list (l.179-191) — which is what gives a
+ -- real ScrollFrame its scroll range. GUARDED, because our loader has no ``
+ -- element, so `$parentScrollChildFrame` exists only if the owner declared one itself.
+ -- (This is the same gap BenillaFauxScrollBar_OnValueChanged documents; closing it is a
+ -- loader change, not a FrameXML one.)
+ local scrollChildFrame = getglobal(frameName .. "ScrollChildFrame")
+ if scrollChildFrame then
+ if numItems > 0 then
+ scrollChildFrame:SetHeight(numItems * valueStep)
+ scrollChildFrame:Show()
+ else
+ scrollChildFrame:Hide()
+ end
+ end
+
+ -- Shrink because the bar is shown / widen because it is hidden (ref l.205-223).
+ if showScrollBar then
+ if highlightFrame then highlightFrame:SetWidth(smallHighlightWidth) end
+ if button then
+ for i = 1, numToDisplay do getglobal(button .. i):SetWidth(smallWidth) end
+ end
+ else
+ if highlightFrame then highlightFrame:SetWidth(bigHighlightWidth) end
+ if button then
+ for i = 1, numToDisplay do getglobal(button .. i):SetWidth(bigWidth) end
+ end
+ end
+
+ return showScrollBar
end
-- A wheel spin over the list → one row (WoW convention: +1 up, −1 down). Called from the
-- owner window's OnMouseWheel (the engine bubbles the spin up the parent chain, so the handler
-- lives on the window, not a child — TrainerFrame.xml's own note).
+ --
+ -- KEEPS the `Benilla` prefix on purpose: the reference has no faux-scroll wheel function to
+ -- take the name of. Its `ScrollFrameTemplate_OnMouseWheel` is the REAL-scroll one and moves
+ -- by half the bar's height; its faux owners each write a wheel body of their own inline.
function BenillaFauxScrollFrame_OnMouseWheel(frame, delta)
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
BenillaScrollBar_Step(scrollBar, -delta)
@@ -388,7 +501,7 @@
UIPanelScrollFrameTemplate's scrollbar anchors). An owner instantiates this and, in its own
OnLoad, sets `frame.updateFunc` to its list-repaint.
-->
-
+
@@ -398,7 +511,7 @@
- BenillaFauxScrollFrame_OnLoad(self)
+ ScrollFrame_OnLoad()
diff --git a/crates/benilla-app/assets/ui/SkillFrame.xml b/crates/benilla-app/assets/ui/SkillFrame.xml
index 65d32dd7..49b1d822 100644
--- a/crates/benilla-app/assets/ui/SkillFrame.xml
+++ b/crates/benilla-app/assets/ui/SkillFrame.xml
@@ -8,20 +8,20 @@
`Expand`/`CollapseSkillHeader`/`SetSelectedSkill`/`GetSelectedSkill`, decision 0437's skills
seam, `crates/benilla-ui/src/script/skills.rs`, built alongside this file).
- A CHILD PAGE of `BenillaCharacterFrame` (ref: `parent="CharacterFrame"`, `id="4"`), shown/
- hidden by `CharacterFrame_ShowSubFrame` exactly like `BenillaPaperDollFrame` — but NOT a
+ A CHILD PAGE of `CharacterFrame` (ref: `parent="CharacterFrame"`, `id="4"`), shown/
+ hidden by `CharacterFrame_ShowSubFrame` exactly like `PaperDollFrame` — but NOT a
physically nested `` the way PaperDollFrame is: PaperDollFrame is declared INSIDE
CharacterFrame.xml's own `` tree (a real engine parent/child), while this page is a
SEPARATE file's top-level `` (CharacterFrame.xml is shared-tree with another session
this pass, so only its tab machinery is touched here — not its frame tree). `parent=` is
transcribed for fidelity but is a NO-OP in this engine (grepped `benilla-ui/src/loader`: the
XML `parent` attribute is read nowhere — only physical `` nesting creates a real
- parent). Position instead rides `` (a NAMED
+ parent). Position instead rides `` (a NAMED
lookup, works regardless of tree position, the same mechanism every sibling-anchor in this
codebase already uses) — loaded right after `CharacterFrame.xml` in the load list so it
paints ON TOP of it (later file = higher z, the established house convention every top-level
window already relies on implicitly). Because a true engine Hide-cascade doesn't reach a
- non-child, `CharacterFrame.xml`'s own `BenillaCharacterFrame_OnHide` was extended (its own
+ 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
page's non-nested shape requires, done once so later non-nested tabs (Pet/Reputation/Honor)
inherit it for free.
@@ -29,7 +29,7 @@
NO OWN PORTRAIT (matches the ref: `ref-ui/SkillFrame.xml` l.130-213 draws no portrait Texture
at all): the shared quadrant background art below (`UI-Character-General-TopLeft/TopRight`,
the SAME "General" variant the ref uses, l.136-159) carries the SAME baked-in portrait-ring
- cutout every PaperDollInfoFrame quadrant variant shares, so `BenillaCharacterFrame`'s own
+ cutout every PaperDollInfoFrame quadrant variant shares, so `CharacterFrame`'s own
portrait/name/level (drawn at a lower z, underneath) shows through unchanged — exactly the
ref's own real-client compositing (SkillFrame is a nested child there too, relying on the
same cutout rather than redrawing the portrait itself).
@@ -42,10 +42,10 @@
v1 DEVIATIONS (each re-cited at its own site below):
1. THE DETAIL PANE IS MINIMAL-PLUS-DESCRIPTION-PLUS-ABANDON: the selected skill's rank
- StatusBar + name + the DESCRIPTION BODY (`BenillaSkillDetailDescriptionText` — ref
+ StatusBar + name + the DESCRIPTION BODY (`SkillDetailDescriptionText` — ref
`SkillDetailDescriptionText`/`SKILL_DESCRIPTION`, real since `GetSkillLineInfo`'s 13th
return carries `SkillLine.dbc` col 12) + the ABANDON BUTTON
- (`BenillaSkillDetailUnlearnButton` → the `UNLEARN_SKILL` confirm → `AbandonSkill` →
+ (`SkillDetailUnlearnButton` → the `UNLEARN_SKILL` confirm → `AbandonSkill` →
`CMSG_UNLEARN_SKILL`; the isAbandonable predicate is `SkillRaceClassInfo.flags & 0x20`
— the server's own gate, real 5875 data: primary professions only). STILL OMITTED:
the training-point spend row (the ref's `LeftArrow`/`RightArrow`/`LearnSkillButton`/
@@ -58,8 +58,8 @@
3. NO ACCEPT/CANCEL BUTTONS (ref l.318-357's `SkillFrameAcceptButton`/`SkillFrameCancelButton`,
the latter ALSO commented dead in the ref): both back the training-point economy (Accept/
CancelSkillUps) this pane doesn't ship, AND this page has no window of its own to close —
- `BenillaCharacterFrame`'s single shared close X (CharacterFrame.xml) already covers every
- tab, matching `BenillaPaperDollFrame`'s own lack of a per-page close button.
+ `CharacterFrame`'s single shared close X (CharacterFrame.xml) already covers every
+ tab, matching `PaperDollFrame`'s own lack of a per-page close button.
4. THE RANK TEXT FORMAT IS THE REF'S BYTE-EXACT ONE, not the task brief's own paraphrase:
`ref-ui/SkillFrame.lua` l.178-186 builds `rank .. " (" .. color .. modifier .. close ..
")/" .. max` (e.g. "57 (|cff20ff20+3|r)/75") — transcribed verbatim (the
@@ -75,7 +75,7 @@
7. `LockHighlight`/`UnlockHighlight` ENGINE GAP (TalentFrame.xml/TradeSkillFrame.xml's own
"no engine equivalent" posture, grepped `benilla-ui/src/script/button.rs` — ships
texture-pairs/SetText/Enable/Disable/RegisterForClicks/SetButtonState/Click only): the
- persistent selection glow is `BenillaSkillHighlight`, the separate overlay Frame
+ persistent selection glow is `SkillHighlight`, the separate overlay Frame
Trainer/TradeSkillFrame already use, moved onto the selected row's StatusBar.
8. GlobalStrings this window itself needs: NONE beyond the tab label `SKILLS` (already
declared in CharacterFrame.xml, quoted there byte-exact with its GlobalStrings.lua line
@@ -182,9 +182,9 @@
-- same fill (ref l.220-226): shown only for an abandonable line, carrying the name the
-- confirm popup formats and the index its accept abandons.
function BenillaSkillFrame_UpdateDetail()
- local bar = getglobal("BenillaSkillDetailBar")
- local desc = getglobal("BenillaSkillDetailDescriptionText")
- local unlearn = getglobal("BenillaSkillDetailUnlearnButton")
+ local bar = getglobal("SkillDetailBar")
+ local desc = getglobal("SkillDetailDescriptionText")
+ local unlearn = getglobal("SkillDetailUnlearnButton")
local index = GetSelectedSkill()
if index == 0 then
bar:Hide()
@@ -252,8 +252,8 @@
-- skillCostType coloring + stepCost/rankCost training-up arrows never apply here, since
-- GetSkillLineInfo always reports them nil/0, this file's header comment deviation 1).
function BenillaSkillFrame_SetRow(i, index, numRows)
- local bar = getglobal("BenillaSkillRankFrame" .. i)
- local label = getglobal("BenillaSkillTypeLabel" .. i)
+ local bar = getglobal("SkillRankFrame" .. i)
+ local label = getglobal("SkillTypeLabel" .. i)
if index > numRows then
bar:Hide()
@@ -292,11 +292,11 @@
-- TradeSkillFrame.xml "ref's own dead code is dropped" precedent).
function BenillaSkillFrame_Update()
local numRows = GetNumSkillLines()
- local scrollFrame = getglobal("BenillaSkillListScrollFrame")
- BenillaFauxScrollFrame_Update(scrollFrame, numRows, SKILLFRAME_SKILLS_DISPLAYED, SKILLFRAME_SKILL_HEIGHT)
- local offset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
+ local scrollFrame = getglobal("SkillListScrollFrame")
+ FauxScrollFrame_Update(scrollFrame, numRows, SKILLFRAME_SKILLS_DISPLAYED, SKILLFRAME_SKILL_HEIGHT)
+ local offset = FauxScrollFrame_GetOffset(scrollFrame)
- local highlight = getglobal("BenillaSkillHighlight")
+ local highlight = getglobal("SkillHighlight")
highlight:Hide()
local selected = GetSelectedSkill()
@@ -304,7 +304,7 @@
local index = i + offset
BenillaSkillFrame_SetRow(i, index, numRows)
if selected == index and index <= numRows then
- highlight:SetPoint("TOPLEFT", getglobal("BenillaSkillRankFrame" .. i), "TOPLEFT", -5, 0)
+ highlight:SetPoint("TOPLEFT", getglobal("SkillRankFrame" .. i), "TOPLEFT", -5, 0)
highlight:Show()
end
end
@@ -317,7 +317,7 @@
allExpanded = false
end
end
- local collapseAll = getglobal("BenillaSkillCollapseAllButton")
+ local collapseAll = getglobal("SkillCollapseAllButton")
if numRows == 0 then
collapseAll:Disable()
else
@@ -370,12 +370,12 @@
-- driven the TrainerFrame.xml BenillaTrainerCollapseAllButton way since this engine's
-- CollapseSkillHeader(0)/ExpandSkillHeader(0) already fold/unfold every group at once).
function BenillaSkillCollapseAllButton_OnClick()
- local b = getglobal("BenillaSkillCollapseAllButton")
+ local b = getglobal("SkillCollapseAllButton")
if b.collapsed then
ExpandSkillHeader(0)
else
CollapseSkillHeader(0)
- BenillaFauxScrollFrame_SetOffset(getglobal("BenillaSkillListScrollFrame"), 0)
+ FauxScrollFrame_SetOffset(getglobal("SkillListScrollFrame"), 0)
end
BenillaSkillFrame_Update()
end
@@ -383,27 +383,29 @@
-- The window's wheel handler (bubbled up the parent chain from a row) → one row via the faux
-- kit (TrainerFrame.xml's own note: the engine bubbles a spin up the parent chain).
function BenillaSkillFrame_OnMouseWheel(delta)
- BenillaFauxScrollFrame_OnMouseWheel(getglobal("BenillaSkillListScrollFrame"), delta)
+ BenillaFauxScrollFrame_OnMouseWheel(getglobal("SkillListScrollFrame"), delta)
end
-- ── Lifecycle (ref SkillFrame_OnLoad/_OnShow/_OnEvent, l.4-22, collapsed to the events this
-- page's feed actually fires: CHARACTER_POINTS_CHANGED backs the training-point economy this
-- pane doesn't ship, this file's header comment deviation 1). ─────────────────────────────────
- function BenillaSkillFrame_OnLoad(frame)
+ function SkillFrame_OnLoad()
+ local frame = this
frame:RegisterEvent("SKILL_LINES_CHANGED")
- getglobal("BenillaSkillListScrollFrame").updateFunc = BenillaSkillFrame_Update
+ getglobal("SkillListScrollFrame").updateFunc = BenillaSkillFrame_Update
-- The description body hangs under the detail bar — anchored here, not in XML,
-- because the bar is a child frame declared after the page's layers and anchors
-- resolve at SetPoint time (an XML forward reference silently lands on the page;
-- the MailFrame error-text OnLoad precedent, ref MailFrame.xml l.1040).
- BenillaSkillDetailDescriptionText:SetPoint("TOPLEFT", "BenillaSkillDetailBar", "BOTTOMLEFT", -2, -10)
+ SkillDetailDescriptionText:SetPoint("TOPLEFT", "SkillDetailBar", "BOTTOMLEFT", -2, -10)
end
- function BenillaSkillFrame_OnShow()
+ function SkillFrame_OnShow()
BenillaSkillFrame_Update()
end
- function BenillaSkillFrame_OnEvent(frame)
+ function SkillFrame_OnEvent(event)
+ local frame = this
if event == "SKILL_LINES_CHANGED" and frame:IsVisible() then
BenillaSkillFrame_Update()
end
@@ -470,11 +472,11 @@
Reputation (the ref's 3) is not built — the same deviation CharacterFrame.xml's tab row
already flags. `character_tests.rs` pins every page's id against its slot in
BENILLA_CHARACTERFRAME_SUBFRAMES, so the day Reputation lands this is loud, not silent. -->
-
+
+ (this file's header comment) — pinned to CharacterFrame's own TOPLEFT by name. -->
-
+
@@ -506,14 +508,14 @@
last row sits slightly lower than the ref's fixed-slot geometry — a pixel-fit
adjustment, not a structural deviation). -->
-
+
-
+
-
+
@@ -532,11 +534,11 @@
-
+ BenillaSkillFrame_OnMouseWheel(arg1)
@@ -544,18 +546,18 @@
-
+
-
-
-
+
+
+
-
+
-
+
@@ -568,11 +570,11 @@
-
+
-
+
@@ -580,44 +582,44 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+ same-TOPLEFT-as-row1 convention. updateFunc set in SkillFrame_OnLoad. -->
+
@@ -626,7 +628,7 @@
skill's own rank StatusBar + name — no description/abandon/spend row. Reuses the SAME
rank template the list rows use (its Border click is harmless here: no .skillIndex is
ever set on this instance, so BenillaSkillRankFrame_OnClick's guard no-ops it). -->
-
+
-
+
-
+
@@ -656,9 +658,9 @@
- BenillaSkillFrame_OnLoad(self)
- BenillaSkillFrame_OnShow()
- BenillaSkillFrame_OnEvent(self)
+ SkillFrame_OnLoad()
+ SkillFrame_OnShow()
+ SkillFrame_OnEvent(event)BenillaSkillFrame_OnMouseWheel(arg1)
diff --git a/crates/benilla-app/assets/ui/SpellBookFrame.xml b/crates/benilla-app/assets/ui/SpellBookFrame.xml
index 7d7c6bca..817154e2 100644
--- a/crates/benilla-app/assets/ui/SpellBookFrame.xml
+++ b/crates/benilla-app/assets/ui/SpellBookFrame.xml
@@ -8,16 +8,17 @@
line cites below are into that extraction's own `ref-SpellBookFrame.xml`/`.lua`).
Docks the left UIPanel slot exactly like Character/Merchant/Gossip/Quest/QuestLog
- (`UiPanels.xml`'s `UIPanelWindows["BenillaSpellBookFrame"] = { area = "left", pushable =
+ (`UiPanels.xml`'s `UIPanelWindows["SpellBookFrame"] = { area = "left", pushable =
0 }`, mirroring the ref's own `SpellBookFrame` row) — so it competes for/replaces those
windows, never stacks with them. `ToggleSpellBook(bookType)` (ref `SpellBookFrame.lua:
- 9-50`, kept BARE — not Benilla-prefixed — matching this codebase's own `ToggleCharacter`/
+ 9-50`) is the addon-facing entry point; like every frame name here it is the reference's own
`ToggleQuestLog` precedent) is the show/hide entry point; the app's 'P' binding calls it
with `BOOKTYPE_SPELL` (`ui_script/input.rs`, ref `Bindings.xml:582-583`).
- ONE deliberate naming deviation from the ref, applied consistently: every frame this file
- defines is Benilla-prefixed (`BenillaSpellBookFrame`, `BenillaSpellButton1`, …) — the
- same posture every other window in this codebase already takes.
+ Every frame this file defines wears the REFERENCE's name (`SpellBookFrame`, `SpellButton1`,
+ …), because addons address them by it — the `Benilla` prefix these carried until the
+ reference-naming pass is gone. A child with no counterpart in the 1.12 namespace keeps the
+ bare stripped name so `$parent`/`GetName()..` composition inside the family stays coherent.
The book MODEL (tabs = skill lines, the flat slot list, offsets) is built entirely by the
app from `PlayerActions.spells` through the `Spell.dbc`/`SkillLine.dbc` join
@@ -36,7 +37,7 @@
`crate::ui_pet_book`'s feed of `SMSG_PET_SPELLS`' own spell list. Two reference QUIRKS are
reproduced on purpose here and each carries a do-not-fix note at its site (the standing
rule from decision 1030): the pet page **ignores its page number**, and its page arrows
- write the *spell* book's page counter. See `BenillaSpellBook_GetSpellID` and the two
+ write the *spell* book's page counter. See `SpellBook_GetSpellID` and the two
page-turn handlers.
Named deferrals/OMITs (each re-cited at its own site below):
@@ -96,7 +97,7 @@
-- codebase's own ToggleCharacter/ToggleQuestLog precedent. The ref's own DUPLICATED
-- prev/next enable/disable (once inside the `currentPage > maxPages` clamp branch, once
-- again unconditionally right after — both produce the identical end state) collapses to
- -- the ONE BenillaSpellBook_UpdatePageArrows() call already shared by every other
+ -- the ONE SpellBook_UpdatePageArrows() call already shared by every other
-- page-turn site below — a redundant-code cleanup, not a behavior change.
function ToggleSpellBook(bookType)
-- ref l.11-14: asking for a pet book you haven't got does NOTHING — not even close a
@@ -104,29 +105,30 @@
if bookType == BOOKTYPE_PET and not HasPetSpells() then
return
end
- local isVisible = BenillaSpellBookFrame:IsVisible()
- HideUIPanel(BenillaSpellBookFrame)
- if (not isVisible or (BenillaSpellBookFrame.bookType ~= bookType)) then
- BenillaSpellBookFrame.bookType = bookType
- ShowUIPanel(BenillaSpellBookFrame)
+ local isVisible = SpellBookFrame:IsVisible()
+ HideUIPanel(SpellBookFrame)
+ if (not isVisible or (SpellBookFrame.bookType ~= bookType)) then
+ SpellBookFrame.bookType = bookType
+ ShowUIPanel(SpellBookFrame)
end
BenillaSpellBook_ClampPage()
- BenillaSpellBook_UpdatePageArrows()
- BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, BenillaSpellBook_GetCurrentPage()))
+ SpellBook_UpdatePageArrows()
+ SpellBookPageText:SetText(format(PAGE_NUMBER, SpellBook_GetCurrentPage()))
end
-- Clamp the selected tab's page back onto range (ref's own inline logic inside
-- ToggleSpellBook, l.22-26) — split out so both ToggleSpellBook and a future re-entry
-- share it, rather than inlining it twice.
function BenillaSpellBook_ClampPage()
- local currentPage, maxPages = BenillaSpellBook_GetCurrentPage()
+ local currentPage, maxPages = SpellBook_GetCurrentPage()
if currentPage > maxPages then
- SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] = maxPages
+ SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] = maxPages
BenillaUpdateSpells()
end
end
- function BenillaSpellBookFrame_OnLoad(frame)
+ function SpellBookFrame_OnLoad()
+ local frame = this
frame:RegisterEvent("SPELLS_CHANGED")
-- LEARNED_SPELL_IN_TAB OMITTED — this file's header comment.
frame.bookType = BOOKTYPE_SPELL
@@ -134,45 +136,46 @@
SPELLBOOK_PAGENUMBERS[i] = 1
end
SPELLBOOK_PAGENUMBERS["pet"] = 1 -- ref l.66; read by GetCurrentPage, written by nothing
- BenillaSpellBookSkillLineTab_OnClick(1)
+ SpellBookSkillLineTab_OnClick(1)
end
-- ref SpellBookFrame_OnEvent (l.75-92), the SPELLS_CHANGED branch only (LEARNED_SPELL_IN_TAB
-- OMITTED, header comment).
- function BenillaSpellBookFrame_OnEvent(frame)
+ function SpellBookFrame_OnEvent()
+ local frame = this
if event == "SPELLS_CHANGED" and frame:IsVisible() then
- BenillaSpellBookFrame_Update()
- BenillaSpellBook_UpdatePageArrows()
+ SpellBookFrame_Update()
+ SpellBook_UpdatePageArrows()
end
end
-- ref SpellBookFrame_OnShow (l.94-105): MultiActionBar_ShowAllGrids OMITTED (header
-- comment); the tab-flash UIFrameFlash call goes with LEARNED_SPELL_IN_TAB.
-- UpdateMicroButtons is LIVE as of the micro-menu slice (MicroMenu.xml).
- function BenillaSpellBookFrame_OnShow()
+ function SpellBookFrame_OnShow()
if UpdateMicroButtons then UpdateMicroButtons() end
- BenillaSpellBookFrame_Update(1)
+ SpellBookFrame_Update(1)
end
-- ref SpellBookFrame_Update (l.107-164), whole. Three things happen here and they are
-- easy to conflate: the skill-line strip is gated on the book TYPE (the pet book has no
-- tabs at all — GetSpellTabInfo never sees it), the Spell/Pet toggle row is gated on
-- HasPetSpells(), and the title + open sound fork on which book is up.
- function BenillaSpellBookFrame_Update(showing)
+ function SpellBookFrame_Update(showing)
if showing then
- BenillaSpellBookSkillLineTab_OnClick(BenillaSpellBookFrame.selectedSkillLine)
+ SpellBookSkillLineTab_OnClick(SpellBookFrame.selectedSkillLine)
BenillaUpdateSpells()
end
- local isPet = BenillaSpellBookFrame.bookType == BOOKTYPE_PET
+ local isPet = SpellBookFrame.bookType == BOOKTYPE_PET
local numSkillLineTabs = GetNumSpellTabs()
for i = 1, MAX_SKILLLINE_TABS do
- local skillLineTab = getglobal("BenillaSpellBookSkillLineTab" .. i)
+ local skillLineTab = getglobal("SpellBookSkillLineTab" .. i)
if i <= numSkillLineTabs and not isPet then
local name, texture = GetSpellTabInfo(i)
skillLineTab:SetNormalTexture(texture)
skillLineTab.tooltip = name
skillLineTab:Show()
- skillLineTab:SetChecked(BenillaSpellBookFrame.selectedSkillLine == i)
+ skillLineTab:SetChecked(SpellBookFrame.selectedSkillLine == i)
else
skillLineTab:Hide()
end
@@ -181,26 +184,26 @@
-- ref l.108-111 + l.141-151: hide the row first, then raise exactly the tabs that
-- exist. Tab 3 never appears in 1.12 (no third book) and is hidden by the template's
-- own `hidden="true"`; it is kept so the anchor chain reads like the ref's.
- BenillaSpellBookFrameTabButton1:Hide()
- BenillaSpellBookFrameTabButton2:Hide()
+ SpellBookFrameTabButton1:Hide()
+ SpellBookFrameTabButton2:Hide()
local hasPetSpells, petToken = HasPetSpells()
- BenillaSpellBookFrame.petTitle = nil
+ SpellBookFrame.petTitle = nil
if hasPetSpells then
- BenillaSpellBookFrame_SetTabType(BenillaSpellBookFrameTabButton1, BOOKTYPE_SPELL)
- BenillaSpellBookFrame_SetTabType(BenillaSpellBookFrameTabButton2, BOOKTYPE_PET, petToken)
+ SpellBookFrame_SetTabType(SpellBookFrameTabButton1, BOOKTYPE_SPELL)
+ SpellBookFrame_SetTabType(SpellBookFrameTabButton2, BOOKTYPE_PET, petToken)
elseif isPet then
-- The pet died/was dismissed while its book was open: the ref closes the window
-- and reverts, rather than showing an empty pet page (l.147-151).
- HideUIPanel(BenillaSpellBookFrame)
- BenillaSpellBookFrame.bookType = BOOKTYPE_SPELL
+ HideUIPanel(SpellBookFrame)
+ SpellBookFrame.bookType = BOOKTYPE_SPELL
isPet = false
end
if not isPet then
- BenillaSpellBookTitleText:SetText(SPELLBOOK)
+ SpellBookTitleText:SetText(SPELLBOOK)
if showing then PlaySound("igSpellBookOpen") end
else
- BenillaSpellBookTitleText:SetText(BenillaSpellBookFrame.petTitle)
+ SpellBookTitleText:SetText(SpellBookFrame.petTitle)
-- ref l.161: the pet book opens on the ABILITY kit, not the spellbook one. Both
-- ship in the real SoundEntries.dbc.
if showing then PlaySound("igAbilityOpen") end
@@ -210,7 +213,7 @@
-- ref SpellBookFrame_SetTabType (l.166-183). `binding` is what the tab's tooltip appends
-- the bound key to (MicroButtonTooltipText); `petTitle` is set here as a side effect,
-- exactly as the ref does, because this is the only place the token is in hand.
- function BenillaSpellBookFrame_SetTabType(tabButton, bookType, token)
+ function SpellBookFrame_SetTabType(tabButton, bookType, token)
if bookType == BOOKTYPE_SPELL then
tabButton.bookType = BOOKTYPE_SPELL
tabButton:SetText(SPELLBOOK)
@@ -220,12 +223,12 @@
local label = getglobal("PET_TYPE_" .. (token or "PET")) or PET_TYPE_PET
tabButton:SetText(label)
tabButton.binding = "TOGGLEPETBOOK"
- BenillaSpellBookFrame.petTitle = label
+ SpellBookFrame.petTitle = label
end
-- The tab for the book you are ON is disabled, which is also what draws it as the
-- selected (Tab1-Selected) plate — the ref uses for the "current"
-- look rather than a checked state.
- if BenillaSpellBookFrame.bookType == bookType then
+ if SpellBookFrame.bookType == bookType then
tabButton:Disable()
else
tabButton:Enable()
@@ -244,16 +247,16 @@
frame.clock = frame.clock - BENILLA_PET_AUTOCAST_PERIOD
end
for i = 1, SPELLS_PER_PAGE do
- BenillaPetAutocast_Update(getglobal("BenillaSpellButton" .. i), frame.clock)
+ BenillaPetAutocast_Update(getglobal("SpellButton" .. i), frame.clock)
end
end
-- ref SpellBookFrame_OnHide (l.186-204): MultiActionBar_HideAllGrids and the tab-flash
-- cleanup OMITTED (header comment); always the spell-book close kit (no pet book to branch
-- on). UpdateMicroButtons is LIVE as of the micro-menu slice (MicroMenu.xml).
- function BenillaSpellBookFrame_OnHide()
+ function SpellBookFrame_OnHide()
if UpdateMicroButtons then UpdateMicroButtons() end
- if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
+ if SpellBookFrame.bookType == BOOKTYPE_PET then
PlaySound("igAbilityClose") -- ref l.191
else
PlaySound("igSpellBookClose")
@@ -266,7 +269,7 @@
-- page (this file's own convention: explicit button params, not ambient `this`).
function BenillaUpdateSpells()
for i = 1, SPELLS_PER_PAGE do
- BenillaSpellButton_UpdateButton(getglobal("BenillaSpellButton" .. i))
+ BenillaSpellButton_UpdateButton(getglobal("SpellButton" .. i))
end
end
@@ -277,7 +280,8 @@
-- PET_BAR_UPDATE is LIVE (ref l.214 + l.227-231) and it is the pet page's whole repaint
-- wire: the pet book's cooldowns, autocast bits and active ring all move without the BOOK
-- changing, so SPELLS_CHANGED alone would leave a stale page.
- function BenillaSpellButton_OnLoad(button)
+ function SpellButton_OnLoad()
+ local button = this
button:RegisterEvent("SPELLS_CHANGED")
button:RegisterEvent("CURRENT_SPELL_CAST_CHANGED")
button:RegisterEvent("SPELL_UPDATE_COOLDOWN")
@@ -293,13 +297,14 @@
BenillaSpellButton_UpdateButton(button)
end
- function BenillaSpellButton_OnEvent(button)
+ function SpellButton_OnEvent(event)
+ local button = this
if event == "SPELLS_CHANGED" or event == "SPELL_UPDATE_COOLDOWN" then
BenillaSpellButton_UpdateButton(button)
elseif event == "CURRENT_SPELL_CAST_CHANGED" then
BenillaSpellButton_UpdateSelection(button)
elseif event == "PET_BAR_UPDATE" then
- if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
+ if SpellBookFrame.bookType == BOOKTYPE_PET then
BenillaSpellButton_UpdateButton(button)
end
end
@@ -312,8 +317,8 @@
-- The ref's out-of-tab bounds fork collapses into the one SetChecked: IsCurrentCast
-- answers nil past the book's end already.
function BenillaSpellButton_UpdateSelection(button)
- local id = BenillaSpellBook_GetSpellID(button:GetID())
- button:SetChecked(IsCurrentCast(id, BenillaSpellBookFrame.bookType))
+ local id = SpellBook_GetSpellID(button:GetID())
+ button:SetChecked(IsCurrentCast(id, SpellBookFrame.bookType))
end
-- ref SpellButton_OnEnter (l.235-244): the engine's SetSpell — the byte-verified spell
@@ -322,12 +327,12 @@
-- verified showRank=0).
function BenillaSpellButton_OnEnter(button)
if not GameTooltip then return end -- engine tests load file subsets
- local id = BenillaSpellBook_GetSpellID(button:GetID())
- if not GetSpellName(id, BenillaSpellBookFrame.bookType) then
+ local id = SpellBook_GetSpellID(button:GetID())
+ if not GetSpellName(id, SpellBookFrame.bookType) then
return
end
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
- GameTooltip:SetSpell(id, BenillaSpellBookFrame.bookType)
+ GameTooltip:SetSpell(id, SpellBookFrame.bookType)
end
-- ref SpellButton_OnClick (l.263-290): the MacroFrame branch and the right-click pet
@@ -336,17 +341,22 @@
-- `id > MAX_SPELLS` guard is dropped too: GetSpellName/CastSpell/PickupSpell already
-- answer nil/no-op for any id past the book's real length, so the extra guard checks
-- nothing a MAX_SPELLS constant would add here.
- function BenillaSpellButton_OnClick(button, drag, mouseButton)
- local id = BenillaSpellBook_GetSpellID(button:GetID())
+ function SpellButton_OnClick(drag)
+ local button = this
+ -- The mouse button is the ambient `arg1`, exactly as the reference reads it (l.284) —
+ -- its XML passes nothing on OnClick and a bare `1` on the two drag slots. Only the
+ -- `drag` falsy arm below reaches it, so the drag slots never read a stale arg1.
+ local mouseButton = arg1
+ local id = SpellBook_GetSpellID(button:GetID())
button:SetChecked("false")
if drag then
- PickupSpell(id, BenillaSpellBookFrame.bookType)
+ PickupSpell(id, SpellBookFrame.bookType)
elseif not IsShiftKeyDown() and mouseButton and mouseButton ~= "LeftButton"
- and BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
+ and SpellBookFrame.bookType == BOOKTYPE_PET then
-- ref l.284-285: on the PET page a non-left click flips autocast instead of
-- casting. Shift still outranks it — the ref tests shift first, so a shift
-- right-click on a pet spell picks it up.
- ToggleSpellAutocast(id, BenillaSpellBookFrame.bookType)
+ ToggleSpellAutocast(id, SpellBookFrame.bookType)
elseif IsShiftKeyDown() then
-- ref SpellButton_OnClick l.271-283 (LIVE since decision 0983): with the macro
-- editor open a shift-click APPENDS `/cast [()]` to the body instead of
@@ -355,8 +365,8 @@
-- The `/cast` alias comes from the shipped strings (SLASH_CAST1), never typed here.
local macroFrame = getglobal("BenillaMacroFrame")
if macroFrame and macroFrame:IsVisible() then
- local spellName, subSpellName = GetSpellName(id, BenillaSpellBookFrame.bookType)
- if spellName and not IsSpellPassive(id, BenillaSpellBookFrame.bookType) then
+ local spellName, subSpellName = GetSpellName(id, SpellBookFrame.bookType)
+ if spellName and not IsSpellPassive(id, SpellBookFrame.bookType) then
local line = (SLASH_CAST1 or "/cast") .. " " .. spellName
if subSpellName and string.len(subSpellName) > 0 then
line = line .. "(" .. subSpellName .. ")"
@@ -364,10 +374,10 @@
BenillaMacroFrame_AddMacroLine(line)
end
else
- PickupSpell(id, BenillaSpellBookFrame.bookType)
+ PickupSpell(id, SpellBookFrame.bookType)
end
else
- CastSpell(id, BenillaSpellBookFrame.bookType)
+ CastSpell(id, SpellBookFrame.bookType)
BenillaSpellButton_UpdateSelection(button) -- ref l.288: the ring re-reads at once
end
end
@@ -383,15 +393,15 @@
if GameTooltip:IsOwned(button) then
BenillaSpellButton_OnEnter(button)
end
- if not BenillaSpellBookFrame.selectedSkillLine then
- BenillaSpellBookFrame.selectedSkillLine = 1
+ if not SpellBookFrame.selectedSkillLine then
+ SpellBookFrame.selectedSkillLine = 1
end
- local isPet = BenillaSpellBookFrame.bookType == BOOKTYPE_PET
- local temp, tabTexture, offset, numSpells = GetSpellTabInfo(BenillaSpellBookFrame.selectedSkillLine)
+ local isPet = SpellBookFrame.bookType == BOOKTYPE_PET
+ local temp, tabTexture, offset, numSpells = GetSpellTabInfo(SpellBookFrame.selectedSkillLine)
offset = offset or 0
numSpells = numSpells or 0
- BenillaSpellBookFrame.selectedSkillLineOffset = offset
- local id = BenillaSpellBook_GetSpellID(button:GetID())
+ SpellBookFrame.selectedSkillLineOffset = offset
+ local id = SpellBook_GetSpellID(button:GetID())
local name = button:GetName()
local iconTexture = getglobal(name .. "IconTexture")
local spellString = getglobal(name .. "SpellName")
@@ -416,7 +426,7 @@
else
button:Enable()
end
- local texture = GetSpellTexture(id, BenillaSpellBookFrame.bookType)
+ local texture = GetSpellTexture(id, SpellBookFrame.bookType)
local highlightTexture = getglobal(name .. "Highlight")
local normalTexture = getglobal(name .. "NormalTexture")
if (not texture) or (texture == "") then
@@ -434,7 +444,7 @@
-- The cooldown sweep + the enable-keyed icon dim (ref l.359-365): enable == 0 is an
-- on-hold cooldown (Stealth/Feign Death parked until SMSG_COOLDOWN_EVENT) — the
-- widget stays hidden (CooldownFrame_SetTimer's own gate) and the icon reads 40%.
- local start, duration, enable = GetSpellCooldown(id, BenillaSpellBookFrame.bookType)
+ local start, duration, enable = GetSpellCooldown(id, SpellBookFrame.bookType)
CooldownFrame_SetTimer(cooldown, start, duration, enable)
if enable == 1 then
iconTexture:SetVertexColor(1.0, 1.0, 1.0)
@@ -444,11 +454,11 @@
-- ref l.367-377: the autocast pair. `GetSpellAutocast` answers (nil, nil) for the
-- player book by the engine's own short-circuit, so this needs no isPet guard — the
-- overlay and the sparkle simply never come on outside the pet page.
- local autoCastAllowed, autoCastEnabled = GetSpellAutocast(id, BenillaSpellBookFrame.bookType)
+ local autoCastAllowed, autoCastEnabled = GetSpellAutocast(id, SpellBookFrame.bookType)
if autoCastAllowed then autocastable:Show() else autocastable:Hide() end
button.autocasting = autoCastEnabled
- local spellName, subSpellName = GetSpellName(id, BenillaSpellBookFrame.bookType)
- local isPassive = IsSpellPassive(id, BenillaSpellBookFrame.bookType)
+ local spellName, subSpellName = GetSpellName(id, SpellBookFrame.bookType)
+ local isPassive = IsSpellPassive(id, SpellBookFrame.bookType)
if isPassive then
normalTexture:SetVertexColor(0, 0, 0)
highlightTexture:SetTexture("Interface\\Buttons\\UI-PassiveHighlight")
@@ -479,47 +489,47 @@
-- reads `SPELLBOOK_PAGENUMBERS["pet"]` for the pet one — a key nothing writes. So on the
-- pet page the arrows turn nothing, the page text says a number the page does not honour,
-- and the *spell* book's page counter moves behind your back. All three are the reference's
- -- and all three are reproduced; `BenillaSpellBook_GetSpellID`'s note is the other half.
+ -- and all three are reproduced; `SpellBook_GetSpellID`'s note is the other half.
function BenillaSpellBookPrevPageButton_OnClick()
- local pageNum = BenillaSpellBook_GetCurrentPage() - 1
- SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] = pageNum
- BenillaSpellBook_UpdatePageArrows()
- BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
+ local pageNum = SpellBook_GetCurrentPage() - 1
+ SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] = pageNum
+ SpellBook_UpdatePageArrows()
+ SpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
BenillaUpdateSpells()
-- ref l.414-417: the pet page re-asserts its own title, because the spell book's page
-- text handler is shared and the title is not.
- if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
- BenillaSpellBookTitleText:SetText(BenillaSpellBookFrame.petTitle)
+ if SpellBookFrame.bookType == BOOKTYPE_PET then
+ SpellBookTitleText:SetText(SpellBookFrame.petTitle)
end
PlaySound("igAbiliityPageTurn")
end
function BenillaSpellBookNextPageButton_OnClick()
- local pageNum = BenillaSpellBook_GetCurrentPage() + 1
- SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] = pageNum
- BenillaSpellBook_UpdatePageArrows()
- BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
+ local pageNum = SpellBook_GetCurrentPage() + 1
+ SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] = pageNum
+ SpellBook_UpdatePageArrows()
+ SpellBookPageText:SetText(format(PAGE_NUMBER, pageNum))
BenillaUpdateSpells()
- if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
- BenillaSpellBookTitleText:SetText(BenillaSpellBookFrame.petTitle)
+ if SpellBookFrame.bookType == BOOKTYPE_PET then
+ SpellBookTitleText:SetText(SpellBookFrame.petTitle)
end
PlaySound("igAbiliityPageTurn")
end
-- ── The skill-line tabs (ref SpellBookSkillLineTab_OnClick, l.436-457) ───────────────────
- function BenillaSpellBookSkillLineTab_OnClick(id)
+ function SpellBookSkillLineTab_OnClick(id)
local update
if not id then
update = 1
id = this:GetID()
end
- BenillaSpellBookFrame.selectedSkillLine = id
+ SpellBookFrame.selectedSkillLine = id
local name, texture, offset, numSpells = GetSpellTabInfo(id)
- BenillaSpellBookFrame.selectedSkillLineOffset = offset or 0
- BenillaSpellBookFrame.selectedSkillLineNumSpells = numSpells or 0
- BenillaSpellBook_UpdatePageArrows()
- BenillaSpellBookFrame_Update()
- BenillaSpellBookPageText:SetText(format(PAGE_NUMBER, BenillaSpellBook_GetCurrentPage()))
+ SpellBookFrame.selectedSkillLineOffset = offset or 0
+ SpellBookFrame.selectedSkillLineNumSpells = numSpells or 0
+ SpellBook_UpdatePageArrows()
+ SpellBookFrame_Update()
+ SpellBookPageText:SetText(format(PAGE_NUMBER, SpellBook_GetCurrentPage()))
if update then
BenillaUpdateSpells()
end
@@ -533,26 +543,26 @@
-- always shows book entries 1..12 no matter what page number the window is displaying. A
-- well-trained hunter pet can know more than twelve spells, so this is visible, and it is
-- still the reference's behaviour. Its sibling quirk is in the two page-turn handlers.
- function BenillaSpellBook_GetSpellID(id)
- if BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
+ function SpellBook_GetSpellID(id)
+ if SpellBookFrame.bookType == BOOKTYPE_PET then
return id
end
- return id + (BenillaSpellBookFrame.selectedSkillLineOffset or 0)
- + (SPELLS_PER_PAGE * ((SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] or 1) - 1))
+ return id + (SpellBookFrame.selectedSkillLineOffset or 0)
+ + (SPELLS_PER_PAGE * ((SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] or 1) - 1))
end
-- ref SpellBook_UpdatePageArrows (l.467-479).
- function BenillaSpellBook_UpdatePageArrows()
- local currentPage, maxPages = BenillaSpellBook_GetCurrentPage()
+ function SpellBook_UpdatePageArrows()
+ local currentPage, maxPages = SpellBook_GetCurrentPage()
if currentPage == 1 then
- BenillaSpellBookPrevPageButton:Disable()
+ SpellBookPrevPageButton:Disable()
else
- BenillaSpellBookPrevPageButton:Enable()
+ SpellBookPrevPageButton:Enable()
end
if currentPage == maxPages then
- BenillaSpellBookNextPageButton:Disable()
+ SpellBookNextPageButton:Disable()
else
- BenillaSpellBookNextPageButton:Enable()
+ SpellBookNextPageButton:Enable()
end
end
@@ -564,15 +574,15 @@
-- The pet branch reads `SPELLBOOK_PAGENUMBERS["pet"]`, which is the counter NOTHING ever
-- writes — see the page-turn handlers. So it is always 1, and with it `maxPages > 1` the
-- Next arrow stays enabled forever on a big pet book. Reference behaviour; see the
- -- do-not-fix note on `BenillaSpellBook_GetSpellID`.
- function BenillaSpellBook_GetCurrentPage()
+ -- do-not-fix note on `SpellBook_GetSpellID`.
+ function SpellBook_GetCurrentPage()
local numPetSpells = HasPetSpells()
- if numPetSpells and BenillaSpellBookFrame.bookType == BOOKTYPE_PET then
+ if numPetSpells and SpellBookFrame.bookType == BOOKTYPE_PET then
return SPELLBOOK_PAGENUMBERS["pet"] or 1,
max(ceil(numPetSpells / SPELLS_PER_PAGE), 1)
end
- local currentPage = SPELLBOOK_PAGENUMBERS[BenillaSpellBookFrame.selectedSkillLine] or 1
- local temp, texture, offset, numSpells = GetSpellTabInfo(BenillaSpellBookFrame.selectedSkillLine)
+ local currentPage = SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] or 1
+ local temp, texture, offset, numSpells = GetSpellTabInfo(SpellBookFrame.selectedSkillLine)
local maxPages = max(ceil((numSpells or 0) / SPELLS_PER_PAGE), 1)
return currentPage, maxPages
end
@@ -601,7 +611,7 @@
- BenillaSpellBookSkillLineTab_OnClick()
+ SpellBookSkillLineTab_OnClick()
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
if self.tooltip then
@@ -671,14 +681,14 @@
- BenillaSpellButton_OnLoad(self)
- BenillaSpellButton_OnEvent(self)
- BenillaSpellButton_OnClick(self, nil, arg1)
+ SpellButton_OnLoad()
+ SpellButton_OnEvent(event)
+ SpellButton_OnClick()BenillaSpellButton_UpdateButton(self)
- BenillaSpellButton_OnClick(self, 1)
- BenillaSpellButton_OnClick(self, 1)
+ SpellButton_OnClick(1)
+ SpellButton_OnClick(1)BenillaSpellButton_OnEnter(self)GameTooltip:Hide()
@@ -726,7 +736,7 @@
-
+
@@ -762,12 +772,12 @@
-
+
-
+
@@ -779,53 +789,53 @@
-
+
- HideUIPanel(BenillaSpellBookFrame)
+ HideUIPanel(SpellBookFrame)
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -835,17 +845,17 @@
-
+
-
+
-
+
@@ -860,74 +870,74 @@
ref's own column-major pattern (col1 ids 1-6, col2 ids 7-12) — SpellBook_GetSpellID
reads a button's `id=` as its within-page/within-tab 1-based position, not its
screen order. -->
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
- BenillaSpellBookFrame_OnLoad(self)
- BenillaSpellBookFrame_OnEvent(self)
- BenillaSpellBookFrame_OnShow()
- BenillaSpellBookFrame_OnHide()
+ SpellBookFrame_OnLoad()
+ SpellBookFrame_OnEvent()
+ SpellBookFrame_OnShow()
+ SpellBookFrame_OnHide()BenillaSpellBookFrame_OnUpdate(self, arg1)
diff --git a/crates/benilla-app/assets/ui/StackSplit.xml b/crates/benilla-app/assets/ui/StackSplit.xml
index 6426b56a..704407a5 100644
--- a/crates/benilla-app/assets/ui/StackSplit.xml
+++ b/crates/benilla-app/assets/ui/StackSplit.xml
@@ -39,7 +39,7 @@
-- ContainerFrameItemButton_OnClick's own closure). maxStack < 2 hides instead of showing (a
-- 1-stack has nothing to split) — the ref's own guard.
function OpenStackSplitFrame(maxStack, owner, anchor, anchorTo)
- local frame = BenillaStackSplitFrame
+ local frame = StackSplitFrame
if frame.owner then
frame.owner.hasStackSplit = nil
end
@@ -51,9 +51,9 @@
frame.owner = owner
owner.hasStackSplit = true
frame.split = 1
- BenillaStackSplitText:SetText(frame.split)
- BenillaStackSplitLeftButton:Disable()
- BenillaStackSplitRightButton:Enable()
+ StackSplitText:SetText(frame.split)
+ StackSplitLeftButton:Disable()
+ StackSplitRightButton:Enable()
-- ref l.21-22: ClearAllPoints first — SetPoint only replaces an anchor of the SAME
-- point, and the anchor callers choose (BagFrame.xml passes "BOTTOMRIGHT") differs from
-- this frame's authored placeholder ("CENTER"), so skipping this would leave both live.
@@ -67,7 +67,7 @@
-- (no caller has a live reason to resize an open spinner today) — kept so a later caller
-- (the paper doll / bank slice) drops in without a new function.
function UpdateStackSplitFrame(maxStack)
- local frame = BenillaStackSplitFrame
+ local frame = StackSplitFrame
frame.maxStack = maxStack
if frame.maxStack < 2 then
if frame.owner then
@@ -78,44 +78,44 @@
end
if frame.split > frame.maxStack then
frame.split = frame.maxStack
- BenillaStackSplitText:SetText(frame.split)
+ StackSplitText:SetText(frame.split)
end
if frame.split == frame.maxStack then
- BenillaStackSplitRightButton:Disable()
+ StackSplitRightButton:Disable()
else
- BenillaStackSplitRightButton:Enable()
+ StackSplitRightButton:Enable()
end
if frame.split == 1 then
- BenillaStackSplitLeftButton:Disable()
+ StackSplitLeftButton:Disable()
else
- BenillaStackSplitLeftButton:Enable()
+ StackSplitLeftButton:Enable()
end
end
function BenillaStackSplitLeft_Click()
- local frame = BenillaStackSplitFrame
+ local frame = StackSplitFrame
if frame.split == 1 then return end
frame.split = frame.split - 1
- BenillaStackSplitText:SetText(frame.split)
+ StackSplitText:SetText(frame.split)
if frame.split == 1 then
- BenillaStackSplitLeftButton:Disable()
+ StackSplitLeftButton:Disable()
end
- BenillaStackSplitRightButton:Enable()
+ StackSplitRightButton:Enable()
end
function BenillaStackSplitRight_Click()
- local frame = BenillaStackSplitFrame
+ local frame = StackSplitFrame
if frame.split == frame.maxStack then return end
frame.split = frame.split + 1
- BenillaStackSplitText:SetText(frame.split)
+ StackSplitText:SetText(frame.split)
if frame.split == frame.maxStack then
- BenillaStackSplitRightButton:Disable()
+ StackSplitRightButton:Disable()
end
- BenillaStackSplitLeftButton:Enable()
+ StackSplitLeftButton:Enable()
end
function BenillaStackSplitOkay_Click()
- local frame = BenillaStackSplitFrame
+ local frame = StackSplitFrame
if frame.owner then
frame.owner.SplitStack(frame.owner, frame.split)
end
@@ -123,7 +123,7 @@
end
function BenillaStackSplitCancel_Click()
- BenillaStackSplitFrame:Hide()
+ StackSplitFrame:Hide()
end
-- OnHide (ref l.158-162): ANY hide — Okay, Cancel, or ToggleGameMenu's chain hook below —
@@ -131,8 +131,8 @@
-- l.35-39, checks `this.hasStackSplit` to auto-close the spinner if the SLOT itself goes away
-- mid-split — no consumer needs that half here, but the flag's own clear is this function's
-- job regardless of which button caused the hide).
- function BenillaStackSplitFrame_OnHide()
- local frame = BenillaStackSplitFrame
+ function StackSplitFrame_OnHide()
+ local frame = StackSplitFrame
if frame.owner then
frame.owner.hasStackSplit = nil
end
@@ -152,7 +152,7 @@
-
+
@@ -167,7 +167,7 @@
-
+
@@ -176,7 +176,7 @@
-
+
@@ -188,7 +188,7 @@
BenillaStackSplitLeft_Click()
-
+
@@ -201,7 +201,7 @@
-
+
@@ -210,7 +210,7 @@
-
+
@@ -220,7 +220,7 @@
- BenillaStackSplitFrame_OnHide()
+ StackSplitFrame_OnHide()
diff --git a/crates/benilla-app/assets/ui/StanceBar.xml b/crates/benilla-app/assets/ui/StanceBar.xml
index 58f43bed..75e4dbae 100644
--- a/crates/benilla-app/assets/ui/StanceBar.xml
+++ b/crates/benilla-app/assets/ui/StanceBar.xml
@@ -78,15 +78,15 @@
-- bar to the form count (hide the whole frame at 0 — a formless class never shows it),
-- then paint each button — icon, checked (active), the 1.0/0.4 castable tint (ref l.210-214),
-- cooldown swipe (CooldownFrame_SetTimer, Cooldown.xml's shared transcription).
- function BenillaShapeshiftBar_Update()
+ function ShapeshiftBar_Update()
local numForms = GetNumShapeshiftForms()
if numForms > 0 then
- BenillaShapeshiftBarFrame:Show()
+ ShapeshiftBarFrame:Show()
else
- BenillaShapeshiftBarFrame:Hide()
+ ShapeshiftBarFrame:Hide()
end
for i = 1, NUM_SHAPESHIFT_SLOTS do
- local button = getglobal("BenillaShapeshiftButton" .. i)
+ local button = getglobal("ShapeshiftButton" .. i)
if i <= numForms then
local texture, name, isActive, isCastable = GetShapeshiftFormInfo(i)
button.icon:SetTexture(texture or BENILLA_FALLBACK_ICON)
@@ -109,14 +109,16 @@
end
end
- function BenillaShapeshiftBar_OnLoad(frame)
+ function ShapeshiftBar_OnLoad()
+ local frame = this
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("UPDATE_SHAPESHIFT_FORMS")
- BenillaShapeshiftBar_Update()
+ ShapeshiftBar_Update()
end
- function BenillaShapeshiftBar_OnEvent(frame)
- BenillaShapeshiftBar_Update()
+ function ShapeshiftBar_OnEvent()
+ local frame = this
+ ShapeshiftBar_Update()
end
]]>
@@ -157,56 +159,56 @@
-
+
-
+
-
+ BenillaShapeshiftButton_OnLoad(self, 1)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 2)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 3)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 4)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 5)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 6)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 7)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 8)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 9)
-
-
+
+ BenillaShapeshiftButton_OnLoad(self, 10)
- BenillaShapeshiftBar_OnLoad(self)
- BenillaShapeshiftBar_OnEvent(self)
+ ShapeshiftBar_OnLoad()
+ ShapeshiftBar_OnEvent()
diff --git a/crates/benilla-app/assets/ui/TalentFrame.xml b/crates/benilla-app/assets/ui/TalentFrame.xml
index 35c1cd76..da9f2396 100644
--- a/crates/benilla-app/assets/ui/TalentFrame.xml
+++ b/crates/benilla-app/assets/ui/TalentFrame.xml
@@ -147,8 +147,8 @@
- Bare `min`/`max`/`select` (used verbatim in `_DrawLines`/`_SetPrereqs` below): CONFIRMED
available — `min`/`max` are house-stdlib aliases for `math.min`/`math.max`
(`benilla-ui/src/script/stdlib.rs`), `select` is stock Lua 5.1. The ref's own `arg`/`arg.n`
- Lua-5.0 vararg table (`_SetPrereqs`) has no equivalent here — `LUA_COMPAT_VARARG` isn't
- shipped (WorldMapFrame.xml's own identical note) — replaced with `select("#", ...)`/
+ Lua-5.0 vararg table (`_SetPrereqs`) is written the 5.1-native way
+ (WorldMapFrame.xml's own identical note) — replaced with `select("#", ...)`/
`select(i, ...)` over a trailing `...`, WorldMapFrame.xml's own precedent for the identical
substitution.
- `GetTalentTabInfo` called TWICE in a row by the ref's own `TalentFrame_Update` (l.152-154:
@@ -1049,7 +1049,7 @@
diff --git a/crates/benilla-app/assets/ui/TaxiFrame.xml b/crates/benilla-app/assets/ui/TaxiFrame.xml
index 5665bcc7..da4d5ca5 100644
--- a/crates/benilla-app/assets/ui/TaxiFrame.xml
+++ b/crates/benilla-app/assets/ui/TaxiFrame.xml
@@ -29,7 +29,7 @@
demand. `BENILLA_TAXI_MAX_NODES = 50` below comfortably covers the real 5875
`TaxiNodes.dbc`'s per-continent max (43 nodes on Eastern Kingdoms, 39 on Kalimdor —
counted this session, `crate::ui_taxi`'s module doc). The route-line textures stay
- genuinely dynamic (`BenillaTaxiRouteMap:CreateTexture(...)`, ref l.101/142) — plain
+ genuinely dynamic (`TaxiRouteMap:CreateTexture(...)`, ref l.101/142) — plain
region creation carries no template/inherits, so THAT half of the reference runs
completely unmodified.
2. THE TITLE READS AN EVENT ARG, not `UnitName("npc")` (ref TaxiFrame.lua:27). This
@@ -41,13 +41,13 @@
3. THE "NO SINGLE-HOP DESTINATION" REFUSAL (`DrawOneHopLines`, ref l.161-164) calls
`BenillaErrorsFrame_AddMessage` (`ErrorsFrame.xml`'s real Lua-callable seam, byte-exact
`ERR_TAXINOPATHS` text below) rather than the reference's `UIErrorsFrame:AddMessage` —
- this engine's error toast is `BenillaErrorsFrame`, not an intrinsic `MessageFrame`
+ this engine's error toast is `UIErrorsFrame`, not an intrinsic `MessageFrame`
object, so the ref's OO call becomes the plain function call. `ErrorsFrame.xml` loads
well before this file in the shipped list (`ui_script/mod.rs`), so the global already
exists by the time this ever runs.
`` (an intrinsic type this engine doesn't have) becomes a plain ``
- (`BenillaTaxiRouteMap`) per the house rule for every non-shipped intrinsic (decision 0246's
+ (`TaxiRouteMap`) per the house rule for every non-shipped intrinsic (decision 0246's
patch-chain rule; the `Cooldown`/`GameTooltip`/etc. precedents) — it carries no behavior of
its own beyond being a Frame that owns child regions/buttons, so the plain type loses
nothing.
@@ -86,7 +86,8 @@
-- (U+2019, "don\226\128\153t"), not a straight one.
local ERR_TAXINOPATHS = "You don\226\128\153t know any flight locations connected to this one."
- function BenillaTaxiFrame_OnLoad(frame)
+ function TaxiFrame_OnLoad()
+ local frame = this
-- The "npc" portrait token (decision 0421): the open taxi session's flight master
-- bakes into the ring, resolved exactly like Gossip/Merchant/Trainer's own OnLoad.
SetPortraitTexture(getglobal(frame:GetName() .. "Portrait"), "npc")
@@ -97,29 +98,30 @@
-- Deviation 2 above: the flight master's name rides TAXIMAP_OPENED's arg1 (the app
-- resolves it through the NameCache) rather than a live UnitName("npc") read.
function BenillaTaxiFrame_SetTitle(name)
- getglobal("BenillaTaxiNameText"):SetText(name or "")
+ getglobal("TaxiNameText"):SetText(name or "")
end
-- ref TaxiFrame.lua:24-80, restructured only at the node-button loop (deviation 1 — a
-- fixed pool walked 1..BENILLA_TAXI_MAX_NODES instead of grown via CreateFrame).
- function BenillaTaxiFrame_OnEvent(frame)
+ function TaxiFrame_OnEvent(event)
+ local frame = this
if event == "TAXIMAP_OPENED" then
BenillaTaxiFrame_SetTitle(arg1)
-- Set the texture coords on the map, then hand the region to the app's art pick.
- BenillaTaxiMap:SetTexCoord(0, 1, 0, 1)
- SetTaxiMap(BenillaTaxiMap)
+ TaxiMap:SetTexCoord(0, 1, 0, 1)
+ SetTaxiMap(TaxiMap)
-- Draw nodes: every pool slot up to the pushed count shows its type's icon at its
-- pushed position; the rest hide (ref l.44-63, the static-pool shape).
local num_nodes = NumTaxiNodes()
for index = 1, BENILLA_TAXI_MAX_NODES do
- local button = getglobal("BenillaTaxiButton" .. index)
+ local button = getglobal("TaxiButton" .. index)
local ntype = TaxiNodeGetType(index)
if index <= num_nodes and ntype ~= "NONE" then
local x, y = TaxiNodePosition(button:GetID())
button:ClearAllPoints()
- button:SetPoint("CENTER", "BenillaTaxiMap", "BOTTOMLEFT", x * TAXI_MAP_WIDTH, y * TAXI_MAP_HEIGHT)
+ button:SetPoint("CENTER", "TaxiMap", "BOTTOMLEFT", x * TAXI_MAP_WIDTH, y * TAXI_MAP_HEIGHT)
button:SetNormalTexture(TaxiButtonTypes[ntype].file)
button:Show()
else
@@ -153,8 +155,8 @@
local numRoutes = GetNumRoutes(index)
local line
local sX, sY, dX, dY
- local w = BenillaTaxiRouteMap:GetWidth()
- local h = BenillaTaxiRouteMap:GetHeight()
+ local w = TaxiRouteMap:GetWidth()
+ local h = TaxiRouteMap:GetHeight()
local ntype = TaxiNodeGetType(index)
if ntype == "REACHABLE" then
@@ -163,7 +165,7 @@
if numRoutes > NUM_TAXI_ROUTES then
for i = NUM_TAXI_ROUTES + 1, numRoutes do
- line = BenillaTaxiRouteMap:CreateTexture("TaxiRoute" .. i, "BACKGROUND")
+ line = TaxiRouteMap:CreateTexture("TaxiRoute" .. i, "BACKGROUND")
line:SetTexture("Interface\\TaxiFrame\\UI-Taxi-Line")
end
NUM_TAXI_ROUTES = numRoutes
@@ -176,7 +178,7 @@
sY = TaxiGetSrcY(index, i) * h
dX = TaxiGetDestX(index, i) * w
dY = TaxiGetDestY(index, i) * h
- DrawRouteLine(line, "BenillaTaxiRouteMap", sX, sY, dX, dY, 32)
+ DrawRouteLine(line, "TaxiRouteMap", sX, sY, dX, dY, 32)
line:Show()
else
line:Hide()
@@ -193,14 +195,14 @@
NUM_TAXI_ROUTES = 0
-- Draw all flightpaths within one hop of current location — ref TaxiFrame.lua:129-165,
- -- verbatim except the canvas name ("TaxiRouteMap" -> "BenillaTaxiRouteMap") and the
+ -- verbatim except the canvas name ("TaxiRouteMap" -> "TaxiRouteMap") and the
-- no-single-hop refusal (deviation 3 above: BenillaErrorsFrame_AddMessage, not
-- UIErrorsFrame:AddMessage).
function DrawOneHopLines()
local line
local sX, sY, dX, dY
- local w = BenillaTaxiRouteMap:GetWidth()
- local h = BenillaTaxiRouteMap:GetHeight()
+ local w = TaxiRouteMap:GetWidth()
+ local h = TaxiRouteMap:GetHeight()
local numNodes = NumTaxiNodes()
local numLines = 0
local numSingleHops = 0
@@ -209,7 +211,7 @@
numSingleHops = numSingleHops + 1
numLines = numLines + 1
if numLines > NUM_TAXI_ROUTES then
- line = BenillaTaxiRouteMap:CreateTexture("TaxiRoute" .. numLines, "BACKGROUND")
+ line = TaxiRouteMap:CreateTexture("TaxiRoute" .. numLines, "BACKGROUND")
line:SetTexture("Interface\\TaxiFrame\\UI-Taxi-Line")
NUM_TAXI_ROUTES = numLines
else
@@ -220,7 +222,7 @@
sY = TaxiGetSrcY(i, 1) * h
dX = TaxiGetDestX(i, 1) * w
dY = TaxiGetDestY(i, 1) * h
- DrawRouteLine(line, "BenillaTaxiRouteMap", sX, sY, dX, dY, 32)
+ DrawRouteLine(line, "TaxiRouteMap", sX, sY, dX, dY, 32)
line:Show()
end
end
@@ -230,7 +232,7 @@
end
if numSingleHops == 0 then
BenillaErrorsFrame_AddMessage(ERR_TAXINOPATHS, 1.0, 0.1, 0.1)
- HideUIPanel(BenillaTaxiFrame)
+ HideUIPanel(TaxiFrame)
end
end
@@ -303,7 +305,7 @@
-- vanilla's client-side close sends no packet; the app clears TaxiState on this
-- intent (which fires TAXIMAP_CLOSED), and we hide immediately so there's no lag.
CloseTaxiMap()
- HideUIPanel(BenillaTaxiFrame)
+ HideUIPanel(TaxiFrame)
end
]]>
@@ -347,7 +349,7 @@
-
+
@@ -384,7 +386,7 @@
-
+
@@ -394,7 +396,7 @@
-
+
@@ -406,7 +408,7 @@
-
+
@@ -419,73 +421,73 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- BenillaTaxiFrame_OnLoad(self)
+ TaxiFrame_OnLoad()
- BenillaTaxiFrame_OnEvent(self)
+ TaxiFrame_OnEvent(event)
diff --git a/crates/benilla-app/assets/ui/TradeFrame.xml b/crates/benilla-app/assets/ui/TradeFrame.xml
index 32188c12..46aedcdd 100644
--- a/crates/benilla-app/assets/ui/TradeFrame.xml
+++ b/crates/benilla-app/assets/ui/TradeFrame.xml
@@ -24,7 +24,8 @@
BENILLA_MAX_TRADE_ITEMS = 7 -- MAX_TRADE_ITEMS (ref TradeFrame.lua l.1)
-- ── Load / event dispatch (ref TradeFrame_OnLoad l.4 / TradeFrame_OnEvent l.18) ───────────
- function BenillaTradeFrame_OnLoad(frame)
+ function TradeFrame_OnLoad()
+ local frame = this
frame:RegisterEvent("TRADE_SHOW")
frame:RegisterEvent("TRADE_UPDATE")
frame:RegisterEvent("TRADE_CLOSED")
@@ -37,71 +38,72 @@
frame:RegisterEvent("PLAYER_TRADE_MONEY")
end
- function BenillaTradeFrame_OnEvent(frame)
+ function TradeFrame_OnEvent()
+ local frame = this
if event == "TRADE_SHOW" or event == "TRADE_UPDATE" then
ShowUIPanel(frame)
if not frame:IsVisible() then
CloseTrade()
return
end
- BenillaTradeFrameTradeButton:Enable()
- BenillaTradeFrame_Update()
+ TradeFrameTradeButton:Enable()
+ TradeFrame_Update()
elseif event == "PLAYER_TRADE_MONEY" then
- BenillaMoneyInput_SetCopper("BenillaTradePlayerInputMoney", GetPlayerTradeMoney())
+ BenillaMoneyInput_SetCopper("TradePlayerInputMoney", GetPlayerTradeMoney())
elseif event == "TRADE_CLOSED" then
HideUIPanel(frame)
elseif event == "TRADE_PLAYER_ITEM_CHANGED" then
- BenillaTradeFrame_UpdatePlayerItem(arg1)
+ TradeFrame_UpdatePlayerItem(arg1)
elseif event == "TRADE_TARGET_ITEM_CHANGED" then
- BenillaTradeFrame_UpdateTargetItem(arg1)
+ TradeFrame_UpdateTargetItem(arg1)
elseif event == "TRADE_ACCEPT_UPDATE" then
- BenillaTradeFrame_SetAcceptState(arg1, arg2)
+ TradeFrame_SetAcceptState(arg1, arg2)
end
end
- function BenillaTradeFrame_OnShow()
- BenillaTradeFrame.acceptState = 0
+ function TradeFrame_OnShow()
+ TradeFrame.acceptState = 0
end
- function BenillaTradeFrame_OnHide()
+ function TradeFrame_OnHide()
-- The client-side close verb (ref TradeFrame_OnHide l.144) → CMSG_CANCEL_TRADE + a local
-- clear (the app's drain); clear our money entry so a later trade opens fresh (ref l.146).
- BenillaMoneyInput_ResetMoney("BenillaTradePlayerInputMoney")
+ BenillaMoneyInput_ResetMoney("TradePlayerInputMoney")
CloseTrade()
end
-- A keystroke in our money input (ref TradeFrame_UpdateMoney l.162): read the copper total,
-- clamp to what we actually carry, and offer it → SetTradeMoney → CMSG_SET_TRADE_GOLD.
- function BenillaTradeFrame_UpdateMoney()
- local copper = BenillaMoneyInput_GetCopper("BenillaTradePlayerInputMoney")
+ function TradeFrame_UpdateMoney()
+ local copper = BenillaMoneyInput_GetCopper("TradePlayerInputMoney")
if GetMoney and copper > GetMoney() then
copper = GetPlayerTradeMoney()
- BenillaMoneyInput_SetCopper("BenillaTradePlayerInputMoney", copper)
+ BenillaMoneyInput_SetCopper("TradePlayerInputMoney", copper)
end
SetTradeMoney(copper)
end
-- ── Full repaint (ref TradeFrame_Update l.39): portraits, names, all 7 slots each side, the
-- two money displays, the accept highlights hidden. ────────────────────────────────────
- function BenillaTradeFrame_Update()
- SetPortraitTexture(BenillaTradeFramePlayerPortrait, "player")
+ function TradeFrame_Update()
+ SetPortraitTexture(TradeFramePlayerPortrait, "player")
-- The partner rides the shared "npc" portrait booth (the app points it at the partner
-- entity, decision 0592); the name comes from GetTradePartnerName() in place of the
-- reference's UnitName("NPC").
- SetPortraitTexture(BenillaTradeFrameRecipientPortrait, "npc")
- BenillaTradeFramePlayerNameText:SetText(UnitName("player") or "")
- BenillaTradeFrameRecipientNameText:SetText(GetTradePartnerName() or "")
+ SetPortraitTexture(TradeFrameRecipientPortrait, "npc")
+ TradeFramePlayerNameText:SetText(UnitName("player") or "")
+ TradeFrameRecipientNameText:SetText(GetTradePartnerName() or "")
for i = 1, BENILLA_MAX_TRADE_ITEMS do
- BenillaTradeFrame_UpdatePlayerItem(i)
- BenillaTradeFrame_UpdateTargetItem(i)
+ TradeFrame_UpdatePlayerItem(i)
+ TradeFrame_UpdateTargetItem(i)
end
-- Our gold is the editable input, reflected via PLAYER_TRADE_MONEY (not repainted here, so
-- the repaint never clobbers mid-typing); only the partner's read-only gold repaints.
- BenillaMoney_Set("BenillaTradeRecipientMoneyFrame", GetTargetTradeMoney())
- BenillaTradeHighlightPlayer:Hide()
- BenillaTradeHighlightRecipient:Hide()
- BenillaTradeHighlightPlayerEnchant:Hide()
- BenillaTradeHighlightRecipientEnchant:Hide()
+ BenillaMoney_Set("TradeRecipientMoneyFrame", GetTargetTradeMoney())
+ TradeHighlightPlayer:Hide()
+ TradeHighlightRecipient:Hide()
+ TradeHighlightPlayerEnchant:Hide()
+ TradeHighlightRecipientEnchant:Hide()
end
-- One slot's paint, shared by both columns (ref TradeFrame_UpdatePlayerItem l.54 /
@@ -152,14 +154,14 @@
end
end
- function BenillaTradeFrame_UpdatePlayerItem(id)
+ function TradeFrame_UpdatePlayerItem(id)
local name, texture, numItems, isUsable, enchantment = GetTradePlayerItemInfo(id)
- BenillaTradeSlot_SetItem("BenillaTradePlayerItem"..id, id, name, texture, numItems, isUsable, enchantment, nil)
+ BenillaTradeSlot_SetItem("TradePlayerItem"..id, id, name, texture, numItems, isUsable, enchantment, nil)
end
- function BenillaTradeFrame_UpdateTargetItem(id)
+ function TradeFrame_UpdateTargetItem(id)
local name, texture, numItems, quality, isUsable, enchantment = GetTradeTargetItemInfo(id)
- BenillaTradeSlot_SetItem("BenillaTradeRecipientItem"..id, id, name, texture, numItems, isUsable, enchantment, 1)
+ BenillaTradeSlot_SetItem("TradeRecipientItem"..id, id, name, texture, numItems, isUsable, enchantment, 1)
end
-- A trade slot's item button as a cursor drop target (decision 0592 P2, ref ClickTradeButton /
@@ -178,33 +180,33 @@
-- The accept glow (ref TradeFrame_SetAcceptState l.116): each side's column + enchant-slot
-- highlight tracks who has pressed Trade; our own accept disables the Trade button.
- function BenillaTradeFrame_SetAcceptState(playerState, targetState)
- BenillaTradeFrame.acceptState = playerState
+ function TradeFrame_SetAcceptState(playerState, targetState)
+ TradeFrame.acceptState = playerState
if playerState == 1 then
- BenillaTradeHighlightPlayer:Show()
- BenillaTradeHighlightPlayerEnchant:Show()
- BenillaTradeFrameTradeButton:Disable()
+ TradeHighlightPlayer:Show()
+ TradeHighlightPlayerEnchant:Show()
+ TradeFrameTradeButton:Disable()
else
- BenillaTradeHighlightPlayer:Hide()
- BenillaTradeHighlightPlayerEnchant:Hide()
- BenillaTradeFrameTradeButton:Enable()
+ TradeHighlightPlayer:Hide()
+ TradeHighlightPlayerEnchant:Hide()
+ TradeFrameTradeButton:Enable()
end
if targetState == 1 then
- BenillaTradeHighlightRecipient:Show()
- BenillaTradeHighlightRecipientEnchant:Show()
+ TradeHighlightRecipient:Show()
+ TradeHighlightRecipientEnchant:Show()
else
- BenillaTradeHighlightRecipient:Hide()
- BenillaTradeHighlightRecipientEnchant:Hide()
+ TradeHighlightRecipient:Hide()
+ TradeHighlightRecipientEnchant:Hide()
end
end
-- Cancel (ref TradeFrameCancelButton_OnClick l.136): with our accept up, drop it (stay in the
-- trade); otherwise close the window (→ OnHide → CloseTrade).
- function BenillaTradeFrameCancelButton_OnClick()
- if BenillaTradeFrame.acceptState == 1 then
+ function TradeFrameCancelButton_OnClick()
+ if TradeFrame.acceptState == 1 then
CancelTradeAccept()
else
- HideUIPanel(BenillaTradeFrame)
+ HideUIPanel(TradeFrame)
end
end
]]>
@@ -304,18 +306,18 @@
-
+
-
+
-
+
@@ -338,92 +340,92 @@
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
@@ -432,32 +434,32 @@
AcceptTrade()
-
+
-
+
- BenillaTradeFrameCancelButton_OnClick()
+ TradeFrameCancelButton_OnClick()
-
+
-
+
- HideUIPanel(BenillaTradeFrame)
+ HideUIPanel(TradeFrame)
-
+
@@ -476,7 +478,7 @@
getglobal(this:GetParent():GetName().."Silver"):SetFocus()getglobal(this:GetParent():GetName().."Silver"):SetFocus()this:ClearFocus()
- if ( BenillaMoneyInput_Consume("BenillaTradePlayerInputMoney") ) then BenillaTradeFrame_UpdateMoney() end
+ if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() endthis:HighlightText(0, 0)this:HighlightText()
@@ -497,7 +499,7 @@
getglobal(this:GetParent():GetName().."Copper"):SetFocus()getglobal(this:GetParent():GetName().."Copper"):SetFocus()this:ClearFocus()
- if ( BenillaMoneyInput_Consume("BenillaTradePlayerInputMoney") ) then BenillaTradeFrame_UpdateMoney() end
+ if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() endthis:HighlightText(0, 0)this:HighlightText()
@@ -516,7 +518,7 @@
this:ClearFocus()
- if ( BenillaMoneyInput_Consume("BenillaTradePlayerInputMoney") ) then BenillaTradeFrame_UpdateMoney() end
+ if ( BenillaMoneyInput_Consume("TradePlayerInputMoney") ) then TradeFrame_UpdateMoney() endthis:HighlightText(0, 0)this:HighlightText()
@@ -525,7 +527,7 @@
-
+
@@ -548,10 +550,10 @@
- BenillaTradeFrame_OnLoad(self)
- BenillaTradeFrame_OnEvent(self)
- BenillaTradeFrame_OnShow(); PlaySound("igCharacterInfoOpen")
- BenillaTradeFrame_OnHide(); PlaySound("igCharacterInfoClose")
+ TradeFrame_OnLoad()
+ TradeFrame_OnEvent()
+ TradeFrame_OnShow(); PlaySound("igCharacterInfoOpen")
+ TradeFrame_OnHide(); PlaySound("igCharacterInfoClose")
diff --git a/crates/benilla-app/assets/ui/TradeSkillFrame.xml b/crates/benilla-app/assets/ui/TradeSkillFrame.xml
index a2f2ca6f..09520b1f 100644
--- a/crates/benilla-app/assets/ui/TradeSkillFrame.xml
+++ b/crates/benilla-app/assets/ui/TradeSkillFrame.xml
@@ -24,7 +24,7 @@
token those NPC-session windows use), the UIPanel slot system (carries NO hardcoded
— ShowUIPanel/SetLeftFrame SetPoints it), OnShow/OnHide PlaySound pair.
- TrainerFrame.xml — the FLAT layout constraint (below), the faux-scroll list kit
- (BenillaFauxScrollFrameTemplate, ScrollTemplates.xml), the locally-reconstructed
+ (FauxScrollFrameTemplate, ScrollTemplates.xml), the locally-reconstructed
UIPanelButtonTemplate/close-button/item-button idioms (no such templates ship in this house
dialect — TalentFrame.xml's own ENGINE API note), the persistent selection-highlight Frame.
@@ -127,8 +127,8 @@
`GetText("*_ABBR", nil, n)` locale-pluralization dispatch collapses to English
singular/plural directly — `UiPanels.xml`'s `StaticPopupTimeUnit` is the identical-shaped
precedent for this SAME `GetText` call elsewhere in the reference.
- - Lua-5.0 `arg`/`arg.n` varargs have no equivalent (`LUA_COMPAT_VARARG` isn't shipped,
- WorldMapFrame.xml/TalentFrame.xml's own identical note) — `BuildColoredListString`'s
+ - Lua-5.0 `arg`/`arg.n` varargs are written the 5.1-native way
+ (WorldMapFrame.xml/TalentFrame.xml's own identical note) — `BuildColoredListString`'s
varargs use `select("#", ...)`/`select(i, ...)`, their own precedent for the
substitution; the new `TradeSkillFilterFrame_LoadSubClasses`/`_LoadInvSlots` (ref l.331/
377) use the same substitution.
@@ -412,8 +412,8 @@
end
local scrollFrame = getglobal("BenillaTradeSkillListScrollFrame")
- BenillaFauxScrollFrame_Update(scrollFrame, numTradeSkills, TRADE_SKILLS_DISPLAYED, TRADE_SKILL_HEIGHT)
- local skillOffset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
+ FauxScrollFrame_Update(scrollFrame, numTradeSkills, TRADE_SKILLS_DISPLAYED, TRADE_SKILL_HEIGHT)
+ local skillOffset = FauxScrollFrame_GetOffset(scrollFrame)
local highlight = getglobal("BenillaTradeSkillHighlight")
highlight:Hide()
@@ -545,7 +545,7 @@
-- doc), using the button's own `.collapsed` flag (re-derived every Update() from the real
-- header states above, so it stays honest regardless of how a fold happened). The ref's raw
-- `TradeSkillListScrollFrameScrollBar:SetValue(0)` becomes this file's own
- -- `BenillaFauxScrollFrame_SetOffset(...,0)` wrapper (already this file's own Show/OnEvent
+ -- `FauxScrollFrame_SetOffset(...,0)` wrapper (already this file's own Show/OnEvent
-- convention). No direct `BenillaTradeSkillFrame_Update()` call — the ref doesn't call one
-- either; `CollapseTradeSkillSubClass`/`ExpandTradeSkillSubClass` raise the engine's touched
-- flag, answered with `TRADE_SKILL_UPDATE` the same frame (header comment, deviation 1). ──────
@@ -555,7 +555,7 @@
ExpandTradeSkillSubClass(0)
else
self.collapsed = true
- BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
+ FauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
CollapseTradeSkillSubClass(0)
end
end
@@ -781,7 +781,7 @@
else
BenillaTradeSkillFrame_SetSelection(GetTradeSkillSelectionIndex())
end
- BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
+ FauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
SetPortraitTexture(getglobal("BenillaTradeSkillFramePortrait"), "player")
BenillaTradeSkillFrame_Update()
ShowUIPanel(frame)
@@ -797,7 +797,7 @@
BenillaTradeSkillFrame_SetSelection(selection)
elseif GetNumTradeSkills() > 0 then
BenillaTradeSkillFrame_SetSelection(GetFirstTradeSkill())
- BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
+ FauxScrollFrame_SetOffset(getglobal("BenillaTradeSkillListScrollFrame"), 0)
end
BenillaTradeSkillFrame_Update()
elseif event == "TRADE_SKILL_CLOSE" then
@@ -1131,11 +1131,11 @@
-
-
+
diff --git a/crates/benilla-app/assets/ui/TrainerFrame.xml b/crates/benilla-app/assets/ui/TrainerFrame.xml
index db134d71..742d38bd 100644
--- a/crates/benilla-app/assets/ui/TrainerFrame.xml
+++ b/crates/benilla-app/assets/ui/TrainerFrame.xml
@@ -31,7 +31,7 @@
the shipped 1.12.1 Blizzard_TrainerUI.lua:6-9's own values — restored from
benilla/saved-variables.lua at load, and pushed into the engine on show because the list
builder resets its mask on every packet.
- - THE SCROLL BAR IS REAL. BenillaTrainerListScrollFrame (BenillaFauxScrollFrameTemplate): a
+ - THE SCROLL BAR IS REAL. BenillaTrainerListScrollFrame (FauxScrollFrameTemplate): a
draggable Slider bar + up/down step arrows over the fixed 11 rows (the faux-scroll re-binds
rows to a moving offset — it never really scrolls a child). The wheel still lives on the
WINDOW (the engine bubbles a spin up the parent chain, so a spin over a row reaches here).
@@ -258,8 +258,8 @@
-- The faux-scroll bar reconciles to the row count, then hands back the top-row offset.
local scrollFrame = getglobal("BenillaTrainerListScrollFrame")
- BenillaFauxScrollFrame_Update(scrollFrame, numServices, BENILLA_TRAINER_ROWS, BENILLA_TRAINER_ROW_HEIGHT)
- local skillOffset = BenillaFauxScrollFrame_GetOffset(scrollFrame)
+ FauxScrollFrame_Update(scrollFrame, numServices, BENILLA_TRAINER_ROWS, BENILLA_TRAINER_ROW_HEIGHT)
+ local skillOffset = FauxScrollFrame_GetOffset(scrollFrame)
getglobal("BenillaTrainerSkillHighlight"):Hide()
@@ -445,7 +445,7 @@
end
setglobal("TRAINER_FILTER_" .. strupper(this.value), on)
SetTrainerServiceTypeFilter(this.value, on)
- BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
+ FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
BenillaTrainerFrame_Update()
end
@@ -459,7 +459,7 @@
ExpandTrainerSkillLine(0)
else
b.collapsed = 1
- BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
+ FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
CollapseTrainerSkillLine(0)
end
BenillaTrainerFrame_Update()
@@ -503,7 +503,7 @@
-- Fresh session: top of the list, a first selection (ref ClassTrainerFrame_Show).
BenillaTrainerFrame_ApplyFilter()
BenillaTrainerFrame_SetTitle(arg1)
- BenillaFauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
+ FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
if GetTrainerSelectionIndex() == 0 then
selectFirstService()
end
@@ -750,11 +750,11 @@
-
-
+
diff --git a/crates/benilla-app/assets/ui/UIDropDownMenu.xml b/crates/benilla-app/assets/ui/UIDropDownMenu.xml
index b381c681..9d328c3e 100644
--- a/crates/benilla-app/assets/ui/UIDropDownMenu.xml
+++ b/crates/benilla-app/assets/ui/UIDropDownMenu.xml
@@ -15,9 +15,17 @@
CloseDropDownMenus(level), Disable/EnableButton, SetButtonText, SetAnchor, JustifyText,
GetCurrentDropDown, the 2-second hover-away auto-close counting through the level chain.
+ THE COLOR SWATCH ROW came in with the colour picker (ColorPickerFrame.xml): the full
+ `hasColorSwatch` / `r` / `g` / `b` / `swatchFunc` / `hasOpacity` / `opacity` / `opacityFunc` /
+ `cancelFunc` field set, the `$parentColorSwatch` button on the row template, and
+ `UIDropDownMenuButton_OpenColorPicker` (ref UIDropDownMenu.lua l.801-815). It is the second half
+ of one feature, not a separate one: `hasColorSwatch` draws the square, clicking the square is
+ the ONLY way the reference's colour picker ever opens, and `hasColorSwatch` is **145 files /
+ 82 corpus addons**.
+
OUT (each waits for its first customer): menu level 3 (UIDROPDOWNMENU_MAXLEVELS is 2 — the
- unit popups' deepest law; the XML gains DropDownList3 the day a customer needs it), color
- swatches + the color picker + opacity, info.textHeight's per-row SetFont, the newbie
+ unit popups' deepest law; the XML gains DropDownList3 the day a customer needs it),
+ info.textHeight's per-row SetFont, the newbie
tooltips on rows (the ref gates them behind a default-off cvar), message() on the row/level
overflow guards (no dev-error dialog surface; the guard returns silently). The uiScale
correction came IN with its first scaled customer (0992, the options window's dropdown
@@ -41,8 +49,9 @@
- MENU displayMode hides the capsule art trio when present but skips the ref's toggle-button
texture blanking + re-anchor (every MENU customer here is a born-hidden 10x10 frame whose
capsule never draws).
- - The disabled-row InvisibleButton spans to the row's RIGHT (the ref stops it at the color
- swatch, which is OUT).
+ - (Was: "the disabled-row InvisibleButton spans to the row's RIGHT — the ref stops it at the
+ color swatch, which is OUT". The swatch is IN now, so the ref's own anchor is restored:
+ `RIGHT relativeTo="$parentColorSwatch" relativePoint="LEFT"`.)
- The list OnShow resets showTimer AND isCounting (the ref writes `showTime`, a field
nothing reads — transcribed as the intent, not the typo).
-->
@@ -97,8 +106,10 @@
end
-- The ref's width law, run from the list's OnUpdate (the header's WIDTH SETTLE): rows
- -- measure text + 60, +10 for an arrow, +10 for an icon, -30 when notCheckable; the list
- -- takes max + 15. Waits until every text row has a real (nonzero) measure.
+ -- measure text + 60, +10 for an arrow OR a colour swatch, +10 for an icon, -30 when
+ -- notCheckable; the list takes max + 15. Waits until every text row has a real (nonzero)
+ -- measure. (The ref's `if ( info.hasArrow or info.hasColorSwatch )` is ONE +10, not two —
+ -- the two ride the same right-hand slot, and a row never has both.)
function UIDropDownMenu_SettleWidth(listFrame)
local listFrameName = listFrame:GetName();
if ( listFrame.fixedWidth ) then
@@ -116,7 +127,7 @@
if ( width > 0 ) then
seen = seen + 1;
width = width + 60;
- if ( button.hasArrow ) then
+ if ( button.hasArrow or button.hasColorSwatch ) then
width = width + 10;
end
if ( button.notCheckable ) then
@@ -296,6 +307,15 @@
button.func = info.func;
button.owner = info.owner;
+ -- The colour-swatch half of the info table (ref l.229-233), passed to the row so the
+ -- swatch's OnClick can hand it all to ColorPickerFrame. `swatchFunc` becomes the
+ -- picker's `func`, which is why it is the picker's LIVE-preview callback and not an
+ -- on-accept one.
+ button.hasOpacity = info.hasOpacity;
+ button.opacity = info.opacity;
+ button.opacityFunc = info.opacityFunc;
+ button.cancelFunc = info.cancelFunc;
+ button.swatchFunc = info.swatchFunc;
button.keepShownOnClick = info.keepShownOnClick;
button.tooltipTitle = info.tooltipTitle;
button.tooltipText = info.tooltipText;
@@ -376,6 +396,20 @@
end
button.checked = info.checked;
+ -- The colour square (ref l.318-328). Its NormalTexture is the swatch art, tinted to the
+ -- row's colour; the row keeps r/g/b so the swatch's OnClick can seed the picker.
+ local colorSwatch = getglobal(listFrameName.."Button"..index.."ColorSwatch");
+ if ( info.hasColorSwatch ) then
+ getglobal(colorSwatch:GetName().."NormalTexture"):SetVertexColor(info.r, info.g, info.b);
+ button.r = info.r;
+ button.g = info.g;
+ button.b = info.b;
+ colorSwatch:Show();
+ else
+ colorSwatch:Hide();
+ end
+ button.hasColorSwatch = info.hasColorSwatch;
+
-- notClickable disables white; isTitle disables gold (ref order — title wins).
if ( info.notClickable ) then
info.disabled = 1;
@@ -743,6 +777,26 @@
return getglobal(this:GetName().."NormalText"):GetText();
end
+ -- ref l.801-815, verbatim. THE entry point to the colour picker: every colour option in
+ -- every FrameXML dropdown and every Ace2/Dewdrop menu row lands here. Note what it does
+ -- that the picker itself does not — `previousValues` is captured HERE, and it is the table
+ -- `cancelFunc` is handed, so Cancel restoring the old colour is this line's doing.
+ function UIDropDownMenuButton_OpenColorPicker(button)
+ CloseMenus();
+ if ( not button ) then
+ button = this;
+ end
+ UIDROPDOWNMENU_MENU_VALUE = button.value;
+ ColorPickerFrame.func = button.swatchFunc;
+ ColorPickerFrame.hasOpacity = button.hasOpacity;
+ ColorPickerFrame.opacityFunc = button.opacityFunc;
+ ColorPickerFrame.opacity = button.opacity;
+ ColorPickerFrame:SetColorRGB(button.r, button.g, button.b);
+ ColorPickerFrame.previousValues = {r = button.r, g = button.g, b = button.b, opacity = button.opacity};
+ ColorPickerFrame.cancelFunc = button.cancelFunc;
+ ShowUIPanel(ColorPickerFrame);
+ end
+
function UIDropDownMenu_DisableButton(level, id)
getglobal("DropDownList"..level.."Button"..id):Disable();
end
@@ -781,6 +835,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CloseMenus();
+ UIDropDownMenuButton_OpenColorPicker(this:GetParent());
+
+
+ CloseDropDownMenus(this:GetParent():GetParent():GetID() + 1);
+ getglobal(this:GetName().."SwatchBg"):SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b);
+ UIDropDownMenu_StopCounting(this:GetParent():GetParent());
+
+
+ getglobal(this:GetName().."SwatchBg"):SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b);
+ UIDropDownMenu_StartCounting(this:GetParent():GetParent());
+
+
+
+
@@ -799,12 +888,13 @@
+ alive (timeout + deeper-level close). It stops at the colour swatch's LEFT edge
+ (ref l.111-113) so a disabled row's swatch is still clickable. -->
-
+
diff --git a/crates/benilla-app/assets/ui/UIPanelTemplates.xml b/crates/benilla-app/assets/ui/UIPanelTemplates.xml
new file mode 100644
index 00000000..c2406d7c
--- /dev/null
+++ b/crates/benilla-app/assets/ui/UIPanelTemplates.xml
@@ -0,0 +1,433 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getglobal(this:GetName() .. "Left"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
+ getglobal(this:GetName() .. "Middle"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
+ getglobal(this:GetName() .. "Right"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Down")
+
+
+ getglobal(this:GetName() .. "Left"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
+ getglobal(this:GetName() .. "Middle"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
+ getglobal(this:GetName() .. "Right"):SetTexture("Interface\\Buttons\\UI-Panel-Button-Up")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HideUIPanel(this:GetParent())
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ parent:SetValue(parent:GetValue() - (parent:GetHeight() / 2))
+ PlaySound("UChatScrollButton")
+
+
+
+
+
+
+
+ local parent = this:GetParent()
+ parent:SetValue(parent:GetValue() + (parent:GetHeight() / 2))
+ PlaySound("UChatScrollButton")
+
+
+
+
+
+ this:GetParent():SetVerticalScroll(arg1)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ScrollFrame_OnLoad()
+ ScrollFrame_OnScrollRangeChanged(arg2)
+
+
+ local scrollbar = getglobal(this:GetName() .. "ScrollBar")
+ scrollbar:SetValue(arg1)
+ local min
+ local max
+ min, max = scrollbar:GetMinMaxValues()
+ if (arg1 == 0) then
+ getglobal(scrollbar:GetName() .. "ScrollUpButton"):Disable()
+ else
+ getglobal(scrollbar:GetName() .. "ScrollUpButton"):Enable()
+ end
+ if ((scrollbar:GetValue() - max) == 0) then
+ getglobal(scrollbar:GetName() .. "ScrollDownButton"):Disable()
+ else
+ getglobal(scrollbar:GetName() .. "ScrollDownButton"):Enable()
+ end
+
+ ScrollFrameTemplate_OnMouseWheel(arg1)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ this:ClearFocus()
+ this:HighlightText(0, 0)
+ this:HighlightText()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/crates/benilla-app/assets/ui/UIParent.xml b/crates/benilla-app/assets/ui/UIParent.xml
index a3f07462..e3bb222f 100644
--- a/crates/benilla-app/assets/ui/UIParent.xml
+++ b/crates/benilla-app/assets/ui/UIParent.xml
@@ -17,6 +17,29 @@
-->
+
+
+
-
+
@@ -1167,17 +1172,17 @@
-
-
+
+
-
+
-
+
@@ -1235,7 +1240,7 @@
every login. Steady-state behavior is identical — Update Shows/Hides on target changes. -->
-
+
@@ -1452,7 +1457,7 @@
-
+
-
+
@@ -1514,30 +1519,31 @@
frames' own deferred buff panel, not here.
-->
diff --git a/crates/benilla-app/assets/ui/UnitPopup.xml b/crates/benilla-app/assets/ui/UnitPopup.xml
index 2ec8327c..5c60a540 100644
--- a/crates/benilla-app/assets/ui/UnitPopup.xml
+++ b/crates/benilla-app/assets/ui/UnitPopup.xml
@@ -624,7 +624,7 @@
elseif ( button == "PET_ABANDON" ) then
StaticPopup_Show("ABANDON_PET");
elseif ( button == "PET_PAPERDOLL" ) then
- ToggleCharacter("BenillaPetPaperDollFrame");
+ ToggleCharacter("PetPaperDollFrame");
elseif ( button == "PET_RENAME" ) then
StaticPopup_Show("RENAME_PET");
end
diff --git a/crates/benilla-app/assets/ui/WorldMapFrame.xml b/crates/benilla-app/assets/ui/WorldMapFrame.xml
index 9a3176cc..21222f23 100644
--- a/crates/benilla-app/assets/ui/WorldMapFrame.xml
+++ b/crates/benilla-app/assets/ui/WorldMapFrame.xml
@@ -157,8 +157,8 @@
end
-- ── The continent/zone dropdowns (ref verbatim, except the vararg walk: the reference's
- -- `arg.n` needs LUA_COMPAT_VARARG, which stock 5.1 — and ours — doesn't ship; select()
- -- is the 5.1-native spelling). ─────────────────────────────────────────────────────────
+ -- `arg.n` is written as select("#", ...), the 5.1-native spelling and this tree's house
+ -- style for it). ──────────────────────────────────────────────────────────────────────
function WorldMapContinentsDropDown_OnLoad()
UIDropDownMenu_Initialize(this, WorldMapContinentsDropDown_Initialize);
UIDropDownMenu_SetWidth(130);
diff --git a/crates/benilla-app/assets/ui/ZoneText.xml b/crates/benilla-app/assets/ui/ZoneText.xml
index d0b9ee79..c4e6eb56 100644
--- a/crates/benilla-app/assets/ui/ZoneText.xml
+++ b/crates/benilla-app/assets/ui/ZoneText.xml
@@ -170,8 +170,11 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
end
]]>
+ (ref carries toplevel="true" on both; still dropped here, but the reason has changed.
+ `SetToplevel` is no longer a gap — the raise law is built, `script::object::toplevel` —
+ and its real trigger is Show, not a click, so "click-raise is meaningless on mouse-less
+ splash text" was the wrong argument. What holds now: these are LOW-strata splash frames
+ that overlap nothing in LOW, so the occlusion gate declines. One word to re-add.) -->
diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc
index b792c91d..dcf280a9 100644
--- a/crates/benilla-app/assets/ui/benilla.toc
+++ b/crates/benilla-app/assets/ui/benilla.toc
@@ -20,6 +20,11 @@
# The named virtual Font objects (decision 0084) — must load first so any FontString's
# `inherits="GameFontNormal"` resolves against a registered object.
Fonts.xml
+# 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,
+# DialogButtonNormalText/HighlightText) and everything above it in FrameXML.toc is engine.
+BasicControls.xml
# The UIPanel slot manager (decision 0084 §2): script-only, no frames of its own — must load
# before any panel frame (Gossip/Merchant today) so ShowUIPanel/HideUIPanel already exist
# when those frames' OnLoad/OnEvent reference them.
@@ -32,7 +37,10 @@ UiPanels.xml
UIParent.xml
# The tooltip: a hover window the bag/merchant/loot/unit frames drive. Loaded before them so
# `GameTooltip` + its verbs exist when their OnEnter handlers fire (runtime, so order
-# is not strictly required — but it keeps the dependency legible).
+# is not strictly required — but it keeps the dependency legible). It also declares the two
+# virtual templates (GameTooltipTemplate, ShoppingTooltipTemplate), which IS an order
+# requirement: ItemRef.xml below expands one, and so do the 27 corpus addons that name
+# GameTooltipTemplate in an XML inherits=.
GameTooltip.xml
# The dropdown kit (0203 phase 2's closing sub-slice, widened for 0434 phase 5):
# UIDropDownMenuTemplate + DropDownList1/2 + the UIDropDownMenu_* API — before its
@@ -43,9 +51,11 @@ UIDropDownMenu.xml
# children's OnLoad initialize into it.
UnitPopup.xml
# The chat-link click router (SetItemRef): item links → ItemRefTooltip:SetHyperlink; a
-# player name → left-click whisper / right-click FRIEND dropdown. After GameTooltip.xml
-# (GameTooltip_OnLoad) AND the dropdown kit + UnitPopup — its FriendsDropDown inherits
-# UIDropDownMenuTemplate and opens a UnitPopup FRIEND menu.
+# player name → left-click whisper / right-click FRIEND dropdown. After GameTooltip.xml —
+# ItemRefTooltip inherits=GameTooltipTemplate, so this order is now REQUIRED, not just legible
+# (GameTooltip_OnLoad was the old, runtime-only reason) — AND after the dropdown kit +
+# UnitPopup, since its FriendsDropDown inherits UIDropDownMenuTemplate and opens a UnitPopup
+# FRIEND menu.
ItemRef.xml
# The shared status-bar numerals machinery (decision 1082): TextStatusBar_* + the opt-in
# template — before its customers (the XP bar in ActionBar.xml today; the unit-frame
@@ -53,7 +63,7 @@ ItemRef.xml
# loads TextStatusBar right before its unit frames, same seat as here.
TextStatusBar.xml
UnitFrames.xml
-# The combo-point dots (decision 0869) — anchored to BenillaTargetFrame, so after
+# The combo-point dots (decision 0869) — anchored to TargetFrame, so after
# UnitFrames.xml; its fade chain needs UiPanels.xml's UIFrameFade above.
ComboFrame.xml
# The center-screen scrolling combat text (decision 0578) — the Blizzard_CombatText
@@ -79,26 +89,26 @@ Cooldown.xml
ActionBar.xml
# The two always-on bottom multibars (actions 61-72 / 49-60): pure XML/Lua over
# ActionBar.xml's shared button template + handler set — loads right after it
-# (inherits BenillaActionButtonTemplate, anchors to BenillaActionButton1, and its
+# (inherits BenillaActionButtonTemplate, anchors to ActionButton1, and its
# buttons join the BENILLA_ACTION_BUTTONS roster ActionBar.xml declares).
MultiBars.xml
# The stance/shapeshift bar: hidden until `crate::ui_shapeshift`'s feed pushes a
# non-empty form list (a formless class never shows it). After Cooldown.xml
-# (CooldownFrame_SetTimer) + ActionBar.xml (BENILLA_FALLBACK_ICON, the BenillaActionBar
+# (CooldownFrame_SetTimer) + ActionBar.xml (BENILLA_FALLBACK_ICON, the MainMenuBar
# anchor target).
StanceBar.xml
# The pet action bar (decision 0982): hidden until `crate::ui_pet`'s feed reports a bar,
# i.e. until the server sends `SMSG_PET_SPELLS` for a live pet or charm — so a class that
# never has one never sees it. Right after the stance bar, whose row it shares (either
# one showing raises UIParent.xml's managed "pet" delta), and after Cooldown.xml
-# (CooldownFrame_SetTimer), ActionBar.xml (the `BenillaActionBar` anchor target) and
+# (CooldownFrame_SetTimer), ActionBar.xml (the `MainMenuBar` anchor target) and
# GameTooltip.xml (its hover). It is also the pet ARC's file, so decision 1066's three
# right-click-menu dialogs (ABANDON_PET / RENAME_PET / PETRENAMECONFIRM) register here —
# which adds UiPanels.xml to its dependency set, already far above.
PetActionBar.xml
# The micro-button row in the bar's right-hand recess (the ref's own file split, its TOC
# loads MainMenuBarMicroButtons right after MainMenuBar). After ActionBar.xml: it anchors
-# into BenillaActionBarArtFrame by name and seats itself with that file's
+# into MainMenuBarArtFrame by name and seats itself with that file's
# BenillaActionBarArt_SeatAbove. It must come BEFORE the panels it toggles, because each of
# their OnShow/OnHide calls the `UpdateMicroButtons` this file defines (the panel frames
# themselves are read back through getglobal, so the reverse dependency is call-time).
@@ -117,9 +127,47 @@ GameTime.xml
# other consumer of the zone-text host globals.
ZoneText.xml
# The faux-scroll kit (decisions 0247/0250/0251): BenillaScrollBarTemplate (the draggable
-# Slider scroll bar) + BenillaFauxScrollFrameTemplate + the BenillaFauxScrollFrame_* API —
+# Slider scroll bar) + FauxScrollFrameTemplate + the reference's own FauxScrollFrame_*
+# API (Update/GetOffset/SetOffset/OnVerticalScroll + ScrollFrame_OnLoad) —
# before its first customer (the trainer list) below.
ScrollTemplates.xml
+# The reference's SHARED widget kit (decision 1203's queue): UIPanelButtonTemplate,
+# UICheckButtonTemplate, UIPanelCloseButton, InputBoxTemplate, the UIPanelScroll* family and
+# their siblings, under the reference's own names — the templates a third-party addon names in
+# `CreateFrame(kind, name, parent, "Template")`, which nothing in benilla's own XML consumes
+# yet (each of our windows still carries its private copy; see the file's header). After
+# ScrollTemplates.xml because UIPanelScrollFrameTemplate's OnLoad is that file's
+# ScrollFrame_OnLoad, and this file supplies the two remaining handlers of that same kit
+# (ScrollFrameTemplate_OnMouseWheel, ScrollFrame_OnScrollRangeChanged).
+UIPanelTemplates.xml
+# The reference's shared OPTIONS widgets: OptionsCheckButtonTemplate /
+# UIOptionsCheckButtonTemplate / OptionsSliderTemplate / OptionsButtonTemplate /
+# OptionFrameBoxTemplate. Straight after UIPanelTemplates.xml — two of them inherit its
+# UICheckButtonTemplate and UIPanelButtonTemplate. Not benilla's own options window
+# (OptionsFrame.xml, far below): these are the shared names addons build panels out of.
+OptionsFrameTemplates.xml
+# The reference's money-frame kit: MoneyFrameTemplate/SmallMoneyFrameTemplate + MoneyTypeInfo
+# + the MoneyFrame_* API. Needs only Fonts (NumberFontNormal/NumberFontNormalLarge), and
+# nothing benilla ships consumes it yet — our own eight coin displays run on MerchantFrame.xml's
+# BenillaMoney_* slot kit, and MoneyFrame.xml's header is where the two are reconciled. Seated
+# beside the scroll kit (the other shared widget kit) and ahead of every window that shows
+# coins, which is where the reference's own TOC puts it too.
+MoneyFrame.xml
+# The shared colour dialog + its opacity slider (`ColorPickerFrame`, `OpacitySliderFrame`,
+# `ColorPickerOkayButton`, `ColorPickerCancelButton`). Last of the "shared FrameXML surface with no
+# benilla consumer yet" cluster that runs from UIPanelTemplates.xml down — the same posture as
+# MoneyFrame.xml above, and for the same reason: 86 of the 218 corpus addons reach this window (64
+# of those files are `Dewdrop-2.0.lua`, which drives THIS one rather than rolling its own), and
+# nothing we ship opens it. The seat is a REQUIREMENT, not tidiness: its Okay/Cancel buttons
+# `inherits="GameMenuButtonTemplate"`, which is resolved at LOAD, and that template lives in
+# UIPanelTemplates.xml six lines up (it moved there from GameMenuFrame.xml — the reference's own
+# file for it — precisely because GameMenuFrame.xml is deliberately LAST). Everything else it needs
+# is far above (Fonts' GameFontNormal/…Small trio; UiPanels' ShowUIPanel/HideUIPanel, its
+# UISpecialFrames entry and the ESC rung that cancel-clicks it). UIDropDownMenu.xml, which owns the
+# colour-swatch row that OPENS this window, sits above too — but that direction is call-time only
+# (`UIDropDownMenuButton_OpenColorPicker` reaches `ColorPickerFrame` by name when the swatch is
+# clicked), so the two could sit either way round.
+ColorPickerFrame.xml
# The fullscreen world map (decision 0203 phase 2), over the worldmap host API
# (`crate::ui_world_map` feeds it); the 'M' binding (ui_script/input.rs) calls its
# ToggleWorldMap().
@@ -160,8 +208,8 @@ PetPaperDollFrame.xml
# The Skills tab (decision 0437 phase 4): a CharacterFrame child page, but a separate
# top-level file (SkillFrame.xml's own header comment — this engine has no cross-file
# `parent=` attachment, so it's positioned by name via `` instead). Loaded immediately after CharacterFrame.xml so it
-# paints ON TOP of it (later file = higher z) and so `BenillaCharacterFrame` already exists
+# "CharacterFrame">` instead). Loaded immediately after CharacterFrame.xml so it
+# paints ON TOP of it (later file = higher z) and so `CharacterFrame` already exists
# for that anchor to resolve against.
SkillFrame.xml
# The inspect window (decision 0631): another player's paper doll. Beside the character
@@ -218,7 +266,7 @@ ItemTextFrame.xml
# session windows below.
TrainerFrame.xml
# The bank window (decision 0604 phase 4): the vault, purchase ladder, and the 6 bank-bag
-# popouts, over the C_Container feed already carrying container -1/5..10 (ui_items.rs) +
+# popouts, over the container feed already carrying container -1/5..10 (ui_items.rs) +
# the bank Lua surface (benilla-ui/src/script/bank.rs) + crate::ui_bank's feed/drain.
# Loaded AFTER MerchantFrame.xml (reuses its BenillaMoney_* coin helpers for the purchase
# row + purse) and AFTER BagFrame.xml, already above (its BenillaBagSlotTemplate/
@@ -289,6 +337,10 @@ KeyBindingsPage.xml
# (all far above); sits right before GameMenuFrame so the menu — which reaches it by
# name at click time — stays the last, outermost layer.
OptionsFrame.xml
+# The in-game AddOns list (decision 1197) — the ESC menu's AddOns entry. Before GameMenuFrame,
+# which reaches AddonList_Toggle by name at click time; it depends only on the AddOn API verbs
+# (1191) and defines its own templates, so it can sit anywhere after the fonts.
+AddonList.xml
# The game menu (decision 0674): the frame ESC opens, plus the CAMP/QUIT dialogs and their
# event driver. LAST on purpose — it is the outermost layer of the shell, it depends on
# everything it touches being loaded (UiPanels' popup engine + panel manager, MicroMenu's
diff --git a/crates/benilla-app/examples/addon_harness.rs b/crates/benilla-app/examples/addon_harness.rs
new file mode 100644
index 00000000..ff80548f
--- /dev/null
+++ b/crates/benilla-app/examples/addon_harness.rs
@@ -0,0 +1,219 @@
+//! `addon_harness` — load a folder of addons, one per VM, and print what happened.
+//!
+//! ```text
+//! cargo run -q -p benilla-app --example addon_harness -- [--verbose] [--why ]
+//! ```
+//!
+//! The instrument decision 1188 phase 6 asks for: *"which addons work" is a number that can be
+//! re-read on any day*. The mechanics, and what the numbers are and are not worth, are in
+//! [`benilla_app::addon_harness`]'s module doc — read it before quoting anything from here.
+//!
+//! **Expect a long tail and do not chase 100 %** (1188's own instruction). The report is a
+//! distribution, not a pass/fail: a handful of addons will always want features we have not built.
+use benilla_app::addon_harness;
+
+fn main() {
+ let mut args = std::env::args().skip(1);
+ let Some(root) = args.next() else {
+ eprintln!(
+ "usage: addon_harness [--verbose] [--why ]"
+ );
+ std::process::exit(2);
+ };
+ let rest: Vec = args.collect();
+ let verbose = rest.iter().any(|a| a == "--verbose");
+ // `--why ` — the addons behind one blocker row, with their verbatim first errors.
+ // The ranked table collapses quoted names by design (1193); this is the read-back, and two of
+ // this arc's decisions came from doing it by hand (1206, 1210).
+ let why = rest
+ .iter()
+ .position(|a| a == "--why")
+ .and_then(|i| rest.get(i + 1))
+ .cloned();
+ let root = std::path::PathBuf::from(root);
+
+ let reports = addon_harness::survey(&root);
+ if reports.is_empty() {
+ eprintln!(
+ "no addons under {} — is that an AddOns folder?",
+ root.display()
+ );
+ std::process::exit(1);
+ }
+
+ let loaded = reports.iter().filter(|r| r.loaded).count();
+ let clean = reports
+ .iter()
+ .filter(|r| r.loaded && r.missing_globals.is_empty())
+ .count();
+ let blocked = reports
+ .iter()
+ .filter(|r| !r.missing_deps.is_empty())
+ .count();
+
+ println!("\n{} addon(s) under {}", reports.len(), root.display());
+ // Which VM the survey ran against. Without an install there is no GlobalStrings.lua, ~5,000
+ // globals are missing, and every number below is worse for a reason that has nothing to do
+ // with the client — say so rather than letting two machines' numbers be compared in silence.
+ println!(
+ " VM: our whole FrameXML + a seated session{}\n",
+ if addon_harness::seated_with_global_strings() {
+ " + the real GlobalStrings.lua"
+ } else {
+ " ** no install found: GlobalStrings absent, these numbers are NOT comparable **"
+ }
+ );
+ // The tree these numbers came from. Two runs are comparable only if this matches: in a dev
+ // build `assets/ui` is read from the SOURCE TREE, so anything else editing the checkout moves
+ // the headline with no rebuild. Quoting a delta across two different digests is how a wrong
+ // attribution got into a decision record (1209).
+ println!(
+ " FrameXML digest : {}",
+ addon_harness::framexml_digest()
+ );
+ println!(
+ " loaded without a single load error : {loaded}/{}",
+ reports.len()
+ );
+ println!(
+ " ...and calling nothing we lack : {clean}/{}",
+ reports.len()
+ );
+ println!(" with a dependency not installed : {blocked}");
+ // The stricter column, and the one that answers what the survey is really asking. Every other
+ // number here is LOAD-time; this one drives the client's own session start
+ // (ADDON_LOADED -> VARIABLES_LOADED -> PLAYER_LOGIN -> PLAYER_ENTERING_WORLD, then a second of
+ // ticks) and reports what the addon's HANDLERS raised. Four decision records in this arc end
+ // with "the headline cannot see this"; this is the number that can.
+ let survived = reports
+ .iter()
+ .filter(|r| r.loaded && r.session_errors.is_empty())
+ .count();
+ println!(
+ " ...and survived a session start : {survived}/{}",
+ reports.len()
+ );
+
+ // What the session start broke, ranked the way `blockers` ranks load failures.
+ let mut session: std::collections::BTreeMap = Default::default();
+ for r in reports.iter().filter(|r| r.loaded) {
+ if let Some(e) = r.session_errors.first() {
+ *session.entry(addon_harness::normalise(e)).or_default() += 1;
+ }
+ }
+ if !session.is_empty() {
+ let mut rows: Vec<(String, usize)> = session.into_iter().collect();
+ rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
+ println!("\n what broke at SESSION START (addons that loaded clean, first error each):");
+ for (err, count) in rows.into_iter().take(12) {
+ println!(" {count:>4} {err}");
+ }
+ }
+
+ // The distribution, because a mean would hide the shape.
+ let mut buckets = [0usize; 5];
+ for r in &reports {
+ let n = r.missing_globals.len();
+ buckets[match n {
+ 0 => 0,
+ 1..=2 => 1,
+ 3..=5 => 2,
+ 6..=15 => 3,
+ _ => 4,
+ }] += 1;
+ }
+ println!("\n missing-global count per addon:");
+ for (label, n) in ["0", "1-2", "3-5", "6-15", "16+"].iter().zip(buckets) {
+ println!(" {label:>5} {n:>4} {}", "#".repeat(n.min(60)));
+ }
+
+ // What actually STOPPED them — the ranked first error. Read this before the demand list: a
+ // wall 60 addons hit is worth more than a verb 60 addons would like (decision 1193).
+ println!("\n what stopped them (addons whose FIRST load error was each):");
+ for (err, count) in addon_harness::blockers(&reports).into_iter().take(12) {
+ println!(" {count:>4} {err}");
+ }
+
+ // Templates an addon names in `CreateFrame(..., "Template")` that we have never declared
+ // (decision 1203). Its own list because it is invisible to every other number here: an
+ // unresolved template raises no load error, so the addon scores as a pass and paints nothing.
+ let templates = addon_harness::template_demand(&reports);
+ if !templates.is_empty() {
+ println!("\n most-wanted missing TEMPLATES (addons naming each in CreateFrame):");
+ for (name, count) in templates.into_iter().take(12) {
+ println!(" {count:>4} {name}");
+ }
+ }
+
+ // The same question over the OTHER axis, and the one that actually moves the headline: a
+ // template named in an addon's own XML `inherits=`. Unlike the list above this failure is
+ // usually LOUD — the element's `` fires at load and its first line is normally
+ // `getglobal(this:GetName().."Text")` — which is why transcribing the reference's shared kit
+ // was worth twelve addons while the CreateFrame list predicted none of them. Printed second
+ // and separately because merging the two would hide exactly that difference.
+ let inherits = addon_harness::inherits_demand(&reports);
+ if !inherits.is_empty() {
+ println!("\n most-wanted missing TEMPLATES (addons naming each in an XML inherits=):");
+ for (name, count) in inherits.into_iter().take(12) {
+ println!(" {count:>4} {name}");
+ }
+ }
+
+ // Frames and tables, ranked separately: a missing function is a Rust verb to write, a missing
+ // frame is FrameXML to transcribe, and the two queues go to different people. The scan was
+ // blind to this whole shape until 2026-08-11 — a window 86 addons reach scored 0.
+ let tables = addon_harness::table_demand(&reports);
+ if !tables.is_empty() {
+ println!("\n most-wanted missing FRAMES/TABLES (addons indexing each):");
+ for (name, count) in tables.into_iter().take(16) {
+ println!(" {count:>4} {name}");
+ }
+ }
+
+ println!("\n most-wanted missing globals (addons wanting each):");
+ for (name, count) in addon_harness::demand(&reports).into_iter().take(30) {
+ println!(" {count:>4} {name}");
+ }
+
+ if let Some(pattern) = &why {
+ let hits = addon_harness::blocked_by(&reports, pattern);
+ println!(
+ "\n addons whose first error matches {pattern:?} ({}):",
+ hits.len()
+ );
+ for (name, err) in &hits {
+ println!(" {name}");
+ // Two lines, not one. The first is the message; the SECOND is mlua's first traceback
+ // frame, and for the row this instrument is most often pointed at that frame is the
+ // whole answer — `in local '(for generator)'` is what tells a generic-for
+ // (decision 1202) apart from any other call of a table value.
+ for line in err.lines().take(2) {
+ println!(" {}", line.trim());
+ }
+ }
+ if hits.is_empty() {
+ println!(" (none — the pattern is matched against the NORMALISED row as printed)");
+ }
+ }
+
+ if verbose {
+ println!("\n per addon:");
+ for r in &reports {
+ let iface = r
+ .interface
+ .iter()
+ .map(u32::to_string)
+ .collect::>()
+ .join(",");
+ println!(
+ " {:<28} iface={:<12} {:<8} missing={:<4} {}",
+ r.name,
+ if iface.is_empty() { "-".into() } else { iface },
+ if r.loaded { "loaded" } else { "ERRORS" },
+ r.missing_globals.len(),
+ r.errors.first().map(String::as_str).unwrap_or("")
+ );
+ }
+ }
+ println!();
+}
diff --git a/crates/benilla-app/src/addon_harness.rs b/crates/benilla-app/src/addon_harness.rs
new file mode 100644
index 00000000..0f3f747e
--- /dev/null
+++ b/crates/benilla-app/src/addon_harness.rs
@@ -0,0 +1,1565 @@
+//! **The addon corpus harness** (1188 phase 6) — load every addon in a folder, one at a time, and
+//! report what happened as numbers that can be re-read on any day.
+//!
+//! 1188 asks for a harness rather than a one-off, and the reason is in its own closing line:
+//! *"that harness plus phase 0's coverage script are this arc's instruments, and they are what
+//! make the remaining work a list instead of an argument."* `scripts/api-coverage.sh` answers
+//! *what surface do we present*; this answers *what does that surface actually carry*.
+//!
+//! ## One VM per addon, deliberately
+//!
+//! Every addon is surveyed in a **fresh** [`UiScript`] with our own FrameXML loaded underneath it.
+//! That costs a full UI load per addon and buys the only property that makes the report readable:
+//! one addon's failure cannot be another's. Loading them all into one VM means the first addon to
+//! leave a global in a bad state gets blamed for the next twenty, and the distribution 1188 asks
+//! for stops meaning anything.
+//!
+//! **What one VM per addon costs, stated because it bounds every number here.** The real client
+//! loads every addon into ONE Lua state, so a library embedded in *any* addon's `Libs\` is global
+//! for every addon that loads after it. Here it is not. An addon that ships no libraries and relies
+//! on a sibling's copy — `FuBar_CustomMenuFu` ships one Lua file and calls
+//! `AceLibrary("Tablet-2.0")`, which no addon it can reach provides — fails in this survey and
+//! would work in a real session. **So the headline is a floor, not an estimate**, and a
+//! `Cannot find a library instance of X` row is this limitation before it is a gap of ours. The
+//! isolation is still the right trade (see above: one addon's failure must not be another's); it
+//! is the reporting that has to say so. Pinned by
+//! [`dependency_tests::a_sibling_addons_embedded_library_is_invisible`].
+//!
+//! The FrameXML underneath is not optional either — an addon calls `UIDropDownMenu_Initialize` and
+//! `GameTooltip_SetDefaultAnchor` as readily as it calls `UnitName`, and roughly half of what looks
+//! like "the WoW API" is Lua the client ships (decision 1190: 1,100 engine functions vs 1,075
+//! FrameXML ones). Surveying against a bare VM would report most of FrameXML as missing.
+//!
+//! ## What "missing" means here, and what it does not
+//!
+//! [`AddonReport::missing_globals`] is a **static** read: the names the addon's own source calls
+//! like functions, minus everything the loaded VM has, minus what the addon defines itself. It is
+//! deliberately not a runtime trace, because an addon's API calls overwhelmingly happen in
+//! handlers that only fire in a live session — a load-time trace would report almost nothing and
+//! read as success.
+//!
+//! The cost of that choice, stated rather than hidden: it over-reports. A name reached only on a
+//! path the addon never takes still counts, and a name built at runtime (`getglobal("Unit"..verb)`)
+//! is invisible to it. So the list is a **prioritisation signal**, exactly like
+//! `api-coverage.sh`'s — read the ranked aggregate, not any single row, and never quote it as a
+//! pass rate.
+//!
+//! ## Running it
+//!
+//! ```text
+//! cargo run -q -p benilla-app --example addon_harness --
+//! ```
+
+use std::collections::{BTreeMap, BTreeSet};
+use std::path::{Path, PathBuf};
+
+use benilla_ui::script::UiScript;
+use benilla_ui::toc::Toc;
+
+/// What one addon did.
+#[derive(Debug, Clone)]
+pub struct AddonReport {
+ pub name: String,
+ /// `## Interface`, as written. 1.12 is `11200`; the corpus is full of older values, and we
+ /// deliberately do not refuse them (decision 1191 §6).
+ pub interface: Vec,
+ /// Did every file in its manifest load without an error?
+ pub loaded: bool,
+ /// Load errors, verbatim, tagged by file.
+ pub errors: Vec,
+ /// Names it calls that the VM does not have — see the module doc on what this is worth.
+ pub missing_globals: Vec,
+ /// Dependencies named in its `.toc` that are not in the folder.
+ pub missing_deps: Vec,
+ /// Templates it names in `CreateFrame(kind, name, parent, "Template")` that the VM has never
+ /// declared (decision 1203).
+ ///
+ /// **A blind spot by construction until it was measured.** `CreateFrame`'s fourth argument was
+ /// ignored outright until today; now it is honoured, and the survey's headline did not move at
+ /// all — because an unresolved template produces *no load error*, so `loaded` cannot see it and
+ /// `missing_globals` does not either (a template is not a global). An addon gets a bare frame,
+ /// loads clean, and paints nothing.
+ ///
+ /// Static, like `missing_globals`, and with the same caveat: read the ranked aggregate.
+ pub missing_templates: Vec,
+ /// Templates it names in an XML `inherits=` that the VM has never declared.
+ ///
+ /// [`Self::missing_templates`]'s twin, added because the first was measuring the wrong axis.
+ /// 1203 built `CreateFrame`'s fourth argument and ranked what it could not resolve; the
+ /// transcription that followed then moved the headline by twelve addons, and **not one of
+ /// those twelve came through `CreateFrame`** — every one failed on an `inherits=` in its own
+ /// XML. The two are ranked separately rather than merged because they behave differently:
+ /// an unresolved `CreateFrame` template is silent (1203 §2), while an unresolved `inherits=`
+ /// is usually loud, because the very next line of the element's `` is
+ /// `getglobal(this:GetName().."Text")` and the loader fires that `` immediately.
+ ///
+ /// Same static caveat, plus one of its own: `inherits=` spans two namespaces — a `` names a FONT — so a name registered as either is resolved.
+ pub missing_inherits: Vec,
+ /// Errors raised **after** the files loaded, while the session start was driven —
+ /// `ADDON_LOADED` → `VARIABLES_LOADED` → `PLAYER_LOGIN` → `PLAYER_ENTERING_WORLD`, then a few
+ /// ticks to drain `OnUpdate` and anything scheduled.
+ ///
+ /// **This is the survey's answer to its own oldest blind spot.** Every number beside it is
+ /// load-time, and this arc has now written the same sentence into four decision records —
+ /// 1203, 1205, 1211 and the state-texture pass all end with *"the headline cannot see this"*,
+ /// because an addon whose file scope runs clean scores as a pass no matter what its handlers
+ /// do. The handlers are where addons actually live: an `OnEvent` that fires on `PLAYER_LOGIN`
+ /// is the single most common shape in the corpus.
+ ///
+ /// Names it INDEXES (`Foo.bar`, `Foo:baz()`) that the VM does not have — frames and tables,
+ /// where [`Self::missing_globals`] is functions.
+ ///
+ /// Two lists because the two queues go to different places: a missing function is a verb to
+ /// write in Rust, a missing frame or table is FrameXML to transcribe. And ranking them together
+ /// would mis-rank, which is 1207's lesson.
+ ///
+ /// **The scan was blind to this shape until 2026-08-11.** `ColorPickerFrame` — a window
+ /// **86 corpus addons reach** — scored exactly 0 on the most-wanted list, because the corpus
+ /// spells it `ColorPickerFrame.func` and `ColorPickerFrame:SetColorRGB`, never `ColorPickerFrame(`.
+ /// The same blindness hid `GameTooltip`, `WorldFrame`, `ChatFrame1` and every other FrameXML
+ /// frame global. Third instrument correction of this arc, after 1209 and 1210.
+ pub missing_tables: Vec,
+ /// Kept **separate from [`Self::loaded`] on purpose.** Folding these in would silently change
+ /// what the headline means and make every number in every past decision record incomparable
+ /// (1209's whole subject). `loaded` still means exactly "no LOAD errors"; this is a second,
+ /// stricter column beside it.
+ pub session_errors: Vec,
+}
+
+/// Survey every addon folder under `root`.
+///
+/// `root` is an AddOns folder — one subfolder per addon, each with a `.toc`. Anything else
+/// is skipped in silence, exactly as discovery does, because a stray `Backup/` is the common case.
+pub fn survey(root: &Path) -> Vec {
+ let mut names: Vec = std::fs::read_dir(root)
+ .into_iter()
+ .flatten()
+ .flatten()
+ .filter(|e| e.path().is_dir())
+ .filter_map(|e| e.file_name().to_str().map(str::to_owned))
+ .filter(|n| manifest_path(root, n).is_some())
+ .collect();
+ names.sort();
+
+ let installed: BTreeSet = names.iter().map(|n| n.to_ascii_lowercase()).collect();
+ names
+ .iter()
+ .map(|n| survey_one(root, n, &installed))
+ .collect()
+}
+
+/// `//.toc`, matched case-insensitively — a real 1.12 addon may ship
+/// `MyAddon/myaddon.toc`, and on a case-sensitive filesystem an exact probe would not find it.
+fn manifest_path(root: &Path, name: &str) -> Option {
+ let want = format!("{name}.toc");
+ std::fs::read_dir(root.join(name))
+ .ok()?
+ .flatten()
+ .find(|e| {
+ e.file_name()
+ .to_str()
+ .is_some_and(|f| f.eq_ignore_ascii_case(&want))
+ })
+ .map(|e| e.path())
+}
+
+fn survey_one(root: &Path, name: &str, installed: &BTreeSet) -> AddonReport {
+ let Some(toc_path) = manifest_path(root, name) else {
+ return AddonReport {
+ name: name.to_string(),
+ interface: Vec::new(),
+ loaded: false,
+ errors: vec!["no manifest".into()],
+ missing_globals: Vec::new(),
+ missing_deps: Vec::new(),
+ missing_templates: Vec::new(),
+ missing_inherits: Vec::new(),
+ missing_tables: Vec::new(),
+ session_errors: Vec::new(),
+ };
+ };
+ // Decoded, not `read_to_string`'d: five of the corpus's 218 manifests are cp1252 and would
+ // otherwise parse as an empty `.toc` — an addon with no files and no dependencies, which reads
+ // as a clean pass (decision 1193).
+ let toc = Toc::parse(&benilla_ui::source::decode(
+ &std::fs::read(&toc_path).unwrap_or_default(),
+ ));
+ let missing_deps: Vec = toc
+ .dependencies()
+ .into_iter()
+ .filter(|d| !installed.contains(&d.to_ascii_lowercase()))
+ .map(str::to_owned)
+ .collect();
+
+ // A VM with our whole interface under it — see the module doc on why per-addon and why loaded.
+ let mut script = match UiScript::new() {
+ Ok(s) => s,
+ Err(e) => {
+ return AddonReport {
+ name: name.to_string(),
+ interface: toc.interface_versions(),
+ loaded: false,
+ errors: vec![format!("VM: {e}")],
+ missing_globals: Vec::new(),
+ missing_deps,
+ missing_templates: Vec::new(),
+ missing_inherits: Vec::new(),
+ missing_tables: Vec::new(),
+ session_errors: Vec::new(),
+ }
+ }
+ };
+ script.set_screen_size(1024.0, 768.0);
+ seat_a_session(&mut script);
+ let _ = crate::ui_script::load_default_ui(&script);
+ // The addon's DEPENDENCIES, first and recursively — `AddOn_Load 0x51f240`'s own first two
+ // steps (1191 §2, byte-verified). Surveying an addon without them is surveying a state the
+ // real client never presents: `FuBar_Aspect` declares `## Dependencies: Ace, FuBar` and opens
+ // with `ace:LoadTranslation(...)`, so in isolation it fails on a global its dependency was
+ // always going to define. Fifteen corpus addons failed that way, on us rather than on
+ // themselves.
+ //
+ // Loaded BEFORE `globals_of` below, so a name a dependency provides does not also count as a
+ // missing global — the same double-count the FrameXML-underneath decision avoided.
+ load_dependencies(&script, root, &toc, installed, &mut BTreeSet::new());
+ let known = globals_of(&script);
+
+ let errors = load_addon_files(&script, root, name, &toc);
+ let (missing_globals, missing_tables) = missing_calls(root, name, &toc, &known);
+ // AFTER the addon's files: a template it declares in its OWN XML is registered by then, so it
+ // is not missing. The check asks the VM's live registry, not a name list.
+ let missing_templates = missing_templates(&script, root, name, &toc);
+ let missing_inherits = missing_inherits(&script, root, name, &toc);
+ let session_errors = drive_session_start(&mut script, name);
+
+ AddonReport {
+ name: name.to_string(),
+ interface: toc.interface_versions(),
+ loaded: errors.is_empty(),
+ errors,
+ missing_globals,
+ missing_deps,
+ missing_templates,
+ missing_inherits,
+ missing_tables,
+ session_errors,
+ }
+}
+
+/// Drive the client's own session-start sequence over a loaded addon and report what its HANDLERS
+/// raised — the errors no other number here can see.
+///
+/// The order is the reference's, byte-verified inside `UI_Init 0x48fbf0` and already pinned by
+/// `ui_script::addons`' own test: every addon's `ADDON_LOADED`, then `VARIABLES_LOADED`, then
+/// `PLAYER_LOGIN`; `PLAYER_ENTERING_WORLD` follows in the cascade. Then a few ticks, because a
+/// great deal of addon code runs from `OnUpdate` or from something scheduled on the first one.
+///
+/// **Deliberately not `ui_script::finish_ui_load`**, which is the production path: that also runs
+/// `load_saved_variables`, which reads the machine's real `BENILLA_HOME`. A survey must not depend
+/// on — or write to — the director's own saved variables, so the events are fired directly.
+///
+/// **Ten ticks of 0.1 s, and the bound is the point.** An addon with a busy `OnUpdate` would
+/// otherwise run for as long as we let it, and 218 VMs multiply whatever that is. One simulated
+/// second reaches the common `ScheduleEvent(..., 0)`/`(..., 0.05)` shapes and Ace's own one-second
+/// `AceEvent_FullyInitialized` timer; it does not reach a ten-second self-heal, and that
+/// under-report is stated rather than hidden.
+fn drive_session_start(script: &mut UiScript, name: &str) -> Vec {
+ let before = script.errors().len();
+ script.fire_event(
+ "ADDON_LOADED",
+ vec![benilla_ui::script::ScriptValue::Str(name.to_string())],
+ );
+ for event in ["VARIABLES_LOADED", "PLAYER_LOGIN", "PLAYER_ENTERING_WORLD"] {
+ script.fire_event(event, Vec::new());
+ }
+ for _ in 0..10 {
+ script.tick(0.1);
+ }
+ script.errors().split_off(before)
+}
+
+/// Load an addon's declared dependencies into the VM, depth-first, each at most once.
+///
+/// The reference's own order (`AddOn_Load 0x51f240`, 1191 §2): **OptionalDeps first, failures
+/// ignored; RequiredDeps next, a failure aborts the dependent's load; then its own files.** Both
+/// halves are walked, in that order; the harness does not reproduce the *abort*, only the **state**
+/// a dependent's file scope actually meets, and it reports a missing required dependency separately
+/// ([`AddonReport::missing_deps`]) while an absent optional one is silent, as the client's is.
+///
+/// This doc claimed the same thing from 1193 onward while the code read `dependencies()` only —
+/// the third doc-vs-code lie this arc has found in its own instruments (the others: a module doc
+/// claiming it printed which mode a run was in, and a comment claiming the leading-capital filter
+/// covered Lua locals). **A sentence describing behaviour is a claim, and an unverified claim in a
+/// comment is worse than none, because it stops the next reader checking.**
+///
+/// Errors inside a dependency are **not** attributed to the dependent. A library that fails is its
+/// own row in this survey; blaming its consumers would count one fault N times, which is the
+/// mistake the one-VM-per-addon rule exists to prevent.
+fn load_dependencies(
+ script: &UiScript,
+ root: &Path,
+ toc: &Toc,
+ installed: &BTreeSet,
+ seen: &mut BTreeSet,
+) {
+ // OPTIONAL first, then required — the reference's own order, and the half this walk was
+ // missing. The doc above has claimed since 1193 that the two are "folded together"; only
+ // `dependencies()` was ever read, so `## OptionalDeps: FuBar, Ace2` did nothing and the survey
+ // met a state the real client never produces. That is the exact bug `load_dependencies` exists
+ // to prevent, in the half nobody checked. **130 corpus addons declare optional deps**, and it
+ // is how the whole FuBar family gets AceLibrary: `FuBar_BagFu`'s `.toc` lists
+ // `FuBarPlugin-2.0.lua` BEFORE `AceLibrary.lua`, and FuBarPlugin raises
+ // "FuBarPlugin-2.0 requires AceLibrary." unless the `Ace2` addon loaded first.
+ let deps: Vec<&str> = toc
+ .optional_dependencies()
+ .into_iter()
+ .chain(toc.dependencies())
+ .collect();
+ for dep in deps {
+ let key = dep.to_ascii_lowercase();
+ if !installed.contains(&key) || !seen.insert(key) {
+ continue; // not installed (already reported), or already in this VM
+ }
+ let Some(dep_toc) = manifest_path(root, dep)
+ .and_then(|p| std::fs::read(p).ok())
+ .map(|b| Toc::parse(&benilla_ui::source::decode(&b)))
+ else {
+ continue;
+ };
+ load_dependencies(script, root, &dep_toc, installed, seen);
+ let _ = load_addon_files(script, root, dep, &dep_toc);
+ }
+}
+
+/// Template names the addon passes to `CreateFrame` that the VM cannot resolve.
+///
+/// Scanned rather than traced, for `missing_globals`' reason: the calls overwhelmingly happen in
+/// handlers a load-time survey never fires. The pattern is narrow — a **string literal** in the
+/// fourth argument position — so `CreateFrame(t, n, p, someVariable)` is invisible here, which is
+/// the honest under-report to make. It is also not comment-stripped: a `CreateFrame` inside a
+/// comment counts, and that over-report is the cheaper error of the two.
+fn missing_templates(script: &UiScript, root: &Path, name: &str, toc: &Toc) -> Vec {
+ let mut wanted: BTreeSet = BTreeSet::new();
+ for path in source_files(root, name, toc) {
+ let Some(text) = read_text(root, &path) else {
+ continue;
+ };
+ for (i, _) in text.match_indices("CreateFrame") {
+ let rest = &text[i + "CreateFrame".len()..];
+ let Some(open) = rest.find('(') else { continue };
+ let Some(close) = rest[open..].find(')') else {
+ continue;
+ };
+ let Some(fourth) = rest[open + 1..open + close].split(',').nth(3) else {
+ continue;
+ };
+ let f = fourth.trim();
+ // A quoted literal, either quote style. `inherits` is a comma-separated LIST in
+ // FrameXML and `CreateFrame` takes the same shape, so split it.
+ let lit = f
+ .strip_prefix('"')
+ .and_then(|s| s.strip_suffix('"'))
+ .or_else(|| f.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')));
+ if let Some(lit) = lit {
+ wanted.extend(
+ lit.split(',')
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty()),
+ );
+ }
+ }
+ }
+ wanted
+ .into_iter()
+ .filter(|name| !script.has_framexml_template(name))
+ .collect()
+}
+
+/// Template names the addon names in an XML `inherits=` that the VM cannot resolve.
+///
+/// [`missing_templates`]'s twin over the *other* way a template is asked for, and the one that
+/// turned out to carry the weight (see [`AddonReport::missing_inherits`]). Same discipline as
+/// every scanner here: a plain attribute read, not comment-stripped, over the addon's XML only.
+///
+/// Two deliberate rules, both of which the CreateFrame scanner does not need:
+///
+/// - **A name registered as a FONT resolves.** `inherits=` is one attribute over two namespaces,
+/// and `` is the single most common use of it in any
+/// corpus. Asking only the template registry would bury the real answer under font names.
+/// - **`virtual="true"` elements the addon declares itself are already in the registry**, because
+/// this runs after its files have loaded — the same ordering `missing_templates` relies on.
+fn missing_inherits(script: &UiScript, root: &Path, name: &str, toc: &Toc) -> Vec {
+ let mut wanted: BTreeSet = BTreeSet::new();
+ for path in source_files(root, name, toc) {
+ if !path.to_ascii_lowercase().ends_with(".xml") {
+ continue;
+ }
+ let Some(text) = read_text(root, &path) else {
+ continue;
+ };
+ for (i, _) in text.match_indices("inherits=") {
+ let rest = &text[i + "inherits=".len()..];
+ let quote = match rest.chars().next() {
+ Some(q @ ('"' | '\'')) => q,
+ _ => continue,
+ };
+ let Some(end) = rest[1..].find(quote) else {
+ continue;
+ };
+ wanted.extend(
+ rest[1..1 + end]
+ .split(',')
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty()),
+ );
+ }
+ }
+ wanted
+ .into_iter()
+ .filter(|n| !script.has_framexml_template(n) && !script.has_font_object(n))
+ .collect()
+}
+
+/// Every source file an addon reaches — its manifest entries plus the `
+
+"#,
+ )
+ .unwrap();
+ std::fs::write(dir.join("src/core.lua"), "ProbeAddonGreeting = 'hello'\n").unwrap();
+ std::fs::write(
+ dir.join("src/templates.xml"),
+ "",
+ )
+ .unwrap();
+ // The sibling library addon, reached across the AddOns root — and it includes a file of
+ // its OWN by bare name, so the base has to follow the include down a level rather than
+ // staying on the includer's.
+ std::fs::write(
+ tmp.join("ProbeLib/core/lib.xml"),
+ "",
+ )
+ .unwrap();
+ std::fs::write(
+ tmp.join("ProbeLib/core/deep.xml"),
+ "",
+ )
+ .unwrap();
+
+ let addon = Addon {
+ name: "ProbeAddon".into(),
+ toc: Toc::parse(&std::fs::read_to_string(dir.join("ProbeAddon.toc")).unwrap()),
+ source: Source::Dir(tmp.clone()),
+ };
+ let mut script = UiScript::new().unwrap();
+ script.set_screen_size(1024.0, 768.0);
+ let failures = addon.load(&script);
+ assert!(failures.is_empty(), "addon load errors: {failures:#?}");
+
+ assert_eq!(
+ script.eval::("return ProbeAddonInclude == true").ok(),
+ Some(true),
+ "a bare-name resolved against the INCLUDING FILE's directory (src/), not \
+ the addon root — Bagnon's `templates.xml` missed entirely before 1186"
+ );
+ assert_eq!(
+ script.eval::("return ProbeLibLoaded == true").ok(),
+ Some(true),
+ "`..\\..\\ProbeLib\\core\\lib.xml` reached a SIBLING addon — the shared-library \
+ pattern 1184's per-addon sandbox blocked"
+ );
+ assert_eq!(
+ script.eval::("return ProbeLibDeep == true").ok(),
+ Some(true),
+ "and the sibling's own bare-name resolved against ITS folder, so the base \
+ follows the include tree down rather than staying on the includer"
+ );
+ assert_eq!(
+ script
+ .eval::("return ProbeAddonGreeting == 'hello'")
+ .ok(),
+ Some(true),
+ "a ",
+ )
+ .unwrap();
+ let all = vec![
+ Addon {
+ name: "Shared".into(),
+ toc: Toc::parse(&std::fs::read_to_string(shared.join("Shared.toc")).unwrap()),
+ source: Source::Dir(tmp.clone()),
+ },
+ dir_addon(&tmp, "UpperDep", "## Dependencies: SHARED\n"),
+ dir_addon(&tmp, "LowerDep", "## Dependencies: shared\n"),
+ ];
+ let mut script = UiScript::new().unwrap();
+ let mut w = Walk::default();
+ for a in &all {
+ let _ = w.load(&mut script, &all, &a.name);
+ }
+ assert!(w.failures.is_empty(), "no failures: {:?}", w.failures);
+ assert_eq!(
+ script.eval::("return SharedLoads").ok(),
+ Some(1),
+ "the shared dependency's files ran exactly once"
+ );
+ let _ = std::fs::remove_dir_all(&tmp);
+ }
+
+ /// A dependency cycle is reported once and does not recurse forever.
+ #[test]
+ fn a_dependency_cycle_is_caught() {
+ let tmp =
+ std::env::temp_dir().join(format!("benilla-addon-cycle-test-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&tmp);
+ std::fs::create_dir_all(&tmp).unwrap();
+ let all = vec![
+ dir_addon(&tmp, "Ping", "## Dependencies: Pong\n"),
+ dir_addon(&tmp, "Pong", "## Dependencies: Ping\n"),
+ ];
+ let mut script = UiScript::new().unwrap();
+ let mut w = Walk::default();
+ for a in &all {
+ let _ = w.load(&mut script, &all, &a.name);
+ }
+ assert!(w.loaded.is_empty(), "neither side of a cycle loads");
+ assert!(
+ w.failures.iter().any(|f| f.contains("cycle")),
+ "the cycle is named: {:?}",
+ w.failures
+ );
+ let _ = std::fs::remove_dir_all(&tmp);
+ }
+
+ /// There is exactly ONE addon root and it is ours (decision 1185) — and a capture has none,
+ /// so a baseline cannot depend on what is installed on the machine that runs it (0008).
+ ///
+ /// The capture arm is `local_state::home()`'s, already hermetic under `$WOW_CAPTURE`, which is
+ /// why passing `None` here IS the capture case rather than a stand-in for it.
+ #[test]
+ fn there_is_one_addon_root_and_it_is_ours() {
+ assert_eq!(
+ root_from(Some(PathBuf::from("/state"))),
+ Some(PathBuf::from("/state/AddOns"))
+ );
+ assert_eq!(root_from(None), None, "a capture run has no addon root");
+ }
+
+ /// A manifest's `.lua` entries run as chunks; everything else is FrameXML.
+ ///
+ /// The classifier, not the load — `\` is a path separator in a `.toc` written for Windows, and
+ /// the extension compare is case-insensitive for the same reason discovery's is.
+ #[test]
+ fn lua_entries_are_told_apart_from_framexml() {
+ assert!(is_lua("Core.lua"));
+ assert!(is_lua("Libs\\LibStub\\LibStub.LUA"));
+ assert!(is_lua("deep/nested/file.Lua"));
+ assert!(!is_lua("Frames.xml"));
+ assert!(!is_lua("Bindings.XML"));
+ assert!(!is_lua("README"));
+ assert!(!is_lua("weird.lua.xml"));
+ }
+
+ /// An addon folder with no matching `.toc` is not an addon, and one whose `.toc` differs only
+ /// in case still is — the reference is a case-insensitive filesystem and a real addon shipped
+ /// as `MyAddon/myaddon.toc` has to load on Linux too.
+ #[test]
+ fn discovery_matches_the_manifest_case_insensitively() {
+ let tmp =
+ std::env::temp_dir().join(format!("benilla-addon-case-test-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&tmp);
+ std::fs::create_dir_all(tmp.join("Cased")).unwrap();
+ std::fs::create_dir_all(tmp.join("Bare")).unwrap();
+ std::fs::write(tmp.join("Cased/cased.TOC"), "## Interface: 11200\n").unwrap();
+ std::fs::write(tmp.join("Bare/notes.txt"), "not an addon").unwrap();
+ assert!(manifest_in(&tmp.join("Cased"), "Cased").is_some());
+ assert!(manifest_in(&tmp.join("Bare"), "Bare").is_none());
+ let _ = std::fs::remove_dir_all(&tmp);
+ }
+
+ // ───────────────────────────── lifecycle events (1188 phase 1) ─────────────────────────────
+
+ /// A frame that appends `":"` to a global for every event it is registered for —
+ /// the witness the lifecycle tests read. Written as an addon file rather than injected into
+ /// the VM, so the events travel the same `` path a real addon's would.
+ fn recorder_xml(events: &[&str]) -> String {
+ let registers: String = events
+ .iter()
+ .map(|e| format!("this:RegisterEvent(\"{e}\");"))
+ .collect();
+ format!(
+ r#"
+
+
+ {registers}
+ table.insert(EventLog, event .. ":" .. tostring(arg1));
+
+
+"#
+ )
+ }
+
+ /// Read the witness back as a `Vec`.
+ fn event_log(script: &UiScript) -> Vec {
+ script
+ .eval::>("return EventLog")
+ .expect("EventLog")
+ }
+
+ /// **`ADDON_LOADED` reaches a third-party addon, carries its own name, and fires after its
+ /// files** — 1188 phase 1's acceptance test, and the half of 1178's falsifier that said
+ /// *"today it loads and reads; it does not receive"*.
+ ///
+ /// The `arg1` half is not cosmetic: every addon in the ecosystem opens with
+ /// `if arg1 == "MyAddon" then` and does nothing at all otherwise, so a wrong or missing arg1
+ /// is indistinguishable from the event never arriving.
+ #[test]
+ fn addon_loaded_carries_the_addons_own_name_and_fires_after_its_files() {
+ let tmp =
+ std::env::temp_dir().join(format!("benilla-addon-loaded-test-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&tmp);
+ let dir = tmp.join("EventProbe");
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(
+ dir.join("EventProbe.toc"),
+ "## Interface: 11200\nprobe.lua\nprobe.xml\n",
+ )
+ .unwrap();
+ // File-scope Lua, which the reference runs BEFORE the event. Recording the marker here and
+ // asserting it below is what proves the ordering rather than merely the delivery.
+ std::fs::write(
+ dir.join("probe.lua"),
+ "EventLog = {}\ntable.insert(EventLog, \"files-ran\")\n",
+ )
+ .unwrap();
+ std::fs::write(dir.join("probe.xml"), recorder_xml(&["ADDON_LOADED"])).unwrap();
+
+ let all = vec![Addon {
+ name: "EventProbe".into(),
+ toc: Toc::parse(&std::fs::read_to_string(dir.join("EventProbe.toc")).unwrap()),
+ source: Source::Dir(tmp.clone()),
+ }];
+ let mut script = UiScript::new().unwrap();
+ script.set_screen_size(1024.0, 768.0);
+ let mut w = Walk::default();
+ let _ = w.load(&mut script, &all, "EventProbe");
+ assert!(w.failures.is_empty(), "load errors: {:?}", w.failures);
+
+ assert_eq!(
+ event_log(&script),
+ vec!["files-ran", "ADDON_LOADED:EventProbe"],
+ "the addon's own files run first, THEN ADDON_LOADED with its own folder name as arg1 \
+ (`AddOn_Load 0x51f240`: files 0x51f3fa, event 0x51f5ad)"
+ );
+ }
+
+ /// **`benilla` never appears in an `ADDON_LOADED`.**
+ ///
+ /// FrameXML does not go through `AddOn_Load` and gets no such event (wow-5875-re
+ /// `system/ui/ui.md`), and our own interface is FrameXML's counterpart. An addon that watches
+ /// `ADDON_LOADED` to detect *another* addon would otherwise see a name no reference client
+ /// ever sends.
+ ///
+ /// The guard is structural — the builtin loads through [`super::manifest`] and never enters
+ /// [`Walk`] — so this asserts the structure holds rather than that a filter fires: it loads
+ /// the builtin's own manifest entries the way production does, alongside a real walk.
+ #[test]
+ fn the_builtin_interface_never_fires_addon_loaded() {
+ let tmp = std::env::temp_dir().join(format!(
+ "benilla-addon-builtin-silent-test-{}",
+ std::process::id()
+ ));
+ let _ = std::fs::remove_dir_all(&tmp);
+ let dir = tmp.join("EventProbe");
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(
+ dir.join("EventProbe.toc"),
+ "## Interface: 11200\nprobe.xml\n",
+ )
+ .unwrap();
+ std::fs::write(dir.join("probe.xml"), recorder_xml(&["ADDON_LOADED"])).unwrap();
+
+ let mut script = UiScript::new().unwrap();
+ script.set_screen_size(1024.0, 768.0);
+ script.run("EventLog = {}").unwrap();
+
+ let all = vec![Addon {
+ name: "EventProbe".into(),
+ toc: Toc::parse(&std::fs::read_to_string(dir.join("EventProbe.toc")).unwrap()),
+ source: Source::Dir(tmp.clone()),
+ }];
+ let mut w = Walk::default();
+ let _ = w.load(&mut script, &all, "EventProbe");
+ // The builtin, loaded the way production loads it — through its own manifest, not the walk.
+ let builtin = Addon::builtin();
+ let _ = builtin.load_files(&script, builtin.toc.files.get(..1).unwrap_or_default());
+
+ let log = event_log(&script);
+ assert!(
+ !log.iter().any(|e| e.contains("benilla")),
+ "benilla must never appear in an ADDON_LOADED: {log:?}"
+ );
+ assert_eq!(
+ log,
+ vec!["ADDON_LOADED:EventProbe"],
+ "exactly the one third-party addon announced itself"
+ );
+ }
+
+ /// **The three UI-init events fire in the reference's order:** every addon's `ADDON_LOADED`,
+ /// then `VARIABLES_LOADED`, then `PLAYER_LOGIN`.
+ ///
+ /// Byte-verified straight-line order inside `UI_Init 0x48fbf0` — `0x4900a3` loads the addons
+ /// (each `0x51f5ad`), `0x4900b2` fires `VARIABLES_LOADED`, `0x490168` enters the cascade that
+ /// fires `PLAYER_LOGIN`. This drives the production functions in the production order rather
+ /// than re-stating it: [`super::load_third_party`] then [`super::super::finish_ui_load`].
+ ///
+ /// It is the ordering an addon depends on — restore state on `ADDON_LOADED`, and by
+ /// `PLAYER_LOGIN` everything saved is in place.
+ #[test]
+ fn the_ui_init_events_fire_in_the_reference_order() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let tmp =
+ std::env::temp_dir().join(format!("benilla-addon-order-test-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&tmp);
+ let home = tmp.join("benilla-config");
+ let dir = home.join("AddOns").join("EventProbe");
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(
+ dir.join("EventProbe.toc"),
+ "## Interface: 11200\nprobe.xml\n",
+ )
+ .unwrap();
+ std::fs::write(
+ dir.join("probe.xml"),
+ recorder_xml(&["ADDON_LOADED", "VARIABLES_LOADED", "PLAYER_LOGIN"]),
+ )
+ .unwrap();
+
+ // Hermetic: point the whole state folder at the tempdir, so discovery finds exactly this
+ // addon and the saved-variables read cannot touch the machine's real file.
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let _h =
+ crate::local_state::test_env::EnvGuard::set("BENILLA_HOME", home.to_str().unwrap());
+
+ let mut script = UiScript::new().unwrap();
+ script.set_screen_size(1024.0, 768.0);
+ script.run("EventLog = {}").unwrap();
+ let failures = load_third_party(&mut script, None);
+ assert!(failures.is_empty(), "load errors: {failures:?}");
+ crate::ui_script::finish_ui_load(&mut script);
+
+ assert_eq!(
+ event_log(&script),
+ vec![
+ "ADDON_LOADED:EventProbe",
+ "VARIABLES_LOADED:nil",
+ "PLAYER_LOGIN:nil",
+ ],
+ "every non-LoadOnDemand addon's ADDON_LOADED precedes VARIABLES_LOADED, which \
+ precedes PLAYER_LOGIN"
+ );
+ let _ = std::fs::remove_dir_all(&tmp);
+ }
+
+ // ────────────────────────── the AddOn API + enable state (1188 phase 2) ──────────────────────
+
+ /// Write a whole AddOns root under a temp `benilla-config`, and point `BENILLA_HOME` at it.
+ /// Returns the guards, which must be held for the duration of the test.
+ fn hermetic_root(tag: &str) -> (PathBuf, crate::local_state::test_env::EnvGuard) {
+ // **The pid is load-bearing**, not decoration: two `benilla_app` test binaries can run at
+ // once (a concurrent session, or `--all-targets`), and a fixed path plus the
+ // `remove_dir_all` below means each wipes the other's tree mid-test. `ui_saved.rs` keys
+ // its temp the same way for the same reason.
+ let tmp =
+ std::env::temp_dir().join(format!("benilla-addon-api-{tag}-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&tmp);
+ let home = tmp.join("benilla-config");
+ std::fs::create_dir_all(home.join("AddOns")).unwrap();
+ let guard =
+ crate::local_state::test_env::EnvGuard::set("BENILLA_HOME", home.to_str().unwrap());
+ (home, guard)
+ }
+
+ fn write_addon(home: &Path, name: &str, toc: &str, files: &[(&str, &str)]) {
+ let dir = home.join("AddOns").join(name);
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join(format!("{name}.toc")), toc).unwrap();
+ for (f, body) in files {
+ std::fs::write(dir.join(f), body).unwrap();
+ }
+ }
+
+ /// [`write_addon`] with **raw bytes**, because the files this crate must survive are not text.
+ fn write_addon_bytes(home: &Path, name: &str, toc: &[u8], files: &[(&str, &[u8])]) {
+ let dir = home.join("AddOns").join(name);
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join(format!("{name}.toc")), toc).unwrap();
+ for (f, body) in files {
+ std::fs::write(dir.join(f), body).unwrap();
+ }
+ }
+
+ /// **An addon whose files are not valid UTF-8 loads anyway** (decision 1193) — the corpus's
+ /// single largest blocker, and one nobody would have guessed.
+ ///
+ /// Three separate failures, all of which used to be silent-ish and all of which are here:
+ ///
+ /// 1. a **cp1252 `.toc`** made the addon *invisible to discovery* — `manifest_in` read it with
+ /// `read_to_string`, got `None`, and a folder with no readable manifest is not an addon. 5
+ /// of a real 218-addon corpus vanished this way, and the harness scored them as clean
+ /// passes because an unparsed manifest lists no files to fail on.
+ /// 2. a **BOM'd `.lua`** reached the lexer with `EF BB BF` in front and died on
+ /// `unexpected symbol`. 160 corpus files carry one.
+ /// 3. a **cp1252 `.lua`** read as absent, so the loader reported "not found" for a file that
+ /// was right there. `AceAddon-2.0.lua` is one, embedded in ~30 addons.
+ ///
+ /// The literal survives as **bytes**, not as decoded text: Lua 5.0 strings are byte strings
+ /// and the reference hands `luaL_loadbuffer` the file unmodified, so `string.len` on a cp1252
+ /// literal must answer what it answers there. That assertion is the one that would fail if
+ /// somebody "helpfully" transcoded chunks to UTF-8 later.
+ #[test]
+ fn an_addon_whose_files_are_not_utf8_still_loads() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let (home, _guard) = hermetic_root("encoding");
+ // `## Notes:` holds a cp1252 a-umlaut (0xE4) — one byte, and the whole addon disappeared.
+ let mut toc = b"## Interface: 11200\n## Title: Sch\xE4tze\nlocale.lua\nboot.lua\n".to_vec();
+ toc.splice(0..0, [0xEFu8, 0xBB, 0xBF]); // ...and a BOM on the manifest too (14 in corpus).
+ write_addon_bytes(
+ &home,
+ "Umlaut",
+ &toc,
+ &[
+ // A cp1252 locale file: the German/French half of the vanilla ecosystem.
+ ("locale.lua", &b"UmlautWord = \"Sch\xE4tze\"\n"[..]),
+ // A BOM'd chunk: valid UTF-8, three bytes the lexer cannot start on.
+ (
+ "boot.lua",
+ &b"\xEF\xBB\xBFUmlautLoaded = true\nUmlautLen = string.len(UmlautWord)\n"[..],
+ ),
+ ],
+ );
+
+ let mut script = UiScript::new().unwrap();
+ let failures = load_third_party(&mut script, None);
+ assert!(failures.is_empty(), "load errors: {failures:?}");
+
+ // Discovery saw it at all — the `.toc` decoded rather than read as absent.
+ assert_eq!(script.eval::("return GetNumAddOns()").ok(), Some(1));
+ assert_eq!(
+ script
+ .eval::("return GetAddOnMetadata('Umlaut', 'Title')")
+ .ok(),
+ Some("Sch\u{e4}tze".to_string()),
+ "the manifest's cp1252 title decoded to the glyph its author typed, not to nothing"
+ );
+ // Both chunks ran.
+ assert_eq!(
+ script.eval::("return UmlautLoaded == true").ok(),
+ Some(true),
+ "a BOM'd .lua ran — the three-byte mark was stripped, not handed to the lexer"
+ );
+ // ...and the cp1252 literal is still BYTES, which is the reference's semantics.
+ assert_eq!(
+ script.eval::("return UmlautLen").ok(),
+ Some(7),
+ "`Sch\\xE4tze` is 7 bytes in the file and must be 7 bytes in Lua — transcoding the \
+ chunk to UTF-8 would make it 8 and silently move every string.sub in the addon"
+ );
+ }
+
+ /// The AddOns screen's write **merges** rather than replaces (decision 1197).
+ ///
+ /// A name in the file that is not installed right now belongs to an addon that will be again.
+ /// Rewriting the file from the installed list alone forgets the player's choice on every
+ /// uninstall — the same rule `parse_enable_state` already documents for the read side, and the
+ /// one that has to hold on the write side or the read's carefulness is wasted.
+ #[test]
+ fn the_addons_screen_write_merges_with_what_is_already_on_disk() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let (home, _guard) = hermetic_root("screenwrite");
+ let id = ("Realm".to_string(), "Char".to_string());
+ let path = enable_state_path(Some(&id)).unwrap();
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::fs::write(&path, "Gone: disabled\nStays: enabled\n").unwrap();
+
+ write_enable_state(Some(&id), &[("Stays".into(), false), ("New".into(), true)]);
+
+ let written = std::fs::read_to_string(&path).unwrap();
+ assert!(
+ written.contains("Gone: disabled"),
+ "an uninstalled addon's choice survives — {written:?}"
+ );
+ assert!(
+ written.contains("Stays: disabled"),
+ "the edit applied — {written:?}"
+ );
+ assert!(
+ written.contains("New: enabled"),
+ "a new row appended — {written:?}"
+ );
+ let _ = home;
+ }
+
+ /// **`GetAddOnInfo` returns the manifest's own `## Title` and `## Notes`** — 1188 phase 2's
+ /// first acceptance test, and the reason the registry carries directives rather than a name.
+ ///
+ /// The return shape is the reference's, read off `Interface\GlueXML\AddonList.lua`:
+ /// `name, title, notes, url, loadable, reason, security, newVersion`. A row that shifts by one
+ /// puts an addon's notes in its URL field, which nothing would notice until a manager rendered
+ /// it, so the whole tuple is asserted rather than the two fields the phase names.
+ #[test]
+ fn get_addon_info_returns_the_manifests_own_title_and_notes() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("info");
+ write_addon(
+ &home,
+ "Probe",
+ "## Interface: 11200\n## Title: Probe Title\n## Notes: What it does\n## URL: http://example\n## Version: 1.2\n",
+ &[],
+ );
+ let mut script = UiScript::new().unwrap();
+ let _ = load_third_party(&mut script, None);
+
+ assert_eq!(script.eval::("return GetNumAddOns()").ok(), Some(1));
+ assert_eq!(
+ script
+ .eval::>(
+ "local n,t,no,u,l,r,s,nv = GetAddOnInfo(1) \
+ return { n, t, no, u, tostring(l), tostring(r), s, tostring(nv) }"
+ )
+ .ok(),
+ Some(vec![
+ "Probe".into(),
+ "Probe Title".into(),
+ "What it does".into(),
+ "http://example".into(),
+ "1".into(), // loadable
+ "nil".into(), // no reason — it loads
+ "INSECURE".into(),
+ "nil".into(), // no version-check service to report a new version
+ ])
+ );
+ // Both spellings of the argument, and the raw directives.
+ assert_eq!(
+ script
+ .eval::("return (GetAddOnInfo('probe'))")
+ .ok()
+ .as_deref(),
+ Some("Probe"),
+ "index OR name, case-insensitively — the reference's verbs all take either"
+ );
+ assert_eq!(
+ script
+ .eval::("return GetAddOnMetadata(1, 'Version')")
+ .ok()
+ .as_deref(),
+ Some("1.2")
+ );
+ assert_eq!(
+ script
+ .eval::("return IsAddOnLoaded('Probe') == 1")
+ .ok(),
+ Some(true)
+ );
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+
+ /// **Disabling an addon in the enable-state file stops it loading** — phase 2's second
+ /// acceptance test.
+ ///
+ /// The file is the reference's own `AddOns.txt` format, `: enabled|disabled` per line,
+ /// confirmed against a real 1.12 install. It also checks the direction that matters more in
+ /// practice: an addon the file does not mention loads, so dropping a folder in just works.
+ #[test]
+ fn a_disabled_addon_does_not_load_and_an_unlisted_one_does() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("disable");
+ write_addon(
+ &home,
+ "Off",
+ "## Interface: 11200\nran.lua\n",
+ &[("ran.lua", "OffRan = true")],
+ );
+ write_addon(
+ &home,
+ "On",
+ "## Interface: 11200\nran.lua\n",
+ &[("ran.lua", "OnRan = true")],
+ );
+ // "On" is deliberately absent from the file.
+ std::fs::create_dir_all(home.join("addons")).unwrap();
+ std::fs::write(home.join("addons/Realm-Char.txt"), "Off: disabled\n").unwrap();
+
+ let mut script = UiScript::new().unwrap();
+ let id = ("Realm".to_string(), "Char".to_string());
+ let failures = load_third_party(&mut script, Some(&id));
+
+ assert!(
+ failures.is_empty(),
+ "a disabled addon is a player's choice, never a load failure: {failures:?}"
+ );
+ assert_eq!(
+ script.eval::("return OffRan == nil").ok(),
+ Some(true),
+ "the disabled addon's files must not have run"
+ );
+ assert_eq!(
+ script.eval::("return OnRan == true").ok(),
+ Some(true),
+ "an addon the file never mentions is enabled — a dropped-in folder just works"
+ );
+ // And the API agrees with the loader about what happened.
+ assert_eq!(
+ script
+ .eval::>(
+ "local _,_,_,_,l,r = GetAddOnInfo('Off') return { tostring(l), tostring(r) }"
+ )
+ .ok(),
+ Some(vec!["nil".into(), "DISABLED".into()]),
+ "not loadable, with the reference's own reason token"
+ );
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+
+ /// **A LoadOnDemand addon loads on `LoadAddOn` and not before** — phase 2's third acceptance
+ /// test, and the one that needs the loader to run from inside a Lua binding.
+ ///
+ /// `LoadAddOn` is called *from Lua*, synchronously, and its side effects are asserted on the
+ /// next line — exactly how `UIParentLoadAddOn` uses it. A deferred implementation would fail
+ /// here, which is the point.
+ #[test]
+ fn a_load_on_demand_addon_loads_only_when_asked() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("lod");
+ write_addon(
+ &home,
+ "Demand",
+ "## Interface: 11200\n## Title: Demand\n## LoadOnDemand: 1\nlate.lua\nlate.xml\n",
+ &[
+ ("late.lua", "DemandRan = true"),
+ (
+ "late.xml",
+ "\
+ DemandEvent = arg1\
+ ",
+ ),
+ ],
+ );
+ let mut script = UiScript::new().unwrap();
+ script.set_screen_size(1024.0, 768.0);
+ let _ = load_third_party(&mut script, None);
+
+ // Discovered and described, but NOT run.
+ assert_eq!(script.eval::("return GetNumAddOns()").ok(), Some(1));
+ assert_eq!(
+ script
+ .eval::("return IsAddOnLoadOnDemand(1) == 1")
+ .ok(),
+ Some(true)
+ );
+ assert_eq!(
+ script.eval::("return DemandRan == nil").ok(),
+ Some(true),
+ "a LoadOnDemand addon must not run at startup"
+ );
+ assert_eq!(
+ script
+ .eval::("return IsAddOnLoaded('Demand') == nil")
+ .ok(),
+ Some(true)
+ );
+
+ // ...then loads synchronously, from Lua, and is usable on the very next statement.
+ assert_eq!(
+ script
+ .eval::>(
+ "local loaded, reason = LoadAddOn('Demand') \
+ return { tostring(loaded), tostring(reason), tostring(DemandRan), \
+ tostring(getglobal('DemandFrame') ~= nil) }"
+ )
+ .ok(),
+ Some(vec!["1".into(), "nil".into(), "true".into(), "true".into()]),
+ "LoadAddOn returns loaded=1 and its files have ALREADY run when it returns — the \
+ reference's UIParentLoadAddOn uses the addon's frames on the next line"
+ );
+ assert_eq!(
+ script
+ .eval::("return IsAddOnLoaded('Demand') == 1")
+ .ok(),
+ Some(true)
+ );
+ // A demand load fires ADDON_LOADED too, at the same position: after the files.
+ assert_eq!(
+ script.eval::("return DemandEvent").ok().as_deref(),
+ Some("Demand"),
+ "the addon's own frame, registered by its own XML, saw its own ADDON_LOADED"
+ );
+ // A second load is a no-op that still answers success, as the reference does.
+ assert_eq!(
+ script
+ .eval::("local l = LoadAddOn('Demand') return tostring(l)")
+ .ok()
+ .as_deref(),
+ Some("1")
+ );
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+
+ /// The reason tokens are the reference's own, for the cases a caller branches on.
+ ///
+ /// They are not free-form strings: the reference splices them into
+ /// `getglobal("ADDON_"..reason)` to find a localized label, so a token we invent renders as a
+ /// nil global in somebody's addon manager.
+ #[test]
+ fn load_addon_answers_with_the_references_reason_tokens() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("reasons");
+ write_addon(&home, "Plain", "## Interface: 11200\n", &[]);
+ write_addon(
+ &home,
+ "Off",
+ "## Interface: 11200\n## LoadOnDemand: 1\n",
+ &[],
+ );
+ std::fs::create_dir_all(home.join("addons")).unwrap();
+ std::fs::write(home.join("addons/Realm-Char.txt"), "Off: disabled\n").unwrap();
+
+ let mut script = UiScript::new().unwrap();
+ let id = ("Realm".to_string(), "Char".to_string());
+ let _ = load_third_party(&mut script, Some(&id));
+
+ for (call, want) in [
+ ("LoadAddOn('NoSuchAddon')", "MISSING"),
+ ("LoadAddOn('Off')", "DISABLED"),
+ // Loaded at startup because it is not LoadOnDemand, so this is the success case; the
+ // NOT_DEMAND_LOADED arm needs an addon that is neither loaded nor LoadOnDemand, which
+ // cannot happen once it is enabled — asserted through the disabled one above instead.
+ ("LoadAddOn('Plain')", "nil"),
+ ] {
+ assert_eq!(
+ script
+ .eval::(&format!("local _, r = {call} return tostring(r)"))
+ .ok()
+ .as_deref(),
+ Some(want),
+ "{call}"
+ );
+ }
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+
+ /// Enable state survives the session: `DisableAddOn` from Lua is written back in the
+ /// reference's own `AddOns.txt` format, and read back as a disable next time.
+ #[test]
+ fn the_enable_state_round_trips_through_the_file() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("roundtrip");
+ write_addon(&home, "Keep", "## Interface: 11200\n", &[]);
+ write_addon(
+ &home,
+ "Drop",
+ "## Interface: 11200\nran.lua\n",
+ &[("ran.lua", "DropRan = true")],
+ );
+ let id = ("Realm".to_string(), "Char".to_string());
+
+ let mut script = UiScript::new().unwrap();
+ let _ = load_third_party(&mut script, Some(&id));
+ script.run("DisableAddOn('Drop')").unwrap();
+ save_enable_state(&script, Some(&id));
+
+ let written = std::fs::read_to_string(home.join("addons/Realm-Char.txt")).unwrap();
+ assert_eq!(
+ written, "Drop: disabled\nKeep: enabled\n",
+ "the reference's own one-line-per-addon format"
+ );
+
+ // A fresh session reads it back and the addon stays off.
+ let mut next = UiScript::new().unwrap();
+ let _ = load_third_party(&mut next, Some(&id));
+ assert_eq!(
+ next.eval::("return DropRan == nil").ok(),
+ Some(true),
+ "the disable survived the session"
+ );
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+
+ // ───────────────────────────── saved variables (1188 phase 3) ─────────────────────────────
+
+ /// **An addon sets a saved variable, the session ends, a new session sees it** — 1188 phase 3's
+ /// acceptance test, end to end through the production path.
+ ///
+ /// It also pins the two orderings that make the mechanism work, both byte-verified:
+ /// *account file then per-character file* (`0x51f4b5`, `0x51f53b`), so a per-character value
+ /// wins; and *files → saved variables → `ADDON_LOADED`* (`0x51f5ad`), so the addon's own
+ /// file-scope default is assigned first, overwritten second, and read by the handler third.
+ /// Reverse either and the saved value can never win — which is a bug that looks exactly like
+ /// "settings do not persist".
+ #[test]
+ fn an_addons_saved_variables_survive_the_session() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("saved");
+ write_addon(
+ &home,
+ "Keeper",
+ "## Interface: 11200\n## SavedVariables: KeeperDB\n\
+ ## SavedVariablesPerCharacter: KeeperChar\nkeeper.lua\nkeeper.xml\n",
+ &[
+ // File-scope defaults, exactly where the reference puts them.
+ (
+ "keeper.lua",
+ "KeeperDB = { count = 0 }\nKeeperChar = 'default'\n",
+ ),
+ (
+ "keeper.xml",
+ "\
+ KeeperSawAtEvent = KeeperDB.count\
+ ",
+ ),
+ ],
+ );
+ let id = ("Realm".to_string(), "Char".to_string());
+
+ // ── session one: defaults, then the addon changes them ──
+ let mut script = UiScript::new().unwrap();
+ script.set_screen_size(1024.0, 768.0);
+ let failures = load_third_party(&mut script, Some(&id));
+ assert!(failures.is_empty(), "{failures:?}");
+ assert_eq!(
+ script.eval::("return KeeperSawAtEvent").ok(),
+ Some(0),
+ "first run: no file yet, so ADDON_LOADED sees the file-scope default"
+ );
+ script
+ .run("KeeperDB.count = 7 KeeperDB.note = 'hi' KeeperChar = 'mine'")
+ .unwrap();
+ crate::ui_script::shutdown_ui_state(&mut script, Some(&id));
+
+ // ── session two: a fresh VM reads them back ──
+ let mut next = UiScript::new().unwrap();
+ next.set_screen_size(1024.0, 768.0);
+ let failures = load_third_party(&mut next, Some(&id));
+ assert!(failures.is_empty(), "{failures:?}");
+ assert_eq!(
+ next.eval::("return KeeperDB.count").ok(),
+ Some(7),
+ "the saved value overwrote the addon's own file-scope default"
+ );
+ assert_eq!(
+ next.eval::("return KeeperDB.note").ok().as_deref(),
+ Some("hi"),
+ "a table round-trips whole, not just the scalar that changed"
+ );
+ assert_eq!(
+ next.eval::("return KeeperChar").ok().as_deref(),
+ Some("mine"),
+ "the per-character file loaded too"
+ );
+ assert_eq!(
+ next.eval::("return KeeperSawAtEvent").ok(),
+ Some(7),
+ "ADDON_LOADED handlers see the RESTORED value — the whole reason the event is last"
+ );
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+
+ /// The written bytes are the recorded grammar (decision 1128's, itself byte-verified against
+ /// `0x7043f0`/`0x704480`): `NAME = value`, bracketed keys, TAB indent, a trailing comma on
+ /// every entry, and the file split by scope.
+ ///
+ /// Asserted as bytes rather than by re-reading, because "it round-trips through our own
+ /// loader" is exactly the check that passes for a private format. The reference's own client
+ /// has to be able to read this.
+ #[test]
+ fn the_saved_file_bytes_match_the_recorded_grammar() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("grammar");
+ write_addon(
+ &home,
+ "Gram",
+ "## Interface: 11200\n## SavedVariables: GramDB\n\
+ ## SavedVariablesPerCharacter: GramChar\ngram.lua\n",
+ &[("gram.lua", "GramDB = {}\nGramChar = 0\n")],
+ );
+ let id = ("Realm".to_string(), "Char".to_string());
+ let mut script = UiScript::new().unwrap();
+ let _ = load_third_party(&mut script, Some(&id));
+ script
+ .run("GramDB = { ['on'] = true, ['n'] = 2, ['s'] = 'a\\\"b', ['t'] = { 1 } } GramChar = 5")
+ .unwrap();
+ crate::ui_script::shutdown_ui_state(&mut script, Some(&id));
+
+ let account = std::fs::read_to_string(home.join("saved/Gram.lua")).unwrap();
+ let body = account.lines().skip(2).collect::>().join("\n");
+ assert_eq!(
+ body,
+ "GramDB = {\n\
+ \t[\"n\"] = 2,\n\
+ \t[\"on\"] = true,\n\
+ \t[\"s\"] = \"a\\\"b\",\n\
+ \t[\"t\"] = {\n\
+ \t\t[1] = 1,\n\
+ \t},\n\
+ }",
+ "keys always bracketed and quoted, TAB indent per level, trailing comma on every \
+ entry, `\\\"` escaped — the recorded grammar\nGOT:\n{account}"
+ );
+ // Scope split: the per-character global is in the per-character file and nowhere else.
+ assert!(!account.contains("GramChar"), "account file: {account}");
+ let per_char = std::fs::read_to_string(home.join("saved/Realm-Char/Gram.lua")).unwrap();
+ assert!(
+ per_char.ends_with("GramChar = 5\n"),
+ "per-char file: {per_char}"
+ );
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+
+ /// `PLAYER_LOGOUT` fires **before** the writes — an addon's last chance to mutate a saved
+ /// global, and worthless if it fires after.
+ ///
+ /// The reference's own tail is `PLAYER_LEAVING_WORLD` → `PLAYER_LOGOUT` → the writes
+ /// (`0x490c2a` before `0x490c7e`/`0x490c83`). This asserts the observable consequence rather
+ /// than the call order: a value set *in the handler* has to reach the file.
+ #[test]
+ fn player_logout_fires_before_the_write() {
+ let _l = crate::local_state::test_env::ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
+ let (home, _h) = hermetic_root("logout");
+ write_addon(
+ &home,
+ "Last",
+ "## Interface: 11200\n## SavedVariables: LastDB\nlast.lua\nlast.xml\n",
+ &[
+ ("last.lua", "LastDB = 'unset'"),
+ (
+ "last.xml",
+ "\
+ if event == \"PLAYER_LOGOUT\" then LastDB = 'written at logout' end\
+ \
+ ",
+ ),
+ ],
+ );
+ let id = ("Realm".to_string(), "Char".to_string());
+ let mut script = UiScript::new().unwrap();
+ script.set_screen_size(1024.0, 768.0);
+ let _ = load_third_party(&mut script, Some(&id));
+ crate::ui_script::shutdown_ui_state(&mut script, Some(&id));
+
+ let written = std::fs::read_to_string(home.join("saved/Last.lua")).unwrap();
+ assert!(
+ written.contains("LastDB = \"written at logout\""),
+ "the value the PLAYER_LOGOUT handler set must reach the file — if the write ran \
+ first this reads 'unset':\n{written}"
+ );
+ let _ = std::fs::remove_dir_all(home.parent().unwrap());
+ }
+}
diff --git a/crates/benilla-app/src/ui_script/bag_tests.rs b/crates/benilla-app/src/ui_script/bag_tests.rs
index 265a32b8..da7e45c0 100644
--- a/crates/benilla-app/src/ui_script/bag_tests.rs
+++ b/crates/benilla-app/src/ui_script/bag_tests.rs
@@ -23,7 +23,7 @@ fn load_xml(s: &UiScript, file: &str) -> usize {
}
/// The equipped-bag BAR icons must draw ABOVE the action-bar art, not under it. The bar buttons are
-/// relocated onto `BenillaActionBarArtFrame` but are top-level frames, so they default to a lower
+/// relocated onto `MainMenuBarArtFrame` but are top-level frames, so they default to a lower
/// frame level than the bar's own child-hierarchy art (the ExpBar dwarf notches + metal/well art) —
/// which would then paint over the centered icons, leaving the ring but no bag icon. The OnLoad
/// `BenillaActionBarArt_SeatAbove` seats them one level above the art (the action buttons' level).
@@ -34,7 +34,7 @@ fn bag_bar_icons_draw_above_the_action_bar_art() {
// The screen the client defaults to; the action bar centers here and the bag bar lands over its
// right end, where the dwarf-notch strip overlaps — the exact geometry that reproduced the bug.
s.set_screen_size(1600.0, 900.0);
- // ActionBar.xml carries both the anchor target (BenillaActionBarArtFrame) and the occluder (the
+ // ActionBar.xml carries both the anchor target (MainMenuBarArtFrame) and the occluder (the
// ExpBar dwarf art); MerchantFrame.xml is BagFrame's documented purse-helper dep.
for file in [
"Fonts.xml",
@@ -784,7 +784,7 @@ fn drag_across_two_slots_queues_the_same_move_a_click_pickup_would() {
}
/// A second bag window (decision 0216 slice 2): bag 1's snapshot feeds through the SAME
-/// C_Container/BenillaBagWindow_Update plumbing the backpack uses, opened via the bag-bar path
+/// container/BenillaBagWindow_Update plumbing the backpack uses, opened via the bag-bar path
/// (`BenillaBagBarSlot_OnClick`, not the backpack toggle) and painting its own slot 1 icon.
#[test]
fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
@@ -981,28 +981,21 @@ fn shift_click_on_a_stack_opens_the_split_frame() {
s.set_screen_size(1024.0, 768.0);
let (x, y) = open_backpack_with_a_five_stack(&mut s);
- assert!(!s
- .eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap());
+ assert!(!s.eval::("return StackSplitFrame:IsShown()").unwrap());
s.set_modifiers(true, false, false);
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
s.set_modifiers(false, false, false);
assert!(
- s.eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap(),
+ s.eval::("return StackSplitFrame:IsShown()").unwrap(),
"shift-click opened the split frame"
);
assert!(
s.cursor_item().is_none(),
"the shift fork never picks the stack up"
);
- assert_eq!(
- s.eval::("return BenillaStackSplitFrame.maxStack")
- .unwrap(),
- 5
- );
+ assert_eq!(s.eval::("return StackSplitFrame.maxStack").unwrap(), 5);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -1018,22 +1011,15 @@ fn split_okay_then_a_placement_queues_the_split_move() {
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
s.set_modifiers(false, false, false);
- assert!(s
- .eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap());
+ assert!(s.eval::("return StackSplitFrame:IsShown()").unwrap());
// Bump the spinner from 1 to 3, then Okay — the carry lands on the cursor.
s.run("BenillaStackSplitRight_Click()").unwrap();
s.run("BenillaStackSplitRight_Click()").unwrap();
- assert_eq!(
- s.eval::("return BenillaStackSplitFrame.split")
- .unwrap(),
- 3
- );
+ assert_eq!(s.eval::("return StackSplitFrame.split").unwrap(), 3);
s.run("BenillaStackSplitOkay_Click()").unwrap();
assert!(
- !s.eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap(),
+ !s.eval::("return StackSplitFrame:IsShown()").unwrap(),
"Okay hides the spinner"
);
let held = s.cursor_item().expect("Okay picked up the split carry");
@@ -1078,9 +1064,7 @@ fn a_plain_click_hides_an_open_split_frame() {
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
s.set_modifiers(false, false, false);
- assert!(s
- .eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap());
+ assert!(s.eval::("return StackSplitFrame:IsShown()").unwrap());
s.run(
"BENILLA_TEST_B9 = nil\n\
@@ -1092,8 +1076,7 @@ fn a_plain_click_hides_an_open_split_frame() {
s.run("BenillaBagSlot_OnClick(BENILLA_TEST_B9, \"LeftButton\")")
.unwrap();
assert!(
- !s.eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap(),
+ !s.eval::("return StackSplitFrame:IsShown()").unwrap(),
"the plain click on an unrelated slot hid the spinner"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -1321,10 +1304,9 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
// Keyless: the button is hidden and the plate is the dwarf one.
s.set_has_key(false);
- s.run("BenillaMainMenuBar_UpdateKeyRing()").unwrap();
+ s.run("MainMenuBar_UpdateKeyRing()").unwrap();
assert!(
- !s.eval::("return BenillaKeyRingButton:IsShown()")
- .unwrap(),
+ !s.eval::("return KeyRingButton:IsShown()").unwrap(),
"no key ⇒ no keyring button"
);
assert_eq!(
@@ -1339,8 +1321,7 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
s.set_container(-2, Some(keyring(8, true)));
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(-2)]);
assert!(
- s.eval::("return BenillaKeyRingButton:IsShown()")
- .unwrap(),
+ s.eval::("return KeyRingButton:IsShown()").unwrap(),
"the first key reveals the keyring button"
);
assert_eq!(
@@ -1349,8 +1330,8 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
"exactly the two RIGHT-hand strips swap to the keyring plate"
);
for (strip, top, bottom) in [
- ("BenillaActionBarTexture2", 0.6640625, 1.0),
- ("BenillaActionBarTexture3", 0.1640625, 0.5),
+ ("MainMenuBarTexture2", 0.6640625, 1.0),
+ ("MainMenuBarTexture3", 0.1640625, 0.5),
] {
let (t, b) = s
.eval::<(f64, f64)>(&format!(
@@ -1367,10 +1348,8 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
// edge at -234 from the bar's right; ours steps 36px buttons with -6 gaps and lands at -235.
// Within a pixel of the real bar — which is the check that matters, because the socket the
// button sits in is PAINTED INTO the keyring plate and cannot be nudged to meet it.
- let bar_right = s.eval::("return BenillaActionBar:GetRight()").unwrap();
- let button_left = s
- .eval::("return BenillaKeyRingButton:GetLeft()")
- .unwrap();
+ let bar_right = s.eval::("return MainMenuBar:GetRight()").unwrap();
+ let button_left = s.eval::("return KeyRingButton:GetLeft()").unwrap();
assert!(
(bar_right - button_left - 235.0).abs() < 1.5,
"the keyring button must land in the plate's painted socket — the ref's own -234, got {}",
@@ -1379,9 +1358,9 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
// The performance meter clears the new socket (ref l.180: -227 → -235).
let perf_right = s
- .eval::("return BenillaPerformanceBarFrame:GetRight()")
+ .eval::("return MainMenuBarPerformanceBarFrame:GetRight()")
.unwrap();
- let bar_right = s.eval::("return BenillaActionBar:GetRight()").unwrap();
+ let bar_right = s.eval::("return MainMenuBar:GetRight()").unwrap();
assert!(
(bar_right - perf_right - 235.0).abs() < 0.01,
"the meter slid to -235 with the keyring up (got {})",
@@ -1389,12 +1368,11 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
);
// And it reverts: destroying the last key takes the keyring back off the bar (our divergence
- // from the ref's one-way saved-variable latch — see BenillaMainMenuBar_UpdateKeyRing).
+ // from the ref's one-way saved-variable latch — see MainMenuBar_UpdateKeyRing).
s.set_has_key(false);
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(-2)]);
assert!(
- !s.eval::("return BenillaKeyRingButton:IsShown()")
- .unwrap(),
+ !s.eval::("return KeyRingButton:IsShown()").unwrap(),
"losing the last key takes the button away again"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -1418,7 +1396,7 @@ fn the_keyring_button_opens_a_keyring_window() {
assert!(!s
.eval::("return BenillaKeyRingFrame:IsShown()")
.unwrap());
- s.run("BenillaKeyRingButton_OnClick(BenillaKeyRingButton)")
+ s.run("BenillaKeyRingButton_OnClick(KeyRingButton)")
.unwrap();
assert!(
s.eval::("return BenillaKeyRingFrame:IsShown()")
@@ -1451,12 +1429,12 @@ fn the_keyring_button_opens_a_keyring_window() {
assert_eq!(shown, 8, "only the level-unlocked keyring slots are drawn");
// The key is in the window (slot 1 of 8 is the LAST chain button — size - physIndex + 1).
assert_eq!(
- s.eval::("return C_Container.GetContainerItemID(KEYRING_CONTAINER, 1)")
+ s.eval::("return BenillaGetContainerItemID(KEYRING_CONTAINER, 1)")
.unwrap(),
7146
);
- s.run("BenillaKeyRingButton_OnClick(BenillaKeyRingButton)")
+ s.run("BenillaKeyRingButton_OnClick(KeyRingButton)")
.unwrap();
assert!(!s
.eval::("return BenillaKeyRingFrame:IsShown()")
@@ -1497,10 +1475,10 @@ fn a_key_dropped_on_the_button_files_itself() {
}),
);
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(-2)]);
- s.run("C_Container.PickupContainerItem(0, 3)").unwrap();
+ s.run("PickupContainerItem(0, 3)").unwrap();
assert!(s.eval::("return CursorHasItem()").unwrap());
- s.run("BenillaKeyRingButton_OnClick(BenillaKeyRingButton)")
+ s.run("BenillaKeyRingButton_OnClick(KeyRingButton)")
.unwrap();
assert_eq!(
s.take_container_moves(),
@@ -1527,15 +1505,15 @@ fn a_key_dropped_on_the_button_files_itself() {
);
}
s.set_container(-2, Some(full));
- s.run("C_Container.PickupContainerItem(0, 3)").unwrap();
- s.run("BenillaKeyRingButton_OnClick(BenillaKeyRingButton)")
+ s.run("PickupContainerItem(0, 3)").unwrap();
+ s.run("BenillaKeyRingButton_OnClick(KeyRingButton)")
.unwrap();
assert!(
s.take_container_moves().is_empty(),
"a full keyring queues no move"
);
assert_eq!(
- s.eval::("return BenillaErrorsFrameLine1:GetText()")
+ s.eval::("return UIErrorsFrameLine1:GetText()")
.unwrap(),
"Your keyring is full.",
"and says so — the reference's own dangling string name prints nothing"
@@ -1591,11 +1569,7 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
};
// Nothing is animating until a push arrives.
- for b in [
- "BenillaBagToggle",
- "BenillaBagBarSlot2",
- "BenillaKeyRingButton",
- ] {
+ for b in ["BenillaBagToggle", "BenillaBagBarSlot2", "KeyRingButton"] {
assert!(!shown(&s, b), "{b}'s card starts hidden");
}
@@ -1609,11 +1583,7 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
);
s.resolve();
assert!(shown(&s, "BenillaBagBarSlot2"), "bag 2's card plays");
- for b in [
- "BenillaBagToggle",
- "BenillaBagBarSlot1",
- "BenillaKeyRingButton",
- ] {
+ for b in ["BenillaBagToggle", "BenillaBagBarSlot1", "KeyRingButton"] {
assert!(!shown(&s, b), "{b} took nothing, so {b} animates nothing");
}
// The pushed icon reaches the RENDERER, not just the Lua state — exactly one quad carries it.
@@ -1725,7 +1695,7 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
benilla_ui::script::ScriptValue::Str("Interface\\Icons\\INV_Misc_Key_03".into()),
],
);
- assert!(shown(&s, "BenillaKeyRingButton"), "the keyring card plays");
+ assert!(shown(&s, "KeyRingButton"), "the keyring card plays");
assert!(
!shown(&s, "BenillaBagToggle"),
"and the backpack's does not"
diff --git a/crates/benilla-app/src/ui_script/bank_tests.rs b/crates/benilla-app/src/ui_script/bank_tests.rs
index bfe191c7..03e3213b 100644
--- a/crates/benilla-app/src/ui_script/bank_tests.rs
+++ b/crates/benilla-app/src/ui_script/bank_tests.rs
@@ -60,13 +60,11 @@ fn bankframe_opened_shows_sets_title_and_opens_backpack() {
// Pre-open default: the XML-authored placeholder.
assert_eq!(
- s.eval::("return BenillaBankFrameTitleText:GetText()")
+ s.eval::("return BankFrameTitleText:GetText()")
.unwrap(),
"Banker"
);
- assert!(!s
- .eval::("return BenillaBankFrame:IsVisible()")
- .unwrap());
+ assert!(!s.eval::("return BankFrame:IsVisible()").unwrap());
let _ = s.take_sounds(); // ignore anything from load (every frame is hidden; nothing should fire)
@@ -78,11 +76,9 @@ fn bankframe_opened_shows_sets_title_and_opens_backpack() {
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
- assert!(s
- .eval::("return BenillaBankFrame:IsVisible()")
- .unwrap());
+ assert!(s.eval::("return BankFrame:IsVisible()").unwrap());
assert_eq!(
- s.eval::("return BenillaBankFrameTitleText:GetText()")
+ s.eval::("return BankFrameTitleText:GetText()")
.unwrap(),
"Grumnus Steelshaper"
);
@@ -91,7 +87,7 @@ fn bankframe_opened_shows_sets_title_and_opens_backpack() {
"opening the bank opens the backpack (the 0561 OpenBackpack contract)"
);
assert!(
- s.eval::("return GetLeftFrame() == BenillaBankFrame")
+ s.eval::("return GetLeftFrame() == BankFrame")
.unwrap(),
"ShowUIPanel landed the bank at the left slot"
);
@@ -130,8 +126,7 @@ fn bankframe_closed_queues_close_closes_open_popouts_and_plays_the_close_kit() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
- !s.eval::("return BenillaBankFrame:IsVisible()")
- .unwrap(),
+ !s.eval::("return BankFrame:IsVisible()").unwrap(),
"BANKFRAME_CLOSED hides the window"
);
assert!(
@@ -271,7 +266,7 @@ fn bag_buttons_tint_by_purchase_count_and_texture_from_the_bank_bag_feed() {
};
// Button 1: its own bag icon (BenillaGetBankBagTexture(1)), purchased -> white.
- let (x1, y1) = center("BenillaBankBagButton1");
+ let (x1, y1) = center("BankBagButton1");
let c1 = color_at(x1, y1, "INV_Misc_Bag_08").expect("button 1 shows its fed bag icon");
assert!(
near(c1, 1.0, 1.0, 1.0),
@@ -279,7 +274,7 @@ fn bag_buttons_tint_by_purchase_count_and_texture_from_the_bank_bag_feed() {
);
// Button 2 (purchased, no bag fed): the fallback texture, still white.
- let (x2, y2) = center("BenillaBankBagButton2");
+ let (x2, y2) = center("BankBagButton2");
let c2 =
color_at(x2, y2, "PaperDoll-Slot-Bag").expect("button 2 shows the empty-slot fallback");
assert!(
@@ -289,10 +284,10 @@ fn bag_buttons_tint_by_purchase_count_and_texture_from_the_bank_bag_feed() {
// Buttons 3..6 (unpurchased): the fallback texture, tinted the reference's exact red.
for name in [
- "BenillaBankBagButton3",
- "BenillaBankBagButton4",
- "BenillaBankBagButton5",
- "BenillaBankBagButton6",
+ "BankBagButton3",
+ "BankBagButton4",
+ "BankBagButton5",
+ "BankBagButton6",
] {
let (x, y) = center(name);
let c = color_at(x, y, "PaperDoll-Slot-Bag")
@@ -319,7 +314,7 @@ fn purchase_flow_shows_popup_queues_the_intent_and_the_row_hides_when_full() {
s.fire_event("BANKFRAME_OPENED", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
- s.eval::("return BenillaBankFramePurchaseInfo:IsShown()")
+ s.eval::("return BankFramePurchaseInfo:IsShown()")
.unwrap(),
"the purchase row shows while unfilled"
);
@@ -354,7 +349,7 @@ fn purchase_flow_shows_popup_queues_the_intent_and_the_row_hides_when_full() {
s.fire_event("PLAYERBANKBAGSLOTS_CHANGED", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
- !s.eval::("return BenillaBankFramePurchaseInfo:IsShown()")
+ !s.eval::("return BankFramePurchaseInfo:IsShown()")
.unwrap(),
"the purchase row hides once full (6 purchased)"
);
@@ -377,8 +372,8 @@ fn a_bank_bag_button_answers_the_right_button_too() {
let (cx, cy): (f64, f64) = s
.eval(
- "return (BenillaBankBagButton1:GetLeft() + BenillaBankBagButton1:GetRight()) / 2, \
- (BenillaBankBagButton1:GetTop() + BenillaBankBagButton1:GetBottom()) / 2",
+ "return (BankBagButton1:GetLeft() + BankBagButton1:GetRight()) / 2, \
+ (BankBagButton1:GetTop() + BankBagButton1:GetBottom()) / 2",
)
.unwrap();
s.mouse_button(cx as f32, cy as f32, "RightButton", true);
@@ -430,8 +425,8 @@ fn clicking_item_slot_one_with_an_empty_cursor_queues_the_pickup() {
let (cx, cy): (f64, f64) = s
.eval(
- "return (BenillaBankItem1:GetLeft() + BenillaBankItem1:GetRight()) / 2, \
- (BenillaBankItem1:GetTop() + BenillaBankItem1:GetBottom()) / 2",
+ "return (BankItem1:GetLeft() + BankItem1:GetRight()) / 2, \
+ (BankItem1:GetTop() + BankItem1:GetBottom()) / 2",
)
.unwrap();
s.mouse_button(cx as f32, cy as f32, "LeftButton", true);
diff --git a/crates/benilla-app/src/ui_script/basic_controls_tests.rs b/crates/benilla-app/src/ui_script/basic_controls_tests.rs
new file mode 100644
index 00000000..38526460
--- /dev/null
+++ b/crates/benilla-app/src/ui_script/basic_controls_tests.rs
@@ -0,0 +1,164 @@
+//! The shipped `assets/ui/BasicControls.xml` — the reference's second file, driven the way an
+//! addon drives it.
+//!
+//! Nothing benilla ships calls `message`, `TEXT` or `_ERRORMESSAGE`; their only consumers are
+//! third-party addons (26 call `TEXT`, ~10 genuinely call `message`, two replace `_ERRORMESSAGE`).
+//! So every test here enters from Lua the way those addons do.
+
+use benilla_ui::script::UiScript;
+
+/// One shipped file into `s`, asserting it loaded clean.
+fn load_xml(s: &UiScript, file: &str) {
+ let text = std::fs::read_to_string(
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("assets/ui")
+ .join(file),
+ )
+ .unwrap();
+ let doc = benilla_ui::framexml::parse(&text).unwrap();
+ let report = benilla_ui::loader::load(s, &doc, &|_| None);
+ assert!(
+ report.errors.is_empty(),
+ "{file}: loader errors: {:?}",
+ report.errors
+ );
+}
+
+/// Fonts then BasicControls — the manifest's own order, and the whole dependency this file has.
+fn basic_controls() -> UiScript {
+ let mut s = UiScript::new().unwrap();
+ load_xml(&s, "Fonts.xml");
+ load_xml(&s, "BasicControls.xml");
+ s.set_screen_size(1024.0, 768.0);
+ s.resolve();
+ s
+}
+
+/// **`message("...")` puts its text in the dialog and shows it** — the whole contract, and the
+/// reason the frame has to exist rather than the function being a stub that prints somewhere.
+#[test]
+fn message_shows_the_script_errors_dialog_carrying_its_text() {
+ let s = basic_controls();
+ assert!(
+ !s.eval::("return ScriptErrors:IsVisible()").unwrap(),
+ "the dialog starts hidden (DialogBoxFrame's `hidden=true`)"
+ );
+
+ s.run(r#"message("Validation error: [nil]")"#).unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert!(s.eval::("return ScriptErrors:IsVisible()").unwrap());
+ assert_eq!(
+ s.eval::("return ScriptErrors_Message:GetText()")
+ .unwrap(),
+ "Validation error: [nil]"
+ );
+
+ // The ref guard: a second message while the dialog is up does NOT overwrite the first.
+ s.run(r#"message("second")"#).unwrap();
+ assert_eq!(
+ s.eval::("return ScriptErrors_Message:GetText()")
+ .unwrap(),
+ "Validation error: [nil]",
+ "`if not ScriptErrors:IsVisible()` — the first error is the one you keep"
+ );
+
+ // And OKAY hides it, which is the only way it closes.
+ s.run("ScriptErrorsButton:Click()").unwrap();
+ assert!(!s.eval::("return ScriptErrors:IsVisible()").unwrap());
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+}
+
+/// `_ERRORMESSAGE` is the reference's own name for the body, it **returns its argument**, and it
+/// is replaceable — which is the entire point for the two corpus addons that replace it.
+#[test]
+fn error_message_returns_its_argument_and_can_be_replaced() {
+ let s = basic_controls();
+ assert_eq!(
+ s.eval::(r#"return _ERRORMESSAGE("boom")"#).unwrap(),
+ "boom",
+ "the ref returns `message` — a caller may chain on it"
+ );
+
+ // ImprovedErrorFrame's shape: swap the global, then `message` must route through the swap.
+ s.run(
+ r#"Captured = nil
+ _ERRORMESSAGE = function(m) Captured = m return m end
+ message("routed")"#,
+ )
+ .unwrap();
+ assert_eq!(s.eval::("return Captured").unwrap(), "routed");
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+}
+
+/// The default error handler is still ours, deliberately — BasicControls.xml drops the
+/// reference's `seterrorhandler(_ERRORMESSAGE)` so a runtime fault stays visible to
+/// `UiScript::errors()` (and therefore to the corpus harness) instead of becoming a modal.
+/// The divergence is stated at the site; this is what pins it.
+#[test]
+fn the_default_error_handler_is_not_replaced_by_error_message() {
+ let s = basic_controls();
+ assert!(
+ !s.eval::("return geterrorhandler() == _ERRORMESSAGE")
+ .unwrap(),
+ "installing _ERRORMESSAGE as the default would blind every instrument in the arc"
+ );
+ // ...but the pair still works, which is what an addon's pcall wrapper actually calls.
+ s.run(r#"geterrorhandler()("reported")"#).unwrap();
+ assert!(
+ s.errors().iter().any(|e| e.contains("reported")),
+ "geterrorhandler()(msg) reaches the error channel: {:?}",
+ s.errors()
+ );
+ assert!(
+ !s.eval::("return ScriptErrors:IsVisible()").unwrap(),
+ "and it does NOT pop the dialog"
+ );
+}
+
+/// `TEXT` is the identity function, and 26 corpus addons call it because shipped FrameXML does.
+#[test]
+fn text_is_the_identity_function_the_corpus_expects() {
+ let s = basic_controls();
+ assert_eq!(
+ s.eval::(r#"return TEXT("Level")"#).unwrap(),
+ "Level"
+ );
+ assert_eq!(s.eval::("return TEXT(7)").unwrap(), 7);
+}
+
+/// The dialog kit an addon inherits from directly: `DialogBoxFrame` plus the three named textures.
+/// The OKAY button must hide *the caller's* frame, and `$parent` must resolve against the caller's
+/// name — `MyDialogButton`, never `DialogBoxFrameButton`.
+#[test]
+fn a_dialog_box_from_the_template_names_and_closes_the_callers_frame() {
+ let s = basic_controls();
+ s.run(
+ r#"MyDialog = CreateFrame("Frame", "MyDialog", UIParent, "DialogBoxFrame")
+ MyDialog:SetWidth(384) MyDialog:SetHeight(128)
+ MyDialog:Show()"#,
+ )
+ .unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert!(
+ s.eval::(r#"return getglobal("MyDialogButton") ~= nil"#)
+ .unwrap(),
+ "the caller's name won through $parent"
+ );
+ assert!(
+ s.eval::(r#"return getglobal("DialogBoxFrameButton") == nil"#)
+ .unwrap(),
+ "and the template's own name published nothing"
+ );
+ assert_eq!(
+ s.eval::(r#"return getglobal("MyDialogButton"):GetText()"#)
+ .unwrap(),
+ "OKAY"
+ );
+
+ s.run("MyDialogButton:Click()").unwrap();
+ assert!(
+ !s.eval::("return MyDialog:IsVisible()").unwrap(),
+ "`this:GetParent():Hide()` closed the caller's frame, not the template's"
+ );
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+}
diff --git a/crates/benilla-app/src/ui_script/buff_tests.rs b/crates/benilla-app/src/ui_script/buff_tests.rs
index 9dcc2aa7..e6545ecf 100644
--- a/crates/benilla-app/src/ui_script/buff_tests.rs
+++ b/crates/benilla-app/src/ui_script/buff_tests.rs
@@ -1,15 +1,21 @@
//! The player buff bar (`assets/ui/BuffFrame.xml`, decisions 0255/0257) against its reference
//! behaviour. The XML/Lua is the unit under test; the app-side feed (`crate::ui_aura`) is stubbed by
-//! pushing an [`AuraState`] list straight through [`UiScript::set_auras`] and firing `UNIT_AURA`, so
-//! these exercise the *button* handlers — the row/filter wiring, the dispel-tinted border, the
-//! stack count, the GetTime()-based countdown, the warning flash, and the right-click cancel — the
-//! way the reference's own `BuffButton_*` did, but over our Era `UnitAura` bindings.
+//! pushing an [`AuraState`] list straight through [`UiScript::set_auras`] and firing
+//! `PLAYER_AURAS_CHANGED`, so these exercise the *button* handlers — the row/filter wiring, the
+//! dispel-tinted border, the stack count, the countdown, the warning flash, and the right-click
+//! cancel — the way the reference's own `BuffButton_*` do.
+//!
+//! **The window runs on the 1.12 verbs and the reference's index space** (b2ede294 landed the
+//! `GetPlayerBuff` family): a button's `id` is a 0-based ordinal within its own filter,
+//! `GetPlayerBuff` turns it into a CACHE POSITION, and every read below — texture, dispel class,
+//! stacks, time left, cancel, tooltip — consumes that position. The tests drive it the way the
+//! corpus does, including the `while GetPlayerBuff(i) >= 0 do` walk and a cancel by position.
//!
//! State is read back through the widget (`IsShown`/`GetText`/`GetAlpha`) and the paint through the
//! [`draw list`](UiScript::extract) (icon path + border tint), the same two-lens split the cast-bar
//! tests use — Lua-visible state is blind to what actually renders.
-use benilla_ui::script::{AuraState, ExtractedQuad, QuadContent, ScriptValue, UiScript};
+use benilla_ui::script::{AuraState, ExtractedQuad, QuadContent, UiScript};
/// Load one shipped `assets/ui/`, panicking on any loader error.
fn load_xml(s: &UiScript, file: &str) {
@@ -61,13 +67,23 @@ fn aura(
expiration_time,
helpful,
cancelable,
+ // What the feed derives for a permanent aura: no finite duration to display. Here the two
+ // agree by construction, but they are different questions in the real feed — the flag is
+ // DBC-derived and correct on the frame the aura appears, before any duration packet lands
+ // (`benilla::ui_aura::until_cancelled`).
+ until_cancelled: expiration_time == 0.0,
+ channeled: false,
}
}
/// Push the list, then fire the discrete-change event the app's feed fires — the buttons repaint.
+///
+/// `PLAYER_AURAS_CHANGED`, no args: the reference's own event, which `ui_aura` fires beside the
+/// Era-shaped `UNIT_AURA` on the same rebuild. It is what the buttons register for now, and it is
+/// what every corpus aura addon registers for.
fn push(s: &mut UiScript, auras: Vec) {
s.set_auras("player", Some(auras));
- s.fire_event("UNIT_AURA", vec![ScriptValue::Str("player".into())]);
+ s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
}
/// One tick of the app's real order (OnUpdate → resolve → draw list), returning the quads.
@@ -289,8 +305,9 @@ fn right_clicking_a_cancelable_buff_queues_its_spell_cancel() {
s.take_cancel_aura_requests().is_empty(),
"nothing queued yet"
);
- // The RightButtonUp handler -> CancelUnitBuff -> the app drains one CMSG_CANCEL_AURA by spell id.
- s.eval::<()>("BenillaBuffButton_OnClick(BuffButton0)")
+ // The RightButtonUp handler -> CancelPlayerBuff(this.buffIndex) -> the app drains one
+ // CMSG_CANCEL_AURA by spell id.
+ s.eval::<()>("this = BuffButton0; BuffButton_OnClick()")
.unwrap();
assert_eq!(s.take_cancel_aura_requests(), vec![1126]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -374,9 +391,24 @@ fn a_refreshed_duration_reaches_the_bar_with_no_aura_event() {
// The same poll is what shows a timer that arrived AFTER the icon did — the fresh-apply case,
// where the duration packet's stamp lands a frame later than the descriptor delta.
- s.set_auras("player", Some(vec![mark(0.0)])); // icon up, no duration joined yet
+ //
+ // **What the icon-first frame looks like MOVED when the bar came onto the 1.12 verbs.** The
+ // permanence test used to be `expirationTime > 0`, which cannot tell "no duration, ever" from
+ // "the duration has not landed yet" and so drew nothing for both. It is now `untilCancelled` —
+ // GetPlayerBuff's second return, derived from the SPELL (`ui_aura::until_cancelled`) — so a
+ // timed aura awaiting its stamp is known to be timed, and the reference counts it from `0 s`
+ // and pulses it for the frame or two until the packet joins. `mark` is a timed spell, so that
+ // is what it does here; the permanent case (a stance) is
+ // [`a_timed_aura_counts_down_and_a_permanent_one_shows_no_timer`], and it still shows nothing.
+ let mut pending = mark(0.0);
+ pending.until_cancelled = false; // a timed spell, stamp not yet arrived
+ s.set_auras("player", Some(vec![pending]));
frame(&mut s, 0.1);
- assert!(!shown(&s, "BuffButton0Duration"), "no timer yet");
+ assert_eq!(
+ text(&s, "BuffButton0Duration"),
+ "0 s",
+ "the reference draws the floor of a timed aura it has no stamp for, not a blank"
+ );
s.set_auras("player", Some(vec![mark(60.0)])); // the stamp joins, still no event
frame(&mut s, 0.1);
assert_eq!(
@@ -419,7 +451,7 @@ fn the_duration_switch_hides_the_timers_and_closes_their_gutter() {
assert!((shown_gap - 15.0).abs() < 1e-3, "gutter: {shown_gap}");
// The switch, applied the way the options row applies it.
- s.run("SHOW_BUFF_DURATIONS = \"0\"; BenillaBuffButtons_UpdatePositions()")
+ s.run("SHOW_BUFF_DURATIONS = \"0\"; BuffButtons_UpdatePositions()")
.unwrap();
frame(&mut s, 0.1);
assert!(!shown(&s, "BuffButton0Duration"), "no timer drawn");
@@ -435,10 +467,285 @@ fn the_duration_switch_hides_the_timers_and_closes_their_gutter() {
);
// And back: the text is re-written from the poll, not from anything it kept.
- s.run("SHOW_BUFF_DURATIONS = \"1\"; BenillaBuffButtons_UpdatePositions()")
+ s.run("SHOW_BUFF_DURATIONS = \"1\"; BuffButtons_UpdatePositions()")
.unwrap();
frame(&mut s, 0.1);
assert!(shown(&s, "BuffButton0Duration"));
assert_eq!(text(&s, "BuffButton0Duration"), "19 s");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
+
+/// A mixed bar: three buffs and two debuffs in one insertion-ordered cache.
+fn mixed_bar() -> Vec {
+ vec![
+ // pos 0 — buff, cancelable, timed
+ aura(
+ 1126,
+ "Mark of the Wild",
+ "Interface\\Icons\\A",
+ true,
+ 1,
+ None,
+ 120.0,
+ true,
+ ),
+ // pos 1 — debuff, Magic, 3 stacks
+ aura(
+ 589,
+ "Shadow Word: Pain",
+ "Interface\\Icons\\B",
+ false,
+ 3,
+ Some("Magic"),
+ 18.0,
+ false,
+ ),
+ // pos 2 — buff, cancelable
+ aura(
+ 1459,
+ "Arcane Intellect",
+ "Interface\\Icons\\C",
+ true,
+ 1,
+ None,
+ 600.0,
+ true,
+ ),
+ // pos 3 — debuff, Poison
+ aura(
+ 2818,
+ "Deadly Poison",
+ "Interface\\Icons\\D",
+ false,
+ 5,
+ Some("Poison"),
+ 12.0,
+ false,
+ ),
+ // pos 4 — buff, NOT cancelable (a stance): right-click must be refused
+ aura(
+ 2457,
+ "Battle Stance",
+ "Interface\\Icons\\E",
+ true,
+ 1,
+ None,
+ 0.0,
+ false,
+ ),
+ ]
+}
+
+/// **The two index spaces, end to end.** The button `id` is a 0-based ordinal within its own
+/// filter; `GetPlayerBuff` turns it into a CACHE POSITION that is absolute across filters. The bar
+/// is only correct if the two agree: button N of a row must draw the aura the corpus's own walk
+/// finds at that ordinal, and the position it cached must be the one an unfiltered walk reports.
+///
+/// The walk is spelled the way the corpus spells it — `while GetPlayerBuff(i) >= 0 do`
+/// (`_LazyPig/LazyPig.lua:1174`, `Zorlen/Zorlen.lua:2797`) — because `-1`, not nil, is the
+/// terminator, and because the default filter is `HELPFUL|HARMFUL`: the walk must see the debuffs
+/// too or the whole thing silently halves.
+#[test]
+fn the_buttons_and_the_corpus_walk_agree_on_the_cache_positions() {
+ let mut s = harness();
+ push(&mut s, mixed_bar());
+ frame(&mut s, 0.1);
+
+ // The corpus walk: five auras, buffs AND debuffs, positions identical to the ordinals under
+ // the default filter — and it terminates.
+ let visited = s
+ .eval::(
+ r#"local i = 0
+ while GetPlayerBuff(i) >= 0 do
+ assert((GetPlayerBuff(i)) == i, "unfiltered walk is position-identical")
+ i = i + 1
+ assert(i < 100, "GetPlayerBuff walk did not terminate")
+ end
+ return i"#,
+ )
+ .unwrap();
+ assert_eq!(visited, 5, "three buffs and two debuffs, one cache");
+
+ // Each row filled from its own filter, in cache order — and each button's CACHED position is
+ // the absolute one, not its ordinal.
+ for (button, position) in [
+ ("BuffButton0", 0), // helpful ordinal 0 -> position 0
+ ("BuffButton1", 2), // helpful ordinal 1 -> position 2 (the debuff at 1 is skipped)
+ ("BuffButton2", 4), // helpful ordinal 2 -> position 4
+ ("BuffButton16", 1), // harmful ordinal 0 -> position 1
+ ("BuffButton17", 3), // harmful ordinal 1 -> position 3
+ ] {
+ assert!(shown(&s, button), "{button} draws its aura");
+ assert_eq!(
+ s.eval::(&format!("return {button}.buffIndex"))
+ .unwrap(),
+ position,
+ "{button} caches the CACHE POSITION, not its per-filter ordinal"
+ );
+ }
+ // The rows stop where the filter runs out; nothing bleeds across.
+ assert!(!shown(&s, "BuffButton3"), "only three buffs");
+ assert!(!shown(&s, "BuffButton18"), "only two debuffs");
+ assert_eq!(
+ s.eval::("return BuffButton3.buffIndex").unwrap(),
+ -1,
+ "a miss is -1, the sentinel the corpus terminates on — never nil"
+ );
+
+ // The reads that hang off the position land on the right aura: the 3-stack Magic debuff and
+ // the 5-stack Poison one are in the right buttons, tinted from their own dispel classes.
+ assert_eq!(text(&s, "BuffButton16Count"), "3");
+ assert_eq!(text(&s, "BuffButton17Count"), "5");
+ assert_eq!(
+ text(&s, "BuffButton0Count"),
+ "",
+ "a single stack shows nothing"
+ );
+ let quads = frame(&mut s, 0.1);
+ let poison = quads
+ .iter()
+ .filter_map(|q| match &q.content {
+ QuadContent::Texture {
+ path: Some(p),
+ color: Some(c),
+ ..
+ } if p.ends_with("UI-Debuff-Overlays") => Some(*c),
+ _ => None,
+ })
+ // DebuffTypeColor["Poison"] = 0.00, 0.60, 0.00
+ .any(|c| c[0].abs() < 1e-3 && (c[1] - 0.60).abs() < 1e-3 && c[2].abs() < 1e-3);
+ assert!(poison, "the second debuff button wears the Poison tint");
+ assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+}
+
+/// **Right-click cancels by cache position.** `BuffButton_OnClick` is
+/// `CancelPlayerBuff(this.buffIndex)` — the reference's own line — so the button that gets
+/// cancelled is the one under the cursor even when its ordinal and its position differ, which is
+/// the case for every buff after the first debuff.
+///
+/// The gate is the app's, unchanged: a cancelable buff queues its SPELL id, a stance and a debuff
+/// are silent no-ops. That last part is what a per-filter id could not guarantee on its own — here
+/// it is structural, because a harmful button's handle simply names a harmful record.
+#[test]
+fn right_clicking_cancels_the_aura_under_the_cursor_by_its_cache_position() {
+ let mut s = harness();
+ push(&mut s, mixed_bar());
+ frame(&mut s, 0.1);
+ assert!(s.take_cancel_aura_requests().is_empty());
+
+ // The SECOND buff button: helpful ordinal 1, cache position 2, spell 1459. A bar that cancelled
+ // by ordinal would send 589 — the debuff sitting at position 1.
+ s.eval::<()>("this = BuffButton1; BuffButton_OnClick()")
+ .unwrap();
+ assert_eq!(
+ s.take_cancel_aura_requests(),
+ vec![1459],
+ "the aura under the cursor, not the record at the ordinal"
+ );
+
+ // A stance (helpful, not cancelable), both debuff buttons, and an empty slot: all silent.
+ for button in ["BuffButton2", "BuffButton16", "BuffButton17", "BuffButton7"] {
+ s.eval::<()>(&format!("this = {button}; BuffButton_OnClick()"))
+ .unwrap();
+ }
+ assert!(
+ s.take_cancel_aura_requests().is_empty(),
+ "a stance, a debuff and an empty button are all no-ops"
+ );
+
+ // The first buff still cancels normally.
+ s.eval::<()>("this = BuffButton0; BuffButton_OnClick()")
+ .unwrap();
+ assert_eq!(s.take_cancel_aura_requests(), vec![1126]);
+ assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+}
+
+/// **The temporary-enchant row** (ref `BuffFrame_Enchant_OnUpdate`, l.162-233). With nothing
+/// enchanted it hides both slots and parks the bar on its resting point; with a weapon enchanted it
+/// shows the icon, counts the expiry down from MILLISECONDS, and slides the top buff row left to
+/// clear it — while rows 2 and 3, which hang off TempEnchant1 and TemporaryEnchantFrame, stay put.
+#[test]
+fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside() {
+ let mut s = harness();
+ // A LOADED bar, deliberately: the enchant slots must be driven by GetWeaponEnchantInfo and by
+ // nothing else. The reference builds them from `BuffButtonTemplate` and blanks the four aura
+ // handlers with empty ``/``/`` elements; our loader treats an
+ // empty handler body as "none given" and so KEEPS the inherited one (`compile_handler` returns
+ // None and `apply_scripts` never calls SetScript). Under that idiom TempEnchant1 would take
+ // `BuffButton_OnLoad`, register PLAYER_AURAS_CHANGED, and paint the first buff in the corner —
+ // and a right-click on it would cancel that aura. Hence the standalone template, and hence a
+ // populated bar here: an empty aura list cannot tell the two apart.
+ push(&mut s, mixed_bar());
+ frame(&mut s, 0.1);
+
+ // Nothing enchanted: both slots hidden, and the bar sits where it has always sat.
+ assert!(
+ !shown(&s, "TempEnchant1"),
+ "the enchant slots are not buff buttons: no aura may reach them"
+ );
+ assert!(!shown(&s, "TempEnchant2"));
+ // The direct probe: none of the three aura handlers is even REGISTERED on an enchant slot.
+ // This is what the reference's empty-element idiom is supposed to achieve and what our loader
+ // does not do, so it is asserted rather than assumed.
+ for handler in ["OnLoad", "OnEvent", "OnClick"] {
+ assert!(
+ s.eval::(&format!(
+ "return TempEnchant1:GetScript(\"{handler}\") == nil"
+ ))
+ .unwrap(),
+ "an enchant slot must carry no {handler} — it is not a buff button"
+ );
+ }
+ s.resolve();
+ let resting = s.eval::("return BuffFrame:GetRight()").unwrap();
+ let row2 = s.eval::("return BuffButton16:GetRight()").unwrap();
+ assert!(
+ (resting - (1024.0 - 175.0)).abs() < 1e-3,
+ "UIParent TOPRIGHT -175: {resting}"
+ );
+
+ // A main-hand enchant, 8 minutes out. The app feeds it the way `ui_char` does.
+ s.set_weapon_enchants(
+ Some(benilla_ui::script::WeaponEnchant {
+ remaining_ms: Some(480_000),
+ charges: 0,
+ }),
+ None,
+ );
+ frame(&mut s, 0.1);
+ assert!(
+ shown(&s, "TempEnchant1"),
+ "the enchanted weapon takes slot 1"
+ );
+ assert!(!shown(&s, "TempEnchant2"));
+ assert_eq!(
+ s.eval::("return TempEnchant1:GetID()").unwrap(),
+ 16,
+ "the live-API MainHandSlot id, which is what its item tooltip hover needs"
+ );
+ assert_eq!(
+ text(&s, "TempEnchant1Duration"),
+ "8 m",
+ "480000 ms / 1000 -> 480 s -> SecondsToTimeAbbrev"
+ );
+
+ // The top buff row slid left by one icon plus the 5px gutter; the debuff row did not move.
+ s.resolve();
+ let shifted = s.eval::("return BuffFrame:GetRight()").unwrap();
+ assert!(
+ (resting - shifted - 37.0).abs() < 1e-3,
+ "one 32px enchant column + the 5px gutter: {resting} -> {shifted}"
+ );
+ assert!(
+ (s.eval::("return BuffButton16:GetRight()").unwrap() - row2).abs() < 1e-3,
+ "the debuff row hangs off TemporaryEnchantFrame and stays put"
+ );
+
+ // And back: the enchant drops, the row hides and the bar returns to its resting point.
+ s.set_weapon_enchants(None, None);
+ frame(&mut s, 0.1);
+ assert!(!shown(&s, "TempEnchant1"));
+ s.resolve();
+ assert!((s.eval::("return BuffFrame:GetRight()").unwrap() - resting).abs() < 1e-3);
+ assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+}
diff --git a/crates/benilla-app/src/ui_script/cast_tests.rs b/crates/benilla-app/src/ui_script/cast_tests.rs
index 119e5167..f2586851 100644
--- a/crates/benilla-app/src/ui_script/cast_tests.rs
+++ b/crates/benilla-app/src/ui_script/cast_tests.rs
@@ -412,14 +412,17 @@ fn managed_positions_track_the_bottom_bar_stack() {
assert_eq!(bottom(&s, "ChatFrame1"), 85.0, "chat baseY");
// The always-on bottom multibars appear (0270): bottomEither for the bar, bottomLeft for chat.
- s.run("BenillaMultiBarBottomLeft = { IsShown = function() return true end }; BenillaMultiBarBottomRight = BenillaMultiBarBottomLeft; UIParent_ManageFramePositions()")
+ // The bar stubs carry a no-op SetPoint: `MultiBarBottomLeft` and `ShapeshiftBarFrame` are
+ // themselves rows in UIPARENT_MANAGED_FRAME_POSITIONS, so since those frames wear their
+ // reference names the pass positions them as well as reading their visibility.
+ s.run("MultiBarBottomLeft = { IsShown = function() return true end, SetPoint = function() end }; MultiBarBottomRight = MultiBarBottomLeft; UIParent_ManageFramePositions()")
.unwrap();
s.resolve();
assert_eq!(bottom(&s, "CastingBarFrame"), 100.0, "60 + bottomEither 40");
assert_eq!(bottom(&s, "ChatFrame1"), 102.0, "85 + bottomLeft 17");
// The stance bar shows (the warrior at login): the pet term, plus chat's both-flags extra.
- s.run("BenillaShapeshiftBarFrame = { IsShown = function() return true end }; UIParent_ManageFramePositions()")
+ s.run("ShapeshiftBarFrame = { IsShown = function() return true end, SetPoint = function() end }; UIParent_ManageFramePositions()")
.unwrap();
s.resolve();
assert_eq!(bottom(&s, "CastingBarFrame"), 140.0, "60 + 40 + pet 40");
@@ -430,7 +433,7 @@ fn managed_positions_track_the_bottom_bar_stack() {
);
// It hides again (a druid leaving forms is the live case): everything settles back.
- s.run("BenillaShapeshiftBarFrame = { IsShown = function() return false end }; UIParent_ManageFramePositions()")
+ s.run("ShapeshiftBarFrame = { IsShown = function() return false end, SetPoint = function() end }; UIParent_ManageFramePositions()")
.unwrap();
s.resolve();
assert_eq!(bottom(&s, "CastingBarFrame"), 100.0);
diff --git a/crates/benilla-app/src/ui_script/character_tests.rs b/crates/benilla-app/src/ui_script/character_tests.rs
index a4c31940..98366154 100644
--- a/crates/benilla-app/src/ui_script/character_tests.rs
+++ b/crates/benilla-app/src/ui_script/character_tests.rs
@@ -165,16 +165,11 @@ fn shipped_character_frame_drives_end_to_end() {
s.take_sounds().is_empty(),
"no sound at load (never transitions)"
);
- assert!(!s
- .eval::("return BenillaCharacterFrame:IsVisible()")
- .unwrap());
+ assert!(!s.eval::("return CharacterFrame:IsVisible()").unwrap());
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
assert!(s.errors().is_empty(), "open errors: {:?}", s.errors());
- assert!(s
- .eval::("return BenillaCharacterFrame:IsVisible()")
- .unwrap());
+ assert!(s.eval::("return CharacterFrame:IsVisible()").unwrap());
assert_eq!(
s.take_sounds(),
vec![SoundRequest::KitName("igCharacterInfoOpen".into())],
@@ -182,18 +177,18 @@ fn shipped_character_frame_drives_end_to_end() {
);
assert_eq!(
- s.eval::("return BenillaCharacterNameText:GetText()")
+ s.eval::("return CharacterNameText:GetText()")
.unwrap(),
"Benilla"
);
assert_eq!(
- s.eval::("return BenillaCharacterLevelText:GetText()")
+ s.eval::("return CharacterLevelText:GetText()")
.unwrap(),
"Level 12 Night Elf Warrior"
);
// STR effective 15, posBuff 2 > 0 → green (ref PaperDollFrame_SetStats).
assert_eq!(
- s.eval::("return BenillaCharacterStatFrame1StatText:GetText()")
+ s.eval::("return CharacterStatFrame1StatText:GetText()")
.unwrap(),
"|cff20ff2015|r"
);
@@ -202,12 +197,12 @@ fn shipped_character_frame_drives_end_to_end() {
// never a re-declared region in an instance, which this engine renders as a SECOND
// anchorless default-font FontString (the white/overlapping-label regression).
assert_eq!(
- s.eval::("return BenillaCharacterStatFrame1Label:GetText()")
+ s.eval::("return CharacterStatFrame1Label:GetText()")
.unwrap(),
"Strength:"
);
assert_eq!(
- s.eval::("return BenillaCharacterAttackFrameLabel:GetText()")
+ s.eval::("return CharacterAttackFrameLabel:GetText()")
.unwrap(),
"Melee Attack"
);
@@ -215,9 +210,9 @@ fn shipped_character_frame_drives_end_to_end() {
s.resolve();
// The template's label sits LEFT-anchored at its row's left edge.
assert_eq!(
- s.eval::("return BenillaCharacterStatFrame1Label:GetLeft()")
+ s.eval::("return CharacterStatFrame1Label:GetLeft()")
.unwrap(),
- s.eval::("return BenillaCharacterStatFrame1:GetLeft()")
+ s.eval::("return CharacterStatFrame1:GetLeft()")
.unwrap(),
);
// Exactly ONE region carries the label text, in the small gold font (GameFontNormalSmall:
@@ -250,19 +245,18 @@ fn shipped_character_frame_drives_end_to_end() {
);
// Armor (school 0) reads straight off UnitArmor's base.
assert_eq!(
- s.eval::("return BenillaCharacterArmorFrameStatText:GetText()")
+ s.eval::("return CharacterArmorFrameStatText:GetText()")
.unwrap(),
"120"
);
// MagicResFrame1 = id 6 (arcane) — resistances[6] = 3.
assert_eq!(
- s.eval::("return BenillaMagicResText1:GetText()")
- .unwrap(),
+ s.eval::("return MagicResText1:GetText()").unwrap(),
"3"
);
// No ranged weapon in this fixture: the N/A fallback (ref PaperDollFrame_SetRangedAttack).
assert_eq!(
- s.eval::("return BenillaCharacterRangedAttackFrameStatText:GetText()")
+ s.eval::("return CharacterRangedAttackFrameStatText:GetText()")
.unwrap(),
"N/A"
);
@@ -286,14 +280,10 @@ fn shipped_character_frame_drives_end_to_end() {
// Hovering an EMPTY slot (Neck, never populated) shows the slot-name tooltip fallback.
let neck_center = {
- let l: f32 = s.eval("return BenillaCharacterNeckSlot:GetLeft()").unwrap();
- let r: f32 = s
- .eval("return BenillaCharacterNeckSlot:GetRight()")
- .unwrap();
- let t: f32 = s.eval("return BenillaCharacterNeckSlot:GetTop()").unwrap();
- let b: f32 = s
- .eval("return BenillaCharacterNeckSlot:GetBottom()")
- .unwrap();
+ let l: f32 = s.eval("return CharacterNeckSlot:GetLeft()").unwrap();
+ let r: f32 = s.eval("return CharacterNeckSlot:GetRight()").unwrap();
+ let t: f32 = s.eval("return CharacterNeckSlot:GetTop()").unwrap();
+ let b: f32 = s.eval("return CharacterNeckSlot:GetBottom()").unwrap();
((l + r) * 0.5, (t + b) * 0.5)
};
s.mouse_move(neck_center.0, neck_center.1);
@@ -308,10 +298,10 @@ fn shipped_character_frame_drives_end_to_end() {
// siblings); a transcription that drops the flag renders the ~380px sentence as one
// unwrapped line (the director's tooltip max-width report).
let res_center = {
- let l: f32 = s.eval("return BenillaMagicResFrame1:GetLeft()").unwrap();
- let r: f32 = s.eval("return BenillaMagicResFrame1:GetRight()").unwrap();
- let t: f32 = s.eval("return BenillaMagicResFrame1:GetTop()").unwrap();
- let b: f32 = s.eval("return BenillaMagicResFrame1:GetBottom()").unwrap();
+ let l: f32 = s.eval("return MagicResFrame1:GetLeft()").unwrap();
+ let r: f32 = s.eval("return MagicResFrame1:GetRight()").unwrap();
+ let t: f32 = s.eval("return MagicResFrame1:GetTop()").unwrap();
+ let b: f32 = s.eval("return MagicResFrame1:GetBottom()").unwrap();
((l + r) * 0.5, (t + b) * 0.5)
};
s.mouse_move(res_center.0, res_center.1);
@@ -341,7 +331,7 @@ 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(BenillaCharacterModelFrame)")
+ s.run("BenillaPaperDollModel_RotateRight(CharacterModelFrame)")
.unwrap();
assert!(
(s.paperdoll_yaw() - 0.64).abs() < 0.001,
@@ -354,12 +344,9 @@ fn shipped_character_frame_drives_end_to_end() {
);
// ToggleCharacter() again closes it through HideUIPanel, playing the close kit.
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
assert!(s.errors().is_empty(), "close errors: {:?}", s.errors());
- assert!(!s
- .eval::("return BenillaCharacterFrame:IsVisible()")
- .unwrap());
+ assert!(!s.eval::("return CharacterFrame:IsVisible()").unwrap());
assert_eq!(
s.take_sounds(),
vec![SoundRequest::KitName("igCharacterInfoClose".into())],
@@ -368,11 +355,11 @@ fn shipped_character_frame_drives_end_to_end() {
}
/// Regression — the close button's z-order (the reported "missing red X"): the character window
-/// carries no border art of its own; its whole frame-and-background IS `BenillaPaperDollFrame`'s
+/// carries no border art of its own; its whole frame-and-background IS `PaperDollFrame`'s
/// BACKGROUND layer, a full-window page created AFTER the close button. Every frame defaults to
/// level 0, so within the window draw order is creation order (`order.rs`) — the page's art painted
/// over the earlier-created button and it vanished. The button's `OnLoad` raises its frame level
-/// (the same idiom `BenillaCharacterNameFrame` uses), so its art must paint AFTER every one of the
+/// (the same idiom `CharacterNameFrame` uses), so its art must paint AFTER every one of the
/// page's own quads. Checked on the real extracted draw order, not merely the level integer.
#[test]
fn close_button_draws_above_the_paper_doll_page() {
@@ -384,8 +371,7 @@ fn close_button_draws_above_the_paper_doll_page() {
load_xml(&s, "CharacterFrame.xml");
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("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
s.resolve();
// extract() returns quads already sorted ascending by draw order, so index == paint order.
@@ -395,14 +381,14 @@ fn close_button_draws_above_the_paper_doll_page() {
// The window's page art draws first; its LAST quad is the top-most page pixel.
let page_last = quads
.iter()
- .rposition(|q| owner(q).as_deref() == Some("BenillaPaperDollFrame"))
+ .rposition(|q| owner(q).as_deref() == Some("PaperDollFrame"))
.expect("the paper-doll page renders its background art");
// The close button's red-X normal texture must render at all …
let close_x = quads
.iter()
.position(|q| {
- owner(q).as_deref() == Some("BenillaCharacterFrameCloseButton")
+ owner(q).as_deref() == Some("CharacterFrameCloseButton")
&& matches!(&q.content,
QuadContent::Texture { path: Some(p), .. } if p.contains("MinimizeButton-Up"))
})
@@ -430,11 +416,10 @@ fn level_line_survives_no_player_snapshot_yet() {
// 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("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
assert!(s.errors().is_empty(), "open errors: {:?}", s.errors());
assert_eq!(
- s.eval::("return BenillaCharacterLevelText:GetText()")
+ s.eval::("return CharacterLevelText:GetText()")
.unwrap(),
"Level 0 ? ?"
);
@@ -465,15 +450,14 @@ fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() {
s.set_inventory_slots(inventory_with_head_item());
// The window must be SHOWN for `extract()` to emit its quads below (hidden="true" by
// default) — the icon-dim check needs a real render pass, not just the engine-side state.
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
s.take_sounds();
s.resolve();
assert!(!s.eval::("return IsInventoryItemLocked(1)").unwrap());
assert!(!s.eval::("return CursorHasItem()").unwrap());
- s.run(r#"BenillaPaperDollSlot_OnClick(BenillaCharacterHeadSlot, "LeftButton")"#)
+ s.run(r#"BenillaPaperDollSlot_OnClick(CharacterHeadSlot, "LeftButton")"#)
.unwrap();
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
@@ -495,7 +479,7 @@ fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() {
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(BenillaCharacterHeadSlot, "LeftButton")"#)
+ s.run(r#"BenillaPaperDollSlot_OnClick(CharacterHeadSlot, "LeftButton")"#)
.unwrap();
assert!(!s.eval::("return CursorHasItem()").unwrap());
assert!(!s.eval::("return IsInventoryItemLocked(1)").unwrap());
@@ -529,17 +513,17 @@ fn cursor_update_highlights_fitting_doll_slots_while_holding_a_bag_item() {
.unwrap();
s.set_container(0, Some(backpack_with_fitting_helm()));
- s.run("C_Container.PickupContainerItem(0, 1)").unwrap();
+ 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());
assert!(
- s.eval::("return lockHighlighted['BenillaCharacterHeadSlot'] == true")
+ s.eval::("return lockHighlighted['CharacterHeadSlot'] == true")
.unwrap(),
"the fitting slot locks its highlight"
);
assert!(
- s.eval::("return unlockHighlighted['BenillaCharacterNeckSlot'] == true")
+ s.eval::("return unlockHighlighted['CharacterNeckSlot'] == true")
.unwrap(),
"a non-fitting slot stays unhighlighted"
);
@@ -558,28 +542,19 @@ fn model_pane_click_auto_equips_a_held_bag_item() {
load_xml(&s, "CharacterFrame.xml");
s.set_unit("player", Some(player_unit()));
s.set_player_combat_stats(Some(combat_stats()));
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
s.take_sounds();
s.resolve();
s.set_container(0, Some(backpack_with_fitting_helm()));
- s.run("C_Container.PickupContainerItem(0, 1)").unwrap();
+ s.run("PickupContainerItem(0, 1)").unwrap();
assert!(s.eval::("return CursorHasItem()").unwrap());
let center = {
- let l: f32 = s
- .eval("return BenillaCharacterModelFrame:GetLeft()")
- .unwrap();
- let r: f32 = s
- .eval("return BenillaCharacterModelFrame:GetRight()")
- .unwrap();
- let t: f32 = s
- .eval("return BenillaCharacterModelFrame:GetTop()")
- .unwrap();
- let b: f32 = s
- .eval("return BenillaCharacterModelFrame:GetBottom()")
- .unwrap();
+ let l: f32 = s.eval("return CharacterModelFrame:GetLeft()").unwrap();
+ let r: f32 = s.eval("return CharacterModelFrame:GetRight()").unwrap();
+ let t: f32 = s.eval("return CharacterModelFrame:GetTop()").unwrap();
+ let b: f32 = s.eval("return CharacterModelFrame:GetBottom()").unwrap();
((l + r) * 0.5, (t + b) * 0.5)
};
s.mouse_button(center.0, center.1, "LeftButton", true);
@@ -607,8 +582,7 @@ fn broken_equipped_item_tints_its_doll_slot_red() {
let mut inv = inventory_with_head_item();
inv[1].as_mut().unwrap().durability = Some((0, 40));
s.set_inventory_slots(inv);
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
s.fire_event(
"UNIT_INVENTORY_CHANGED",
vec![ScriptValue::Str("player".into())],
@@ -756,21 +730,14 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
.unwrap()
};
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
- assert!(
- shown(&mut s, "BenillaPaperDollFrame"),
- "opens on the doll page"
- );
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
+ 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);
- assert!(
- shown(&mut s, "BenillaSkillFrame"),
- "Skills tab shows the page"
- );
- assert!(!shown(&mut s, "BenillaPaperDollFrame"), "doll page yields");
+ assert!(shown(&mut s, "SkillFrame"), "Skills tab shows the page");
+ assert!(!shown(&mut s, "PaperDollFrame"), "doll page yields");
// Select a skill row — arms the detail pane (the report's precondition).
let row = text_center(&mut s, "Blacksmithing");
@@ -779,11 +746,11 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
s.eval::("return GetSelectedSkill()").unwrap() > 0,
"the row click selects"
);
- assert!(shown(&mut s, "BenillaSkillDetailBar"), "detail bar arms");
+ assert!(shown(&mut s, "SkillDetailBar"), "detail bar arms");
// The description body renders through the SKILL_DESCRIPTION format (skillType is "" in every
// reachable 1.12 branch) — the nil-global half of the report, pinned.
assert_eq!(
- s.eval::("return BenillaSkillDetailDescriptionText:GetText()")
+ s.eval::("return SkillDetailDescriptionText:GetText()")
.unwrap(),
"|cffffffff|r Working with metals.",
"the detail description renders via SKILL_DESCRIPTION"
@@ -793,10 +760,10 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
// name; accepting queues the CMSG_UNLEARN_SKILL intent BY SKILL ID and removes nothing
// locally (the server's SetSkill(id,0,0) round trip owns the removal).
assert!(
- shown(&mut s, "BenillaSkillDetailUnlearnButton"),
+ shown(&mut s, "SkillDetailUnlearnButton"),
"the unlearn button shows for a profession"
);
- s.run("BenillaSkillDetailUnlearnButton:Click()").unwrap();
+ s.run("SkillDetailUnlearnButton:Click()").unwrap();
assert!(
shown(&mut s, "StaticPopup1"),
"the UNLEARN_SKILL confirm opens"
@@ -823,7 +790,7 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
let defense = text_center(&mut s, "Defense");
click(&mut s, defense);
assert!(
- !shown(&mut s, "BenillaSkillDetailUnlearnButton"),
+ !shown(&mut s, "SkillDetailUnlearnButton"),
"no unlearn button for Defense"
);
// Restore the profession selection so the round trip below leaves familiar state.
@@ -834,23 +801,23 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
let tab1 = text_center(&mut s, "Character");
assert_eq!(
s.hit_test_name(tab1.0, tab1.1).as_deref(),
- Some("BenillaCharacterFrameTab1"),
+ Some("CharacterFrameTab1"),
"the Character tab OWNS its point while Skills is up (the wheel catcher must not)"
);
click(&mut s, tab1);
assert!(
- shown(&mut s, "BenillaPaperDollFrame"),
+ shown(&mut s, "PaperDollFrame"),
"the Character tab switches back (the 2026-07-17 report)"
);
- assert!(!shown(&mut s, "BenillaSkillFrame"), "Skills page yields");
+ 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);
- assert!(shown(&mut s, "BenillaSkillFrame"), "second trip to Skills");
+ assert!(shown(&mut s, "SkillFrame"), "second trip to Skills");
let tab1 = text_center(&mut s, "Character");
click(&mut s, tab1);
- assert!(shown(&mut s, "BenillaPaperDollFrame"), "second trip back");
+ assert!(shown(&mut s, "PaperDollFrame"), "second trip back");
assert!(
s.errors().is_empty(),
@@ -883,12 +850,11 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
load_xml(&s, "CharacterFrame.xml");
s.set_unit("player", Some(player_unit()));
s.set_player_combat_stats(Some(combat_stats()));
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
s.resolve();
// The left arrow's centre, in screen points.
- let btn = "BenillaCharacterModelFrameRotateLeftButton";
+ let btn = "CharacterModelFrameRotateLeftButton";
let (bx, by) = {
let l: f32 = s.eval(&format!("return {btn}:GetLeft()")).unwrap();
let r: f32 = s.eval(&format!("return {btn}:GetRight()")).unwrap();
@@ -929,7 +895,7 @@ fn rotate_arrows_tap_twice_and_spin_while_held() {
);
// The right arrow spins the other way (held-RIGHT subtracts).
- let rbtn = "BenillaCharacterModelFrameRotateRightButton";
+ let rbtn = "CharacterModelFrameRotateRightButton";
let (rx, ry) = {
let l: f32 = s.eval(&format!("return {rbtn}:GetLeft()")).unwrap();
let r: f32 = s.eval(&format!("return {rbtn}:GetRight()")).unwrap();
@@ -996,14 +962,14 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
}
let selected = |s: &mut UiScript| {
- s.eval::("return PanelTemplates_GetSelectedTab(BenillaCharacterFrame)")
+ s.eval::("return PanelTemplates_GetSelectedTab(CharacterFrame)")
.unwrap()
};
// A tab wears its "Active" (=Disabled) art exactly when it is the selected one — the visible
// half of the same fact, so a SetTab that updated the number without repainting fails here too.
let wearing_active_art = |s: &mut UiScript, tab: u32| {
s.eval::(&format!(
- "return BenillaCharacterFrameTab{tab}MiddleDisabled:IsVisible()"
+ "return CharacterFrameTab{tab}MiddleDisabled:IsVisible()"
))
.unwrap()
};
@@ -1013,20 +979,18 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
};
// Open on Character (the `C` binding), then move to Skills the same way.
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
assert_eq!(selected(&mut s), 1);
- s.run(r#"ToggleCharacter("BenillaSkillFrame")"#).unwrap();
- assert!(shown(&mut s, "BenillaSkillFrame"));
+ s.run(r#"ToggleCharacter("SkillFrame")"#).unwrap();
+ assert!(shown(&mut s, "SkillFrame"));
assert_eq!(selected(&mut s), 3, "the row follows a keybind to Skills");
assert!(wearing_active_art(&mut s, 3));
assert!(!wearing_active_art(&mut s, 1));
// THE REPORT: `C` from the Skills page. The page goes back to Character — and so must the row.
- s.run(r#"ToggleCharacter("BenillaPaperDollFrame")"#)
- .unwrap();
- assert!(shown(&mut s, "BenillaPaperDollFrame"));
- assert!(!shown(&mut s, "BenillaSkillFrame"));
+ s.run(r#"ToggleCharacter("PaperDollFrame")"#).unwrap();
+ assert!(shown(&mut s, "PaperDollFrame"));
+ assert!(!shown(&mut s, "SkillFrame"));
assert_eq!(
selected(&mut s),
1,
@@ -1041,14 +1005,11 @@ 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.
- s.run("BenillaCharacterFrameTab1:Click()").unwrap();
+ s.run("CharacterFrameTab1:Click()").unwrap();
assert!(
- shown(&mut s, "BenillaCharacterFrame"),
+ shown(&mut s, "CharacterFrame"),
"the selected tab is disabled — clicking it cannot close the window"
);
- assert!(
- shown(&mut s, "BenillaPaperDollFrame"),
- "…or change the page"
- );
+ assert!(shown(&mut s, "PaperDollFrame"), "…or change the page");
assert!(s.errors().is_empty(), "no handler errors: {:?}", s.errors());
}
diff --git a/crates/benilla-app/src/ui_script/chat_tests.rs b/crates/benilla-app/src/ui_script/chat_tests.rs
index db99f804..830b4b4a 100644
--- a/crates/benilla-app/src/ui_script/chat_tests.rs
+++ b/crates/benilla-app/src/ui_script/chat_tests.rs
@@ -274,14 +274,10 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
// Drag Fireball off its book button (press → past the 4px threshold → the payload is up).
let (l, r, t, b) = (
- s.eval::("return BenillaSpellButton1:GetLeft()")
- .unwrap(),
- s.eval::("return BenillaSpellButton1:GetRight()")
- .unwrap(),
- s.eval::("return BenillaSpellButton1:GetTop()")
- .unwrap(),
- s.eval::("return BenillaSpellButton1:GetBottom()")
- .unwrap(),
+ s.eval::("return SpellButton1:GetLeft()").unwrap(),
+ s.eval::("return SpellButton1:GetRight()").unwrap(),
+ s.eval::("return SpellButton1:GetTop()").unwrap(),
+ s.eval::("return SpellButton1:GetBottom()").unwrap(),
);
let (x1, y1) = ((l + r) * 0.5, (t + b) * 0.5);
s.mouse_button(x1, y1, "LeftButton", true);
@@ -338,7 +334,7 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
slots,
}),
);
- s.run("C_Container.PickupContainerItem(0, 1)").unwrap();
+ s.run("PickupContainerItem(0, 1)").unwrap();
assert!(s.cursor_item().is_some(), "fixture: the item is held");
s.mouse_button(cx, cy, "LeftButton", true);
s.mouse_button(cx, cy, "LeftButton", false);
@@ -348,3 +344,160 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
+
+// ── ChatTypeInfo: the addon-facing color table ────────────────────────────────────────────────
+
+/// `ChatTypeInfo` carries the shipped default chat colors twice: once in `assets/ui/ChatFrame.xml`
+/// for addons to read, and once in [`crate::ui_chat::default_color`] for our own feed to render.
+/// Both are the same wow-re byte table (`chat-color-table.md`, the static registry at
+/// `.rdata 0x804710`) — so this is the gate that makes the duplication safe: every kind we model
+/// must agree to the byte, and the table's shape (`sticky`/`id`) must be the reference's.
+#[test]
+fn chat_type_info_matches_the_host_color_table() {
+ use crate::ui_chat::{default_color, ChatEventKind as K};
+
+ /// Each modeled kind and its `ChatTypeInfo` key — the reference's own spellings.
+ const PAIRS: &[(&str, K, i64)] = &[
+ ("SAY", K::Say, 1),
+ ("PARTY", K::Party, 2),
+ ("RAID", K::Raid, 3),
+ ("GUILD", K::Guild, 4),
+ ("OFFICER", K::Officer, 5),
+ ("YELL", K::Yell, 6),
+ ("WHISPER", K::Whisper, 7),
+ ("WHISPER_INFORM", K::WhisperInform, 8),
+ ("EMOTE", K::Emote, 9),
+ ("TEXT_EMOTE", K::TextEmote, 10),
+ ("SYSTEM", K::System, 11),
+ ("MONSTER_SAY", K::MonsterSay, 12),
+ ("MONSTER_YELL", K::MonsterYell, 13),
+ ("MONSTER_EMOTE", K::MonsterEmote, 14),
+ ("MONSTER_WHISPER", K::MonsterWhisper, 27),
+ ("CHANNEL", K::Channel, 15),
+ ("CHANNEL_JOIN", K::ChannelJoin, 16),
+ ("CHANNEL_LEAVE", K::ChannelLeave, 17),
+ ("CHANNEL_NOTICE", K::ChannelNotice, 19),
+ ("CHANNEL_NOTICE_USER", K::ChannelNoticeUser, 20),
+ ("CHANNEL_LIST", K::ChannelList, 18),
+ ("AFK", K::Afk, 21),
+ ("DND", K::Dnd, 22),
+ ("IGNORED", K::Ignored, 23),
+ ("SKILL", K::Skill, 24),
+ ("LOOT", K::Loot, 25),
+ ("MONEY", K::Money, 87),
+ ("COMBAT_XP_GAIN", K::CombatXpGain, 46),
+ ("RAID_LEADER", K::RaidLeader, 88),
+ ("RAID_WARNING", K::RaidWarning, 89),
+ ("RAID_BOSS_EMOTE", K::RaidBossEmote, 91),
+ ("BATTLEGROUND", K::Battleground, 93),
+ ("BATTLEGROUND_LEADER", K::BattlegroundLeader, 94),
+ ("BG_SYSTEM_NEUTRAL", K::BgSystemNeutral, 83),
+ ("BG_SYSTEM_ALLIANCE", K::BgSystemAlliance, 84),
+ ("BG_SYSTEM_HORDE", K::BgSystemHorde, 85),
+ ];
+
+ let s = chat_frame();
+ for (name, kind, want_id) in PAIRS {
+ let (r, g, b, id): (f64, f64, f64, i64) = s
+ .eval(&format!(
+ r#"local i = ChatTypeInfo["{name}"] return i.r, i.g, i.b, i.id"#
+ ))
+ .unwrap_or_else(|e| panic!(r#"ChatTypeInfo["{name}"]: {e}"#));
+ let got = [
+ (r * 255.0).round() as u8,
+ (g * 255.0).round() as u8,
+ (b * 255.0).round() as u8,
+ ];
+ assert_eq!(got, default_color(*kind), r#"ChatTypeInfo["{name}"] color"#);
+ // `id` is GetChatTypeIndex's 1-based registry slot, asserted EXACTLY rather than as a
+ // range: four of these ship the same FFDBB7 and a range check would let a transposition
+ // inside that family through with both colors still matching.
+ assert_eq!(id, *want_id, r#"ChatTypeInfo["{name}"].id"#);
+ }
+}
+
+/// The table's *shape*, transcribed rather than derived: the reference's 105 keys, its five sticky
+/// types (the set `ui_chat::edit`'s `SendType::sticky` already quotes), the two keys FrameXML
+/// declares that the engine's color registry does not contain, and the ten boot-seeded channel
+/// extras. An addon reads `.sticky` and `.id` as often as it reads the color.
+#[test]
+fn chat_type_info_has_the_references_shape() {
+ let s = chat_frame();
+
+ let count: i64 = s
+ .eval("local n = 0 for _ in pairs(ChatTypeInfo) do n = n + 1 end return n")
+ .unwrap();
+ assert_eq!(count, 105, "the reference declares 105 keys");
+
+ let sticky: String = s
+ .eval(
+ "local t = {} for k, v in pairs(ChatTypeInfo) do if v.sticky == 1 then \
+ table.insert(t, k) end end table.sort(t) return table.concat(t, \" \")",
+ )
+ .unwrap();
+ assert_eq!(sticky, "BATTLEGROUND GUILD PARTY RAID SAY");
+
+ // REPLY and COMBAT_ERROR are declared by FrameXML and absent from the engine's 94-entry
+ // registry, so GetChatTypeIndex answers 0 for both — but their COLORS differ, which is the
+ // trap this asserts. Nothing ever writes COMBAT_ERROR. REPLY is written by hand inside the
+ // UPDATE_CHAT_COLOR handler, which mirrors WHISPER into it (ChatFrame.lua l.1357-1365), so
+ // its end state is WHISPER's FF80FF. Seeding both white reads as symmetric and is wrong.
+ for (name, want) in [
+ ("REPLY", [255u8, 128, 255]),
+ ("COMBAT_ERROR", [255, 255, 255]),
+ ] {
+ let (id, r, g, b): (i64, f64, f64, f64) = s
+ .eval(&format!(
+ r#"local i = ChatTypeInfo["{name}"] return i.id, i.r, i.g, i.b"#
+ ))
+ .unwrap();
+ assert_eq!(id, 0, r#""{name}".id — not in the engine's registry"#);
+ let got = [
+ (r * 255.0).round() as u8,
+ (g * 255.0).round() as u8,
+ (b * 255.0).round() as u8,
+ ];
+ assert_eq!(got, want, r#""{name}" color"#);
+ }
+
+ // The extras: CHANNEL1..CHANNEL10, indices 95..104, each the live CHANNEL entry's FFC0C0.
+ for n in 1..=10 {
+ let (id, r, g, b): (i64, f64, f64, f64) = s
+ .eval(&format!(
+ r#"local i = ChatTypeInfo["CHANNEL{n}"] return i.id, i.r, i.g, i.b"#
+ ))
+ .unwrap();
+ assert_eq!(id, 94 + n, "CHANNEL{n}.id");
+ let rgb = [
+ (r * 255.0).round() as u8,
+ (g * 255.0).round() as u8,
+ (b * 255.0).round() as u8,
+ ];
+ assert_eq!(rgb, [255, 192, 192], "CHANNEL{n} color");
+ }
+}
+
+/// **Every event name we fire is a key the reference's own `ChatTypeInfo` carries.**
+/// `ChatFrame_OnEvent` recovers the type with `strsub(event, 10)` and indexes
+/// `ChatTypeInfo[type]` with it — a name that misses that table is a name whose colour, id and
+/// sticky flag an addon cannot look up, so this is the check that our `CHAT_MSG_*` spellings are
+/// the reference's and not ours.
+///
+/// Swept over `ChatEventKind::ALL` rather than a second hand-written list, so a kind added without
+/// a matching table key fails here instead of shipping a name nothing can resolve.
+#[test]
+fn fired_event_names_are_all_chat_type_info_keys() {
+ use crate::ui_chat::{event_name, ChatEventKind as K};
+
+ let s = chat_frame();
+ for &kind in K::ALL {
+ let name = event_name(kind);
+ let key = name
+ .strip_prefix("CHAT_MSG_")
+ .unwrap_or_else(|| panic!("{name}: every fired chat event is CHAT_MSG_-prefixed"));
+ let present: bool = s
+ .eval(&format!(r#"return ChatTypeInfo["{key}"] ~= nil"#))
+ .unwrap();
+ assert!(present, r#"{name} → ChatTypeInfo["{key}"] is missing"#);
+ }
+}
diff --git a/crates/benilla-app/src/ui_script/color_picker_tests.rs b/crates/benilla-app/src/ui_script/color_picker_tests.rs
new file mode 100644
index 00000000..9f3a74e9
--- /dev/null
+++ b/crates/benilla-app/src/ui_script/color_picker_tests.rs
@@ -0,0 +1,526 @@
+//! The shipped `assets/ui/ColorPickerFrame.xml` + the dropdown's colour-swatch row, driven the way
+//! the corpus drives them.
+//!
+//! Nothing benilla ships opens this window. Its consumers are third-party addons — 86 of the 218
+//! reach `ColorPickerFrame`, and 64 of those files are copies of `Dewdrop-2.0.lua`, THE Ace2 menu
+//! library, so its block below is not one addon's idiom but the path every colour option in every
+//! Ace2 config menu takes. Every test here therefore enters from Lua exactly as they do; the
+//! Dewdrop and AceConsole sequences are transcribed from corpus copies, cited per test.
+
+use benilla_ui::script::{QuadContent, UiScript};
+
+/// Load one shipped `assets/ui/` into `s`, panicking on any loader error.
+fn load_xml(s: &UiScript, file: &str) {
+ let text = std::fs::read_to_string(
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("assets/ui")
+ .join(file),
+ )
+ .unwrap();
+ let doc = benilla_ui::framexml::parse(&text).unwrap();
+ let report = benilla_ui::loader::load(s, &doc, &|_| None);
+ assert!(
+ report.errors.is_empty(),
+ "{file}: loader errors: {:?}",
+ report.errors
+ );
+}
+
+/// The manifest prefix the picker needs: fonts, the panel manager (`ShowUIPanel`/`HideUIPanel`,
+/// `UISpecialFrames`, the ESC chain), the shared widget kit (`GameMenuButtonTemplate`), then the
+/// window. `UIDropDownMenu.xml` rides along because `CloseMenus` and the swatch row live there.
+fn picker() -> UiScript {
+ let mut s = UiScript::new().unwrap();
+ for file in [
+ "Fonts.xml",
+ "UiPanels.xml",
+ "UIParent.xml",
+ "GameTooltip.xml",
+ "UIDropDownMenu.xml",
+ "ScrollTemplates.xml",
+ "UIPanelTemplates.xml",
+ "ColorPickerFrame.xml",
+ ] {
+ load_xml(&s, file);
+ }
+ s.set_screen_size(1024.0, 768.0);
+ s.resolve();
+ s
+}
+
+/// What the widget hands back after being *set* to `(r, g, b)` — the client's whole colour law, run
+/// through the engine's own transcription of it rather than restated here.
+///
+/// It is emphatically **not** the identity, and not a simple quantize either: `SetColorRGB` rounds
+/// half-up into bytes and stores HSV floats, while every read path floors, so 9.75 % of colours come
+/// back a step low on one channel (wow-re `system/ui/scratch/colorselect-color-law.md`, exhaustive
+/// over all 256³). `benilla_ui::script::colorselect`'s own tests pin that law against wow-re's
+/// measured witnesses; these tests use it as a given so they stay about the Dewdrop plumbing.
+fn after_round_trip(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
+ let mut cs = benilla_ui::widget::ColorSelectState::default();
+ cs.set_rgb(r, g, b);
+ cs.rgb_f64()
+}
+
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+// The window's own API surface
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+
+/// The four names addons address by string all exist, are the right kinds, and the two buttons'
+/// `OnClick` is fetchable — which is not decoration: `AceConsole-2.0.lua` l.1402 does
+/// `ColorPickerOkayButton:GetScript("OnClick")` and refuses to install its colour option at all if
+/// `ColorPickerOkayButton` is nil.
+#[test]
+fn the_named_pieces_exist_and_their_scripts_are_fetchable() {
+ let s = picker();
+ assert!(s
+ .eval::("return ColorPickerFrame ~= nil and ColorPickerFrame.SetColorRGB ~= nil")
+ .unwrap());
+ assert!(s
+ .eval::("return OpacitySliderFrame ~= nil and OpacitySliderFrame.GetValue ~= nil")
+ .unwrap());
+ for button in ["ColorPickerOkayButton", "ColorPickerCancelButton"] {
+ assert!(
+ s.eval::(&format!("return {button} ~= nil")).unwrap(),
+ "{button} is missing"
+ );
+ assert!(
+ s.eval::(&format!(
+ r#"return type({button}:GetScript("OnClick")) == "function""#
+ ))
+ .unwrap(),
+ "{button}'s OnClick is not fetchable"
+ );
+ }
+ // Born hidden, like the reference's `hidden="true"`.
+ assert!(!s
+ .eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap());
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+}
+
+/// `SetColorRGB` paints the preview swatch through `OnColorSelect`, and `GetColorRGB` answers the
+/// client's quantized colour. The swatch is the ONLY thing in the window that shows the colour, and
+/// nothing but this handler ever writes it.
+///
+/// The assertion goes through `extract()` on purpose. `ColorSwatch:SetTexture(r, g, b)` is the
+/// *solid-colour* form of SetTexture — it replaces the texture, it does not tint one — so
+/// `GetVertexColor` still reads the `` the XML declared (1,1,1,1) and would have "passed"
+/// this test while the swatch drew nothing. What the renderer receives is the only honest witness.
+#[test]
+fn set_color_rgb_paints_the_swatch_and_reads_back_the_widgets_colour() {
+ let mut s = picker();
+ s.run("ShowUIPanel(ColorPickerFrame)").unwrap();
+ s.run("ColorPickerFrame:SetColorRGB(0.2, 0.4, 0.8)")
+ .unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+
+ let (r, g, b): (f64, f64, f64) = s.eval("return ColorPickerFrame:GetColorRGB()").unwrap();
+ assert_eq!((r, g, b), after_round_trip(0.2, 0.4, 0.8));
+
+ s.resolve();
+ let want = [r as f32, g as f32, b as f32];
+ let painted = s.extract().into_iter().any(|q| match &q.content {
+ QuadContent::Texture {
+ path: None,
+ color: Some(c),
+ ..
+ } => [c[0], c[1], c[2]] == want && c[3] == 1.0,
+ _ => false,
+ });
+ assert!(
+ painted,
+ "no solid quad carrying the widget's colour {want:?} — the swatch is not painted"
+ );
+}
+
+/// `hasOpacity` is what decides whether the window has an opacity slider at all — and the window's
+/// WIDTH with it. Both arms of the reference's `OnShow` (l.161-170).
+#[test]
+fn has_opacity_shows_the_slider_and_widens_the_window() {
+ let mut s = picker();
+
+ // Without it: no slider, the narrow window. (`GetWidth` reports the *resolved* rect, so the
+ // OnShow's SetWidth needs a resolve pass before it is readable — the same round-trip every
+ // geometry assertion in this file set makes.)
+ s.run("ColorPickerFrame.hasOpacity = nil ShowUIPanel(ColorPickerFrame)")
+ .unwrap();
+ s.resolve();
+ assert!(!s
+ .eval::("return OpacitySliderFrame:IsVisible()")
+ .unwrap());
+ assert_eq!(
+ s.eval::("return ColorPickerFrame:GetWidth()").unwrap(),
+ 305.0
+ );
+ s.run("HideUIPanel(ColorPickerFrame)").unwrap();
+
+ // With it: the slider shows, seeded from `.opacity`, and the window is back to 365.
+ s.run("ColorPickerFrame.hasOpacity = 1 ColorPickerFrame.opacity = 0.25 ShowUIPanel(ColorPickerFrame)")
+ .unwrap();
+ s.resolve();
+ assert!(s
+ .eval::("return OpacitySliderFrame:IsVisible()")
+ .unwrap());
+ assert_eq!(
+ s.eval::("return OpacitySliderFrame:GetValue()")
+ .unwrap(),
+ 0.25
+ );
+ assert_eq!(
+ s.eval::("return ColorPickerFrame:GetWidth()").unwrap(),
+ 365.0
+ );
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+}
+
+/// Moving the slider runs `opacityFunc` on every change — the live-preview half of the contract,
+/// and the reference's own `` (l.146-152).
+#[test]
+fn the_opacity_slider_drives_opacity_func_on_every_change() {
+ let s = picker();
+ s.run(
+ r#"
+ seen = {}
+ ColorPickerFrame.opacityFunc = function()
+ table.insert(seen, OpacitySliderFrame:GetValue())
+ end
+ "#,
+ )
+ .unwrap();
+ s.run("OpacitySliderFrame:SetValue(0.5)").unwrap();
+ s.run("OpacitySliderFrame:SetValue(0.75)").unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert_eq!(s.eval::("return table.getn(seen)").unwrap(), 2);
+ assert_eq!(s.eval::("return seen[1]").unwrap(), 0.5);
+ assert_eq!(s.eval::("return seen[2]").unwrap(), 0.75);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+// Dewdrop-2.0 — the sequence 64 corpus files share
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+
+/// The `Dewdrop-2.0.lua` block verbatim in shape (a corpus copy, l.425-465): set `func` to a closure
+/// that reads `GetColorRGB()` + `1 - OpacitySliderFrame:GetValue()` and calls the addon's setter
+/// with the closed-over arg prefix, mirror it into `opacityFunc`, set `opacity = 1 - this.opacity`,
+/// `SetColorRGB`, capture the old values into `cancelFunc`, `ShowUIPanel`.
+///
+/// Two things this proves beyond "it runs": the setter fires **at `SetColorRGB` time**, before the
+/// window is even shown (the reference's own behaviour — `func` is a live preview, and it is why
+/// `hasOpacity`/`opacity` are assigned before the colour), and clicking **Okay** commits with the
+/// slider's final value.
+fn dewdrop_open(s: &UiScript, r: f64, g: f64, b: f64, opacity: f64) {
+ s.run(&format!(
+ r#"
+ applied = {{}}
+ -- the addon's own setter, the `func` Dewdrop closes over
+ local func = function(a1, r, g, b, a)
+ table.insert(applied, {{ key = a1, r = r, g = g, b = b, a = a }})
+ end
+ local a1 = "bordercolor" -- Dewdrop's colorArg1
+ local hasOpacity = 1
+ local this = {{ r = {r}, g = {g}, b = {b}, opacity = {opacity}, hasOpacity = 1 }}
+
+ ColorPickerFrame.func = function()
+ if func then
+ local r, g, b = ColorPickerFrame:GetColorRGB()
+ local a = hasOpacity and 1 - OpacitySliderFrame:GetValue() or nil
+ func(a1, r, g, b, a)
+ end
+ end
+ ColorPickerFrame.hasOpacity = this.hasOpacity
+ ColorPickerFrame.opacityFunc = ColorPickerFrame.func
+ ColorPickerFrame.opacity = 1 - this.opacity
+ ColorPickerFrame:SetColorRGB(this.r, this.g, this.b)
+ local pr, pg, pb, pa = this.r, this.g, this.b, this.opacity
+ ColorPickerFrame.cancelFunc = function()
+ func(a1, pr, pg, pb, pa)
+ end
+ ShowUIPanel(ColorPickerFrame)
+ "#
+ ))
+ .unwrap();
+}
+
+/// **Okay commits.** The Dewdrop sequence, then the OK button: the addon's setter receives the
+/// quantized colour and the slider-derived alpha, and the window closes.
+#[test]
+fn the_dewdrop_sequence_commits_on_okay() {
+ let s = picker();
+ dewdrop_open(&s, 0.1, 0.5, 0.9, 0.25);
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+
+ // The live preview has already run TWICE before the window is even on screen, and both are the
+ // reference's own: `SetColorRGB` fires `OnColorSelect` → `func()`, and then `OnShow`'s
+ // `OpacitySliderFrame:SetValue(this.opacity)` moves the slider off its `defaultValue="1"` and
+ // fires `OnValueChanged` → `opacityFunc()`, which Dewdrop has aliased to the same closure.
+ // Pinned rather than glossed: an addon whose `func` writes through to the server sees both.
+ assert_eq!(s.eval::("return table.getn(applied)").unwrap(), 2);
+ assert!(s
+ .eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap());
+ // The window came up with the slider at `1 - opacity`, which is what OnShow seeded.
+ assert_eq!(
+ s.eval::("return OpacitySliderFrame:GetValue()")
+ .unwrap(),
+ 0.75
+ );
+
+ // The player drags the opacity down, then accepts.
+ s.run("OpacitySliderFrame:SetValue(0.4)").unwrap();
+ s.run("ColorPickerOkayButton:Click()").unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert!(!s
+ .eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap());
+
+ // Last call wins: the addon holds the quantized colour and `1 - slider`.
+ let (key, r, g, b, a): (String, f64, f64, f64, f64) = s
+ .eval("local t = applied[table.getn(applied)] return t.key, t.r, t.g, t.b, t.a")
+ .unwrap();
+ assert_eq!(
+ key, "bordercolor",
+ "the closed-over arg prefix is preserved"
+ );
+ assert_eq!((r, g, b), after_round_trip(0.1, 0.5, 0.9));
+ // 1e-6, not exact: a Slider holds `f32` (the client's `CSimpleSlider` does too), so
+ // `1 - GetValue()` is `1 - f32(0.4)` = 0.59999999…, not the literal 0.6.
+ assert!(
+ (a - 0.6).abs() < 1e-6,
+ "alpha is 1 - the slider's 0.4, got {a}"
+ );
+}
+
+/// **Cancel restores.** The half people get wrong: Cancel must put back the colour the addon came
+/// in with, and it does it by *calling `cancelFunc`* — the window has no memory of its own.
+#[test]
+fn cancel_restores_the_previous_colour_through_cancel_func() {
+ let s = picker();
+ dewdrop_open(&s, 0.1, 0.5, 0.9, 0.25);
+
+ // The player fiddles: a new colour previewed live, and the opacity moved.
+ s.run("ColorPickerFrame:SetColorRGB(1, 0, 0)").unwrap();
+ s.run("OpacitySliderFrame:SetValue(0.05)").unwrap();
+ let (mid_r, mid_a): (f64, f64) = s
+ .eval("local t = applied[table.getn(applied)] return t.r, t.a")
+ .unwrap();
+ assert_eq!(mid_r, 1.0, "the live preview really did go red");
+ assert!((mid_a - 0.95).abs() < 1e-9);
+
+ // …then backs out.
+ s.run("ColorPickerCancelButton:Click()").unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert!(!s
+ .eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap());
+
+ let (r, g, b, a): (f64, f64, f64, f64) = s
+ .eval("local t = applied[table.getn(applied)] return t.r, t.g, t.b, t.a")
+ .unwrap();
+ assert_eq!(
+ (r, g, b, a),
+ (0.1, 0.5, 0.9, 0.25),
+ "cancelFunc replays the addon's OWN pre-open values — raw, never through the widget"
+ );
+}
+
+/// ESC cancels too, and that is a deviation working as designed: the reference hooks ``
+/// on the frame, this engine routes the press through `ToggleGameMenu`'s chain (UiPanels.xml), and
+/// the rung cancel-CLICKS rather than hiding — so the colour comes back and the game menu does not
+/// open behind it.
+#[test]
+fn escape_cancels_rather_than_merely_hiding() {
+ let s = picker();
+ dewdrop_open(&s, 0.1, 0.5, 0.9, 0.25);
+ s.run("ColorPickerFrame:SetColorRGB(1, 0, 0)").unwrap();
+
+ s.run("ToggleGameMenu()").unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert!(!s
+ .eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap());
+ let (r, g, b): (f64, f64, f64) = s
+ .eval("local t = applied[table.getn(applied)] return t.r, t.g, t.b")
+ .unwrap();
+ assert_eq!(
+ (r, g, b),
+ (0.1, 0.5, 0.9),
+ "ESC ran cancelFunc, not a bare Hide"
+ );
+}
+
+/// `AceConsole-2.0.lua` l.1402-1406, verbatim in shape: fetch the Okay button's own handler, replace
+/// it with one that calls the original and then does the addon's own commit. If `GetScript` returned
+/// nil, or `SetScript` did not take, the addon's arm would never run — and 20+ corpus addons ship
+/// this library.
+#[test]
+fn ace_console_can_chain_the_okay_buttons_onclick() {
+ let s = picker();
+ s.run(
+ r#"
+ chained = {}
+ ColorPickerFrame.func = function() table.insert(chained, "original") end
+ if ColorPickerOkayButton then
+ local ColorPickerOkayButton_OnClick = ColorPickerOkayButton:GetScript("OnClick")
+ ColorPickerOkayButton:SetScript("OnClick", function()
+ if ColorPickerOkayButton_OnClick then
+ ColorPickerOkayButton_OnClick()
+ end
+ table.insert(chained, "ace")
+ end)
+ end
+ ShowUIPanel(ColorPickerFrame)
+ "#,
+ )
+ .unwrap();
+ s.run("ColorPickerOkayButton:Click()").unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert_eq!(s.eval::("return table.getn(chained)").unwrap(), 2);
+ assert_eq!(s.eval::("return chained[1]").unwrap(), "original");
+ assert_eq!(s.eval::("return chained[2]").unwrap(), "ace");
+ assert!(
+ !s.eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap(),
+ "the original handler still hid the window"
+ );
+}
+
+/// ESC's other path: `CloseWindows` walks `UISpecialFrames`, and `ColorPickerFrame` is in it now
+/// (decision 1206 declined to seed it only because we shipped no such frame). This is the plain
+/// `Hide` the reference does on that path — no cancel — reached when something else closes the
+/// world's windows.
+#[test]
+fn the_picker_is_a_uispecialframe_and_close_windows_puts_it_away() {
+ let s = picker();
+ let listed: bool = s
+ .eval(
+ r#"
+ for _, name in ipairs(UISpecialFrames) do
+ if name == "ColorPickerFrame" then return true end
+ end
+ return false
+ "#,
+ )
+ .unwrap();
+ assert!(listed, "the reference's own entry is seeded");
+
+ s.run("ShowUIPanel(ColorPickerFrame)").unwrap();
+ let found: bool = s.eval("return CloseWindows() ~= nil").unwrap();
+ assert!(found, "CloseWindows reports it closed something");
+ assert!(!s
+ .eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap());
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+}
+
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+// The dropdown's colour-swatch row — the other half of the same feature
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+
+/// A menu row with `hasColorSwatch` shows its square tinted to `r/g/b`, and clicking the square is
+/// what opens the picker — seeding `func`/`hasOpacity`/`opacity`/`cancelFunc`, the colour, AND
+/// `previousValues`, which is captured here and nowhere else. Then Cancel gets it back.
+///
+/// The info table is the reference's own documented shape (`UIDropDownMenu.lua` l.114-125) and the
+/// exact one FloatingChatFrame's colour rows use.
+#[test]
+fn a_dropdown_row_with_has_color_swatch_opens_the_picker_and_cancel_restores() {
+ let s = picker();
+ s.run(
+ r#"
+ restored = {}
+ picked = {}
+ local dd = CreateFrame("Frame", "TestColorDropDown", nil, "UIDropDownMenuTemplate")
+ UIDropDownMenu_Initialize(dd, function()
+ local info = {}
+ info.text = "Border Color"
+ info.hasColorSwatch = 1
+ info.r = 0.2
+ info.g = 0.4
+ info.b = 0.6
+ info.hasOpacity = 1
+ info.opacity = 0.3
+ info.notCheckable = 1
+ info.swatchFunc = function()
+ local r, g, b = ColorPickerFrame:GetColorRGB()
+ table.insert(picked, r .. "," .. g .. "," .. b)
+ end
+ info.opacityFunc = function() end
+ info.cancelFunc = function(previous)
+ table.insert(restored, previous.r .. "," .. previous.g .. "," .. previous.b .. "," .. previous.opacity)
+ end
+ UIDropDownMenu_AddButton(info)
+ end, "MENU")
+ ToggleDropDownMenu(1, nil, dd, "TestColorDropDown", 0, 0)
+ "#,
+ )
+ .unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+
+ // The square is up and wears the row's colour.
+ assert!(s
+ .eval::("return DropDownList1Button1ColorSwatch:IsVisible()")
+ .unwrap());
+ let (r, g, b): (f64, f64, f64) = s
+ .eval("return DropDownList1Button1ColorSwatchNormalTexture:GetVertexColor()")
+ .unwrap();
+ assert!(
+ (r - 0.2).abs() < 1e-6 && (g - 0.4).abs() < 1e-6 && (b - 0.6).abs() < 1e-6,
+ "the swatch is tinted to info.r/g/b, got {r},{g},{b}"
+ );
+
+ // Clicking it opens the picker, seeded from the row.
+ s.run("DropDownList1Button1ColorSwatch:Click()").unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert!(s
+ .eval::("return ColorPickerFrame:IsVisible()")
+ .unwrap());
+ assert!(
+ !s.eval::("return DropDownList1:IsVisible()").unwrap(),
+ "CloseMenus() shut the menu first, the ref's own first line"
+ );
+ assert_eq!(
+ s.eval::("return ColorPickerFrame.opacity").unwrap(),
+ 0.3
+ );
+ assert!(s
+ .eval::("return OpacitySliderFrame:IsVisible()")
+ .unwrap());
+ let (pr, pg, pb): (f64, f64, f64) = s.eval("return ColorPickerFrame:GetColorRGB()").unwrap();
+ assert_eq!((pr, pg, pb), after_round_trip(0.2, 0.4, 0.6));
+ // …and swatchFunc became the picker's live-preview `func`, which already ran once.
+ assert_eq!(s.eval::("return table.getn(picked)").unwrap(), 1);
+
+ // Cancel hands `previousValues` — the table OpenColorPicker captured — to the addon.
+ s.run("ColorPickerCancelButton:Click()").unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert_eq!(
+ s.eval::("return restored[1]").unwrap(),
+ "0.2,0.4,0.6,0.3",
+ "cancelFunc(previousValues) carries r/g/b/opacity straight off the row"
+ );
+}
+
+/// A row WITHOUT `hasColorSwatch` keeps its square hidden — the branch that stops every ordinary
+/// menu row from sprouting a colour button.
+#[test]
+fn a_row_without_the_flag_has_no_swatch() {
+ let s = picker();
+ s.run(
+ r#"
+ local dd = CreateFrame("Frame", "TestPlainDropDown", nil, "UIDropDownMenuTemplate")
+ UIDropDownMenu_Initialize(dd, function()
+ local info = {}
+ info.text = "Just A Row"
+ info.notCheckable = 1
+ UIDropDownMenu_AddButton(info)
+ end, "MENU")
+ ToggleDropDownMenu(1, nil, dd, "TestPlainDropDown", 0, 0)
+ "#,
+ )
+ .unwrap();
+ assert!(s.errors().is_empty(), "{:?}", s.errors());
+ assert!(!s
+ .eval::("return DropDownList1Button1ColorSwatch:IsVisible()")
+ .unwrap());
+}
diff --git a/crates/benilla-app/src/ui_script/combo_frame_tests.rs b/crates/benilla-app/src/ui_script/combo_frame_tests.rs
index b353194c..95fdc61d 100644
--- a/crates/benilla-app/src/ui_script/combo_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/combo_frame_tests.rs
@@ -28,7 +28,7 @@ fn load_xml(s: &UiScript, file: &str) {
}
/// The manifest order this file actually ships in: `UiPanels.xml` for `UIFrameFade`,
-/// `UnitFrames.xml` for the `BenillaTargetFrame` the dots anchor to.
+/// `UnitFrames.xml` for the `TargetFrame` the dots anchor to.
fn load_combo_frame() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
@@ -69,11 +69,11 @@ fn settle(s: &mut UiScript) {
}
fn shown(s: &mut UiScript) -> bool {
- s.eval("return BenillaComboFrame:IsShown()").unwrap()
+ s.eval("return ComboFrame:IsShown()").unwrap()
}
fn highlight_alpha(s: &mut UiScript, i: u32) -> f64 {
- s.eval(&format!("return BenillaComboPoint{i}Highlight:GetAlpha()"))
+ s.eval(&format!("return ComboPoint{i}Highlight:GetAlpha()"))
.unwrap()
}
@@ -111,7 +111,7 @@ fn combo_frame_follows_the_point_count() {
.eval(
r#"
for i = 1, MAX_COMBO_POINTS do
- if getglobal("BenillaComboPoint" .. i .. "Highlight"):GetAlpha() < 0.99 then
+ if getglobal("ComboPoint" .. i .. "Highlight"):GetAlpha() < 0.99 then
return false
end
end
@@ -141,13 +141,13 @@ fn a_repaint_at_the_same_count_does_not_re_flare() {
settle(&mut s);
// Both settled: highlights up, shines burned out.
- let shine: f64 = s.eval("return BenillaComboPoint2Shine:GetAlpha()").unwrap();
+ let shine: f64 = s.eval("return ComboPoint2Shine:GetAlpha()").unwrap();
assert_eq!(shine, 0.0, "the shine fades back out");
// A re-select of the SAME unit at the same count: no new fade is queued for either dot.
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
let fading: bool = s
- .eval("return UIFrameIsFading(BenillaComboPoint1Highlight) ~= nil")
+ .eval("return UIFrameIsFading(ComboPoint1Highlight) ~= nil")
.unwrap();
assert!(!fading, "an unchanged count re-flares nothing");
assert!(
diff --git a/crates/benilla-app/src/ui_script/content.rs b/crates/benilla-app/src/ui_script/content.rs
index 9032a440..6bc2721d 100644
--- a/crates/benilla-app/src/ui_script/content.rs
+++ b/crates/benilla-app/src/ui_script/content.rs
@@ -5,8 +5,9 @@
//! here instead of touching `std::fs`.
//!
//! Until 1175 they read `concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui")` — the *build*
-//! machine's source tree. On a player's machine those 61 files resolve to nothing and the client
-//! boots with no interface at all: not a crash, just an empty screen and 61 log lines nobody sees.
+//! machine's source tree. On a player's machine every one of those files resolves to nothing and
+//! the client boots with no interface at all: not a crash, just an empty screen and one log line
+//! per manifest entry that nobody sees.
//! `assets/ui` is our own content (MIT/Apache, not Blizzard's — the contract's hard rule is
//! untouched), so it can simply be part of the program.
//!
@@ -23,7 +24,7 @@
//!
//! ## Why not the asset server
//!
-//! [`super::manifest::MANIFEST`] is a 61-entry **dependency-ordered** manifest loaded synchronously
+//! [`super::manifest::MANIFEST`] is a **dependency-ordered** manifest loaded synchronously
//! at a known point (a template must exist before the frame that inherits it), and the asset server
//! is async. Serving these through it would mean re-deriving a dependency graph that a hand-ordered
//! list already encodes correctly. This stays a byte lookup.
@@ -34,8 +35,10 @@
//! *not* built here (1175 "Rejected"), but the shape leaves room for it: it would be one more arm
//! at the front of [`read`]. Building it now would be a mod system with no mods.
-/// The shipped UI tree, compiled in: `benilla.toc` and the 61 files it names, ~2.8 MB — under
-/// 2.5 % of the release binary, and what it buys is "the interface cannot be missing".
+/// The shipped UI tree, compiled in: `benilla.toc` and every file it names, a few MB — a small
+/// single-digit % of the release binary, and what it buys is "the interface cannot be missing".
+/// (A hard file count lived here until it had silently drifted by four; the manifest is the
+/// authority on how many there are, and `manifest::tests` is what keeps the two agreeing.)
static UI: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/assets/ui");
/// The text of one shipped UI file, by path relative to `assets/ui` — `None` if we do not ship it.
@@ -63,6 +66,40 @@ fn read_source_tree(req: &str) -> Option {
std::fs::read_to_string(dir.join(req)).ok()
}
+/// A digest of the FrameXML **this process will actually load** — the manifest plus every file it
+/// names, hashed in load order, as eight hex digits.
+///
+/// It exists because a number taken from the corpus harness is meaningless without it. In a dev
+/// build [`read`] prefers the SOURCE TREE, so editing an `assets/ui` file changes what a survey
+/// loads **with no rebuild** — and this repo's worktrees are routinely shared by several agents at
+/// once. Three separate measurements this arc were taken across a moving tree and one of them
+/// landed a wrong attribution in a decision record: 87/218 was credited to a single table when a
+/// controlled A/B later put it at 75, with the other twelve belonging to a neighbour's uncommitted
+/// files. The stamp does not prevent that; it makes it visible, which is all an instrument can do.
+///
+/// FNV-1a, no dependency, not cryptographic and not meant to be: the only question it answers is
+/// *were these two runs looking at the same interface*.
+pub(crate) fn digest() -> String {
+ let mut h: u64 = 0xcbf2_9ce4_8422_2325;
+ let mut eat = |bytes: &[u8]| {
+ for b in bytes {
+ h ^= u64::from(*b);
+ h = h.wrapping_mul(0x1000_0000_01b3);
+ }
+ };
+ eat(super::manifest::MANIFEST.as_bytes());
+ if let Some(toc) = read(super::manifest::MANIFEST) {
+ eat(toc.as_bytes());
+ }
+ for name in super::addons::Addon::builtin().toc.files {
+ eat(name.as_bytes());
+ if let Some(text) = read(&name) {
+ eat(text.as_bytes());
+ }
+ }
+ format!("{:08x}", h as u32)
+}
+
/// Every shipped file's path, relative to `assets/ui`. The compiled-in set is the authority —
/// a dev build must not appear to ship a file that is only on this machine.
#[cfg(test)]
diff --git a/crates/benilla-app/src/ui_script/delete_item_tests.rs b/crates/benilla-app/src/ui_script/delete_item_tests.rs
index 0698b0df..91bf551c 100644
--- a/crates/benilla-app/src/ui_script/delete_item_tests.rs
+++ b/crates/benilla-app/src/ui_script/delete_item_tests.rs
@@ -72,7 +72,7 @@ fn setup() -> UiScript {
/// `DELETE_ITEM_CONFIRM(name, quality)` the driver listens for.
fn pick_up_and_drop_in_world(s: &mut UiScript) {
s.set_container(0, Some(one_item_backpack()));
- s.run("C_Container.PickupContainerItem(0, 1)").unwrap();
+ s.run("PickupContainerItem(0, 1)").unwrap();
assert!(s.cursor_item().is_some(), "fixture: the item is held");
// Off past every frame (the bag sits bottom-right; off-screen negative is always clear).
s.mouse_button(-50.0, -50.0, "LeftButton", true);
@@ -153,7 +153,7 @@ fn delete_item_confirm_no_clears_without_destroying() {
"the clear's ITEM_LOCK_CHANGED repaints the bag (the stuck-darkened slot, 0218)"
);
assert!(
- !s.eval::("local i = C_Container.GetContainerItemInfo(0, 1) return i.isLocked")
+ !s.eval::("local _, _, locked = GetContainerItemInfo(0, 1) return locked")
.unwrap(),
"the source slot reads unlocked again"
);
diff --git a/crates/benilla-app/src/ui_script/dressup_tests.rs b/crates/benilla-app/src/ui_script/dressup_tests.rs
index 9ffc83e8..d682edf5 100644
--- a/crates/benilla-app/src/ui_script/dressup_tests.rs
+++ b/crates/benilla-app/src/ui_script/dressup_tests.rs
@@ -102,9 +102,7 @@ fn ctrl_click_on_a_bag_item_opens_the_room_wearing_it() {
s.set_screen_size(1024.0, 768.0);
load_room(&s);
let (x, y) = backpack_with_jerky(&mut s);
- assert!(!s
- .eval::("return BenillaDressUpFrame:IsVisible()")
- .unwrap());
+ assert!(!s.eval::("return DressUpFrame:IsVisible()").unwrap());
s.set_modifiers(false, true, false);
s.mouse_button(x, y, "LeftButton", true);
@@ -112,8 +110,7 @@ fn ctrl_click_on_a_bag_item_opens_the_room_wearing_it() {
s.set_modifiers(false, false, false);
assert!(
- s.eval::("return BenillaDressUpFrame:IsVisible()")
- .unwrap(),
+ s.eval::("return DressUpFrame:IsVisible()").unwrap(),
"ctrl-click opened the dressing room"
);
assert_eq!(
@@ -145,11 +142,10 @@ fn shift_click_posts_the_link_with_chat_open_and_splits_with_it_closed() {
s.mouse_button(x, y, "LeftButton", false);
s.set_modifiers(false, false, false);
assert!(
- s.eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap(),
+ s.eval::("return StackSplitFrame:IsShown()").unwrap(),
"with chat closed, shift-click still opens the stack splitter"
);
- s.run("BenillaStackSplitFrame:Hide()").unwrap();
+ s.run("StackSplitFrame:Hide()").unwrap();
// Chat open → the link, and no splitter.
assert!(s.focus_editbox("ChatFrameEditBox"));
@@ -164,8 +160,7 @@ fn shift_click_posts_the_link_with_chat_open_and_splits_with_it_closed() {
"the item's full escaped link landed in the chat box"
);
assert!(
- !s.eval::("return BenillaStackSplitFrame:IsShown()")
- .unwrap(),
+ !s.eval::("return StackSplitFrame:IsShown()").unwrap(),
"with chat open the splitter must NOT open"
);
assert!(
@@ -197,8 +192,7 @@ fn ctrl_clicking_a_chat_link_previews_it() {
"a chat link's id reached TryOn through DressUpItemLink's own gsub"
);
assert!(
- s.eval::("return BenillaDressUpFrame:IsVisible()")
- .unwrap(),
+ s.eval::("return DressUpFrame:IsVisible()").unwrap(),
"and the room opened"
);
// The plain click still shows the link tooltip rather than the room (no modifier).
@@ -243,7 +237,7 @@ fn the_paper_doll_slots_preview_and_post_what_you_wear() {
// CTRL on the head slot → the room, wearing the helm.
s.set_modifiers(false, true, false);
- s.run("BenillaPaperDollSlot_OnClick(BenillaCharacterHeadSlot, \"LeftButton\")")
+ s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
.unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
@@ -254,7 +248,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(BenillaCharacterHeadSlot, \"LeftButton\")")
+ s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
.unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
@@ -299,7 +293,7 @@ fn shift_clicking_an_unresolved_slot_posts_nothing_and_never_raises() {
assert!(s.focus_editbox("ChatFrameEditBox"));
s.set_modifiers(true, false, false);
- s.run("BenillaPaperDollSlot_OnClick(BenillaCharacterHeadSlot, \"LeftButton\")")
+ s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
.unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
@@ -312,7 +306,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(BenillaCharacterHeadSlot, \"LeftButton\")")
+ s.run("BenillaPaperDollSlot_OnClick(CharacterHeadSlot, \"LeftButton\")")
.unwrap();
s.set_modifiers(false, false, false);
assert!(
@@ -330,7 +324,7 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_room(&s);
- s.run("ShowUIPanel(BenillaDressUpFrame)").unwrap();
+ s.run("ShowUIPanel(DressUpFrame)").unwrap();
let _ = s.take_sounds();
let _ = s.take_dressup_intents();
s.resolve();
@@ -341,7 +335,7 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
"ref UIParent.lua:1422"
);
- s.run("BenillaDressUpFrameResetButton:Click()").unwrap();
+ s.run("DressUpFrameResetButton:Click()").unwrap();
assert_eq!(s.take_dressup_intents(), vec![DressUpIntent::Dress]);
assert_eq!(
s.take_sounds(),
@@ -353,10 +347,10 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
let before = s.dressup_yaw();
let (x, y) = s
.eval::<(f32, f32)>(
- "return (BenillaDressUpModelFrameRotateLeftButton:GetLeft() \
- + BenillaDressUpModelFrameRotateLeftButton:GetRight()) / 2, \
- (BenillaDressUpModelFrameRotateLeftButton:GetTop() \
- + BenillaDressUpModelFrameRotateLeftButton:GetBottom()) / 2",
+ "return (DressUpModelFrameRotateLeftButton:GetLeft() \
+ + DressUpModelFrameRotateLeftButton:GetRight()) / 2, \
+ (DressUpModelFrameRotateLeftButton:GetTop() \
+ + DressUpModelFrameRotateLeftButton:GetBottom()) / 2",
)
.unwrap();
s.mouse_button(x, y, "LeftButton", true);
@@ -368,7 +362,7 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
s.dressup_yaw()
);
- s.run("HideUIPanel(BenillaDressUpFrame)").unwrap();
+ s.run("HideUIPanel(DressUpFrame)").unwrap();
assert_eq!(
s.take_dressup_intents(),
vec![DressUpIntent::Close],
diff --git a/crates/benilla-app/src/ui_script/durability_tests.rs b/crates/benilla-app/src/ui_script/durability_tests.rs
index 8adddc97..f38d6c7f 100644
--- a/crates/benilla-app/src/ui_script/durability_tests.rs
+++ b/crates/benilla-app/src/ui_script/durability_tests.rs
@@ -404,7 +404,7 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
s.resolve();
let dur_bottom: f32 = s.eval("return DurabilityFrame:GetBottom()").unwrap();
- let watch_top: f32 = s.eval("return BenillaQuestWatchFrame:GetTop()").unwrap();
+ let watch_top: f32 = s.eval("return QuestWatchFrame:GetTop()").unwrap();
assert!(
(watch_top - dur_bottom).abs() <= 0.5,
"tracker top {watch_top} must sit flush under the durability bottom {dur_bottom}"
diff --git a/crates/benilla-app/src/ui_script/errors_tests.rs b/crates/benilla-app/src/ui_script/errors_tests.rs
index cf99c280..0e57abdf 100644
--- a/crates/benilla-app/src/ui_script/errors_tests.rs
+++ b/crates/benilla-app/src/ui_script/errors_tests.rs
@@ -30,9 +30,7 @@ fn info_and_error_messages_stack_hold_and_expire() {
load_xml(&s, "ErrorsFrame.xml");
// Empty at load.
- assert!(!s
- .eval::("return BenillaErrorsFrame:IsVisible()")
- .unwrap());
+ assert!(!s.eval::("return UIErrorsFrame:IsVisible()").unwrap());
// A quest progress toast: yellow (ref UIErrorsFrame.lua:12), on the top line.
s.fire_event(
@@ -40,17 +38,15 @@ fn info_and_error_messages_stack_hold_and_expire() {
vec![ScriptValue::Str("Tough Wolf Meat: 2/8".into())],
);
assert!(s.errors().is_empty(), "info errors: {:?}", s.errors());
- assert!(s
- .eval::("return BenillaErrorsFrame:IsVisible()")
- .unwrap());
+ assert!(s.eval::("return UIErrorsFrame:IsVisible()").unwrap());
assert_eq!(
- s.eval::("return BenillaErrorsFrameLine1:GetText()")
+ s.eval::("return UIErrorsFrameLine1:GetText()")
.unwrap(),
"Tough Wolf Meat: 2/8"
);
assert!(s
.eval::(
- "local m = BenillaErrorsFrame.messages[1] return m.r == 1.0 and m.g == 1.0 and m.b == 0.0"
+ "local m = UIErrorsFrame.messages[1] return m.r == 1.0 and m.g == 1.0 and m.b == 0.0"
)
.unwrap());
@@ -61,35 +57,31 @@ fn info_and_error_messages_stack_hold_and_expire() {
vec![ScriptValue::Str("You are too far away!".into())],
);
assert_eq!(
- s.eval::("return BenillaErrorsFrameLine1:GetText()")
+ s.eval::("return UIErrorsFrameLine1:GetText()")
.unwrap(),
"You are too far away!"
);
assert_eq!(
- s.eval::("return BenillaErrorsFrameLine2:GetText()")
+ s.eval::("return UIErrorsFrameLine2:GetText()")
.unwrap(),
"Tough Wolf Meat: 2/8"
);
assert!(s
.eval::(
- "local m = BenillaErrorsFrame.messages[1] return m.r == 1.0 and m.g == 0.1 and m.b == 0.1"
+ "local m = UIErrorsFrame.messages[1] return m.r == 1.0 and m.g == 0.1 and m.b == 0.1"
)
.unwrap());
// Hold 5 s (ref displayDuration), then the fade window, then gone — the frame hides once the
// last message expires.
s.tick(4.0);
- assert!(s
- .eval::("return BenillaErrorsFrame:IsVisible()")
- .unwrap());
+ assert!(s.eval::("return UIErrorsFrame:IsVisible()").unwrap());
s.tick(2.5); // both past 5 s hold + 1 s fade
- assert!(!s
- .eval::("return BenillaErrorsFrame:IsVisible()")
- .unwrap());
+ assert!(!s.eval::("return UIErrorsFrame:IsVisible()").unwrap());
assert!(s.errors().is_empty(), "expiry errors: {:?}", s.errors());
}
-/// **The toast must outrank an open panel window.** `BenillaErrorsFrame` is 512x60 at TOP (0,-122)
+/// **The toast must outrank an open panel window.** `UIErrorsFrame` is 512x60 at TOP (0,-122)
/// and every left-slot panel is 384-wide at TOPLEFT (0,-104), so they overlap. The reference puts
/// this frame in HIGH (`UIErrorsFrame.xml` l.4) and benilla had dropped it, leaving it in the
/// default MEDIUM alongside the panels.
@@ -111,18 +103,14 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, "QuestLogFrame.xml");
// A left-slot panel open, and the toast raised after it — the order that must not decide.
- s.eval::<()>("ShowUIPanel(BenillaQuestLogFrame)").unwrap();
+ s.eval::<()>("ShowUIPanel(QuestLogFrame)").unwrap();
s.fire_event(
"UI_ERROR_MESSAGE",
vec![ScriptValue::Str("Out of range.".into())],
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
- assert!(s
- .eval::("return BenillaErrorsFrame:IsVisible()")
- .unwrap());
- assert!(s
- .eval::("return BenillaQuestLogFrame:IsVisible()")
- .unwrap());
+ assert!(s.eval::