Files
benilla-octo/crates/benilla-protocol/tests/client.rs
T
SamandClaude Opus 5 959fb8ba08 duels, inspecting other players, and water that ends at the bank
- Duels. Challenge another player from their right-click menu, or with `/duel`, and
  `/forfeit` to give it up. Both popups are there — the challenge you accept or decline, and
  the warning when you leave the field — with the flag countdown between them, and the outcome
  printed in chat when someone is knocked out or flees. Read off the reference client's own
  duel packets and the three globals it keeps client-side, which is why the challenger reads
  "You have requested a duel." even though the server never echoes that back.
- Inspect. Right-click a player and open their paper doll: their nineteen publicly visible
  equipment slots with icon, name, quality and a linkable item, and their body standing in
  the pane, which follows along if you retarget while the window is open. No stack counts and
  no durability — those are not fields another player publishes, which is exactly why the
  reference client's inspect doll has never shown them either.
- Water knows where it stops. Swimming used to trigger in the dry tunnel thirty yards under
  Stormwind's canal, and in mid-air over Blackrock's lava. A liquid is now the grid the map
  file actually stores — one wet flag per cell, and the surface height a bilinear over the
  four corners of the cell you are standing in, the same sample the reference client takes —
  so the water starts at the bank and its surface sits where your eye puts it. Lava is a
  liquid you swim in, an indoor liquid asks the building you are inside, and `/liquid` prints
  every surface claiming your feet and where each one came from.
- Creature parts authored to appear only on death — the voidwalker, the banshee, the ooze,
  the stone elemental, the infernal — were drawing the whole time: material alpha now bakes
  per animation sequence and the unit lane actually evaluates it. The character and inspect
  model panes are lit by the reference widget's own single light instead of blowing out white,
  the rotate arrows spin while you hold them, and the transparent sort ladder had its sign
  backwards, so overhead names draw over water again instead of being painted out by a canal
  behind them. Underneath, the protocol writer's 95-verb drawer and its thousand-line spell
  module split into per-family files — no behaviour change, just a tree you can find things in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 18:22:05 +07:00

593 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Client-body + query wire tests (mirrors `src/messages/client.rs`): the golden client-encoded
//! bodies (auth/char-create/chat/emote/stand-state/movement/teleport-ack) and the creature/name
//! query request+response roundtrip. Split out of the former `tests/messages.rs` (decision-adjacent
//! mechanical split — see `tests/common` for the shared fixtures and methodology note).
mod common;
use benilla_protocol::events::{decode, SessionEvent};
use benilla_protocol::messages::{self, MovementInfo};
use benilla_protocol::wire::Vector3d;
use benilla_protocol::ServerPacket;
use common::hx;
/// The faction tongue every chat send must speak ([`messages::faction_language`]): race → tongue
/// VERIFIED against the live world DB (`playercreateinfo_spell` — Alliance races 1/3/4/7 learn
/// spell 668 Language Common, Horde races 2/5/6/8 learn 669 Language Orcish); wire ids VERIFIED
/// vmangos `SharedDefines.h:256-261` (`LANG_ORCISH = 1`, `LANG_COMMON = 7`).
#[test]
fn faction_language_per_race() {
for race in [1u8, 3, 4, 7] {
assert_eq!(
messages::faction_language(race),
messages::LANGUAGE_COMMON,
"alliance race {race}"
);
}
for race in [2u8, 5, 6, 8] {
assert_eq!(
messages::faction_language(race),
messages::LANGUAGE_ORCISH,
"horde race {race}"
);
}
}
#[test]
fn client_bodies_golden() {
let proof: [u8; 20] = std::array::from_fn(|i| (i as u8).wrapping_mul(3).wrapping_add(7));
assert_eq!(
messages::auth_session(5875, "TESTUSER", 0x1122_3344, &proof),
hx("f31600000000000054455354555345520044332211070a0d101316191c1f2225282b2e3134373a3d4000000000789c030000000001"),
"CMSG_AUTH_SESSION body"
);
// Zero-appearance body (the create-if-empty starter): name + [race,class,gender] + 5 zeros + 0.
assert_eq!(
messages::char_create(&messages::CharCreateReq {
name: "Benilla".into(),
race: 1,
class: 1,
gender: 0,
skin: 0,
face: 0,
hair_style: 0,
hair_color: 0,
facial_hair: 0,
}),
hx("42656e696c6c6100010100000000000000"),
"CMSG_CHAR_CREATE body (zero appearance)"
);
// Distinct appearance dials (3/4/5/6/7) — a misplaced byte can't pass. Body is name + nul +
// race(01) class(01) gender(00) skin(03) face(04) hairStyle(05) hairColor(06) facialHair(07)
// outfit(00), matching vmangos's read order (`Packets/Character.cpp:4-19`).
assert_eq!(
messages::char_create(&messages::CharCreateReq {
name: "Benilla".into(),
race: 1,
class: 1,
gender: 0,
skin: 3,
face: 4,
hair_style: 5,
hair_color: 6,
facial_hair: 7,
}),
hx("42656e696c6c6100010100030405060700"),
"CMSG_CHAR_CREATE body (distinct appearance)"
);
assert_eq!(
messages::messagechat(0, 7, ".tele Westfall"),
hx("00000000070000002e74656c65205765737466616c6c00"),
"CMSG_MESSAGECHAT body"
);
// The three-field vmangos shape (`Misc.cpp:60-65`): textEmote, emoteNum(0), full target
// guid. The original two-field body was silently discarded server-side (the guid read ran
// off the packet) — /wave did nothing on any client until the director caught it on the ref.
assert_eq!(
messages::text_emote(101, 0x2A),
hx("65000000000000002a00000000000000"),
"CMSG_TEXT_EMOTE body"
);
assert_eq!(
messages::stand_state_change(1),
hx("01000000"),
"CMSG_STANDSTATECHANGE body: one u32 animState (Misc.cpp:35-38)"
);
// The chat-type consts feeding CMSG_MESSAGECHAT's type field (VERIFIED vmangos
// `SharedDefines.h:1194..1202`, the 5875 band).
assert_eq!(messages::CHAT_TYPE_SAY, 0x0, "ChatMsg::CHAT_MSG_SAY");
assert_eq!(messages::CHAT_TYPE_YELL, 0x5, "ChatMsg::CHAT_MSG_YELL");
assert_eq!(
messages::CHAT_TYPE_WHISPER,
0x6,
"ChatMsg::CHAT_MSG_WHISPER"
);
assert_eq!(messages::CHAT_TYPE_EMOTE, 0x8, "ChatMsg::CHAT_MSG_EMOTE");
// /yell and /emote share `messagechat`'s body shape, only the type field differs.
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_YELL, 7, "for the horde"),
hx("0500000007000000666f722074686520686f72646500"),
"CMSG_MESSAGECHAT (yell) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_EMOTE, 7, "dances"),
hx("080000000700000064616e63657300"),
"CMSG_MESSAGECHAT (emote) body"
);
// A Horde say speaks Orcish (0x1) in the language field — hardcoded Common here once made
// vmangos drop every Horde character's sends at the `KnowsLanguage` gate, dot-commands
// included (only an SMSG_NOTIFICATION came back).
assert_eq!(
messages::messagechat(
messages::CHAT_TYPE_SAY,
messages::LANGUAGE_ORCISH,
"for the horde"
),
hx("0000000001000000666f722074686520686f72646500"),
"CMSG_MESSAGECHAT (say, Orcish) body"
);
// CMSG_MESSAGECHAT (whisper): type + language, then the target name, then the message — both
// NUL-terminated C-strings, target *before* message (VERIFIED vmangos
// `Server/Packets/Chat.cpp:3-12`, `ChatMessage::ReadFromWorldPacket`).
assert_eq!(
messages::messagechat_whisper(7, "Bob", "hi there"),
hx("0600000007000000426f6200686920746865726500"),
"CMSG_MESSAGECHAT (whisper) body"
);
// The full 8-byte LE guid body, shared by CMSG_PLAYER_LOGIN / CMSG_SET_ACTIVE_MOVER /
// CMSG_SET_SELECTION (all read `recv_data >> guid` server-side — a raw uint64, not a packed guid).
assert_eq!(
messages::full_guid(0x1234_5678_9abc_def0),
hx("f0debc9a78563412"),
"full guid body"
);
// CMSG_SET_SELECTION opcode value (317 / 0x013D) — verified vmangos `Opcodes_1_12_1.h`.
assert_eq!(
messages::opcode::CMSG_SET_SELECTION,
0x013D,
"CMSG_SET_SELECTION opcode"
);
// CMSG_INSPECT (276 / 0x0114) — verified vmangos `Opcodes_1_12_1.h`; body is the same raw
// 8-byte guid (`WorldPackets::Misc::Inspect`), NOT a packed guid. Decision 0631.
assert_eq!(
messages::opcode::CMSG_INSPECT,
0x0114,
"CMSG_INSPECT opcode"
);
assert_eq!(
messages::full_guid(0x1234_5678_9abc_def0),
hx("f0debc9a78563412"),
"CMSG_INSPECT body (a raw guid, same shape as full_guid)"
);
let mi = MovementInfo {
flags: 0x1,
timestamp: 0x0102_0304,
position: Vector3d {
x: -8949.95,
y: -132.493,
z: 83.5312,
},
orientation: 1.25,
transport: None,
pitch: 0.0,
fall_time: 0,
jump: None,
};
assert_eq!(
messages::movement(&mi),
hx("0100000004030201cdd70bc6357e04c3f90fa7420000a03f00000000"),
"MSG_MOVE_* body"
);
assert_eq!(
messages::teleport_ack(0x1234_5678_9abc_def0, 7, 0x0102_0304),
hx("f0debc9a785634120700000004030201"),
"MSG_MOVE_TELEPORT_ACK body"
);
}
/// The full sendable `CMSG_MESSAGECHAT` type set beyond the original four (decision 0288 phase 1) —
/// every one VERIFIED against vmangos `Handlers/ChatHandler.cpp`'s `HandleChatMessageOpcode` switch
/// (253-655): PARTY/RAID/GUILD/OFFICER/RAID_LEADER/RAID_WARNING/BATTLEGROUND(+LEADER)/AFK/DND all
/// share [`messages::messagechat`]'s plain shape (no target field); CHANNEL carries the channel name
/// the same way WHISPER carries its target (`messages::messagechat_channel`). Bytes hand-computed
/// from the vmangos layout (`Server/Packets/Chat.cpp:3-12`), independent of the Rust builder.
#[test]
fn messagechat_sendable_types_golden() {
assert_eq!(messages::CHAT_TYPE_PARTY, 0x1, "ChatMsg::CHAT_MSG_PARTY");
assert_eq!(messages::CHAT_TYPE_RAID, 0x2, "ChatMsg::CHAT_MSG_RAID");
assert_eq!(messages::CHAT_TYPE_GUILD, 0x3, "ChatMsg::CHAT_MSG_GUILD");
assert_eq!(
messages::CHAT_TYPE_OFFICER,
0x4,
"ChatMsg::CHAT_MSG_OFFICER"
);
assert_eq!(
messages::CHAT_TYPE_CHANNEL,
0xE,
"ChatMsg::CHAT_MSG_CHANNEL"
);
assert_eq!(messages::CHAT_TYPE_AFK, 0x14, "ChatMsg::CHAT_MSG_AFK");
assert_eq!(messages::CHAT_TYPE_DND, 0x15, "ChatMsg::CHAT_MSG_DND");
assert_eq!(
messages::CHAT_TYPE_RAID_LEADER,
0x57,
"ChatMsg::CHAT_MSG_RAID_LEADER"
);
assert_eq!(
messages::CHAT_TYPE_RAID_WARNING,
0x58,
"ChatMsg::CHAT_MSG_RAID_WARNING"
);
assert_eq!(
messages::CHAT_TYPE_BATTLEGROUND,
0x5C,
"ChatMsg::CHAT_MSG_BATTLEGROUND"
);
assert_eq!(
messages::CHAT_TYPE_BATTLEGROUND_LEADER,
0x5D,
"ChatMsg::CHAT_MSG_BATTLEGROUND_LEADER"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_PARTY, 7, "MT is icon 8"),
hx("01000000070000004d542069732069636f6e203800"),
"CMSG_MESSAGECHAT (party) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_RAID, 7, "form up"),
hx("0200000007000000666f726d20757000"),
"CMSG_MESSAGECHAT (raid) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_GUILD, 7, "hi guild"),
hx("03000000070000006869206775696c6400"),
"CMSG_MESSAGECHAT (guild) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_OFFICER, 7, "officers only"),
hx("04000000070000006f66666963657273206f6e6c7900"),
"CMSG_MESSAGECHAT (officer) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_RAID_LEADER, 7, "pull in 5"),
hx("570000000700000070756c6c20696e203500"),
"CMSG_MESSAGECHAT (raid leader) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_RAID_WARNING, 7, "BLOODLUST NOW"),
hx("5800000007000000424c4f4f444c555354204e4f5700"),
"CMSG_MESSAGECHAT (raid warning) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_BATTLEGROUND, 7, "defend flag"),
hx("5c00000007000000646566656e6420666c616700"),
"CMSG_MESSAGECHAT (battleground) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_BATTLEGROUND_LEADER, 7, "push mid"),
hx("5d0000000700000070757368206d696400"),
"CMSG_MESSAGECHAT (battleground leader) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_AFK, 7, "be back soon"),
hx("14000000070000006265206261636b20736f6f6e00"),
"CMSG_MESSAGECHAT (afk) body"
);
// A bare `/afk` (no message) still needs the NUL-terminated empty string, not a truncated body.
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_AFK, 7, ""),
hx("140000000700000000"),
"CMSG_MESSAGECHAT (afk, empty message) body"
);
assert_eq!(
messages::messagechat(messages::CHAT_TYPE_DND, 7, "do not disturb"),
hx("1500000007000000646f206e6f74206469737475726200"),
"CMSG_MESSAGECHAT (dnd) body"
);
// CHANNEL: type + language, then the channel name, then the message — the same
// target-before-message shape as whisper (vmangos's `whisperTargetOrChannel` union).
assert_eq!(
messages::messagechat_channel(0, "General", "wtb boar livers"),
hx("0e0000000000000047656e6572616c0077746220626f6172206c697665727300"),
"CMSG_MESSAGECHAT (channel) body"
);
// The generic builder both named wrappers are thin shells over: `target: None` matches
// `messagechat`, `target: Some(_)` matches `messagechat_whisper`/`messagechat_channel`.
assert_eq!(
messages::messagechat_kind(messages::CHAT_TYPE_SAY, 7, None, "hi"),
messages::messagechat(messages::CHAT_TYPE_SAY, 7, "hi"),
"messagechat_kind(None) == messagechat"
);
assert_eq!(
messages::messagechat_kind(messages::CHAT_TYPE_WHISPER, 7, Some("Bob"), "hi"),
messages::messagechat_whisper(7, "Bob", "hi"),
"messagechat_kind(Some) == messagechat_whisper"
);
}
/// The channel wire family's CMSG bodies (decision 0288 phase 1) — join/leave/list + the full
/// moderation set, ALL verified vmangos `Server/Packets/Channel.cpp` (every `ReadFromWorldPacket`
/// there is a channel-name cstring, optionally followed by a second cstring: a password or a target
/// player name). Bytes hand-computed from that layout, independent of the Rust builder.
#[test]
fn channel_client_bodies_golden() {
assert_eq!(
messages::join_channel("General", ""),
hx("47656e6572616c0000"),
"CMSG_JOIN_CHANNEL body (no password)"
);
assert_eq!(
messages::join_channel("Secret", "hunter2"),
hx("5365637265740068756e7465723200"),
"CMSG_JOIN_CHANNEL body (with password)"
);
assert_eq!(
messages::leave_channel("General"),
hx("47656e6572616c00"),
"CMSG_LEAVE_CHANNEL body"
);
assert_eq!(
messages::channel_list("Trade"),
hx("547261646500"),
"CMSG_CHANNEL_LIST body"
);
assert_eq!(
messages::channel_password("General", "hunter2"),
hx("47656e6572616c0068756e7465723200"),
"CMSG_CHANNEL_PASSWORD body"
);
assert_eq!(
messages::channel_set_owner("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_SET_OWNER body"
);
assert_eq!(
messages::channel_owner("General"),
hx("47656e6572616c00"),
"CMSG_CHANNEL_OWNER body"
);
assert_eq!(
messages::channel_moderator("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_MODERATOR body"
);
assert_eq!(
messages::channel_unmoderator("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_UNMODERATOR body"
);
assert_eq!(
messages::channel_mute("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_MUTE body"
);
assert_eq!(
messages::channel_unmute("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_UNMUTE body"
);
assert_eq!(
messages::channel_invite("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_INVITE body"
);
assert_eq!(
messages::channel_kick("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_KICK body"
);
assert_eq!(
messages::channel_ban("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_BAN body"
);
assert_eq!(
messages::channel_unban("General", "Bob"),
hx("47656e6572616c00426f6200"),
"CMSG_CHANNEL_UNBAN body"
);
assert_eq!(
messages::channel_announcements("General"),
hx("47656e6572616c00"),
"CMSG_CHANNEL_ANNOUNCEMENTS body"
);
assert_eq!(
messages::channel_moderate("General"),
hx("47656e6572616c00"),
"CMSG_CHANNEL_MODERATE body"
);
// Every opcode in the family (VERIFIED vmangos `Server/Protocol/Opcodes_1_12_1.h:154-171`,
// decimal 151-168).
assert_eq!(messages::opcode::CMSG_JOIN_CHANNEL, 0x0097);
assert_eq!(messages::opcode::CMSG_LEAVE_CHANNEL, 0x0098);
assert_eq!(messages::opcode::SMSG_CHANNEL_NOTIFY, 0x0099);
assert_eq!(messages::opcode::CMSG_CHANNEL_LIST, 0x009A);
assert_eq!(messages::opcode::SMSG_CHANNEL_LIST, 0x009B);
assert_eq!(messages::opcode::CMSG_CHANNEL_PASSWORD, 0x009C);
assert_eq!(messages::opcode::CMSG_CHANNEL_SET_OWNER, 0x009D);
assert_eq!(messages::opcode::CMSG_CHANNEL_OWNER, 0x009E);
assert_eq!(messages::opcode::CMSG_CHANNEL_MODERATOR, 0x009F);
assert_eq!(messages::opcode::CMSG_CHANNEL_UNMODERATOR, 0x00A0);
assert_eq!(messages::opcode::CMSG_CHANNEL_MUTE, 0x00A1);
assert_eq!(messages::opcode::CMSG_CHANNEL_UNMUTE, 0x00A2);
assert_eq!(messages::opcode::CMSG_CHANNEL_INVITE, 0x00A3);
assert_eq!(messages::opcode::CMSG_CHANNEL_KICK, 0x00A4);
assert_eq!(messages::opcode::CMSG_CHANNEL_BAN, 0x00A5);
assert_eq!(messages::opcode::CMSG_CHANNEL_UNBAN, 0x00A6);
assert_eq!(messages::opcode::CMSG_CHANNEL_ANNOUNCEMENTS, 0x00A7);
assert_eq!(messages::opcode::CMSG_CHANNEL_MODERATE, 0x00A8);
}
/// The remaining decision-0288 phase-1 small bodies: `CMSG_CHAT_IGNORED` (reuses
/// [`messages::full_guid`] — VERIFIED vmangos `WorldPackets::Misc::ChatIgnored::
/// ReadFromWorldPacket`, `Server/Packets/Misc.cpp:127-130`, a raw un-packed guid), `CMSG_PLAYED_TIME`
/// (empty), and `MSG_RANDOM_ROLL`'s client→server request shape (VERIFIED vmangos
/// `WorldPackets::Group::RandomRoll::ReadFromWorldPacket`, `Server/Packets/Group.cpp:39-43`).
#[test]
fn chat_ignored_played_time_random_roll_golden() {
assert_eq!(
messages::full_guid(0x1234_5678_9abc_def0),
hx("f0debc9a78563412"),
"CMSG_CHAT_IGNORED body (a raw guid, same shape as full_guid)"
);
assert_eq!(messages::opcode::CMSG_CHAT_IGNORED, 0x0225);
assert_eq!(
messages::played_time(),
Vec::<u8>::new(),
"CMSG_PLAYED_TIME body: empty"
);
assert_eq!(messages::opcode::CMSG_PLAYED_TIME, 0x01CC);
assert_eq!(messages::opcode::SMSG_PLAYED_TIME, 0x01CD);
assert_eq!(
messages::random_roll(1, 100),
hx("0100000064000000"),
"MSG_RANDOM_ROLL (request) body"
);
assert_eq!(messages::opcode::MSG_RANDOM_ROLL, 0x01FB);
}
#[test]
fn name_and_creature_query_roundtrip() {
// CMSG_CREATURE_QUERY body: entry u32 + full 8-byte guid (vmangos QueryCreature::ReadFromWorldPacket).
assert_eq!(
messages::creature_query(69, 0x1234_5678_9abc_def0),
hx("45000000f0debc9a78563412"),
"CMSG_CREATURE_QUERY body"
);
// SMSG_NAME_QUERY_RESPONSE: guid, name, realm (empty), race/gender/class u32s (vmangos
// NameQueryResponse::AppendBodyTo, 1.12.1 includes the realm string).
let body = hx("070000000000000042656e696c6c610000010000000000000001000000");
match messages::parse_server(messages::opcode::SMSG_NAME_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::NameQueryResponse {
guid,
name,
race,
gender,
class,
} => {
assert_eq!(guid, 7);
assert_eq!(name, "Benilla");
assert_eq!((race, gender, class), (1, 0, 1));
}
_ => panic!("name query response"),
}
// SMSG_CREATURE_QUERY_RESPONSE hit: entry, name, 3 empty names, subname, the 7-u32 tail
// (type_flags, TYPE — kept: the TAB critter filter's input — family, rank, unk,
// pet_spell_id, display_id), 2 u8 tail.
let body = hx(concat!(
"45000000",
"596f756e6720576f6c6600", // "Young Wolf"
"000000", // name2..4 empty
"5465737400", // subname "Test"
"10000000", // type_flags = 0x10 (hide-faction-tooltip)
"01000000", // type = 1 (Beast)
"0000000000000000000000000000000000000000", // family..display_id (5 × u32)
"0101" // civilian, racial_leader
));
match messages::parse_server(messages::opcode::SMSG_CREATURE_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::CreatureQueryResponse { entry, info } => {
assert_eq!(entry, 69);
assert_eq!(
info,
Some(benilla_protocol::messages::CreatureQueryInfo {
name: "Young Wolf".into(),
subname: "Test".into(),
creature_type: 1,
rank: 0,
type_flags: 0x10,
civilian: true,
racial_leader: true,
})
);
}
_ => panic!("creature query response"),
}
// Miss: the lone entry echoed with the top bit set (vmangos "NO CREATURE INFO" branch).
let body = hx("d2040080");
match messages::parse_server(messages::opcode::SMSG_CREATURE_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::CreatureQueryResponse { entry, info } => {
assert_eq!(entry, 1234);
assert_eq!(info, None);
}
_ => panic!("creature query miss"),
}
}
/// The event layer's subname law: an EMPTY wire subname is NO subname (vmangos sends "" for
/// creatures without one; the real client renders no line for it — the nameplate builder's
/// verified shape, `0x608f50`). `Some("")` reaching a consumer painted an empty tooltip line
/// whose zero-extent chain slot spilled every later line below the plate.
#[test]
fn creature_query_empty_subname_decodes_to_none() {
let body = |subname_hex: &str| {
hx(&format!(
"45000000{}000000{}{}",
"4465707574792057696c6c656d00", // "Deputy Willem" (+3 empty names after)
subname_hex,
// type_flags, type=7 Humanoid, family..display_id (5 × u32), civilian, racial_leader
"000000000700000000000000000000000000000000000000000000000000"
))
};
let parse = |b: &[u8]| {
messages::parse_server(messages::opcode::SMSG_CREATURE_QUERY_RESPONSE, b).unwrap()
};
match decode(parse(&body("00"))).as_slice() {
[SessionEvent::CreatureName {
entry,
name,
subname,
..
}] => {
assert_eq!(*entry, 69);
assert_eq!(name.as_deref(), Some("Deputy Willem"));
assert_eq!(*subname, None, "an empty wire subname is NO subname");
}
other => panic!("expected one CreatureName event, got {other:?}"),
}
// Control: a real subname survives verbatim.
match decode(parse(&body("5465737400"))).as_slice() {
[SessionEvent::CreatureName { subname, .. }] => {
assert_eq!(subname.as_deref(), Some("Test"));
}
other => panic!("expected one CreatureName event, got {other:?}"),
}
}
/// The keepalive pair, byte-exact both ways: the `CMSG_PING` body is `{u32 sequence, u32 lastRtt}`
/// LE (VERIFIED wow-re net W1 `SendPing 0x537e10` + vmangos `_HandlePing`'s read order), and the
/// `SMSG_PONG` echo (`{u32 sequence}`) parses and decodes to the event the io layer times against
/// its ping clock. A new wire body never lands without a golden (method).
#[test]
fn ping_body_golden_and_pong_roundtrip() {
assert_eq!(
messages::ping(1, 0),
hx("0100000000000000"),
"first ping of a connection: seq 1, no RTT yet"
);
assert_eq!(
messages::ping(0x0A0B_0C0D, 23),
hx("0d0c0b0a17000000"),
"CMSG_PING body: sequence then lastRtt, both u32 LE"
);
let packet = messages::parse_server(messages::opcode::SMSG_PONG, &hx("0d0c0b0a")).unwrap();
assert!(matches!(
packet,
ServerPacket::Pong {
sequence: 0x0A0B_0C0D
}
));
match decode(packet).as_slice() {
[SessionEvent::Pong {
sequence: 0x0A0B_0C0D,
}] => {}
other => panic!("expected one Pong event, got {other:?}"),
}
}