Files
benilla-octo/crates/benilla-protocol/tests/client.rs
T
SamandClaude Fable 5.1 e72016630c NTFS load order, a title-sorted AddOns list, and tooltips at the cursor
- addons load in the order NTFS lists a folder, case-insensitive with an underscore before the letters, the order the vanilla corpus was written against and the one that decides which addon provides a shared library (2178)
- GetNumAddOns and the index form of every AddOn verb walk a title-sorted list that hides the Blizzard addons the server says to hide, and the character-select AddOns list is in title order (2175)
- SetPoint and SetAllPoints raise the reference's own errors for a frame name that does not exist or an omitted relative frame instead of quietly anchoring to the parent, and an explicit nil means the screen root (2176)
- a tooltip owned with ANCHOR_CURSOR follows the mouse, which pfUI's tooltip, xpbar and chat modules, pfQuest's browser and TipBuddy ask for; an omitted or unknown anchor is ANCHOR_LEFT (2176)
- a $parent name handed to CreateFrame, CreateTexture or CreateFontString expands as it does in XML, so FonzAppraiser's thirty dropdowns and pfQuest's search icon get the names they asked for (2176)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 21:03:49 +07:00

779 lines
32 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));
// The tail is the addon-info block: `342` uncompressed + the zlib stream of the stock twelve
// (decision 1497). Byte-identical to a real 1.12.1.5875 client's — the same 130 compressed
// bytes as the 2006 retail capture wow-5875-re verified this against.
assert_eq!(
messages::auth_session(
5875,
"TESTUSER",
0x1122_3344,
&proof,
&messages::STOCK_SECURE_ADDONS
),
hx(concat!(
"f31600000000000054455354555345520044332211070a0d101316191c1f2225282b2e3134373a3d40",
"56010000",
"789c75ccbd0ec2300c04e0f21ebc0c614095c842c38c4ce2220bc7a98ccb4f9f1e16240673eb777781",
"695940cb693367a326c7be5bd5c77adf7d12be16c08c7124e41249a8c2e495480ac9c53dd8b67a064b",
"f8340f15467367bb38cc7ac7978bbddc26ccfe3042d6e6ca01a8b8908051fcb7a45070b812f33f2641",
"fdb5379019668f",
)),
"CMSG_AUTH_SESSION body"
);
// No secure addons: the tail is *absent*, never a zero size (which no real client emits).
assert_eq!(
messages::auth_session(5875, "TESTUSER", 0x1122_3344, &proof, &[]),
hx("f31600000000000054455354555345520044332211070a0d101316191c1f2225282b2e3134373a3d40"),
"CMSG_AUTH_SESSION body with no secure addons"
);
// 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 addon broadcast body** (`SendAddonMessage`, decision 1235) — `CMSG_MESSAGECHAT` on one of
/// four ordinary lanes with the `LANG_ADDON` sentinel in the language field. Bytes hand-computed
/// from the layout, independent of the Rust builder.
///
/// VERIFIED in `WoW.exe` (5875) — wow-re `system/ui/scratch/addon-chat-law.md` §5, the binding
/// `0x49f920`: opcode `0x95` (`0x49facf`), u32 chat type (`0x49fad8`), u32 language from
/// `or ebx,-0x1` (`0x49fab9`) written at `0x49fae1`, then the message CString (`0x49faf0`). There
/// is **no prefix field and no target field on the wire** — the prefix is glued to the message
/// with a literal TAB (`_snprintf(dst, 0x800, "%s\t%s", …)` at `0x49f9b3`, format `0x844b5c`, raw
/// `25 73 09 25 73 00`) and the pair rides as one C-string. The far client splits on the FIRST tab
/// (`0x49a8d0`).
///
/// Corroborated end-to-end against a live vmangos by `examples/addon_chat_probe` (decision 1029):
/// the sentinel comes back intact and the `0x09` survives (addon chat skips `SanitizeChatMessage`,
/// `Handlers/ChatHandler.cpp:49`).
#[test]
fn addon_message_bodies_golden() {
// `LANG_ADDON` — the whole discriminator. Not a tongue, and never rewritten by the server.
assert_eq!(
messages::LANGUAGE_ADDON,
0xFFFF_FFFF,
"LANG_ADDON (vmangos SharedDefines.h:270)"
);
// oRA2 `Core.lua:563`, verbatim: SendAddonMessage("CTRA", msg, "RAID") — the corpus's one
// live caller of this verb.
// 02 00 00 00 CHAT_MSG_RAID
// ff ff ff ff LANG_ADDON
// 43 54 52 41 "CTRA"
// 09 TAB — the prefix/message separator
// 73 74 61 74 75 73 "status"
// 00 the message C-string's NUL
assert_eq!(
messages::messagechat(
messages::CHAT_TYPE_RAID,
messages::LANGUAGE_ADDON,
"CTRA\tstatus"
),
hx("02000000ffffffff435452410973746174757300"),
"CMSG_MESSAGECHAT (addon, RAID) body"
);
// The other three lanes of the client's four-value whitelist (`0x49fa3f`-`0x49fa4e`). Only the
// type byte moves; the sentinel and the tab-composed payload are identical.
assert_eq!(
messages::messagechat(
messages::CHAT_TYPE_PARTY,
messages::LANGUAGE_ADDON,
"oRA\thello"
),
hx("01000000ffffffff6f52410968656c6c6f00"),
"CMSG_MESSAGECHAT (addon, PARTY) body"
);
assert_eq!(
messages::messagechat(
messages::CHAT_TYPE_GUILD,
messages::LANGUAGE_ADDON,
"oRA\thello"
),
hx("03000000ffffffff6f52410968656c6c6f00"),
"CMSG_MESSAGECHAT (addon, GUILD) body"
);
assert_eq!(
messages::messagechat(
messages::CHAT_TYPE_BATTLEGROUND,
messages::LANGUAGE_ADDON,
"oRA\thello"
),
hx("5c000000ffffffff6f52410968656c6c6f00"),
"CMSG_MESSAGECHAT (addon, BATTLEGROUND) body"
);
// AceEvent-2.0.lua's own line — `SendAddonMessage("LOOT_OPENED", "", "RAID")`, the call
// replicated in 24 of the 218 corpus addons. **The empty message still carries its tab**: the
// composition is unconditional, so the payload is `"LOOT_OPENED\t"` and the body ends TAB, NUL.
// A builder that "helpfully" dropped a trailing separator would make the far client read
// prefix `"LOOT_OPENED"` with no message at all — which is the same thing, but only by
// accident of the receiver's no-tab rule; the bytes must still match the reference's.
assert_eq!(
messages::messagechat(
messages::CHAT_TYPE_RAID,
messages::LANGUAGE_ADDON,
"LOOT_OPENED\t"
),
hx("02000000ffffffff4c4f4f545f4f50454e45440900"),
"CMSG_MESSAGECHAT (addon, empty message keeps its TAB) body"
);
// An addon line is byte-identical to ordinary speech on the same lane EXCEPT the language
// field — the property the receive-side gate rests on, pinned so a future refactor cannot
// quietly reintroduce a tongue here.
let addon = messages::messagechat(messages::CHAT_TYPE_PARTY, messages::LANGUAGE_ADDON, "x\ty");
let speech =
messages::messagechat(messages::CHAT_TYPE_PARTY, messages::LANGUAGE_COMMON, "x\ty");
assert_eq!(addon.len(), speech.len(), "same shape");
assert_eq!(addon[0..4], speech[0..4], "same chat type");
assert_eq!(addon[8..], speech[8..], "same payload");
assert_eq!(&addon[4..8], &[0xFF, 0xFF, 0xFF, 0xFF], "LANG_ADDON");
assert_eq!(&speech[4..8], &[0x07, 0x00, 0x00, 0x00], "LANG_COMMON");
}
/// 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 — the TAB critter filter's input — FAMILY, RANK, unk, pet_spell_id,
// display_id), 2 u8 tail. Family and rank are given DIFFERENT non-zero values on purpose:
// they are adjacent dwords, so a one-column slip between them reads as plausible data and
// shows up only as a pet whose level line names the wrong beast (decision 1062). `display_id`
// is non-zero for the same reason (decision 1676): it sits behind two alignment-only dwords
// that were zeros here, so a slip into either read 0 — indistinguishable from "no model" —
// and the stable window would have drawn an empty booth for every pet.
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)
"01000000", // pet_family = 1 (Wolf)
"02000000", // rank = 2 (rare elite)
"0000000000000000", // unk, pet_spell_list_id
"15020000", // display_id = 533 — the model a stabled pet is drawn from
"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,
pet_family: 1,
rank: 2,
type_flags: 0x10,
civilian: true,
racial_leader: true,
display_id: 533,
})
);
}
_ => 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:?}"),
}
}
/// **`SMSG_ADDON_INFO`, against the retail capture's own bytes** (decision 2175).
///
/// The reply carries no count and no names: the client re-walks the `## Secure:` list it sent and
/// reads one record per addon. The 2006 capture is the minimal form — 96 bytes, 12 x
/// `{status = 2, infoProvided = 1, keyProvided = 0, revision = 0u32, urlProvided = 0}` — and
/// `status = 2` is what sets `[rec+0x29] = 1` (`0x51db84`) and drops the addon from the Lua index
/// space (wow-re `system/net/scratch/cmsg-auth-session-addon-block.md` §6).
#[test]
fn the_addon_info_reply_pairs_its_statuses_back_against_what_we_sent() {
let capture: Vec<u8> = std::iter::repeat_n(hx("0201000000000000"), 12)
.flatten()
.collect();
assert_eq!(capture.len(), 96, "the capture is 12 x 8 bytes");
let packet = messages::parse_server(messages::opcode::SMSG_ADDON_INFO, &capture).unwrap();
let ServerPacket::AddonInfo { statuses } = packet else {
panic!("expected AddonInfo");
};
assert_eq!(statuses, vec![2u8; 12]);
// Paired back against the block we send, every stock addon is hidden from the index space —
// which is why the reference's AddOns list shows the player's addons and none of Blizzard's.
let hidden = messages::hidden_from_reply(&statuses, &messages::STOCK_SECURE_ADDONS);
assert_eq!(hidden.len(), 12);
assert!(
hidden.iter().all(|n| n.starts_with("Blizzard_")),
"{hidden:?}"
);
// A record that says it carries a key and a url is 8 + 256 + 256 bytes, and `status = 1`
// leaves the addon VISIBLE — the exclusion is `2` alone, not "the server said something".
let mut fat = vec![1u8, 1, 1];
fat.extend(std::iter::repeat_n(0xABu8, 256)); // modulus
fat.extend([0u8; 4]); // revision
fat.push(1); // urlProvided
fat.extend(std::iter::repeat_n(b'x', 256)); // url
let packet = messages::parse_server(messages::opcode::SMSG_ADDON_INFO, &fat).unwrap();
let ServerPacket::AddonInfo { statuses } = packet else {
panic!("expected AddonInfo");
};
assert_eq!(statuses, vec![1u8], "the fat record parsed whole");
assert!(messages::hidden_from_reply(&statuses, &messages::STOCK_SECURE_ADDONS).is_empty());
// A truncated record contributes nothing, and the whole ones before it survive — the pairing
// against what we sent cannot slip.
let mut short = hx("0201000000000000").to_vec();
short.extend([2u8, 1]); // a second record that stops mid-way
let packet = messages::parse_server(messages::opcode::SMSG_ADDON_INFO, &short).unwrap();
let ServerPacket::AddonInfo { statuses } = packet else {
panic!("expected AddonInfo");
};
assert_eq!(statuses, vec![2u8], "only the whole record counted");
}