diff --git a/crates/benilla-app/src/target/hover.rs b/crates/benilla-app/src/target/hover.rs index 336358e2..ab8a7ace 100644 --- a/crates/benilla-app/src/target/hover.rs +++ b/crates/benilla-app/src/target/hover.rs @@ -87,7 +87,7 @@ pub(super) struct PickPose<'w, 's> { /// the picker's param limit. #[derive(bevy::ecs::system::SystemParam)] pub(super) struct TargetHoverState<'w, 's> { - /// For `IsSelectable`'s `UNIT_FIELD_CREATEDBY` clause. + /// For `IsSelectable`'s `UNIT_FIELD_CREATEDBY` clause and the pick filter's possession clause. self_guid: Res<'w, crate::net::SelfGuid>, /// `0x6e61a0`'s pick flag `0x20`, as the frame began. picks_self: Res<'w, crate::spell::PicksSelf>, @@ -101,15 +101,34 @@ pub(super) struct TargetHoverState<'w, 's> { } impl TargetHoverState<'_, '_> { + /// `0x480610`'s two exclusions, which come first and which a pick whose flags carry `0x20` + /// skips (`48062c`, [`crate::spell::PicksSelf`]; `0x481050` ORs it in only for a word + /// `0x6e61a0` passes, so outside targeting they always run): the active player (`480638`), + /// and a unit they possess, an Eye of Kilrogg or a Mind Controlled target + /// ([`super::is_possessed_by`], `48064f`-`480674`), so the cursor and the click pass through + /// it to what lies behind. + fn excludes(&self, is_self: bool, store: Option<&ObjectStore>) -> bool { + !self.picks_self.0 && (is_self || super::is_possessed_by(store, self.self_guid.0)) + } + /// Whether an object of this kind is a candidate of this pick, the posed pass and the box - /// fallback alike: every unit and player outside targeting, and while targeting the ones - /// `0x480610` passes for the word ([`crate::spell::targeting::PickChecks::admits_unit`]); - /// a corpse (`0x480816`) always outside targeting, whose pick flags carry `0x40`, and while - /// targeting only one `0x6e6260` passes. A corpse whose store has not streamed is refused - /// then. The hover and the press, which latches this pick, share it. - fn is_candidate(&self, entity: Entity, kind: EntityKind, store: Option<&ObjectStore>) -> bool { + /// fallback alike: every unit and player outside targeting but the two [`Self::excludes`] + /// drops, and while targeting the ones `0x480610` passes for the word + /// ([`crate::spell::targeting::PickChecks::admits_unit`]); a corpse (`0x480816`) always + /// outside targeting, whose pick flags carry `0x40`, and while targeting only one `0x6e6260` + /// passes. A corpse whose store has not streamed is refused then. `is_self` is the active + /// player's own body. The hover and the press, which latches this pick, share it. + fn is_candidate( + &self, + entity: Entity, + kind: EntityKind, + is_self: bool, + store: Option<&ObjectStore>, + ) -> bool { match kind { - EntityKind::Unit | EntityKind::Player => self.units.admits_unit(entity, store), + EntityKind::Unit | EntityKind::Player => { + !self.excludes(is_self, store) && self.units.admits_unit(entity, store) + } EntityKind::Corpse => { let Some(word) = self.corpse_pick.0 else { return true; @@ -249,15 +268,11 @@ pub(super) fn update_hover( let mut candidates: Vec<(Entity, u8, Vec>, Vec)> = Vec::new(); // Units with skinned parts: out of the box fallback even when the broad phase rejects them. let mut faithful: HashSet = HashSet::new(); - let picks_self = target_state.picks_self.0; for (entity, is_self, gt, net, anims, drv, store, children, mount_child, drawn, held) in &roots { - if is_self && !picks_self { - continue; - } // Units, players and corpses: the reference picks every CGObject in one trace and // switches on type at the end, below. - if !target_state.is_candidate(entity, net.kind, store) { + if !target_state.is_candidate(entity, net.kind, is_self, store) { continue; } // Not drawn, not clickable. Checked before `faithful.insert`, so an undrawn unit is out by @@ -353,16 +368,15 @@ pub(super) fn update_hover( if faithful.contains(&parent) { continue; // posed-mesh-tested above } - if !picks_self && roots.get(parent).is_ok_and(|root| root.1) { - continue; - } // The same candidates as the posed pick. A bone pile lands here: its corpse model has no // skeleton, so only this box test can pick it. let Ok((_, parent_net)) = units.get(parent) else { continue; }; - let store = roots.get(parent).ok().and_then(|root| root.6); - if !target_state.is_candidate(parent, parent_net.kind, store) { + let root = roots.get(parent).ok(); + let is_self = root.as_ref().is_some_and(|root| root.1); + let store = root.and_then(|root| root.6); + if !target_state.is_candidate(parent, parent_net.kind, is_self, store) { continue; } if let Some(t) = ray_mesh_bounds(origin, dir, aabb, gt) { @@ -1087,7 +1101,8 @@ mod tests { unit_store(2, 0, 100) } - /// The pick over `word`, the way a frame runs it: the word published, then the pick. + /// The pick over `word`, the way a frame runs it: the word published, its flags `0x20` and the + /// unit legs both, then the pick. fn pick_under(world: &mut World, word: Option) -> Hovered { let mut targeting = world.resource_mut::(); match word { @@ -1097,6 +1112,9 @@ mod tests { world .run_system_cached(crate::spell::targeting::publish_unit_pick) .expect("the word publishes"); + world + .run_system_cached(crate::spell::targeting::publish_picks_self) + .expect("the self bit publishes"); world .run_system_cached(update_hover) .expect("the pick runs"); @@ -1223,6 +1241,117 @@ mod tests { assert_eq!(hovered.distance, f32::MAX); } + /// A living Orc creature whose `UNIT_FIELD_FLAGS` are `flags`, with its `UNIT_FIELD_CHARMEDBY` + /// (10) and `UNIT_FIELD_CREATEDBY` (14) the guids `charmed_by` and `created_by`, 0 for none. + fn owned(flags: u32, charmed_by: u32, created_by: u32) -> ObjectStore { + ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[ + (35, 2), + (46, flags), + (22, 100), + (10, charmed_by), + (11, 0), + (14, created_by), + (15, 0), + ])) + } + + /// The word's plain unit bit (`0x0002`), which `0x6e61a0` takes the player for (`0x850e`), and + /// the enemy bit (`0x0080`), which it does not. + const SELF_WORD: u16 = 0x0002; + const ENEMY_WORD: u16 = 0x0080; + + /// `0x480610`, `48064f`-`480676`: a unit flagged `UNIT_FLAG_POSSESSED` whose charmer, else + /// creator, is the active player is no candidate outside targeting, or while a word without + /// `0x20` stands, so the pick passes through it to the unit behind. Us is guid 1. + #[test] + fn a_unit_you_possess_lets_the_pick_through_to_the_one_behind() { + let possessed = crate::target::UNIT_FLAG_POSSESSED; + let cases: [(&str, ObjectStore); 3] = [ + ( + "a Mind Controlled unit, charmed by us", + owned(possessed, 1, 0), + ), + ("an Eye of Kilrogg, created by us", owned(possessed, 0, 1)), + ( + "charmed by us, created by another: the charmer comes first", + owned(possessed, 1, 7), + ), + ]; + for (what, front_store) in cases { + let mut world = unit_pick_world(); + let front = unit_at(&mut world, 10, 5.0, front_store); + let rear = unit_at(&mut world, 11, 0.0, foe()); + for word in [None, Some(ENEMY_WORD)] { + let hovered = pick_under(&mut world, word); + assert_eq!( + hovered.target, + Some(rear), + "{what}: the rear, word {word:?}" + ); + assert_eq!(hovered.guid, Some(11), "{what}: word {word:?}"); + } + // The word that takes us takes it too: the exclusions are skipped (`48062c`). + assert_eq!( + pick_under(&mut world, Some(SELF_WORD)).target, + Some(front), + "{what}: while flag 0x20 stands" + ); + assert_eq!( + pick_under(&mut world, None).target, + Some(rear), + "{what}: and excluded again once the word ends" + ); + } + } + + /// With nothing behind it, the unit you possess leaves the pick empty, which is no refusal. + #[test] + fn a_lone_unit_you_possess_is_no_pick_at_all() { + let mut world = unit_pick_world(); + unit_at( + &mut world, + 10, + 0.0, + owned(crate::target::UNIT_FLAG_POSSESSED, 1, 0), + ); + let hovered = pick_under(&mut world, None); + assert_eq!((hovered.target, hovered.guid), (None, None)); + assert!(!hovered.refused); + } + + /// The exclusion asks for the flag, then for us: an owned unit that is not possessed, a unit + /// someone else possesses, and one whose charmer is another even where its creator is us, all + /// stay candidates. + #[test] + fn a_unit_you_do_not_possess_keeps_the_pick() { + let possessed = crate::target::UNIT_FLAG_POSSESSED; + let cases: [(&str, ObjectStore); 5] = [ + ("possessed, charmed by another", owned(possessed, 2, 0)), + ("possessed, created by another", owned(possessed, 0, 2)), + ( + "possessed, charmed by another, created by us: the charmer comes first", + owned(possessed, 2, 1), + ), + ( + "our own pet, created by us but not possessed", + owned(0, 0, 1), + ), + ("possessed by no one", owned(possessed, 0, 0)), + ]; + for (what, front_store) in cases { + let mut world = unit_pick_world(); + let front = unit_at(&mut world, 10, 5.0, front_store); + unit_at(&mut world, 11, 0.0, foe()); + for word in [None, Some(ENEMY_WORD), Some(SELF_WORD)] { + assert_eq!( + pick_under(&mut world, word).target, + Some(front), + "{what}: word {word:?}" + ); + } + } + } + /// `0x481116`: only a word with unit bits (`0x878e`) puts units and players in the pick. A /// ground word, a lock word and a bag-item word pick none, so a unit in front of the ground /// or the chest is no obstacle, and a unit word does pick them. diff --git a/crates/benilla-app/src/target/mod.rs b/crates/benilla-app/src/target/mod.rs index 0beae743..4006c6e1 100644 --- a/crates/benilla-app/src/target/mod.rs +++ b/crates/benilla-app/src/target/mod.rs @@ -62,7 +62,9 @@ pub(crate) use flash::CombatFlash; pub(crate) use price_discount::vendor_price_discount; #[cfg(test)] pub(crate) use price_discount::{stormwind_fixture, HUMAN_WARRIOR}; -pub(crate) use relations::{can_assist, can_attack, can_interact, corpse_friendly}; +pub(crate) use relations::{ + can_assist, can_attack, can_interact, corpse_friendly, is_possessed_by, UNIT_FLAG_POSSESSED, +}; // The attack validator's target half, run in the call that asks, for the player's Attack and the // pet bar's; the melee probe's press with nothing selected. pub(crate) use scan::{AttackNearestRequest, AttackPick}; diff --git a/crates/benilla-app/src/target/relations.rs b/crates/benilla-app/src/target/relations.rs index 5e69e49e..fedb36cb 100644 --- a/crates/benilla-app/src/target/relations.rs +++ b/crates/benilla-app/src/target/relations.rs @@ -86,6 +86,23 @@ pub(crate) fn is_selectable(store: Option<&ObjectStore>, self_guid: Option) || (self_guid.is_some() && store.0.unit_created_by() == self_guid) } +/// `UNIT_FLAG_POSSESSED`, `UNIT_FIELD_FLAGS` bit 24 (vmangos `UnitDefines.h:569`), which the +/// reference reads as `[[unit+0x110]+0xA3] & 1`. +pub(crate) const UNIT_FLAG_POSSESSED: u32 = 0x0100_0000; + +/// Whether the active player possesses this unit (Mind Control, an Eye of Kilrogg, Eyes of the +/// Beast): `UNIT_FLAG_POSSESSED` set and `UNIT_FIELD_CHARMEDBY`, else `UNIT_FIELD_CREATEDBY`, the +/// active player's guid. The own-pet resolver `0x5ee5a0` (`0x5ee626`, `0x5ee62f`) and the world +/// pick's filter `0x480610` (`0x48064f`-`0x480674`) read this same clause. No store, no owner or +/// no active player is no possession. +pub(crate) fn is_possessed_by(store: Option<&ObjectStore>, self_guid: Option) -> bool { + let (Some(store), Some(me)) = (store, self_guid) else { + return false; + }; + store.0.unit_flags() & UNIT_FLAG_POSSESSED != 0 + && store.0.unit_owner(OwnerFallback::CreatedBy) == Some(me) +} + /// `0x6067d0(player, corpse)`, the corpse's reaction gate: `0x6064c0` compares the player's /// template (`UNIT_FIELD_FACTIONTEMPLATE`) toward the dead player's race's (`0x5d7120`: the race /// byte of `CORPSE_FIELD_BYTES_1`, then `ChrRaces.dbc` column 2) with the template comparator @@ -201,4 +218,40 @@ mod tests { } assert!(!gate(0) && !gate(10), "a race with no row"); } + + /// `0x480658`-`0x480667`: the flag, then the charmer if there is one, else the creator; an + /// unset owner or an unknown active player is no possession. + #[test] + fn possession_is_the_flag_and_the_charmer_else_the_creator() { + let store = |flags: u32, charmed_by: u32, created_by: u32| { + ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[ + (46, flags), + (10, charmed_by), + (11, 0), + (14, created_by), + (15, 0), + ])) + }; + let me = Some(0x77); + let flag = UNIT_FLAG_POSSESSED; + assert!( + is_possessed_by(Some(&store(flag, 0x77, 0)), me), + "charmed by us" + ); + assert!( + is_possessed_by(Some(&store(flag, 0, 0x77)), me), + "created by us" + ); + assert!( + !is_possessed_by(Some(&store(flag, 0x99, 0x77)), me), + "a charmer other than us comes before our creation" + ); + assert!(!is_possessed_by(Some(&store(0, 0x77, 0x77)), me), "no flag"); + assert!(!is_possessed_by(Some(&store(flag, 0, 0)), me), "no owner"); + assert!( + !is_possessed_by(Some(&store(flag, 0, 0)), None), + "no active player" + ); + assert!(!is_possessed_by(None, me), "no descriptor"); + } } diff --git a/crates/benilla-app/src/ui_pet/bar.rs b/crates/benilla-app/src/ui_pet/bar.rs index 9e18e767..08ad0464 100644 --- a/crates/benilla-app/src/ui_pet/bar.rs +++ b/crates/benilla-app/src/ui_pet/bar.rs @@ -12,9 +12,9 @@ use benilla_protocol::messages::{ use benilla_ui::script::{PetActionView, UiScript}; use crate::net::{GuidIndex, ObjectStore}; +use crate::target::UNIT_FLAG_POSSESSED; use crate::ui_action::Spells; -use super::drain::UNIT_FLAG_POSSESSED; use super::PetBar; /// `GetPetActionsUsable()` (`0x4bcf70`): false while the state's bit 27 is set or the pet is diff --git a/crates/benilla-app/src/ui_pet/drain.rs b/crates/benilla-app/src/ui_pet/drain.rs index 5dda81eb..d3943aec 100644 --- a/crates/benilla-app/src/ui_pet/drain.rs +++ b/crates/benilla-app/src/ui_pet/drain.rs @@ -13,16 +13,12 @@ use benilla_protocol::messages::{ use benilla_ui::script::UiScript; use crate::net::{ClientCommand, NetCommands, ObjectStore}; -use crate::target::Selection; +use crate::target::{is_possessed_by, Selection}; use crate::ui_action::Spells; use super::bar::active_aura_press; use super::{PetBar, PetUnit}; -/// `UNIT_FLAG_POSSESSED`, `UNIT_FIELD_FLAGS` bit 24 (vmangos `UnitDefines.h:569`), which the -/// reference reads as `[[pet+0x110]+0xA3] & 1`. -pub(super) const UNIT_FLAG_POSSESSED: u32 = 0x0100_0000; - /// What a pet bar press reads and writes: `CastPetAction`, `PetAttack` and the other orders, and /// the pet book's `CastSpell(id, "pet")`. #[derive(bevy::ecs::system::SystemParam)] @@ -287,14 +283,12 @@ pub(crate) fn pet_stop_on_old_target_clear( /// `0x5ee5a0`: the unit the player possesses, which must be the bar's unit for an Attack press to /// latch (`0x4bd420`, `0x4bd42e`). It needs `UNIT_FLAG_POSSESSED` (`0x5ee626`) and charmed-by, -/// else created-by, us (`0x5ee62f`); vmangos sets that flag only with possession (Mind Control, -/// Eyes of the Beast), never for an ordinary pet. The reference also tests the active mover -/// (`0x5ee5bc`, `0x5ee5e9`), not modelled here: vmangos sets the mover with the flag. +/// else created-by, us (`0x5ee62f`), [`is_possessed_by`]; vmangos sets that flag only with +/// possession (Mind Control, Eyes of the Beast), never for an ordinary pet. The reference also +/// tests the active mover (`0x5ee5bc`, `0x5ee5e9`), not modelled here: vmangos sets the mover +/// with the flag. pub(super) fn possessing(store: Option<&ObjectStore>, self_guid: Option) -> bool { - store.is_some_and(|s| { - s.0.unit_flags() & UNIT_FLAG_POSSESSED != 0 - && s.0.unit_owner(benilla_protocol::OwnerFallback::CreatedBy) == self_guid - }) + is_possessed_by(store, self_guid) } /// The Attack order, the one press validated before it sends: the type-7 arm branches only on diff --git a/crates/benilla-app/src/ui_pet/press_tests.rs b/crates/benilla-app/src/ui_pet/press_tests.rs index 61970442..ec01bd46 100644 --- a/crates/benilla-app/src/ui_pet/press_tests.rs +++ b/crates/benilla-app/src/ui_pet/press_tests.rs @@ -297,7 +297,7 @@ fn a_possessed_units_press_starts_no_pet_gcd() { let charmed = ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[ (CHARMEDBY, ME as u32), (CHARMEDBY + 1, 0), - (FLAGS, super::drain::UNIT_FLAG_POSSESSED), + (FLAGS, crate::target::UNIT_FLAG_POSSESSED), ])); let mut rig = rig(vec![(CLAW, claw())], &[CLAW], Some(charmed)); diff --git a/crates/benilla-app/src/ui_pet/tests.rs b/crates/benilla-app/src/ui_pet/tests.rs index b7b64bc0..894988ba 100644 --- a/crates/benilla-app/src/ui_pet/tests.rs +++ b/crates/benilla-app/src/ui_pet/tests.rs @@ -9,6 +9,7 @@ use benilla_protocol::messages::{ use benilla_ui::script::PetActionView; use crate::net::{ClientCommand, NetCommands, ObjectStore}; +use crate::target::UNIT_FLAG_POSSESSED; use super::bar::*; use super::drain::*;