/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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user