Files
benilla-octo/crates/benilla-protocol/src/messages/client.rs
T
SamandClaude Opus 5.5 018af495bf comments: protocol, the chat, client, combat-log, death, duel, gameobject, ticket, gossip and group messages
1,079 comment lines become 390; the code is unchanged. Corrected
against the code:
- area spirit healers are modelled;
- the resurrect name length is skipped, not cross-checked;
- SMSG_CHAT_WRONG_FACTION is handled in parse.rs, not chat.rs.

AFK/DND clearing in the member snapshot matches the reference and is no
longer called a deviation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 20:09:59 +07:00

354 lines
13 KiB
Rust

//! Client packet body builders: plain body bytes, framed and encrypted elsewhere.
use super::addons::{addon_tail, SecureAddon};
use super::{CharCreateReq, MovementInfo};
/// `CMSG_AUTH_SESSION` body: build, server id, account, seed, proof, then the addon block's
/// uncompressed size and zlib stream (`0x51d910`); stock sends [`super::STOCK_SECURE_ADDONS`].
pub fn auth_session(
build: u32,
username: &str,
client_seed: u32,
client_proof: &[u8; 20],
addons: &[SecureAddon],
) -> Vec<u8> {
let tail = addon_tail(addons);
let mut body = Vec::with_capacity(34 + username.len() + tail.len());
body.extend_from_slice(&build.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes()); // server_id
body.extend_from_slice(username.as_bytes());
body.push(0);
body.extend_from_slice(&client_seed.to_le_bytes());
body.extend_from_slice(client_proof);
body.extend_from_slice(&tail);
body
}
/// `CMSG_CHAR_CREATE` body: the name cstring then nine bytes (`Packets/Character.cpp:4-19`); the
/// last, `outfit_id`, is 0 because the server ignores it (`CharacterHandler.cpp:310`).
pub fn char_create(req: &CharCreateReq) -> Vec<u8> {
let mut body = Vec::with_capacity(req.name.len() + 10);
body.extend_from_slice(req.name.as_bytes());
body.push(0);
body.extend_from_slice(&[
req.race,
req.class,
req.gender,
req.skin,
req.face,
req.hair_style,
req.hair_color,
req.facial_hair,
0,
]);
body
}
/// `CMSG_MESSAGECHAT` body: `u32` type and language, the target or channel name for whisper and
/// channel only (`None` otherwise), then the message (vmangos `Server/Packets/Chat.cpp:3-12`).
pub fn messagechat_kind(
chat_type: u32,
language: u32,
target: Option<&str>,
message: &str,
) -> Vec<u8> {
let mut body = Vec::with_capacity(message.len() + target.map_or(0, str::len) + 10);
body.extend_from_slice(&chat_type.to_le_bytes());
body.extend_from_slice(&language.to_le_bytes());
if let Some(target) = target {
body.extend_from_slice(target.as_bytes());
body.push(0);
}
body.extend_from_slice(message.as_bytes());
body.push(0);
body
}
/// `CMSG_MESSAGECHAT` body for every type without a target or channel name.
pub fn messagechat(chat_type: u32, language: u32, message: &str) -> Vec<u8> {
messagechat_kind(chat_type, language, None, message)
}
/// `CMSG_MESSAGECHAT` body for a whisper: the target's player name precedes the message.
pub fn messagechat_whisper(language: u32, target: &str, message: &str) -> Vec<u8> {
messagechat_kind(super::CHAT_TYPE_WHISPER, language, Some(target), message)
}
/// `CMSG_MESSAGECHAT` body for a channel line: the channel name precedes the message.
pub fn messagechat_channel(language: u32, channel: &str, message: &str) -> Vec<u8> {
messagechat_kind(super::CHAT_TYPE_CHANNEL, language, Some(channel), message)
}
/// Back-to-back cstrings, the shape of every `CMSG_CHANNEL_*` body (`Packets/Channel.cpp`).
fn cstrings_body(strings: &[&str]) -> Vec<u8> {
let mut body = Vec::with_capacity(strings.iter().map(|s| s.len() + 1).sum());
for s in strings {
body.extend_from_slice(s.as_bytes());
body.push(0);
}
body
}
/// `CMSG_JOIN_CHANNEL` body: channel name, then password (empty for none); 1.12 sends no channel
/// id (`Channel.cpp:3-7`).
pub fn join_channel(name: &str, password: &str) -> Vec<u8> {
cstrings_body(&[name, password])
}
/// `CMSG_LEAVE_CHANNEL` body: the channel name (`Channel.cpp:9-12`).
pub fn leave_channel(name: &str) -> Vec<u8> {
cstrings_body(&[name])
}
/// `CMSG_CHANNEL_LIST` body, the member-roster request: the channel name (`Channel.cpp:14-17`).
pub fn channel_list(name: &str) -> Vec<u8> {
cstrings_body(&[name])
}
/// `CMSG_CHANNEL_PASSWORD` body: channel name, then the new password (`Channel.cpp:19-23`).
pub fn channel_password(name: &str, password: &str) -> Vec<u8> {
cstrings_body(&[name, password])
}
/// `CMSG_CHANNEL_SET_OWNER` body: channel name, then the new owner's name (`Channel.cpp:25-29`).
pub fn channel_set_owner(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_OWNER` body: the channel name (`Channel.cpp:31-34`); the answer is a
/// `SMSG_CHANNEL_NOTIFY` owner notice.
pub fn channel_owner(name: &str) -> Vec<u8> {
cstrings_body(&[name])
}
/// `CMSG_CHANNEL_MODERATOR` body: channel name, then the player's name (`Channel.cpp:36-40`).
pub fn channel_moderator(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_UNMODERATOR` body: channel name, then the player's name (`Channel.cpp:42-46`).
pub fn channel_unmoderator(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_MUTE` body: channel name, then the player's name (`Channel.cpp:48-52`).
pub fn channel_mute(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_UNMUTE` body: channel name, then the player's name (`Channel.cpp:54-58`).
pub fn channel_unmute(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_INVITE` body: channel name, then the invitee's name (`Channel.cpp:60-64`).
pub fn channel_invite(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_KICK` body: channel name, then the player's name (`Channel.cpp:66-70`).
pub fn channel_kick(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_BAN` body: channel name, then the player's name (`Channel.cpp:72-76`).
pub fn channel_ban(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_UNBAN` body: channel name, then the player's name (`Channel.cpp:78-82`).
pub fn channel_unban(name: &str, player: &str) -> Vec<u8> {
cstrings_body(&[name, player])
}
/// `CMSG_CHANNEL_ANNOUNCEMENTS` body, the join/leave notice toggle (`Channel.cpp:84-87`).
pub fn channel_announcements(name: &str) -> Vec<u8> {
cstrings_body(&[name])
}
/// `CMSG_CHANNEL_MODERATE` body, the moderation toggle (`Channel.cpp:89-92`).
pub fn channel_moderate(name: &str) -> Vec<u8> {
cstrings_body(&[name])
}
/// `CMSG_PLAYED_TIME` body, the `/played` request: empty (`Handlers/MiscHandler.cpp:935`).
pub fn played_time() -> Vec<u8> {
Vec::new()
}
/// `CMSG_QUERY_TIME` body: empty (`Handlers/QueryHandler.cpp:107`). The answer is the server's
/// clock in unix seconds, the epoch timed-quest deadlines are stamped in.
pub fn query_time() -> Vec<u8> {
Vec::new()
}
/// `MSG_RANDOM_ROLL` request body (`Server/Packets/Group.cpp:39-43`); the server silently drops
/// one outside `minimum <= maximum <= 10000`.
pub fn random_roll(min: u32, max: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&min.to_le_bytes());
body.extend_from_slice(&max.to_le_bytes());
body
}
/// `CMSG_TEXT_EMOTE` body: `u32` EmotesText.dbc id, `u32` text variation (only relayed), `u64`
/// target or 0 (`Misc.cpp:60-65`). The server echoes `SMSG_TEXT_EMOTE` to us as well.
pub fn text_emote(text_id: u32, target: u64) -> Vec<u8> {
let mut b = Vec::with_capacity(16);
b.extend_from_slice(&text_id.to_le_bytes());
b.extend_from_slice(&0u32.to_le_bytes()); // emoteNum: text-variation 0
b.extend_from_slice(&target.to_le_bytes());
b
}
pub fn full_guid(guid: u64) -> Vec<u8> {
guid.to_le_bytes().to_vec()
}
/// `CMSG_CREATURE_QUERY` body: the template entry, then a full guid (vmangos `QueryCreature`).
pub fn creature_query(entry: u32, guid: u64) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&entry.to_le_bytes());
body.extend_from_slice(&guid.to_le_bytes());
body
}
/// `CMSG_PET_NAME_QUERY` body: the pet number, then its full guid (`Server/Packets/Pet.cpp:3-7`);
/// the server stays silent unless the number matches the live pet (`PetHandler.cpp:190-192`).
pub fn pet_name_query(pet_number: u32, guid: u64) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&pet_number.to_le_bytes());
body.extend_from_slice(&guid.to_le_bytes());
body
}
/// Body of a `MSG_MOVE_*` message: a `MovementInfo`.
pub fn movement(info: &MovementInfo) -> Vec<u8> {
let mut body = Vec::with_capacity(28);
info.write(&mut body);
body
}
/// `CMSG_MOVE_TIME_SKIPPED` body: the mover's unpacked guid, then the skipped ms
/// (`Server/Packets/Movement.cpp:10-14`); the server's relayed `MSG_MOVE_TIME_SKIPPED` packs it.
pub fn move_time_skipped(guid: u64, lag_ms: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&guid.to_le_bytes());
body.extend_from_slice(&lag_ms.to_le_bytes());
body
}
/// `CMSG_MOVE_SPLINE_DONE` body: the `MovementInfo` at the ride's end, the acked spline id, and a
/// float the server skips but requires. The 1.12 client writes its completion fraction there
/// (`0x600b10`); we send only at completion, so it is always 1.0.
pub fn move_spline_done(info: &MovementInfo, spline_id: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(36);
info.write(&mut body);
body.extend_from_slice(&spline_id.to_le_bytes());
body.extend_from_slice(&1.0f32.to_le_bytes()); // completion fraction (server-skipped)
body
}
/// `MSG_MOVE_TELEPORT_ACK` body: an unpacked guid (vmangos requires it), counter, client time.
pub fn teleport_ack(guid: u64, counter: u32, time_ms: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(16);
body.extend_from_slice(&guid.to_le_bytes());
body.extend_from_slice(&counter.to_le_bytes());
body.extend_from_slice(&time_ms.to_le_bytes());
body
}
/// Body of all six `CMSG_FORCE_*_SPEED_CHANGE_ACK`s: the mover's unpacked guid, the echoed
/// counter, our live `MovementInfo`, the echoed speed. The server needs an exact counter and a
/// speed within 0.01 (`Unit::FindPendingMovementSpeedChange`), then moves us to that pose.
pub fn force_speed_ack(guid: u64, counter: u32, info: &MovementInfo, speed: f32) -> Vec<u8> {
let mut body = Vec::with_capacity(44);
body.extend_from_slice(&guid.to_le_bytes());
body.extend_from_slice(&counter.to_le_bytes());
info.write(&mut body);
body.extend_from_slice(&speed.to_le_bytes());
body
}
/// `CMSG_PING` body, the ~30 s keepalive (`0x537e10`): the sequence, then the last round trip in
/// ms, which the server stores as our latency; it echoes the sequence in `SMSG_PONG`.
pub fn ping(sequence: u32, last_rtt_ms: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&sequence.to_le_bytes());
body.extend_from_slice(&last_rtt_ms.to_le_bytes());
body
}
/// Body of a movement-mode ack: unpacked guid, the echoed counter, our `MovementInfo`, then
/// `u32 apply` for every mode but root (`Server/Packets/Movement.cpp:38-59`). `info.flags` must
/// carry the applied mode bit: vmangos kicks a root ack without it (`MovementHandler.cpp:722-729`)
/// and takes the flags as the mover's new ones for the other modes.
pub fn move_flag_ack(guid: u64, counter: u32, info: &MovementInfo, apply: Option<bool>) -> Vec<u8> {
let mut body = Vec::with_capacity(48);
body.extend_from_slice(&guid.to_le_bytes());
body.extend_from_slice(&counter.to_le_bytes());
info.write(&mut body);
if let Some(apply) = apply {
body.extend_from_slice(&u32::from(apply).to_le_bytes());
}
body
}
/// `CMSG_MOVE_KNOCK_BACK_ACK` body, a movement-mode ack without `apply` (`Movement.cpp:38-68`).
/// `info` must carry `MOVEFLAG_JUMPING` and the launch quad: the server matches the counter and
/// all four floats within 0.01 (`Unit::FindPendingMovementKnockbackChange`), builds observers'
/// `MSG_MOVE_KNOCK_BACK` from this info, and never re-sends a knockback (`Unit.cpp:6887`).
pub fn knock_back_ack(guid: u64, counter: u32, info: &MovementInfo) -> Vec<u8> {
move_flag_ack(guid, counter, info, None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::Vector3d;
/// vmangos `MoveSplineDone::ReadFromWorldPacket` reads info, spline id, then a skipped float.
#[test]
fn move_spline_done_body_golden() {
let info = MovementInfo {
flags: 0, // no SWIMMING / JUMPING ⇒ no conditional MovementInfo tails
timestamp: 0x1122_3344,
position: Vector3d {
x: 1.0,
y: 2.0,
z: 3.0,
},
orientation: 0.5,
transport: None,
pitch: 0.0,
fall_time: 0x5566_7788,
jump: None,
};
let body = move_spline_done(&info, 0xAABB_CCDD);
#[rustfmt::skip]
let want: &[u8] = &[
0x00, 0x00, 0x00, 0x00, // flags
0x44, 0x33, 0x22, 0x11, // timestamp
0x00, 0x00, 0x80, 0x3F, // pos.x = 1.0
0x00, 0x00, 0x00, 0x40, // pos.y = 2.0
0x00, 0x00, 0x40, 0x40, // pos.z = 3.0
0x00, 0x00, 0x00, 0x3F, // orientation = 0.5
0x88, 0x77, 0x66, 0x55, // fall_time
0xDD, 0xCC, 0xBB, 0xAA, // splineId
0x00, 0x00, 0x80, 0x3F, // completion fraction 1.0 (server-skipped)
];
assert_eq!(body, want, "CMSG_MOVE_SPLINE_DONE body");
let mi = movement(&info);
assert_eq!(
&body[..mi.len()],
&mi[..],
"MovementInfo comes first, verbatim"
);
assert_eq!(
body.len(),
mi.len() + 8,
"…then splineId + the skipped float"
);
}
}