Files
benilla-octo/crates/benilla-app/assets/ui/LootFrame.xml
T
SamandClaude Fable 5 c0b7a613a9 the stable master is in, and master loot hands drops to the raid
- the stable master: `MSG_LIST_STABLED_PETS` both ways, the four mutations answered by one result byte, two slots priced from `StableSlotPrices.dbc` (500 and 50000 copper — the cap falls out of the data, corroborated by vmangos's `MAX_PET_STABLES` and the reference's `NUM_PET_STABLE_SLOTS`), rows seated by slot never by position (a petless hunter's list starts at a stabled row), slot 0 read from the wire because a dismissed pet has a row and no live unit, a re-list after every success because no result carries one, and `GetPetIcon` reading `CreatureFamily.dbc`'s icon column — which ships `Ability_Druid_CatForm` for both warlock families, asserted as shipped
- master loot: `CMSG_LOOT_MASTER_GIVE` (the slot is a byte here where the roll family's is a u32) and `SMSG_LOOT_MASTER_LIST`; the click gate is the wire's slot-type byte and nothing else — the reference never asks who the master looter is — so a MASTER row fires `OPEN_MASTER_LOOT_LIST` instead of a take and auto-loot skips every non-ALLOW row; the candidate array is bucketed by raid subgroup into blocks of five (a dense list would have filed every candidate under the wrong "Group N"), an unresolved name is nil so `UPDATE_MASTER_LOOT_LIST` has something to repaint, and a give re-checks the row is still MASTER before sending; found on the way: a master looter in another raid subgroup lit the crown on your own portrait
- `SetLootMethod` takes its third (threshold) argument; the reference's silent reset to Uncommon when it is absent is flagged and not adopted; the stable's model pane draws nothing yet — a stabled pet is a database row with no world entity, and a booth that renders from a bare display id is the next arc

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

675 lines
40 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), LootSlot(slot) (the row click's action), 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).
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 (`LootSlot(button.slot)`, this file's
-- model since 0084), so the gate has to be an early return here. Named divergence, not a
-- transcription slip: 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`). Moving
-- the take host-side onto the widget, and shrinking `LootSlot()` to that continuation, is
-- 1067's named follow-up.
--
-- 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
LootSlot(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
function LootFrame_OnLoad()
local frame = this
frame.page = 1
frame:RegisterEvent("LOOT_OPENED")
frame:RegisterEvent("LOOT_UPDATE")
frame:RegisterEvent("LOOT_CLOSED")
-- 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
HideUIPanel(frame)
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" 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.
-->
<Frame name="GroupLootDropDown" parent="UIParent" inherits="UIDropDownMenuTemplate" id="1" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
</Anchors>
<Scripts>
<OnLoad>GroupLootDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
</Ui>