world pick: while no word carries flag 0x20, a unit the active player possesses is no candidate, beside the active player, so the cursor and click reach the unit behind it (0x480610, 0x48064f-0x480674) (#659)
The world pick dropped only the active player. A unit you possess stayed a candidate, so under a Mind Control or an Eye of Kilrogg it highlighted under the cursor and a click selected it. `0x480610`, the pick's unit and player filter, applies two exclusions whenever the pick flags lack `0x20`: - the active player (`0x480638` → `0x480676`); - a unit with `UNIT_FIELD_FLAGS & 0x01000000` (`UNIT_FLAG_POSSESSED`, `0x48064f`) whose `UNIT_FIELD_CHARMEDBY`, else `UNIT_FIELD_CREATEDBY`, is the active player (`0x480658`-`0x480674`). Outside targeting the flags always lack `0x20`. `0x481050` ORs it only for a word that can bind yourself. Changes: - `TargetHoverState::excludes` holds both exclusions under the `PicksSelf` gate. `is_candidate` asks it on the posed pass and on the box path, so the two separate self skips are gone. - The possession test moves to `target::relations::is_possessed_by`. The pet bar's `possessing` now wraps it. Tests: - `a_unit_you_possess_lets_the_pick_through_to_the_one_behind`: Mind Control, an Eye of Kilrogg, and a unit charmed by you but created by another, outside targeting and under an enemy word. The rear unit wins, and under a `0x20` word the possessed unit is the pick again. - `a_lone_unit_you_possess_is_no_pick_at_all`. - `a_unit_you_do_not_possess_keeps_the_pick`, with five controls. - `possession_is_the_flag_and_the_charmer_else_the_creator`. Fixes #649 🤖 Generated with [Claude Code](https://claude.com/claude-code) 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
3a0728cde2
commit
6a36d8c351
@@ -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<AssetId<Mesh>>, Vec<Mat4>)> = Vec::new();
|
||||
// Units with skinned parts: out of the box fallback even when the broad phase rejects them.
|
||||
let mut faithful: HashSet<Entity> = 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<u16>) -> Hovered {
|
||||
let mut targeting = world.resource_mut::<crate::spell::SpellTargeting>();
|
||||
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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -86,6 +86,23 @@ pub(crate) fn is_selectable(store: Option<&ObjectStore>, self_guid: Option<u64>)
|
||||
|| (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<u64>) -> 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<u64>) -> 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
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user