Files
benilla-octo/crates/benilla-protocol/src/messages/binder.rs
T
SamandClaude Opus 5 9a184d3476 the guild window opens: roster, ranks, notes and the message of the day
- the social window's third tab is live — the roster in both its views, the member card with its promote/demote/remove verbs, the rank editor's thirteen permissions, the guild information board, and the invite, leave, kick and note dialogs behind them
- setting your hearthstone at an innkeeper now asks before it takes and says so afterwards, and the innkeeper's gossip lines get their own icons back
- unavailable talents grey out, and every greyed icon in the client now goes through the same luminance fold the original used
- the buff tooltip's remaining-time line reads gold with the client's own hour/minute ladder, the macro editor takes a shift-click to paste a spell, trainer rows repaint after a purchase, and a long book page stops flickering its ellipsis

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 17:38:54 +07:00

97 lines
4.2 KiB
Rust

//! The innkeeper bind family — the confirm question, its answer, and the "you are bound" echo
//! (opcodes 747, 437, 344; decision 1331).
//!
//! Three packets and one law worth stating plainly, because getting it wrong is silent: **selecting
//! the innkeeper's gossip line does not bind anything.** vmangos's `GOSSIP_OPTION_INNKEEPER` arm
//! (`Player.cpp:12341`) closes the gossip menu and calls `Player::SetBindPoint`, which sends
//! `SMSG_BINDER_CONFIRM` and stops. The bind happens only when the client answers
//! `CMSG_BINDER_ACTIVATE` (`HandleBinderActivateOpcode`, `NPCHandler.cpp:479`), at which point the
//! innkeeper casts spell 3286 "Bind" on the player and `SPELL_EFFECT_BIND`
//! (`SpellEffects.cpp:5837`) writes the homebind and sends `SMSG_BINDPOINTUPDATE` +
//! `SMSG_PLAYERBOUND`. A client that shows the gossip line and never sends the answer therefore
//! looks exactly like a server that ignored the click — which is how B249 read.
//!
//! | opcode | direction | body |
//! |---|---|---|
//! | `SMSG_BINDER_CONFIRM` 0x2eb | in | `u64` binder guid |
//! | `CMSG_BINDER_ACTIVATE` 0x1b5 | out | `u64` binder guid |
//! | `SMSG_PLAYERBOUND` 0x158 | in | `u64` binder guid + `u32` area id |
//!
//! The server bodies are VERIFIED against vmangos `Server/Packets/Npc.{h,cpp}` and
//! `Server/Packets/Misc.{h,cpp}` (`BinderConfirm`, `BinderActivate`, `PlayerBound`). What the
//! *client* puts in `CMSG_BINDER_ACTIVATE` is the guid it stored from the confirm: vmangos reads
//! that guid and uses it (`GetNPCIfCanInteractWith(packet.npcGuid, UNIT_NPC_FLAG_INNKEEPER)`), so
//! unlike the duel echo this one is load-bearing — a zero here is an ignored bind.
use std::io;
use crate::wire::{read_u32_le, read_u64_le};
/// `SMSG_PLAYERBOUND` — the server's echo once the bind has actually taken.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlayerBound {
/// The NPC that bound us (vmangos sends the *caster* of spell 3286, i.e. the innkeeper).
pub binder: u64,
/// The `AreaTable` id the player is now bound in — the same id `SMSG_BINDPOINTUPDATE` carries
/// in the packet beside it, so the two can never disagree.
pub area: u32,
}
/// Body of `CMSG_BINDER_ACTIVATE`: the binder's full 8-byte guid — the one the client stored from
/// [`read_binder_confirm`]. **Not decorative:** `HandleBinderActivateOpcode` resolves this guid to
/// a live `UNIT_NPC_FLAG_INNKEEPER` creature in interact range and drops the request if it can't,
/// so an empty or stale guid binds nothing and reports nothing.
pub fn binder_activate(binder_guid: u64) -> Vec<u8> {
binder_guid.to_le_bytes().to_vec()
}
/// Read `SMSG_BINDER_CONFIRM` (vmangos `Npc.cpp`, `buffer << binderGuid`): one full 8-byte guid.
///
/// This is the *question*, and it arrives with `SMSG_GOSSIP_COMPLETE` right before it — the gossip
/// window is already closing when the dialog goes up, which is why the confirm is not part of the
/// gossip session's state.
pub(super) fn read_binder_confirm(r: &mut &[u8]) -> io::Result<u64> {
read_u64_le(r)
}
/// Read `SMSG_PLAYERBOUND` (vmangos `Misc.cpp`, `buffer << binderGuid << areaId`).
pub(super) fn read_player_bound(r: &mut &[u8]) -> io::Result<PlayerBound> {
Ok(PlayerBound {
binder: read_u64_le(r)?,
area: read_u32_le(r)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
/// The activate body is the bare guid, little-endian — the shape vmangos's `BinderActivate`
/// reads back.
#[test]
fn the_activate_body_is_one_little_endian_guid() {
assert_eq!(
binder_activate(0x0000_0000_0000_2a01),
vec![0x01, 0x2a, 0, 0, 0, 0, 0, 0]
);
}
/// Confirm in, bound out: both read the guid first, and `SMSG_PLAYERBOUND` carries the area
/// id after it.
#[test]
fn the_two_inbound_bodies_round_trip() {
let guid: u64 = 0xF130_0000_0001_2345;
let mut bytes = guid.to_le_bytes().to_vec();
assert_eq!(read_binder_confirm(&mut &bytes[..]).unwrap(), guid);
bytes.extend_from_slice(&141u32.to_le_bytes());
assert_eq!(
read_player_bound(&mut &bytes[..]).unwrap(),
PlayerBound {
binder: guid,
area: 141
}
);
}
}