signpost tooltips survive the map, and the quest log stops blanking

- a signpost or mailbox tooltip shows with Questie loaded, and after opening the map: the cursor-seated plate carries an owner, so Questie's OnShow hook no longer hides it (2255)
- picking up a quest whose template is not cached no longer blanks the quest log or jumps the selection: the missing quest is skipped, not the pass (2256)
- the spellbook's skill-line tab flashes when a spell lands in it (2252)
- the GPU meter's resolve rides the frame's own encoder, with no submit or poll of its own: −1.8 ms on Intel's DX12 driver when armed (2254)
- the audio starvation watch no longer charges a stream's own spin-up as silence (2253)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-15 17:02:24 +07:00
co-authored by Claude Fable 5.1
parent f2377ec846
commit 5c9d919a0c
16 changed files with 1492 additions and 186 deletions
@@ -139,8 +139,14 @@ pub(super) fn feed_ui_input(
// nobody can see must not eat the click or arm a tooltip, and the else-arm below is exactly the
// "no pointer here" bookkeeping (leave the hovered frame once, disarm any press/drag every
// frame) that keeps a stale gesture from firing when the UI comes back.
// The headless hover probe's aim stands in for a cursor the window does not have (2250/2255),
// so `PointerOverUi` rises over a panel and falls off it in an automated run exactly as it does
// for a person — which is what lets a rig run reproduce "open the map, close it, and the world
// under it goes quiet". A person's pointer always wins; an unarmed probe answers `None` and
// nothing here changes.
if let Some(cursor) = window
.cursor_position()
.or_else(crate::target::hover_probe_point)
.filter(|_| !ui_hidden && !synthetic)
{
// Window cursor is logical px, y-down from top-left; the UI is y-up 768-virtual units
+6
View File
@@ -1019,6 +1019,12 @@ mod shape_gate;
#[cfg(test)]
mod event_shape_gate;
/// The VERB-FIRED event gate (decision 2251) — the third question on the same seam: 1883/1889
/// compare names and 2140 compares arguments; this asks WHO fires it, because a stock file that
/// calls a verb for its side effect of an event repaints nothing when the verb fires nothing.
#[cfg(test)]
mod verb_event_gate;
/// The reference's BasicControls.xml — TEXT/message/_ERRORMESSAGE and the ScriptErrors dialog,
/// none of which benilla itself calls: every test enters from Lua the way an addon does.
#[cfg(test)]
@@ -2357,12 +2357,6 @@ mod tests {
flow works without it",
),
("ITEM_TEXT_TRANSLATION", "ItemTextFrame.lua"),
(
"LEARNED_SPELL_IN_TAB",
"SpellBookFrame.lua — the tab flash when a new spell lands in it; the reference \
fires it with the tab index on spell add, which benilla's spellbook feed does not \
derive yet (1952)",
),
("PET_UI_CLOSE", "PetPaperDollFrame.lua"),
("PET_UI_UPDATE", "PetPaperDollFrame.lua"),
("PLAYER_DAMAGE_DONE_MODS", "PaperDollFrame.lua"),
@@ -515,3 +515,53 @@ fn buff_hover_hangs_below_left_of_the_button() {
"buff tooltip hangs its TOPRIGHT on the button's BOTTOMLEFT"
);
}
/// **The cursor-seated GameObject plate carries an OWNER** — the store the reference's publisher
/// makes through the SetOwner core (`0x492a01 → 0x52ffe0(owner, 6, 0, 0)`, whose `0x53000c`
/// writes `+0x314`), and the one arm of ours that used to skip it (decision 2255).
///
/// Every other world plate reached an owner by accident, through Lua: the corner arm and the unit
/// flow both fire `OnTooltipSetDefaultAnchor`, and the stock handler calls
/// `GameTooltip:SetOwner(UIParent, …)`. The cursor arm fires nothing, so `IsOwned` answered false
/// for exactly the GENERIC(5) objects — a signpost, a mailbox — and the next test is what that
/// cost.
#[test]
fn a_cursor_seated_gameobject_plate_is_owned() {
let mut s = harness(&[]);
assert!(s.world_tooltip_gameobject("Brill", &[], Some((512.0, 384.0))));
let owned: bool = s.eval("return GameTooltip:IsOwned(UIParent)").unwrap();
assert!(
owned,
"the signpost plate is owned; errors: {:?}",
s.errors()
);
}
/// **An addon's `OnShow` hook must not hide the plate the world hover just built** — the
/// director's signpost with no tooltip (decision 2255).
///
/// `!Questie` installs an `OnShow` on GameTooltip at PLAYER_LOGIN (`Questie:hookTooltip` — it
/// installs one precisely *because* the stock plate has none) whose handler ends in
/// `GameTooltip:Show()`. Lua's `:Show()` is the reference's EXISTENCE GATE `0x530a80`: owner and
/// line count both non-zero, or it takes the effective-hide `0x530a60` instead. So an unowned
/// plate hides itself the instant it is shown — through our own faithful implementation of that
/// gate, ~26 ms after the engine built it, on every signpost, for the whole session.
///
/// The reference cannot reach that state, because its publisher writes the owner *before* the
/// plate is ever shown. With the owner written, so do we.
#[test]
fn a_cursor_seated_gameobject_plate_survives_an_addons_on_show_hook() {
let mut s = harness(&[]);
// Questie's hook, in one line: the plate's own show event calls Show() again.
s.run(r#"GameTooltip:SetScript("OnShow", function() GameTooltip:Show() end)"#)
.unwrap();
assert!(s.world_tooltip_gameobject("Brill", &[], Some((512.0, 384.0))));
let shown: bool = s
.eval("return GameTooltip:IsShown() and true or false")
.unwrap();
assert!(
shown,
"the signpost plate is still up; errors: {:?}",
s.errors()
);
}
@@ -0,0 +1,418 @@
//! **The VERB-FIRED event gate** — `reference/1.12-verb-events.tsv` against the module that
//! registers each verb (decision 2251).
//!
//! ## The third question on the event seam
//!
//! Two gates compare event NAMES (`reference_ui`: a stock listener nothing feeds, 1889; a fire the
//! reference has no name for, 1883) and one compares ARGUMENTS (`event_shape_gate`, 2140). None of
//! them asks **who** fires it, and that is the hole B389/2244 fell through. The reference's
//! `SetTrainerServiceTypeFilter` commits through `0x4d8c90`, whose whole body is *write the mask,
//! re-run the finalizer, fire `TRAINER_UPDATE`*; so the stock `Blizzard_TrainerUI.lua` never
//! repaints the list itself — it calls the verb for its side effect. benilla's verb set the mask
//! and fired nothing. The name gate stayed green because the packet arm fires the same event, and
//! the tests stayed green because they asserted the mask. Our own retired `TrainerFrame.xml` had
//! repainted explicitly, so the behaviour left with the file that was compensating for it (1957),
//! and the director found it twelve days later. Every window that went stock (1751) carries the
//! same exposure: a verb the stock Lua calls *for the event*.
//!
//! ## The rule
//!
//! For every pair the reference fires on the verb's own call path (the table: the verb's body, or
//! a helper that only registered verbs call), and every verb benilla registers: **a file that
//! registers the verb also fires the event** — or the pair is declared below, and the declaration
//! is *checked*, never trusted:
//!
//! * [`ELSEWHERE`] — benilla fires it from a named feed instead, on the state the verb changed
//! (the verb bumps a generation, sets a touched flag, queues an intent the app drains). That
//! file must fire it, and the verb's own file must not, or the row is stale.
//! * [`GAP`] — nothing fires it on this verb's path, for a stated reason. The verb's own file
//! must still not fire it, or the row is stale.
//!
//! A verb no file registers is skipped: unbuilt verbs are `scripts/api-coverage.sh`'s queue
//! (1178), not this gate's.
//!
//! ## What "registers" and "fires" mean here, and the limit
//!
//! Text, not execution: the quoted literal `"Verb"` or `"EVENT"` in a non-test source file, with
//! comment lines dropped and the trailing test module cut. Lenient in exactly one direction — any
//! file that names the verb and fires the event passes — because the honest alternative, proving
//! the path *executes*, is a per-verb fixture, and 2244's own test is the measure of a fixture that
//! asserts the wrong thing. What this cannot see: a fire that is in the file but off the verb's
//! path — an `ELSEWHERE` row is a claim the session that wrote it read at the feed. What it sees
//! every time is the migration class itself: a verb whose module fires nothing.
use std::collections::BTreeSet;
use std::path::PathBuf;
/// One row of `reference/1.12-verb-events.tsv`.
struct Pair {
verb: String,
event: String,
shape: String,
via: String,
}
/// Pairs benilla fires from a feed, on the state the verb changes. `(verb, event, the file that
/// fires it)`; the file is matched as a path suffix. Each was read at the feed when the row was
/// written — the trigger is named so the next reader can check it in one grep.
const ELSEWHERE: &[(&str, &str, &str)] = &[
// The sell slot's diff (`auction_sell_item` moved, or `sell_slot_dirty`).
(
"CalculateAuctionDeposit",
"NEW_AUCTION_UPDATE",
"benilla-app/src/ui_auction/mod.rs",
),
(
"ClickAuctionSellItemButton",
"NEW_AUCTION_UPDATE",
"benilla-app/src/ui_auction/mod.rs",
),
// The query goes to the wire; the inbox feed announces the list when it lands.
(
"CheckInbox",
"MAIL_INBOX_UPDATE",
"benilla-app/src/ui_mail/mod.rs",
),
// `craft_close` / `trade_skill_close` are drained by the app, whose store transition
// `(Some, None)` is the fire.
("CloseCraft", "CRAFT_CLOSE", "benilla-app/src/ui_craft.rs"),
(
"CloseTradeSkill",
"TRADE_SKILL_CLOSE",
"benilla-app/src/ui_tradeskill.rs",
),
// `take_quest_log_collapses`: the app owns the collapse set, re-feeds the list, fires.
(
"CollapseQuestHeader",
"QUEST_LOG_UPDATE",
"benilla-app/src/ui_quest_log.rs",
),
(
"ExpandQuestHeader",
"QUEST_LOG_UPDATE",
"benilla-app/src/ui_quest_log.rs",
),
// `trade_skill_touched`, set by all four verbs, drained by `take_trade_skill_touched`.
(
"CollapseTradeSkillSubClass",
"TRADE_SKILL_UPDATE",
"benilla-app/src/ui_tradeskill.rs",
),
(
"ExpandTradeSkillSubClass",
"TRADE_SKILL_UPDATE",
"benilla-app/src/ui_tradeskill.rs",
),
(
"SetTradeSkillInvSlotFilter",
"TRADE_SKILL_UPDATE",
"benilla-app/src/ui_tradeskill.rs",
),
(
"SetTradeSkillSubClassFilter",
"TRADE_SKILL_UPDATE",
"benilla-app/src/ui_tradeskill.rs",
),
// `repeat_count` moved (`repeat_changed`).
(
"DoTradeSkill",
"UPDATE_TRADESKILL_RECAST",
"benilla-app/src/ui_tradeskill.rs",
),
// `take_loot_roll_confirms`: a Need/Greed on a bind-on-pickup roll is diverted to the popup.
(
"ConfirmLootRoll",
"CANCEL_LOOT_ROLL",
"benilla-app/src/ui_loot_roll.rs",
),
(
"ConfirmLootRoll",
"CONFIRM_LOOT_ROLL",
"benilla-app/src/ui_loot_roll.rs",
),
(
"RollOnLoot",
"CANCEL_LOOT_ROLL",
"benilla-app/src/ui_loot_roll.rs",
),
(
"RollOnLoot",
"CONFIRM_LOOT_ROLL",
"benilla-app/src/ui_loot_roll.rs",
),
// `keybinds.generation` moves; `sync_dispatch` re-derives the table and fires.
(
"SetBinding",
"UPDATE_BINDINGS",
"benilla-app/src/bindings.rs",
),
// A `SessionRequest::StopCinematic`; the player stops, and the flip to not-playing fires.
(
"StopCinematic",
"CINEMATIC_STOP",
"benilla-app/src/cinematic.rs",
),
// The toggle goes to the wire and the bar's pushed key changes on the reply — one round trip
// later than the reference, which fires locally from the toggle.
(
"TogglePetAutocast",
"PET_BAR_UPDATE",
"benilla-app/src/ui_pet/bar.rs",
),
(
"ToggleSpellAutocast",
"PET_BAR_UPDATE",
"benilla-app/src/ui_pet/bar.rs",
),
];
/// Pairs nothing fires on the verb's path, each with its reason. A row is a gap with a name,
/// never a reason to stop; the reference's own site is in the table for the grep into wow-re.
const GAP: &[(&str, &str, &str)] = &[
(
"CancelSkillUps",
"SKILL_LINES_CHANGED",
"resets temp skill points over a table this model keeps empty (`skills.rs`), so nothing \
moves and `ui_char.rs`'s feed has nothing to announce; the reference fires from \
0x4d3e3a regardless",
),
(
"ClickTargetTradeButton",
"TRADE_REPLACE_ENCHANT",
"the trade-window enchant leg (0592 P3) is not built; the verb is the money arm only",
),
(
"CloseTrade",
"PLAYER_TRADE_MONEY",
"`ui_trade.rs` announces money only while the trade is open; the reference's close zeroes \
the offer and fires from 0x4bfdce, under a frame that TRADE_CLOSED hides first",
),
(
"CollapseCraftSkillLine",
"CRAFT_UPDATE",
"a no-op here: the craft list is flat, the header law was never ported from TradeSkill \
(0446, 0530's follow-up), so there is nothing to repaint",
),
(
"ExpandCraftSkillLine",
"CRAFT_UPDATE",
"the same no-op as CollapseCraftSkillLine",
),
(
"PetDismiss",
"PET_DISMISS_START",
"no shipped file listens; the dismiss reaches the wire through the app's drain and the \
pet frame follows the unit's departure",
),
(
"SelectGossipOption",
"GOSSIP_ENTER_CODE",
"the code-entry gossip option kind is not modelled (`reference_ui`'s UNPRODUCED row)",
),
(
"SetPetStablePaperdoll",
"PET_STABLE_UPDATE_PAPERDOLL",
"inert by design: the stable's model pane is an app-side booth that follows the selection \
every frame (1676), so the VM has nothing to point",
),
(
"UpdateMapHighlight",
"CLOSE_WORLD_MAP",
"0x4a7620 fires it (site 0x4a7835, id 373) on a path the highlight verb reaches under a \
condition nobody has read; unbuilt until carved",
),
];
fn table() -> Vec<Pair> {
let tsv = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-verb-events.tsv"
);
let text = std::fs::read_to_string(tsv).expect("reference/1.12-verb-events.tsv");
text.lines()
.filter(|l| !l.starts_with('#') && !l.starts_with("verb\t"))
.filter_map(|l| {
let f: Vec<&str> = l.split('\t').collect();
(f.len() >= 4).then(|| Pair {
verb: f[0].to_string(),
event: f[1].to_string(),
shape: f[2].to_string(),
via: f[3].to_string(),
})
})
.collect()
}
/// Every non-test `.rs` under `crates/`: comment lines dropped, the trailing test module cut.
fn sources() -> Vec<(PathBuf, String)> {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root")
.join("crates");
let mut out = Vec::new();
let mut stack = vec![root];
while let Some(dir) = stack.pop() {
for e in std::fs::read_dir(&dir).into_iter().flatten().flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
continue;
}
// The same file-name rule the sibling gates use: a test's own literals are not a
// surface we ship — and neither are the gates' own declared lists, this file's
// included (a `*_gate.rs` is a `#[cfg(test)]` module whose name does not say so).
let name = p.to_string_lossy();
if p.extension().is_none_or(|x| x != "rs")
|| name.contains("test")
|| name.ends_with("_gate.rs")
{
continue;
}
let whole = std::fs::read_to_string(&p).unwrap_or_default();
let head = whole
.rfind("#[cfg(test)]\nmod ")
.map_or(whole.as_str(), |i| &whole[..i]);
let text: String = head
.lines()
.filter(|l| !l.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
out.push((p, text));
}
}
out.sort();
out
}
fn quoted(text: &str, name: &str) -> bool {
text.contains(&format!("\"{name}\""))
}
fn short(p: &std::path::Path) -> String {
let s = p.to_string_lossy();
s.split_once("/crates/")
.map_or(s.to_string(), |(_, t)| t.to_string())
}
/// The files that register `verb`, and whether any of them fires `event`.
fn verb_module_fires(src: &[(PathBuf, String)], verb: &str, event: &str) -> (Vec<String>, bool) {
let registrars: Vec<&(PathBuf, String)> = src.iter().filter(|(_, t)| quoted(t, verb)).collect();
let fires = registrars.iter().any(|(_, t)| quoted(t, event));
(registrars.iter().map(|(p, _)| short(p)).collect(), fires)
}
/// **Every event the reference fires from inside a verb is fired by the module that registers
/// the verb** — or declared, with the declaration checked.
#[test]
fn every_event_the_reference_fires_from_a_verb_is_fired_by_the_verb_s_module() {
let src = sources();
assert!(src.len() > 100, "source walk found {} files", src.len());
let declared: BTreeSet<(&str, &str)> = ELSEWHERE
.iter()
.chain(GAP.iter())
.map(|(v, e, _)| (*v, *e))
.collect();
let mut surprises = Vec::new();
let mut seen = 0;
for pair in table() {
let (registrars, fires) = verb_module_fires(&src, &pair.verb, &pair.event);
if registrars.is_empty() {
continue; // an unbuilt verb: api-coverage.sh's queue, not this gate's
}
seen += 1;
if fires || declared.contains(&(pair.verb.as_str(), pair.event.as_str())) {
continue;
}
let fired_by: Vec<String> = src
.iter()
.filter(|(_, t)| quoted(t, &pair.event))
.map(|(p, _)| short(p))
.collect();
surprises.push(format!(
"{} -> {} [{}{}] registered in {}; {}",
pair.verb,
pair.event,
pair.shape,
if pair.via == "-" {
String::new()
} else {
format!(" via {}", pair.via)
},
registrars.join(", "),
if fired_by.is_empty() {
"fired NOWHERE in benilla".to_string()
} else {
format!("fired only by {}", fired_by.join(", "))
},
));
}
assert!(
seen > 20,
"only {seen} pairs had a registered verb — the walk or the table is off"
);
assert!(
surprises.is_empty(),
"the reference fires these events from inside the verb, and the module that registers \
the verb here fires nothing — the stock Lua that calls the verb for its side effect \
repaints nothing (2244's class). Fire it from the verb, or declare the pair: ELSEWHERE \
when a feed fires it on THIS verb's change and you read that at the feed, GAP with the \
reason otherwise:\n {}",
surprises.join("\n ")
);
}
/// **A declaration is checked, not trusted** — an `ELSEWHERE` file that stopped firing, a verb
/// module that now fires what its row says it does not, a row for a pair the table no longer
/// carries: each fails until the row is corrected.
#[test]
fn the_declared_rows_are_still_true() {
let src = sources();
let table = table();
let in_table =
|verb: &str, event: &str| table.iter().any(|p| p.verb == verb && p.event == event);
let mut wrong = Vec::new();
for (verb, event, file) in ELSEWHERE {
if !in_table(verb, event) {
wrong.push(format!(
"ELSEWHERE {verb} -> {event}: not a pair in the table any more"
));
}
let named: Vec<&(PathBuf, String)> = src
.iter()
.filter(|(p, _)| p.to_string_lossy().ends_with(file))
.collect();
if named.is_empty() {
wrong.push(format!(
"ELSEWHERE {verb} -> {event}: no source file ends with {file}"
));
} else if !named.iter().any(|(_, t)| quoted(t, event)) {
wrong.push(format!(
"ELSEWHERE {verb} -> {event}: {file} no longer fires it"
));
}
if verb_module_fires(&src, verb, event).1 {
wrong.push(format!(
"ELSEWHERE {verb} -> {event}: the verb's own module fires it now — take the row out"
));
}
}
for (verb, event, _) in GAP {
if !in_table(verb, event) {
wrong.push(format!(
"GAP {verb} -> {event}: not a pair in the table any more"
));
}
if verb_module_fires(&src, verb, event).1 {
wrong.push(format!(
"GAP {verb} -> {event}: the verb's own module fires it now — take the row out"
));
}
}
assert!(
wrong.is_empty(),
"stale declarations:\n {}",
wrong.join("\n ")
);
}