diff --git a/crates/benilla-ui/src/script/tests/tooltip.rs b/crates/benilla-ui/src/script/tests/tooltip.rs index dcb24950..14bda3b7 100644 --- a/crates/benilla-ui/src/script/tests/tooltip.rs +++ b/crates/benilla-ui/src/script/tests/tooltip.rs @@ -162,6 +162,56 @@ fn owner_clear_and_hide_lifecycle() { assert!(s.take_errors().is_empty()); } +/// **A hover rebuild gives the addon its `OnShow` back.** The bag loop re-enters +/// `ContainerFrameItemButton_OnEnter` every `TOOLTIP_UPDATE_TIME` (`ContainerFrame.lua:693`; pfUI's +/// bag slots inherit `ContainerFrameItemButtonTemplate`, so they run it too), and that is +/// `SetOwner` + a content `Set*` — the clear wipes every line an addon appended. The reference +/// hides the plate inside `SetOwner` itself (`0x531183` calls `[vtable+0x84]` = `0x530a60`, the +/// `SetOwner(NULL)` core plus the base hide) and the content `Set*` re-shows it (`0x530a80`), so +/// every turn of the loop raises `OnHide` then `OnShow`. +/// +/// `LibItemTooltip-1.0` — MasterTradeSkills' and Gromissingcrafts' whole tooltip contract — listens +/// on a scratch child frame of `GameTooltip`, so without that pair it is told once: the enhanced +/// tooltip lasts 0.2 s and reverts to the bare one for as long as the cursor stays. +#[test] +fn every_hover_rebuild_raises_onshow_for_the_addon_that_annotates_it() { + let mut s = script(); + s.set_screen_size(800.0, 600.0); + s.run( + r#" + shows = 0 + hides = 0 + local slot = CreateFrame("Button", "BagSlot") + slot:SetPoint("CENTER", 0, 0); slot:SetWidth(10); slot:SetHeight(10) + local tt = CreateFrame("GameTooltip", "TTHover") + -- LibItemTooltip's mechanism verbatim: a scratch child of the tooltip, on OnShow. + local probe = CreateFrame("Frame", nil, tt) + probe:SetScript("OnShow", function() + shows = shows + 1 + tt:AddLine("Recipes") + tt:Show() + end) + probe:SetScript("OnHide", function() hides = hides + 1 end) + -- Start where a real hover starts: the plate is down (a `CreateFrame`d one is born shown). + tt:Hide() + shows = 0 + hides = 0 + -- Three turns of the hover loop, in the shape ContainerFrameItemButton_OnEnter has. + for i = 1, 3 do + tt:SetOwner(slot, "ANCHOR_RIGHT") + tt:SetText("Copper Ore") + end + assert(shows == 3, "every rebuild raises OnShow, got " .. tostring(shows)) + -- The first SetOwner found the plate hidden; the other two hid it. + assert(hides == 2, "a rebuild over a shown plate raises OnHide, got " .. tostring(hides)) + assert(tt:NumLines() == 2, "the addon's line survives the rebuild, got " + .. tostring(tt:NumLines())) + "#, + ) + .unwrap(); + assert!(s.take_errors().is_empty()); +} + /// `SetText` shows the tooltip and `AddLine` does not. `r, g, b` apply only when the r slot is a /// number, so `AddLine(text, "", r, g, b)` draws in the default gold, as the reference's zone /// tooltip does. diff --git a/crates/benilla-ui/src/script/tooltip/verbs.rs b/crates/benilla-ui/src/script/tooltip/verbs.rs index 07c631b9..6e0b85cb 100644 --- a/crates/benilla-ui/src/script/tooltip/verbs.rs +++ b/crates/benilla-ui/src/script/tooltip/verbs.rs @@ -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, Option)| { 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. diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index 2181346c..41aab434 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -413,3 +413,4 @@ with the load checked after each. | `GameTooltip:SetHyperlinkCompareItem` | **built** | 12 raises in one session, from pfUI's equipment-compare tooltips (`modules/eqcompare.lua:21,24,72`). Not 1.12 — it is in ClassicAPI's surface, so the same family as `UnitSpellHaste` and `C_ChatBubbles`. The earlier note here guessed at a shim (`GetItemInfo`'s `itemEquipLoc` → `GetInventorySlotInfo` → `SetInventoryItem`) and called the equip-location table "real work" — but **that table already existed**: `tooltip_item/mod.rs`'s `equip_slots_for` was written for 1.12's own `SetMerchantCompareItem`/`SetAuctionCompareItem`, and its two-seat rows (finger → 11,12; trinket → 13,14; one-hand → 16,17) are exactly what `index` is for. So the verb is `compare_against_worn` — the same shopping-tooltip body, the same `CURRENTLY_EQUIPPED` latch — reached from a link rather than a list row, registered in `compat/classic_api/tooltip.rs`. `index` is 1-based over the *candidate* slots, so an empty second seat is not a candidate and index 2 answers nothing. Arguments 3 and 4 (`shift`, `comparisonTooltip`) are accepted and ignored: that module has already made the shift decision before calling (its merchant path passes `1` with no key held) and does its own `SetOwner`/`SetPoint` for the anchoring | | SuperWoW's `LootSparkle` sparkled nothing | **fixed** — the decision was right, the switch feeding it was never on | Reported from play: *"Loot sparkle not working on a mining ore I'm looking at"*, with the unit arm visibly working on lootable mobs — which is the shape of the bug, since only the GameObject arm reads the switch. `LootSparkleMode` derived `Default`, so it booted `false` while its row registered `"1"`. A boot fires `CvarChanged` only for rows `config.toml` actually carries, and writing `"1"` over a value already `"1"` is a no-op that fires nothing either, so **nothing could ever tell the resource its real default** — not the boot, not the player toggling it. `vplates.rs`'s `VPlateMode::default` carries that warning in as many words, from `NameplateOcclusion` shipping inert the same way, and this is the same mistake one feature later. Fixed with a hand-written `Default` and the twin test `the_loot_sparkle_default_agrees_with_its_registered_cvar`, which reads the registered row rather than restating it. Swept the rest: of the 32 resources a `CvarChanged` observer writes, 6 derive `Default` (`AssistAttack`, `AuraCastFeed`, `AutoSelfCast`, `BlockTrades`, `FpsJournalSetting`, `GuildMemberNotify`) and **every one of their rows registers `"0"`**, so `LootSparkle` was the only one of this shape | | `PlayerModel:SetUnit` tracked the token instead of holding the unit | **fixed** | unitscan's alert reads `SetUnit'target'` on the rare it just found by name, and the addon restores the player's own target on the next line — so a pane that re-resolved `"target"` per paint showed the card for one frame and then emptied. Reported the moment the by-name targeting stopped leaving the target on the rare: *"unitscan is not targetting anymore, very nice, but now it only shows the 3d model when I do target"*. The reference resolves the token in `SetUnit`'s own rebuild worker (`0x505b50`, the one `RefreshUnit` also runs) and nothing re-resolves it per paint — which is why FrameXML re-issues `SetUnit` from `PaperDollFrame_OnShow` and on `UNIT_MODEL_CHANGED`, and why pfUI can detect a portrait change with **no event at all** by pinning a scratch pane and comparing `GetModel` against the shown one (`api/unitframes.lua:2069`), a test that only says anything if a pane holds what it was given. `ModelState::unit_guid` is that pin, resolved at the call and read by one helper (`Model::unit_pane_subject`) for both the paint gate and the extract. A pane seated **before** its unit streamed in keeps re-reading its token, since our subject arrives asynchronously where the reference's is already in the object manager; `RefreshUnit` re-pins, as its worker does. Test: `a_unit_pane_keeps_the_unit_it_was_given_when_the_token_moves_on`. Still true and still not built: `GetModel()` answers nil on a unit pane here, so pfUI's eventless detection leg can never fire — its event leg does, and that is what draws its portraits | +| An item tooltip's rebuild was silent, so an addon's lines lasted one refresh | **fixed** | 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, and then the plate reverts to the bare one and stays bare while the cursor sits still. The loop behind it is stock: `ContainerFrameItemButton_OnUpdate` re-enters `..._OnEnter` every `TOOLTIP_UPDATE_TIME` (`ContainerFrame.lua:693` in the player's own `patch-4.mpq`, and 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**: 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. `LibItemTooltip-1.0` — the tooltip contract of MasterTradeSkills and Gromissingcrafts — hangs entirely on a scratch child frame of `GameTooltip` listening for `OnShow`, which is why exactly those two addons' lines vanished while pfUI's `ItemID` and sell-price lines, hooked on the `Set*` methods themselves, survived every refresh. All addresses read off the player's own `WoW.exe` |