The mouseover unit carries the hovered guid, so UnitIsUnit("mouseover", "target") answers 1 over the target: unit snapshots take their guid (#502)
Hovering a unit now makes `UnitIsUnit("mouseover", "target")` (or any other token naming the same unit) answer 1, as in 1.12.1, where `UnitIsUnit` (`0x516070`) resolves both tokens through `0x515970` and compares their guids, and `mouseover` reads the hovered unit's guid (`0xb4e2c8`/`0xb4e2cc`, written by `0x492890`). The `mouseover` unit state was pushed with guid 0, so every such comparison answered nil.
`ui_unit::snapshot` now takes the unit's guid and sets it, so every unit state built from a snapshot carries it; the callers that set it by hand afterwards no longer do.
Fixes #496
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bacbb791db
commit
1a37909c9c
@@ -318,9 +318,8 @@ pub(super) fn feed_party(
|
||||
if Some(*guid) == self_guid {
|
||||
let (_, _, store) = self_pair?;
|
||||
let name = names.peek(*guid).map(str::to_string);
|
||||
let mut s = crate::ui_unit::snapshot(store, name, 0, chr);
|
||||
let mut s = crate::ui_unit::snapshot(store, *guid, name, 0, chr);
|
||||
s.is_player = true;
|
||||
s.guid = *guid;
|
||||
s.raid_target = group.raid_target_index(*guid);
|
||||
s.faction_group = own_group.clone();
|
||||
// A same-faction friendly player, as in `member_unit_state`.
|
||||
@@ -644,13 +643,14 @@ fn member_unit_state(
|
||||
classes: Option<&benilla_formats::ChrClasses>,
|
||||
) -> UnitState {
|
||||
let mut s = match store {
|
||||
Some(store) => crate::ui_unit::snapshot(store, Some(m.name.clone()), 0, classes),
|
||||
Some(store) => crate::ui_unit::snapshot(store, m.guid, Some(m.name.clone()), 0, classes),
|
||||
// Unstreamed: the roster record, snapshotted from the descriptor at despawn (`0x5f0880`),
|
||||
// seated at 1/1 for a member never seen (`0x4e82d0`) and patched by the wire. The
|
||||
// reference's getters read the descriptor, then the party record (`0x496400`), then the
|
||||
// pet record (`0x496420`); a `partyN` token is never a pet, so the pet leg cannot arise.
|
||||
None => UnitState {
|
||||
exists: true,
|
||||
guid: m.guid,
|
||||
name: Some(m.name.clone()),
|
||||
health: stats.and_then(|s| s.cur_hp).map_or(0, u32::from),
|
||||
max_health: stats.and_then(|s| s.max_hp).map_or(0, u32::from),
|
||||
@@ -672,7 +672,6 @@ fn member_unit_state(
|
||||
s.is_player = true;
|
||||
// A party member is always a same-faction friendly player: the popup's `UnitCanCooperate`
|
||||
// gate reads the reaction, which neither leg resolves for a party token.
|
||||
s.guid = m.guid;
|
||||
s.raid_target = group.raid_target_index(m.guid);
|
||||
s.reaction = 5;
|
||||
s.faction_group = own_group;
|
||||
|
||||
@@ -81,9 +81,7 @@ pub(super) fn feed_pet_unit(
|
||||
.resolve_unit(pet_guid, Some(store), &commands)
|
||||
.map(str::to_string);
|
||||
// No `ChrClasses.dbc`: the reference reads a class only for TYPEMASK_PLAYER.
|
||||
let mut s = snapshot(store, name, 0, None);
|
||||
s.guid = pet_guid;
|
||||
s
|
||||
snapshot(store, pet_guid, name, 0, None)
|
||||
});
|
||||
|
||||
let dirty = match (&fresh, &memory.pushed) {
|
||||
|
||||
@@ -628,7 +628,9 @@ fn drive_mouseover_tooltip(
|
||||
.resolve_unit(guid, Some(store), &commands)
|
||||
.map(str::to_string);
|
||||
let reaction = unit_reaction(rx.factions.as_deref(), &rx.reputations, store, self_store);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
// The hovered guid, the pair `0x492890` writes to `0xb4e2c8`/`0xb4e2cc` and the token
|
||||
// resolver `0x515970` reads for `"mouseover"`, so `UnitIsUnit` can match it.
|
||||
let mut s = snapshot(store, guid, name, reaction, chr);
|
||||
enrich_unit(
|
||||
&mut s,
|
||||
guid,
|
||||
|
||||
@@ -517,3 +517,87 @@ fn the_spell_feed_runs_after_the_trainer_feed() {
|
||||
feed_spell_tooltips
|
||||
));
|
||||
}
|
||||
|
||||
/// The hover pushes `"mouseover"` with the hovered guid, the pair `0x492890` writes to
|
||||
/// `0xb4e2c8`/`0xb4e2cc`: `UnitIsUnit` (`0x516070`) resolves both tokens through `0x515970` and
|
||||
/// compares guids, so hovering the target answers 1 and hovering anyone else nil.
|
||||
#[test]
|
||||
fn the_mouseover_token_carries_the_hovered_guid() {
|
||||
use benilla_protocol::ObjectFields;
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
|
||||
const ME: u64 = 0x77;
|
||||
const WOLF: u64 = 0xF130_0000_4500_0001;
|
||||
const BOAR: u64 = 0xF130_0000_4600_0002;
|
||||
|
||||
let mut app = App::new();
|
||||
let (tx, _rx) = crossbeam_channel::unbounded();
|
||||
app.insert_resource(NetCommands(tx))
|
||||
.insert_resource(crate::ui_script::UiScaleCvar(1.0))
|
||||
.init_resource::<Hovered>()
|
||||
.init_resource::<HoveredObject>()
|
||||
.init_resource::<NameCache>()
|
||||
.init_resource::<crate::net::Reputations>()
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.init_resource::<crate::go_templates::GameObjectTemplates>()
|
||||
.init_resource::<Items>()
|
||||
.init_resource::<PlayerActions>();
|
||||
app.world_mut()
|
||||
.spawn((SelfPlayer, ObjectStore(ObjectFields::default())));
|
||||
let wolf = app
|
||||
.world_mut()
|
||||
.spawn(ObjectStore(ObjectFields::default()))
|
||||
.id();
|
||||
let boar = app
|
||||
.world_mut()
|
||||
.spawn(ObjectStore(ObjectFields::default()))
|
||||
.id();
|
||||
|
||||
// `"player"` and `"target"` as the unit feed pushes them, each with its guid.
|
||||
let mut script = UiScript::new().unwrap();
|
||||
for (token, guid) in [("player", ME), ("target", WOLF)] {
|
||||
script.set_unit(
|
||||
token,
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
guid,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
app.insert_non_send_resource(script);
|
||||
|
||||
let hover = |app: &mut App, entity: Entity, guid: u64| {
|
||||
*app.world_mut().resource_mut::<Hovered>() = Hovered {
|
||||
target: Some(entity),
|
||||
guid: Some(guid),
|
||||
..Default::default()
|
||||
};
|
||||
app.world_mut()
|
||||
.run_system_once(drive_mouseover_tooltip)
|
||||
.unwrap();
|
||||
};
|
||||
let is_unit = |app: &mut App, other: &str| -> Option<i64> {
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.eval::<Option<i64>>(&format!(r#"return UnitIsUnit("mouseover", "{other}")"#))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
hover(&mut app, wolf, WOLF);
|
||||
assert_eq!(is_unit(&mut app, "target"), Some(1), "hovering the target");
|
||||
assert_eq!(is_unit(&mut app, "player"), None, "the target is not us");
|
||||
|
||||
hover(&mut app, boar, BOAR);
|
||||
assert_eq!(
|
||||
is_unit(&mut app, "target"),
|
||||
None,
|
||||
"another unit is not the target"
|
||||
);
|
||||
assert_eq!(
|
||||
is_unit(&mut app, "mouseover"),
|
||||
Some(1),
|
||||
"the token is itself"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -770,10 +770,12 @@ pub(crate) fn unit_reaction(
|
||||
ring_reaction(factions, reputations, Some(store), self_store) + 1
|
||||
}
|
||||
|
||||
/// Build a unit snapshot from a streamed descriptor, its cached name and its `UnitReaction`
|
||||
/// (`1..=7`, or `0` where none is resolved, as for `"player"`); `classes` feeds the relic column.
|
||||
/// Build a unit snapshot from a streamed descriptor, its guid, its cached name and its
|
||||
/// `UnitReaction` (`1..=7`, or `0` where none is resolved, as for `"player"`); `classes` feeds the
|
||||
/// relic column.
|
||||
pub(crate) fn snapshot(
|
||||
store: &ObjectStore,
|
||||
guid: u64,
|
||||
name: Option<String>,
|
||||
reaction: u8,
|
||||
classes: Option<&ChrClasses>,
|
||||
@@ -786,6 +788,8 @@ pub(crate) fn snapshot(
|
||||
exists: true,
|
||||
// A live descriptor is `0x468460` having succeeded, all of `UnitIsVisible` (`0x516030`).
|
||||
has_object: true,
|
||||
// What the token resolver `0x515970` yields, and `UnitIsUnit` (`0x516070`) compares.
|
||||
guid,
|
||||
name,
|
||||
// The UI getters: `UNIT_DYNFLAG_DEAD` (feign death) zeroes `UnitHealth` (`0x5174d0`) and
|
||||
// `UnitMana` (`0x517670`) but not the maxima, so its edge fires the reference's pair
|
||||
@@ -1187,12 +1191,11 @@ fn feed_units(
|
||||
let name = names
|
||||
.resolve_unit(guid.0, Some(store), &commands)
|
||||
.map(str::to_string);
|
||||
let mut s = snapshot(store, name, 0, chr);
|
||||
let mut s = snapshot(store, guid.0, name, 0, chr);
|
||||
s.is_player = true;
|
||||
// Every token pushed here is streamed, so connected; real link-death rides only the group
|
||||
// roster's status byte, which the party feed reads for its own tokens.
|
||||
s.is_connected = true;
|
||||
s.guid = guid.0;
|
||||
s.raid_target = group.raid_target_index(guid.0);
|
||||
s.faction_group = faction_group(store, factions.as_deref());
|
||||
s.faction_group_localized = faction_group_localized(store, factions.as_deref());
|
||||
@@ -1226,8 +1229,7 @@ fn feed_units(
|
||||
store,
|
||||
self_pair.map(|(s, _)| s),
|
||||
);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
s.guid = guid;
|
||||
let mut s = snapshot(store, guid, name, reaction, chr);
|
||||
s.is_connected = true;
|
||||
s.raid_target = group.raid_target_index(guid);
|
||||
s.faction_group = faction_group(store, factions.as_deref());
|
||||
@@ -1270,8 +1272,7 @@ fn feed_units(
|
||||
store,
|
||||
self_pair.map(|(s, _)| s),
|
||||
);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
s.guid = guid;
|
||||
let mut s = snapshot(store, guid, name, reaction, chr);
|
||||
s.is_connected = true;
|
||||
s.raid_target = group.raid_target_index(guid);
|
||||
s.faction_group = faction_group(store, factions.as_deref());
|
||||
@@ -1338,8 +1339,7 @@ fn feed_units(
|
||||
store,
|
||||
self_pair.map(|(s, _)| s),
|
||||
);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
s.guid = guid;
|
||||
let mut s = snapshot(store, guid, name, reaction, chr);
|
||||
s.is_connected = true;
|
||||
s.raid_target = group.raid_target_index(guid);
|
||||
s.faction_group = faction_group(store, factions.as_deref());
|
||||
@@ -1642,6 +1642,7 @@ mod tests {
|
||||
let team = |fields: &[(u16, u32)]| {
|
||||
snapshot(
|
||||
&ObjectStore(ObjectFields::from_pairs(fields)),
|
||||
0,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
@@ -2307,6 +2308,7 @@ mod tests {
|
||||
];
|
||||
let alive = snapshot(
|
||||
&ObjectStore(ObjectFields::from_pairs(&vitals)),
|
||||
0,
|
||||
Some("Hunter".into()),
|
||||
0,
|
||||
None,
|
||||
@@ -2319,6 +2321,7 @@ mod tests {
|
||||
&ObjectStore(ObjectFields::from_pairs(
|
||||
&[vitals.as_slice(), &[(DYNFLAGS, 0x20)]].concat(),
|
||||
)),
|
||||
0,
|
||||
Some("Hunter".into()),
|
||||
0,
|
||||
None,
|
||||
|
||||
Reference in New Issue
Block a user