/target, /assist and /follow, and right click opens a clam
- /target and /assist find units by name like the original, and /follow walks you behind your target - right click opens clams and lockboxes, and the tooltip promises it - the freeze thaws clean, poses release instead of sticking, and you drop to neutral standing before it grips - unit frame art layers over the bars like the original Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ use benilla_protocol::{decode, EntityKind, WorldSession, WORLD_PORT};
|
||||
use clap::Parser;
|
||||
|
||||
use probes::{
|
||||
Attack, Aura, Charge, Ctx, Death, EquipPackSlot, GiverStatus, GroundFx, Loot, Probe,
|
||||
Attack, Aura, Charge, Ctx, Death, EquipPackSlot, GiverStatus, GroundFx, Loot, OpenItem, Probe,
|
||||
QueryNames, Quest, QuestItem, QuestLog, Speed, Spells, Spirit, SwapPackSlots, UsePackSlot,
|
||||
Vendor, WorldState,
|
||||
};
|
||||
@@ -81,6 +81,14 @@ struct Cli {
|
||||
/// delta or a destroy on that item's guid (consumed), or an explicit cast-result refusal.
|
||||
#[arg(long)]
|
||||
use_pack_slot: Option<u8>,
|
||||
/// Live-verify `CMSG_OPEN_ITEM`: `.additem` a Small Barnacled Clam (entry 7973 — LOOTABLE,
|
||||
/// LockID 0, the director's own case), find it in the backpack, and run the fork a bag
|
||||
/// right-click makes for an *openable* item — `CMSG_OPEN_ITEM(bagIndex, slot)`, never
|
||||
/// `CMSG_USE_ITEM` (a clam has no on-use spell, so the use goes nowhere) — requiring
|
||||
/// `SMSG_LOOT_RESPONSE` on the **item's own guid**. Releases the window and subtracts the copy
|
||||
/// afterwards. Needs a GM account (the deploy's probes are gmlevel 6).
|
||||
#[arg(long)]
|
||||
open_item: bool,
|
||||
/// Send a `/say` line right after entering the world — the GM dot-command channel
|
||||
/// (`.additem 3732`, `.repairitems`, …) for setting up probe scenarios. Repeatable; the lines go
|
||||
/// out in order (e.g. `--say ".modify money 5000000" --say ".go creature 1"`).
|
||||
@@ -344,6 +352,9 @@ fn main() -> Result<()> {
|
||||
if let Some(n) = cli.use_pack_slot {
|
||||
probes.push(Box::new(UsePackSlot { n }));
|
||||
}
|
||||
if cli.open_item {
|
||||
probes.push(Box::new(OpenItem));
|
||||
}
|
||||
if let Some(n) = cli.equip_pack_slot {
|
||||
probes.push(Box::new(EquipPackSlot { n }));
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ mod equip_pack_slot;
|
||||
mod giverstatus;
|
||||
mod groundfx;
|
||||
mod loot;
|
||||
mod open_item;
|
||||
mod query_names;
|
||||
mod quest;
|
||||
mod quest_item;
|
||||
@@ -87,6 +88,7 @@ pub(crate) use equip_pack_slot::EquipPackSlot;
|
||||
pub(crate) use giverstatus::GiverStatus;
|
||||
pub(crate) use groundfx::GroundFx;
|
||||
pub(crate) use loot::Loot;
|
||||
pub(crate) use open_item::OpenItem;
|
||||
pub(crate) use query_names::QueryNames;
|
||||
pub(crate) use quest::Quest;
|
||||
pub(crate) use quest_item::QuestItem;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
//! `--open-item`: the openable-item wire — the fork a bag right-click makes when the clicked
|
||||
//! item's template carries the LOOTABLE bit (`ItemInfo::openable`). The client does NOT send
|
||||
//! `CMSG_USE_ITEM` for such an item (a clam has no on-use spell at all, so the use goes nowhere —
|
||||
//! the director's "there is no way to open clams"): it sends `CMSG_OPEN_ITEM(bagIndex, slot)`, and
|
||||
//! the server answers `SMSG_LOOT_RESPONSE` **on the item's own guid**, i.e. a loot window over a
|
||||
//! thing in your bag rather than a corpse in the world.
|
||||
//!
|
||||
//! This probe proves the whole round trip against the live server: add a clam, open it by bag
|
||||
//! position, require a loot response carrying rows on that guid, then release the window and
|
||||
//! subtract the copy so the character is left as found.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use benilla_protocol::{decode, SessionEvent};
|
||||
|
||||
use crate::probes::{Ctx, Probe};
|
||||
|
||||
/// The probe target: "Small Barnacled Clam" (entry 7973) — the director's own case. Template
|
||||
/// `Flags` carries LOOTABLE (`0x4`) with `LockID = 0`, so it is openable the moment it exists: no
|
||||
/// key, no lockpicking, no rigging. Its `item_loot_template` rows are what come back.
|
||||
const ITEM_ENTRY: u32 = 7973;
|
||||
|
||||
pub(crate) struct OpenItem;
|
||||
|
||||
impl Probe for OpenItem {
|
||||
fn stage(&mut self, cx: &mut Ctx) -> Result<()> {
|
||||
cx.session.send_chat(&format!(".additem {ITEM_ENTRY}"))?;
|
||||
println!("sent GM: .additem {ITEM_ENTRY}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify(&mut self, cx: &mut Ctx) -> Result<()> {
|
||||
let world = &mut *cx.world;
|
||||
let session = &mut *cx.session;
|
||||
|
||||
// 1) Find the clam in the backpack, and remember WHICH slot — the wire addresses the item
|
||||
// by position (bag 255 + the absolute player-array slot), never by guid.
|
||||
let sf = world
|
||||
.self_fields
|
||||
.as_ref()
|
||||
.context("no self descriptor — can't walk the backpack")?;
|
||||
let (slot0, item_guid) = (0..16)
|
||||
.filter_map(|i| sf.player_pack_slot(i).filter(|g| *g != 0).map(|g| (i, g)))
|
||||
.find(|(_, g)| world.item_entries.get(g).copied() == Some(ITEM_ENTRY))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"--open-item: item {ITEM_ENTRY} isn't in the backpack — did `.additem` land? \
|
||||
(needs a GM account; try a longer --seconds)"
|
||||
)
|
||||
})?;
|
||||
let wire_slot = 23 + slot0;
|
||||
println!(
|
||||
"\nopenable item {ITEM_ENTRY}: guid {item_guid:#x} in pack slot {} → \
|
||||
CMSG_OPEN_ITEM bag 255 slot {wire_slot}",
|
||||
slot0 + 1
|
||||
);
|
||||
|
||||
// 2) The fork's own packet. A loot response on the ITEM's guid is the proof: nothing else
|
||||
// in the protocol opens a loot window over a bag position.
|
||||
session.open_item(255, wire_slot)?;
|
||||
let drain_until = Instant::now() + Duration::from_secs(5);
|
||||
let mut verdict: Option<Result<String, String>> = None;
|
||||
while Instant::now() < drain_until && verdict.is_none() {
|
||||
let Ok(msg) = session.recv() else { continue };
|
||||
for ev in decode(msg) {
|
||||
match ev {
|
||||
SessionEvent::LootResponse {
|
||||
guid,
|
||||
loot_type,
|
||||
gold,
|
||||
items,
|
||||
} if guid == item_guid => {
|
||||
verdict = Some(Ok(format!(
|
||||
"SMSG_LOOT_RESPONSE on the ITEM guid — type {loot_type}, {} copper, \
|
||||
{} row(s): {}",
|
||||
gold,
|
||||
items.len(),
|
||||
items
|
||||
.iter()
|
||||
.map(|i| format!("{}x{}", i.count, i.item_id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)));
|
||||
}
|
||||
SessionEvent::LootError { guid, error } if guid == item_guid => {
|
||||
verdict = Some(Err(format!("server refused with loot error {error}")));
|
||||
}
|
||||
SessionEvent::InventoryFailure { reason, .. } => {
|
||||
verdict = Some(Err(format!(
|
||||
"server refused with EQUIP_ERR {reason} (locked? dead? flying?)"
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let v = verdict
|
||||
.context("--open-item: no reaction to CMSG_OPEN_ITEM within 5s")?
|
||||
.map_err(|e| anyhow::anyhow!("--open-item: {e}"))?;
|
||||
println!("✅ open item: {v}");
|
||||
|
||||
// 3) Close the window again — an abandoned loot session would follow the character.
|
||||
session.loot_release(item_guid)?;
|
||||
let until = Instant::now() + Duration::from_secs(2);
|
||||
while Instant::now() < until {
|
||||
let Ok(msg) = session.recv() else { continue };
|
||||
for ev in decode(msg) {
|
||||
if let SessionEvent::LootReleaseResponse { guid } = ev {
|
||||
if guid == item_guid {
|
||||
println!("✅ release: SMSG_LOOT_RELEASE_RESPONSE on {guid:#x}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Leave the probe character as found (the clam survives an un-emptied open).
|
||||
session.send_chat(&format!(".additem {ITEM_ENTRY} -1"))?;
|
||||
println!("cleanup: .additem {ITEM_ENTRY} -1");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -191,8 +191,45 @@ impl ItemInfo {
|
||||
pub fn placeable_on_action_bar(&self) -> bool {
|
||||
self.use_spell.is_some() || self.inventory_type != 0
|
||||
}
|
||||
|
||||
/// Does a right-click **open** this concrete instance rather than use it? `instance_flags` is
|
||||
/// the item object's `ITEM_FIELD_FLAGS` (0 for a template-only view, which can never be
|
||||
/// openable — you can't right-click a hyperlink).
|
||||
///
|
||||
/// The one predicate behind both halves of the affordance: the tooltip's green
|
||||
/// `<Right Click to Open>` line and the `CMSG_OPEN_ITEM` fork. It is the reference's own
|
||||
/// tooltip condition (wow-re `tooltip-content-law.md` §1-OPENABLE, byte-read at
|
||||
/// `0x52e2f8`–`0x52e321`): the template LOOTABLE bit [`ITEM_FLAG_LOOTABLE`] behind its lock
|
||||
/// sub-gate — a [`ItemInfo::lock_id`] item only opens once the instance carries
|
||||
/// [`ITEM_DYNFLAG_UNLOCKED`] — **or** a wrapped gift ([`ITEM_FLAG_WRAPPER`] on the template
|
||||
/// plus [`ITEM_DYNFLAG_WRAPPED`] on the instance). vmangos's `HandleOpenItemOpcode` gates on
|
||||
/// exactly the same pair, so the two ends agree: what the tooltip promises is what the server
|
||||
/// will honour, and a still-locked junkbox neither advertises the line nor sends the packet.
|
||||
pub fn openable(&self, instance_flags: u32) -> bool {
|
||||
let lootable = self.flags & ITEM_FLAG_LOOTABLE != 0
|
||||
&& (self.lock_id == 0 || instance_flags & ITEM_DYNFLAG_UNLOCKED != 0);
|
||||
let gift =
|
||||
self.flags & ITEM_FLAG_WRAPPER != 0 && instance_flags & ITEM_DYNFLAG_WRAPPED != 0;
|
||||
lootable || gift
|
||||
}
|
||||
}
|
||||
|
||||
/// `ITEM_FLAG_LOOTABLE` — the **template** bit that makes an item right-clickable into a loot
|
||||
/// window (a clam, a lockbox, a Gnomish Mind Control Cap's box). vmangos
|
||||
/// `ItemPrototype.h:66`, whose own comment names the client behaviour this drives: "It or lockid
|
||||
/// set enable for client show 'Right click to open'".
|
||||
pub const ITEM_FLAG_LOOTABLE: u32 = 0x0000_0004;
|
||||
/// `ITEM_FLAG_WRAPPER` — the template bit for gift wrapping (vmangos `ItemPrototype.h:73`); paired
|
||||
/// with [`ITEM_DYNFLAG_WRAPPED`] on the instance it makes the wrapped present openable.
|
||||
pub const ITEM_FLAG_WRAPPER: u32 = 0x0000_0200;
|
||||
/// `ITEM_DYNFLAG_UNLOCKED` — the **instance** (`ITEM_FIELD_FLAGS`) bit a lockbox gains once it has
|
||||
/// been picked/keyed open; until then a `LockID` item refuses to open (vmangos
|
||||
/// `HandleOpenItemOpcode`'s `EQUIP_ERR_ITEM_LOCKED`).
|
||||
pub const ITEM_DYNFLAG_UNLOCKED: u32 = 0x0000_0004;
|
||||
/// `ITEM_DYNFLAG_WRAPPED` — the instance bit on a gift-wrapped item; opening it unwraps to the
|
||||
/// present (vmangos swaps the entry from `character_gifts` rather than sending loot).
|
||||
pub const ITEM_DYNFLAG_WRAPPED: u32 = 0x0000_0008;
|
||||
|
||||
/// One `Damage` block ([`ItemInfo::damages`] — block 0 is also mirrored into
|
||||
/// [`ItemInfo::dmg_min`]/[`ItemInfo::dmg_max`]/[`ItemInfo::dmg_type`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -508,6 +545,18 @@ pub fn use_item(bag_index: u8, slot: u8, spell_slot: u8, go_target: Option<u64>)
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_OPEN_ITEM` (VERIFIED vmangos `OpenItem::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/Spell.cpp` + `.h:36-45`; opcode 172 `Opcodes_1_12_1.h:175`): `bagIndex`, `slot`
|
||||
/// — the same two bytes and the same bag addressing as [`use_item`], and nothing else (no spell
|
||||
/// ordinal, no targets block: opening is not a cast).
|
||||
///
|
||||
/// The right-click fork for an [`ItemInfo::openable`] item. The server answers on the **item's own
|
||||
/// guid**: `SendLoot(item, LOOT_CORPSE)` for a lootable, or — for a wrapped gift — an entry swap
|
||||
/// and no window at all.
|
||||
pub fn open_item(bag_index: u8, slot: u8) -> Vec<u8> {
|
||||
vec![bag_index, slot]
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUTOEQUIP_ITEM` (VERIFIED vmangos `AutoEquipItem::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/Item.cpp:17-21` + `.h:31-39`; opcode 266 `Opcodes_1_12_1.h:269`): source
|
||||
/// `srcbag`/`srcslot` (both `uint8`), the same bag addressing as [`use_item`]. The real client
|
||||
|
||||
@@ -105,9 +105,10 @@ pub use group::{
|
||||
PartyMemberStatsInfo, RaidTargetUpdate, ReadyCheck, GROUP_MEMBER_ASSISTANT,
|
||||
};
|
||||
pub use items::{
|
||||
auto_equip_item, auto_store_bag_item, destroy_item, item_query, set_ammo, split_item,
|
||||
swap_inv_item, swap_item, use_item, ItemDamage, ItemInfo, ItemSpellEntry, ItemUseSpell,
|
||||
BAG_PLAYER_INVENTORY, SLOT_BAG_FIRST, SLOT_PACK_FIRST,
|
||||
auto_equip_item, auto_store_bag_item, destroy_item, item_query, open_item, set_ammo,
|
||||
split_item, swap_inv_item, swap_item, use_item, ItemDamage, ItemInfo, ItemSpellEntry,
|
||||
ItemUseSpell, BAG_PLAYER_INVENTORY, ITEM_DYNFLAG_UNLOCKED, ITEM_DYNFLAG_WRAPPED,
|
||||
ITEM_FLAG_LOOTABLE, ITEM_FLAG_WRAPPER, SLOT_BAG_FIRST, SLOT_PACK_FIRST,
|
||||
};
|
||||
pub use loot::{
|
||||
autostore_loot_item, loot, loot_error, loot_money, loot_release, loot_roll, loot_type,
|
||||
|
||||
@@ -218,6 +218,12 @@ pub const CMSG_GAMEOBJECT_QUERY: u16 = 0x005E; // 94
|
||||
pub const CMSG_QUEST_QUERY: u16 = 0x005C; // 92
|
||||
pub const CMSG_ITEM_QUERY_SINGLE: u16 = 0x0056; // 86
|
||||
pub const CMSG_USE_ITEM: u16 = 0x00AB; // 171
|
||||
/// VERIFIED vmangos `Opcodes_1_12_1.h:175`: 172. Body in [`super::items::open_item`] — the bag
|
||||
/// position of an *openable* item (a clam, an unlocked lockbox, a wrapped gift). The right-click
|
||||
/// fork `CMSG_USE_ITEM` never takes: the server answers `SMSG_LOOT_RESPONSE` on the **item's own
|
||||
/// guid** (`HandleOpenItemOpcode` → `SendLoot(item, LOOT_CORPSE)`), so the loot window opens over
|
||||
/// a thing in your bag rather than a corpse in the world.
|
||||
pub const CMSG_OPEN_ITEM: u16 = 0x00AC; // 172
|
||||
/// VERIFIED vmangos `Opcodes_1_12_1.h`: 177. Body in [`super::gameobj_use`] — a full guid, the
|
||||
/// GameObject to use (decision 0236). Not interchangeable with `CMSG_LOOT`: the server rejects a
|
||||
/// GameObject guid on `CMSG_LOOT`, so a chest opens its loot through this opcode.
|
||||
|
||||
@@ -360,6 +360,15 @@ impl WorldSession {
|
||||
)
|
||||
}
|
||||
|
||||
/// Open an item by bag position (`CMSG_OPEN_ITEM`) — the unsplit twin of
|
||||
/// [`WorldWriter::open_item`], for the probe's live open-item verification.
|
||||
pub fn open_item(&mut self, bag_index: u8, slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_OPEN_ITEM,
|
||||
&messages::open_item(bag_index, slot),
|
||||
)
|
||||
}
|
||||
|
||||
/// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`) — the unsplit twin of
|
||||
/// [`WorldWriter::auto_equip_item`], for the probe's live equip verification.
|
||||
pub fn auto_equip_item(&mut self, bag_index: u8, slot: u8) -> Result<()> {
|
||||
|
||||
@@ -43,6 +43,18 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Open an item by bag position (`CMSG_OPEN_ITEM`, layout in [`messages::open_item`]) — crack
|
||||
/// the clam, empty the picked lockbox, unwrap the gift. The right-click fork for an
|
||||
/// [`crate::ItemInfo::openable`] item; the server answers with `SMSG_LOOT_RESPONSE` on the
|
||||
/// item's **own** guid (so the loot window opens over a thing in the bag), or an equip error
|
||||
/// on a refusal (still locked, dead, flying).
|
||||
pub fn open_item(&mut self, bag_index: u8, slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_OPEN_ITEM,
|
||||
&messages::open_item(bag_index, slot),
|
||||
)
|
||||
}
|
||||
|
||||
/// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`, layout in [`messages::auto_equip_item`]) — the
|
||||
/// server picks the destination slot. Success arrives as inventory-slot values deltas (and the
|
||||
/// visible-item change everyone renders); refusal as `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
|
||||
@@ -42,6 +42,15 @@ fn item_query_wire() {
|
||||
// CMSG_AUTOEQUIP_ITEM (vmangos AutoEquipItem::ReadFromWorldPacket): bagIndex, slot.
|
||||
assert_eq!(messages::auto_equip_item(255, 25), hx("ff19"));
|
||||
|
||||
// CMSG_OPEN_ITEM (vmangos OpenItem::ReadFromWorldPacket, Server/Packets/Spell.cpp:19-23):
|
||||
// bagIndex, slot — and NOTHING else (no spell ordinal, no targets block; opening is not a
|
||||
// cast). The same two bytes as auto-equip under a different opcode: this is the fork a
|
||||
// clam/lockbox/gift takes instead of USE_ITEM, and the only one the server answers with
|
||||
// SendLoot on the item's own guid. Backpack slot 3 = bag 255 + player-array slot 25; an item
|
||||
// inside an equipped bag addresses that bag's own player-array slot (19..22) + inner slot.
|
||||
assert_eq!(messages::open_item(255, 25), hx("ff19"));
|
||||
assert_eq!(messages::open_item(19, 0), hx("1300"));
|
||||
|
||||
// SMSG_INVENTORY_CHANGE_FAILURE (vmangos InventoryChangeFailure::AppendBodyTo): the level
|
||||
// refusal (reason 1) carries a u32 required level before the guids; others don't.
|
||||
let level_fail = messages::parse_server(
|
||||
|
||||
Reference in New Issue
Block a user