forked from EllesmereGaming/EllesmereUI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEllesmereUI_Kick.lua
More file actions
250 lines (238 loc) · 12.1 KB
/
Copy pathEllesmereUI_Kick.lua
File metadata and controls
250 lines (238 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
if EUI_CLIENT_BLOCKED then return end -- pre-12.1 client failsafe (EllesmereUI_ClientGate.lua)
--------------------------------------------------------------------------------
-- EllesmereUI_Kick.lua
-- Shared interrupt spell lookup and cast-bar tint helpers for nameplates
-- and unit frames.
--------------------------------------------------------------------------------
local kickSpellsByClass = {
DEATHKNIGHT = { 47528 },
WARRIOR = { 6552 },
WARLOCK = { 19647, 89766, 119910, 1276467, 132409 },
SHAMAN = { 57994 },
ROGUE = { 1766 },
PRIEST = { 15487 },
PALADIN = { 31935, 96231 },
MONK = { 116705 },
MAGE = { 2139 },
HUNTER = { 187707, 147362 },
EVOKER = { 351338 },
DRUID = { 38675, 78675, 106839 },
DEMONHUNTER = { 183752 },
}
local activeKickSpell
-- A summoned demon's interrupt beats anything the player bank still reports.
--
-- The two banks were previously treated as one pool and the loop kept the LAST match,
-- so resolution depended on this table's ORDER rather than on what the player can
-- actually cast. A Demonology Warlock with a Felguard out has Axe Toss as their only
-- interrupt, but a later Warlock entry also answered as known, overwrote it, and left
-- the cast bar reading the cooldown of a spell that never fires. Kicking changed
-- nothing on screen: the bar stayed tinted "interrupt ready" and the kick-prediction
-- tick, which reads the same spell, was wrong for the same reason. Other specs were
-- unaffected because only one of their entries ever answers.
--
-- Last-match is preserved WITHIN each bank so no other class's resolution
-- changes; only the pet-over-player precedence is new.
local function RefreshKickAbility()
local playerClass = UnitClassBase("player")
local classKicks = kickSpellsByClass[playerClass]
activeKickSpell = nil
if not classKicks then return end
local petHit, playerHit
for i = 1, #classKicks do
local spellId = classKicks[i]
if C_SpellBook and C_SpellBook.IsSpellKnownOrInSpellBook then
if Enum and Enum.SpellBookSpellBank
and C_SpellBook.IsSpellKnownOrInSpellBook(spellId, Enum.SpellBookSpellBank.Pet) then
petHit = spellId
elseif C_SpellBook.IsSpellKnownOrInSpellBook(spellId) then
playerHit = spellId
end
elseif IsSpellKnown and IsSpellKnown(spellId) then
playerHit = spellId
end
end
activeKickSpell = petHit or playerHit
end
local function ComputeCastBarTint(readyTint, baseTint)
if not activeKickSpell then
return baseTint.r, baseTint.g, baseTint.b
end
if not (C_Spell and C_Spell.GetSpellCooldownDuration) then
return baseTint.r, baseTint.g, baseTint.b
end
if not (C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean) then
return baseTint.r, baseTint.g, baseTint.b
end
local cdTime = C_Spell.GetSpellCooldownDuration(activeKickSpell)
if not (cdTime and cdTime.IsZero) then
return baseTint.r, baseTint.g, baseTint.b
end
local offCooldown = cdTime:IsZero()
local rVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.r, readyTint.r)
local gVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.g, readyTint.g)
local bVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.b, readyTint.b)
return rVal, gVal, bVal
end
EllesmereUI = EllesmereUI or {}
EllesmereUI.GetActiveKickSpell = function()
return activeKickSpell
end
EllesmereUI.RefreshKickAbility = RefreshKickAbility
EllesmereUI.ComputeCastBarTint = ComputeCastBarTint
-- Secure unit context menu (12.0.7+).
-- 12.0.7 gates SecureUnitButton_OnClick: a "menu"/"togglemenu" action is silently
-- dropped unless C_ClickBindings has a binding for that button (the default
-- RightButton -> OpenContextMenu interaction is missing for many users / wiped by
-- click-cast setups). Re-opening the menu from insecure Lua instead TAINTS it, so
-- its protected items (Set Focus -> FocusUnit, Follow, etc.) throw
-- ADDON_ACTION_FORBIDDEN. The only way the protected items work is a SECURE open.
--
-- Fix: route right-click through the UN-gated "click" secure action to a hidden
-- child SecureActionButton, whose own SecureActionButton_OnClick (NOT gated -- only
-- SecureUnitButton_OnClick is) runs "togglemenu" securely. "useparent-unit" makes
-- the proxy resolve the unit from the parent unit button, so it works for static
-- frames AND header-managed (party/raid) frames whose unit changes. Call
-- AttachSecureUnitMenu(frame) on any unit button that needs a right-click menu
-- instead of setting *type2 = "togglemenu".
-- 12.1 zoned-out raid member -> PET menu misclassification fix. The proxy's
-- "togglemenu" secure action classifies the menu through a UnitIsUnit chain
-- that checks "pet" BEFORE UnitIsPlayer, and its token special-cases cover
-- party/boss/focus/arena but NOT raid (SecureTemplates.lua SECURE_ACTIONS.
-- togglemenu, marked "Unused by Blizzard code" -- the default UI never runs
-- it, which is why base frames don't show the bug; party frames are immune
-- via the token special-case, matching the raid-only field report). For a
-- raid member whose unit data has not streamed (zoned elsewhere), the
-- engine-side UnitIsUnit(raidN, "pet") comparison can misfire and the whole
-- chain resolves PET. Post-hook the opener: a PET-family menu opening for a
-- raid/party token whose GUID is a Player is that exact misfire -- re-open
-- the correct player menu. The re-open runs from this (tainted) hook, so
-- protected items (Set Focus/Follow) can throw for THAT menu instance only;
-- the trade for not showing a pet menu on a player. Legitimate pet menus
-- (unit "pet"/"partypetN"/"raidpetN") never match the signature, and the
-- correct which comes from the TOKEN (no unit APIs -- UnitInRaid/identity
-- reads can be SECRET for exactly these unstreamed units). Installed lazily
-- with the first menu proxy; zero cost until a menu actually opens.
local menuFixHooked = false
local function InstallMenuClassifierFix()
if menuFixHooked or type(UnitPopup_OpenMenu) ~= "function" then return end
menuFixHooked = true
local reopening = false
hooksecurefunc("UnitPopup_OpenMenu", function(which, contextData)
if reopening then return end
if which ~= "PET" and which ~= "OTHERPET" and which ~= "OTHERBATTLEPET" then return end
local unit = contextData and contextData.unit
if type(unit) ~= "string" then return end
local lu = unit:lower()
local isRaidToken = lu:match("^raid[0-9]+$") ~= nil
if not isRaidToken and not lu:match("^party[0-9]+$") then return end
local guid = UnitGUID(unit)
if issecretvalue and issecretvalue(guid) then return end
if type(guid) ~= "string" or not guid:find("^Player%-") then return end
reopening = true
-- FRESH context table, never the inbound one: OpenMenu ENRICHES its
-- contextData in place (playerLocation/accountInfo) and asserts those
-- fields are nil on entry -- re-passing the first open's table throws
-- "assertion failed" at UnitPopupShared:53 (field-caught 2026-08-14;
-- the live misfire classifies as OTHERBATTLEPET, same field capture).
UnitPopup_OpenMenu(isRaidToken and "RAID_PLAYER" or "PARTY", { unit = unit })
reopening = false
end)
end
local menuProxies = setmetatable({}, { __mode = "k" })
-- 12.1: proxies are GLOBALLY NAMED so bindings can reach them via "/click
-- <name>" (macro transport). 12.1 broke the "click" secure action outright
-- (a typo: SecureTemplates.lua:564 calls HasAnyForbiddenAspects on the
-- mouse-button STRING instead of the delegate); /click hits
-- SecureActionButton_OnClick directly and is unaffected.
local proxyCounter = 0
-- Create (once) and return the hidden SecureActionButton proxy for a unit button.
-- Use this when wiring a SPECIFIC click/key binding to the menu -- it does NOT
-- touch the frame's own type attributes (so it won't clobber other bindings).
function EllesmereUI.GetSecureMenuProxy(frame)
if not frame then return end
InstallMenuClassifierFix()
local proxy = menuProxies[frame]
if not proxy then
local proxyName
proxyCounter = proxyCounter + 1
proxyName = "EUISecureMenuProxy" .. proxyCounter
proxy = CreateFrame("Button", proxyName, frame, "SecureActionButtonTemplate")
proxy:SetSize(1, 1)
proxy:SetAlpha(0)
proxy:EnableMouse(false) -- never catches real mouse; only the secure click delegate reaches it
proxy:RegisterForClicks("AnyUp")
proxy:SetAttribute("type", "togglemenu")
-- The secure resolver looks up type by BUTTON SUFFIX (RightButton -> type2);
-- the bare "type" may not fall back, so set every button explicitly.
for i = 1, 5 do proxy:SetAttribute("type" .. i, "togglemenu") end
proxy:SetAttribute("useparent-unit", true)
-- Act on mouse-up regardless of the "cast on key down" CVar. Without this,
-- SecureActionButton_OnClick's clickAction gate skips the menu action on the
-- up-click when ActionButtonUseKeyDown is on (the delegate fires an up).
proxy:SetAttribute("useOnKeyDown", false)
menuProxies[frame] = proxy
end
return proxy
end
-- Same idea as GetSecureMenuProxy but for the "target" action. 12.0.7 gates a
-- raw "target" on unit buttons unless the button has a default ClickBindings
-- Interaction binding -- only plain unmodified left-click has one, so every other
-- target binding (other buttons, modifiers, keybinds) resolves to None and is
-- dropped. Routing those through this ungated SecureActionButton proxy restores
-- them. Used only for non-left-click target bindings (see ClickCast).
local targetProxies = setmetatable({}, { __mode = "k" })
function EllesmereUI.GetSecureTargetProxy(frame)
if not frame then return end
local proxy = targetProxies[frame]
if not proxy then
local proxyName
proxyCounter = proxyCounter + 1
proxyName = "EUISecureTargetProxy" .. proxyCounter
proxy = CreateFrame("Button", proxyName, frame, "SecureActionButtonTemplate")
proxy:SetSize(1, 1)
proxy:SetAlpha(0)
proxy:EnableMouse(false) -- never catches real mouse; only the secure click delegate reaches it
proxy:RegisterForClicks("AnyUp")
proxy:SetAttribute("type", "target")
-- type looked up by button SUFFIX (RightButton -> type2); set every button.
for i = 1, 5 do proxy:SetAttribute("type" .. i, "target") end
proxy:SetAttribute("useparent-unit", true)
-- Act on the up-click regardless of the "cast on key down" CVar (same
-- clickAction gate that bit the menu proxy).
proxy:SetAttribute("useOnKeyDown", false)
targetProxies[frame] = proxy
end
return proxy
end
-- Route a unit button's default RIGHT-CLICK to the secure menu proxy via the
-- ungated "click" action. Clears any specific type2 so the wildcard governs.
function EllesmereUI.AttachSecureUnitMenu(frame)
if not frame then return end
local proxy = EllesmereUI.GetSecureMenuProxy(frame)
frame:SetAttribute("type2", nil)
-- Macro transport ("/click <proxy>") instead of the "click" action:
-- the 12.1 click action crashes on a Blizzard typo (see above).
frame:SetAttribute("*type2", "macro")
frame:SetAttribute("*macrotext2", "/click " .. proxy:GetName())
frame:SetAttribute("*clickbutton2", nil)
return proxy
end
local kickFrame = CreateFrame("Frame")
kickFrame:RegisterEvent("PLAYER_LOGIN")
kickFrame:RegisterEvent("SPELLS_CHANGED")
-- Swapping demons swaps the interrupt (Felguard's Axe Toss vs Felhunter's Spell
-- Lock), and the resolution above now reads the pet bank, so it has to re-run
-- when the pet changes. SPELLS_CHANGED covers most swaps but is not guaranteed
-- for every summon, and a stale pick here is invisible until the user kicks.
if kickFrame.RegisterUnitEvent then
kickFrame:RegisterUnitEvent("UNIT_PET", "player")
else
kickFrame:RegisterEvent("UNIT_PET")
end
kickFrame:SetScript("OnEvent", function()
RefreshKickAbility()
end)
if UnitGUID("player") then
RefreshKickAbility()
end