fix: TargetUnit("npc") selects the interaction NPC, as 1.12.1 does (#800)
`TargetUnit("npc")`, `AssistUnit("npc")` and `SpellTargetUnit("npc")` did nothing with a merchant, gossip or quest window open: the app's unit-token resolver answered nobody for `npc`.
In 1.12.1 the resolver (`0x515970`) matches `npc` as a whole token (`0x515bec`, an unbounded compare, so `npctarget` stays unknown) and takes the interaction NPC's guid `[0xb4e2d0]`, which `0x4930d0` writes when the window opens.
`npc` now resolves to the held unit `InteractNpc` names. Tests cover the three verbs, a closed window and a streamed-out NPC, and `/script TargetUnit("npc")` typed into the stock chat box.
Fixes #707
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
622e1c3e7a
commit
ab909df6d9
@@ -750,6 +750,47 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `SpellTargetUnit("npc")` binds the interaction NPC (`0x6e6de1` into `0x515970`, whose `npc`
|
||||
/// compare reads `[0xb4e2d0]`), not the selection; with no window open it names no unit and
|
||||
/// the cast ends with "Out of range.".
|
||||
#[test]
|
||||
fn spell_target_unit_binds_the_interaction_npc() {
|
||||
use crate::ui_session::InteractNpc;
|
||||
|
||||
let (mut world, rx, ally) = unit_world(10.0);
|
||||
world
|
||||
.resource_mut::<crate::net::GuidIndex>()
|
||||
.0
|
||||
.insert(ALLY, ally);
|
||||
world.insert_resource(InteractNpc(Some(ally), Some(ALLY)));
|
||||
arm(&mut world, HEAL, 0x0002);
|
||||
spell_target_unit(&mut world, "npc");
|
||||
assert!(matches!(
|
||||
rx.try_recv(),
|
||||
Ok(ClientCommand::CastSpell {
|
||||
spell_id: HEAL,
|
||||
target: Some(ALLY),
|
||||
})
|
||||
));
|
||||
assert!(errors(&mut world).is_empty());
|
||||
assert!(!world.resource::<SpellTargeting>().active());
|
||||
|
||||
let (mut world, rx, ally) = unit_world(10.0);
|
||||
world
|
||||
.resource_mut::<crate::net::GuidIndex>()
|
||||
.0
|
||||
.insert(ALLY, ally);
|
||||
world.init_resource::<InteractNpc>();
|
||||
arm(&mut world, HEAL, 0x0002);
|
||||
spell_target_unit(&mut world, "npc");
|
||||
assert!(rx.try_recv().is_err(), "no window, no send");
|
||||
assert_eq!(
|
||||
errors(&mut world),
|
||||
vec![crate::ui_action::CastFail::local(HEAL, 0x59)]
|
||||
);
|
||||
assert!(!world.resource::<SpellTargeting>().active());
|
||||
}
|
||||
|
||||
/// `SpellCanTargetUnit` resolves a chain as `SpellTargetUnit` does and answers the verdict of
|
||||
/// the unit it ends on (`0x6e6d3a` into `0x515970`, then `0x6e6460`): the ally we and our pet
|
||||
/// target, in range of the spell or not, and nobody past a unit with no target.
|
||||
|
||||
@@ -443,4 +443,124 @@ mod tests {
|
||||
"THIRD targets nobody: silent, nothing deselected"
|
||||
);
|
||||
}
|
||||
|
||||
/// `TargetUnit("npc")` and `AssistUnit("npc")` name the interaction NPC, `[0xb4e2d0]`, which
|
||||
/// `0x515970` reads on a whole-token `npc` compare (`0x515bec`-`0x515c00`): the vendor whose
|
||||
/// window is open, and for an assist the unit it targets. With no window open, or the NPC no
|
||||
/// longer held, `npc` names nobody and the selection stays.
|
||||
#[test]
|
||||
fn target_and_assist_unit_name_the_interaction_npc() {
|
||||
use crate::net::{Guid, ObjectStore, SelfPlayer};
|
||||
use crate::ui_session::InteractNpc;
|
||||
use benilla_ui::script::UiScript;
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
|
||||
const ME: u64 = 1;
|
||||
const VENDOR: u64 = 0xF130_0000_9800_0001;
|
||||
const GUARD: u64 = 0xF130_0000_0000_0002;
|
||||
// `OBJECT_FIELD_TYPE` 2 and `UNIT_FIELD_TARGET` 16, a 2-field guid.
|
||||
let unit = |target: u64| {
|
||||
ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[
|
||||
(2, 0x09),
|
||||
(16, target as u32),
|
||||
(17, (target >> 32) as u32),
|
||||
]))
|
||||
};
|
||||
|
||||
let mut world = script_world();
|
||||
world.spawn((SelfPlayer, Guid(ME), unit(0)));
|
||||
let vendor = world.spawn((Guid(VENDOR), unit(GUARD))).id();
|
||||
let guard = world.spawn((Guid(GUARD), unit(0))).id();
|
||||
let index = &mut world.resource_mut::<crate::net::GuidIndex>().0;
|
||||
index.insert(VENDOR, vendor);
|
||||
index.insert(GUARD, guard);
|
||||
world.insert_resource(InteractNpc(Some(vendor), Some(VENDOR)));
|
||||
|
||||
let run = |world: &mut World, lua: &str| {
|
||||
world
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.eval::<()>(lua)
|
||||
.expect("the binding runs");
|
||||
world
|
||||
.run_system_once(
|
||||
|mut script: NonSendMut<UiScript>, mut select: ScriptSelect| {
|
||||
for request in script.take_selection_requests() {
|
||||
select.select(request);
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("the applier runs as a one-shot system");
|
||||
world.resource::<super::super::Selection>().guid
|
||||
};
|
||||
let reset = |world: &mut World| {
|
||||
let mut sel = world.resource_mut::<super::super::Selection>();
|
||||
sel.target = None;
|
||||
sel.guid = None;
|
||||
};
|
||||
|
||||
assert_eq!(run(&mut world, r#"TargetUnit("npc")"#), Some(VENDOR));
|
||||
reset(&mut world);
|
||||
assert_eq!(run(&mut world, r#"TargetUnit("NPC")"#), Some(VENDOR));
|
||||
reset(&mut world);
|
||||
assert_eq!(run(&mut world, r#"AssistUnit("npc")"#), Some(GUARD));
|
||||
|
||||
// The NPC streamed out with its window still up: the object manager holds no unit.
|
||||
world
|
||||
.resource_mut::<crate::net::GuidIndex>()
|
||||
.0
|
||||
.remove(&VENDOR);
|
||||
assert_eq!(run(&mut world, r#"TargetUnit("npc")"#), Some(GUARD));
|
||||
|
||||
// The window closed: nobody, and the selection stays.
|
||||
world
|
||||
.resource_mut::<crate::net::GuidIndex>()
|
||||
.0
|
||||
.insert(VENDOR, vendor);
|
||||
world.insert_resource(InteractNpc::default());
|
||||
assert_eq!(run(&mut world, r#"TargetUnit("npc")"#), Some(GUARD));
|
||||
}
|
||||
|
||||
/// The path a player takes: `/script TargetUnit("npc")` typed into the stock chat box runs
|
||||
/// `SlashCmdList["SCRIPT"]` (`ChatFrame.lua:1074-1075`) and selects the vendor whose window is
|
||||
/// open.
|
||||
#[test]
|
||||
fn typing_target_unit_npc_in_the_stock_chat_box_selects_the_vendor() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
use crate::net::{Guid, ObjectStore, SelfPlayer};
|
||||
use crate::ui_session::InteractNpc;
|
||||
use benilla_ui::script::UiScript;
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
|
||||
const ME: u64 = 1;
|
||||
const VENDOR: u64 = 0xF130_0000_9800_0001;
|
||||
let unit = || ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[(2, 0x09)]));
|
||||
|
||||
let mut world = script_world();
|
||||
world.insert_non_send_resource(crate::ui_script::chat_tests::chat_frame());
|
||||
world.spawn((SelfPlayer, Guid(ME), unit()));
|
||||
let vendor = world.spawn((Guid(VENDOR), unit())).id();
|
||||
world
|
||||
.resource_mut::<crate::net::GuidIndex>()
|
||||
.0
|
||||
.insert(VENDOR, vendor);
|
||||
world.insert_resource(InteractNpc(Some(vendor), Some(VENDOR)));
|
||||
|
||||
let mut script = world.non_send_resource_mut::<UiScript>();
|
||||
assert!(script.focus_editbox("ChatFrameEditBox"));
|
||||
script.char_input(r#"/script TargetUnit("npc")"#);
|
||||
assert!(script.key_input("ENTER"), "the box consumes ENTER");
|
||||
world
|
||||
.run_system_once(
|
||||
|mut script: NonSendMut<UiScript>, mut select: ScriptSelect| {
|
||||
for request in script.take_selection_requests() {
|
||||
select.select(request);
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("the applier runs as a one-shot system");
|
||||
assert_eq!(
|
||||
world.resource::<super::super::Selection>().guid,
|
||||
Some(VENDOR)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use benilla_ui::script::{ExtractedQuad, QuadContent, UiScript};
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The chat stack, fonts first so `inherits="ChatFontNormal"` resolves.
|
||||
fn chat_frame() -> UiScript {
|
||||
pub(crate) fn chat_frame() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
|
||||
@@ -794,7 +794,7 @@ mod loot_tests;
|
||||
mod group_loot_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod chat_tests;
|
||||
pub(crate) mod chat_tests;
|
||||
|
||||
/// The chat bubble's `UIMenu` kit driven as a menu: the rows' label and shortcut anchoring.
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -583,15 +583,15 @@ pub(crate) fn player_token_guid(
|
||||
|
||||
/// The one unit-token resolver, the reference's `0x515970`: case-insensitive compares, then the
|
||||
/// object manager; a caller wanting a type tests the resolved unit. [`Selection`] is a parameter
|
||||
/// because [`crate::target::SelectCommit`] holds it as `ResMut`. `npc` is recognised but
|
||||
/// unresolved here, a quiet nil.
|
||||
/// because [`crate::target::SelectCommit`] holds it as `ResMut`.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct UnitTokens<'w, 's> {
|
||||
/// `Option`, like the two below: a UI-only harness lacks their plugins; a bare `Res` panics.
|
||||
/// `Option`, like those below: a UI-only harness lacks their plugins; a bare `Res` panics.
|
||||
index: Option<Res<'w, crate::net::GuidIndex>>,
|
||||
pet: Option<Res<'w, crate::ui_pet::PetBar>>,
|
||||
hovered: Option<Res<'w, crate::target::Hovered>>,
|
||||
hovered_go: Option<Res<'w, crate::target::HoveredObject>>,
|
||||
interact: Option<Res<'w, crate::ui_session::InteractNpc>>,
|
||||
group: Res<'w, crate::ui_party::GroupState>,
|
||||
pub(crate) stores: Query<'w, 's, &'static ObjectStore>,
|
||||
me: Query<'w, 's, (Entity, &'static Guid), With<SelfPlayer>>,
|
||||
@@ -645,8 +645,8 @@ impl UnitTokens<'_, '_> {
|
||||
.and_then(|member| self.pet_of(member, true)),
|
||||
UnitBase::Raid(row) => self.raid_member(row).and_then(|guid| self.held(guid)),
|
||||
UnitBase::Party(row) => self.party_member(row).and_then(|guid| self.held(guid)),
|
||||
// The interaction NPC is unresolved here, a quiet nil.
|
||||
UnitBase::Npc => None,
|
||||
// The interaction NPC's guid, `[0xb4e2d0]` (`0x515c00`).
|
||||
UnitBase::Npc => self.held(self.interact.as_ref()?.1?),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2204,7 +2204,6 @@ mod tests {
|
||||
"targetfoo",
|
||||
"mouseoverx",
|
||||
"raid3targettargetfoo",
|
||||
"npc",
|
||||
"npctarget",
|
||||
"bogus",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user