diff --git a/crates/benilla-app/src/ui_party/feed.rs b/crates/benilla-app/src/ui_party/feed.rs index 16b8d144..7e0d87be 100644 --- a/crates/benilla-app/src/ui_party/feed.rs +++ b/crates/benilla-app/src/ui_party/feed.rs @@ -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; diff --git a/crates/benilla-app/src/ui_pet/unit.rs b/crates/benilla-app/src/ui_pet/unit.rs index bdb8d8d5..76704672 100644 --- a/crates/benilla-app/src/ui_pet/unit.rs +++ b/crates/benilla-app/src/ui_pet/unit.rs @@ -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) { diff --git a/crates/benilla-app/src/ui_tooltip/mod.rs b/crates/benilla-app/src/ui_tooltip/mod.rs index f6122ccf..6008a93e 100644 --- a/crates/benilla-app/src/ui_tooltip/mod.rs +++ b/crates/benilla-app/src/ui_tooltip/mod.rs @@ -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, diff --git a/crates/benilla-app/src/ui_tooltip/tests.rs b/crates/benilla-app/src/ui_tooltip/tests.rs index ada69261..588538fa 100644 --- a/crates/benilla-app/src/ui_tooltip/tests.rs +++ b/crates/benilla-app/src/ui_tooltip/tests.rs @@ -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::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::(); + 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 { + 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 { + app.world_mut() + .non_send_resource_mut::() + .eval::>(&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" + ); +} diff --git a/crates/benilla-app/src/ui_unit.rs b/crates/benilla-app/src/ui_unit.rs index 81f9fb1b..273448a8 100644 --- a/crates/benilla-app/src/ui_unit.rs +++ b/crates/benilla-app/src/ui_unit.rs @@ -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, 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,