corpse: each way a grave can draw no plate now says which one it was

He hovered a grave for the instrumentation and the log has **no** corpse line at
all — which, as the earlier note said, means the question is now a different
one: either no corpse object arrived, or the pick never landed on it, or the
plate refused it silently.

Two of those were still silent. The bone-pile refusal now logs too, though it is
the reference's own answer (`0x5d76d0`: bones with `CORPSE_FIELD_DYNAMIC_FLAGS`
bit 0 clear shows no name) — from the player's chair it looks identical to a
miss, and it is the likeliest shape for a grave. A picked corpse with no fields
logs as well.

`log_new_corpses` is the third leg: one line per corpse object the server sends,
with its flags, owner and chosen model, on `Added<ObjectStore>` so it touches
only the frame's new arrivals. It answers whether the grave he hovered was a
body or a bone pile without asking him to hover anything, and separates "nothing
arrived" from the two plate-side misses.

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 18:14:17 -06:00
co-authored by Claude Opus 5
parent 13989fb09b
commit 4e92fcf1a1
4 changed files with 55 additions and 2 deletions
+5 -1
View File
@@ -672,7 +672,11 @@ impl Plugin for EntitiesPlugin {
(spell_fx::fire_fx_anim_events, spell_fx::advance_fx_anim),
// A gear change re-dresses the standing visual in place, every attachment left
// alone as in the reference; a new corpse arms its Dead or Drowned pose.
(attach::redress_player_looks, corpse::pose_corpses),
(
attach::redress_player_looks,
corpse::pose_corpses,
corpse::log_new_corpses,
),
// A mount change re-seats the rig on the mount's attachment 0 or back, as the
// reference re-parents the body model (`0x712f70`, `0x713020`).
reseat_mounts,
+30
View File
@@ -102,6 +102,36 @@ pub(in crate::entities) fn ensure_bones_display(
bones.0.insert(key, dm);
}
/// Say, once per corpse the server sends, what a plate over it would have to work with.
///
/// "Mousing over a grave gives no tooltip" has three shapes and only the last is a bug in the
/// plate: no corpse object arrived at all, the mouse pick never landed on it, or the plate refused
/// it. The plate's own misses already log (`ui_tooltip::note_corpse_miss`, which now covers the
/// bone-pile refusal too), so one line per corpse object is what separates the first shape from
/// the other two — and it tells us whether the grave he hovered was a body or a bone pile, which
/// is the whole question (`0x5d76d0` shows no name over a bone pile with nothing to take).
///
/// On `Added<ObjectStore>`, so this touches only the frame's new arrivals; a later field delta
/// re-inserts the store and may say it again, which is worth having when a flag flips.
pub(super) fn log_new_corpses(
corpses: Query<(&NetEntity, &crate::net::Guid, &ObjectStore), Added<ObjectStore>>,
) {
for (net, guid, store) in &corpses {
if net.kind != EntityKind::Corpse {
continue;
}
let s = &store.0;
info!(
"corpse: {:#x} arrives — bones={} lootable={} owner={:#x} model={:?}",
guid.0,
s.corpse_is_bones(),
s.corpse_lootable(),
s.corpse_owner().unwrap_or(0),
corpse_model(net, Some(store)),
);
}
}
/// The corpse's pose is armed, once: `0x5d6260` caches the verdict at `[corpse+0x2ac]` and nothing
/// recomputes it.
#[derive(Component)]
+6 -1
View File
@@ -847,7 +847,12 @@ pub(crate) mod schedule_tests {
/// unit it picks is then committed **by GUID**, so a stale yard or two cannot select something
/// else. The pairs that would matter — its order against the script tick that reads what it
/// publishes — are declared (`UnitFeed`, `.before(UnitFeedDispatch)`).
const UPDATE_ACTIONABLE_CEILING: usize = 4_972;
/// +1 for `entities::corpse::log_new_corpses`, which reads three components and writes
/// nothing: its one pair is against the collider finisher, an exclusive `&mut World` system
/// that pairs with everything it has no declared order against. The order can only decide
/// which frame the line prints in, and `Added<ObjectStore>` still fires exactly once either
/// way.
const UPDATE_ACTIONABLE_CEILING: usize = 4_973;
const UPDATE_ACTIONABLE_SLACK: usize = 40;
fn ratchet(what: &str, n: usize, ceiling: usize, slack: usize) {
+14
View File
@@ -693,10 +693,24 @@ fn drive_mouseover_tooltip(
.filter(|_| go.is_none())
{
let Some(store) = stores.get(entity).ok() else {
note_corpse_miss(
&mut memo.corpse_miss,
guid,
"the picked corpse carries no fields",
);
return;
};
// A bone pile with nothing to take publishes no mouseover: no plate, no brighten.
if !crate::target::corpse_mouseover_eligible(store) {
// Logged too, though it is the reference's own answer (`0x5d76d0`: bones, and
// `CORPSE_FIELD_DYNAMIC_FLAGS` bit 0 clear, so nothing to take). "No tooltip on a
// grave" looks identical to the two misses below from the player's chair, and this
// line is what tells them apart.
note_corpse_miss(
&mut memo.corpse_miss,
guid,
"a bone pile with no loot, which the reference refuses too (`0x5d76d0`)",
);
if !matches!(*last, LastHover::None) {
script.world_tooltip_fade();
*last = LastHover::None;