benilla's layer speaks the 1.12 API alone: handlers take this and no arguments, GetBinding answers the 1.12 list, the Key Bindings page captures through its window, /errors chains the error handler, and the host hooks leave the VM (#569)
benilla's layer now uses only the 1.12 API and the 1.12 Lua forms, and the host hooks it used are gone from the VM. - **The VM calls handlers as 1.12.1 does.** A handler body is compiled as the chunk itself, so `self` in it is an ordinary global (`0x704c70`). A handler is called with no arguments (`0x705199`). The loader reads no `function=` attribute. - **`GetBinding`/`GetNumBindings` answer the 1.12 list** (`0x4b7f60`): `HEADER_*` section rows, plus every key bound to a command, with no category column. The stock `KeyBindingFrame` now shows its section headers and both key columns. - **The Key Bindings page captures keys through its own window**, as `Blizzard_BindingUI` does: the frame's `OnKeyDown` for keys, clicks for mouse buttons, and the wheel. It takes the `Benilla` prefix, so loading the stock binding addon no longer breaks it. The UI pointer feed now hands frames mouse buttons 4 and 5. - **`/errors` keeps its log by chaining the Lua error handler**, and the stock ScriptErrors dialog is unchanged. Load failures that never raise now print one chat line each at world entry. - **Slash commands:** `/reload` and `/errors` are `SlashCmdList` rows in the layer. `/convertraid` is gone; 1.12.1 has no such command. The dev commands are host rows in dev builds only, so a player build answers them with the unknown-command line once. - **Naming:** every global the layer defines is `Benilla`-prefixed. `BenillaBindCapture`, `KeyBindings_OnHostKey`, the three script-error globals and `SubmitChatInput` are removed. Verified: - New tests for each rule and each removed surface; 16 mutations each fail a test. - Region dumps of the Options window (general page, Key Bindings, armed) are identical before and after. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run bound a key, a Shift chord and the middle button through the page, and opened `/errors`. Built on macOS; no platform seam touched. Fixes #539 Fixes #544 Fixes #407 Part of #171 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0f36374445
commit
05e204de10
@@ -10,6 +10,8 @@
|
||||
-- The stock body, captured before it is shadowed; nil when no client data was sourced.
|
||||
local refItemButtonOnEnter = ContainerFrameItemButton_OnEnter
|
||||
|
||||
-- The stock name, redefined: the stock item buttons call it, and our keyring slot needs
|
||||
-- the bag-slot tooltip.
|
||||
function ContainerFrameItemButton_OnEnter(button)
|
||||
if not button then
|
||||
button = this
|
||||
@@ -72,7 +74,8 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- After the stock ContainerFrame.lua has loaded, so its callers reach these.
|
||||
-- The stock names, redefined after the stock ContainerFrame.lua has loaded, so its callers
|
||||
-- (the vendor, bank, mail and trade windows) reach these.
|
||||
OpenBackpack = BenillaOpenBackpack
|
||||
CloseBackpack = BenillaCloseBackpack
|
||||
CloseAllBags = BenillaCloseAllBags
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ERA_WINDOW_SCALE (OptionsFrame.xml) stands in for the era's UIParent scale; read at
|
||||
-- every show, then the stock OnShow.
|
||||
-- BENILLA_ERA_WINDOW_SCALE (OptionsFrame.xml) stands in for the era's UIParent scale;
|
||||
-- read at every show, then the stock OnShow.
|
||||
local Ref_GameMenuFrame_OnShow = GameMenuFrame:GetScript("OnShow")
|
||||
GameMenuFrame:SetScript("OnShow", function()
|
||||
this:SetScale(ERA_WINDOW_SCALE or 1)
|
||||
this:SetScale(BENILLA_ERA_WINDOW_SCALE or 1)
|
||||
Ref_GameMenuFrame_OnShow()
|
||||
end)
|
||||
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
OptionsFrame.xml, which loads after. Only commands the engine dispatches appear.
|
||||
Deviation: the layout is the era Settings panel's Keybindings page (collapsible category
|
||||
sections), because 1.12's settings screens are much worse to use. Capture follows 1.12's
|
||||
Blizzard_BindingUI: the next key or mouse 3/4/5 press binds, left and right clicks only
|
||||
select, and ESC while armed binds Escape (its OnKeyDown has no ESCAPE filter).
|
||||
Blizzard_BindingUI: while a capsule is selected the window is keyboard-enabled, so the
|
||||
next key binds through its OnKeyDown, and a mouse 3/4/5 click or a wheel notch binds;
|
||||
left and right clicks only select, and ESC while armed binds Escape (its OnKeyDown has no
|
||||
ESCAPE filter).
|
||||
Deviation: every bind, unbind and reset saves at once, as the era panel commits, where
|
||||
1.12 had Okay and Cancel; only Reset To Default and leaving the character set confirm.
|
||||
-->
|
||||
|
||||
<!-- 1.12's KeyBindingFrameBindingButtonTemplate; all five buttons reach the capture. -->
|
||||
<Button name="KeyBindingButtonTemplate" virtual="true">
|
||||
<!-- 1.12's KeyBindingFrameBindingButtonTemplate; all five buttons reach the handler. -->
|
||||
<Button name="BenillaKeyBindingButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="180" y="22"/></Size>
|
||||
<ButtonText name="$parentText"/>
|
||||
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
|
||||
@@ -22,13 +24,13 @@
|
||||
<DisabledFont inherits="GameFontDisableSmall"/>
|
||||
<Scripts>
|
||||
<OnLoad>this:RegisterForClicks("LeftButtonUp", "RightButtonUp", "MiddleButtonUp", "Button4Up", "Button5Up")</OnLoad>
|
||||
<OnClick>KeyBindingButton_OnClick(arg1)</OnClick>
|
||||
<OnClick>BenillaKeyBindingButton_OnClick(arg1)</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- A binding row or a section header in 1.12's QuestLogTitleButton dress, since the era's
|
||||
expand arrows are not in 1.12; 25 tall, the era's row height. -->
|
||||
<Frame name="KeyBindingRowTemplate" virtual="true">
|
||||
<Frame name="BenillaKeyBindingRowTemplate" virtual="true">
|
||||
<Size><AbsDimension x="560" y="25"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
@@ -61,13 +63,13 @@
|
||||
<NormalFont inherits="GameFontNormal"/>
|
||||
<HighlightFont inherits="GameFontHighlight"/>
|
||||
<Scripts>
|
||||
<OnClick>KeyBindings_ToggleSection(this.sectionIndex)</OnClick>
|
||||
<OnClick>BenillaKeyBindings_ToggleSection(this.sectionIndex)</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentKey1Button" inherits="KeyBindingButtonTemplate" id="1">
|
||||
<Button name="$parentKey1Button" inherits="BenillaKeyBindingButtonTemplate" id="1">
|
||||
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="175" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="$parentKey2Button" inherits="KeyBindingButtonTemplate" id="2">
|
||||
<Button name="$parentKey2Button" inherits="BenillaKeyBindingButtonTemplate" id="2">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="$parentKey1Button" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
@@ -75,7 +77,7 @@
|
||||
|
||||
<Script><![CDATA[
|
||||
-- ── Strings ── a 1.12 GlobalStrings global, or the fallback for a harness without them.
|
||||
function KeyBindings_String(name, fallback)
|
||||
function BenillaKeyBindings_String(name, fallback)
|
||||
local s = getglobal(name)
|
||||
if s then return s end
|
||||
return fallback
|
||||
@@ -83,9 +85,9 @@
|
||||
|
||||
-- ── Key display ── 1.12's GetBindingText in full form: modifier prefixes stay tokens, the
|
||||
-- base key reads its KEY_* global. "CTRL--" is CTRL plus the minus key.
|
||||
function KeyBindings_KeyText(key)
|
||||
function BenillaKeyBindings_KeyText(key)
|
||||
if not key then
|
||||
return NORMAL_FONT_COLOR_CODE .. KeyBindings_String("NOT_BOUND", "Not Bound") .. FONT_COLOR_CODE_CLOSE
|
||||
return NORMAL_FONT_COLOR_CODE .. BenillaKeyBindings_String("NOT_BOUND", "Not Bound") .. FONT_COLOR_CODE_CLOSE
|
||||
end
|
||||
local prefix = ""
|
||||
local rest = key
|
||||
@@ -104,43 +106,55 @@
|
||||
break
|
||||
end
|
||||
end
|
||||
return prefix .. KeyBindings_String("KEY_" .. rest, rest)
|
||||
return prefix .. BenillaKeyBindings_String("KEY_" .. rest, rest)
|
||||
end
|
||||
|
||||
-- ── Page state ── `searchPainted` maps OptionsFrame.xml's search rows to binding indices.
|
||||
KeyBindingsPage = { sections = {}, items = {} }
|
||||
BenillaKeyBindingsPage = { sections = {}, items = {} }
|
||||
|
||||
KEYBINDINGS_ROW_BASE = "BenillaOptionsFrameContainerBodyKeybindingsRow"
|
||||
KEYBINDINGS_DISPLAYED = 19
|
||||
KEYBINDINGS_ROW_HEIGHT = 25
|
||||
KEYBINDINGS_SEARCH_ROWS = 8
|
||||
BENILLA_KEYBINDINGS_ROW_BASE = "BenillaOptionsFrameContainerBodyKeybindingsRow"
|
||||
BENILLA_KEYBINDINGS_DISPLAYED = 19
|
||||
BENILLA_KEYBINDINGS_ROW_HEIGHT = 25
|
||||
BENILLA_KEYBINDINGS_SEARCH_ROWS = 8
|
||||
|
||||
-- ── Section model ── one section per category in Bindings.xml order, collapsed at first
|
||||
-- as in the era; a rebuild keeps each state.
|
||||
function KeyBindings_BuildSections()
|
||||
local page = KeyBindingsPage
|
||||
-- ── Section model ── one section per `HEADER_*` row of the binding list, in its
|
||||
-- order (Blizzard_BindingUI.lua:84-92), collapsed at first as in the era; a rebuild keeps
|
||||
-- each state. `token` is the header's GlobalStrings key, `fallback` its bare name.
|
||||
-- Deviation: the spacer headers BLANK, BLANK2 and BLANK3 over multibars 2-4
|
||||
-- (Bindings.xml:883/967/1051), which 1.12 draws blank (BINDING_HEADER_BLANK is " ", the
|
||||
-- other two are not defined), continue the section above, because this page's sections
|
||||
-- are named and collapsible.
|
||||
function BenillaKeyBindings_IsHeader(command)
|
||||
return strsub(command, 1, 6) == "HEADER"
|
||||
end
|
||||
|
||||
function BenillaKeyBindings_BuildSections()
|
||||
local page = BenillaKeyBindingsPage
|
||||
local was = {}
|
||||
for _, s in ipairs(page.sections) do
|
||||
was[s.token] = s.expanded
|
||||
end
|
||||
page.sections = {}
|
||||
local byToken = {}
|
||||
local sec = nil
|
||||
for i = 1, GetNumBindings() do
|
||||
local _, category = GetBinding(i)
|
||||
local sec = byToken[category]
|
||||
if not sec then
|
||||
sec = { token = category, indices = {}, expanded = was[category] }
|
||||
byToken[category] = sec
|
||||
table.insert(page.sections, sec)
|
||||
local command = GetBinding(i)
|
||||
if BenillaKeyBindings_IsHeader(command) then
|
||||
local token = "BINDING_" .. command
|
||||
if not (sec and strfind(command, "^HEADER_BLANK%d*$")) then
|
||||
sec = { token = token, fallback = strsub(command, 8), indices = {},
|
||||
expanded = was[token] }
|
||||
table.insert(page.sections, sec)
|
||||
end
|
||||
elseif sec then
|
||||
table.insert(sec.indices, i)
|
||||
end
|
||||
table.insert(sec.indices, i)
|
||||
end
|
||||
KeyBindings_Flatten()
|
||||
BenillaKeyBindings_Flatten()
|
||||
end
|
||||
|
||||
-- The visible list the faux scroll runs over.
|
||||
function KeyBindings_Flatten()
|
||||
local page = KeyBindingsPage
|
||||
function BenillaKeyBindings_Flatten()
|
||||
local page = BenillaKeyBindingsPage
|
||||
page.items = {}
|
||||
for n, sec in ipairs(page.sections) do
|
||||
table.insert(page.items, { header = n })
|
||||
@@ -153,38 +167,38 @@
|
||||
end
|
||||
|
||||
-- A header click, voiced with the checkbox sound.
|
||||
function KeyBindings_ToggleSection(n)
|
||||
local sec = KeyBindingsPage.sections[n]
|
||||
function BenillaKeyBindings_ToggleSection(n)
|
||||
local sec = BenillaKeyBindingsPage.sections[n]
|
||||
if not sec then return end
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
sec.expanded = not sec.expanded
|
||||
KeyBindings_Flatten()
|
||||
KeyBindingsPage_Update()
|
||||
BenillaKeyBindings_Flatten()
|
||||
BenillaKeyBindingsPage_Update()
|
||||
end
|
||||
|
||||
-- Silent, for the capture fixture.
|
||||
function KeyBindings_ExpandSection(n, on)
|
||||
local sec = KeyBindingsPage.sections[n]
|
||||
function BenillaKeyBindings_ExpandSection(n, on)
|
||||
local sec = BenillaKeyBindingsPage.sections[n]
|
||||
if not sec then return end
|
||||
sec.expanded = on and true or false
|
||||
KeyBindings_Flatten()
|
||||
BenillaKeyBindings_Flatten()
|
||||
end
|
||||
|
||||
-- ── Painters ── shared by the list and the search rows, which capture in place.
|
||||
function KeyBindings_PaintBindRow(name, index)
|
||||
local command, _, key1, key2 = GetBinding(index)
|
||||
function BenillaKeyBindings_PaintBindRow(name, index)
|
||||
local command, key1, key2 = GetBinding(index)
|
||||
local b1 = getglobal(name .. "Key1Button")
|
||||
local b2 = getglobal(name .. "Key2Button")
|
||||
b1.commandName = command
|
||||
b2.commandName = command
|
||||
b1:SetText(KeyBindings_KeyText(key1))
|
||||
b2:SetText(KeyBindings_KeyText(key2))
|
||||
b1:SetText(BenillaKeyBindings_KeyText(key1))
|
||||
b2:SetText(BenillaKeyBindings_KeyText(key2))
|
||||
if key1 then b1:SetAlpha(1) else b1:SetAlpha(0.8) end
|
||||
if key2 then b2:SetAlpha(1) else b2:SetAlpha(0.8) end
|
||||
local desc = getglobal(name .. "Description")
|
||||
desc:SetText(KeyBindings_String("BINDING_NAME_" .. command, command))
|
||||
desc:SetText(BenillaKeyBindings_String("BINDING_NAME_" .. command, command))
|
||||
desc:Show()
|
||||
local page = KeyBindingsPage
|
||||
local page = BenillaKeyBindingsPage
|
||||
if page.selected == command and page.keyID == 1 then
|
||||
b1:LockHighlight()
|
||||
else
|
||||
@@ -200,11 +214,11 @@
|
||||
getglobal(name .. "Header"):Hide()
|
||||
end
|
||||
|
||||
function KeyBindings_PaintHeaderRow(name, n)
|
||||
local sec = KeyBindingsPage.sections[n]
|
||||
function BenillaKeyBindings_PaintHeaderRow(name, n)
|
||||
local sec = BenillaKeyBindingsPage.sections[n]
|
||||
local header = getglobal(name .. "Header")
|
||||
header.sectionIndex = n
|
||||
header:SetText(KeyBindings_String(sec.token, sec.token))
|
||||
header:SetText(BenillaKeyBindings_String(sec.token, sec.fallback))
|
||||
local icon = getglobal(name .. "HeaderIcon")
|
||||
if sec.expanded then
|
||||
icon:SetTexture("Interface\\Buttons\\UI-MinusButton-Up")
|
||||
@@ -218,34 +232,34 @@
|
||||
end
|
||||
|
||||
-- ── The list repaint (1.12 KeyBindingFrame_Update) ──
|
||||
function KeyBindingsPage_Update()
|
||||
local page = KeyBindingsPage
|
||||
function BenillaKeyBindingsPage_Update()
|
||||
local page = BenillaKeyBindingsPage
|
||||
local scroll = getglobal("BenillaOptionsFrameContainerBodyKeybindingsScrollFrame")
|
||||
local offset = FauxScrollFrame_GetOffset(scroll)
|
||||
for i = 1, KEYBINDINGS_DISPLAYED do
|
||||
local name = KEYBINDINGS_ROW_BASE .. i
|
||||
for i = 1, BENILLA_KEYBINDINGS_DISPLAYED do
|
||||
local name = BENILLA_KEYBINDINGS_ROW_BASE .. i
|
||||
local row = getglobal(name)
|
||||
local item = page.items[offset + i]
|
||||
if item and item.header then
|
||||
KeyBindings_PaintHeaderRow(name, item.header)
|
||||
BenillaKeyBindings_PaintHeaderRow(name, item.header)
|
||||
row:Show()
|
||||
elseif item then
|
||||
KeyBindings_PaintBindRow(name, item.bind)
|
||||
BenillaKeyBindings_PaintBindRow(name, item.bind)
|
||||
row:Show()
|
||||
else
|
||||
row:Hide()
|
||||
end
|
||||
end
|
||||
FauxScrollFrame_Update(scroll, table.getn(page.items),
|
||||
KEYBINDINGS_DISPLAYED, KEYBINDINGS_ROW_HEIGHT)
|
||||
BENILLA_KEYBINDINGS_DISPLAYED, BENILLA_KEYBINDINGS_ROW_HEIGHT)
|
||||
-- Our trough follows the bar `FauxScrollFrame_Update` has just shown or hidden.
|
||||
BenillaScrollTrough_Follow(scroll:GetName())
|
||||
KeyBindings_UpdateUnbindKey()
|
||||
BenillaKeyBindings_UpdateUnbindKey()
|
||||
end
|
||||
|
||||
-- The page entry: a rebind or a set switch may have changed the table.
|
||||
function KeyBindingsPage_Refresh()
|
||||
KeyBindings_BuildSections()
|
||||
function BenillaKeyBindingsPage_Refresh()
|
||||
BenillaKeyBindings_BuildSections()
|
||||
local box = getglobal("BenillaOptionsFrameContainerBodyKeybindingsCharacterRowCheck")
|
||||
if GetCurrentBindingSet() == 2 then
|
||||
box:SetChecked(1)
|
||||
@@ -253,23 +267,28 @@
|
||||
box:SetChecked(nil)
|
||||
end
|
||||
getglobal("BenillaOptionsFrameContainerBodyKeybindingsScrollFrameScrollBar"):SetValue(0)
|
||||
KeyBindingsPage_Update()
|
||||
BenillaKeyBindingsPage_Update()
|
||||
end
|
||||
|
||||
function KeyBindings_Output(text)
|
||||
function BenillaKeyBindings_Output(text)
|
||||
getglobal("BenillaOptionsFrameContainerBodyKeybindingsOutput"):SetText(text)
|
||||
end
|
||||
|
||||
-- ── Selection ── arms the host's capture, which hands its chord to KeyBindings_OnHostKey.
|
||||
function KeyBindings_SetSelected(command, keyID)
|
||||
KeyBindingsPage.selected = command
|
||||
KeyBindingsPage.keyID = keyID
|
||||
BenillaBindCapture(command ~= nil)
|
||||
-- ── Selection ── while a capsule is selected the window takes every key and the
|
||||
-- wheel, as 1.12's keyboard-enabled KeyBindingFrame does (Blizzard_BindingUI.xml:73,
|
||||
-- :541-543); unarmed it takes neither, so keys and the wheel reach the game.
|
||||
function BenillaKeyBindings_SetSelected(command, keyID)
|
||||
BenillaKeyBindingsPage.selected = command
|
||||
BenillaKeyBindingsPage.keyID = keyID
|
||||
if BenillaOptionsFrame then
|
||||
BenillaOptionsFrame:EnableKeyboard(command ~= nil)
|
||||
BenillaOptionsFrame:EnableMouseWheel(command ~= nil)
|
||||
end
|
||||
end
|
||||
|
||||
function KeyBindings_UpdateUnbindKey()
|
||||
function BenillaKeyBindings_UpdateUnbindKey()
|
||||
local unbind = getglobal("BenillaOptionsFrameContainerUnbind")
|
||||
if KeyBindingsPage.selected then
|
||||
if BenillaKeyBindingsPage.selected then
|
||||
unbind:Enable()
|
||||
else
|
||||
unbind:Disable()
|
||||
@@ -277,94 +296,134 @@
|
||||
end
|
||||
|
||||
-- Every search row: a bind from one can unbind a victim shown on another.
|
||||
function KeyBindings_RepaintVisible()
|
||||
if KeyBindingsPage.searchPainted then
|
||||
for name, index in pairs(KeyBindingsPage.searchPainted) do
|
||||
KeyBindings_PaintBindRow(name, index)
|
||||
function BenillaKeyBindings_RepaintVisible()
|
||||
if BenillaKeyBindingsPage.searchPainted then
|
||||
for name, index in pairs(BenillaKeyBindingsPage.searchPainted) do
|
||||
BenillaKeyBindings_PaintBindRow(name, index)
|
||||
end
|
||||
KeyBindings_UpdateUnbindKey()
|
||||
BenillaKeyBindings_UpdateUnbindKey()
|
||||
else
|
||||
KeyBindingsPage_Update()
|
||||
BenillaKeyBindingsPage_Update()
|
||||
end
|
||||
end
|
||||
|
||||
function KeyBindingButton_OnClick(button)
|
||||
-- As 1.12: while armed, a middle, 4 or 5 press is a bind the capture takes after this.
|
||||
local page = KeyBindingsPage
|
||||
-- 1.12's KeyBindingButton_OnClick (Blizzard_BindingUI.lua:228-258): while armed, a middle,
|
||||
-- 4 or 5 click binds that button.
|
||||
function BenillaKeyBindingButton_OnClick(button)
|
||||
local page = BenillaKeyBindingsPage
|
||||
if page.selected then
|
||||
if button ~= "LeftButton" and button ~= "RightButton" then
|
||||
BenillaKeyBindings_OnKeyDown(button)
|
||||
return
|
||||
end
|
||||
if page.selected == this.commandName and page.keyID == this:GetID() then
|
||||
KeyBindings_SetSelected(nil)
|
||||
KeyBindings_Output("")
|
||||
BenillaKeyBindings_SetSelected(nil)
|
||||
BenillaKeyBindings_Output("")
|
||||
else
|
||||
KeyBindings_SetSelected(this.commandName, this:GetID())
|
||||
KeyBindings_Output(string.format(
|
||||
KeyBindings_String("BIND_KEY_TO_COMMAND", "Press Key to Bind to Command -> %s"),
|
||||
KeyBindings_String("BINDING_NAME_" .. this.commandName, this.commandName)))
|
||||
BenillaKeyBindings_SetSelected(this.commandName, this:GetID())
|
||||
BenillaKeyBindings_Output(string.format(
|
||||
BenillaKeyBindings_String("BIND_KEY_TO_COMMAND", "Press Key to Bind to Command -> %s"),
|
||||
BenillaKeyBindings_String("BINDING_NAME_" .. this.commandName, this.commandName)))
|
||||
end
|
||||
else
|
||||
KeyBindings_SetSelected(this.commandName, this:GetID())
|
||||
KeyBindings_Output(string.format(
|
||||
KeyBindings_String("BIND_KEY_TO_COMMAND", "Press Key to Bind to Command -> %s"),
|
||||
KeyBindings_String("BINDING_NAME_" .. this.commandName, this.commandName)))
|
||||
BenillaKeyBindings_SetSelected(this.commandName, this:GetID())
|
||||
BenillaKeyBindings_Output(string.format(
|
||||
BenillaKeyBindings_String("BIND_KEY_TO_COMMAND", "Press Key to Bind to Command -> %s"),
|
||||
BenillaKeyBindings_String("BINDING_NAME_" .. this.commandName, this.commandName)))
|
||||
end
|
||||
KeyBindings_RepaintVisible()
|
||||
BenillaKeyBindings_RepaintVisible()
|
||||
end
|
||||
|
||||
function KeyBindings_Commit()
|
||||
function BenillaKeyBindings_Commit()
|
||||
SaveBindings(GetCurrentBindingSet())
|
||||
end
|
||||
|
||||
-- `chord` is ALT-CTRL-SHIFT-TOKEN; 1.12's OnKeyDown armed branch, then the live commit.
|
||||
function KeyBindings_OnHostKey(chord)
|
||||
local page = KeyBindingsPage
|
||||
-- 1.12's KeyBindingFrame_OnKeyDown armed branch (Blizzard_BindingUI.lua:140-212), then
|
||||
-- the live commit: `button` is a mouse button's or the wheel's name, else the key is
|
||||
-- `arg1`, the window's OnKeyDown argument. Unarmed it does nothing: the window takes keys
|
||||
-- only while armed, so 1.12's unarmed ESCAPE arm never runs here.
|
||||
function BenillaKeyBindings_OnKeyDown(button)
|
||||
local page = BenillaKeyBindingsPage
|
||||
if not page.selected then
|
||||
return
|
||||
end
|
||||
if button == "LeftButton" then
|
||||
button = "BUTTON1"
|
||||
elseif button == "RightButton" then
|
||||
button = "BUTTON2"
|
||||
elseif button == "MiddleButton" then
|
||||
button = "BUTTON3"
|
||||
elseif button == "Button4" then
|
||||
button = "BUTTON4"
|
||||
elseif button == "Button5" then
|
||||
button = "BUTTON5"
|
||||
end
|
||||
local chord
|
||||
if button then
|
||||
if button == "BUTTON1" or button == "BUTTON2" then
|
||||
return
|
||||
end
|
||||
chord = button
|
||||
else
|
||||
chord = arg1
|
||||
end
|
||||
if chord == "UNKNOWN" or chord == "SHIFT" or chord == "CTRL" or chord == "ALT" then
|
||||
return
|
||||
end
|
||||
if IsShiftKeyDown() then chord = "SHIFT-" .. chord end
|
||||
if IsControlKeyDown() then chord = "CTRL-" .. chord end
|
||||
if IsAltKeyDown() then chord = "ALT-" .. chord end
|
||||
local selected = page.selected
|
||||
local oldAction = GetBindingAction(chord)
|
||||
if oldAction ~= "" and oldAction ~= selected then
|
||||
local k1, k2 = GetBindingKey(oldAction)
|
||||
if (not k1 or k1 == chord) and (not k2 or k2 == chord) then
|
||||
KeyBindings_Output(string.format(
|
||||
KeyBindings_String("KEY_UNBOUND_ERROR", "|cffff0000%s Function is Now Unbound!|r"),
|
||||
KeyBindings_String("BINDING_NAME_" .. oldAction, oldAction)))
|
||||
BenillaKeyBindings_Output(string.format(
|
||||
BenillaKeyBindings_String("KEY_UNBOUND_ERROR", "|cffff0000%s Function is Now Unbound!|r"),
|
||||
BenillaKeyBindings_String("BINDING_NAME_" .. oldAction, oldAction)))
|
||||
else
|
||||
KeyBindings_Output(KeyBindings_String("KEY_BOUND", "Key Bound Successfully"))
|
||||
BenillaKeyBindings_Output(BenillaKeyBindings_String("KEY_BOUND", "Key Bound Successfully"))
|
||||
end
|
||||
else
|
||||
KeyBindings_Output(KeyBindings_String("KEY_BOUND", "Key Bound Successfully"))
|
||||
BenillaKeyBindings_Output(BenillaKeyBindings_String("KEY_BOUND", "Key Bound Successfully"))
|
||||
end
|
||||
local key1, key2 = GetBindingKey(selected)
|
||||
if key1 then SetBinding(key1) end
|
||||
if key2 then SetBinding(key2) end
|
||||
if page.keyID == 1 then
|
||||
KeyBindings_SetOrRestore(chord, selected, key1)
|
||||
BenillaKeyBindings_SetOrRestore(chord, selected, key1)
|
||||
if key2 then SetBinding(key2, selected) end
|
||||
else
|
||||
if key1 then SetBinding(key1, selected) end
|
||||
KeyBindings_SetOrRestore(chord, selected, key2)
|
||||
BenillaKeyBindings_SetOrRestore(chord, selected, key2)
|
||||
end
|
||||
KeyBindings_Commit()
|
||||
KeyBindings_SetSelected(nil)
|
||||
KeyBindings_RepaintVisible()
|
||||
BenillaKeyBindings_Commit()
|
||||
BenillaKeyBindings_SetSelected(nil)
|
||||
BenillaKeyBindings_RepaintVisible()
|
||||
end
|
||||
|
||||
-- 1.12's KeyBindingFrame_SetBinding, message and all; SetBinding refuses only a non-key.
|
||||
function KeyBindings_SetOrRestore(key, command, oldKey)
|
||||
function BenillaKeyBindings_SetOrRestore(key, command, oldKey)
|
||||
if not SetBinding(key, command) then
|
||||
if oldKey then SetBinding(oldKey, command) end
|
||||
KeyBindings_Output(KeyBindings_String(
|
||||
BenillaKeyBindings_Output(BenillaKeyBindings_String(
|
||||
"KEYBINDINGFRAME_MOUSEWHEEL_ERROR",
|
||||
"Can't bind mousewheel to actions with up and down states"))
|
||||
end
|
||||
end
|
||||
|
||||
-- The list's OnMouseWheel while armed (Blizzard_BindingUI.xml:377-387).
|
||||
function BenillaKeyBindings_OnMouseWheel(delta)
|
||||
if delta > 0 then
|
||||
BenillaKeyBindings_OnKeyDown("MOUSEWHEELUP")
|
||||
else
|
||||
BenillaKeyBindings_OnKeyDown("MOUSEWHEELDOWN")
|
||||
end
|
||||
end
|
||||
|
||||
-- 1.12's Unbind Key body, then the live commit.
|
||||
function KeyBindings_Unbind()
|
||||
local page = KeyBindingsPage
|
||||
function BenillaKeyBindings_Unbind()
|
||||
local page = BenillaKeyBindingsPage
|
||||
local key1, key2 = GetBindingKey(page.selected)
|
||||
if key1 then SetBinding(key1) end
|
||||
if key2 then SetBinding(key2) end
|
||||
@@ -373,67 +432,70 @@
|
||||
else
|
||||
if key1 then SetBinding(key1, page.selected) end
|
||||
end
|
||||
KeyBindings_Commit()
|
||||
KeyBindings_SetSelected(nil)
|
||||
KeyBindings_Output("")
|
||||
KeyBindings_RepaintVisible()
|
||||
BenillaKeyBindings_Commit()
|
||||
BenillaKeyBindings_SetSelected(nil)
|
||||
BenillaKeyBindings_Output("")
|
||||
BenillaKeyBindings_RepaintVisible()
|
||||
end
|
||||
|
||||
-- ── Character-specific set ── checking saves set 2 at once (LoadBindings(2) falls back
|
||||
-- to the account set); unchecking confirms first, as the era does. The accept loads set 1
|
||||
-- before saving it, or the character binds would be written into the account set.
|
||||
function KeyBindings_CharacterToggle(box)
|
||||
function BenillaKeyBindings_CharacterToggle(box)
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
if box:GetChecked() then
|
||||
LoadBindings(2)
|
||||
SaveBindings(2)
|
||||
KeyBindings_SetSelected(nil)
|
||||
KeyBindings_Output("")
|
||||
KeyBindingsPage_Refresh()
|
||||
BenillaKeyBindings_SetSelected(nil)
|
||||
BenillaKeyBindings_Output("")
|
||||
BenillaKeyBindingsPage_Refresh()
|
||||
else
|
||||
box:SetChecked(1)
|
||||
StaticPopup_Show("CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS")
|
||||
StaticPopup_Show("BENILLA_CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS")
|
||||
end
|
||||
end
|
||||
|
||||
StaticPopupDialogs["CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS"] = {
|
||||
text = KeyBindings_String("CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS",
|
||||
-- Our key: Blizzard_BindingUI.lua:10 defines the stock one for its own window.
|
||||
StaticPopupDialogs["BENILLA_CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS"] = {
|
||||
text = BenillaKeyBindings_String("CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS",
|
||||
"Really switch to general key bindings? All key bindings specific to this character will be permanantly deleted."),
|
||||
button1 = KeyBindings_String("OKAY", "Okay"),
|
||||
button2 = KeyBindings_String("CANCEL", "Cancel"),
|
||||
button1 = BenillaKeyBindings_String("OKAY", "Okay"),
|
||||
button2 = BenillaKeyBindings_String("CANCEL", "Cancel"),
|
||||
OnAccept = function()
|
||||
LoadBindings(1)
|
||||
SaveBindings(1)
|
||||
KeyBindings_SetSelected(nil)
|
||||
KeyBindings_Output("")
|
||||
KeyBindingsPage_Refresh()
|
||||
BenillaKeyBindings_SetSelected(nil)
|
||||
BenillaKeyBindings_Output("")
|
||||
BenillaKeyBindingsPage_Refresh()
|
||||
end,
|
||||
}
|
||||
-- Reset To Default confirms, as the era's does; our string, since 1.12 has none.
|
||||
StaticPopupDialogs["CONFIRM_RESET_KEYBINDINGS"] = {
|
||||
StaticPopupDialogs["BENILLA_CONFIRM_RESET_KEYBINDINGS"] = {
|
||||
text = "Are you sure you want to reset all keybindings to their defaults?",
|
||||
button1 = KeyBindings_String("OKAY", "Okay"),
|
||||
button2 = KeyBindings_String("CANCEL", "Cancel"),
|
||||
button1 = BenillaKeyBindings_String("OKAY", "Okay"),
|
||||
button2 = BenillaKeyBindings_String("CANCEL", "Cancel"),
|
||||
OnAccept = function()
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
LoadBindings(0)
|
||||
KeyBindings_Commit()
|
||||
KeyBindings_SetSelected(nil)
|
||||
KeyBindings_Output("")
|
||||
KeyBindingsPage_Refresh()
|
||||
BenillaKeyBindings_Commit()
|
||||
BenillaKeyBindings_SetSelected(nil)
|
||||
BenillaKeyBindings_Output("")
|
||||
BenillaKeyBindingsPage_Refresh()
|
||||
end,
|
||||
}
|
||||
|
||||
-- ── Search ── a binding's tag is its display name uppercased, as the era's.
|
||||
-- A static pool of KEYBINDINGS_SEARCH_ROWS rows, where the era's results scroll.
|
||||
function KeyBindings_SearchMatches(words)
|
||||
-- A static pool of BENILLA_KEYBINDINGS_SEARCH_ROWS rows, where the era's results scroll.
|
||||
function BenillaKeyBindings_SearchMatches(words)
|
||||
local out = {}
|
||||
for i = 1, GetNumBindings() do
|
||||
local command = GetBinding(i)
|
||||
local tag = string.upper(KeyBindings_String("BINDING_NAME_" .. command, command))
|
||||
local s = OptionsSearch_Score(tag, words)
|
||||
if s then
|
||||
table.insert(out, { score = s, index = i })
|
||||
if not BenillaKeyBindings_IsHeader(command) then
|
||||
local tag = string.upper(BenillaKeyBindings_String("BINDING_NAME_" .. command, command))
|
||||
local s = BenillaOptionsSearch_Score(tag, words)
|
||||
if s then
|
||||
table.insert(out, { score = s, index = i })
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The script error log, which 1.12 does not have, so every name carries the `Benilla` prefix
|
||||
(`tests::reference_surface`); the stock `ScriptErrors` dialog shows one message, this keeps
|
||||
the log. Shaped like the reference's CraftFrame. It polls the engine:
|
||||
`BenillaGetNumScriptErrors()` is (retained, total-ever), `BenillaGetScriptErrorInfo(i)` is
|
||||
`seq, kind, message, count` (1-based, oldest first, nil past the end).
|
||||
The script error log, which 1.12 does not have, so every name carries the `Benilla` prefix;
|
||||
the stock `ScriptErrors` dialog shows one message, this keeps the log. Shaped like the
|
||||
reference's CraftFrame. It collects as a 1.12 error-log addon does: it wraps the handler
|
||||
`geterrorhandler` holds, the stock `_ERRORMESSAGE` (BasicControls.xml:16), which still runs
|
||||
after it. An addon that later calls `seterrorhandler` replaces the chain, as in 1.12.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- A pushable above every stock panel's, so a game window opening pushes the log aside.
|
||||
@@ -18,6 +18,39 @@
|
||||
-- A `seq`, not an index: indices shift when the cap evicts.
|
||||
BenillaScriptLogSelected = nil
|
||||
|
||||
-- Distinct messages kept before the oldest is evicted.
|
||||
BENILLA_SCRIPTLOG_CAP = 256
|
||||
-- Rows oldest first, each { seq, message, count }; `total` is the last `seq` handed out,
|
||||
-- kept apart from the row count so an eviction or a clear never renumbers.
|
||||
BenillaScriptLog = { rows = {}, total = 0 }
|
||||
|
||||
-- A repeat bumps its row's count and keeps its place.
|
||||
function BenillaScriptLog_Record(message)
|
||||
if type(message) ~= "string" then message = tostring(message) end
|
||||
local log = BenillaScriptLog
|
||||
local found = nil
|
||||
for i = 1, table.getn(log.rows) do
|
||||
if log.rows[i].message == message then found = log.rows[i] end
|
||||
end
|
||||
if found then
|
||||
found.count = found.count + 1
|
||||
else
|
||||
log.total = log.total + 1
|
||||
if table.getn(log.rows) >= BENILLA_SCRIPTLOG_CAP then table.remove(log.rows, 1) end
|
||||
table.insert(log.rows, { seq = log.total, message = message, count = 1 })
|
||||
end
|
||||
if BenillaScriptLogFrame and BenillaScriptLogFrame:IsVisible() then
|
||||
BenillaScriptLog_Update()
|
||||
end
|
||||
end
|
||||
|
||||
-- The chain: the log first, guarded so a fault in it cannot stop the stock handler.
|
||||
local benillaStockErrorHandler = geterrorhandler()
|
||||
seterrorhandler(function(message)
|
||||
pcall(BenillaScriptLog_Record, message)
|
||||
return benillaStockErrorHandler(message)
|
||||
end)
|
||||
|
||||
-- `strsub` counts bytes, so the cut backs up over UTF-8 continuation bytes (0x80..0xBF).
|
||||
function BenillaScriptLog_RowText(message)
|
||||
local cut = strfind(message, "\n", 1, 1)
|
||||
@@ -35,18 +68,10 @@
|
||||
return head
|
||||
end
|
||||
|
||||
-- `load` red (the addon is not running), `error` amber (code raised), `warn` grey (a call
|
||||
-- was accepted and quietly did nothing).
|
||||
function BenillaScriptLog_RowColor(kind)
|
||||
if kind == "load" then return 1.0, 0.3, 0.3 end
|
||||
if kind == "warn" then return 0.65, 0.65, 0.65 end
|
||||
return 1.0, 0.82, 0.0
|
||||
end
|
||||
|
||||
function BenillaScriptLog_Update()
|
||||
local shown, total = BenillaGetNumScriptErrors()
|
||||
local shown = table.getn(BenillaScriptLog.rows)
|
||||
local total = BenillaScriptLog.total
|
||||
|
||||
-- "Problems", because two of the three kinds are not errors.
|
||||
local summary
|
||||
if total == 0 then
|
||||
summary = "No problems this session."
|
||||
@@ -68,18 +93,17 @@
|
||||
for i = 1, BENILLA_SCRIPTLOG_ROWS do
|
||||
local button = getglobal("BenillaScriptLogRow" .. i)
|
||||
local index = offset + i
|
||||
local seq, kind, message, count = BenillaGetScriptErrorInfo(index)
|
||||
if seq then
|
||||
button.seq = seq
|
||||
button.message = message
|
||||
local row = BenillaScriptLog.rows[index]
|
||||
if row then
|
||||
button.seq = row.seq
|
||||
button.message = row.message
|
||||
-- `#12 x1113`: one deduplicated failure that happened 1,113 times.
|
||||
local label = "#" .. seq
|
||||
if count > 1 then label = label .. " x" .. count end
|
||||
local label = "#" .. row.seq
|
||||
if row.count > 1 then label = label .. " x" .. row.count end
|
||||
button.index:SetText(label)
|
||||
button.label:SetText(BenillaScriptLog_RowText(message))
|
||||
button.label:SetTextColor(BenillaScriptLog_RowColor(kind))
|
||||
button.label:SetText(BenillaScriptLog_RowText(row.message))
|
||||
button:Show()
|
||||
if seq == BenillaScriptLogSelected then selectedRow = button end
|
||||
if row.seq == BenillaScriptLogSelected then selectedRow = button end
|
||||
else
|
||||
button.seq = nil
|
||||
button.message = nil
|
||||
@@ -103,11 +127,9 @@
|
||||
function BenillaScriptLog_UpdateDetail()
|
||||
local text = ""
|
||||
if BenillaScriptLogSelected then
|
||||
local shown = BenillaGetNumScriptErrors()
|
||||
for i = 1, shown do
|
||||
local seq, _, message = BenillaGetScriptErrorInfo(i)
|
||||
if seq == BenillaScriptLogSelected then
|
||||
text = message
|
||||
for _, row in ipairs(BenillaScriptLog.rows) do
|
||||
if row.seq == BenillaScriptLogSelected then
|
||||
text = row.message
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -133,7 +155,7 @@
|
||||
BenillaScriptLog_Update()
|
||||
end
|
||||
|
||||
function BenillaScriptLogFrame_OnLoad(self)
|
||||
function BenillaScriptLogFrame_OnLoad()
|
||||
-- Masks the square icon to the portrait ring.
|
||||
SetPortraitToTexture("BenillaScriptLogFramePortrait",
|
||||
"Interface\\Icons\\INV_Misc_Note_01")
|
||||
@@ -157,7 +179,7 @@
|
||||
|
||||
-- `/errors clear`: keeps the numbering, so a number never names two failures.
|
||||
function BenillaScriptLog_Clear()
|
||||
BenillaClearScriptErrors()
|
||||
BenillaScriptLog.rows = {}
|
||||
BenillaScriptLogSelected = nil
|
||||
if getglobal("BenillaScriptLogFrame"):IsVisible() then
|
||||
BenillaScriptLog_Update()
|
||||
@@ -184,10 +206,10 @@
|
||||
<HighlightTexture file="Interface\Buttons\UI-Listbox-Highlight2" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
self.index = getglobal(self:GetName() .. "Index")
|
||||
self.label = getglobal(self:GetName() .. "Label")
|
||||
this.index = getglobal(this:GetName() .. "Index")
|
||||
this.label = getglobal(this:GetName() .. "Label")
|
||||
</OnLoad>
|
||||
<OnClick>BenillaScriptLogRow_OnClick(self)</OnClick>
|
||||
<OnClick>BenillaScriptLogRow_OnClick(this)</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
@@ -263,10 +285,10 @@
|
||||
<Size><AbsDimension x="293" y="128"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-314"/></Offset></Anchor></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>self:SetScrollChild(getglobal(self:GetName() .. "Child"))</OnLoad>
|
||||
<OnMouseWheel>BenillaScroll_OnMouseWheel(self, arg1)</OnMouseWheel>
|
||||
<OnVerticalScroll>BenillaScroll_SyncBar(self)</OnVerticalScroll>
|
||||
<OnScrollRangeChanged>BenillaScroll_SyncBar(self)</OnScrollRangeChanged>
|
||||
<OnLoad>this:SetScrollChild(getglobal(this:GetName() .. "Child"))</OnLoad>
|
||||
<OnMouseWheel>BenillaScroll_OnMouseWheel(this, arg1)</OnMouseWheel>
|
||||
<OnVerticalScroll>BenillaScroll_SyncBar(this)</OnVerticalScroll>
|
||||
<OnScrollRangeChanged>BenillaScroll_SyncBar(this)</OnScrollRangeChanged>
|
||||
</Scripts>
|
||||
<Frames>
|
||||
<Slider name="$parentBar" inherits="BenillaScrollBarTemplate" hidden="true">
|
||||
@@ -274,7 +296,7 @@
|
||||
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="6" y="-16"/></Offset></Anchor>
|
||||
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="6" y="16"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnValueChanged>BenillaScrollBar_OnValueChanged(self, arg1)</OnValueChanged></Scripts>
|
||||
<Scripts><OnValueChanged>BenillaScrollBar_OnValueChanged(this, arg1)</OnValueChanged></Scripts>
|
||||
</Slider>
|
||||
<Frame name="$parentChild">
|
||||
<Size><AbsDimension x="293" y="128"/></Size>
|
||||
@@ -304,34 +326,32 @@
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>BenillaScriptLogFrame_OnLoad(self)</OnLoad>
|
||||
<OnLoad>BenillaScriptLogFrame_OnLoad()</OnLoad>
|
||||
<!-- A spin bubbles up the parent chain; the detail pane's own handler wins there. -->
|
||||
<OnMouseWheel>BenillaScriptLogFrame_OnMouseWheel(arg1)</OnMouseWheel>
|
||||
<OnShow>BenillaScriptLog_Update()</OnShow>
|
||||
<OnUpdate>BenillaScriptLogFrame_OnUpdate()</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- Benilla's slash commands, handed to SubmitChatInput (ui_chat::input); loads after the
|
||||
stock ChatFrame.xml, which declares SlashCmdList. -->
|
||||
<!-- The player's commands, as an addon registers them; after the stock ChatFrame.lua, which
|
||||
declares SlashCmdList. -->
|
||||
<Script><![CDATA[
|
||||
local BENILLA_COMMANDS = {
|
||||
{ "CHATTEST", "/chattest" },
|
||||
{ "SHOT", "/shot" },
|
||||
{ "LIQUID", "/liquid" },
|
||||
{ "CASTVIS", "/castvis" },
|
||||
{ "PARTYTEST", "/partytest" },
|
||||
{ "REACTION", "/reaction", "/react" },
|
||||
{ "RELOAD", "/reload" },
|
||||
{ "ERRORS", "/errors", "/err" },
|
||||
{ "CONVERTRAID", "/convertraid" },
|
||||
}
|
||||
for _, row in ipairs(BENILLA_COMMANDS) do
|
||||
local key = "BENILLA_" .. row[1]
|
||||
for i = 2, table.getn(row) do
|
||||
setglobal("SLASH_" .. key .. (i - 1), row[i])
|
||||
end
|
||||
SlashCmdList[key] = function(msg)
|
||||
SubmitChatInput(row[2] .. " " .. (msg or ""))
|
||||
-- Deviation: 1.12 has no `/reload` (later clients added it; 1.12 reloads through
|
||||
-- `/console reloadui`); kept so a player can reload the interface after toggling addons.
|
||||
SLASH_BENILLA_RELOAD1 = "/reload"
|
||||
SlashCmdList["BENILLA_RELOAD"] = function()
|
||||
ReloadUI()
|
||||
end
|
||||
|
||||
-- `/errors` toggles the log and `/errors clear` empties it; `/err` is the short form.
|
||||
SLASH_BENILLA_ERRORS1 = "/errors"
|
||||
SLASH_BENILLA_ERRORS2 = "/err"
|
||||
SlashCmdList["BENILLA_ERRORS"] = function(msg)
|
||||
local arg = string.lower((string.gsub(msg or "", "^%s*(.-)%s*$", "%1")))
|
||||
if arg == "clear" then
|
||||
BenillaScriptLog_Clear()
|
||||
else
|
||||
BenillaScriptLog_Toggle()
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnValueChanged>BenillaFauxScrollBar_OnValueChanged(self, arg1)</OnValueChanged>
|
||||
<OnValueChanged>BenillaFauxScrollBar_OnValueChanged(this, arg1)</OnValueChanged>
|
||||
</Scripts>
|
||||
<ThumbTexture name="$parentThumbTexture" file="Interface\Buttons\UI-ScrollBar-Knob">
|
||||
<Size><AbsDimension x="16" y="16"/></Size>
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
//! An addon's `Bindings.xml` rows ([`benilla_ui::bindings_xml`]) dispatch here too: a resolved
|
||||
//! chord names a [`Bound`], either a registry [`Cmd`] or an index into the addon table.
|
||||
//!
|
||||
//! While the Keybindings page has a capsule selected (`BenillaBindCapture`), raw input is
|
||||
//! canonicalized (`ALT-CTRL-SHIFT-<TOKEN>`) and handed to the page, by the 1.12 law: lone
|
||||
//! modifiers ignored, left and right clicks stay UI clicks, ESC binds like any key.
|
||||
//!
|
||||
//! Persistence: `benilla-config/bindings/account.txt` and `<Realm>-<Char>.txt` ([`store`]); the
|
||||
//! character file's existence is the character-set state, as in the reference.
|
||||
|
||||
@@ -442,7 +438,6 @@ fn latch_and_dispatch(
|
||||
// affordances: a player build keeps the reference's `CTRL-SHIFT-P` fallback to `SHIFT-P`.
|
||||
let dev_plane = ctrl && shift && !alt && crate::run_mode::dev_affordances();
|
||||
|
||||
let armed = script.as_ref().is_some_and(|s| s.bind_capture_armed());
|
||||
// A binding body runs as `ExecuteBinding` runs it, so the movement functions' gate passes.
|
||||
let run_lua = |script: &mut Option<NonSendMut<UiScript>>, lua: &str, tag: &str| {
|
||||
if let Some(s) = script.as_mut() {
|
||||
@@ -452,47 +447,6 @@ fn latch_and_dispatch(
|
||||
}
|
||||
};
|
||||
|
||||
// ── The capture seam ── a Keybindings capsule is selected: swallow raw input and hand the
|
||||
// chord string to the page's Lua. The 1.12 law: lone modifiers and unknown keys ignored,
|
||||
// left and right stay UI clicks, the wheel is a chord. Super is not a 1.12 modifier, so a
|
||||
// Super press is ignored outright.
|
||||
if armed {
|
||||
let mut captured: Option<String> = None;
|
||||
for ev in keyboard.read() {
|
||||
if ev.state != ButtonState::Pressed || ev.repeat || sup {
|
||||
continue;
|
||||
}
|
||||
if let Some(token) = chord::key_token(ev.key_code) {
|
||||
captured = Some(chord::chord_string(alt, ctrl, shift, token));
|
||||
}
|
||||
}
|
||||
for b in [MouseButton::Middle, MouseButton::Forward, MouseButton::Back] {
|
||||
if buttons.just_pressed(b) && !sup {
|
||||
if let Some(token) = chord::mouse_token(b) {
|
||||
captured = Some(chord::chord_string(alt, ctrl, shift, token));
|
||||
}
|
||||
}
|
||||
}
|
||||
if scroll.delta.y != 0.0 && !sup {
|
||||
let token = if scroll.delta.y > 0.0 {
|
||||
"MOUSEWHEELUP"
|
||||
} else {
|
||||
"MOUSEWHEELDOWN"
|
||||
};
|
||||
captured = Some(chord::chord_string(alt, ctrl, shift, token));
|
||||
}
|
||||
if let Some(chord_str) = captured {
|
||||
// The Keybindings page's own handler, not a binding body.
|
||||
if let Some(s) = script.as_mut() {
|
||||
if let Err(e) = s.run(&crate::ui_script::host_key_capture(&chord_str)) {
|
||||
warn!("bindings(capture): {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Releases still unlatch below so a key held across the arm cannot stick; nothing new
|
||||
// latches or fires while armed.
|
||||
}
|
||||
|
||||
// ── Who owns this frame's keys ── a focused EditBox eats every key while it holds focus; a
|
||||
// shown keyboard frame ate the keys in `capture.consumed`. Both suppress a press and nothing
|
||||
// else: in the reference a focused box makes the movement handlers no-ops, so the direction
|
||||
@@ -533,7 +487,7 @@ fn latch_and_dispatch(
|
||||
if !repeat {
|
||||
state.down.push(key);
|
||||
}
|
||||
if armed || (typing && !arrow_exempt) || eaten || sup || repeat {
|
||||
if (typing && !arrow_exempt) || eaten || sup || repeat {
|
||||
continue;
|
||||
}
|
||||
if state.latched.iter().any(|&(k, _)| k == BindKey::Key(key)) {
|
||||
@@ -579,7 +533,6 @@ fn latch_and_dispatch(
|
||||
MouseButton::Back,
|
||||
] {
|
||||
if buttons.just_pressed(b)
|
||||
&& !armed
|
||||
&& !sup
|
||||
&& hover.0.is_none()
|
||||
&& !state.latched.iter().any(|&(k, _)| k == BindKey::Mouse(b))
|
||||
@@ -619,7 +572,7 @@ fn latch_and_dispatch(
|
||||
let wheel = wheel_lines(scroll.unit, scroll.delta.y);
|
||||
// Over chrome only (`PointerOverUiPanel`): the wheel still zooms with the cursor on a
|
||||
// nameplate, a mouse-enabled widget but not a panel.
|
||||
if wheel != 0.0 && !armed && !sup && !over_ui.0 {
|
||||
if wheel != 0.0 && !sup && !over_ui.0 {
|
||||
let (key, amount) = if wheel > 0.0 {
|
||||
(BindKey::WheelUp, wheel)
|
||||
} else {
|
||||
@@ -1071,59 +1024,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The armed capture seam, driven by a real wheel event.
|
||||
#[test]
|
||||
fn an_armed_capture_takes_a_wheel_notch() {
|
||||
let mut script = UiScript::new().expect("VM");
|
||||
script.register_bindings(®istry_commands());
|
||||
script
|
||||
.run(
|
||||
r#"CAPTURED = nil
|
||||
function KeyBindings_OnHostKey(chord) CAPTURED = chord end
|
||||
BenillaBindCapture(true)"#,
|
||||
)
|
||||
.expect("arm");
|
||||
let mut app = vm_harness(script);
|
||||
|
||||
app.world_mut().write_message(MouseWheel {
|
||||
unit: MouseScrollUnit::Line,
|
||||
x: 0.0,
|
||||
y: 1.0,
|
||||
window: Entity::PLACEHOLDER,
|
||||
});
|
||||
app.update();
|
||||
assert_eq!(
|
||||
lua_str(&app, "tostring(CAPTURED)"),
|
||||
"MOUSEWHEELUP",
|
||||
"a wheel notch while armed is a binding key"
|
||||
);
|
||||
assert!(
|
||||
!state(&app).fired(cmd::CAMERA_ZOOM_IN),
|
||||
"the armed seam swallows the notch — it must not also zoom"
|
||||
);
|
||||
|
||||
// Down, and a modified notch.
|
||||
app.world_mut().write_message(MouseWheel {
|
||||
unit: MouseScrollUnit::Line,
|
||||
x: 0.0,
|
||||
y: -1.0,
|
||||
window: Entity::PLACEHOLDER,
|
||||
});
|
||||
app.update();
|
||||
assert_eq!(lua_str(&app, "tostring(CAPTURED)"), "MOUSEWHEELDOWN");
|
||||
press_key(&mut app, KeyCode::ShiftLeft);
|
||||
app.world_mut().write_message(MouseWheel {
|
||||
unit: MouseScrollUnit::Line,
|
||||
x: 0.0,
|
||||
y: 1.0,
|
||||
window: Entity::PLACEHOLDER,
|
||||
});
|
||||
app.update();
|
||||
assert_eq!(lua_str(&app, "tostring(CAPTURED)"), "SHIFT-MOUSEWHEELUP");
|
||||
}
|
||||
|
||||
/// The whole wheel-bind path: the real Keybindings page, a capsule armed by a click, a real
|
||||
/// notch, then the bound chord dispatching.
|
||||
/// The whole wheel-bind path: the real Keybindings page, a capsule armed by a click, a notch
|
||||
/// over the page (its OnMouseWheel, as 1.12's window binds it), then the bound chord
|
||||
/// dispatching.
|
||||
#[test]
|
||||
fn a_wheel_notch_binds_through_the_real_page_and_then_dispatches() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -1141,27 +1044,27 @@ mod tests {
|
||||
.unwrap(),
|
||||
crate::ui_script::keybindings_tests::label(&s, "BINDING_NAME_JUMP", "JUMP")
|
||||
);
|
||||
assert!(s.bind_capture_armed());
|
||||
s.resolve();
|
||||
let (x, y): (f32, f32) = s
|
||||
.eval(&format!(
|
||||
"return ({ROW}5:GetLeft() + {ROW}5:GetRight()) / 2, \
|
||||
({ROW}5:GetTop() + {ROW}5:GetBottom()) / 2"
|
||||
))
|
||||
.unwrap();
|
||||
s.mouse_wheel(x, y, 1.0);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("MOUSEWHEELUP")"#)
|
||||
.unwrap(),
|
||||
"JUMP",
|
||||
"a notch on an armed capsule is a bind"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaKeyBindingsPage.selected == nil")
|
||||
.unwrap(),
|
||||
"the completed bind disarms"
|
||||
);
|
||||
let mut app = vm_harness(s);
|
||||
|
||||
app.world_mut().write_message(MouseWheel {
|
||||
unit: MouseScrollUnit::Line,
|
||||
x: 0.0,
|
||||
y: 1.0,
|
||||
window: Entity::PLACEHOLDER,
|
||||
});
|
||||
app.update();
|
||||
{
|
||||
let s = app.world().non_send_resource::<UiScript>();
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("MOUSEWHEELUP")"#)
|
||||
.unwrap(),
|
||||
"JUMP",
|
||||
"a notch on an armed capsule is a bind"
|
||||
);
|
||||
assert!(!s.bind_capture_armed(), "the completed bind disarms");
|
||||
}
|
||||
|
||||
// The bound chord now dispatches: the next notch jumps rather than binding.
|
||||
app.world_mut().write_message(MouseWheel {
|
||||
unit: MouseScrollUnit::Line,
|
||||
|
||||
@@ -78,22 +78,6 @@ impl Chord {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the canonical chord string from live modifier state and a base token.
|
||||
pub(crate) fn chord_string(alt: bool, ctrl: bool, shift: bool, token: &str) -> String {
|
||||
let mut s = String::new();
|
||||
if alt {
|
||||
s.push_str("ALT-");
|
||||
}
|
||||
if ctrl {
|
||||
s.push_str("CTRL-");
|
||||
}
|
||||
if shift {
|
||||
s.push_str("SHIFT-");
|
||||
}
|
||||
s.push_str(token);
|
||||
s
|
||||
}
|
||||
|
||||
/// The 1.12 token for a physical key; `None` for a key the reference names `UNKNOWN` and for the
|
||||
/// modifiers, which are only prefixes (`IsKeyPressIgnoredForBinding`). Both Enters are `ENTER`.
|
||||
pub(crate) fn key_token(k: KeyCode) -> Option<&'static str> {
|
||||
@@ -235,29 +219,17 @@ pub(crate) fn normalize_key(k: KeyCode) -> KeyCode {
|
||||
}
|
||||
}
|
||||
|
||||
/// The 1.12 token for a mouse button: BUTTON4 is winit's `Forward` and BUTTON5 its `Back`, so the
|
||||
/// 1.12 default `BUTTON4 TOGGLEAUTORUN` sits on `Forward`. Which physical button the reference
|
||||
/// calls BUTTON4 is untraced.
|
||||
pub(crate) fn mouse_token(b: MouseButton) -> Option<&'static str> {
|
||||
Some(match b {
|
||||
MouseButton::Left => "BUTTON1",
|
||||
MouseButton::Right => "BUTTON2",
|
||||
MouseButton::Middle => "BUTTON3",
|
||||
MouseButton::Forward => "BUTTON4",
|
||||
MouseButton::Back => "BUTTON5",
|
||||
// The reference names further buttons `BUTTON<n>` (`0x4b6aa0`'s bit-scan fallback).
|
||||
// winit passes the platform's button number, so the sixth button is `Other(5)`.
|
||||
MouseButton::Other(n) => return EXTRA_BUTTONS.get(usize::from(n).wrapping_sub(5)).copied(),
|
||||
})
|
||||
}
|
||||
|
||||
/// `BUTTON6` to `BUTTON20`, indexed by `Other(n) - 5`.
|
||||
/// `BUTTON6` to `BUTTON20`, indexed by `Other(n) - 5`: the reference names further buttons
|
||||
/// `BUTTON<n>` (`0x4b6aa0`'s bit-scan fallback), and winit passes the platform's button number, so
|
||||
/// the sixth button is `Other(5)`.
|
||||
const EXTRA_BUTTONS: &[&str] = &[
|
||||
"BUTTON6", "BUTTON7", "BUTTON8", "BUTTON9", "BUTTON10", "BUTTON11", "BUTTON12", "BUTTON13",
|
||||
"BUTTON14", "BUTTON15", "BUTTON16", "BUTTON17", "BUTTON18", "BUTTON19", "BUTTON20",
|
||||
];
|
||||
|
||||
/// Token to base input: the inverse of [`key_token`] and [`mouse_token`], plus the wheel pair.
|
||||
/// Token to base input: the inverse of [`key_token`], the mouse buttons and the wheel pair.
|
||||
/// BUTTON4 is winit's `Forward` and BUTTON5 its `Back`, so the 1.12 default `BUTTON4
|
||||
/// TOGGLEAUTORUN` sits on `Forward`; which physical button the reference calls BUTTON4 is untraced.
|
||||
fn token_key(t: &str) -> Option<BindKey> {
|
||||
use KeyCode::*;
|
||||
if let Some(b) = match t {
|
||||
@@ -455,15 +427,11 @@ mod tests {
|
||||
"{k:?} names '{token}', which SetBinding refuses"
|
||||
);
|
||||
}
|
||||
for b in [
|
||||
MouseButton::Left,
|
||||
MouseButton::Right,
|
||||
MouseButton::Middle,
|
||||
MouseButton::Forward,
|
||||
MouseButton::Back,
|
||||
for token in [
|
||||
"BUTTON1", "BUTTON2", "BUTTON3", "BUTTON4", "BUTTON5", "BUTTON20",
|
||||
] {
|
||||
let token = mouse_token(b).expect("named");
|
||||
assert!(normalize_binding_key(token).is_some(), "{b:?} → '{token}'");
|
||||
assert!(token_key(token).is_some(), "'{token}' names no button");
|
||||
assert!(normalize_binding_key(token).is_some(), "'{token}'");
|
||||
}
|
||||
for token in ["MOUSEWHEELUP", "MOUSEWHEELDOWN"] {
|
||||
assert!(normalize_binding_key(token).is_some());
|
||||
@@ -626,10 +594,6 @@ mod tests {
|
||||
})
|
||||
);
|
||||
assert_eq!(Chord::parse("BOGUS"), None);
|
||||
assert_eq!(
|
||||
chord_string(true, false, true, "PAGEDOWN"),
|
||||
"ALT-SHIFT-PAGEDOWN"
|
||||
);
|
||||
assert_eq!(
|
||||
Chord::parse("ALT-SHIFT-PAGEDOWN").unwrap(),
|
||||
Chord {
|
||||
|
||||
@@ -1129,9 +1129,9 @@ const TABLE: &[Spec] = &[
|
||||
None
|
||||
),
|
||||
// ── MultiActionBar (BINDING_HEADER_MULTIACTIONBAR) ──────────────────────────────────
|
||||
// The four multibars' `runOnUp` pairs (`Bindings.xml:799-1134`), stock MultiActionBars.lua's.
|
||||
// Deviation: 1.12 files bars 2-4 under the spacer headers `BLANK`, `BLANK2` and `BLANK3`; they
|
||||
// sit under MULTIACTIONBAR here because this page's sections are named.
|
||||
// The four multibars' `runOnUp` pairs (`Bindings.xml:799-1134`), stock MultiActionBars.lua's;
|
||||
// bars 2-4 under the spacer headers `BLANK`, `BLANK2` and `BLANK3`, as `Bindings.xml:883`,
|
||||
// `:967` and `:1051` file them.
|
||||
spec!(
|
||||
"MULTIACTIONBAR1BUTTON1",
|
||||
MULTIACTIONBAR,
|
||||
@@ -1254,7 +1254,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON1",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 1)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 1)"#
|
||||
@@ -1264,7 +1264,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON2",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 2)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 2)"#
|
||||
@@ -1274,7 +1274,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON3",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 3)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 3)"#
|
||||
@@ -1284,7 +1284,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON4",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 4)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 4)"#
|
||||
@@ -1294,7 +1294,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON5",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 5)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 5)"#
|
||||
@@ -1304,7 +1304,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON6",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 6)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 6)"#
|
||||
@@ -1314,7 +1314,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON7",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 7)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 7)"#
|
||||
@@ -1324,7 +1324,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON8",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 8)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 8)"#
|
||||
@@ -1334,7 +1334,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON9",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 9)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 9)"#
|
||||
@@ -1344,7 +1344,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON10",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 10)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 10)"#
|
||||
@@ -1354,7 +1354,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON11",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 11)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 11)"#
|
||||
@@ -1364,7 +1364,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR2BUTTON12",
|
||||
MULTIACTIONBAR,
|
||||
BLANK,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarBottomRight", 12)"#,
|
||||
r#"MultiActionButtonUp("MultiBarBottomRight", 12)"#
|
||||
@@ -1375,7 +1375,7 @@ const TABLE: &[Spec] = &[
|
||||
// The vertical bars: `MultiBarRight` is bar 3, `MultiBarLeft` bar 4.
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON1",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 1)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 1)"#
|
||||
@@ -1385,7 +1385,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON2",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 2)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 2)"#
|
||||
@@ -1395,7 +1395,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON3",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 3)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 3)"#
|
||||
@@ -1405,7 +1405,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON4",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 4)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 4)"#
|
||||
@@ -1415,7 +1415,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON5",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 5)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 5)"#
|
||||
@@ -1425,7 +1425,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON6",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 6)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 6)"#
|
||||
@@ -1435,7 +1435,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON7",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 7)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 7)"#
|
||||
@@ -1445,7 +1445,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON8",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 8)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 8)"#
|
||||
@@ -1455,7 +1455,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON9",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 9)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 9)"#
|
||||
@@ -1465,7 +1465,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON10",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 10)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 10)"#
|
||||
@@ -1475,7 +1475,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON11",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 11)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 11)"#
|
||||
@@ -1485,7 +1485,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR3BUTTON12",
|
||||
MULTIACTIONBAR,
|
||||
BLANK2,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarRight", 12)"#,
|
||||
r#"MultiActionButtonUp("MultiBarRight", 12)"#
|
||||
@@ -1495,7 +1495,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON1",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 1)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 1)"#
|
||||
@@ -1505,7 +1505,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON2",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 2)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 2)"#
|
||||
@@ -1515,7 +1515,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON3",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 3)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 3)"#
|
||||
@@ -1525,7 +1525,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON4",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 4)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 4)"#
|
||||
@@ -1535,7 +1535,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON5",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 5)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 5)"#
|
||||
@@ -1545,7 +1545,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON6",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 6)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 6)"#
|
||||
@@ -1555,7 +1555,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON7",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 7)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 7)"#
|
||||
@@ -1565,7 +1565,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON8",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 8)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 8)"#
|
||||
@@ -1575,7 +1575,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON9",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 9)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 9)"#
|
||||
@@ -1585,7 +1585,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON10",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 10)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 10)"#
|
||||
@@ -1595,7 +1595,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON11",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 11)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 11)"#
|
||||
@@ -1605,7 +1605,7 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
spec!(
|
||||
"MULTIACTIONBAR4BUTTON12",
|
||||
MULTIACTIONBAR,
|
||||
BLANK3,
|
||||
Kind::EdgeUpDown(
|
||||
r#"MultiActionButtonDown("MultiBarLeft", 12)"#,
|
||||
r#"MultiActionButtonUp("MultiBarLeft", 12)"#
|
||||
@@ -2101,6 +2101,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Each registered command sits under the header the install's `Bindings.xml` files it under:
|
||||
/// the last `header=` at or before its row, so the host's `HEADER_*` rows (a category change
|
||||
/// opens one) are the reference's, the spacers over multibars 2-4 included.
|
||||
#[test]
|
||||
fn every_command_sits_under_its_bindings_xml_header() {
|
||||
let Some(binds) = install_bindings() else {
|
||||
return;
|
||||
};
|
||||
let mut section = String::new();
|
||||
let mut under = std::collections::HashMap::new();
|
||||
for b in &binds {
|
||||
if b.platform.as_deref().is_some_and(|p| p != "windows") {
|
||||
continue;
|
||||
}
|
||||
if let Some(h) = &b.header {
|
||||
section = format!("BINDING_HEADER_{h}");
|
||||
}
|
||||
under.insert(b.name.clone(), section.clone());
|
||||
}
|
||||
for spec in SPECS {
|
||||
assert_eq!(
|
||||
under.get(spec.name).map(String::as_str),
|
||||
Some(spec.category),
|
||||
"{} is filed under the wrong header",
|
||||
spec.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The install's own `Bindings.xml`, parsed.
|
||||
fn install_bindings() -> Option<Vec<benilla_ui::bindings_xml::AddonBinding>> {
|
||||
let data = benilla_formats::wow_data_or_skip!(None);
|
||||
|
||||
@@ -1036,7 +1036,7 @@ pub(super) fn seed_ui_fixture(
|
||||
if let Err(e) = script.run(
|
||||
"ShowUIPanel(BenillaOptionsFrame); \
|
||||
BenillaOptionsFrameCategoryListRowKeybindings:Click(); \
|
||||
KeyBindings_ExpandSection(1, true); KeyBindingsPage_Update()",
|
||||
BenillaKeyBindings_ExpandSection(1, true); BenillaKeyBindingsPage_Update()",
|
||||
) {
|
||||
warn!("capture: ui-keybindings seed failed: {e}");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
//! player's own strings define, read off the UI VM's globals the way `ChatEdit_ParseText` walks
|
||||
//! them (`ChatFrame.lua:2164-2200`): `SLASH_<INDEX><n>` for the actions, `EMOTE<i>_CMD<j>` →
|
||||
//! `EMOTE<i>_TOKEN` for the emotes. An action wins over an emote alias of the same name, as the
|
||||
//! reference tries `SlashCmdList` first; benilla's own commands, literals, go in after both.
|
||||
//! reference tries `SlashCmdList` first; benilla's dev instruments, literals, go in after both.
|
||||
//! benilla's player commands (`/reload`, `/errors`) are its layer's `SlashCmdList` rows, which a
|
||||
//! line this table does not know reaches through the drain's unknown-command arm.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -50,16 +52,6 @@ pub(crate) enum SlashIndex {
|
||||
MacroHelp,
|
||||
/// `/console <line>`: `ConsoleExec(msg)`, as the reference's handler (`ChatFrame.lua:671`).
|
||||
Console,
|
||||
/// `/reload`, the rebuild `/console reloadUI` runs. Deviation: 1.12 has no such alias (later
|
||||
/// clients added it); kept so a player can reload the interface after toggling addons.
|
||||
ReloadUi,
|
||||
/// `/errors`, `/err`: opens benilla's script error log. Deviation: 1.12 has only the
|
||||
/// `ScriptErrors` modal, which shows a burst's first error and keeps none; addon users need
|
||||
/// the list, so it is in every build.
|
||||
ScriptErrors,
|
||||
/// `/convertraid`: convert the party to a raid (`CMSG_GROUP_RAID_CONVERT`, leader only).
|
||||
/// Deviation: 1.12's only trigger is the Raid tab's Convert button (`RaidFrame.xml`).
|
||||
ConvertRaid,
|
||||
}
|
||||
|
||||
impl SlashIndex {
|
||||
@@ -101,15 +93,11 @@ impl SlashIndex {
|
||||
Self::MacroUi => "MACRO",
|
||||
Self::MacroHelp => "MACROHELP",
|
||||
Self::Console => "CONSOLE",
|
||||
// No shipped strings under these three keys: their aliases are literals in `build`.
|
||||
Self::ReloadUi => "RELOADUI",
|
||||
Self::ScriptErrors => "BENILLASCRIPTERRORS",
|
||||
Self::ConvertRaid => "BENILLACONVERTRAID",
|
||||
}
|
||||
}
|
||||
|
||||
/// Every registered index; any other command answers `HELP_TEXT_SIMPLE`, as unknown ones do.
|
||||
const ALL: [Self; 38] = [
|
||||
const ALL: [Self; 35] = [
|
||||
Self::Reply,
|
||||
Self::Join,
|
||||
Self::Leave,
|
||||
@@ -145,9 +133,6 @@ impl SlashIndex {
|
||||
Self::MacroUi,
|
||||
Self::MacroHelp,
|
||||
Self::Console,
|
||||
Self::ReloadUi,
|
||||
Self::ScriptErrors,
|
||||
Self::ConvertRaid,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -199,8 +184,8 @@ pub(crate) enum Command {
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct SlashCommands {
|
||||
by_alias: HashMap<String, Command>,
|
||||
/// Aliases per source, logged at boot: `(slash, emote, benilla additions, dev instruments)`.
|
||||
counts: (usize, usize, usize, usize),
|
||||
/// Aliases per source, logged at boot: `(slash, emote, dev instruments)`.
|
||||
counts: (usize, usize, usize),
|
||||
}
|
||||
|
||||
impl SlashCommands {
|
||||
@@ -246,27 +231,7 @@ impl SlashCommands {
|
||||
}
|
||||
let emote_aliases = by_alias.len() - slash_aliases;
|
||||
|
||||
// 3 · benilla's player-facing additions, in every build; after the walks, so shipped wins.
|
||||
insert(
|
||||
&mut by_alias,
|
||||
"reload",
|
||||
Command::Slash(SlashIndex::ReloadUi),
|
||||
);
|
||||
for alias in ["errors", "err"] {
|
||||
insert(
|
||||
&mut by_alias,
|
||||
alias,
|
||||
Command::Slash(SlashIndex::ScriptErrors),
|
||||
);
|
||||
}
|
||||
insert(
|
||||
&mut by_alias,
|
||||
"convertraid",
|
||||
Command::Slash(SlashIndex::ConvertRaid),
|
||||
);
|
||||
let added_aliases = by_alias.len() - slash_aliases - emote_aliases;
|
||||
|
||||
// 4 · the dev instruments, dev builds only; a player build answers them as unknown.
|
||||
// 3 · the dev instruments, dev builds only; a player build answers them as unknown.
|
||||
let before_dev = by_alias.len();
|
||||
if crate::run_mode::dev_affordances() {
|
||||
for dev in DevCmd::ALL {
|
||||
@@ -279,13 +244,13 @@ impl SlashCommands {
|
||||
|
||||
Self {
|
||||
by_alias,
|
||||
counts: (slash_aliases, emote_aliases, added_aliases, dev_aliases),
|
||||
counts: (slash_aliases, emote_aliases, dev_aliases),
|
||||
}
|
||||
}
|
||||
|
||||
/// Distinct aliases per source (the shipped strings repeat: `EMOTE87_CMD1` and `_CMD2` are
|
||||
/// both `"/sit"`); a player build reports 0 dev aliases.
|
||||
pub(super) fn counts(&self) -> (usize, usize, usize, usize) {
|
||||
pub(super) fn counts(&self) -> (usize, usize, usize) {
|
||||
self.counts
|
||||
}
|
||||
}
|
||||
@@ -298,6 +263,23 @@ fn insert(map: &mut HashMap<String, Command>, alias: &str, cmd: Command) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The dev instruments as `SlashCmdList` rows (`BENILLA_<COMMAND>`), so a typed one reaches the
|
||||
/// drain through the stock `ChatEdit_ParseText`; registered on a freshly loaded VM in a build with
|
||||
/// dev affordances only, so a player build answers them `HELP_TEXT_SIMPLE`, as 1.12 does.
|
||||
pub(crate) fn register_dev_commands(script: &benilla_ui::script::UiScript, dev: bool) {
|
||||
if !dev {
|
||||
return;
|
||||
}
|
||||
for cmd in DevCmd::ALL {
|
||||
let aliases: Vec<String> = cmd.aliases().iter().map(|a| format!("/{a}")).collect();
|
||||
let aliases: Vec<&str> = aliases.iter().map(String::as_str).collect();
|
||||
let key = format!("BENILLA_{}", cmd.aliases()[0].to_ascii_uppercase());
|
||||
if let Err(e) = script.register_host_slash_command(&key, &aliases) {
|
||||
warn!("chat: registering /{}: {e}", cmd.aliases()[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the table in `PostStartup`, after `Startup` has inserted the VM and the emote catalog.
|
||||
pub(crate) fn build_slash_commands(
|
||||
mut commands: Commands,
|
||||
@@ -314,11 +296,11 @@ pub(crate) fn build_slash_commands(
|
||||
|name| globals.get::<String>(name).ok().filter(|s| !s.is_empty()),
|
||||
|token| emotes.text_id(token),
|
||||
);
|
||||
let (slash, emote, added, dev) = table.counts();
|
||||
let (slash, emote, dev) = table.counts();
|
||||
// The shipped 1.12 data gives the counts `real_alias_table_resolves_the_shipped_commands` pins.
|
||||
info!(
|
||||
"chat: slash table — {slash} command aliases, {emote} emote aliases, \
|
||||
{added} benilla additions, {dev} instrument aliases"
|
||||
{dev} instrument aliases"
|
||||
);
|
||||
if emote == 0 {
|
||||
error!("chat: NO emote aliases — every /wave-style command is dead (GlobalStrings?)");
|
||||
|
||||
@@ -169,8 +169,8 @@ pub(super) fn drain_chat_input(
|
||||
return;
|
||||
};
|
||||
let mut queue = engine_verbs(&mut script, emotes.as_deref());
|
||||
// benilla's own commands, from the `SlashCmdList` entries in `ScriptLogFrame.xml` once the
|
||||
// stock `ChatEdit_ParseText` has found no built-in, and probe lines.
|
||||
// The dev instruments, from their host `SlashCmdList` rows once the stock `ChatEdit_ParseText`
|
||||
// has found no built-in (dev builds), and probe lines.
|
||||
for raw in script.take_chat_input() {
|
||||
let msg = raw.trim();
|
||||
if msg.is_empty() {
|
||||
@@ -537,11 +537,6 @@ pub(super) fn drain_chat_input(
|
||||
let _ = commands.0.send(ClientCommand::GroupInvite { name });
|
||||
}
|
||||
}
|
||||
// The server judges it: a raid-typed `SMSG_GROUP_LIST`, or an
|
||||
// `SMSG_PARTY_COMMAND_RESULT` error.
|
||||
ParsedChat::ConvertRaid => {
|
||||
let _ = commands.0.send(ClientCommand::GroupRaidConvert);
|
||||
}
|
||||
ParsedChat::Uninvite { name } => {
|
||||
if let Some(name) =
|
||||
name.or_else(|| target_player_name(&selection, &names, &commands))
|
||||
@@ -800,10 +795,6 @@ pub(super) fn drain_chat_input(
|
||||
ParsedChat::Quit => {
|
||||
script.queue_session_request(benilla_ui::script::SessionRequest::Quit)
|
||||
}
|
||||
// The deferred rebuild `ReloadUI()` queues.
|
||||
ParsedChat::ReloadUi => {
|
||||
script.queue_session_request(benilla_ui::script::SessionRequest::ReloadUi)
|
||||
}
|
||||
// Deferred to the world, since a command is `fn(&mut World, &str)`; its lines print
|
||||
// as system text.
|
||||
ParsedChat::Console { line } => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! What a submitted slash line means: [`parse_line`] resolves it through the boot-built command
|
||||
//! table ([`super::super::commands`], the shipped `GlobalStrings.lua` aliases) into a
|
||||
//! [`ParsedChat`] for [`super`] to execute. A typed line reaches it only through benilla's own
|
||||
//! `SlashCmdList` entries, once the stock `ChatEdit_ParseText` finds no built-in; a probe's line
|
||||
//! reaches it directly.
|
||||
//! [`ParsedChat`] for [`super`] to execute. A typed line reaches it only through the dev
|
||||
//! instruments' `SlashCmdList` rows (dev builds), once the stock `ChatEdit_ParseText` finds no
|
||||
//! built-in; a probe's line reaches it directly.
|
||||
|
||||
use crate::ui_chat::commands::{Command, DevCmd, SlashCommands, SlashIndex};
|
||||
|
||||
@@ -49,9 +49,6 @@ pub(in crate::ui_chat) enum ParsedChat {
|
||||
Liquid,
|
||||
/// `/reaction [name]`: every input and rung of the reaction ladder for one unit.
|
||||
Reaction { name: Option<String> },
|
||||
/// `/convertraid` (`CMSG_GROUP_RAID_CONVERT`), benilla's own: 1.12 converts only from the
|
||||
/// RaidFrame's Convert button.
|
||||
ConvertRaid,
|
||||
/// `/help`.
|
||||
Help,
|
||||
/// An `EmotesText` command (`/wave` is 101), sent as `CMSG_TEXT_EMOTE` at the selection.
|
||||
@@ -102,8 +99,6 @@ pub(in crate::ui_chat) enum ParsedChat {
|
||||
Follow { name: Option<String> },
|
||||
/// `/macrohelp`: `ChatFrame_DisplayMacroHelpText`'s five `MACRO_HELP_TEXT_LINE`s.
|
||||
MacroHelp,
|
||||
/// `/reload`, benilla's own: the in-world rebuild `ReloadUI()` queues.
|
||||
ReloadUi,
|
||||
/// A `/console` line the CVar store did not consume, for the `ConsoleCommand` registry
|
||||
/// ([`crate::console::execute`]).
|
||||
Console { line: String },
|
||||
@@ -288,18 +283,6 @@ fn slash_command(index: SlashIndex, args: &str) -> ParsedChat {
|
||||
body: "ShowMacroFrame()".into(),
|
||||
},
|
||||
S::MacroHelp => ParsedChat::MacroHelp,
|
||||
// `/reload` is the rebuild `/console reloadUI` runs (`0x4035f0`, which reads no arguments).
|
||||
S::ReloadUi => ParsedChat::ReloadUi,
|
||||
S::ConvertRaid => ParsedChat::ConvertRaid,
|
||||
// `/errors`, benilla's own: toggles the layer's script error log; `/errors clear` empties
|
||||
// it. A no-op when the run boots without the layer.
|
||||
S::ScriptErrors => ParsedChat::Lua {
|
||||
body: if args.trim().eq_ignore_ascii_case("clear") {
|
||||
crate::ui_script::ERRORS_CLEAR.into()
|
||||
} else {
|
||||
crate::ui_script::ERRORS_TOGGLE.into()
|
||||
},
|
||||
},
|
||||
// `SlashCmdList["CONSOLE"]` (`ChatFrame.lua:671`) is `ConsoleExec(msg)`. A typed line runs
|
||||
// it; one that skipped the edit box calls the same verb, so a CVar line writes the CVar
|
||||
// and anything else comes back through `engine_verbs`.
|
||||
|
||||
@@ -1437,7 +1437,11 @@ fn real_alias_table_resolves_the_shipped_commands() {
|
||||
);
|
||||
}
|
||||
assert_eq!(parse_line("/macrohelp"), ParsedChat::MacroHelp);
|
||||
assert_eq!(parse_line("/convertraid"), ParsedChat::ConvertRaid);
|
||||
// 1.12 has none of these: `/convertraid` (the Raid tab's button converts), and benilla's
|
||||
// `/reload` and `/errors`, which are its layer's `SlashCmdList` rows, not this table's.
|
||||
for line in ["/convertraid", "/reload", "/errors", "/err"] {
|
||||
assert_eq!(parse_line(line), ParsedChat::Unknown, "{line}");
|
||||
}
|
||||
// `/console` from a line that skipped the stock edit box forwards to the stock handler's verb.
|
||||
assert_eq!(
|
||||
parse_line("/console fpsJournal 1"),
|
||||
@@ -1476,9 +1480,8 @@ fn real_alias_table_resolves_the_shipped_commands() {
|
||||
}
|
||||
}
|
||||
// The shipped surface: 68 distinct aliases over 36 `SlashCmdList` indices and 225 emote
|
||||
// commands over 169 `EmotesText` names (aliases repeat; EMOTE27 "UNUSED" has no row). Then
|
||||
// benilla's own `/reload`, `/errors`, `/err` and `/convertraid`, which are not 1.12 commands,
|
||||
// in every build, and 7 instrument aliases, in dev builds only.
|
||||
// commands over 169 `EmotesText` names (aliases repeat; EMOTE27 "UNUSED" has no row), then 7
|
||||
// instrument aliases, in dev builds only.
|
||||
let instruments = if crate::run_mode::dev_affordances() {
|
||||
7
|
||||
} else {
|
||||
@@ -1486,8 +1489,8 @@ fn real_alias_table_resolves_the_shipped_commands() {
|
||||
};
|
||||
assert_eq!(
|
||||
table.counts(),
|
||||
(68, 225, 4, instruments),
|
||||
"(slash, emote, benilla addition, instrument) aliases"
|
||||
(68, 225, instruments),
|
||||
"(slash, emote, instrument) aliases"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -719,8 +719,8 @@ fn backpack() -> ContainerState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The menu wears the options window's `ERA_WINDOW_SCALE` on show, as the era client draws the two
|
||||
/// at one density; loaded with OptionsFrame.xml, which defines it.
|
||||
/// The menu wears the options window's `BENILLA_ERA_WINDOW_SCALE` on show, as the era client draws
|
||||
/// the two at one density; loaded with OptionsFrame.xml, which defines it.
|
||||
#[test]
|
||||
fn the_menu_rides_the_shared_era_window_scale() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
@@ -737,7 +737,7 @@ fn the_menu_rides_the_shared_era_window_scale() {
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
let k = s.eval::<f64>("return GameMenuFrame:GetScale()").unwrap();
|
||||
let want = s.eval::<f64>("return ERA_WINDOW_SCALE").unwrap();
|
||||
let want = s.eval::<f64>("return BENILLA_ERA_WINDOW_SCALE").unwrap();
|
||||
assert!((k - want).abs() < 1e-6, "menu scale {k} != knob {want}");
|
||||
assert!((want - 0.78).abs() < 1e-6, "the knob itself moved: {want}");
|
||||
}
|
||||
|
||||
@@ -47,6 +47,18 @@ impl PointerFeed<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The buttons the pointer feed hands the UI, by the names its handlers read (`arg1` of `OnClick`,
|
||||
/// `OnMouseDown` and `OnMouseUp`, and the ones `RegisterForClicks` takes,
|
||||
/// `Blizzard_BindingUI.xml:10`); buttons 4 and 5 on the physical buttons the binding chords call
|
||||
/// BUTTON4 and BUTTON5 (`bindings::chord`).
|
||||
const UI_MOUSE_BUTTONS: [(MouseButton, &str); 5] = [
|
||||
(MouseButton::Left, "LeftButton"),
|
||||
(MouseButton::Right, "RightButton"),
|
||||
(MouseButton::Middle, "MiddleButton"),
|
||||
(MouseButton::Forward, "Button4"),
|
||||
(MouseButton::Back, "Button5"),
|
||||
];
|
||||
|
||||
/// One `OnMouseWheel` call per whole notch, `arg1 = ±1`, the fraction carried in `notches`: a
|
||||
/// trackpad gesture arrives as a `Pixel` trickle, and the stock handlers act on the sign alone
|
||||
/// (`ScrollFrameTemplate_OnMouseWheel` scrolls half a pane per call, `UIPanelTemplates.lua:150`).
|
||||
@@ -162,11 +174,7 @@ pub(super) fn feed_ui_input(
|
||||
);
|
||||
}
|
||||
}
|
||||
for (btn, name) in [
|
||||
(MouseButton::Left, "LeftButton"),
|
||||
(MouseButton::Right, "RightButton"),
|
||||
(MouseButton::Middle, "MiddleButton"),
|
||||
] {
|
||||
for (btn, name) in UI_MOUSE_BUTTONS {
|
||||
if buttons.just_pressed(btn) {
|
||||
// A left press that would drop into the world (`world_drop_click`: any payload over
|
||||
// terrain or nothing) is consumed now: the drop fires on the release, but the world
|
||||
@@ -390,6 +398,32 @@ mod tests {
|
||||
assert!(sum <= 1.0, "…and they may move at most one notch: {sum}");
|
||||
}
|
||||
|
||||
/// Every button 1.12's frames take reaches the UI, under the name its binding chord agrees
|
||||
/// with, so the Key Bindings page binds mouse 4 and 5 from a click.
|
||||
#[test]
|
||||
fn the_ui_takes_all_five_buttons_under_the_chords_names() {
|
||||
let names: Vec<&str> = UI_MOUSE_BUTTONS.iter().map(|(_, n)| *n).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
"LeftButton",
|
||||
"RightButton",
|
||||
"MiddleButton",
|
||||
"Button4",
|
||||
"Button5"
|
||||
]
|
||||
);
|
||||
for (i, (button, _)) in UI_MOUSE_BUTTONS.iter().enumerate() {
|
||||
let chord = crate::bindings::chord::Chord::parse(&format!("BUTTON{}", i + 1));
|
||||
assert_eq!(
|
||||
chord.map(|c| c.key),
|
||||
Some(crate::bindings::chord::BindKey::Mouse(*button)),
|
||||
"BUTTON{} is {button:?}",
|
||||
i + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_notch_fires_once() {
|
||||
let mut s = wheel_pane();
|
||||
|
||||
@@ -11,17 +11,7 @@ use crate::bindings::commands::SPECS;
|
||||
/// does.
|
||||
pub(crate) fn harness() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let cmds: Vec<KeybindCommand> = SPECS
|
||||
.iter()
|
||||
.map(|spec| KeybindCommand {
|
||||
name: spec.name,
|
||||
category: spec.category,
|
||||
run_on_up: spec.run_on_up(),
|
||||
default1: spec.d1,
|
||||
default2: spec.d2,
|
||||
})
|
||||
.collect();
|
||||
s.register_bindings(&cmds);
|
||||
register_specs(&mut s);
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in [
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
@@ -54,10 +44,27 @@ pub(crate) fn harness() -> UiScript {
|
||||
s
|
||||
}
|
||||
|
||||
/// The host's command registry, as `seed_bindings_for_vm` registers it.
|
||||
pub(crate) fn register_specs(s: &mut UiScript) {
|
||||
let cmds: Vec<KeybindCommand> = SPECS
|
||||
.iter()
|
||||
.map(|spec| KeybindCommand {
|
||||
name: spec.name,
|
||||
category: spec.category,
|
||||
run_on_up: spec.run_on_up(),
|
||||
default1: spec.d1,
|
||||
default2: spec.d2,
|
||||
})
|
||||
.collect();
|
||||
s.register_bindings(&cmds);
|
||||
}
|
||||
|
||||
/// A label as the page resolves it: the GlobalStrings global named `token`, else `raw`.
|
||||
pub(crate) fn label(s: &UiScript, token: &str, raw: &str) -> String {
|
||||
s.eval::<String>(&format!(r#"return KeyBindings_String("{token}", "{raw}")"#))
|
||||
.unwrap()
|
||||
s.eval::<String>(&format!(
|
||||
r#"return BenillaKeyBindings_String("{token}", "{raw}")"#
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Open the options window on the Keybindings page.
|
||||
@@ -69,6 +76,53 @@ pub(crate) fn on_page(s: &mut UiScript) {
|
||||
|
||||
const ROW: &str = "BenillaOptionsFrameContainerBodyKeybindingsRow";
|
||||
|
||||
/// Whether the window takes keys and the wheel: a selected capsule arms it, as 1.12's
|
||||
/// keyboard-enabled `KeyBindingFrame` (`Blizzard_BindingUI.xml:73`).
|
||||
pub(crate) fn armed(s: &UiScript) -> bool {
|
||||
s.eval::<bool>(
|
||||
"return BenillaOptionsFrame:IsKeyboardEnabled() == 1 \
|
||||
and BenillaOptionsFrame:IsMouseWheelEnabled() == 1 \
|
||||
and BenillaKeyBindingsPage.selected ~= nil",
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// A key press as the host feeds it (`ui_script::input`): ENTER, ESCAPE and TAB through the named
|
||||
/// walk, every other key through the frame walk by its 1.12 name. Answers whether a frame took it.
|
||||
pub(crate) fn press(s: &mut UiScript, key: &str) -> bool {
|
||||
match key {
|
||||
"ENTER" | "ESCAPE" | "TAB" => s.key_input(key),
|
||||
_ => s.frame_key_input(key),
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame's centre in screen pixels, where the pointer feed hits it.
|
||||
pub(crate) fn centre(s: &mut UiScript, frame: &str) -> (f32, f32) {
|
||||
s.resolve();
|
||||
let (l, r, t, b, k): (f64, f64, f64, f64, f64) = s
|
||||
.eval(&format!(
|
||||
"local f = {frame} \
|
||||
local k = (f.GetEffectiveScale and f or f:GetParent()):GetEffectiveScale() \
|
||||
return f:GetLeft(), f:GetRight(), f:GetTop(), f:GetBottom(), k"
|
||||
))
|
||||
.unwrap();
|
||||
(((l + r) * 0.5 * k) as f32, ((t + b) * 0.5 * k) as f32)
|
||||
}
|
||||
|
||||
/// A click of `button` on `frame` through the pointer feed.
|
||||
pub(crate) fn click(s: &mut UiScript, frame: &str, button: &str) {
|
||||
let (x, y) = centre(s, frame);
|
||||
s.mouse_move(x, y);
|
||||
s.mouse_button(x, y, button, true);
|
||||
s.mouse_button(x, y, button, false);
|
||||
}
|
||||
|
||||
/// One wheel notch over the page, through the pointer feed.
|
||||
pub(crate) fn wheel(s: &mut UiScript, delta: f32) {
|
||||
let (x, y) = centre(s, "BenillaOptionsFrameContainerBodyKeybindings");
|
||||
s.mouse_wheel(x, y, delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_page_is_an_options_category_with_the_collapsed_honest_tree() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -90,10 +144,12 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() {
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaOptionsFrameContainerDefaults:IsEnabled() ~= 0")
|
||||
.unwrap());
|
||||
// The categories in `Bindings.xml` order, each a header collapsed by default, as in the era.
|
||||
// The categories in `Bindings.xml` order, each a header collapsed by default, as in the era;
|
||||
// the spacer headers over multibars 2-4 continue the section above (the page's deviation).
|
||||
let mut expected: Vec<&str> = Vec::new();
|
||||
for spec in SPECS {
|
||||
if !expected.contains(&spec.category) {
|
||||
if !expected.contains(&spec.category) && !spec.category.starts_with("BINDING_HEADER_BLANK")
|
||||
{
|
||||
expected.push(spec.category);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +181,7 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() {
|
||||
r#"BINDING_HEADER_MOVEMENT = "Movement Keys"
|
||||
BINDING_NAME_MOVEANDSTEER = "Move and Steer"
|
||||
KEY_BUTTON3 = "Middle Mouse"
|
||||
KeyBindingsPage_Update()"#,
|
||||
BenillaKeyBindingsPage_Update()"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -168,21 +224,23 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
|
||||
on_page(&mut s);
|
||||
s.run(&format!("{ROW}1Header:Click()")).unwrap(); // expand Movement
|
||||
s.take_keybind_requests();
|
||||
// Row 3 is MOVEFORWARD (W, UP); selecting its Key 1 capsule arms the capture.
|
||||
assert!(!s.bind_capture_armed());
|
||||
// Unarmed, the window takes no key: it reaches the game's bindings.
|
||||
assert!(!armed(&s));
|
||||
assert!(!press(&mut s, "J"), "an unarmed page lets a key through");
|
||||
// Row 3 is MOVEFORWARD (W, UP); selecting its Key 1 capsule arms the window.
|
||||
s.run(&format!("{ROW}3Key1Button:Click()")).unwrap();
|
||||
assert!(
|
||||
s.bind_capture_armed(),
|
||||
"a selected capsule arms the capture"
|
||||
);
|
||||
assert!(armed(&s), "a selected capsule arms the window");
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaOptionsFrameContainerUnbind:IsEnabled() ~= 0")
|
||||
.unwrap(),
|
||||
"Unbind arms with the selection"
|
||||
);
|
||||
// A lone modifier is no key (`Blizzard_BindingUI.lua:172-174`): still armed.
|
||||
assert!(press(&mut s, "SHIFT"), "the armed window takes it");
|
||||
assert!(armed(&s), "…and waits for a real key");
|
||||
// J, a key no command holds, takes slot 1 from W; UP stays in slot 2 and the bind saves.
|
||||
s.run(r#"KeyBindings_OnHostKey("J")"#).unwrap();
|
||||
assert!(!s.bind_capture_armed(), "a completed bind disarms");
|
||||
assert!(press(&mut s, "J"), "the armed window takes the key");
|
||||
assert!(!armed(&s), "a completed bind disarms");
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"local k1, k2 = GetBindingKey("MOVEFORWARD"); return k1 == "J" and k2 == "UP""#
|
||||
@@ -202,7 +260,7 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
|
||||
// T is ATTACKTARGET's only key, so taking it names the victim in red (1.12's
|
||||
// `KEY_UNBOUND_ERROR`, `Blizzard_BindingUI.lua:185-190`).
|
||||
s.run(&format!("{ROW}3Key2Button:Click()")).unwrap();
|
||||
s.run(r#"KeyBindings_OnHostKey("T")"#).unwrap();
|
||||
press(&mut s, "T");
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("T")"#).unwrap(),
|
||||
"MOVEFORWARD"
|
||||
@@ -214,9 +272,28 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
|
||||
.contains(&victim),
|
||||
"the newly-bare victim is named"
|
||||
);
|
||||
// A chord: the modifiers held at the press prefix the key, ALT-CTRL-SHIFT order
|
||||
// (`Blizzard_BindingUI.lua:175-183`).
|
||||
s.run(&format!("{ROW}3Key2Button:Click()")).unwrap();
|
||||
s.set_modifiers(true, true, false);
|
||||
press(&mut s, "K");
|
||||
s.set_modifiers(false, false, false);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("CTRL-SHIFT-K")"#)
|
||||
.unwrap(),
|
||||
"MOVEFORWARD"
|
||||
);
|
||||
// ESC while armed binds Escape: 1.12's armed branch has no ESCAPE filter.
|
||||
s.run(&format!("{ROW}3Key2Button:Click()")).unwrap();
|
||||
assert!(press(&mut s, "ESCAPE"));
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("ESCAPE")"#)
|
||||
.unwrap(),
|
||||
"MOVEFORWARD"
|
||||
);
|
||||
// The wheel binds onto a press+release command too: `0x4b7490` never reads the command.
|
||||
s.run(&format!("{ROW}3Key1Button:Click()")).unwrap();
|
||||
s.run(r#"KeyBindings_OnHostKey("MOUSEWHEELUP")"#).unwrap();
|
||||
wheel(&mut s, 1.0);
|
||||
assert!(
|
||||
s.eval::<bool>(r#"local k1 = GetBindingKey("MOVEFORWARD"); return k1 == "MOUSEWHEELUP""#)
|
||||
.unwrap(),
|
||||
@@ -234,7 +311,7 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
|
||||
// A key string the engine rejects restores the slot's old key and shows the only refusal text
|
||||
// 1.12 has, the wheel's (`KeyBindingFrame_SetBinding`, `Blizzard_BindingUI.lua:260-270`).
|
||||
s.run(&format!("{ROW}3Key1Button:Click()")).unwrap();
|
||||
s.run(r#"KeyBindings_OnHostKey("SCROLLLOCK")"#).unwrap();
|
||||
press(&mut s, "SCROLLLOCK");
|
||||
assert!(
|
||||
s.eval::<bool>(r#"local k1 = GetBindingKey("MOVEFORWARD"); return k1 == "MOUSEWHEELUP""#)
|
||||
.unwrap(),
|
||||
@@ -246,15 +323,130 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
|
||||
"Can't bind mousewheel to actions with up and down states"
|
||||
);
|
||||
s.run(&format!("{ROW}3Key1Button:Click()")).unwrap();
|
||||
assert!(s.bind_capture_armed());
|
||||
assert!(armed(&s));
|
||||
s.run(&format!(r#"{ROW}3Key1Button:Click("RightButton")"#))
|
||||
.unwrap();
|
||||
assert!(!s.bind_capture_armed(), "right-click deselects");
|
||||
// Hiding the window disarms: an armed capture with no window would swallow all input.
|
||||
assert!(!armed(&s), "right-click deselects");
|
||||
// Hiding the window disarms.
|
||||
s.run(&format!("{ROW}3Key1Button:Click()")).unwrap();
|
||||
assert!(s.bind_capture_armed());
|
||||
assert!(armed(&s));
|
||||
s.run("HideUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
assert!(!s.bind_capture_armed(), "OnHide disarms");
|
||||
assert!(!armed(&s), "OnHide disarms");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Mouse buttons 3-5 bind through the clicks 1.12's window takes: a capsule's own `OnClick`
|
||||
/// (`Blizzard_BindingUI.lua:246`) and the window's (`Blizzard_BindingUI.xml:538-540`), here the
|
||||
/// page's `OnMouseUp`; left and right clicks only select.
|
||||
#[test]
|
||||
fn mouse_buttons_bind_through_the_capsules_and_the_page() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
on_page(&mut s);
|
||||
s.run(&format!("{ROW}1Header:Click()")).unwrap(); // expand Movement
|
||||
let capsule = format!("{ROW}3Key1Button");
|
||||
click(&mut s, &capsule, "LeftButton");
|
||||
assert!(armed(&s), "a left click selects");
|
||||
click(&mut s, &capsule, "MiddleButton");
|
||||
assert!(!armed(&s));
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("BUTTON3")"#)
|
||||
.unwrap(),
|
||||
"MOVEFORWARD",
|
||||
"a middle click on the armed capsule binds BUTTON3"
|
||||
);
|
||||
// Over the page, away from any capsule: mouse 4 with SHIFT held.
|
||||
click(&mut s, &capsule, "LeftButton");
|
||||
s.set_modifiers(true, false, false);
|
||||
click(&mut s, &format!("{ROW}3Description"), "Button4");
|
||||
s.set_modifiers(false, false, false);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("SHIFT-BUTTON4")"#)
|
||||
.unwrap(),
|
||||
"MOVEFORWARD"
|
||||
);
|
||||
// Unarmed, a middle click on a capsule selects it, as 1.12's does.
|
||||
click(&mut s, &format!("{ROW}4Key2Button"), "MiddleButton");
|
||||
assert!(armed(&s));
|
||||
s.run("BenillaKeyBindings_SetSelected(nil)").unwrap();
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Loading the stock `Blizzard_BindingUI` (an addon manager's Load button does) defines its own
|
||||
/// `KeyBindingButton_OnClick` and popup (`Blizzard_BindingUI.lua:228`, `:10`) and changes no other
|
||||
/// window: the page keeps binding and leaving the character set. The stock window reads the 1.12
|
||||
/// list: a `HEADER_*` row draws a section header, a command's keys fill Key 1 and Key 2.
|
||||
#[test]
|
||||
fn the_page_works_after_the_stock_binding_ui_loads_and_the_stock_window_reads_the_list() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = crate::local_state::test_env::ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (mut s, failures) =
|
||||
super::layer_tests::production_load_with("bindingui", false, "", |_| {});
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
register_specs(&mut s);
|
||||
s.run(r#"assert(LoadAddOn("Blizzard_BindingUI") == 1)"#)
|
||||
.unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return KeyBindingFrame ~= nil and KeyBindingButton_OnClick ~= nil")
|
||||
.unwrap());
|
||||
|
||||
on_page(&mut s);
|
||||
s.run(&format!("{ROW}1Header:Click()")).unwrap(); // expand Movement
|
||||
s.take_keybind_requests();
|
||||
click(&mut s, &format!("{ROW}3Key1Button"), "LeftButton");
|
||||
assert!(armed(&s), "the page's capsule still arms its own window");
|
||||
assert!(
|
||||
s.eval::<bool>("return KeyBindingFrame.selected == nil")
|
||||
.unwrap(),
|
||||
"the stock window is untouched"
|
||||
);
|
||||
press(&mut s, "J");
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("J")"#).unwrap(),
|
||||
"MOVEFORWARD"
|
||||
);
|
||||
assert_eq!(s.take_keybind_requests(), vec![KeybindRequest::Save(1)]);
|
||||
// The character set, both ways, through the page's own popup.
|
||||
let check = "BenillaOptionsFrameContainerBodyKeybindingsCharacterRowCheck";
|
||||
s.run(&format!("{check}:Click()")).unwrap();
|
||||
assert_eq!(s.current_binding_set(), 2);
|
||||
s.run(&format!("{check}:Click()")).unwrap();
|
||||
s.run("StaticPopup1Button1:Click()").unwrap();
|
||||
assert_eq!(
|
||||
s.current_binding_set(),
|
||||
1,
|
||||
"leaving the character set works"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
s.run("HideUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
|
||||
// The stock window over the same list.
|
||||
s.run("ShowUIPanel(KeyBindingFrame)").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return KeyBindingFrame:IsVisible()")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return KeyBindingFrameBinding1Header:IsVisible() \
|
||||
and KeyBindingFrameBinding1Header:GetText() == BINDING_HEADER_MOVEMENT"
|
||||
)
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return KeyBindingFrameBinding2Key1Button:GetText()")
|
||||
.unwrap(),
|
||||
s.eval::<String>(r#"return GetBindingText("BUTTON3", "KEY_")"#)
|
||||
.unwrap(),
|
||||
"Move and Steer's first key in the Key 1 column"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return KeyBindingFrameBinding3Key2Button:GetText()")
|
||||
.unwrap(),
|
||||
s.eval::<String>(r#"return GetBindingText("UP", "KEY_")"#)
|
||||
.unwrap(),
|
||||
"Move Forward's second key in the Key 2 column"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -283,7 +475,7 @@ fn unbind_reset_and_the_live_commit_replace_okay_cancel() {
|
||||
// Deviation: a bind saves at once, as the era panel commits, where 1.12 had Okay and Cancel;
|
||||
// closing the window keeps it.
|
||||
s.run(&format!("{ROW}9Key1Button:Click()")).unwrap();
|
||||
s.run(r#"KeyBindings_OnHostKey("G")"#).unwrap();
|
||||
press(&mut s, "G");
|
||||
assert_eq!(s.take_keybind_requests(), vec![KeybindRequest::Save(1)]);
|
||||
s.run("BenillaOptionsFrameCloseButton:Click()").unwrap();
|
||||
assert_eq!(
|
||||
@@ -384,8 +576,8 @@ fn search_surfaces_bindings_as_live_rows_under_the_redirect_head() {
|
||||
.unwrap());
|
||||
s.run("BenillaOptionsFrameContainerBodyKeybindSearch1Key1Button:Click()")
|
||||
.unwrap();
|
||||
assert!(s.bind_capture_armed());
|
||||
s.run(r#"KeyBindings_OnHostKey("H")"#).unwrap();
|
||||
assert!(armed(&s));
|
||||
press(&mut s, "H");
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("H")"#).unwrap(),
|
||||
"JUMP"
|
||||
@@ -474,8 +666,8 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() {
|
||||
on_page(&mut s);
|
||||
// Every section open: over 100 rows overflow the list's 19 slots.
|
||||
s.run(
|
||||
r#"for i = 1, table.getn(KeyBindingsPage.sections) do KeyBindings_ExpandSection(i, true) end
|
||||
KeyBindingsPage_Update()"#,
|
||||
r#"for i = 1, table.getn(BenillaKeyBindingsPage.sections) do BenillaKeyBindings_ExpandSection(i, true) end
|
||||
BenillaKeyBindingsPage_Update()"#,
|
||||
)
|
||||
.unwrap();
|
||||
const SF: &str = "BenillaOptionsFrameContainerBodyKeybindingsScrollFrame";
|
||||
@@ -536,8 +728,8 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() {
|
||||
);
|
||||
let set_all = |s: &UiScript, open: bool| {
|
||||
s.run(&format!(
|
||||
"for i = 1, table.getn(KeyBindingsPage.sections) do KeyBindings_ExpandSection(i, {}) end
|
||||
KeyBindingsPage_Update()",
|
||||
"for i = 1, table.getn(BenillaKeyBindingsPage.sections) do BenillaKeyBindings_ExpandSection(i, {}) end
|
||||
BenillaKeyBindingsPage_Update()",
|
||||
if open { "true" } else { "nil" }
|
||||
))
|
||||
.unwrap();
|
||||
@@ -603,9 +795,11 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() {
|
||||
after_capsule > after_name,
|
||||
"a spin over a CAPSULE bubbles to the page too: {after_name} -> {after_capsule}"
|
||||
);
|
||||
// While a capsule is armed a spin binds, never scrolls; the page's guard catches a spin the
|
||||
// host lets through.
|
||||
// While a capsule is armed a spin binds, never scrolls.
|
||||
s.run(&format!("{ROW}2Key1Button:Click()")).unwrap();
|
||||
let armed_command = s
|
||||
.eval::<String>(&format!("return {ROW}2Key1Button.commandName"))
|
||||
.unwrap();
|
||||
s.mouse_wheel(wx, wy, -1.0);
|
||||
assert_eq!(
|
||||
s.eval::<f64>(&format!("return FauxScrollFrame_GetOffset({SF})"))
|
||||
@@ -613,7 +807,12 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() {
|
||||
after_capsule,
|
||||
"armed: the wheel must not scroll"
|
||||
);
|
||||
s.run("KeyBindings_SetSelected(nil)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("MOUSEWHEELDOWN")"#)
|
||||
.unwrap(),
|
||||
armed_command,
|
||||
"armed: the spin binds"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -626,11 +825,14 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() {
|
||||
let mut s = harness();
|
||||
on_page(&mut s);
|
||||
|
||||
// The header row a command sits under: the last `HEADER_*` row above it.
|
||||
let category = |s: &mut UiScript, name: &str| {
|
||||
s.eval::<String>(&format!(
|
||||
r#"for i = 1, GetNumBindings() do
|
||||
local n, c = GetBinding(i)
|
||||
if n == "{name}" then return c end
|
||||
r#"local header
|
||||
for i = 1, GetNumBindings() do
|
||||
local n = GetBinding(i)
|
||||
if strsub(n, 1, 6) == "HEADER" then header = n end
|
||||
if n == "{name}" then return header end
|
||||
end"#
|
||||
))
|
||||
.unwrap()
|
||||
@@ -639,7 +841,7 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() {
|
||||
let name = format!("BONUSACTIONBUTTON{i}");
|
||||
assert_eq!(
|
||||
category(&mut s, &name),
|
||||
"BINDING_HEADER_ACTIONBAR",
|
||||
"HEADER_ACTIONBAR",
|
||||
"{name} files under the action bar, as 1.12 does"
|
||||
);
|
||||
let key = s
|
||||
|
||||
@@ -236,8 +236,8 @@ fn the_stock_ui_switch_is_a_dev_builds_alone() {
|
||||
assert!(layer_enabled_by(true, None) && layer_enabled_by(true, Some("0")));
|
||||
}
|
||||
|
||||
/// Booted without the layer, as `WOW_STOCK_UI=1` does: no layer file loads, the stock files raise
|
||||
/// nothing, and the host's calls into the layer are no-ops.
|
||||
/// Booted without the layer, as `WOW_STOCK_UI=1` does: no layer file loads and the stock files
|
||||
/// raise nothing.
|
||||
#[test]
|
||||
fn the_stock_ui_switch_boots_the_core_without_the_layer() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -250,7 +250,7 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() {
|
||||
// One global from each layer file that defines a new one.
|
||||
for name in [
|
||||
"BenillaScrollBar_Step",
|
||||
"KeyBindings_OnHostKey",
|
||||
"BenillaKeyBindings_OnKeyDown",
|
||||
"BenillaOptionsFrame_SelectCategory",
|
||||
"BenillaGameMenuButtonEditMode",
|
||||
"BENILLA_BAG_WAS_OPEN",
|
||||
@@ -263,13 +263,233 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() {
|
||||
}
|
||||
assert!(!s.eval::<bool>("return BenillaOptionsFrame ~= nil").unwrap());
|
||||
assert!(!s.eval::<bool>("return ZZORDER_SAW_LAYER").unwrap());
|
||||
// The host's calls into the layer.
|
||||
for body in [
|
||||
super::ERRORS_TOGGLE.to_string(),
|
||||
super::ERRORS_CLEAR.to_string(),
|
||||
super::host_key_capture("CTRL-J"),
|
||||
] {
|
||||
s.run(&body).unwrap_or_else(|e| panic!("{body}: {e}"));
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Types `line` into the chat box and sends it through the stock `ChatEdit_ParseText`
|
||||
/// (`ChatFrame.lua:2087`), counting the `HELP_TEXT_SIMPLE` lines it prints.
|
||||
fn type_line(s: &UiScript, line: &str) -> i64 {
|
||||
s.run(&format!(
|
||||
r#"HELPS = 0
|
||||
local box = ChatFrameEditBox
|
||||
box.chatFrame = box.chatFrame or DEFAULT_CHAT_FRAME
|
||||
local frame = box.chatFrame
|
||||
local add = frame.AddMessage
|
||||
frame.AddMessage = function(f, text, r, g, b, id)
|
||||
if text == HELP_TEXT_SIMPLE then HELPS = HELPS + 1 end
|
||||
return add(f, text, r, g, b, id)
|
||||
end
|
||||
box:SetText({line:?})
|
||||
ChatEdit_ParseText(box, 1)
|
||||
frame.AddMessage = add"#
|
||||
))
|
||||
.unwrap_or_else(|e| panic!("{line}: {e}"));
|
||||
s.eval("return HELPS").unwrap()
|
||||
}
|
||||
|
||||
/// The layer's player commands are plain `SlashCmdList` rows: `/reload` runs `ReloadUI()`,
|
||||
/// `/errors` (`/err`) toggles the log and `/errors clear` empties it. 1.12 has no `/convertraid`
|
||||
/// (the Raid tab's button converts, `RaidFrame.lua:63-71`), so it answers `HELP_TEXT_SIMPLE`, and
|
||||
/// the forwarding hook `SubmitChatInput` is gone.
|
||||
#[test]
|
||||
fn the_layers_commands_are_slash_rows_and_convertraid_is_unknown() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (mut s, failures) = production_load("slash", false);
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
assert!(s.eval::<bool>("return SubmitChatInput == nil").unwrap());
|
||||
|
||||
s.take_session_requests();
|
||||
assert_eq!(type_line(&s, "/reload"), 0);
|
||||
assert_eq!(
|
||||
s.take_session_requests(),
|
||||
[benilla_ui::script::SessionRequest::ReloadUi],
|
||||
"/reload is ReloadUI()"
|
||||
);
|
||||
assert!(
|
||||
s.take_chat_input().is_empty(),
|
||||
"nothing is handed to the host"
|
||||
);
|
||||
|
||||
assert_eq!(type_line(&s, "/errors"), 0);
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaScriptLogFrame:IsVisible()")
|
||||
.unwrap());
|
||||
assert_eq!(type_line(&s, "/err"), 0);
|
||||
assert!(!s
|
||||
.eval::<bool>("return BenillaScriptLogFrame:IsVisible()")
|
||||
.unwrap());
|
||||
s.run("BenillaScriptLog_Record('kept')").unwrap();
|
||||
assert_eq!(type_line(&s, "/errors clear "), 0);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(BenillaScriptLog.rows)")
|
||||
.unwrap(),
|
||||
0,
|
||||
"/errors clear empties the log"
|
||||
);
|
||||
|
||||
assert_eq!(type_line(&s, "/convertraid"), 1, "the unknown-command line");
|
||||
assert!(s.take_chat_input().is_empty());
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// In a player build the dev instruments are no `SlashCmdList` row, so a typed one answers
|
||||
/// `HELP_TEXT_SIMPLE` once and hands the host nothing to re-run; in a dev build each is a host row
|
||||
/// that hands the host its line.
|
||||
#[test]
|
||||
fn the_dev_commands_are_host_rows_in_a_dev_build_alone() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let lines = [
|
||||
"/chattest",
|
||||
"/shot",
|
||||
"/liquid",
|
||||
"/castvis",
|
||||
"/partytest ping",
|
||||
"/reaction",
|
||||
"/react Probe",
|
||||
];
|
||||
|
||||
let (mut s, failures) = production_load("devcmds-player", false);
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
crate::ui_chat::commands::register_dev_commands(&s, false);
|
||||
for line in lines {
|
||||
assert_eq!(
|
||||
type_line(&s, line),
|
||||
1,
|
||||
"{line}: the unknown-command line, once"
|
||||
);
|
||||
assert!(
|
||||
s.take_chat_input().is_empty(),
|
||||
"{line}: nothing queued to loop"
|
||||
);
|
||||
let cmd = line[1..].split(' ').next().unwrap();
|
||||
assert!(!s.has_slash_command(cmd), "{cmd} is no SlashCmdList row");
|
||||
}
|
||||
drop(s);
|
||||
|
||||
let (mut s, failures) = production_load("devcmds-dev", false);
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
crate::ui_chat::commands::register_dev_commands(&s, true);
|
||||
for line in lines {
|
||||
assert_eq!(type_line(&s, line), 0, "{line}");
|
||||
let queued = s.take_chat_input();
|
||||
assert_eq!(queued.len(), 1, "{line}: {queued:?}");
|
||||
// An alias queues under the command's first one, as the row's handler knows only that.
|
||||
let want = line.replacen("/react ", "/reaction ", 1);
|
||||
assert_eq!(queued[0].trim(), want, "{line}");
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The `/errors` log collects off the Lua error handler, ahead of the stock `_ERRORMESSAGE`
|
||||
/// (`BasicControls.xml:16`), which still shows the `ScriptErrors` dialog: an addon's runtime error,
|
||||
/// an error in one of the layer's own handlers, and one raised in the stock load before the layer
|
||||
/// loaded (the engine hands caught errors to the handler after the load).
|
||||
#[test]
|
||||
fn the_error_log_collects_off_the_error_handler() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let before = r#"
|
||||
local pre = CreateFrame("Button", "ZZPreLayer")
|
||||
pre:SetScript("OnClick", function() error("before the layer") end)
|
||||
pre:Click()
|
||||
"#;
|
||||
let (mut s, failures) = production_load_with("errlog", false, before, |root| {
|
||||
let dir = root.join("ZZErr");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("ZZErr.toc"), "## Interface: 11200\nerr.lua\n").unwrap();
|
||||
std::fs::write(
|
||||
dir.join("err.lua"),
|
||||
"ZZErrButton = CreateFrame('Button', 'ZZErrButton')\n\
|
||||
ZZErrButton:SetScript('OnClick', function() error('an addon boom') end)\n",
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert!(
|
||||
failures.iter().all(|f| f.contains("before the layer")),
|
||||
"load failures: {failures:#?}"
|
||||
);
|
||||
s.run("ZZErrButton:Click()").unwrap();
|
||||
// A layer handler raising: the window's OnShow calls this.
|
||||
s.run("BenillaOptionsFrame_UpdateScale = function() error('a layer boom') end")
|
||||
.unwrap();
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
s.dispatch_script_errors_to_handler();
|
||||
|
||||
let rows: Vec<String> = s
|
||||
.eval(
|
||||
"local out = {} \
|
||||
for _, row in ipairs(BenillaScriptLog.rows) do table.insert(out, row.message) end \
|
||||
return out",
|
||||
)
|
||||
.unwrap();
|
||||
for want in ["before the layer", "an addon boom", "a layer boom"] {
|
||||
assert!(
|
||||
rows.iter().any(|r| r.contains(want)),
|
||||
"{want:?} is in the log: {rows:#?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
s.eval::<bool>("return ScriptErrors:IsVisible()").unwrap(),
|
||||
"the stock handler still runs after the log"
|
||||
);
|
||||
let shown: String = s.eval("return ScriptErrors_Message:GetText()").unwrap();
|
||||
assert!(
|
||||
shown.contains("before the layer"),
|
||||
"the first error shows: {shown}"
|
||||
);
|
||||
// A repeat is one row with a count.
|
||||
s.run("ZZErrButton:Click() ZZErrButton:Click()").unwrap();
|
||||
s.dispatch_script_errors_to_handler();
|
||||
assert_eq!(
|
||||
s.eval::<i64>(
|
||||
"for _, row in ipairs(BenillaScriptLog.rows) do \
|
||||
if strfind(row.message, 'an addon boom', 1, 1) then return row.count end \
|
||||
end"
|
||||
)
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/// Every global the layer defines takes the `Benilla` prefix (a slash alias its `SLASH_BENILLA_`
|
||||
/// form, as `ChatEdit_ParseText` reads it), so no layer name meets a stock or an addon one; a stock
|
||||
/// global the layer redefines keeps its name and is not a new definition here.
|
||||
#[test]
|
||||
fn every_global_the_layer_defines_takes_the_benilla_prefix() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (s, failures) = production_load("prefix", false);
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
let log: Vec<String> = s.eval("return DEFINE_LOG").unwrap();
|
||||
let start = defined_at(&log, "BenillaScrollBar_Step");
|
||||
let end = defined_at(&log, "ZZORDER_SAW_LAYER");
|
||||
let dispatch = |n: &str| {
|
||||
n == "this"
|
||||
|| n == "event"
|
||||
|| n.strip_prefix("arg")
|
||||
.is_some_and(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit()))
|
||||
};
|
||||
let unprefixed: Vec<&String> = log[start..end]
|
||||
.iter()
|
||||
.filter(|n| {
|
||||
!(n.starts_with("Benilla")
|
||||
|| n.starts_with("BENILLA_")
|
||||
|| n.starts_with("SLASH_BENILLA_"))
|
||||
&& !dispatch(n)
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
unprefixed.is_empty(),
|
||||
"layer globals without the Benilla prefix: {unprefixed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -367,6 +367,7 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
// No sound during the load edge, as in the reference's `UI_Init` (`0x48fbfa` → `0x49016d`).
|
||||
silenced_ui_load(&mut script, |script| {
|
||||
let _ = load_ingame_ui(script, identity.as_ref(), &roster, version_check);
|
||||
crate::ui_chat::commands::register_dev_commands(script, crate::run_mode::dev_affordances());
|
||||
// The new Minimap's zoom indices from the persisted CVars, as the reference's minimap reset
|
||||
// copies each CVar into its live index; `Minimap:SetZoom` owns them from here.
|
||||
script.set_minimap_zoom(zoom.0, zoom.1);
|
||||
@@ -385,25 +386,22 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
},
|
||||
);
|
||||
});
|
||||
// Deviation: failed addons are reported in chat, because otherwise an addon that does not
|
||||
// load says nothing on screen. Counted from the deduplicated diagnostics log as `/errors`
|
||||
// lists them, after the whole load edge (handlers included); the VM is fresh, so every `Load`
|
||||
// row is this load's.
|
||||
let failed = script
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.filter(|d| d.kind == benilla_ui::script::diagnostics::DiagnosticKind::Load)
|
||||
.count();
|
||||
if failed > 0 {
|
||||
if let Some(mut chat) = world.get_resource_mut::<crate::ui_chat::ChatLog>() {
|
||||
// Deviation: each failed addon file is reported in chat, one line apiece, because otherwise
|
||||
// an addon that does not load says nothing on screen: a failure that never raised reaches no
|
||||
// error handler, so the `/errors` log cannot hold it. Read from the engine's deduplicated
|
||||
// record after the whole load edge (handlers included); the VM is fresh, so every `Load` row
|
||||
// is this load's.
|
||||
if let Some(mut chat) = world.get_resource_mut::<crate::ui_chat::ChatLog>() {
|
||||
for row in script
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.filter(|d| d.kind == benilla_ui::script::diagnostics::DiagnosticKind::Load)
|
||||
{
|
||||
let head = row.message.lines().next().unwrap_or_default();
|
||||
// Queued: the chat feed drains `ChatLog` once the UI is up.
|
||||
chat.push_event(crate::ui_chat::ChatEvent::text_only(
|
||||
crate::ui_chat::ChatEventKind::System,
|
||||
format!(
|
||||
"{failed} addon load {} — type /errors to see {}.",
|
||||
if failed == 1 { "failure" } else { "failures" },
|
||||
if failed == 1 { "it" } else { "them" }
|
||||
),
|
||||
format!("Addon load failure: {head}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,19 +26,6 @@ pub(super) fn layer_enabled_by(dev_build: bool, stock_ui: Option<&str>) -> bool
|
||||
!(dev_build && stock_ui == Some("1"))
|
||||
}
|
||||
|
||||
/// `/errors`: toggles the layer's script error log; a no-op when the run boots without the layer.
|
||||
pub(crate) const ERRORS_TOGGLE: &str =
|
||||
"if BenillaScriptLog_Toggle then BenillaScriptLog_Toggle() end";
|
||||
|
||||
/// `/errors clear`: empties the layer's script error log, or does nothing without the layer.
|
||||
pub(crate) const ERRORS_CLEAR: &str = "if BenillaScriptLog_Clear then BenillaScriptLog_Clear() end";
|
||||
|
||||
/// Hands a chord the layer's Keybindings page is capturing (`BenillaBindCapture`), a bind string
|
||||
/// such as `CTRL-J`, to its handler; a no-op without the layer.
|
||||
pub(crate) fn host_key_capture(chord: &str) -> String {
|
||||
format!("if KeyBindings_OnHostKey then KeyBindings_OnHostKey(\"{chord}\") end")
|
||||
}
|
||||
|
||||
/// Every entry in load order: the core's rows, none without an install, then the layer's.
|
||||
#[cfg(test)]
|
||||
pub(super) fn manifest_files() -> Vec<String> {
|
||||
|
||||
@@ -34,7 +34,6 @@ pub(crate) use session::VmMemo;
|
||||
// Not test-only: the addon harness loads the whole shipped interface under each addon.
|
||||
pub(crate) use manifest::load_default_ui;
|
||||
pub(crate) use manifest::load_ingame_ui;
|
||||
pub(crate) use manifest::{host_key_capture, ERRORS_CLEAR, ERRORS_TOGGLE};
|
||||
|
||||
/// Whether the pointer is over any UI (the egui dev overlay or a player-UI frame), combined by
|
||||
/// [`arbitrate_pointer_over_ui`]; gameplay reads it, so it is not the dev plugin's.
|
||||
|
||||
@@ -173,7 +173,7 @@ fn the_selected_row_wears_the_gold_wash_and_hover_runs_blue() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
// Scale 1, so rects and the pointer share coordinates; at 1024x768 the fit clamp stays above 1.
|
||||
s.run("ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@@ -275,7 +275,7 @@ fn the_selected_row_wears_the_gold_wash_and_hover_runs_blue() {
|
||||
fn the_ground_dim_draws_over_the_tile_and_clear_of_the_rope() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.run("ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
s.resolve();
|
||||
|
||||
@@ -514,7 +514,7 @@ fn a_track_press_seats_the_thumb_and_keeps_dragging() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness_on(audio_harness());
|
||||
s.set_cvar_host("MasterVolume", "0.1");
|
||||
s.run("ERA_WINDOW_SCALE = 1").unwrap(); // pointer and rects share coordinates
|
||||
s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap(); // pointer and rects share coordinates
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
s.run("BenillaOptionsFrameCategoryListRowAudio:Click()")
|
||||
.unwrap();
|
||||
@@ -1839,7 +1839,7 @@ fn defaults_resets_the_controls_page_to_registered_defaults() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Run a few frames so the page body's fit converges: `OptionsScroll_Fit` reads the previous
|
||||
/// Run a few frames so the page body's fit converges: `BenillaOptionsScroll_Fit` reads the previous
|
||||
/// frame's rects.
|
||||
fn settle(s: &mut UiScript) {
|
||||
for _ in 0..4 {
|
||||
@@ -1942,11 +1942,14 @@ fn a_broad_search_scrolls_the_page_instead_of_overflowing_it() {
|
||||
clip.bottom
|
||||
);
|
||||
|
||||
// Widget coordinates here, not the extract's pixels: the window carries `ERA_WINDOW_SCALE`.
|
||||
// Widget coordinates here, not the extract's pixels: the window carries `BENILLA_ERA_WINDOW_SCALE`.
|
||||
let sf_bottom: f32 = s
|
||||
.eval("return BenillaOptionsFrameContainerScroll:GetBottom()")
|
||||
.unwrap();
|
||||
let tail = |s: &UiScript| -> f32 { s.eval("return OptionsScroll_ContentBottom()").unwrap() };
|
||||
let tail = |s: &UiScript| -> f32 {
|
||||
s.eval("return BenillaOptionsScroll_ContentBottom()")
|
||||
.unwrap()
|
||||
};
|
||||
assert!(
|
||||
tail(&s) < sf_bottom,
|
||||
"the tail starts below the fold ({} vs {sf_bottom})",
|
||||
@@ -2068,7 +2071,7 @@ fn the_page_scroll_bar_wears_the_trough_with_its_arrows_in_the_sockets() {
|
||||
|
||||
// ── The row tooltips ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Hover a row's label half, 60 units in at mid-height. Callers pin `ERA_WINDOW_SCALE = 1` first,
|
||||
/// Hover a row's label half, 60 units in at mid-height. Callers pin `BENILLA_ERA_WINDOW_SCALE = 1` first,
|
||||
/// so the row's rect and the pointer share coordinates.
|
||||
fn hover_label(s: &mut UiScript, frame: &str) {
|
||||
s.resolve();
|
||||
@@ -2112,7 +2115,7 @@ fn a_hovered_row_raises_its_1_12_description_on_the_era_seat() {
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
s.run("OPTION_TOOLTIP_GAMEFIELD_DESELECT = \"Checking this will prevent the deselection.\"")
|
||||
.unwrap();
|
||||
s.run("ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
|
||||
let row = "BenillaOptionsFrameContainerBodyControlsRowStickyTarget";
|
||||
@@ -2181,7 +2184,7 @@ fn a_row_with_no_1_12_string_raises_no_plate() {
|
||||
let mut s = harness_on(audio_harness());
|
||||
s.run("OPTION_TOOLTIP_GAMEFIELD_DESELECT = \"Sticky's own description.\"")
|
||||
.unwrap();
|
||||
s.run("ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
hover_label(
|
||||
&mut s,
|
||||
@@ -2201,10 +2204,10 @@ fn a_row_with_no_1_12_string_raises_no_plate() {
|
||||
);
|
||||
|
||||
// Sticky's plate up, then straight into the mute row with no OnLeave in between.
|
||||
s.run("OptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowStickyTarget, 1)")
|
||||
s.run("BenillaOptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowStickyTarget, 1)")
|
||||
.unwrap();
|
||||
assert!(s.eval::<bool>("return GameTooltip:IsVisible()").unwrap());
|
||||
s.run("OptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowAutoLoot, 1)")
|
||||
s.run("BenillaOptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowAutoLoot, 1)")
|
||||
.unwrap();
|
||||
assert!(
|
||||
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
|
||||
@@ -2232,7 +2235,7 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
|
||||
let listing: String = s
|
||||
.eval(
|
||||
"local out = {} \
|
||||
for page, rows in pairs(OPTIONS_PAGE_ROWS) do \
|
||||
for page, rows in pairs(BENILLA_OPTIONS_PAGE_ROWS) do \
|
||||
for _, rkey in ipairs(rows) do \
|
||||
local row = getglobal(\"BenillaOptionsFrameContainerBody\" .. page .. rkey) \
|
||||
table.insert(out, page .. rkey .. \"=\" .. (row.tip or \"\")) \
|
||||
@@ -2320,7 +2323,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
|
||||
let mut s = harness_on(audio_harness());
|
||||
// Stand-in strings for every key the rows name.
|
||||
s.run(
|
||||
"for page, rows in pairs(OPTIONS_PAGE_ROWS) do \
|
||||
"for page, rows in pairs(BENILLA_OPTIONS_PAGE_ROWS) do \
|
||||
for _, rkey in ipairs(rows) do \
|
||||
local row = getglobal(\"BenillaOptionsFrameContainerBody\" .. page .. rkey) \
|
||||
if row.tip then setglobal(row.tip, \"described: \" .. rkey) end \
|
||||
@@ -2328,7 +2331,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
|
||||
end",
|
||||
)
|
||||
.unwrap();
|
||||
s.run("ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap();
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
|
||||
let mut raised = 0;
|
||||
@@ -2346,7 +2349,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
|
||||
.unwrap();
|
||||
let rows: String = s
|
||||
.eval(&format!(
|
||||
"return table.concat(OPTIONS_PAGE_ROWS.{page}, \",\")"
|
||||
"return table.concat(BENILLA_OPTIONS_PAGE_ROWS.{page}, \",\")"
|
||||
))
|
||||
.unwrap();
|
||||
for rkey in rows.split(',') {
|
||||
@@ -2387,7 +2390,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
|
||||
}
|
||||
|
||||
/// 1.12's AdvancedOptionsCombatText box as saved-global rows: a click writes the global, never a
|
||||
/// CVar, and runs the family's apply, `CombatText_UpdateOrLoad`.
|
||||
/// CVar, and runs the family's apply, `BenillaCombatText_UpdateOrLoad`.
|
||||
#[test]
|
||||
fn the_combat_page_writes_saved_variable_globals_and_applies_them() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -3236,7 +3239,7 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() {
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
|
||||
let keys: Vec<String> = s
|
||||
.eval::<String>("return table.concat(OPTIONS_CATEGORY_KEYS, \",\")")
|
||||
.eval::<String>("return table.concat(BENILLA_OPTIONS_CATEGORY_KEYS, \",\")")
|
||||
.unwrap()
|
||||
.split(',')
|
||||
.map(str::to_string)
|
||||
@@ -3245,8 +3248,8 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() {
|
||||
for key in &keys {
|
||||
let has_rows = s
|
||||
.eval::<bool>(&format!(
|
||||
"return OPTIONS_PAGE_ROWS[\"{key}\"] ~= nil and \
|
||||
getn(OPTIONS_PAGE_ROWS[\"{key}\"]) > 0"
|
||||
"return BENILLA_OPTIONS_PAGE_ROWS[\"{key}\"] ~= nil and \
|
||||
getn(BENILLA_OPTIONS_PAGE_ROWS[\"{key}\"]) > 0"
|
||||
))
|
||||
.unwrap();
|
||||
assert!(
|
||||
@@ -3966,7 +3969,7 @@ fn the_two_option_tabs_fit_their_labels_at_the_drawn_scale() {
|
||||
assert_eq!(
|
||||
num(&mut s, "BenillaOptionsFrame:GetScale()"),
|
||||
0.78,
|
||||
"ERA_WINDOW_SCALE"
|
||||
"BENILLA_ERA_WINDOW_SCALE"
|
||||
);
|
||||
|
||||
for tab in ["BenillaOptionsFrameGameTab", "BenillaOptionsFrameAddOnsTab"] {
|
||||
@@ -4156,7 +4159,7 @@ fn every_registered_reference_cvar_has_a_row_on_our_own_window() {
|
||||
let ours: std::collections::HashSet<String> = s
|
||||
.eval::<Vec<String>>(
|
||||
"local out = {} \
|
||||
for page, rows in pairs(OPTIONS_PAGE_ROWS) do \
|
||||
for page, rows in pairs(BENILLA_OPTIONS_PAGE_ROWS) do \
|
||||
for _, rkey in ipairs(rows) do \
|
||||
local row = getglobal(\"BenillaOptionsFrameContainerBody\" .. page .. rkey) \
|
||||
if row.cvar then table.insert(out, row.cvar) end \
|
||||
@@ -4396,7 +4399,7 @@ fn the_guild_line_greys_with_player_names_and_the_follow_speed_with_the_style()
|
||||
.unwrap()
|
||||
};
|
||||
assert!(speed_thumb(&mut s));
|
||||
s.run("SetCVar(\"cameraSmoothStyle\", \"0\") OptionsPage_Refresh(\"Controls\")")
|
||||
s.run("SetCVar(\"cameraSmoothStyle\", \"0\") BenillaOptionsPage_Refresh(\"Controls\")")
|
||||
.unwrap();
|
||||
assert!(
|
||||
!speed_thumb(&mut s),
|
||||
@@ -4410,7 +4413,7 @@ fn the_guild_line_greys_with_player_names_and_the_follow_speed_with_the_style()
|
||||
:IsMouseEnabled()"
|
||||
)
|
||||
.unwrap());
|
||||
s.run("SetCVar(\"cameraSmoothStyle\", \"1\") OptionsPage_Refresh(\"Controls\")")
|
||||
s.run("SetCVar(\"cameraSmoothStyle\", \"1\") BenillaOptionsPage_Refresh(\"Controls\")")
|
||||
.unwrap();
|
||||
assert!(speed_thumb(&mut s));
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
@@ -1964,3 +1964,75 @@ fn the_layer_does_not_grow() {
|
||||
"assets/ui holds the layer's manifest and its files, nothing else"
|
||||
);
|
||||
}
|
||||
|
||||
/// The layer speaks the 1.12 dialect: a handler body is the chunk itself (`0x704c70`), so it reads
|
||||
/// its frame from `this`, never `self`; the loader reads no `function=`; the strata table
|
||||
/// (`0x8119f8`) has no `BLIZZARD`; `SetDrawLayer` (`0x79a780`) takes a layer name alone; and a
|
||||
/// button's state textures take a texture, never a colour (`0x781970`).
|
||||
#[test]
|
||||
fn the_layer_speaks_the_1_12_dialect() {
|
||||
fn handlers(el: &benilla_ui::framexml::Element, out: &mut Vec<(String, String)>) {
|
||||
if el.tag == "Scripts" {
|
||||
for h in &el.children {
|
||||
out.push((h.tag.clone(), h.body.clone()));
|
||||
assert!(
|
||||
h.attr("function").is_none(),
|
||||
"<{} function=…> is no 1.12 form",
|
||||
h.tag
|
||||
);
|
||||
}
|
||||
}
|
||||
for c in &el.children {
|
||||
handlers(c, out);
|
||||
}
|
||||
}
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
let files = super::addons::Addon::layer().toc.files;
|
||||
assert!(files.len() >= 8, "the layer's toc lists {files:?}");
|
||||
let word = |text: &str, w: &str| {
|
||||
text.match_indices(w).any(|(i, _)| {
|
||||
let before = text[..i].chars().next_back();
|
||||
let after = text[i + w.len()..].chars().next();
|
||||
!before.is_some_and(|c| c.is_alphanumeric() || c == '_')
|
||||
&& !after.is_some_and(|c| c.is_alphanumeric() || c == '_')
|
||||
})
|
||||
};
|
||||
for file in files {
|
||||
let text = std::fs::read_to_string(dir.join(&file)).expect("read");
|
||||
let doc = benilla_ui::framexml::parse(&text).expect("parses");
|
||||
let mut found = Vec::new();
|
||||
for item in &doc.items {
|
||||
use benilla_ui::framexml::TopLevel;
|
||||
if let TopLevel::Template(el) | TopLevel::Instance(el) = item {
|
||||
handlers(el, &mut found);
|
||||
}
|
||||
}
|
||||
for (tag, body) in found {
|
||||
assert!(!word(&body, "self"), "{file}: <{tag}> reads `self`: {body}");
|
||||
}
|
||||
assert!(!text.contains("BLIZZARD"), "{file}: BLIZZARD strata");
|
||||
for line in text.lines().filter(|l| l.contains("SetDrawLayer(")) {
|
||||
assert_eq!(
|
||||
line.matches(',').count(),
|
||||
0,
|
||||
"{file}: SetDrawLayer takes the layer alone: {line}"
|
||||
);
|
||||
}
|
||||
for verb in [
|
||||
"SetNormalTexture(",
|
||||
"SetPushedTexture(",
|
||||
"SetHighlightTexture(",
|
||||
"SetDisabledTexture(",
|
||||
"SetCheckedTexture(",
|
||||
"SetDisabledCheckedTexture(",
|
||||
] {
|
||||
for line in text.lines().filter(|l| l.contains(verb)) {
|
||||
let args = line.split(verb).nth(1).unwrap_or_default();
|
||||
assert!(
|
||||
!args.trim_start().starts_with(|c: char| c.is_ascii_digit()),
|
||||
"{file}: {verb} takes a texture, never a colour: {line}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,8 +865,9 @@ fn the_login_one_shots_wait_for_the_in_game_ui() {
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// An addon that fails to load without raising (a `.toc` naming a file the package lacks) is kept
|
||||
/// in the log, readable from Lua by the error window, and announced in chat.
|
||||
/// An addon that fails to load without raising (a `.toc` naming a file the package lacks) reaches
|
||||
/// no Lua error handler, so the `/errors` log (which collects off the handler) does not hold it:
|
||||
/// the engine's record keeps it, and a chat line names it.
|
||||
#[test]
|
||||
fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -914,57 +915,20 @@ fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() {
|
||||
row.message
|
||||
);
|
||||
|
||||
// 2. Readable from Lua through the window's own reads.
|
||||
let count: i64 = script
|
||||
.eval("local shown = BenillaGetNumScriptErrors() return shown")
|
||||
.expect("BenillaGetNumScriptErrors is installed");
|
||||
assert!(count >= 1, "the window's own read sees it");
|
||||
let seen: String = script
|
||||
.eval(
|
||||
"local text = '' \
|
||||
for i = 1, BenillaGetNumScriptErrors() do \
|
||||
local seq, kind, message = BenillaGetScriptErrorInfo(i) \
|
||||
if kind == 'load' then text = message end \
|
||||
end \
|
||||
return text",
|
||||
)
|
||||
.expect("BenillaGetScriptErrorInfo is installed");
|
||||
assert!(
|
||||
seen.contains("AaMissing"),
|
||||
"the window walks the log and finds it: {seen:?}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
script
|
||||
.eval::<bool>("return BenillaScriptLogFrame ~= nil")
|
||||
.expect("eval"),
|
||||
"ScriptLogFrame.xml loaded and built the window"
|
||||
);
|
||||
|
||||
// The repaint runs over a real row, so a nil global anywhere on its path raises here.
|
||||
// 2. Not in the `/errors` log: it never reached the error handler.
|
||||
script
|
||||
.eval::<()>("BenillaScriptLog_Update() return nil")
|
||||
.expect("the window repaints over a real log without raising");
|
||||
// Some row shows it, not necessarily row 1: the log also holds the warnings a world entry
|
||||
// raises before any addon loads.
|
||||
let row_labels: Vec<String> = (1..=13)
|
||||
.filter_map(|i| {
|
||||
script
|
||||
.eval::<Option<String>>(&format!("return BenillaScriptLogRow{i}Label:GetText()"))
|
||||
.expect("eval")
|
||||
})
|
||||
.collect();
|
||||
.expect("the window repaints without raising");
|
||||
assert!(
|
||||
row_labels.iter().any(|l| l.contains("AaMissing")),
|
||||
"a row shows the failure, trimmed to the row's width: {row_labels:?}"
|
||||
);
|
||||
let summary: String = script
|
||||
.eval::<Option<String>>("return BenillaScriptLogSummary:GetText()")
|
||||
.expect("eval")
|
||||
.unwrap_or_default();
|
||||
assert!(
|
||||
summary.contains("problem"),
|
||||
"the summary line counted them: {summary:?}"
|
||||
!script
|
||||
.eval::<bool>(
|
||||
"for _, row in ipairs(BenillaScriptLog.rows) do \
|
||||
if strfind(row.message, 'AaMissing', 1, 1) then return true end \
|
||||
end \
|
||||
return false"
|
||||
)
|
||||
.expect("eval"),
|
||||
"a failure no handler saw is not in the handler-fed log"
|
||||
);
|
||||
|
||||
// 3. No dialog, since nothing raised: the reference answers an absent or unparseable file
|
||||
@@ -977,12 +941,14 @@ fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() {
|
||||
through `_ERRORMESSAGE` and through every addon handler that replaces it"
|
||||
);
|
||||
|
||||
// 4. Deviation: a chat line tells the player to look, where the reference stays silent,
|
||||
// because an unannounced log goes unread.
|
||||
assert_eq!(
|
||||
world.resource::<crate::ui_chat::ChatLog>().pending_len(),
|
||||
1,
|
||||
"world entry queued the 'N addon load failures — type /errors' line"
|
||||
// 4. Deviation: a chat line names the failure, where the reference stays silent, because
|
||||
// otherwise it reaches no screen.
|
||||
let lines = world.resource::<crate::ui_chat::ChatLog>().pending_lines();
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l.starts_with("Addon load failure: ") && l.contains("AaMissing")),
|
||||
"world entry queued the failure's line: {lines:?}"
|
||||
);
|
||||
|
||||
drop(world);
|
||||
|
||||
@@ -816,7 +816,7 @@ mod ppem_tests {
|
||||
assert_eq!(e.ppem(12.0), 12, "an exact size is itself");
|
||||
assert_eq!(e.ppem(12.48), 12, "…and a fractional one rounds, not snaps");
|
||||
assert_eq!(e.ppem(12.5), 13);
|
||||
// `ERA_WINDOW_SCALE` (0.78), which the Options window and the Game Menu wear.
|
||||
// `BENILLA_ERA_WINDOW_SCALE` (0.78), which the Options window and the Game Menu wear.
|
||||
assert_eq!(e.ppem(16.0 * 0.78), 12);
|
||||
e.dpi = 2.0;
|
||||
assert_eq!(e.ppem(12.0), 24);
|
||||
|
||||
@@ -407,7 +407,7 @@ mod measure_fits_render {
|
||||
"Main Menu",
|
||||
];
|
||||
|
||||
/// `ERA_WINDOW_SCALE` of benilla's game menu and options window: fonts land on no whole size.
|
||||
/// `BENILLA_ERA_WINDOW_SCALE` of benilla's game menu and options window: fonts land on no whole size.
|
||||
const ERA: f32 = 0.78;
|
||||
|
||||
fn spec(h: f32) -> FontSpec<'static> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use mlua::{Function, ObjectLike, Table, Value};
|
||||
use mlua::{Function, ObjectLike, Table};
|
||||
|
||||
use crate::framexml::Element;
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Loader<'_> {
|
||||
// function: `SetScript` (`0x7025c0`) tests only NULL and the first byte
|
||||
// (`0x7025f8`), and nothing trims the body (`0x6f29d0`). The stock blanks
|
||||
// (`BuffFrame.xml:101`) are whitespace, so their `GetScript` answers a function.
|
||||
let cleared = handler.body.is_empty() && handler.attr("function").is_none();
|
||||
let cleared = handler.body.is_empty();
|
||||
let func = if cleared {
|
||||
None
|
||||
} else {
|
||||
@@ -77,11 +77,9 @@ impl Loader<'_> {
|
||||
onload
|
||||
}
|
||||
|
||||
/// Compile a handler body, or resolve its `function=` global: not 1.12, whose loader never
|
||||
/// reads the attribute. A 1.12 body takes no arguments and reads its frame from `this`; the
|
||||
/// `self` parameter is not 1.12 either (our `assets/ui` bodies use it) and falls back to `this`
|
||||
/// for a hook that calls a captured handler bare. The prologue shares the body's first line,
|
||||
/// so line numbers match the reference, which loads the raw body as the chunk (`0x704c70`).
|
||||
/// Compile a handler body as the chunk itself, as the reference loads it (`0x704c70`): it takes
|
||||
/// no arguments and reads its frame from `this`, so a `self` in it is an ordinary global. The
|
||||
/// loader reads no other attribute of the element (no `function=`).
|
||||
pub(super) fn compile_handler(
|
||||
&mut self,
|
||||
handler: &Element,
|
||||
@@ -91,41 +89,24 @@ impl Loader<'_> {
|
||||
) -> Option<Function> {
|
||||
// The raw body, never trimmed: a whitespace body is a real empty chunk (`apply_scripts`).
|
||||
let body = handler.body.as_str();
|
||||
if !body.is_empty() {
|
||||
let src = format!(
|
||||
"return function(self, ...) if self == nil then self = this end {body}\nend"
|
||||
);
|
||||
match self
|
||||
.lua()
|
||||
.load(&src)
|
||||
.set_name(format!("{owner}:{name}"))
|
||||
.set_mode(mlua::ChunkMode::Text)
|
||||
.eval::<Function>()
|
||||
{
|
||||
Ok(f) => return Some(f),
|
||||
Err(e) => {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: compiling <{name}>: {e}"));
|
||||
return None;
|
||||
}
|
||||
if body.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match self
|
||||
.lua()
|
||||
.load(body)
|
||||
.set_name(format!("{owner}:{name}"))
|
||||
.set_mode(mlua::ChunkMode::Text)
|
||||
.into_function()
|
||||
{
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: compiling <{name}>: {e}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
if let Some(global) = handler.attr("function") {
|
||||
match self.lua().globals().get::<Value>(global) {
|
||||
Ok(Value::Function(f)) => return Some(f),
|
||||
_ => {
|
||||
self.warn_once(
|
||||
&format!("fn:{global}"),
|
||||
format!(
|
||||
"{dbg}: <{name} function=\"{global}\">: no such global function (yet)"
|
||||
),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Fire a captured `OnLoad` under the event path's convention, `this` set and restored
|
||||
|
||||
@@ -311,7 +311,7 @@ mod loader_tests {
|
||||
<Frames>
|
||||
<Frame name="$parentChild">
|
||||
<Scripts>
|
||||
<OnLoad>table.insert(loadorder, "child"); ChildLoaded = self:GetName()</OnLoad>
|
||||
<OnLoad>table.insert(loadorder, "child"); ChildLoaded = this:GetName()</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
@@ -330,7 +330,7 @@ mod loader_tests {
|
||||
assert!(s.eval::<bool>("return MyFrameChild ~= nil").unwrap());
|
||||
assert!(s.eval::<bool>("return MyFrameTex ~= nil").unwrap());
|
||||
|
||||
// The child's handler uses `self`, the parent's `this`; both work.
|
||||
// Both handlers read their frame from `this`.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return ChildLoaded").unwrap(),
|
||||
"MyFrameChild"
|
||||
@@ -2409,16 +2409,17 @@ mod chunk_name_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The 1.12 hook idiom: an addon captures a handler with `GetScript` and calls it with no
|
||||
/// arguments, the frame passed only as `this`; `Loader::compile_handler` falls back to `this`.
|
||||
/// A handler body is the chunk itself (`0x704c70`): it takes no arguments and reads its frame
|
||||
/// from `this`, so `self` in it is an ordinary global, and the 1.12 hook idiom (an addon
|
||||
/// captures a handler with `GetScript` and calls it bare) reaches the same frame.
|
||||
#[test]
|
||||
fn a_script_captured_and_called_with_no_arguments_still_sees_its_frame() {
|
||||
fn a_handler_body_is_the_chunk_and_self_is_a_global() {
|
||||
let s = UiScript::new().unwrap();
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Frame name="Hooked" hidden="true">
|
||||
<Scripts>
|
||||
<OnShow>SEEN = self:GetName()</OnShow>
|
||||
<OnShow>SEEN = this:GetName() SELF_SEEN = self</OnShow>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>"#,
|
||||
@@ -2426,11 +2427,15 @@ mod chunk_name_tests {
|
||||
let report = load_in(&s, &doc, "Test.xml", &no_files);
|
||||
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||
|
||||
// The engine's call passes `self` and sets `this`.
|
||||
s.run("SEEN = nil Hooked:Show()").unwrap();
|
||||
s.run("self = 'the global' SEEN = nil Hooked:Show()")
|
||||
.unwrap();
|
||||
assert_eq!(s.eval::<String>("return SEEN").unwrap(), "Hooked");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SELF_SEEN").unwrap(),
|
||||
"the global",
|
||||
"`self` is no parameter of the body"
|
||||
);
|
||||
|
||||
// The addon's: the captured script called bare, with only `this` set.
|
||||
s.run(
|
||||
"SEEN = nil \
|
||||
local original = Hooked:GetScript(\"OnShow\") \
|
||||
@@ -2439,11 +2444,29 @@ mod chunk_name_tests {
|
||||
this = nil",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SEEN").unwrap(),
|
||||
"Hooked",
|
||||
"the reference's own no-argument contract must reach the same frame"
|
||||
assert_eq!(s.eval::<String>("return SEEN").unwrap(), "Hooked");
|
||||
}
|
||||
|
||||
/// The loader reads no `function=` attribute: a handler element with no body clears the
|
||||
/// handler, whatever global the attribute names.
|
||||
#[test]
|
||||
fn a_function_attribute_binds_nothing() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run("function NamedHandler() RAN = true end").unwrap();
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Frame name="Named" hidden="true">
|
||||
<Scripts><OnShow function="NamedHandler"/></Scripts>
|
||||
</Frame>
|
||||
</Ui>"#,
|
||||
);
|
||||
let report = load_in(&s, &doc, "Test.xml", &no_files);
|
||||
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||
s.run("Named:Show()").unwrap();
|
||||
assert!(s.eval::<bool>("return RAN == nil").unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return Named:GetScript(\"OnShow\") == nil")
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,13 +108,13 @@ mod tests {
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnDragStart", function(self, button) drag_starts = drag_starts + 1; drag_button = button end)
|
||||
a:SetScript("OnDragStop", function(self) drag_stops = drag_stops + 1 end)
|
||||
a:SetScript("OnClick", function(self) click_a = click_a + 1 end)
|
||||
a:SetScript("OnDragStart", function() local self, button = this, arg1 drag_starts = drag_starts + 1; drag_button = button end)
|
||||
a:SetScript("OnDragStop", function() local self = this drag_stops = drag_stops + 1 end)
|
||||
a:SetScript("OnClick", function() local self = this click_a = click_a + 1 end)
|
||||
local b = CreateFrame("Frame", "B")
|
||||
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true)
|
||||
b:SetScript("OnReceiveDrag", function(self) receives = receives + 1 end)
|
||||
b:SetScript("OnClick", function(self) click_b = click_b + 1 end)
|
||||
b:SetScript("OnReceiveDrag", function() local self = this receives = receives + 1 end)
|
||||
b:SetScript("OnClick", function() local self = this click_b = click_b + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -170,7 +170,7 @@ mod tests {
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnClick", function(self) clicks = clicks + 1 end)
|
||||
a:SetScript("OnClick", function() local self = this clicks = clicks + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -192,7 +192,7 @@ mod tests {
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetScript("OnClick", function(self) clicks = clicks + 1 end)
|
||||
a:SetScript("OnClick", function() local self = this clicks = clicks + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -271,7 +271,7 @@ mod tests {
|
||||
heard, name, quality = 0, nil, nil
|
||||
local f = CreateFrame("Frame", "Listener")
|
||||
f:RegisterEvent("DELETE_ITEM_CONFIRM")
|
||||
f:SetScript("OnEvent", function(self, event, n, q) heard = heard + 1; name = n; quality = q end)
|
||||
f:SetScript("OnEvent", function() local self, event, n, q = this, event, arg1, arg2 heard = heard + 1; name = n; quality = q end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The script error log: this session's script errors, addon load failures and warnings, kept
|
||||
//! for the player to read. The reference has no such log: its `_ERRORMESSAGE` shows only the first
|
||||
//! message of a burst, and a load failure that never raises reaches no screen. Dispatch and
|
||||
//! `_ERRORMESSAGE` are unchanged by it.
|
||||
//! The engine's record of this session's script errors, addon load failures and warnings, for the
|
||||
//! host (the world-entry load-failure lines, the addon harness) and dev instruments; no Lua global
|
||||
//! reads it. The layer's `/errors` window keeps its own log off the Lua error handler, as a 1.12
|
||||
//! addon does. Dispatch and `_ERRORMESSAGE` are unchanged by it.
|
||||
//!
|
||||
//! A repeat bumps its row's count and keeps its place, rows are oldest first, and past
|
||||
//! [`DIAGNOSTIC_LOG_CAP`] distinct messages the oldest is evicted; `seq` is never reused, so an
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use mlua::{IntoLua, Lua, MultiValue, Value};
|
||||
use mlua::Lua;
|
||||
|
||||
use super::Model;
|
||||
|
||||
@@ -91,19 +91,16 @@ impl DiagnosticLog {
|
||||
}
|
||||
|
||||
/// Retained rows.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.rows.len()
|
||||
}
|
||||
|
||||
/// Distinct failures recorded this session, evicted and cleared rows included.
|
||||
/// Distinct failures recorded this session, evicted rows included.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn total(&self) -> u64 {
|
||||
self.seq
|
||||
}
|
||||
|
||||
/// Forget the rows but not the numbering, so no `#N` ever names two failures.
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.rows.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
@@ -112,17 +109,6 @@ impl super::UiScript {
|
||||
self.model_ref().diagnostics.rows().cloned().collect()
|
||||
}
|
||||
|
||||
/// `(retained rows, distinct failures ever recorded)`.
|
||||
pub fn diagnostic_counts(&self) -> (usize, u64) {
|
||||
let m = self.model_ref();
|
||||
(m.diagnostics.len(), m.diagnostics.total())
|
||||
}
|
||||
|
||||
/// Forget the retained rows; the numbering carries on.
|
||||
pub fn clear_diagnostics(&self) {
|
||||
self.model_mut().diagnostics.clear();
|
||||
}
|
||||
|
||||
/// Retain an addon load failure that never raised (a missing file, an unparseable document, a
|
||||
/// dependency cycle or missing dependency, a broken `Bindings.xml`). It is not dispatched to
|
||||
/// `geterrorhandler()`: the reference logs such a failure and shows nothing. The caller still
|
||||
@@ -159,50 +145,6 @@ pub(crate) fn record_warning(lua: &Lua, msg: &str) {
|
||||
.record(DiagnosticKind::Warning, msg);
|
||||
}
|
||||
|
||||
/// Register the error-log reads `BenillaScriptLogFrame` polls, `Benilla`-prefixed as they are not
|
||||
/// 1.12 API (`tests::reference_surface` enforces the prefix).
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// BenillaGetNumScriptErrors() → retained, totalEverRecorded
|
||||
lua.globals().set(
|
||||
"BenillaGetNumScriptErrors",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok((model.diagnostics.len(), model.diagnostics.total()))
|
||||
})?,
|
||||
)?;
|
||||
// BenillaGetScriptErrorInfo(index) → seq, kind, message, count; 1-based, oldest first, and
|
||||
// nothing for an out-of-range index, so a row gone mid-repaint reads as gone.
|
||||
lua.globals().set(
|
||||
"BenillaGetScriptErrorInfo",
|
||||
lua.create_function(|lua, index: i64| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let Some(row) = usize::try_from(index)
|
||||
.ok()
|
||||
.and_then(|i| i.checked_sub(1))
|
||||
.and_then(|i| model.diagnostics.rows().nth(i))
|
||||
else {
|
||||
return Ok(MultiValue::new());
|
||||
};
|
||||
Ok(MultiValue::from_vec(vec![
|
||||
Value::Integer(row.seq as i64),
|
||||
lua.create_string(row.kind.tag())?.into_lua(lua)?,
|
||||
lua.create_string(&row.message)?.into_lua(lua)?,
|
||||
Value::Integer(i64::from(row.count)),
|
||||
]))
|
||||
})?,
|
||||
)?;
|
||||
lua.globals().set(
|
||||
"BenillaClearScriptErrors",
|
||||
lua.create_function(|lua, ()| {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.diagnostics
|
||||
.clear();
|
||||
Ok(())
|
||||
})?,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -308,19 +250,4 @@ mod tests {
|
||||
);
|
||||
assert_eq!(log.total() as usize, DIAGNOSTIC_LOG_CAP + 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_forgets_rows_but_not_the_numbering() {
|
||||
let mut log = DiagnosticLog::default();
|
||||
log.record(DiagnosticKind::Error, "a");
|
||||
log.record(DiagnosticKind::Error, "b");
|
||||
log.clear();
|
||||
assert_eq!(log.len(), 0);
|
||||
log.record(DiagnosticKind::Error, "c");
|
||||
assert_eq!(
|
||||
log.rows().next().unwrap().seq,
|
||||
3,
|
||||
"a number never names two different failures"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use mlua::{Function, Lua, MultiValue, Table, Value};
|
||||
use mlua::{Function, Lua, Table, Value};
|
||||
|
||||
use super::{Model, ScriptValue, REG_SCRIPTS};
|
||||
use crate::script::object::frame_wrapper;
|
||||
@@ -230,8 +230,8 @@ fn arg_name(i: usize) -> Cow<'static, str> {
|
||||
}
|
||||
|
||||
/// The calling convention, shared by [`fire`] and the loader's `OnLoad`: sets `this`, `event` and
|
||||
/// `arg1..argN`, passes `(self[, event], extra...)`, and restores the globals after the call, even
|
||||
/// on error, so nested fires are safe.
|
||||
/// `arg1..argN` and calls the handler with no arguments, as `0x705199` calls `lua_pcall` with
|
||||
/// zero; the globals are restored after the call, even on error, so nested fires are safe.
|
||||
pub(crate) fn invoke_with_globals(
|
||||
lua: &Lua,
|
||||
wrapper: Table,
|
||||
@@ -257,15 +257,8 @@ pub(crate) fn invoke_with_globals(
|
||||
g.set(arg_name(i + 1).as_ref(), v.clone())?;
|
||||
}
|
||||
|
||||
let mut modern: Vec<Value> = Vec::with_capacity(2 + n);
|
||||
modern.push(Value::Table(wrapper));
|
||||
if let Some(ev) = event_name {
|
||||
modern.push(Value::String(lua.create_string(ev)?));
|
||||
}
|
||||
modern.extend(extra.iter().cloned());
|
||||
|
||||
// A protected call; the globals are restored before its outcome returns.
|
||||
let outcome = func.call::<()>(MultiValue::from_vec(modern));
|
||||
let outcome = func.call::<()>(());
|
||||
|
||||
g.set("this", saved_this)?;
|
||||
g.set("event", saved_event)?;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! The key-binding table behind `GetBinding`, `SetBinding`, `LoadBindings` and their kin: per
|
||||
//! command, a category header and its chords, spelled `[ALT-][CTRL-][SHIFT-]<TOKEN>` and matched by
|
||||
//! string equality as the reference does. Host commands carry their 1.12 defaults; an addon's
|
||||
//! `Bindings.xml` adds ordinary rows that also carry a Lua body.
|
||||
//! command, its chords, spelled `[ALT-][CTRL-][SHIFT-]<TOKEN>` and matched by string equality as
|
||||
//! the reference does, and the one flat list `GetBinding` walks, whose section headers are rows of
|
||||
//! their own. Host commands carry their 1.12 defaults; an addon's `Bindings.xml` adds ordinary rows
|
||||
//! that also carry a Lua body.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -11,8 +12,9 @@ use crate::bindings_xml::AddonBinding;
|
||||
|
||||
use super::Model;
|
||||
|
||||
/// One host-registered command: the 1.12 name, its category header's global string
|
||||
/// (`BINDING_HEADER_MOVEMENT`), whether it also runs on release (`runOnUp`, read only by
|
||||
/// One host-registered command: the 1.12 name, the header global string of the section it is filed
|
||||
/// under (`BINDING_HEADER_MOVEMENT`; a change from the previous command opens a `HEADER_*` row),
|
||||
/// whether it also runs on release (`runOnUp`, read only by
|
||||
/// `RunCommand` at `0x4b7bf1`, never a bindability rule), and the 1.12 default chords.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct KeybindCommand {
|
||||
@@ -127,7 +129,6 @@ fn is_valid_binding_key(key: &str) -> bool {
|
||||
#[derive(Clone, Debug)]
|
||||
struct Entry {
|
||||
name: String,
|
||||
category: String,
|
||||
run_on_up: bool,
|
||||
defaults: [Option<String>; 2],
|
||||
/// The live chords in bind order; `GetBindingKey` and the window show the first two.
|
||||
@@ -138,10 +139,25 @@ struct Entry {
|
||||
body: Option<String>,
|
||||
}
|
||||
|
||||
/// One row of the list `GetNumBindings`/`GetBinding` walk, in load order.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Row {
|
||||
/// A section header, `HEADER_<header>`: the reference stores `sprintf("HEADER_%s", header)`
|
||||
/// (`0x846f58`) as a row of its own with a display ordinal and no script (`0x4b72f5`-`0x4b72fa`).
|
||||
Header(String),
|
||||
/// A command, an index into [`KeybindState::entries`].
|
||||
Command(usize),
|
||||
}
|
||||
|
||||
/// The table, the stored sets and the queued host requests; lives in [`Model`].
|
||||
#[derive(Default)]
|
||||
pub(crate) struct KeybindState {
|
||||
entries: Vec<Entry>,
|
||||
/// The display list, headers and commands in load order.
|
||||
rows: Vec<Row>,
|
||||
/// The section the last host command was filed under, so the next one opens a header only on
|
||||
/// a change.
|
||||
host_section: Option<String>,
|
||||
by_name: HashMap<String, usize>,
|
||||
/// Stored sets in entry order: `[0]` account (set 1), `[1]` character (set 2, if any).
|
||||
stored: [Option<Vec<Vec<String>>>; 2],
|
||||
@@ -152,15 +168,39 @@ pub(crate) struct KeybindState {
|
||||
/// Bumped by every change to the live table: the app's signal to re-derive dispatch.
|
||||
generation: u64,
|
||||
requests: Vec<KeybindRequest>,
|
||||
/// Set by `BenillaBindCapture`: the host swallows raw input and hands the page the chord.
|
||||
capture_armed: bool,
|
||||
}
|
||||
|
||||
impl KeybindState {
|
||||
/// The rows `GetNumBindings`/`GetBinding` enumerate: all but `hidden="true"`, since the stock
|
||||
/// window skips only `HEADER` rows (`Blizzard_BindingUI.lua:87`).
|
||||
fn visible(&self) -> impl Iterator<Item = &Entry> {
|
||||
self.entries.iter().filter(|e| !e.hidden)
|
||||
/// The rows `GetNumBindings`/`GetBinding` enumerate: every header and every command but a
|
||||
/// `hidden="true"` one, which the reference numbers from a second counter, negated
|
||||
/// (`0x4b73da`-`0x4b73e5`), so `GetBinding`'s ordinal walk (`0x4b7c80`) never reaches it and
|
||||
/// `GetNumBindings` (`0x4b7f40`) counts only the first.
|
||||
fn visible(&self) -> impl Iterator<Item = &Row> {
|
||||
self.rows.iter().filter(|r| match r {
|
||||
Row::Header(_) => true,
|
||||
Row::Command(i) => !self.entries[*i].hidden,
|
||||
})
|
||||
}
|
||||
|
||||
/// Open a section: a `HEADER_<header>` row, unless one of that name exists, which the reference
|
||||
/// refuses (`0x4b71c9`-`0x4b7258`) and loads the binding under the section already open.
|
||||
fn open_header(&mut self, header: &str) {
|
||||
let name = format!("HEADER_{header}");
|
||||
if !self
|
||||
.rows
|
||||
.iter()
|
||||
.any(|r| matches!(r, Row::Header(h) if h.eq_ignore_ascii_case(&name)))
|
||||
{
|
||||
self.rows.push(Row::Header(name));
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a command and its row.
|
||||
fn push_entry(&mut self, entry: Entry) {
|
||||
let idx = self.entries.len();
|
||||
self.by_name.insert(entry.name.to_ascii_uppercase(), idx);
|
||||
self.entries.push(entry);
|
||||
self.rows.push(Row::Command(idx));
|
||||
}
|
||||
|
||||
/// `SetBinding(key[, command])`: unbind `key` everywhere, then append it to the command's
|
||||
@@ -255,26 +295,29 @@ impl KeybindState {
|
||||
}
|
||||
|
||||
/// Append one addon's parsed `Bindings.xml`, skipping a name already registered, so a host
|
||||
/// command keeps it and no handed-out index moves. A `header` opens a section
|
||||
/// (`BINDING_HEADER_<header>`) until the next one, as in the reference's flat list.
|
||||
/// Deviation: rows before a file's first header take the addon's name as their section, not
|
||||
/// the previous file's, because under a foreign header the player could not find them.
|
||||
/// command keeps it and no handed-out index moves. A `header` adds its `HEADER_<header>` row
|
||||
/// before the binding's own, as in the reference's flat list.
|
||||
/// Deviation: a file whose first binding has no header opens a `HEADER_<addon>` row, where the
|
||||
/// reference files those rows under the previous file's last header, because under a foreign
|
||||
/// header the player could not find them.
|
||||
fn register_addon(&mut self, addon: &str, bindings: &[AddonBinding]) {
|
||||
let mut section = addon.to_string();
|
||||
let mut opened = false;
|
||||
for b in bindings {
|
||||
// A foreign `platform` skips the whole node, header included (`0x4b70c3`-`0x4b70e5`).
|
||||
if b.platform.as_deref().is_some_and(|p| p != THIS_PLATFORM) {
|
||||
continue;
|
||||
}
|
||||
// Before the duplicate skip: a duplicate's header still opens its section.
|
||||
if let Some(h) = &b.header {
|
||||
section = format!("BINDING_HEADER_{h}");
|
||||
}
|
||||
// A name already defined skips the node before its header is read (`0x4b70f1`-`0x4b717e`).
|
||||
let key = b.name.to_ascii_uppercase();
|
||||
if self.by_name.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
let idx = self.entries.len();
|
||||
match &b.header {
|
||||
Some(h) => self.open_header(h),
|
||||
None if !opened => self.open_header(addon),
|
||||
None => {}
|
||||
}
|
||||
opened = true;
|
||||
// The stored chord: the current set first, then the account set, as `load` falls back.
|
||||
let stored = {
|
||||
let cur = if self.current_set == 2 { 1 } else { 0 };
|
||||
@@ -283,10 +326,8 @@ impl KeybindState {
|
||||
.or_else(|| self.stored_by_name[0].get(&key))
|
||||
.cloned()
|
||||
};
|
||||
self.by_name.insert(key, idx);
|
||||
self.entries.push(Entry {
|
||||
self.push_entry(Entry {
|
||||
name: b.name.clone(),
|
||||
category: section.clone(),
|
||||
run_on_up: b.run_on_up,
|
||||
// 1.12's `<Binding>` carries no default chord.
|
||||
defaults: [None, None],
|
||||
@@ -317,11 +358,17 @@ impl super::UiScript {
|
||||
continue;
|
||||
}
|
||||
let defaults = [c.default1.map(str::to_owned), c.default2.map(str::to_owned)];
|
||||
let idx = model.keybinds.entries.len();
|
||||
model.keybinds.by_name.insert(key, idx);
|
||||
model.keybinds.entries.push(Entry {
|
||||
let kb = &mut model.keybinds;
|
||||
if kb.host_section.as_deref() != Some(c.category) {
|
||||
kb.host_section = Some(c.category.to_owned());
|
||||
let header = c
|
||||
.category
|
||||
.strip_prefix("BINDING_HEADER_")
|
||||
.unwrap_or(c.category);
|
||||
kb.open_header(header);
|
||||
}
|
||||
kb.push_entry(Entry {
|
||||
name: c.name.to_owned(),
|
||||
category: c.category.to_owned(),
|
||||
run_on_up: c.run_on_up,
|
||||
keys: defaults.iter().flatten().cloned().collect(),
|
||||
defaults,
|
||||
@@ -425,11 +472,6 @@ impl super::UiScript {
|
||||
pub fn character_bindings_exist(&self) -> bool {
|
||||
self.model_mut().keybinds.stored[1].is_some()
|
||||
}
|
||||
|
||||
/// While true, the host swallows raw input and calls `KeyBindings_OnHostKey("<chord>")`.
|
||||
pub fn bind_capture_armed(&self) -> bool {
|
||||
self.model_mut().keybinds.capture_armed
|
||||
}
|
||||
}
|
||||
|
||||
/// Register one addon's `Bindings.xml` from a bare `&Lua`: `LoadAddOn` runs inside a Lua binding
|
||||
@@ -441,9 +483,10 @@ pub(crate) fn register_addon_bindings(lua: &Lua, addon: &str, bindings: &[AddonB
|
||||
.register_addon(addon, bindings);
|
||||
}
|
||||
|
||||
/// The binding globals. `GetBinding` returns command, category, key1, key2, which is not 1.12's
|
||||
/// shape: 1.12 returns command, key1, key2, with each header a `HEADER_*` row
|
||||
/// (`Blizzard_BindingUI.lua:84`). Only `GetNumBindings` and `GetBinding` skip hidden rows.
|
||||
/// The binding globals. `GetBinding(i)` answers the row's name and then every key bound to it
|
||||
/// (`0x4b7f60`: `1 + matches` values), so a `HEADER_*` row answers its name alone, which the stock
|
||||
/// window reads as three names (`Blizzard_BindingUI.lua:84`, `:87`). Only `GetNumBindings` and
|
||||
/// `GetBinding` skip hidden rows.
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.globals().set(
|
||||
"GetNumBindings",
|
||||
@@ -456,17 +499,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"GetBinding",
|
||||
lua.create_function(|lua, i: usize| {
|
||||
let model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let Some(e) = i
|
||||
.checked_sub(1)
|
||||
.and_then(|i| model.keybinds.visible().nth(i))
|
||||
else {
|
||||
let kb = &model.keybinds;
|
||||
let Some(row) = i.checked_sub(1).and_then(|i| kb.visible().nth(i)) else {
|
||||
return Ok(MultiValue::new());
|
||||
};
|
||||
let mut out = vec![
|
||||
Value::String(lua.create_string(&e.name)?),
|
||||
Value::String(lua.create_string(&e.category)?),
|
||||
];
|
||||
for k in e.keys.iter().take(2) {
|
||||
let (name, keys): (&str, &[String]) = match row {
|
||||
Row::Header(h) => (h, &[]),
|
||||
Row::Command(c) => (&kb.entries[*c].name, &kb.entries[*c].keys),
|
||||
};
|
||||
let mut out = vec![Value::String(lua.create_string(name)?)];
|
||||
for k in keys {
|
||||
out.push(Value::String(lua.create_string(k)?));
|
||||
}
|
||||
Ok(MultiValue::from_iter(out))
|
||||
@@ -545,21 +587,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(model.keybinds.current_set)
|
||||
})?,
|
||||
)?;
|
||||
lua.globals().set(
|
||||
"BenillaCharacterBindingsExist",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(model.keybinds.stored[1].is_some())
|
||||
})?,
|
||||
)?;
|
||||
lua.globals().set(
|
||||
"BenillaBindCapture",
|
||||
lua.create_function(|lua, armed: Option<bool>| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.keybinds.capture_armed = armed.unwrap_or(false);
|
||||
Ok(())
|
||||
})?,
|
||||
)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -596,17 +624,40 @@ mod tests {
|
||||
s
|
||||
}
|
||||
|
||||
/// Every row `GetBinding` answers, each as its values in order.
|
||||
fn rows(s: &UiScript) -> Vec<Vec<String>> {
|
||||
s.eval(
|
||||
"local out = {} \
|
||||
for i = 1, GetNumBindings() do out[i] = { GetBinding(i) } end \
|
||||
return out",
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn row(values: &[&str]) -> Vec<String> {
|
||||
values.iter().map(|v| v.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_table_reads_like_the_reference() {
|
||||
fn the_list_reads_like_the_reference() {
|
||||
let s = script();
|
||||
assert_eq!(s.eval::<usize>("return GetNumBindings()").unwrap(), 3);
|
||||
// Command, category, key1, key2 (not 1.12's three values), the defaults seeded live.
|
||||
// A header row answers its name alone; a command, its name and then every key, no
|
||||
// category (`0x4b7f60`).
|
||||
assert_eq!(
|
||||
rows(&s),
|
||||
[
|
||||
row(&["HEADER_MOVEMENT"]),
|
||||
row(&["MOVEFORWARD", "W", "UP"]),
|
||||
row(&["JUMP", "SPACE", "NUMPAD0"]),
|
||||
row(&["HEADER_CAMERA"]),
|
||||
row(&["CAMERAZOOMIN", "MOUSEWHEELUP"]),
|
||||
]
|
||||
);
|
||||
// Every key, not two.
|
||||
s.run(r#"SetBinding("F", "JUMP")"#).unwrap();
|
||||
assert_eq!(rows(&s)[2], row(&["JUMP", "SPACE", "NUMPAD0", "F"]));
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"local c, cat, k1, k2 = GetBinding(1)
|
||||
return c == "MOVEFORWARD" and cat == "BINDING_HEADER_MOVEMENT"
|
||||
and k1 == "W" and k2 == "UP""#
|
||||
)
|
||||
.eval::<bool>("return GetBinding(0) == nil and GetBinding(6) == nil")
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("NUMPAD0")"#)
|
||||
@@ -614,7 +665,7 @@ mod tests {
|
||||
"JUMP"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("F")"#).unwrap(),
|
||||
s.eval::<String>(r#"return GetBindingAction("G")"#).unwrap(),
|
||||
"",
|
||||
"an unbound key reads as the empty command, like the client"
|
||||
);
|
||||
@@ -748,22 +799,16 @@ mod tests {
|
||||
"registration must move the generation — it is the app's re-derive-dispatch signal"
|
||||
);
|
||||
|
||||
// Three host rows and two addon rows: the addon's `MOVEFORWARD` is skipped and
|
||||
// Its header row, then two commands: the addon's `MOVEFORWARD` is skipped and
|
||||
// `PROBEHIDDEN` is not listed.
|
||||
assert_eq!(s.eval::<usize>("return GetNumBindings()").unwrap(), 5);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"local c, cat, k1 = GetBinding(4)
|
||||
return c == "PROBEHOLD" and cat == "BINDING_HEADER_PROBE" and k1 == nil"#
|
||||
)
|
||||
.unwrap());
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
r#"local c, cat = GetBinding(5)
|
||||
return c == "PROBEEDGE" and cat == "BINDING_HEADER_PROBE""#
|
||||
)
|
||||
.unwrap(),
|
||||
"a row with no header of its own belongs to the section the last header opened"
|
||||
assert_eq!(s.eval::<usize>("return GetNumBindings()").unwrap(), 8);
|
||||
assert_eq!(
|
||||
rows(&s)[5..],
|
||||
[
|
||||
row(&["HEADER_PROBE"]),
|
||||
row(&["PROBEHOLD"]),
|
||||
row(&["PROBEEDGE"])
|
||||
]
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
@@ -795,22 +840,49 @@ mod tests {
|
||||
.eval::<bool>(r#"return SetBinding("MOUSEWHEELDOWN", "PROBEEDGE") == 1"#)
|
||||
.unwrap());
|
||||
|
||||
// A file with no header: its rows' section is the addon's name.
|
||||
// A duplicate's header is never read (`0x4b70f1`-`0x4b717e`); a header already in the
|
||||
// list is refused and the row files under the open one (`0x4b71c9`-`0x4b7258`); a hidden
|
||||
// row's header is a row, since the header is written before the hidden test (`0x4b73bc`).
|
||||
let parsed = crate::bindings_xml::parse(
|
||||
r#"<Bindings>
|
||||
<Binding name="JUMP" header="DUPLICATE">Dup();</Binding>
|
||||
<Binding name="PROBEAGAIN" header="PROBE">Again();</Binding>
|
||||
<Binding name="PROBEHIDDENHEAD" header="QUIET" hidden="true">Quiet();</Binding>
|
||||
</Bindings>"#,
|
||||
)
|
||||
.expect("well-formed");
|
||||
s.register_addon_bindings("ProbeMore", &parsed);
|
||||
assert_eq!(
|
||||
rows(&s)[8..],
|
||||
[row(&["PROBEAGAIN"]), row(&["HEADER_QUIET"])]
|
||||
);
|
||||
|
||||
// A file with no header opens a row of the addon's name (the deviation on
|
||||
// `register_addon`).
|
||||
let parsed = crate::bindings_xml::parse(
|
||||
r#"<Bindings><Binding name="LIBKEY">Lib();</Binding></Bindings>"#,
|
||||
)
|
||||
.expect("well-formed");
|
||||
s.register_addon_bindings("ProbeLib", &parsed);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"local c, cat = GetBinding(6); return c == "LIBKEY" and cat == "ProbeLib""#
|
||||
)
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
rows(&s)[10..],
|
||||
[row(&["HEADER_ProbeLib"]), row(&["LIBKEY"])]
|
||||
);
|
||||
|
||||
// Addon rows only, in registration order, hidden included.
|
||||
let bodies = s.addon_binding_bodies();
|
||||
let names: Vec<&str> = bodies.iter().map(|b| b.name.as_str()).collect();
|
||||
assert_eq!(names, ["PROBEHOLD", "PROBEEDGE", "PROBEHIDDEN", "LIBKEY"]);
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
"PROBEHOLD",
|
||||
"PROBEEDGE",
|
||||
"PROBEHIDDEN",
|
||||
"PROBEAGAIN",
|
||||
"PROBEHIDDENHEAD",
|
||||
"LIBKEY"
|
||||
]
|
||||
);
|
||||
assert!(bodies[0].run_on_up && !bodies[1].run_on_up);
|
||||
assert!(bodies[0].body.contains(r#"keystate == "down""#));
|
||||
}
|
||||
@@ -844,7 +916,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_seeding_feeds_load_and_the_capture_arm_reads_back() {
|
||||
fn host_seeding_feeds_load() {
|
||||
let mut s = script();
|
||||
s.seed_binding_set(1, Some(vec![("JUMP".into(), vec!["F".into()])]));
|
||||
s.load_binding_set(1);
|
||||
@@ -857,11 +929,6 @@ mod tests {
|
||||
s.eval::<String>(r#"return GetBindingAction("W")"#).unwrap(),
|
||||
"MOVEFORWARD"
|
||||
);
|
||||
assert!(!s.bind_capture_armed());
|
||||
s.run("BenillaBindCapture(true)").unwrap();
|
||||
assert!(s.bind_capture_armed());
|
||||
s.run("BenillaBindCapture(false)").unwrap();
|
||||
assert!(!s.bind_capture_armed());
|
||||
let snap = s.keybind_snapshot();
|
||||
assert_eq!(snap[1].0, "JUMP");
|
||||
assert_eq!(snap[1].1, vec!["F".to_string()]);
|
||||
|
||||
@@ -247,26 +247,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
lua.set_named_registry_value(REG_SCROLLINGMESSAGEFRAME_METHODS, m)?;
|
||||
|
||||
// BenillaChatTabPressed(): the chat edit box's OnTabPressed sets a flag for the app's whisper
|
||||
// cycle (`UiScript::take_chat_tab`).
|
||||
lua.globals().set(
|
||||
"BenillaChatTabPressed",
|
||||
lua.create_function(|lua, ()| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.chat_tab = true;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// SubmitChatInput(text): the chat edit box's OnEnterPressed queues the line for the app to
|
||||
// parse as a chat command; an empty line is queued too, and the app takes it as a cancel.
|
||||
lua.globals().set(
|
||||
"SubmitChatInput",
|
||||
lua.create_function(|lua, text: Option<String>| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.chat_input.push(text.unwrap_or_default());
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -699,7 +679,8 @@ mod tests {
|
||||
f:SetPoint('BOTTOMLEFT', 0, 0)\n\
|
||||
f:SetWidth(430)\n\
|
||||
f:SetHeight(120)\n\
|
||||
f:SetScript('OnHyperlinkClick', function(self, link, text, button)\n\
|
||||
f:SetScript('OnHyperlinkClick', function()\n\
|
||||
local link, text, button = arg1, arg2, arg3\n\
|
||||
CLICKED = link .. '#' .. text .. '#' .. button\n\
|
||||
end)",
|
||||
)
|
||||
|
||||
@@ -579,7 +579,6 @@ impl UiScript {
|
||||
worldmap::install(&lua)?;
|
||||
worldstate::install(&lua)?;
|
||||
net_stats::install(&lua)?;
|
||||
diagnostics::install(&lua)?;
|
||||
|
||||
let s = UiScript {
|
||||
lua,
|
||||
@@ -828,7 +827,8 @@ impl UiScript {
|
||||
true
|
||||
}
|
||||
|
||||
/// Drain the chat lines `SubmitChatInput` queued since the last call, for the app to parse.
|
||||
/// Drain the chat lines queued since the last call (a probe's, a host `SlashCmdList` row's), for
|
||||
/// the app to parse.
|
||||
pub fn take_chat_input(&mut self) -> Vec<String> {
|
||||
std::mem::take(&mut self.model_mut().chat_input)
|
||||
}
|
||||
@@ -838,11 +838,6 @@ impl UiScript {
|
||||
self.model_mut().chat_input.push(line);
|
||||
}
|
||||
|
||||
/// Whether Tab was pressed in the chat edit box since the last call: the whisper cycle's cue.
|
||||
pub fn take_chat_tab(&mut self) -> bool {
|
||||
std::mem::take(&mut self.model_mut().chat_tab)
|
||||
}
|
||||
|
||||
/// Replace the hyperlink spans `(frame, y-up rect, link, markup)` the app rasterized this
|
||||
/// frame; a release inside one fires `OnHyperlinkClick` on its frame ([`pointer`]).
|
||||
pub fn set_link_spans(&mut self, spans: Vec<(FrameHandle, Rect, String, String)>) {
|
||||
|
||||
@@ -143,8 +143,6 @@ pub(crate) struct Model {
|
||||
pub(crate) layout_rounds: u64,
|
||||
/// Per frame, rasterized links `(y-up rect, link, full |H…|h markup)` for `OnHyperlinkClick`.
|
||||
pub(crate) link_spans: HashMap<FrameHandle, Vec<(Rect, String, String)>>,
|
||||
/// Tab was pressed in the chat edit box (`BenillaChatTabPressed`), for the whisper cycle.
|
||||
pub(crate) chat_tab: bool,
|
||||
/// Region visuals (texture, colour, text) and layout (anchors, size, justify).
|
||||
pub(crate) region_data: HashMap<RegionHandle, RegionData>,
|
||||
/// Each frame's backdrop plate (`<Backdrop>` or `SetBackdrop`, the client's `frame+0x1ac`).
|
||||
@@ -873,7 +871,7 @@ pub(crate) struct Model {
|
||||
/// Reputation calls queued, applied locally first because none of the three sends is acked.
|
||||
pub(crate) reputation_sends: Vec<reputation::ReputationSend>,
|
||||
|
||||
/// Lines the chat box submitted (`SubmitChatInput`), for the app's slash-command parser.
|
||||
/// Lines for the app's slash-command parser: a probe's, or a host `SlashCmdList` row's.
|
||||
pub(crate) chat_input: Vec<String>,
|
||||
|
||||
/// The world map's pushed catalog and feed, and its selection.
|
||||
@@ -1019,7 +1017,6 @@ impl Model {
|
||||
layout_rounds: 0,
|
||||
resolved: HashMap::new(),
|
||||
link_spans: HashMap::new(),
|
||||
chat_tab: false,
|
||||
region_data: HashMap::new(),
|
||||
backdrops: HashMap::new(),
|
||||
simple_html: simplehtml::SimpleHtmlStates::new(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! `SlashCmdList` dispatch from the host, for a line that reaches it without passing the stock
|
||||
//! `ChatEdit_ParseText` (`ChatFrame.lua`): the same walk, over the table the stock `ChatFrame.lua`
|
||||
//! declares, where the stock commands and benilla's own are entries.
|
||||
//! declares, where the stock commands, benilla's layer's and an addon's are entries.
|
||||
|
||||
use mlua::{Table, Value};
|
||||
|
||||
@@ -25,6 +25,33 @@ impl super::UiScript {
|
||||
self.find_slash_handler(cmd).is_some()
|
||||
}
|
||||
|
||||
/// Register a host command as a `SlashCmdList` row, as an addon registers one: `SLASH_<key><n>`
|
||||
/// for each alias (with its `/`) and a handler that queues `"<first alias> <msg>"` on the chat
|
||||
/// input the host drains ([`Self::take_chat_input`]). A no-op before `ChatFrame.lua` has
|
||||
/// declared `SlashCmdList`.
|
||||
pub fn register_host_slash_command(&self, key: &str, aliases: &[&str]) -> mlua::Result<()> {
|
||||
let globals = self.lua.globals();
|
||||
let Ok(list) = globals.get::<Table>("SlashCmdList") else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(first) = aliases.first() else {
|
||||
return Ok(());
|
||||
};
|
||||
for (n, alias) in aliases.iter().enumerate() {
|
||||
globals.set(format!("SLASH_{key}{}", n + 1), *alias)?;
|
||||
}
|
||||
let first = first.to_string();
|
||||
let handler = self.lua.create_function(move |lua, msg: Option<String>| {
|
||||
let line = format!("{first} {}", msg.unwrap_or_default());
|
||||
lua.app_data_mut::<super::Model>()
|
||||
.expect("model app_data")
|
||||
.chat_input
|
||||
.push(line);
|
||||
Ok(())
|
||||
})?;
|
||||
list.set(key, handler)
|
||||
}
|
||||
|
||||
/// The reference's walk: over `SlashCmdList`'s keys, then `SLASH_<key><n>` to the first gap,
|
||||
/// case-insensitive.
|
||||
fn find_slash_handler(&self, cmd: &str) -> Option<mlua::Function> {
|
||||
@@ -122,6 +149,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A host row is an ordinary `SlashCmdList` row that queues its line for the host.
|
||||
#[test]
|
||||
fn a_host_command_queues_its_line() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.register_host_slash_command("HOSTPROBE", &["/hostprobe", "/hp"])
|
||||
.unwrap();
|
||||
assert!(
|
||||
!s.has_slash_command("hostprobe"),
|
||||
"no table yet: nothing registers"
|
||||
);
|
||||
s.run("SlashCmdList = {}").unwrap();
|
||||
s.register_host_slash_command("HOSTPROBE", &["/hostprobe", "/hp"])
|
||||
.unwrap();
|
||||
assert_eq!(s.eval::<String>("return SLASH_HOSTPROBE2").unwrap(), "/hp");
|
||||
assert!(s.run_slash_command("hp", "a b"));
|
||||
assert_eq!(s.take_chat_input(), ["/hostprobe a b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_non_function_entry_is_not_a_command() {
|
||||
let s = UiScript::new().unwrap();
|
||||
|
||||
@@ -333,7 +333,7 @@ fn disabled_button_swallows_clicks_checkbutton_toggles_before_onclick() {
|
||||
clicks, seen_checked = 0, nil
|
||||
local cb = CreateFrame("CheckButton", "Toggler")
|
||||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100)
|
||||
cb:SetScript("OnClick", function(self, button, down)
|
||||
cb:SetScript("OnClick", function() local self, button, down = this, arg1, arg2
|
||||
clicks = clicks + 1
|
||||
seen_checked = self:GetChecked()
|
||||
end)
|
||||
@@ -376,7 +376,7 @@ fn default_registration_is_left_click_only_right_click_reaches_nothing() {
|
||||
clicks = 0
|
||||
local btn = CreateFrame("Button", "Vendor")
|
||||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100)
|
||||
btn:SetScript("OnClick", function(self, button, down) clicks = clicks + 1 end)
|
||||
btn:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks = clicks + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -399,7 +399,7 @@ fn register_for_clicks_grows_right_click_and_carries_the_button_name() {
|
||||
local btn = CreateFrame("Button", "Vendor")
|
||||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100)
|
||||
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
btn:SetScript("OnClick", function(self, button, down)
|
||||
btn:SetScript("OnClick", function() local self, button, down = this, arg1, arg2
|
||||
clicks = clicks + 1
|
||||
click_btn = button
|
||||
arg1_btn = arg1 -- the 1.12 legacy-global convention, same value
|
||||
@@ -431,7 +431,7 @@ fn down_registration_fires_on_press_and_toggles_checked_once() {
|
||||
local cb = CreateFrame("CheckButton", "QuickSell")
|
||||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100)
|
||||
cb:RegisterForClicks("LeftButtonDown")
|
||||
cb:SetScript("OnClick", function(self, button, down)
|
||||
cb:SetScript("OnClick", function() local self, button, down = this, arg1, arg2
|
||||
clicks = clicks + 1
|
||||
seen_down = down
|
||||
end)
|
||||
|
||||
@@ -36,7 +36,7 @@ fn a_runtime_template_brings_size_anchors_regions_and_a_fired_onload() {
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>ProbeLoadedAs = self:GetName(); ProbeLoadWidth = self:GetWidth()</OnLoad>
|
||||
<OnLoad>ProbeLoadedAs = this:GetName(); ProbeLoadWidth = this:GetWidth()</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>"#,
|
||||
|
||||
@@ -4,20 +4,21 @@
|
||||
use super::common::script;
|
||||
use crate::script::*;
|
||||
|
||||
/// A handler gets no arguments (`0x705199` calls `lua_pcall` with zero); its inputs are the
|
||||
/// `this`, `event` and `argN` globals.
|
||||
#[test]
|
||||
fn fire_event_both_conventions_in_one_handler() {
|
||||
fn an_event_handler_gets_no_arguments_and_reads_the_globals() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "EF")
|
||||
f:RegisterEvent("UNIT_HEALTH")
|
||||
f:SetScript("OnEvent", function(self, event, ...)
|
||||
r_this_eq_self = (this == self) -- legacy `this` global == modern `self`
|
||||
r_event_global = event -- modern `event` arg
|
||||
r_event_eq = (event == _G.event) -- == legacy `event` global
|
||||
r_arg1_eq = (arg1 == arg[1]) -- legacy `arg1` global == the vararg table
|
||||
r_arg1 = arg1
|
||||
r_arg2 = arg[2]
|
||||
f:SetScript("OnEvent", function(a, b, c)
|
||||
r_args = (a == nil and b == nil and c == nil)
|
||||
r_this = (this == EF)
|
||||
r_event = event
|
||||
r_arg1 = arg1
|
||||
r_arg2 = arg2
|
||||
end)
|
||||
"#,
|
||||
)
|
||||
@@ -29,13 +30,12 @@ fn fire_event_both_conventions_in_one_handler() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
|
||||
assert!(s.eval::<bool>("return r_this_eq_self").unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return r_event_global").unwrap(),
|
||||
"UNIT_HEALTH"
|
||||
assert!(
|
||||
s.eval::<bool>("return r_args").unwrap(),
|
||||
"a declared parameter is nil: no positional argument is passed"
|
||||
);
|
||||
assert!(s.eval::<bool>("return r_event_eq").unwrap());
|
||||
assert!(s.eval::<bool>("return r_arg1_eq").unwrap());
|
||||
assert!(s.eval::<bool>("return r_this").unwrap());
|
||||
assert_eq!(s.eval::<String>("return r_event").unwrap(), "UNIT_HEALTH");
|
||||
assert_eq!(s.eval::<String>("return r_arg1").unwrap(), "player");
|
||||
assert_eq!(s.eval::<i64>("return r_arg2").unwrap(), 42);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ fn enable_mouse_gates_hit_testing() {
|
||||
-- transparent to hits; the enabled frame behind it (`a`) must capture.
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
a:SetScript("OnEnter", function() local self = this who = self:GetName() end)
|
||||
local b = CreateFrame("Frame", "B")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(false)
|
||||
b:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
b:SetScript("OnEnter", function() local self = this who = self:GetName() end)
|
||||
-- 1/nil, not a boolean: this is the exact comparison shape that inverts, so the
|
||||
-- test asserts it rather than leaning on truthiness.
|
||||
assert(a:IsMouseEnabled() == 1 and b:IsMouseEnabled() == nil)
|
||||
@@ -49,10 +49,10 @@ fn hit_order_is_strata_then_level_then_the_earlier_linked_frame() {
|
||||
who = nil
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
a:SetScript("OnEnter", function() local self = this who = self:GetName() end)
|
||||
local b = CreateFrame("Frame", "B")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(true)
|
||||
b:SetScript("OnEnter", function(self) who = self:GetName() end)
|
||||
b:SetScript("OnEnter", function() local self = this who = self:GetName() end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -91,8 +91,8 @@ fn mouse_move_fires_enter_then_leave_across_a_boundary_with_correct_self() {
|
||||
enter_self_ok, leave_self_ok = false, false
|
||||
local a = CreateFrame("Frame", "A") -- left half only
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnEnter", function(self) enters = enters + 1; enter_self_ok = (self == a) end)
|
||||
a:SetScript("OnLeave", function(self) leaves = leaves + 1; leave_self_ok = (self == a) end)
|
||||
a:SetScript("OnEnter", function() local self = this enters = enters + 1; enter_self_ok = (self == a) end)
|
||||
a:SetScript("OnLeave", function() local self = this leaves = leaves + 1; leave_self_ok = (self == a) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -122,10 +122,10 @@ fn onclick_fires_on_press_release_same_frame_not_when_release_lands_elsewhere()
|
||||
clicks_a, clicks_b, click_btn = 0, 0, nil
|
||||
local a = CreateFrame("Frame", "A") -- left half
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnClick", function(self, button, down) clicks_a = clicks_a + 1; click_btn = button end)
|
||||
a:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks_a = clicks_a + 1; click_btn = button end)
|
||||
local b = CreateFrame("Frame", "B") -- right half
|
||||
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true)
|
||||
b:SetScript("OnClick", function(self, button, down) clicks_b = clicks_b + 1 end)
|
||||
b:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks_b = clicks_b + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -200,7 +200,7 @@ fn mouse_wheel_passes_delta_to_the_captured_frame() {
|
||||
-- XML-load-time only), so a runtime-created frame needs this explicitly, exactly as it
|
||||
-- needs EnableMouse. Mouse-enablement alone puts nothing in the wheel index.
|
||||
a:EnableMouseWheel(true)
|
||||
a:SetScript("OnMouseWheel", function(self, delta) wheel = delta end)
|
||||
a:SetScript("OnMouseWheel", function() local self, delta = this, arg1 wheel = delta end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -344,8 +344,8 @@ fn a_second_fast_click_fires_on_double_click_instead_of_the_second_on_click() {
|
||||
clicks, doubles, dblbtn = 0, 0, nil
|
||||
local b = CreateFrame("Button", "DblB")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
|
||||
b:SetScript("OnClick", function(self, button) clicks = clicks + 1 end)
|
||||
b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end)
|
||||
b:SetScript("OnClick", function() local self, button = this, arg1 clicks = clicks + 1 end)
|
||||
b:SetScript("OnDoubleClick", function() local self, button = this, arg1 doubles = doubles + 1 dblbtn = button end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -483,7 +483,7 @@ fn a_multi_registered_button_pairs_a_left_click_with_a_right_one() {
|
||||
local b = CreateFrame("Button", "MixDbl")
|
||||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
|
||||
b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end)
|
||||
b:SetScript("OnDoubleClick", function() local self, button = this, arg1 doubles = doubles + 1 dblbtn = button end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -716,8 +716,8 @@ fn two_frames() -> crate::script::UiScript {
|
||||
local function mk(name, x)
|
||||
local f = CreateFrame("Frame", name)
|
||||
f:SetPoint("BOTTOMLEFT", x, 200); f:SetWidth(200); f:SetHeight(200); f:EnableMouse(true)
|
||||
f:SetScript("OnMouseDown", function(self) table.insert(log, self:GetName()..":down") end)
|
||||
f:SetScript("OnMouseUp", function(self) table.insert(log, self:GetName()..":up") end)
|
||||
f:SetScript("OnMouseDown", function() local self = this table.insert(log, self:GetName()..":down") end)
|
||||
f:SetScript("OnMouseUp", function() local self = this table.insert(log, self:GetName()..":up") end)
|
||||
return f
|
||||
end
|
||||
mk("A", 100)
|
||||
@@ -799,8 +799,8 @@ fn a_disabled_button_takes_the_hover_but_fires_no_enter_or_leave() {
|
||||
local function mk(name, x)
|
||||
local b = CreateFrame("Button", name)
|
||||
b:SetPoint("BOTTOMLEFT", x, 0); b:SetWidth(100); b:SetHeight(100); b:EnableMouse(true)
|
||||
b:SetScript("OnEnter", function(self) table.insert(log, self:GetName()..":enter") end)
|
||||
b:SetScript("OnLeave", function(self) table.insert(log, self:GetName()..":leave") end)
|
||||
b:SetScript("OnEnter", function() local self = this table.insert(log, self:GetName()..":enter") end)
|
||||
b:SetScript("OnLeave", function() local self = this table.insert(log, self:GetName()..":leave") end)
|
||||
return b
|
||||
end
|
||||
Off = mk("Off", 0)
|
||||
|
||||
@@ -33,15 +33,13 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
|
||||
// `getfenv(0)`); ours does, as our `getglobal`/`setglobal` are written over it.
|
||||
"_G",
|
||||
// ── WoW API past 1.12 ──
|
||||
// Only `SubmitChatInput` is called from our UI files (`ScriptLogFrame.xml`'s slash
|
||||
// commands); each of the rest awaits its 1.12 equivalent, removal or a reason.
|
||||
// Each awaits its 1.12 equivalent, removal or a reason; none is called from our UI files.
|
||||
"CancelUnitBuff",
|
||||
"GetCursorInfo",
|
||||
"GetInventoryItemID",
|
||||
"GetPlayerFacing",
|
||||
"GetTradePartnerName",
|
||||
"IsGossipOptionCoded",
|
||||
"SubmitChatInput",
|
||||
"UnitAura",
|
||||
"UnitIsAFK",
|
||||
"UnitIsDND",
|
||||
@@ -1514,3 +1512,24 @@ fn the_nameplate_verbs_ignore_their_arguments_and_return_nothing() {
|
||||
"zero values — observably different from nil for a caller that counts"
|
||||
);
|
||||
}
|
||||
|
||||
/// The host seams benilla's own interface used are gone, now that it speaks 1.12 alone: the key
|
||||
/// capture, the error-log reads, the chat forwarders and the uncalled binding-set probe.
|
||||
#[test]
|
||||
fn the_layers_host_seams_are_gone() {
|
||||
let s = script();
|
||||
for name in [
|
||||
"BenillaBindCapture",
|
||||
"BenillaCharacterBindingsExist",
|
||||
"BenillaGetNumScriptErrors",
|
||||
"BenillaGetScriptErrorInfo",
|
||||
"BenillaClearScriptErrors",
|
||||
"SubmitChatInput",
|
||||
"BenillaChatTabPressed",
|
||||
] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!("return {name} == nil")).unwrap(),
|
||||
"{name} is still a global"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,12 +288,12 @@ fn vertical_scroll_fires_as_given_and_update_rect_fires_range_changed() {
|
||||
frame:SetScrollChild(child)
|
||||
|
||||
seen_v = nil
|
||||
frame:SetScript("OnVerticalScroll", function(self, offset)
|
||||
frame:SetScript("OnVerticalScroll", function() local self, offset = this, arg1
|
||||
seen_v = offset
|
||||
assert(self == frame and arg1 == offset, "handler-firing conventions carry the value")
|
||||
end)
|
||||
seen_lo, seen_hi = nil, nil
|
||||
frame:SetScript("OnScrollRangeChanged", function(self, lo, hi)
|
||||
frame:SetScript("OnScrollRangeChanged", function() local self, lo, hi = this, arg1, arg2
|
||||
seen_lo, seen_hi = lo, hi
|
||||
end)
|
||||
"#,
|
||||
@@ -336,7 +336,7 @@ fn update_scroll_child_rect_sees_a_same_tick_resize_with_no_intervening_resolve(
|
||||
s.run(
|
||||
r#"
|
||||
seen_hi = nil
|
||||
SF:SetScript("OnScrollRangeChanged", function(self, lo, hi) seen_hi = hi end)
|
||||
SF:SetScript("OnScrollRangeChanged", function() local self, lo, hi = this, arg1, arg2 seen_hi = hi end)
|
||||
Child:SetWidth(300); Child:SetHeight(600) -- grows well past the frame — all in this ONE tick
|
||||
SF:UpdateScrollChildRect()
|
||||
"#,
|
||||
|
||||
@@ -16,7 +16,7 @@ fn a_resize_fires_on_size_changed_and_a_move_does_not() {
|
||||
Panel = CreateFrame("Frame", "SizedPanel")
|
||||
Panel:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
Panel:SetWidth(200); Panel:SetHeight(50)
|
||||
Panel:SetScript("OnSizeChanged", function(self, aw, ah)
|
||||
Panel:SetScript("OnSizeChanged", function() local self, aw, ah = this, arg1, arg2
|
||||
fires = fires + 1 w = aw h = ah
|
||||
end)
|
||||
"#,
|
||||
@@ -62,7 +62,7 @@ fn an_anchor_driven_resize_fires_it_too() {
|
||||
Child = CreateFrame("Frame", "SizeChild", Parent)
|
||||
Child:SetPoint("BOTTOMLEFT", Parent, "BOTTOMLEFT", 0, 0)
|
||||
Child:SetPoint("TOPRIGHT", Parent, "TOPRIGHT", 0, 0)
|
||||
Child:SetScript("OnSizeChanged", function(self, aw, ah) fires = fires + 1 w = aw end)
|
||||
Child:SetScript("OnSizeChanged", function() local self, aw, ah = this, arg1, arg2 fires = fires + 1 w = aw end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -94,7 +94,7 @@ fn a_handler_that_resizes_its_own_frame_settles_instead_of_spinning() {
|
||||
Sq:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
Sq:SetWidth(200); Sq:SetHeight(50)
|
||||
-- The idiom: "keep me square". It writes back into the very input that fired it.
|
||||
Sq:SetScript("OnSizeChanged", function(self, aw, ah)
|
||||
Sq:SetScript("OnSizeChanged", function() local self, aw, ah = this, arg1, arg2
|
||||
fires = fires + 1
|
||||
if ah ~= aw then self:SetHeight(aw) end
|
||||
end)
|
||||
|
||||
@@ -215,7 +215,7 @@ fn slider_setvalue_fires_onvaluechanged_only_on_change() {
|
||||
local sl = CreateFrame("Slider", "SlEvt")
|
||||
sl:SetMinMaxValues(0, 10)
|
||||
seen = {}
|
||||
sl:SetScript("OnValueChanged", function(self, value)
|
||||
sl:SetScript("OnValueChanged", function() local self, value = this, arg1
|
||||
table.insert(seen, value)
|
||||
assert(self == sl and arg1 == value, "handler-firing conventions carry the value")
|
||||
end)
|
||||
|
||||
@@ -50,7 +50,7 @@ fn statusbar_setvalue_fires_onvaluechanged() {
|
||||
local b = CreateFrame("StatusBar", "SbEvt")
|
||||
b:SetMinMaxValues(0, 10)
|
||||
seen = {}
|
||||
b:SetScript("OnValueChanged", function(self, value)
|
||||
b:SetScript("OnValueChanged", function() local self, value = this, arg1
|
||||
table.insert(seen, value)
|
||||
assert(self == b and arg1 == value, "handler-firing conventions carry the value")
|
||||
end)
|
||||
|
||||
@@ -55,7 +55,7 @@ fn bag_item_money_law_and_fallback() {
|
||||
money_fired = nil
|
||||
local a = CreateFrame("Button", "Slot3"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetScript("OnTooltipAddMoney", function(self) money_fired = arg1 end)
|
||||
tt:SetScript("OnTooltipAddMoney", function() local self = this money_fired = arg1 end)
|
||||
-- No merchant: no money handler fires.
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
tt:SetBagItem(0, 1)
|
||||
|
||||
@@ -649,13 +649,13 @@ fn a_raise_with_a_hidden_child_keeps_the_windows_own_siblings_level() {
|
||||
Close:SetPoint("BOTTOMRIGHT", Win, "BOTTOMRIGHT", -15, 15)
|
||||
Close:SetWidth(100); Close:SetHeight(21)
|
||||
Close:EnableMouse(true)
|
||||
Close:SetScript("OnEnter", function(self) hovered = self:GetName() end)
|
||||
Close:SetScript("OnClick", function(self) clicked = self:GetName() end)
|
||||
Close:SetScript("OnEnter", function() local self = this hovered = self:GetName() end)
|
||||
Close:SetScript("OnClick", function() local self = this clicked = self:GetName() end)
|
||||
|
||||
Body = CreateFrame("Frame", "Body", Win) -- level 2, covers the whole window
|
||||
Body:SetAllPoints(Win)
|
||||
Body:EnableMouse(true)
|
||||
Body:SetScript("OnEnter", function(self) hovered = self:GetName() end)
|
||||
Body:SetScript("OnEnter", function() local self = this hovered = self:GetName() end)
|
||||
Body:Hide()
|
||||
|
||||
Win:Hide()
|
||||
|
||||
@@ -47,7 +47,7 @@ fn tick_runs_onupdate_only_when_effectively_visible() {
|
||||
r#"
|
||||
ticks, last = 0, 0
|
||||
local f = CreateFrame("Frame", "UF")
|
||||
f:SetScript("OnUpdate", function(self, elapsed) ticks = ticks + 1; last = elapsed end)
|
||||
f:SetScript("OnUpdate", function() local self, elapsed = this, arg1 ticks = ticks + 1; last = elapsed end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user