Skip to content

Commit 40d85d9

Browse files
committed
frame: backport Frame:SetAttribute/GetAttribute + unit-frame mouseover/click
Adds native SetAttribute / SetAttributeNoHandler / GetAttribute on the base Frame registry: a per-frame, case-insensitive value store with the 3.3.5 prefix/name/suffix getter precedence, stored on the frame's own Lua table. A `unit` attribute makes the frame a mouseover source. A WorldTick poll of the engine's mouse-focus frame (GetMouseFocus's global) calls the real mouseover setter (FUN_00492890) for its unit -- 1:1 with hovering the 3D model: model highlight, mouseover tooltip, UPDATE_MOUSEOVER_UNIT, and the native GUID slot. Stomp-proof (the engine's 3D-hover setter is event-driven) and independent of the frame's scripts, so an addon setting its own OnEnter can't break it. A `type1`/`type2`/`type` attribute installs a chained OnClick on that frame only (PCall-guarded, Button-only) that dispatches target / assist / focus on the unit at click time, then runs any prior handler. No global hooks: mouseover reads the mouse-focus global; clicks use the frame's own OnClick. Deliberately does NOT MinHook the button-click dispatcher (FUN_00779540) -- SuperWoW's click-casting owns that prologue and a second inline hook corrupts the trampoline (ERROR #132).
1 parent 29e3dab commit 40d85d9

3 files changed

Lines changed: 594 additions & 0 deletions

File tree

docs/API.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ build instructions.
164164
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
165165
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
166166
- [`frame:GetEffectiveAlpha()`](#framegeteffectivealpha)
167+
- [`frame:SetAttribute` / `SetAttributeNoHandler` / `GetAttribute` (+ unit-frame mouseover)](#framesetattributename-value--framesetattributenohandlername-value--framegetattribute)
167168

168169
- [FriendList](#friendlist)
169170
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
@@ -3779,6 +3780,97 @@ UIParent:GetEffectiveAlpha() -- 1
37793780
Minimap:GetEffectiveAlpha() -- ~0.498 (0.5 truncates to 127/255)
37803781
```
37813782

3783+
### `frame:SetAttribute(name, value)` / `frame:SetAttributeNoHandler(name, value)` / `frame:GetAttribute(...)`
3784+
3785+
Backports the frame **attribute** system — a per-frame, case-insensitive
3786+
key→value store — to 1.12 as native methods on every frame. Attributes were
3787+
added in 2.0 with secure frames and don't exist in vanilla at all; `value`
3788+
can be any Lua type and round-trips exactly.
3789+
3790+
```lua
3791+
f:SetAttribute("unit", "party1")
3792+
f:GetAttribute("unit") -- "party1"
3793+
f:SetAttribute("count", 3)
3794+
f:GetAttribute("count") -- 3
3795+
f:GetAttribute("missing") -- nil
3796+
```
3797+
3798+
`GetAttribute` also has the modifier form `GetAttribute(prefix, name, suffix)`,
3799+
which tries, in order, and returns the first match (the same precedence retail
3800+
uses for `type1`/`*type1`-style resolution):
3801+
3802+
1. `prefix..name..suffix`
3803+
2. `"*"..name..suffix`
3804+
3. `prefix..name.."*"`
3805+
4. `"*"..name.."*"`
3806+
5. `name`
3807+
3808+
`SetAttributeNoHandler` is an alias of `SetAttribute` here — see the
3809+
`OnAttributeChanged` note below.
3810+
3811+
**Unit-frame mouseover — the headline use.** Setting a **string `unit`
3812+
attribute** makes the frame a mouseover source: while the cursor is over it,
3813+
the `mouseover` unit token resolves to that frame's unit. This is the piece of
3814+
SecureUnitButton behavior modern unit-frame addons rely on — and since 1.12 has
3815+
no combat lockdown or taint, no secure machinery is needed to provide it.
3816+
3817+
```lua
3818+
local f = CreateFrame("Button", "MyUnitFrame", UIParent)
3819+
f:SetWidth(120); f:SetHeight(40)
3820+
f:SetPoint("CENTER")
3821+
f:SetAttribute("unit", "party1")
3822+
-- Hover it → the `mouseover` token resolves to party1:
3823+
-- UnitName("mouseover"), UnitHealth("mouseover"), GameTooltip:SetUnit("mouseover"),
3824+
-- mouseover-cast, and UPDATE_MOUSEOVER_UNIT all work.
3825+
```
3826+
3827+
How it works, and why it's robust: rather than installing `OnEnter`/`OnLeave`
3828+
on the frame (which an addon's own `SetScript("OnEnter", …)` would overwrite —
3829+
pfUI sets `unit` *before* its scripts, for instance), this mirrors retail and
3830+
SuperWoW's `SetMouseoverUnit`. The engine's mouse-focus frame is watched once
3831+
per frame; when a hovered frame carries a `unit` attribute, the engine's **real
3832+
mouseover setter** is invoked for that unit — **1:1 with hovering the unit's 3D
3833+
model**: the model highlights, the mouseover tooltip builds, and
3834+
`UPDATE_MOUSEOVER_UNIT` fires, in addition to the GUID slot being set. So
3835+
everything that reads mouseover — the resolver's `mouseover` branch,
3836+
`GameTooltip:SetUnit("mouseover")`, `UnitX("mouseover")`, mouseover-cast — sees
3837+
it natively. Because nothing touches the frame's scripts, an addon setting its
3838+
own handlers can't break it; and it's stomp-proof (the engine's 3D-hover setter
3839+
is event-driven, so while the cursor is over UI nothing overwrites the slot). It
3840+
follows live token changes (a `unit="target"` frame tracks your current target
3841+
while hovered). Setting a string `unit` also `EnableMouse`s the frame so a bare
3842+
frame becomes hoverable at all (real unit frames already are). The `unit` value
3843+
may be any token the resolver understands — `"party1"`, `"target"`, `"focus"`,
3844+
`"nameplateN"`, or a raw GUID literal. Set `unit` to a non-string (e.g. `nil`)
3845+
to stop the binding.
3846+
3847+
**Click actions (`type1` / `type2`).** A `type` attribute makes clicking the
3848+
frame act on its `unit`:
3849+
3850+
```lua
3851+
f:SetAttribute("type1", "target") -- left-click targets the unit
3852+
f:SetAttribute("type2", "focus") -- right-click sets ClassicAPI focus to it
3853+
```
3854+
3855+
Left-click reads `type1`, right-click reads `type2`, and both fall back to a
3856+
plain `type` attribute. Supported verbs: `"target"`, `"assist"` (target the
3857+
unit's target), `"focus"`. Setting a `type*` attribute installs a **chained
3858+
`OnClick` on that frame only** (nothing global) — it reads the attributes at
3859+
click time and runs *after* any handler the frame already had. Because it chains
3860+
whatever `OnClick` is present when the `type` attribute is set, set `type*`
3861+
**after** the frame's own scripts (real addons configure attributes after
3862+
building the widget). The frame must be a **Button** registered for the click:
3863+
left is the Button default; right needs `RegisterForClicks("RightButtonUp")`,
3864+
which real unit frames already call. Other verbs (`togglemenu`, `spell`,
3865+
`macro`) aren't backported yet.
3866+
3867+
**`OnAttributeChanged` is not fired.** Retail fires this script from
3868+
`SetAttribute` (and `SetAttributeNoHandler` suppresses it). Making it a real
3869+
`SetScript`-able handler needs a co-hook on the base-frame script-name resolver
3870+
(the analog of the tooltip-side `OnTooltipSet*` work); it isn't required for the
3871+
mouseover use case, so `SetAttribute` currently doesn't fire it and the two
3872+
setters behave identically. Fast-follow.
3873+
37823874
## FriendList
37833875

37843876
### `C_FriendList.SendWhoQueryByName(name)`

src/Offsets.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,12 +1422,27 @@ enum Offsets {
14221422
// clear on `*(context+0xCFC) == self`, i.e. the IsDragging predicate.
14231423
VAR_UI_CONTEXT_PTR = 0x00CF0BD8,
14241424
OFF_UI_CONTEXT_DRAG_TARGET = 0xCFC,
1425+
// Frame currently under the mouse (the CFrameScriptObject* that
1426+
// `GetMouseFocus` returns): `*(*VAR_UI_CONTEXT_PTR + 0x7C)`. 0 when the
1427+
// cursor is over no mouse-enabled frame. `Frame::Attributes` polls this to
1428+
// drive the `mouseover` override from the hovered frame's `unit` attribute.
1429+
OFF_UI_CONTEXT_MOUSE_FOCUS = 0x7C,
14251430
FUN_SCRIPT_FRAME_SHOW = 0x00775750,
14261431
FUN_SCRIPT_FRAME_HIDE = 0x00775810,
14271432
FUN_SCRIPT_FRAME_SETMINRESIZE = 0x00776020,
14281433
FUN_SCRIPT_FRAME_SETMAXRESIZE = 0x007762A0,
14291434
FUN_SCRIPT_FRAME_GETSCRIPT = 0x00774780,
14301435
FUN_SCRIPT_FRAME_SETSCRIPT = 0x007748D0,
1436+
// `frame:EnableMouse(enable)` (Frame registry) — `Frame::Attributes` calls
1437+
// it so a unit-attributed frame registers as the mouse-focus (bare frames
1438+
// otherwise never hover). Standard `int __fastcall(void *L)` Script_* shape.
1439+
FUN_SCRIPT_FRAME_ENABLEMOUSE = 0x00777070,
1440+
// Button OnClick dispatcher — `__thiscall(button, buttonCode)` at
1441+
// 0x00779540, invoking the button's OnClick slot `[button+0x4CC]`. NOTE: do
1442+
// NOT MinHook it — SuperWoW's click-casting inline-hooks the same prologue
1443+
// and a second hook corrupts the trampoline (ERROR #132). `Frame::Attributes`
1444+
// instead installs a normal chained OnClick on the opted-in frame. Kept as
1445+
// the verified dispatch reference only.
14311446
// Frame GetAlpha (own alpha, 0..1) + Region GetParent — walked by
14321447
// Frame::Modern's GetEffectiveAlpha up the parent chain.
14331448
FUN_SCRIPT_FRAME_GETALPHA = 0x00774DC0,

0 commit comments

Comments
 (0)