Files
benilla-octo/crates/benilla/assets/ui/UnitPopup.xml
T
SamandClaude Fable 5 f68c0cd958 /follow works end to end, and a parried swing rings out
- follow a player and the screen names who you're following, the right-click menu grows its Follow row, and your own movement is what cancels it — creatures are refused like the real client
- a locked lockbox click asks the server instead of refusing silently, a quest-starter chest offers its quest before opening, and an item on cooldown hides its green "open" line
- a parried swing plays its clang like the real client keys it
- the README's What works / Not built yet caught up on a week of shipping

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

596 lines
34 KiB
XML

<!--
The unit right-click popups (decision 0434 phase 5) — transcribed from the 1.12 FrameXML
UnitPopup.lua (the whole file: the button/menu tables, ShowMenu, HideButtons, the OnUpdate
enable pass, OnClick) + the UIParent.xml OnUpdate hook (a driver frame here — the DeathFrame
driver pattern; benilla's UIParent carries no OnUpdate).
The menus ride the dropdown kit's MENU displayMode + level-2 submenus (UIDropDownMenu.xml).
The owner frames (TargetFrameDropDown, PlayerFrameDropDown, PartyMemberFrameNDropDown) live
with their frames in UnitFrames.xml / PartyFrame.xml — this file is the shared engine, like
the ref's load split.
DEVIATIONS, deliberate (each waits for its arc):
- NO ADDED ROWS. A "PVP" row briefly sat at the head of SELF (decision 0646 §3); decision 0652
took it back out. 1.12.1 has no PvP toggle in any menu — `TogglePVP()`'s single caller in the
whole shipped UI is `SlashCmdList["PVP"]`, and `UnitPopup.lua` contains the string "PVP"
exactly zero times — so the row was a deviation that made this menu read wrong against the
reference. `/pvp` is the toggle the client has.
- DEFERRED ENTRIES: rows whose backing system doesn't exist yet are HIDDEN outright (the
BENILLA_UNITPOPUP_DEFERRED set in HideButtons) rather than shown-dead: TARGET (the friends
list), GUILD_PROMOTE/GUILD_LEAVE (the guild arc), RESET_INSTANCES (instances). TRADE came out
with decision 0592 P1, DUEL with 0633, INSPECT with 0631, FOLLOW with 0890's follow-up — and
that last entry's stated reason ("its CMSG family is its own slice") was simply WRONG: 0890
verified auto-follow sends nothing at all, it is pure client-side movement, so the row was
waiting on a wire slice that does not exist. It was the motion law it really needed.
Everything a menu SHOWS works. The PET menu and the RAID_* rank rows stay
in the tables but are unreachable (no "pet" token, no raid roster) — their OnClick branches
are trimmed with the rest.
- Lua 5.1: the ref's 5.0 `for i, v in t do` iteration is ipairs/pairs here; strsub → the
TEXT stub below keeps the table lines verbatim (the ref's TEXT() is the localization
identity).
- The newbie tooltips on rows are OUT with the kit's (default-off cvar look).
- CheckInteractDistance now really gates (decision 0631): the binding landed with the inspect
row, off wow-re's §5-VERIFIED `check_interact_dist2` table {10², 11.1111², 10², 30²}. So every
SHOWN row with a non-zero `dist` grays out of range now — INSPECT (dist 1, 10 yd), TRADE
(dist 2, 11.11 yd) and DUEL (dist 3, 10 yd) — where TRADE and DUEL shipped ungated only
because the binding was nil when they landed. The nil-guard in the OnUpdate pass below is
kept as-is.
-->
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Script><![CDATA[
-- The ref's localization identity (UIParent.lua) — keeps the table lines verbatim.
if ( not TEXT ) then
TEXT = function(s) return s end
end
UNITPOPUP_TIMEOUT = 5;
UnitPopupButtons = { };
UnitPopupButtons["CANCEL"] = { text = TEXT(CANCEL), dist = 0, space = 1 };
UnitPopupButtons["TRADE"] = { text = TEXT(TRADE), dist = 2 };
UnitPopupButtons["INSPECT"] = { text = TEXT(INSPECT), dist = 1 };
UnitPopupButtons["TARGET"] = { text = TEXT(TARGET), dist = 0 };
UnitPopupButtons["DUEL"] = { text = TEXT(DUEL), dist = 3, space = 1 };
UnitPopupButtons["WHISPER"] = { text = TEXT(WHISPER), dist = 0 };
UnitPopupButtons["INVITE"] = { text = TEXT(PARTY_INVITE), dist = 0 };
UnitPopupButtons["UNINVITE"] = { text = TEXT(PARTY_UNINVITE), dist = 0 };
UnitPopupButtons["PROMOTE"] = { text = TEXT(PARTY_PROMOTE), dist = 0 };
UnitPopupButtons["GUILD_PROMOTE"] = { text = TEXT(GUILD_PROMOTE), dist = 0 };
UnitPopupButtons["GUILD_LEAVE"] = { text = TEXT(GUILD_LEAVE), dist = 0 };
UnitPopupButtons["LEAVE"] = { text = TEXT(PARTY_LEAVE), dist = 0 };
UnitPopupButtons["FOLLOW"] = { text = TEXT(FOLLOW), dist = 4 };
UnitPopupButtons["PET_PASSIVE"] = { text = TEXT(PET_PASSIVE), dist = 0 };
UnitPopupButtons["PET_DEFENSIVE"] = { text = TEXT(PET_DEFENSIVE), dist = 0 };
UnitPopupButtons["PET_AGGRESSIVE"] = { text = TEXT(PET_AGGRESSIVE), dist = 0 };
UnitPopupButtons["PET_WAIT"] = { text = TEXT(PET_WAIT), dist = 0 };
UnitPopupButtons["PET_FOLLOW"] = { text = TEXT(PET_FOLLOW), dist = 0 };
UnitPopupButtons["PET_ATTACK"] = { text = TEXT(PET_ATTACK), dist = 0 };
UnitPopupButtons["PET_DISMISS"] = { text = TEXT(PET_DISMISS), dist = 0 };
UnitPopupButtons["PET_ABANDON"] = { text = TEXT(PET_ABANDON), dist = 0 };
UnitPopupButtons["PET_PAPERDOLL"] = { text = TEXT(PET_PAPERDOLL), dist = 0 };
UnitPopupButtons["PET_RENAME"] = { text = TEXT(PET_RENAME), dist = 0 };
UnitPopupButtons["LOOT_METHOD"] = { text = TEXT(LOOT_METHOD), dist = 0, nested = 1 };
UnitPopupButtons["FREE_FOR_ALL"] = { text = TEXT(LOOT_FREE_FOR_ALL), dist = 0 };
UnitPopupButtons["ROUND_ROBIN"] = { text = TEXT(LOOT_ROUND_ROBIN), dist = 0 };
UnitPopupButtons["MASTER_LOOTER"] = { text = TEXT(LOOT_MASTER_LOOTER), dist = 0 };
UnitPopupButtons["GROUP_LOOT"] = { text = TEXT(LOOT_GROUP_LOOT), dist = 0 };
UnitPopupButtons["NEED_BEFORE_GREED"] = { text = TEXT(LOOT_NEED_BEFORE_GREED), dist = 0 };
UnitPopupButtons["RESET_INSTANCES"] = { text = TEXT(RESET_INSTANCES), dist = 0 };
UnitPopupButtons["LOOT_THRESHOLD"] = { text = TEXT(LOOT_THRESHOLD), dist = 0, nested = 1 };
UnitPopupButtons["LOOT_PROMOTE"] = { text = TEXT(LOOT_PROMOTE), dist = 0 };
UnitPopupButtons["ITEM_QUALITY2_DESC"] = { text = TEXT(ITEM_QUALITY2_DESC), dist = 0, color = ITEM_QUALITY_COLORS[2] };
UnitPopupButtons["ITEM_QUALITY3_DESC"] = { text = TEXT(ITEM_QUALITY3_DESC), dist = 0, color = ITEM_QUALITY_COLORS[3] };
UnitPopupButtons["ITEM_QUALITY4_DESC"] = { text = TEXT(ITEM_QUALITY4_DESC), dist = 0, color = ITEM_QUALITY_COLORS[4] };
UnitPopupButtons["RAID_LEADER"] = { text = TEXT(NEW_LEADER), dist = 0 };
UnitPopupButtons["RAID_PROMOTE"] = { text = TEXT(PROMOTE), dist = 0 };
UnitPopupButtons["RAID_DEMOTE"] = { text = TEXT(DEMOTE), dist = 0 };
UnitPopupButtons["RAID_REMOVE"] = { text = TEXT(REMOVE), dist = 0 };
UnitPopupButtons["RAID_TARGET_ICON"] = { text = TEXT(RAID_TARGET_ICON), dist = 0, nested = 1 };
UnitPopupButtons["RAID_TARGET_1"] = { text = TEXT(RAID_TARGET_1), dist = 0, checkable = 1, color = {r = 1.0, g = 0.92, b = 0}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0, tCoordRight = 0.25, tCoordTop = 0, tCoordBottom = 0.25 };
UnitPopupButtons["RAID_TARGET_2"] = { text = TEXT(RAID_TARGET_2), dist = 0, checkable = 1, color = {r = 0.98, g = 0.57, b = 0}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0.25, tCoordRight = 0.5, tCoordTop = 0, tCoordBottom = 0.25 };
UnitPopupButtons["RAID_TARGET_3"] = { text = TEXT(RAID_TARGET_3), dist = 0, checkable = 1, color = {r = 0.83, g = 0.22, b = 0.9}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0.5, tCoordRight = 0.75, tCoordTop = 0, tCoordBottom = 0.25 };
UnitPopupButtons["RAID_TARGET_4"] = { text = TEXT(RAID_TARGET_4), dist = 0, checkable = 1, color = {r = 0.04, g = 0.95, b = 0}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0.75, tCoordRight = 1, tCoordTop = 0, tCoordBottom = 0.25 };
UnitPopupButtons["RAID_TARGET_5"] = { text = TEXT(RAID_TARGET_5), dist = 0, checkable = 1, color = {r = 0.7, g = 0.82, b = 0.875}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0, tCoordRight = 0.25, tCoordTop = 0.25, tCoordBottom = 0.5 };
UnitPopupButtons["RAID_TARGET_6"] = { text = TEXT(RAID_TARGET_6), dist = 0, checkable = 1, color = {r = 0, g = 0.71, b = 1}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0.25, tCoordRight = 0.5, tCoordTop = 0.25, tCoordBottom = 0.5 };
UnitPopupButtons["RAID_TARGET_7"] = { text = TEXT(RAID_TARGET_7), dist = 0, checkable = 1, color = {r = 1.0, g = 0.24, b = 0.168}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0.5, tCoordRight = 0.75, tCoordTop = 0.25, tCoordBottom = 0.5 };
UnitPopupButtons["RAID_TARGET_8"] = { text = TEXT(RAID_TARGET_8), dist = 0, checkable = 1, color = {r = 0.98, g = 0.98, b = 0.98}, icon = "Interface\\TargetingFrame\\UI-RaidTargetingIcons", tCoordLeft = 0.75, tCoordRight = 1, tCoordTop = 0.25, tCoordBottom = 0.5 };
UnitPopupButtons["RAID_TARGET_NONE"] = { text = TEXT(NONE), dist = 0, checkable = 1, };
-- First level menus
UnitPopupMenus = { };
UnitPopupMenus["SELF"] = { "LOOT_METHOD", "LOOT_THRESHOLD", "LOOT_PROMOTE", "LEAVE", "RESET_INSTANCES", "RAID_TARGET_ICON", "CANCEL" };
UnitPopupMenus["PET"] = { "PET_PAPERDOLL", "PET_RENAME", "PET_ABANDON", "PET_DISMISS", "CANCEL" };
UnitPopupMenus["PARTY"] = { "WHISPER", "PROMOTE", "LOOT_PROMOTE", "UNINVITE", "INSPECT", "TRADE", "FOLLOW", "DUEL", "RAID_TARGET_ICON", "CANCEL" };
UnitPopupMenus["PLAYER"] = { "WHISPER", "INSPECT", "INVITE", "TRADE", "FOLLOW", "DUEL", "RAID_TARGET_ICON", "CANCEL" };
UnitPopupMenus["RAID"] = { "RAID_LEADER", "RAID_PROMOTE", "RAID_DEMOTE", "RAID_REMOVE", "CANCEL" };
UnitPopupMenus["FRIEND"] = { "WHISPER", "INVITE", "TARGET", "GUILD_PROMOTE", "GUILD_LEAVE", "CANCEL" };
UnitPopupMenus["RAID_TARGET_ICON"] = { "RAID_TARGET_1", "RAID_TARGET_2", "RAID_TARGET_3", "RAID_TARGET_4", "RAID_TARGET_5", "RAID_TARGET_6", "RAID_TARGET_7", "RAID_TARGET_8", "RAID_TARGET_NONE" };
-- Second level menus
UnitPopupMenus["LOOT_METHOD"] = { "FREE_FOR_ALL", "ROUND_ROBIN", "MASTER_LOOTER", "GROUP_LOOT", "NEED_BEFORE_GREED", "CANCEL" };
UnitPopupMenus["LOOT_THRESHOLD"] = { "ITEM_QUALITY2_DESC", "ITEM_QUALITY3_DESC", "ITEM_QUALITY4_DESC", "CANCEL" };
UnitPopupShown = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
UnitLootMethod = {};
UnitLootMethod["freeforall"] = { text = LOOT_FREE_FOR_ALL };
UnitLootMethod["roundrobin"] = { text = LOOT_ROUND_ROBIN };
UnitLootMethod["master"] = { text = LOOT_MASTER_LOOTER };
UnitLootMethod["group"] = { text = LOOT_GROUP_LOOT };
UnitLootMethod["needbeforegreed"] = { text = LOOT_NEED_BEFORE_GREED };
UnitPopupFrames = {
"PlayerFrameDropDown",
"TargetFrameDropDown",
"PartyMemberFrame1DropDown",
"PartyMemberFrame2DropDown",
"PartyMemberFrame3DropDown",
"PartyMemberFrame4DropDown",
};
-- The DEFERRED set (header): entries hidden until their backing arc lands.
BENILLA_UNITPOPUP_DEFERRED = {
TARGET = 1,
GUILD_PROMOTE = 1, GUILD_LEAVE = 1, RESET_INSTANCES = 1,
};
function UnitPopup_ShowMenu(dropdownMenu, which, unit, name, userData)
-- Init variables
dropdownMenu.which = which;
dropdownMenu.unit = unit;
local server = nil;
if ( unit and not name ) then
name = UnitName(unit);
end
dropdownMenu.name = name;
dropdownMenu.userData = userData;
dropdownMenu.server = server;
-- Determine which buttons should be shown or hidden
UnitPopup_HideButtons();
-- If only one menu item (the cancel button) then don't show the menu
local count = 0;
for index, value in ipairs(UnitPopupMenus[which]) do
if ( UnitPopupShown[index] == 1 and value ~= "CANCEL" ) then
count = count + 1;
end
end
if ( count < 1 ) then
return;
end
-- Reflect the live loot method/threshold into the nested rows' labels (ref).
local lootMethod = GetLootMethod();
dropdownMenu.selectedLootMethod = UnitLootMethod[lootMethod].text;
UnitPopupButtons["LOOT_METHOD"].text = dropdownMenu.selectedLootMethod;
dropdownMenu.selectedLootThreshold = getglobal("ITEM_QUALITY"..GetLootThreshold().."_DESC");
UnitPopupButtons["LOOT_THRESHOLD"].text = dropdownMenu.selectedLootThreshold;
-- A non-leader can VIEW the loot settings but not open the submenus (ref).
if ( ((GetNumPartyMembers() > 0) or (GetNumRaidMembers() > 0)) and IsPartyLeader() ) then
UnitPopupButtons["LOOT_METHOD"].nested = 1;
UnitPopupButtons["LOOT_THRESHOLD"].nested = 1;
else
UnitPopupButtons["LOOT_METHOD"].nested = nil;
UnitPopupButtons["LOOT_THRESHOLD"].nested = nil;
end
-- If level 2 dropdown
local info;
local color;
if ( UIDROPDOWNMENU_MENU_LEVEL == 2 ) then
dropdownMenu.which = UIDROPDOWNMENU_MENU_VALUE;
OPEN_DROPDOWNMENUS[UIDROPDOWNMENU_MENU_LEVEL] = {which = dropdownMenu.which, unit = dropdownMenu.unit};
for index, value in ipairs(UnitPopupMenus[UIDROPDOWNMENU_MENU_VALUE]) do
info = {};
info.text = UnitPopupButtons[value].text;
info.owner = UIDROPDOWNMENU_MENU_VALUE;
color = UnitPopupButtons[value].color;
if ( color ) then
info.textR = color.r;
info.textG = color.g;
info.textB = color.b;
end
info.icon = UnitPopupButtons[value].icon;
info.tCoordLeft = UnitPopupButtons[value].tCoordLeft;
info.tCoordRight = UnitPopupButtons[value].tCoordRight;
info.tCoordTop = UnitPopupButtons[value].tCoordTop;
info.tCoordBottom = UnitPopupButtons[value].tCoordBottom;
-- Checked conditions
if ( info.text == dropdownMenu.selectedLootMethod ) then
info.checked = 1;
elseif ( info.text == dropdownMenu.selectedLootThreshold ) then
info.checked = 1;
elseif ( string.sub(value, 1, 12) == "RAID_TARGET_" ) then
local raidTargetIndex = GetRaidTargetIndex(unit);
if ( raidTargetIndex == index ) then
info.checked = 1;
end
end
info.value = value;
info.func = UnitPopup_OnClick;
UIDropDownMenu_AddButton(info, UIDROPDOWNMENU_MENU_LEVEL);
end
return;
end
-- Add dropdown title
if ( unit or name ) then
info = {};
if ( name ) then
info.text = name;
else
info.text = TEXT(UNKNOWN);
end
info.isTitle = 1;
info.notCheckable = 1;
UIDropDownMenu_AddButton(info);
end
-- Set which menu is being opened
OPEN_DROPDOWNMENUS[UIDROPDOWNMENU_MENU_LEVEL] = {which = dropdownMenu.which, unit = dropdownMenu.unit};
-- Show the buttons which are used by this menu
for index, value in ipairs(UnitPopupMenus[which]) do
if ( UnitPopupShown[index] == 1 ) then
info = {};
info.text = UnitPopupButtons[value].text;
info.value = value;
info.owner = which;
info.func = UnitPopup_OnClick;
if ( not UnitPopupButtons[value].checkable ) then
info.notCheckable = 1;
end
-- Text color
if ( value == "LOOT_THRESHOLD" ) then
color = ITEM_QUALITY_COLORS[GetLootThreshold()];
if ( color ) then
info.textR = color.r;
info.textG = color.g;
info.textB = color.b;
end
else
color = UnitPopupButtons[value].color;
if ( color ) then
info.textR = color.r;
info.textG = color.g;
info.textB = color.b;
end
end
-- Icons
info.icon = UnitPopupButtons[value].icon;
info.tCoordLeft = UnitPopupButtons[value].tCoordLeft;
info.tCoordRight = UnitPopupButtons[value].tCoordRight;
info.tCoordTop = UnitPopupButtons[value].tCoordTop;
info.tCoordBottom = UnitPopupButtons[value].tCoordBottom;
-- Checked conditions
if ( string.sub(value, 1, 12) == "RAID_TARGET_" ) then
local raidTargetIndex = GetRaidTargetIndex("target");
if ( raidTargetIndex == index ) then
info.checked = 1;
end
end
if ( UnitPopupButtons[value].nested ) then
info.hasArrow = 1;
end
UIDropDownMenu_AddButton(info);
end
end
PlaySound("igMainMenuOpen");
end
function UnitPopup_HideButtons()
local dropdownMenu = getglobal(UIDROPDOWNMENU_INIT_MENU);
local inParty = 0;
if ( (GetNumPartyMembers() > 0) or (GetNumRaidMembers() > 0) ) then
inParty = 1;
end
local isLeader = 0;
if ( inParty == 1 and IsPartyLeader() ) then
isLeader = 1;
end
local isAssistant = 0;
if ( IsRaidOfficer() ) then
isAssistant = 1;
end
local canCoop = 0;
if ( dropdownMenu.unit and UnitCanCooperate("player", dropdownMenu.unit) ) then
canCoop = 1;
end
for index, value in ipairs(UnitPopupMenus[dropdownMenu.which]) do
UnitPopupShown[index] = 1;
if ( BENILLA_UNITPOPUP_DEFERRED[value] ) then
-- Hidden until its arc lands (header) — checked FIRST so the ref chain
-- below never resurrects one.
UnitPopupShown[index] = 0;
elseif ( value == "DUEL" ) then
-- Ref UnitPopup.lua l.311: shown only for a unit we can cooperate with — an
-- enemy-faction player is already attackable, so there is nothing to arrange.
if ( canCoop == 0 ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "TRADE" ) then
-- Only shown for a unit we can cooperate with (ref UnitPopup.lua l.285) — you
-- cannot trade an enemy-faction player, so the row is hidden, not just doomed.
if ( canCoop == 0 ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "INVITE" ) then
if ( dropdownMenu.unit ) then
if ( canCoop == 0 ) then
UnitPopupShown[index] = 0;
end
else
if ( dropdownMenu.name == UnitName("party1") or
dropdownMenu.name == UnitName("party2") or
dropdownMenu.name == UnitName("party3") or
dropdownMenu.name == UnitName("party4") or
dropdownMenu.name == UnitName("player")) then
UnitPopupShown[index] = 0;
end
end
elseif ( value == "WHISPER" ) then
if ( (dropdownMenu.unit and canCoop == 0) or (dropdownMenu.name == UnitName("player")) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "PROMOTE" ) then
if ( (inParty == 0) or (isLeader == 0) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "UNINVITE" ) then
if ( (inParty == 0) or (isLeader == 0) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "LEAVE" ) then
if ( inParty == 0 ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "FREE_FOR_ALL" ) then
if ( (inParty == 0) or ((isLeader == 0) and (GetLootMethod() ~= "freeforall")) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "ROUND_ROBIN" ) then
if ( (inParty == 0) or ((isLeader == 0) and (GetLootMethod() ~= "roundrobin")) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "MASTER_LOOTER" ) then
if ( (inParty == 0) or ((isLeader == 0) and (GetLootMethod() ~= "master")) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "GROUP_LOOT" ) then
if ( (inParty == 0) or ((isLeader == 0) and (GetLootMethod() ~= "group")) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "NEED_BEFORE_GREED" ) then
if ( (inParty == 0) or ((isLeader == 0) and (GetLootMethod() ~= "needbeforegreed")) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "LOOT_THRESHOLD" ) then
if ( inParty == 0 ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "LOOT_PROMOTE" ) then
local lootMethod, lootMaster = GetLootMethod();
if ( (inParty == 0) or (isLeader == 0) or (lootMethod ~= "master") or (lootMaster and (dropdownMenu.unit == "party"..lootMaster)) or ((dropdownMenu.unit == "player") and lootMaster and (lootMaster == 0)) ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "LOOT_METHOD" ) then
if ( inParty == 0 ) then
UnitPopupShown[index] = 0;
end
elseif ( value == "RAID_LEADER" or value == "RAID_PROMOTE" or value == "RAID_DEMOTE" or value == "RAID_REMOVE" ) then
-- The raid roster (GetRaidRosterInfo) is a later arc — no raid menus open yet.
UnitPopupShown[index] = 0;
elseif ( value == "PET_PAPERDOLL" or value == "PET_RENAME" or value == "PET_ABANDON" or value == "PET_DISMISS" ) then
-- The pet system is a later arc — the PET menu never opens (no "pet" token).
UnitPopupShown[index] = 0;
elseif ( string.sub(value, 1, 12) == "RAID_TARGET_" ) then
if ( (inParty == 0) or ((isLeader == 0) and (isAssistant == 0)) ) then
UnitPopupShown[index] = 0;
end
if ( not (dropdownMenu.which == "SELF") ) then
if ( UnitExists("target") and not UnitInParty("target") ) then
if ( UnitIsPlayer("target") and (not UnitCanCooperate("player", "target") and not UnitIsUnit("target", "player")) ) then
UnitPopupShown[index] = 0;
end
end
end
end
end
end
-- The per-frame enable/disable pass over every open unit-popup menu (ref; driven by the
-- BenillaUnitPopupDriver below — the ref hooks it off UIParent's OnUpdate).
function UnitPopup_OnUpdate(elapsed)
if ( not DropDownList1:IsVisible() ) then
return;
else
-- If none of the unitpopup frames own the open menu then return
local found = nil;
for index, value in ipairs(UnitPopupFrames) do
if ( UIDROPDOWNMENU_OPEN_MENU == value ) then
found = 1;
break;
end
end
if ( not found ) then
return;
end
end
local inParty = 0;
if ( (GetNumPartyMembers() > 0) or (GetNumRaidMembers() > 0) ) then
inParty = 1;
end
local isLeader = 0;
if ( inParty == 1 and IsPartyLeader() ) then
isLeader = 1;
end
local isAssistant = 0;
if ( IsRaidOfficer() ) then
isAssistant = 1;
end
-- Loop through all menus and enable/disable their buttons appropriately
local count;
for level, dropdownFrame in pairs(OPEN_DROPDOWNMENUS) do
if ( dropdownFrame ) then
count = 0;
for index, value in ipairs(UnitPopupMenus[dropdownFrame.which]) do
if ( UnitPopupShown[index] == 1 ) then
count = count + 1;
local enable = 1;
if ( UnitPopupButtons[value].dist > 0 and CheckInteractDistance ) then
if ( not CheckInteractDistance(dropdownFrame.unit, UnitPopupButtons[value].dist) ) then
enable = 0;
end
end
if ( value == "LEAVE" ) then
if ( inParty == 0 ) then
enable = 0;
end
elseif ( value == "INVITE" ) then
if ( inParty == 1 and (isLeader == 0 and isAssistant == 0) ) then
enable = 0;
end
elseif ( value == "UNINVITE" or value == "PROMOTE" ) then
if ( inParty == 0 or isLeader == 0 ) then
enable = 0;
end
elseif ( value == "DUEL" ) then
-- Ref UnitPopup.lua l.502-505, verbatim: greyed while either
-- party is dead or a ghost, or while we lack full control
-- (feared/charmed) — `HasFullControl` is nil-guarded like
-- CheckInteractDistance above: benilla models no loss of control
-- yet, so the clause is inert rather than an error. The row also
-- carries dist = 3, so the shared range leg gates it too.
if ( UnitIsDeadOrGhost("player") or (HasFullControl and not HasFullControl()) or UnitIsDeadOrGhost(dropdownFrame.unit) ) then
enable = 0;
end
elseif ( value == "WHISPER" ) then
if ( dropdownFrame.unit and not UnitIsConnected(dropdownFrame.unit) ) then
enable = 0;
end
elseif ( value == "LOOT_PROMOTE" ) then
local lootMethod, lootMaster = GetLootMethod();
if ( (inParty == 0) or (isLeader == 0) or (lootMethod ~= "master") ) then
enable = 0;
else
local masterName = 0;
if ( not lootMaster or (lootMaster == 0) ) then
masterName = "player";
else
masterName = "party"..lootMaster;
end
if ( dropdownFrame.unit and UnitIsUnit(dropdownFrame.unit, masterName) ) then
enable = 0;
end
end
end
if ( enable == 1 ) then
UIDropDownMenu_EnableButton(level, count+1);
else
UIDropDownMenu_DisableButton(level, count+1);
end
end
end
end
end
end
function UnitPopup_OnClick()
local dropdownFrame = getglobal(UIDROPDOWNMENU_INIT_MENU);
local button = this.value;
local unit = dropdownFrame.unit;
local name = dropdownFrame.name;
if ( button == "WHISPER" ) then
ChatFrame_SendTell(name);
elseif ( button == "INVITE" ) then
if ( unit ) then
InviteToParty(unit);
else
InviteByName(name);
end
elseif ( button == "UNINVITE" ) then
UninviteFromParty(unit);
elseif ( button == "PROMOTE" ) then
PromoteToPartyLeader(unit);
elseif ( button == "LEAVE" ) then
LeaveParty();
elseif ( button == "FREE_FOR_ALL" ) then
SetLootMethod("freeforall");
UIDropDownMenu_SetButtonText(1, 2, UnitPopupButtons[button].text);
UIDropDownMenu_Refresh(dropdownFrame, nil, 1);
elseif ( button == "ROUND_ROBIN" ) then
SetLootMethod("roundrobin");
UIDropDownMenu_SetButtonText(1, 2, UnitPopupButtons[button].text);
UIDropDownMenu_Refresh(dropdownFrame, nil, 1);
elseif ( button == "MASTER_LOOTER" ) then
SetLootMethod("master", name);
UIDropDownMenu_SetButtonText(1, 2, UnitPopupButtons[button].text);
UIDropDownMenu_Refresh(dropdownFrame, nil, 1);
elseif ( button == "GROUP_LOOT" ) then
SetLootMethod("group");
UIDropDownMenu_SetButtonText(1, 2, UnitPopupButtons[button].text);
UIDropDownMenu_Refresh(dropdownFrame, nil, 1);
elseif ( button == "NEED_BEFORE_GREED" ) then
SetLootMethod("needbeforegreed");
UIDropDownMenu_SetButtonText(1, 2, UnitPopupButtons[button].text);
UIDropDownMenu_Refresh(dropdownFrame, nil, 1);
elseif ( button == "LOOT_PROMOTE" ) then
SetLootMethod("master", name);
elseif ( button == "ITEM_QUALITY2_DESC" or button == "ITEM_QUALITY3_DESC" or button == "ITEM_QUALITY4_DESC" ) then
SetLootThreshold(this:GetID()+1);
local color = ITEM_QUALITY_COLORS[this:GetID()+1];
UIDropDownMenu_SetButtonText(1, 3, UnitPopupButtons[button].text, color.r, color.g, color.b);
elseif ( string.sub(button, 1, 12) == "RAID_TARGET_" and button ~= "RAID_TARGET_ICON" ) then
local raidTargetIndex = string.sub(button, 13);
if ( raidTargetIndex == "NONE" ) then
raidTargetIndex = 0;
end
SetRaidTargetIcon(unit, tonumber(raidTargetIndex));
elseif ( button == "DUEL" ) then
-- Challenge to a duel (decision 0633): the app resolves the unit token → player
-- guid → a cast of the duel spell (ref UnitPopup_OnClick l.559, StartDuelUnit).
StartDuelUnit(unit);
elseif ( button == "TRADE" ) then
-- Offer to trade (decision 0592 P1): the app resolves the unit token → player guid
-- → CMSG_INITIATE_TRADE (ref UnitPopup_OnClick's TRADE arm, InitiateTrade(unit)).
InitiateTrade(unit);
elseif ( button == "INSPECT" ) then
-- Open the inspect window (decision 0631) — the ref UnitPopup_OnClick's INSPECT arm
-- verbatim: InspectUnit(unit), which gates on CanInspect and sends CMSG_INSPECT.
InspectUnit(unit);
elseif ( button == "FOLLOW" ) then
-- Begin auto-following — the ref UnitPopup_OnClick's FOLLOW arm verbatim, by NAME
-- and EXACT. That `1` is the resolver's exact-only flag, and it matters: this menu
-- already knows the unit's name to the letter, so unlike `/follow rag` it must not
-- prefix-match its way onto a bystander. Client-side movement only, no wire.
FollowByName(name, 1);
end
-- DEFERRED branches trimmed with their rows (header): TARGET,
-- GUILD_*, RESET_INSTANCES, PET_*, RAID_LEADER/PROMOTE/DEMOTE/REMOVE.
PlaySound("UChatScrollButton");
end
]]></Script>
<!-- The OnUpdate driver (ref UIParent.xml l.21 runs UnitPopup_OnUpdate from UIParent's own
OnUpdate; ours has none — the DeathFrame hidden-driver pattern instead). -->
<Frame name="BenillaUnitPopupDriver" parent="UIParent" hidden="false">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnUpdate>UnitPopup_OnUpdate(arg1)</OnUpdate>
</Scripts>
</Frame>
</Ui>