ui_tooltip: the corpse plate says why it drew none

"Mousing over player graves provides no tooltip" could have been any of three
silent `return`s on a path a player can see, and the log said nothing at all.
Each now reports itself once per corpse and reason: an empty
`CORPSE_FIELD_OWNER`, an owner name that has not answered its `CMSG_NAME_QUERY`,
or a missing `CORPSE_TOOLTIP` global.

The third is already ruled out by probe — `CORPSE_TOOLTIP` reads "Corpse of %s"
on this chain — and the gate above them is correct for a grave
(`corpse_mouseover_eligible`: a body always, a bone pile only while lootable, and
a grave is not bones). So this is the measurement, not the fix: a hover with no
`corpse plate:` line at all would mean the pick is not landing on the corpse,
which is a different bug in a different file.

The name case is normally transient, the query being a round trip away, and is
logged anyway: if it never answers, that one line is the whole diagnosis.

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 09:04:37 -06:00
co-authored by Claude Opus 5
parent aeba81ec2e
commit 92290bb18f
+29
View File
@@ -566,6 +566,22 @@ pub(crate) struct HoverMemo<'s> {
last_lines: Local<'s, crate::ui_script::VmMemo<Option<UnitState>>>,
/// The hover probe's last trace line, so a stationary probe logs it once.
trace: Local<'s, String>,
/// The last `(corpse guid, reason)` the corpse plate gave up on, so a hover that cannot draw
/// one says why **once** instead of every frame it is held.
corpse_miss: Local<'s, Option<(u64, &'static str)>>,
}
/// Report a corpse plate this hover could not build, once per corpse and reason.
///
/// Each of the three ways out below is a silent `return` on a path a player can see: hovering a
/// grave and getting nothing. "Mousing over player graves provides no tooltip" could be any of
/// them and the log said nothing at all, so the first thing to fix was the silence.
fn note_corpse_miss(last: &mut Option<(u64, &'static str)>, guid: u64, why: &'static str) {
if *last == Some((guid, why)) {
return;
}
*last = Some((guid, why));
info!("corpse plate: {guid:#x} draws none — {why}");
}
fn lines_view(s: &UnitState) -> UnitState {
@@ -692,9 +708,17 @@ fn drive_mouseover_tooltip(
}
// A corpse carries `CORPSE_FIELD_OWNER`, not a name; the plate waits for the name query.
let Some(owner) = store.0.corpse_owner() else {
note_corpse_miss(&mut memo.corpse_miss, guid, "CORPSE_FIELD_OWNER is empty");
return;
};
let Some(name) = names.resolve(owner, &commands).map(str::to_string) else {
// Normally transient — the query is in flight and the next frame has it. Logged once
// per corpse all the same: if it never answers, this line is the whole diagnosis.
note_corpse_miss(
&mut memo.corpse_miss,
guid,
"the owner's name has not answered a CMSG_NAME_QUERY yet",
);
return;
};
// `CORPSE_TOOLTIP`, the builder's key; no string, no plate.
@@ -703,6 +727,11 @@ fn drive_mouseover_tooltip(
"CORPSE_TOOLTIP",
&[Arg::S(&name)],
) else {
note_corpse_miss(
&mut memo.corpse_miss,
guid,
"the CORPSE_TOOLTIP global is missing from this chain's FrameXML",
);
return;
};
// No object id: a corpse is not a GameObject, it only borrows this plate's one gold line.