tooltip: SetOwner hides the plate, so a rebuild gives the addon its OnShow back

Reported from play with a video: hovering a bag item draws the enhanced tooltip
— MasterTradeSkills' recipe block, AdvancedTradeSkillWindow2's Bags/Bank/Total
— for about nine frames of sixty, then the plate falls back to the bare one and
stays bare while the cursor sits still.

The loop is stock. `ContainerFrameItemButton_OnUpdate` re-enters `..._OnEnter`
every `TOOLTIP_UPDATE_TIME` (`ContainerFrame.lua:693` in his own patch-4.mpq;
pfUI's bag slots are `ContainerFrameItemButtonTemplate`, so they run it too),
and each turn is `SetOwner` + `SetBagItem` — a clear that wipes every line an
addon appended. What puts them back in the reference is a hide/show pair, and
we had neither half: the `SetOwner` binding hides the plate before it even reads
the owner argument (`0x531179` zeroes the content gate at `+0xd0`, `0x531183`
calls `[vtable+0x84]` = `0x530a60`, which is the `SetOwner(NULL)` core plus the
base hide `0x76ad50`), and the content `Set*` re-shows it through the tooltip's
own Show override (`0x530a80`). So every rebuild raises `OnHide` then `OnShow`.
Ours cleared in place, and the base show early-outs on an already-visible frame
(`0x76ae3b`), so the plate never hid and the `OnShow` never came.

That is why exactly two addons lost their lines while others kept theirs:
`LibItemTooltip-1.0`, the tooltip contract of MasterTradeSkills and
Gromissingcrafts, hangs entirely on a scratch child frame of `GameTooltip`
listening for `OnShow`, where pfUI's ItemID and sell-price lines hook the `Set*`
methods themselves and so re-run on every refresh.

The reference also clears twice per `SetOwner` (once inside the hide, once in
the core) and so fires `OnTooltipCleared` twice; that falls out of calling the
same hide, and the existing lifecycle test already allowed for it.

Addresses read off the player's own `WoW.exe` with objdump, not from memory.
The new test is the hover loop in miniature — three turns of `SetOwner` + a
content `Set*` under a `LibItemTooltip`-shaped child probe — and fails on the
old behaviour with one `OnShow` instead of three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-17 15:38:36 -06:00
co-authored by Claude Opus 5
parent 1287c328e0
commit ed92574cea
3 changed files with 70 additions and 0 deletions
@@ -60,6 +60,25 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_function(
|lua, (this, owner, anchor, x, y): (Table, Table, Value, Option<f32>, Option<f32>)| {
let h = frame_handle_of(lua, &this)?;
// **The plate hides first.** The binding zeroes the content gate (`+0xd0`,
// `0x531179`) and calls the tooltip's own virtual `Hide` (`0x531183`,
// `[vtable+0x84]` = `0x530a60`) on *itself* before it even reads the owner
// argument; that `Hide` is the `SetOwner(NULL)` core (`0x52ffe0`: clear, so
// `OnTooltipCleared`) followed by the base hide (`0x76ad50`, so `OnHide`). The
// content `Set*` that follows then re-shows it through `0x530a80`, and the base
// show raises `OnShow` because the frame really was hidden (`0x76ae3b` early-outs
// on an already-visible one).
//
// That hide/show pair is the **only** signal an addon gets that the plate was
// wiped and wants its lines back, and the hover loop wipes it five times a second:
// `ContainerFrameItemButton_OnUpdate` re-enters `..._OnEnter` every
// `TOOLTIP_UPDATE_TIME` (`ContainerFrame.lua:693`, and pfUI's bag slots inherit
// `ContainerFrameItemButtonTemplate`), which is `SetOwner` + `SetBagItem`. Without
// the hide the rebuild is silent, so an addon's lines live 0.2 s and never come
// back: `LibItemTooltip-1.0` (MasterTradeSkills, Gromissingcrafts) hangs its whole
// contract on a scratch child frame's `OnShow`, and pfQuest and pfUI read the
// tooltip from `OnShow` too.
super::hide_tooltip(lua, h);
let owner_h = frame_handle_of(lua, &owner)?;
// The gate is `lua_isstring` (`0x6f3510`): a string or number is read, and any
// other type counts as absent rather than raising.