Files
benilla-octo/crates/benilla-app/assets/ui/LootFrame.xml
T
SamandClaude Fable 5 67315143f9 the bag windows are the real 1.12 files, and /walk actually walks
- the bag windows are the reference's own `ContainerFrame.xml` running off the player's patch chain — decision 1751 makes the stock 1.12 UI off the install the end state and `assets/ui` scaffolding that retires file-by-file (86 files against the 99-file stock corpus, 72% of the 1128-function global surface already built) — via one ordered manifest, `benilla.toc`, where a bare name is a file we ship and an `Interface\FrameXML\` path is the player's own file; the windows on screen are `ContainerFrame1..12`, the bank's six copy-pasted popouts fold into them at ids 5..10, net −750 lines, and the swap forced real engine verbs into being rather than stubs (`SetBagPortaitTexture` with its id-0 no-texture arm, `GameTooltip:AppendText`, `SetButtonPulse`, and a `<Model>` whose file is the cooldown indicator materializing as the native sweep, which three corpus addons inherit)
- the 1.12 walk toggle lands: `TOGGLERUN` latches one bit with `ToggleRun`'s own silent refusal chain (dead, rooted, on a live server spline), and the walk animation was never missing at all — the flag reaches it through exactly one channel, the speed (`0x5fd100` picks Walk over Run on `speed > 2 × walkSpeed`); pinning it caught our remote extrapolator with the two min rungs backwards — `min(walk, run)` runs BEFORE the backward min, so a walking backpedaller moves at 2.5 yd/s where we would have played them the Run clip at 4.5 — and one shared `current_speed` now keeps the local controller and the extrapolator from ever disagreeing about it
- the instance and raid lockout messages exist — six packets that were dropped as unknown opcodes now land, four of them as the CHAT_MSG_SYSTEM lines the client composes itself, which is why no Lua handler ever surfaced them — plus `IsInInstance`, `ResetInstances`, and the unit popup's "Reset all instances" row behind the reference's own predicate; where the binary contradicts vmangos: the ownership flag means "any instance bind" but vmangos only ever names raid binds, so the row was dead for every character without a raid lockout — term 1 is now also satisfied by our own eyes, walking out of a party dungeon the server itself bound us to
- opening the world map showed 3D world at the sheet's margins: `BlackoutWorld` was parented to the very `UIParent` a fullscreen panel hides, and three more frames had crossed the same boundary — the shared dropdown lists (so the map's continent and zone pickers answered a click with nothing at all), the master-looter menu, and the screenshot confirmation, which could never appear during a cinematic; `frame_flag_gate` now checks the parent seat against the reference both ways a frame acquires one (the attribute, template-inherited included, and `<Frames>` nesting), and its first harvest took 30 divergences down to three accepted ones

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:17:25 +07:00

728 lines
43 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui>
<!--
benilla's loot window (decision 0084), RE-SKINNED to the real 1.12 loot window: the geometry
and art below are QUOTED from the real `LootFrame.xml` (scratchpad `LootFrame.xml`; line
citations inline), turned onto our own feeds/intents. The name `LootFrame`, its panel
registration (UiPanels.xml area="left", pushable=7), the loot feeds, the 4-rows-per-page +
pager logic, and the intent functions are UNCHANGED — only the window's dressing is real now.
A hidden window that shows on LOOT_OPENED and hides on LOOT_CLOSED. It wears the real loot art:
the single UI-LootPanel slab filling the real 256×256 window (ref l.59-60, 88); each row is one
wide click button (our whole-row model) carrying the item icon and, behind the name, the real
UI-QuestItemNameFrame parchment plate (ref LootButtonTemplate, l.4-32); the pager arrows and the
minimize-button close at their real spots.
ENGINE-SLICE CAVEAT (this branch predates the dressing arc's engine slice — named `<Font>`
objects are not implemented here yet, loader.rs "named Font objects are not implemented in v1").
The real window's `inherits="GameFontNormal"` FontStrings are transcribed faithfully (they are
part of the quoted geometry), but until the font-object slice + our Fonts.xml land on this line
the loader warns "unknown template 'GameFontNormal'" and the strings render in the default font
— no worse than before. They light up when the slice merges. No <TexCoords> is used: the loot
panel is a single whole-texture slab, so this window needs none of it.
The dead-target skull sits in the panel's portrait ring: LootFramePortraitOverlay
(Interface\TargetingFrame\TargetDead), quoted from ref l.73-85 (BORDER layer, 58×58 at the
window TOPLEFT +(10,-8)). It is STATIC here — the real client swaps in FishingLoot-Icon for a
fishing loot (LootFrame.lua l.134/139), but we ship no fishing feed, so only the corpse skull
renders (and always, in the ring). Being BORDER (below the ARTWORK panel), it shows through the
panel art's ring cut-out exactly as in the real frame.
Real-window features we OMIT (no feed/model, or unchanged pager logic): the PREV/NEXT text
labels beside the pager (ref l.98-115 — our arrow buttons carry the affordance and our Update()
shows/hides only those; adding synced labels would touch the unchanged pager logic).
The Era loot API this drives (all app-fed via the loot seam, benilla-ui `script/loot.rs`):
GetNumLootItems(), GetLootSlotInfo(slot) -> texture,item,quantity,quality, LootSlotIsItem/
LootSlotIsCoin(slot), GetLootSlotLink(slot) (the row's item link, read by the click's
ctrl/shift arms — decision 1059), BenillaTakeLootSlot(slot) (the row click's action, which
in 1.12 is the C LootButton's and no Lua binding at all — decision 1744), LootSlot(slot) (the
LOOT_BIND confirmation continuation, its only 1.12 meaning), CloseLoot() (from OnHide). The
synthesized coin pile is row 1 when the loot carries gold; a row click sends the money or the
item's wire slot (the app maps it) — unless the row binds on pickup, where it raises the
soulbind confirm instead and the send waits for the OK.
Paging (LootFrame.lua l.27-31,70-73,105-118): 4 rows/page; > 4 items spends one slot on the
pager, showing 3 rows + Up/Down. We keep the current page across content-change repaints
(LOOT_UPDATE) and reset it to 1 only on a fresh open (LOOT_OPENED) — the merchant SHOW/UPDATE
split. (The per-button LOOT_SLOT_CLEARED event is replaced by a full re-snapshot repaint; the
app fires LOOT_UPDATE on any content change, incl. an async item name/quality landing and a
looted/coin-cleared row. The OBSERVABLE matches the ref exactly: the slot list is fixed while
the window is open — GetNumLootItems counts cleared slots, which answer neither IsItem nor
IsCoin, so a looted row's button hides IN PLACE and the rows below never shift up — and a
page left with every button hidden advances to the next page when one exists, the ref's
LOOT_SLOT_CLEARED tail, l.38-50.)
UIPanel slot system (decision 0084 §2): registered in UiPanels.xml as area="left",
pushable=7 — so it carries NO hardcoded <Anchors>; ShowUIPanel SetPoints it (and, when a
pushable=0 window like the merchant also wants the left slot, promotes loot to the center
slot rather than replacing it). Every show is ShowUIPanel, every hide HideUIPanel.
-->
<Script><![CDATA[
BENILLA_LOOT_NUMBUTTONS = 4
-- The 1.12 item-quality palette — now the shared ITEM_QUALITY_COLORS global (Fonts.xml,
-- its home since the unit popup became a second customer); the local name kept for this
-- file's existing readers. Indexed 0..6; an unknown/nil quality falls back to common.
BENILLA_LOOT_QUALITY_COLORS = ITEM_QUALITY_COLORS
local function loot_color(quality)
return BENILLA_LOOT_QUALITY_COLORS[quality] or BENILLA_LOOT_QUALITY_COLORS[1]
end
-- ── Master loot (decision 1675) ───────────────────────────────────────────────────────
-- The quality at or above which handing an item out asks for confirmation first. Ref
-- LootFrame.lua l.3 — and the misspelling is Blizzard's own, kept because the corpus
-- reads THIS name: `MASTER_LOOT_THRESHOLD` (with the S) exists nowhere in 1.12.
MASTER_LOOT_THREHOLD = 4
GIVE_LOOT = GIVE_LOOT or "Give Loot To:" -- l.2006
GROUP = GROUP or "Group" -- l.2029
-- The party bound the dropdown's party arm sweeps to (ref PartyMemberFrame.lua l.1; ours
-- is declared in PartyFrame.xml, which the manifest loads far ahead of this file). Stated
-- through the same first-writer-wins idiom as the strings above rather than assumed: the
-- dropdown's init function RUNS at this file's load (UIDropDownMenu_Initialize calls it
-- immediately), so a missing constant is an arithmetic error at load, not at first use.
MAX_PARTY_MEMBERS = MAX_PARTY_MEMBERS or 4
-- The raid arm's two bounds, same idiom and same reason (declared in PartyFrame.xml /
-- RaidFrame.xml, both loaded first). The reference writes them inline as literal `40` and
-- `5` (l.194/:195); naming them is ours, so the names have to be guaranteed here.
MAX_RAID_MEMBERS = MAX_RAID_MEMBERS or 40
MEMBERS_PER_RAID_GROUP = MEMBERS_PER_RAID_GROUP or 5
CONFIRM_LOOT_DISTRIBUTION = CONFIRM_LOOT_DISTRIBUTION
or "You wish to assign %s to %s. Is this correct?" -- l.848
-- The dialog below captures these two by VALUE at load, so a load-order slip would bake a
-- nil into its buttons rather than fail loudly. FriendsFrame.xml declares them and the
-- manifest loads it first; stated again here so this file's own dialog cannot depend on
-- that staying true.
YES = YES or "Yes" -- l.5463
NO = NO or "No" -- l.2794
-- The confirmation the dropdown raises for an at-or-above-threshold item (ref
-- StaticPopup.lua l.85-94). `data` is the candidate index the dropdown stashed; the SLOT
-- is re-read live from LootFrame.selectedSlot at accept time, exactly as the ref does —
-- which is precisely why the row click below hides this popup first, so an accept can
-- never land on a row the player has since clicked away from.
StaticPopupDialogs["CONFIRM_LOOT_DISTRIBUTION"] = {
text = CONFIRM_LOOT_DISTRIBUTION,
button1 = YES,
button2 = NO,
OnAccept = function(data)
GiveMasterLoot(LootFrame.selectedSlot, data)
end,
timeout = 0,
hideOnEscape = 1,
}
function BenillaLootItem_OnLoad(button, index)
button.lootButton = index
button.icon = getglobal(button:GetName() .. "Icon")
button.name = getglobal(button:GetName() .. "Name")
button.count = getglobal(button:GetName() .. "Count")
-- Left-click loots the row (the C LootButton behaviour, LootFrame.lua l.94/147).
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
end
-- ref LootFrameItem_OnClick (LootFrame.lua l.147-162): CTRL previews the row's item in the
-- dressing room (decision 1060), SHIFT posts its link into an open chat edit box (decision
-- 1059) — both through GetLootSlotLink, the row-link binding this arc added (loot.rs).
--
-- WHERE THE TAKE LIVES, and why the gate below reads three modifiers where FrameXML names
-- two (VERIFIED at the 1.12.1 bytes — wow-re `ui/scratch/loot-click-modifier-gate.md`,
-- decision 1067). The reference's Lua handler suppresses NOTHING, because the take is the C
-- `CLootButton`'s own click behaviour (`LootFrame.lua:94`'s `button:SetSlot(slot)` hands the
-- widget the slot). `CLootButton::OnClick 0x4c1820` runs the Lua `<OnClick>` FIRST and
-- unconditionally, then gates itself on `0x41f8f0(0/1/2)` — byte-identically the predicates
-- behind `IsShiftKeyDown`/`IsControlKeyDown`/**`IsAltKeyDown`** — and only then sends
-- (`0x4c2790` → `0x5e1ad0` → `push 0x108`, CMSG_AUTOSTORE_LOOT_ITEM).
--
-- Three consequences, all of them this handler's shape:
-- · **ALT suppresses the take too**, though no FrameXML file mentions alt. Its gate is C.
-- · The gate is on ANY modifier being HELD, not on "the arm that handled it": a shift-click
-- with the chat box closed does nothing in Lua and still must not loot.
-- · The button code is never read (`0x4c1820` doesn't look at it), so a RIGHT-click loots
-- exactly like a left one — which is why this handler takes no `mouseButton`.
--
-- benilla folds that C action into this Lua handler, so the gate has to be an early return
-- here. The verb it calls is `BenillaTakeLootSlot`, not `LootSlot`, and that is 1067's
-- named follow-up landed (decision 1744): 1.12 exposes no plain "take slot N" binding at
-- all — the real `LootSlot 0x4c2e70` is the LOOT_BIND *confirmation continuation* (flag 1,
-- gated on the stashed slot at `[0x847cec]`; its only FrameXML caller is
-- `StaticPopup.lua:606`, the entry below). Ours keeps exactly that meaning, so an addon
-- calling `LootSlot(n)` on an ordinary row does what it does on the real client: nothing.
--
-- The reference's tail (StaticPopup_Hide("CONFIRM_LOOT_DISTRIBUTION") and the four
-- LootFrame.selected* fields, l.155-161) is the MASTER-LOOT dropdown's bookkeeping —
-- GroupLootDropDown reads exactly those four (l.164+). benilla ships no master loot
-- (benilla-protocol's loot module: CMSG_LOOT_MASTER_GIVE/SMSG_LOOT_MASTER_LIST out of
-- scope), so returning early skips nothing that exists.
--
-- The coin row has no link (it is synthesized, with no item behind it — ui_loot.rs), so a
-- modified click on it finds nil: DressUpItemLink returns on a nil link (DressUpFrame.lua
-- l.10-16) and the insert is skipped. It does not fall through to looting the money either,
-- which is the same law the item rows follow.
function BenillaLootItem_OnClick(button)
-- The row remembers its resolved loot slot from the last Update; act on it (LootFrame.lua
-- l.94-95 stores button.slot, the click calls LootSlot on it).
if not button.slot then return end
local modified = false
if IsControlKeyDown() then
DressUpItemLink(GetLootSlotLink(button.slot)) -- ref l.148-149
modified = true
elseif IsShiftKeyDown() then
-- ref l.150-153, through the shared helper (UIParent.xml): it carries the ref's own
-- "only when the edit box is up" guard AND our nil-link one, which this row needs
-- more than most — the coin row never has a link, and an item row has none until
-- its template lands. A shift-click with chat closed does nothing here: a loot row
-- has no stack to split, unlike a bag slot.
BenillaChatEdit_InsertLink(GetLootSlotLink(button.slot))
modified = true
elseif IsAltKeyDown() then
-- The C gate's third modifier (`0x41f8f0(2)` @ `0x4c1841`). Nothing is bound to
-- alt in FrameXML, so this arm does nothing at all — its entire job is to NOT
-- loot, which is what the real client does and what we did not.
modified = true
end
-- The ref's tail (l.155-161), and it runs on EVERY click — modified ones included,
-- because in the reference this bookkeeping is the Lua <OnClick> and the take is the
-- C handler that runs after it; the modifier gate lives in the C half, so it never
-- skipped these four. Ours folds the take into this function (1067), so the tail is
-- lifted above the early return rather than left below it — same observable.
--
-- What reads them: GroupLootDropDown, which the app opens by firing
-- OPEN_MASTER_LOOT_LIST when the clicked row's wire slot_type is MASTER. That event
-- is raised from the pick drain, i.e. AFTER this handler returns, so the four fields
-- must already be stashed by then — they are the dropdown's only knowledge of WHICH
-- row is being handed out, and its anchor.
StaticPopup_Hide("CONFIRM_LOOT_DISTRIBUTION")
LootFrame.selectedLootButton = button:GetName()
LootFrame.selectedSlot = button.slot
LootFrame.selectedQuality = button.quality
LootFrame.selectedItemName = button.name:GetText()
if modified then return end
BenillaTakeLootSlot(button.slot)
end
-- Hover: the plain engine item tooltip, ITEM rows only — the ref's own gate (its
-- LootButtonTemplate <OnEnter> runs SetLootItem only `if LootSlotIsItem(slot)`, so the
-- coin pile shows no tooltip at all). No hand-added lines: the real tooltip never shows a
-- stack size (the 0274 P1 'Stack Size' drop retired the same line from the bag hovers;
-- this file's copy predated it and survived).
function BenillaLootItem_OnEnter(button)
if not button.slot then return end
if not LootSlotIsItem(button.slot) then return end
local texture, item, quantity, quality, itemId = GetLootSlotInfo(button.slot)
button.itemId = itemId
if not item then return end
-- Snap beside the ICON, not the 160-wide row's far edge (the vendor rows' own idiom —
-- MerchantFrame.xml walks ANCHOR_RIGHT back by a negative x): 37px icon on a 160 row
-- → -123 lands the tip at the icon's right edge.
GameTooltip:SetOwner(button, "ANCHOR_RIGHT", -123, 0)
-- SetLootItem, like the ref's own LootButtonTemplate: the loot ROW is the source, not
-- just its item id, so the tooltip sees the drop's random-suffix roll. Through
-- SetItemById (a template source) a "… of the Monkey" showed `<Random enchantment>`
-- until the item reached a bag — decision 1547.
GameTooltip:SetLootItem(button.slot)
GameTooltip:Show()
end
function BenillaLootItem_OnLeave(button)
GameTooltip:Hide()
end
-- LootFrame_Update (LootFrame.lua l.67-119): fill the visible rows for the current page.
function LootFrame_Update()
local frame = getglobal("LootFrame")
local numItems = GetNumLootItems()
-- Rows per page: 4, or 3 when there's a pager to make room for (l.70-73).
local perPage = BENILLA_LOOT_NUMBUTTONS
if numItems > BENILLA_LOOT_NUMBUTTONS then
perPage = perPage - 1
end
-- Clamp the page. numItems is constant while a window is open (cleared slots stay in
-- the count — the fixed slot layout), so this only guards a stray page value.
local maxPage = 1
if numItems > 0 then
maxPage = math.ceil(numItems / perPage)
end
if frame.page > maxPage then
frame.page = maxPage
end
if frame.page < 1 then
frame.page = 1
end
for index = 1, BENILLA_LOOT_NUMBUTTONS do
local button = getglobal("LootButton" .. index)
local slot = (perPage * (frame.page - 1)) + index
if slot <= numItems and index <= perPage
and (LootSlotIsItem(slot) or LootSlotIsCoin(slot)) then
local texture, item, quantity, quality, itemId = GetLootSlotInfo(slot)
button.icon:SetTexture(texture or BENILLA_FALLBACK_ICON)
button.name:SetText(item or "...")
local color = loot_color(quality)
button.name:SetVertexColor(color.r, color.g, color.b)
-- "xN" stack count only when more than one drops (LootFrame.lua l.87-93).
if (quantity or 1) > 1 then
button.count:SetText(quantity)
else
button.count:SetText("")
end
button.slot = slot
button.itemId = itemId
-- Ref l.96. Read back by the row click into LootFrame.selectedQuality, which
-- is what decides whether handing this row out asks for confirmation first.
button.quality = quality
button:Show()
else
button:Hide()
end
end
-- Pager visibility (LootFrame.lua l.105-118): Up when past page 1, Down when more remain.
if frame.page > 1 then
getglobal("LootUpButton"):Show()
else
getglobal("LootUpButton"):Hide()
end
if frame.page < maxPage then
getglobal("LootDownButton"):Show()
else
getglobal("LootDownButton"):Hide()
end
end
function LootFrame_PageDown()
local frame = getglobal("LootFrame")
frame.page = frame.page + 1
LootFrame_Update()
end
function LootFrame_PageUp()
local frame = getglobal("LootFrame")
frame.page = frame.page - 1
LootFrame_Update()
end
-- ── The soulbind confirm ──────────────────────────────────────────────────────────────
-- ref GlobalStrings.lua l.2612 / l.2818 / l.465, fallback form (the player's own localized
-- file runs off the patch chain first).
LOOT_NO_DROP = LOOT_NO_DROP or "Looting this item will bind it to you."
OKAY = OKAY or "Okay"
CANCEL = CANCEL or "Cancel"
-- ref StaticPopup.lua l.601-611, verbatim. `OnAccept`'s argument is `dialog.data`, which
-- the driver below sets to the event's row — and `LootSlot` is the continuation verb, so
-- this line is the ONLY thing in the client that ever completes a bind-deferred take.
--
-- Nothing else is needed: no OnCancel (declining just leaves the row in the window, and the
-- app drops the pending stash when the window turns over) and no OnUpdate poll, because the
-- window's own LOOT_CLOSED hides it (below, ref LootFrame.lua l.53-54).
StaticPopupDialogs["LOOT_BIND"] = {
text = LOOT_NO_DROP,
button1 = OKAY,
button2 = CANCEL,
OnAccept = function(slot)
LootSlot(slot)
end,
timeout = 0,
exclusive = 1,
hideOnEscape = 1,
}
-- ref UIParent.lua l.317-323. The row rides on `dialog.data` rather than as a text arg —
-- LOOT_NO_DROP has no format specifier, so the dialog shows the same sentence whatever
-- binds; the number is only there to be handed back to LootSlot.
--
-- The reference registers this on UIParent, which owns every bind confirm; benilla hangs it
-- on LootFrame, whose LOOT_CLOSED already has to hide the dialog. Same two moments, one
-- listener, and the arc stays in one file.
function BenillaLootBindConfirm(slot)
local dialog = StaticPopup_Show("LOOT_BIND")
if dialog then
dialog.data = slot
end
end
function LootFrame_OnLoad()
local frame = this
frame.page = 1
frame:RegisterEvent("LOOT_OPENED")
frame:RegisterEvent("LOOT_UPDATE")
frame:RegisterEvent("LOOT_CLOSED")
frame:RegisterEvent("LOOT_BIND_CONFIRM")
-- Master loot (ref l.9-10). Neither event carries an argument: the candidate list is
-- read back through GetMasterLootCandidate at menu-build time, not pushed.
frame:RegisterEvent("OPEN_MASTER_LOOT_LIST")
frame:RegisterEvent("UPDATE_MASTER_LOOT_LIST")
end
function LootFrame_OnEvent(event)
local frame = this
if event == "LOOT_OPENED" then
-- A fresh loot: reset to page 1, paint, show (LootFrame.lua l.14-16).
frame.page = 1
LootFrame_Update()
ShowUIPanel(frame)
elseif event == "LOOT_UPDATE" then
-- A content change while open (name/quality landed, a row/coin removed): repaint,
-- keeping the current page. A looted slot's button hides IN PLACE (the slot stays
-- in the layout as a gap — the ref's LOOT_SLOT_CLEARED, l.22-37).
LootFrame_Update()
-- The ref's LOOT_SLOT_CLEARED tail (l.38-50): a page left with every button hidden
-- moves down to the next page when one exists.
local allHidden = true
for index = 1, BENILLA_LOOT_NUMBUTTONS do
if getglobal("LootButton" .. index):IsVisible() then
allHidden = false
end
end
if allHidden and getglobal("LootDownButton"):IsVisible() then
LootFrame_PageDown()
end
elseif event == "LOOT_CLOSED" then
-- ref l.53-54: the confirm dies with the window it addressed. Its row number would
-- name a slot in the NEXT corpse otherwise.
StaticPopup_Hide("LOOT_BIND")
HideUIPanel(frame)
elseif event == "LOOT_BIND_CONFIRM" then
BenillaLootBindConfirm(arg1)
elseif event == "OPEN_MASTER_LOOT_LIST" then
-- Ref l.58-61. Anchored to the ROW that was clicked, not to the frame — the name
-- the click stashed a moment ago.
ToggleDropDownMenu(1, nil, GroupLootDropDown, LootFrame.selectedLootButton, 0, 0)
elseif event == "UPDATE_MASTER_LOOT_LIST" then
-- Ref l.62-64: refresh IN PLACE, never re-toggle — re-toggling an open menu would
-- close it under the player's cursor.
UIDropDownMenu_Refresh(GroupLootDropDown)
end
end
function LootFrame_OnShow()
-- Open sound — the window-sound convention (decision 0090), wrapper form (this handler
-- has real logic beyond the sound, so it can't be a bare inline PlaySound).
-- (LootFrame.lua LootFrame_OnShow, l.131-141). ONLY the empty-roll fork and the fishing
-- fork are Lua-side: an empty loot plays LOOTWINDOWOPENEMPTY (l.135-136); a fishing loot
-- (IsFishingLoot(), decision 1086) plays "FISHING REEL IN" and swaps the portrait ring's
-- skull for the FishingLoot-Icon (l.137-140) — reset to the skull first, exactly as the
-- ref does (l.133), so a corpse loot after a fishing one gets its skull back. The normal
-- (non-empty, non-fishing) loot-open sound is C-side (the server-driven loot handler),
-- so we stay silent for it — there is no kit to guess. (DR 0086: vmangos masks the
-- lootable flag for empty rolls, so a live empty window is rare — but the branch must
-- exist.)
getglobal("LootPortraitOverlay"):SetTexture("Interface\\TargetingFrame\\TargetDead")
if GetNumLootItems() == 0 then
PlaySound("LOOTWINDOWOPENEMPTY")
elseif IsFishingLoot() then
PlaySound("FISHING REEL IN")
getglobal("LootPortraitOverlay"):SetTexture("Interface\\LootFrame\\FishingLoot-Icon")
end
end
function LootFrame_OnHide()
-- Closing the window releases the loot (LootFrame.lua l.143-145). The app only sends the
-- release when a loot is actually open, so a hide after the state already cleared is a
-- harmless no-op. No close sound here: LootFrame_OnHide plays none (the loot-close kit is
-- C-side), so we stay silent — no kit to guess.
CloseLoot()
end
function BenillaLootCloseButton_OnClick()
-- Hide immediately (client-authoritative); OnHide's CloseLoot() sends the release.
HideUIPanel(getglobal("LootFrame"))
end
-- ── GroupLootDropDown — the master looter's "Give Loot To:" menu ──────────────────────
-- Ref LootFrame.lua l.164-244, transcribed. The frame keeps the reference's BARE name for
-- the same mechanical reason the four GroupLootFrames do (decision 1254/0591 §3):
-- LootFrame_OnEvent resolves it by literal global lookup, and a real 1.12 addon that
-- hooks the master-loot menu expects to find it under this name.
function GroupLootDropDown_OnLoad()
-- "MENU" style: a bare context menu, no capsule chrome — it is opened at the loot row
-- by ToggleDropDownMenu, never drawn in place. Passed by name rather than the ref's
-- bare `this`, the house calling convention (TradeSkillFrame/TrainerFrame precedent).
UIDropDownMenu_Initialize(GroupLootDropDown, GroupLootDropDown_Initialize, "MENU")
end
function GroupLootDropDown_Initialize()
local candidate, info
-- Level 2: one raid group's five candidate slots (ref l.170-184). The parent row
-- stashed the index its probe found, and this walks the five starting THERE — so a
-- group whose first present candidate is not its first slot yields a window offset by
-- the same amount. That is the reference's own arithmetic; kept as-is.
if UIDROPDOWNMENU_MENU_LEVEL == 2 then
local lastIndex = UIDROPDOWNMENU_MENU_VALUE + 5 - 1
for i = UIDROPDOWNMENU_MENU_VALUE, lastIndex do
candidate = GetMasterLootCandidate(i)
if candidate then
info = {}
info.text = candidate
info.textHeight = 12
info.value = i
info.notCheckable = 1
info.func = GroupLootDropDown_GiveLoot
UIDropDownMenu_AddButton(info, UIDROPDOWNMENU_MENU_LEVEL)
end
end
return
end
if GetNumRaidMembers() > 0 then
-- In a raid: a title, then one nested row per group that has any candidate at all
-- (ref l.186-212). The probe is "does any of this block of five answer non-nil".
info = {}
info.text = GIVE_LOOT
info.textHeight = 12
info.notCheckable = 1
info.isTitle = 1
UIDropDownMenu_AddButton(info)
for i = 1, MAX_RAID_MEMBERS, MEMBERS_PER_RAID_GROUP do
for j = i, i + MEMBERS_PER_RAID_GROUP - 1 do
candidate = GetMasterLootCandidate(j)
if candidate then
info = {}
-- The label counts from the loop step `i`, the value from the found
-- slot `j` — the ref uses the two differently on purpose (l.196/:200).
info.text = GROUP .. " " .. math.ceil(i / MEMBERS_PER_RAID_GROUP)
info.textHeight = 12
info.hasArrow = 1
info.notCheckable = 1
info.value = j
info.func = nil
UIDropDownMenu_AddButton(info)
break
end
end
end
else
-- In a party: a flat list, no title row and no nesting (ref l.214-232). The sweep
-- is MAX_PARTY_MEMBERS+1 because the master looter can hand loot to THEMSELVES —
-- the server includes us in the candidate list too (Group::MasterLoot walks the
-- whole group).
for i = 1, MAX_PARTY_MEMBERS + 1, 1 do
candidate = GetMasterLootCandidate(i)
if candidate then
info = {}
info.text = candidate
info.textHeight = 12
info.value = i
info.notCheckable = 1
info.func = GroupLootDropDown_GiveLoot
UIDropDownMenu_AddButton(info)
end
end
end
end
function GroupLootDropDown_GiveLoot()
-- Ref l.234-244. At or above the threshold the assignment is confirmed first; below
-- it, it goes straight out. The popup's message takes the quality-coloured item name
-- and the candidate's own row text.
--
-- The `selectedQuality and` guard is ours, and it is required rather than defensive:
-- benilla resolves an item template asynchronously, so GetLootSlotInfo answers a nil
-- quality (and a nil name) for a row whose template is still in flight — a state the
-- real client's C-side loot record never has. Unguarded, that comparison raises. A nil
-- quality takes the below-threshold arm, which is the safe direction: the assignment
-- still happens, it just doesn't ask first.
if LootFrame.selectedQuality and LootFrame.selectedQuality >= MASTER_LOOT_THREHOLD then
local color = ITEM_QUALITY_COLORS[LootFrame.selectedQuality]
local dialog = StaticPopup_Show("CONFIRM_LOOT_DISTRIBUTION",
color.hex .. (LootFrame.selectedItemName or "") .. FONT_COLOR_CODE_CLOSE,
this:GetText())
if dialog then
dialog.data = this.value
end
else
GiveMasterLoot(LootFrame.selectedSlot, this.value)
end
CloseDropDownMenus()
end
]]></Script>
<!--
One loot row (ref LootButtonTemplate, l.4-57). The real template inherits ItemButtonTemplate
(the 37px item icon + its "xN" count) and adds, to the right of the icon, the UI-QuestItemNameFrame
parchment plate (130×62 at the button's LEFT +30; ref l.7-18) with the item name over it
(GameFontNormal, LEFT, 93×38; ref l.19-30). The real button is just the 37px icon and the name
rides off its RIGHT edge; our whole-row click model keeps the same art but sizes the button to
span icon + plate (37 icon + 8 gap → name at LEFT +45; plate at LEFT +30, 130 wide → 160 total).
-->
<Button name="BenillaLootButtonTemplate" virtual="true">
<Size><AbsDimension x="160" y="37"/></Size>
<Layers>
<!-- The item icon at the row-left (ItemButtonTemplate's 37px $parentIconTexture, ref l.4). -->
<Layer level="BORDER">
<Texture name="$parentIcon">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors>
<Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The parchment name plate behind the name (ref l.7-18): 130×62 at the icon's right,
the button's LEFT +30. -->
<Texture file="Interface\QuestFrame\UI-QuestItemNameFrame">
<Size><AbsDimension x="130" y="62"/></Size>
<Anchors>
<Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="30" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
<!-- The item name (GameFontNormal, LEFT; ref l.19-30), off the icon's right edge: the
button LEFT +45 (icon 37 + the real 8px gap). -->
<FontString name="$parentName" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="93" y="38"/></Size>
<Anchors>
<Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="45" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<Layer level="OVERLAY">
<!-- "xN" stack count INSIDE the icon's bottom-right (ItemButtonTemplate
$parentCount: NumberFontNormal, BOTTOMRIGHT -5,2 — the ref's button IS the
37px icon, ours spans the whole row, so the same inset re-bases off the
button's BOTTOMLEFT: 37-5=32. The bag slots' count is this exact shape). -->
<FontString name="$parentCount" inherits="NumberFontNormal" justifyH="RIGHT">
<Anchors>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="32" y="2"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<!-- Hover art over the ICON only (the ref's real button IS the 37px icon — our whole-row
click model keeps the row hit-rect but must not glow the name plate too; director's
loot-window note). -->
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors>
</HighlightTexture>
<Scripts>
<OnClick>BenillaLootItem_OnClick(self)</OnClick>
<OnEnter>BenillaLootItem_OnEnter(self)</OnEnter>
<OnLeave>BenillaLootItem_OnLeave(self)</OnLeave>
</Scripts>
</Button>
<Frame name="LootFrame" enableMouse="true" toplevel="true" parent="UIParent" hidden="true">
<!-- The real 256×256 window (ref l.59-60). No <Anchors> here — ShowUIPanel SetPoints this to
its slot. -->
<Size><AbsDimension x="256" y="256"/></Size>
<Layers>
<!-- The dead-target skull in the panel's portrait ring (ref LootFramePortraitOverlay,
l.73-85): Interface\TargetingFrame\TargetDead, 58×58 at the window TOPLEFT
+(10,-8). BORDER (below the ARTWORK panel) so it shows through the panel's ring cut-out.
OnShow resets it to the skull and swaps a fishing loot to FishingLoot-Icon
(decision 1086), exactly as the ref's OnShow does (l.133-140). -->
<Layer level="BORDER">
<Texture name="LootPortraitOverlay" file="Interface\TargetingFrame\TargetDead">
<Size><AbsDimension x="58" y="58"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="10" y="-8"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<!-- The single UI-LootPanel slab that IS the window art (ref l.88): no size/anchors → fills
the 256×256 owner. No <TexCoords>. The solid-colour fill is gone (the panel is opaque). -->
<Layer level="ARTWORK">
<Texture file="Interface\LootFrame\UI-LootPanel"/>
<!-- Title: the real ITEMS label (ref l.89-97) — GameFontNormal at CENTER +(-12,102). -->
<FontString name="LootTitle" inherits="GameFontNormal" text="Items">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-12" y="102"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The real row stack (ref l.139-174): row 1 at the window TOPLEFT +(24,-80), each next
row centred on the prior row's BOTTOM -4. -->
<Button name="LootButton1" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="24" y="-80"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 1)</OnLoad></Scripts>
</Button>
<Button name="LootButton2" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="LootButton1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 2)</OnLoad></Scripts>
</Button>
<Button name="LootButton3" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="LootButton2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 3)</OnLoad></Scripts>
</Button>
<Button name="LootButton4" inherits="BenillaLootButtonTemplate">
<Anchors><Anchor point="TOP" relativeTo="LootButton3" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>BenillaLootItem_OnLoad(self, 4)</OnLoad></Scripts>
</Button>
<!-- Pager (ref l.175-216): Up (page back) at the window BOTTOMLEFT +(25,16), Down (page
forward) at the Up button's RIGHT +86 — both in the bottom-left of the panel. The
ChatIcon scroll arrows are the real art (ref l.191-193, 212-214). -->
<Button name="LootUpButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="25" y="16"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
<Scripts><OnClick>LootFrame_PageUp()</OnClick></Scripts>
</Button>
<Button name="LootDownButton" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="LootUpButton" relativePoint="RIGHT"><Offset><AbsDimension x="86" y="0"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
<Scripts><OnClick>LootFrame_PageDown()</OnClick></Scripts>
</Button>
<!-- Close: the real LootCloseButton position (CENTER on the window's TOPRIGHT, -81,-26;
ref l.119-129), on the minimize-button art it already used (the real LootCloseButton
uses this very art, ref l.135-137). -->
<Button name="LootCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-81" y="-26"/></Offset></Anchor>
</Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts><OnClick>BenillaLootCloseButton_OnClick()</OnClick></Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>LootFrame_OnLoad()</OnLoad>
<OnEvent>LootFrame_OnEvent(event)</OnEvent>
<OnShow>LootFrame_OnShow()</OnShow>
<OnHide>LootFrame_OnHide()</OnHide>
</Scripts>
</Frame>
<!--
The master looter's candidate menu (ref LootFrame.xml l.233-242). Bare reference name, and
the reason is mechanical rather than stylistic — the same one decision 1254 recorded for
GroupLootFrame1..4: LootFrame_OnEvent hands ToggleDropDownMenu this frame by literal global
lookup, and a 1.12 addon that wants to extend the master-loot menu expects to find it here.
Hidden and unanchored: ToggleDropDownMenu positions it at the clicked loot row.
No `parent=`, which is the reference's own declaration (l.233, a top-level sibling of
LootFrame) and the dropdown family's rule since 1757: a menu host under `UIParent` goes
invisible with it under an `area = "full"` panel, and a hidden host seats a hidden list.
-->
<Frame name="GroupLootDropDown" inherits="UIDropDownMenuTemplate" id="1" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
</Anchors>
<Scripts>
<OnLoad>GroupLootDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
</Ui>