Files
benilla-octo/crates/benilla-app/assets/ui/AddonList.xml
T
SamandClaude Fable 5 9662553528 addons come alive: Bagnon draws, hovers and clicks
- Lua 5.0's generic-for and nested long strings are restored in a forked VM — 60 corpus addons stopped dying on one deleted opcode
- a day of demand-driven carving takes the 218-addon corpus from 24 addons surviving a session start to 114, with 144 loading without a single error
- Bagnon draws a real bag and answers real gestures — hover tooltip, pickup, use, drag — because our frames wear the reference's names, and the reference's own ContainerFrame.lua runs off the player's install instead of being transcribed
- the physics broad phase leaves: nothing in the client reads a contact, and it was measured at up to 41% of main-thread samples in real sessions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:19:08 +07:00

409 lines
24 KiB
XML

<Ui>
<!--
The in-game AddOns list (decision 1197) — the ESC-menu half of the AddOns UI, and the
button `GameMenuFrame.xml` has been carrying as `GameMenuButton_Pending(self, ADDONS)`
since 0997 ("goes live by deleting one Pending call and writing its OnClick").
SCOPE — this frame has no reference. 1.12 ships an AddOns list only on the GLUE side
(`GlueXML/AddonList.xml/.lua`, opened by CharacterSelectAddonsButton); in-world there is
no such window and the ESC menu has no such entry. This is a deliberate benilla addition
the director asked for, so nothing here is transcribed and nothing pretends to be. What IS
borrowed from the reference is the BEHAVIOUR of its glue twin, because it is the same job:
- the row shape and count: ADDON_BUTTON_HEIGHT = 16, MAX_ADDONS_DISPLAYED = 19
- AddonList_Update's three-colour title rule — gold loadable, red enabled-but-broken,
grey disabled (SetTextColor 1/0.78/0, 1/0.1/0.1, 0.5/0.5/0.5)
- the status column carrying `reason` (our GetAddOnInfo returns the reference's own
token set — decision 1191 §6)
- Okay saves, Cancel discards (SaveAddOns / ResetAddOns)
It is written entirely against the ELEVEN AddOn API verbs 1191 landed — GetNumAddOns,
GetAddOnInfo, GetAddOnDependencies, EnableAddOn, DisableAddOn, IsAddOnLoaded — with no
host support of its own. That is the point of it as much as the window is: if this panel
works, the addon API an addon author would use works, and it is exercised by anyone who
opens the ESC menu rather than by a test nobody runs.
WHEN CHANGES TAKE EFFECT — next login, not now. Enabling an addon mid-session would mean
running its files into a VM that has already fired ADDON_LOADED/VARIABLES_LOADED/
PLAYER_LOGIN, which is not a state the reference's own loader has a path for; disabling
one cannot unload it at all. The panel says so on its face rather than leaving a player to
discover it. The enable state is written at the session's end by the shutdown tail
(decision 1191 §7, the reference's own last step at 0x490bd0).
-->
<Script><![CDATA[
-- Strings. The `X = X or "..."` idiom is MicroMenu.xml's: a real GlobalStrings table
-- loaded later wins, and these are the fallbacks for a run without one.
ADDONS = ADDONS or "AddOns"
ADDON_LIST_ENABLE_ALL = ADDON_LIST_ENABLE_ALL or "Enable All"
ADDON_LIST_DISABLE_ALL = ADDON_LIST_DISABLE_ALL or "Disable All"
ADDON_LIST_APPLY_NOTE = ADDON_LIST_APPLY_NOTE or "Changes take effect at your next login."
OKAY = OKAY or "Okay"
CANCEL = CANCEL or "Cancel"
ADDON_BUTTON_HEIGHT = 16
MAX_ADDONS_DISPLAYED = 19
-- The interface benilla implements. Kept as a name rather than a literal at the call site
-- because `ui_script::addons::INTERFACE_VERSION` is the same constant on the Rust side and
-- the two must not drift.
BENILLA_INTERFACE_VERSION = 11200
-- **`## Interface` mismatch, which `GetAddOnInfo`'s `reason` cannot carry.** 1191 §6 reports
-- the mismatch and never enforces it — enforcing would refuse most of a real vanilla corpus,
-- whose manifests say 11100, 10900, anything — so it is not a reason the addon *cannot*
-- load, and `not_loadable` rightly has no arm for it. That left this screen silent about a
-- status its char-select twin has always shown, for the same addon: two views of one folder
-- (1197 §3) disagreeing. This is the missing half.
--
-- Stated divergence, measured: `GetAddOnMetadata` returns the FIRST `## Interface` line
-- while the Rust `out_of_date()` accepts a match among ALL of them. **Zero of the 218 corpus
-- addons declare it twice**, so the two agree everywhere it is currently observable; a
-- manifest that declared 11100 and 11200 would read out-of-date here and current there.
function AddonList_OutOfDate(name)
local declared = GetAddOnMetadata(name, "Interface")
if not declared then
-- No `## Interface` at all is SILENT, not out of date — the same rule the char
-- select panel pins in `a_manifest_with_no_interface_is_silent`.
return nil
end
return tonumber(declared) ~= BENILLA_INTERFACE_VERSION
end
-- The staged edit. `AddonList.original` is the enable state as the panel found it, so
-- Cancel can put it back — the reference's `ResetAddOns` over the client's own records,
-- which we do not have an engine verb for (SaveAddOns/ResetAddOns are GLUE-namespace
-- globals and deliberately absent in-world, decision 1191 §6).
AddonList = { offset = 0, original = {} }
function AddonList_Toggle()
if AddonListFrame:IsVisible() then
AddonList_OnCancel()
else
AddonList_Show()
end
end
function AddonList_Show()
AddonList.offset = 0
AddonList.original = {}
for i = 1, GetNumAddOns() do
local name, _, _, _, loadable, reason = GetAddOnInfo(i)
-- "enabled" is everything except the player's own DISABLED — a dependency problem
-- is not a checkbox state, it is a status line.
AddonList.original[name] = not (reason == "DISABLED")
end
AddonListFrame:Show()
AddonList_Update()
end
-- One row's worth of facts, so the row painter below reads as a list of assignments
-- rather than as a pile of GetAddOnInfo returns.
-- Slot 4 is `enabled`, and `url` is not in the in-game return at all: the client registers
-- GetAddOnInfo twice and the in-game one (`0x48e390`) answers seven values without a URL,
-- where glue's (`0x46d460`) answers eight with one. We had the glue shape here, so this
-- read `url` out of slot 4 and then had to RECONSTRUCT `enabled` from the reason token —
-- a workaround that existed only because the slot it wanted was occupied by the wrong
-- value. `## URL` lives in the manifest either way, so the tooltip asks for it directly.
local function rowFacts(index)
local name, title, notes, enabled, loadable, reason = GetAddOnInfo(index)
return name, (title or name), loadable, reason, enabled, notes,
GetAddOnMetadata(name, "URL")
end
function AddonList_Update()
local total = GetNumAddOns()
-- Clamp first: a Disable All that shortens nothing still must not leave the offset
-- past the end after the list is re-read.
local maxOffset = total - MAX_ADDONS_DISPLAYED
if maxOffset < 0 then maxOffset = 0 end
if AddonList.offset > maxOffset then AddonList.offset = maxOffset end
if AddonList.offset < 0 then AddonList.offset = 0 end
for i = 1, MAX_ADDONS_DISPLAYED do
local entry = getglobal("AddonListEntry" .. i)
local index = AddonList.offset + i
if index > total then
entry:Hide()
else
local name, title, loadable, reason, enabled = rowFacts(index)
entry:SetID(index)
local check = getglobal("AddonListEntry" .. i .. "Check")
check:SetChecked(enabled)
-- AddonList_Update's own colour rule, its own three values.
local label = getglobal("AddonListEntry" .. i .. "Title")
label:SetText(title)
if loadable then
label:SetTextColor(1.0, 0.78, 0.0)
elseif enabled and reason ~= "DEP_DISABLED" then
label:SetTextColor(1.0, 0.1, 0.1)
else
label:SetTextColor(0.5, 0.5, 0.5)
end
-- The status column: the reference splices its token into a GlobalStrings
-- lookup (`getglobal("ADDON_"..reason)`), which is why the token spelling is
-- load-bearing (1191 §6). A token with no string falls back to the token, so
-- a player sees something rather than a blank.
local status = getglobal("AddonListEntry" .. i .. "Status")
if reason then
status:SetText(getglobal("ADDON_" .. reason) or reason)
elseif AddonList_OutOfDate(name) then
-- INTERFACE_VERSION ("Out of date"), which `reason` can never carry here
-- and which the char-select twin has always shown. The two screens are the
-- same job (1197 §1) and were answering DIFFERENTLY about the same addon:
-- glue read `## Interface` off the folder, this one asked `GetAddOnInfo`,
-- and `not_loadable` has no interface arm — because 1191 §6 reports the
-- mismatch and never enforces it, so it is not a reason the addon cannot
-- load. Surfaced separately for exactly that reason: `loadable` stays true
-- and most of a real vanilla corpus keeps loading.
status:SetText(ADDON_INTERFACE_VERSION or "Out of date")
else
status:SetText("")
end
entry:Show()
end
end
-- The scroll pair exists only while the list overflows — the reference's own
-- GlueScrollFrame_Update condition.
if total > MAX_ADDONS_DISPLAYED then
AddonListScrollUp:Show()
AddonListScrollDown:Show()
else
AddonListScrollUp:Hide()
AddonListScrollDown:Hide()
end
end
function AddonList_Scroll(delta)
AddonList.offset = AddonList.offset + delta
AddonList_Update()
end
function AddonList_ToggleEntry(entryIndex)
local name, _, _, _, _, reason = GetAddOnInfo(entryIndex)
if reason == "DISABLED" then
EnableAddOn(name)
else
DisableAddOn(name)
end
AddonList_Update()
end
function AddonList_SetAll(on)
if on then EnableAllAddOns() else DisableAllAddOns() end
AddonList_Update()
end
function AddonList_OnOk()
PlaySound("igMainMenuOption")
AddonListFrame:Hide()
end
-- Cancel restores what the panel found, addon by addon — `ResetAddOns` written out,
-- because that verb is glue-only and this window is not the glue.
function AddonList_OnCancel()
PlaySound("igMainMenuQuit")
for i = 1, GetNumAddOns() do
local name = GetAddOnInfo(i)
local was = AddonList.original[name]
if was == true then
EnableAddOn(name)
elseif was == false then
DisableAddOn(name)
end
end
AddonListFrame:Hide()
end
-- The hover tooltip: title, notes, dependencies — AddonTooltip_Update's three lines in
-- its order, on the shared GameTooltip rather than a private frame.
function AddonList_RowEnter(row)
local name, title, loadable, reason, enabled, notes = rowFacts(row:GetID())
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
GameTooltip:AddLine(title)
if notes and notes ~= "" then
GameTooltip:AddLine(notes, 1, 1, 1)
end
local deps = ""
local list = { GetAddOnDependencies(row:GetID()) }
for i = 1, getn(list) do
if i == 1 then
deps = (ADDON_DEPENDENCIES or "Dependencies: ") .. list[i]
else
deps = deps .. ", " .. list[i]
end
end
if deps ~= "" then
GameTooltip:AddLine(deps, 1, 0.82, 0)
end
GameTooltip:Show()
end
function AddonList_RowLeave()
GameTooltip:Hide()
end
]]></Script>
<!-- The panel button face. This engine ships no shared UIPanelButtonTemplate — every panel
defines its own (the MacroFrame/TrainerFrame precedent), and a local one also keeps this
window independent of any other file's template names. -->
<Button name="AddonListPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="90" y="22"/></Size>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<!-- The two scroll arrows, the client's own `UI-ScrollBar-ScrollUpButton-*` art. -->
<Button name="AddonListScrollUpTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Up"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Down"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Highlight" alphaMode="ADD"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></HighlightTexture>
</Button>
<Button name="AddonListScrollDownTemplate" virtual="true">
<Size><AbsDimension x="16" y="16"/></Size>
<NormalTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Up"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Down"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Highlight" alphaMode="ADD"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></HighlightTexture>
</Button>
<!-- One row: a checkbox, a title, a status column. 16 tall, the reference's own
ADDON_BUTTON_HEIGHT. -->
<Button name="AddonListEntryTemplate" virtual="true">
<Size><AbsDimension x="440" y="16"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="$parentTitle" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="$parentStatus" inherits="GameFontDisableSmall" justifyH="RIGHT">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<CheckButton name="$parentCheck">
<Size><AbsDimension x="20" y="20"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
<CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/>
<Scripts>
<OnClick>AddonList_ToggleEntry(this:GetParent():GetID())</OnClick>
</Scripts>
</CheckButton>
</Frames>
<Scripts>
<OnEnter>AddonList_RowEnter(this)</OnEnter>
<OnLeave>AddonList_RowLeave()</OnLeave>
<OnClick>AddonList_ToggleEntry(this:GetID())</OnClick>
</Scripts>
</Button>
<Frame name="AddonListFrame" frameStrata="DIALOG" enableMouse="true" hidden="true">
<Size><AbsDimension x="480" y="420"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentTitle" inherits="GameFontNormalLarge">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-18"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="$parentNote" inherits="GameFontDisableSmall">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="60"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="AddonListEntry1" inherits="AddonListEntryTemplate">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="20" y="-46"/></Offset></Anchor></Anchors>
</Button>
<Button name="AddonListEntry2" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry3" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry4" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry5" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry6" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry7" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry8" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry9" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry10" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry11" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry10" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry12" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry11" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry13" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry12" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry14" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry13" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry15" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry14" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry16" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry15" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry17" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry16" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry18" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry17" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListEntry19" inherits="AddonListEntryTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="AddonListEntry18" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="AddonListScrollUp" inherits="AddonListScrollUpTemplate" hidden="true">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-42"/></Offset></Anchor></Anchors>
<Scripts><OnClick>AddonList_Scroll(-1)</OnClick></Scripts>
</Button>
<Button name="AddonListScrollDown" inherits="AddonListScrollDownTemplate" hidden="true">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="86"/></Offset></Anchor></Anchors>
<Scripts><OnClick>AddonList_Scroll(1)</OnClick></Scripts>
</Button>
<Button name="AddonListEnableAll" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="104" y="22"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="20" y="20"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(ADDON_LIST_ENABLE_ALL)</OnLoad>
<OnClick>AddonList_SetAll(1)</OnClick>
</Scripts>
</Button>
<Button name="AddonListDisableAll" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="104" y="22"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="AddonListEnableAll" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(ADDON_LIST_DISABLE_ALL)</OnLoad>
<OnClick>AddonList_SetAll(nil)</OnClick>
</Scripts>
</Button>
<Button name="AddonListOkay" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="90" y="22"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-20" y="20"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(OKAY)</OnLoad>
<OnClick>AddonList_OnOk()</OnClick>
</Scripts>
</Button>
<Button name="AddonListCancel" inherits="AddonListPanelButtonTemplate">
<Size><AbsDimension x="90" y="22"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="AddonListOkay" relativePoint="LEFT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>this:SetText(CANCEL)</OnLoad>
<OnClick>AddonList_OnCancel()</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
AddonListFrameTitle:SetText(ADDONS)
AddonListFrameNote:SetText(ADDON_LIST_APPLY_NOTE)
</OnLoad>
<OnShow>AddonList_Update()</OnShow>
<OnMouseWheel>AddonList_Scroll(-arg1)</OnMouseWheel>
</Scripts>
</Frame>
</Ui>