- The second half of the open-door fix: the system that applies a door's state to its collider had never run once. The state arrives with the spawn, the collider is baked from the model hull frames later, and the reconcile demanded both in the same frame — so every door kept the solid collider it was born with. Open gates and forcefields are walkable now, and the inspector prints solid-or-passable out loud. - The mouseover follows the original's law: an object you cannot use — in use, waiting on a quest, or hostile-faction scenery like the enemy's own doors — shows nothing at all on hover. No tooltip, no highlight, no cursor. Each object type answers for itself, the way the real client dispatches it. - Dun Morogh's snow trees lose their white plates and hard creases: model textures authored clamp-to-edge were all sampled repeat-wrapped, so a canopy's deliberate out-of-range margin wrapped back into opaque texels. The sampler honours the authored address mode now, on this tree, a bad bush, and hundreds of models like them. - Odds and ends: the Blasted Lands lightning can actually strike (its emitters sampled an animation slot that is silent for that model), donation turn-in quests show their real Continue panel, boss emotes expand their $name macros, and a rare one-in-256 login failure — a session key the client hashed differently than the server does — is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
417 lines
18 KiB
Rust
417 lines
18 KiB
Rust
//! Chat + channel wire decode tests (mirrors `src/messages/chat.rs` + `src/messages/channel.rs`,
|
|
//! decision 0288 phase 1): one golden per `SMSG_MESSAGECHAT` per-type wire shape (the reader was
|
|
//! byte-correct before this decision but had zero golden coverage), `SMSG_TEXT_EMOTE`/`SMSG_EMOTE`,
|
|
//! every `SMSG_CHANNEL_NOTIFY` tail shape, `SMSG_CHANNEL_LIST`, `SMSG_CHAT_PLAYER_NOT_FOUND`,
|
|
//! `SMSG_CHAT_WRONG_FACTION`, `SMSG_NOTIFICATION`, `SMSG_PLAYED_TIME`, and `MSG_RANDOM_ROLL`'s
|
|
//! broadcast shape. Bytes
|
|
//! hand-computed from the vmangos layout (citations inline), independent of the Rust decoder — see
|
|
//! `tests/common` for the shared `hx` fixture helper.
|
|
|
|
mod common;
|
|
|
|
use benilla_protocol::events::{decode, SessionEvent};
|
|
use benilla_protocol::messages::{self, ChannelNoticeTail};
|
|
use benilla_protocol::ServerPacket;
|
|
use common::hx;
|
|
|
|
/// `SMSG_MESSAGECHAT`'s five distinct wire shapes (VERIFIED vmangos `ChatHandler::BuildChatPacket`,
|
|
/// `Chat/Chat.cpp:2542-2599`): SAY/PARTY/YELL (sender guid **twice**), MONSTER_SAY/MONSTER_YELL
|
|
/// (guid + length-prefixed name + target guid), CHANNEL (cstring channel + `u32` rank + guid),
|
|
/// MONSTER_WHISPER/RAID_BOSS_WHISPER/RAID_BOSS_EMOTE/MONSTER_EMOTE (length-prefixed name + target
|
|
/// guid, no leading sender guid), and the `default:` shape (one sender guid) — SYSTEM, WHISPER,
|
|
/// EMOTE, RAID, GUILD, OFFICER, AFK, DND, IGNORED, RAID_LEADER/WARNING, BATTLEGROUND(+LEADER) all
|
|
/// fall here. Every shape ends `u32 len (incl NUL) + text + NUL`, then the trailing `u8 chatTag`
|
|
/// (`Chat/Chat.h:86-92`) this decision surfaces on [`messages::ChatMessage::chat_tag`].
|
|
#[test]
|
|
fn message_chat_decodes_every_wire_shape() {
|
|
// SAY: guid twice, chat_tag = AFK (1) — proves the tag rides through on the "two guid" shape.
|
|
let body = hx("0007000000887766554433221188776655443322110900000068692074686572650001");
|
|
match messages::parse_server(messages::opcode::SMSG_MESSAGECHAT, &body).unwrap() {
|
|
ServerPacket::MessageChat(m) => {
|
|
assert_eq!(m.chat_type, messages::CHAT_MSG_SAY);
|
|
assert_eq!(m.language, 7);
|
|
assert_eq!(m.sender_guid, 0x1122_3344_5566_7788);
|
|
assert_eq!(m.sender_name, None);
|
|
assert_eq!(m.channel, None);
|
|
assert_eq!(m.text, "hi there");
|
|
assert_eq!(m.chat_tag, messages::chat_tag::AFK);
|
|
}
|
|
_ => panic!("expected MessageChat"),
|
|
}
|
|
|
|
// MONSTER_SAY: guid + length-prefixed name + target guid, chat_tag = none. Body: type(0b) +
|
|
// language(00000000) + sender guid(ddccbbaa…) + namelen(0f000000) + "Timmy the Wolf\0" +
|
|
// target guid(66778899…) + msglen(05000000) + "Grr!\0" + chatTag(00).
|
|
let body = hx("0b00000000ddccbbaa000000000f00000054696d6d792074686520576f6c6600667788990000000005000000477272210000");
|
|
match messages::parse_server(messages::opcode::SMSG_MESSAGECHAT, &body).unwrap() {
|
|
ServerPacket::MessageChat(m) => {
|
|
assert_eq!(m.chat_type, messages::CHAT_MSG_MONSTER_SAY);
|
|
assert_eq!(m.sender_guid, 0xAABB_CCDD);
|
|
// The addressee — the guid slot between the name and the message. It is the subject
|
|
// the real client's `$`-macro expander resolves this line against (decision 0754), so
|
|
// it has to survive the parse instead of being read and dropped.
|
|
assert_eq!(m.target_guid, 0x9988_7766);
|
|
assert_eq!(m.sender_name.as_deref(), Some("Timmy the Wolf"));
|
|
assert_eq!(m.channel, None);
|
|
assert_eq!(m.text, "Grr!");
|
|
assert_eq!(m.chat_tag, messages::chat_tag::NONE);
|
|
}
|
|
_ => panic!("expected MessageChat"),
|
|
}
|
|
|
|
// CHANNEL: cstring channel + u32 rank + guid. Body: type(0e) + language(00000000) +
|
|
// "General\0" + playerRank(00000000) + sender guid(3412…) + msglen(10000000) +
|
|
// "wtb boar livers\0" + chatTag(00).
|
|
let body = hx("0e0000000047656e6572616c000000000034120000000000001000000077746220626f6172206c69766572730000");
|
|
match messages::parse_server(messages::opcode::SMSG_MESSAGECHAT, &body).unwrap() {
|
|
ServerPacket::MessageChat(m) => {
|
|
assert_eq!(m.chat_type, messages::CHAT_MSG_CHANNEL);
|
|
assert_eq!(m.channel.as_deref(), Some("General"));
|
|
assert_eq!(m.sender_guid, 0x1234);
|
|
assert_eq!(m.sender_name, None);
|
|
assert_eq!(m.text, "wtb boar livers");
|
|
}
|
|
_ => panic!("expected MessageChat"),
|
|
}
|
|
|
|
// MONSTER_WHISPER: length-prefixed name + target guid (no leading sender guid at all). Body:
|
|
// type(1a) + language(00000000) + namelen(0e000000) + "Innkeeper Bob\0" + target guid(55…) +
|
|
// msglen(19000000) + "Welcome, weary traveler!\0".
|
|
let body = hx("1a000000000e000000496e6e6b656570657220426f620055000000000000001900000057656c636f6d652c2077656172792074726176656c6572210000");
|
|
match messages::parse_server(messages::opcode::SMSG_MESSAGECHAT, &body).unwrap() {
|
|
ServerPacket::MessageChat(m) => {
|
|
assert_eq!(m.chat_type, messages::CHAT_MSG_MONSTER_WHISPER);
|
|
assert_eq!(m.sender_guid, 0, "no leading guid on this shape");
|
|
assert_eq!(m.target_guid, 0x55, "the whispered-at player");
|
|
assert_eq!(m.sender_name.as_deref(), Some("Innkeeper Bob"));
|
|
assert_eq!(m.text, "Welcome, weary traveler!");
|
|
}
|
|
_ => panic!("expected MessageChat"),
|
|
}
|
|
|
|
// default shape (SYSTEM): one sender guid (0), chat_tag = GM (3). Body: type(0a) +
|
|
// language(00000000) + sender guid(0000000000000000) + msglen(1a000000) +
|
|
// "This is a system message.\0" + chatTag(03).
|
|
let body = hx(
|
|
"0a0000000000000000000000001a0000005468697320697320612073797374656d206d6573736167652e0003",
|
|
);
|
|
match messages::parse_server(messages::opcode::SMSG_MESSAGECHAT, &body).unwrap() {
|
|
ServerPacket::MessageChat(m) => {
|
|
assert_eq!(m.chat_type, messages::CHAT_MSG_SYSTEM);
|
|
assert_eq!(m.sender_guid, 0);
|
|
assert_eq!(m.text, "This is a system message.");
|
|
assert_eq!(m.chat_tag, messages::chat_tag::GM);
|
|
}
|
|
_ => panic!("expected MessageChat"),
|
|
}
|
|
}
|
|
|
|
/// `SMSG_TEXT_EMOTE` (VERIFIED vmangos `MaNGOS::EmoteChatBuilder::operator()`,
|
|
/// `Handlers/ChatHandler.cpp:681-702`): `u64` performer guid (an `ObjectGuid` write, unpacked) + `u32`
|
|
/// textEmote + `u32` emoteNum + `u32 namelen` + `namelen` bytes (the target's name + NUL, or a lone
|
|
/// NUL when untargeted) — and `SMSG_EMOTE` (VERIFIED vmangos `EmoteNotify::AppendBodyTo`,
|
|
/// `Server/Packets/Misc.cpp:670-674`): `u32` emoteId + `u64` guid.
|
|
#[test]
|
|
fn text_emote_and_emote_decode() {
|
|
let body = hx("7700000000000000650000000000000004000000426f6200");
|
|
let p = messages::parse_server(messages::opcode::SMSG_TEXT_EMOTE, &body).unwrap();
|
|
assert!(matches!(
|
|
p,
|
|
ServerPacket::TextEmote {
|
|
guid: 0x77,
|
|
text_emote: 101,
|
|
}
|
|
));
|
|
assert!(matches!(
|
|
decode(p)[..],
|
|
[SessionEvent::TextEmote {
|
|
guid: 0x77,
|
|
text_emote: 101,
|
|
}]
|
|
));
|
|
|
|
let body = hx("060000008800000000000000");
|
|
let p = messages::parse_server(messages::opcode::SMSG_EMOTE, &body).unwrap();
|
|
assert!(matches!(
|
|
p,
|
|
ServerPacket::Emote {
|
|
guid: 0x88,
|
|
emote_id: 6,
|
|
}
|
|
));
|
|
assert!(matches!(
|
|
decode(p)[..],
|
|
[SessionEvent::Emote {
|
|
guid: 0x88,
|
|
emote_id: 6,
|
|
}]
|
|
));
|
|
}
|
|
|
|
/// Every `SMSG_CHANNEL_NOTIFY` tail shape (VERIFIED vmangos `Channel::Make*`,
|
|
/// `Chat/Channel.cpp:804-1008`), one representative per [`ChannelNoticeTail`] variant, plus the
|
|
/// unrecognized-notice error path.
|
|
#[test]
|
|
fn channel_notify_decodes_each_tail_shape() {
|
|
// 0x00 JOINED: guid.
|
|
let body = hx("0047656e6572616c00aa00000000000000");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::JOINED);
|
|
assert_eq!(n.channel, "General");
|
|
assert_eq!(n.tail, ChannelNoticeTail::Guid(0xAA));
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// 0x02 YOU_JOINED: u32 flags + u32 0 (reserved).
|
|
let body = hx("0247656e6572616c001800000000000000");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::YOU_JOINED);
|
|
assert_eq!(n.tail, ChannelNoticeTail::YouJoined { flags: 0x18 });
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// 0x05 NOT_MEMBER: empty tail — both the instance (`MakeNotMember`) and static
|
|
// (`MakeNotOnPacket`) vmangos producers write this exact shape; no special-casing needed.
|
|
let body = hx("0553656372657400");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::NOT_MEMBER);
|
|
assert_eq!(n.channel, "Secret");
|
|
assert_eq!(n.tail, ChannelNoticeTail::Empty);
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// 0x18 INVITE: a lone actor guid.
|
|
let body = hx("1847656e6572616c00bb00000000000000");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::INVITE);
|
|
assert_eq!(n.tail, ChannelNoticeTail::Actor(0xBB));
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// 0x09 PLAYER_NOT_FOUND: a bare name.
|
|
let body = hx("0947656e6572616c0047686f737400");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::PLAYER_NOT_FOUND);
|
|
assert_eq!(n.tail, ChannelNoticeTail::Name("Ghost".into()));
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// 0x0B CHANNEL_OWNER: the "Nobody" literal vmangos sends for an ownerless non-constant channel.
|
|
let body = hx("0b47656e6572616c004e6f626f647900");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::CHANNEL_OWNER);
|
|
assert_eq!(n.tail, ChannelNoticeTail::Name("Nobody".into()));
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// 0x0C MODE_CHANGE: guid + old flags + new flags.
|
|
let body = hx("0c47656e6572616c00cc000000000000000002");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::MODE_CHANGE);
|
|
assert_eq!(
|
|
n.tail,
|
|
ChannelNoticeTail::ModeChange {
|
|
guid: 0xCC,
|
|
old_flags: 0x00,
|
|
new_flags: 0x02,
|
|
}
|
|
);
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// 0x12 PLAYER_KICKED: target guid + source (actor) guid.
|
|
let body = hx("1247656e6572616c00dd00000000000000ee00000000000000");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap() {
|
|
ServerPacket::ChannelNotify(n) => {
|
|
assert_eq!(n.notice, messages::channel_notice::PLAYER_KICKED);
|
|
assert_eq!(
|
|
n.tail,
|
|
ChannelNoticeTail::Actors {
|
|
target: 0xDD,
|
|
source: 0xEE,
|
|
}
|
|
);
|
|
}
|
|
_ => panic!("expected ChannelNotify"),
|
|
}
|
|
|
|
// Round-trip the decoded ChannelNotify through the event layer once (the mapping is a flat
|
|
// passthrough, but it must actually be wired).
|
|
let body = hx("0047656e6572616c00aa00000000000000");
|
|
let p = messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).unwrap();
|
|
match &decode(p)[..] {
|
|
[SessionEvent::ChannelNotify {
|
|
notice,
|
|
channel,
|
|
tail,
|
|
}] => {
|
|
assert_eq!(*notice, messages::channel_notice::JOINED);
|
|
assert_eq!(channel, "General");
|
|
assert_eq!(*tail, ChannelNoticeTail::Guid(0xAA));
|
|
}
|
|
other => panic!("channel notify decode: {} events", other.len()),
|
|
}
|
|
|
|
// An unrecognized notice byte (outside vmangos's defined 0x00..=0x1F) errors rather than
|
|
// guessing a tail shape.
|
|
let body = hx("2047656e6572616c00");
|
|
assert!(messages::parse_server(messages::opcode::SMSG_CHANNEL_NOTIFY, &body).is_err());
|
|
}
|
|
|
|
/// `SMSG_CHANNEL_LIST` (VERIFIED vmangos `Channel::List`, `Chat/Channel.cpp:513-556`): cstring
|
|
/// channel + `u8` flags + `u32` count + that many `(u64 guid, u8 memberFlags)` rows.
|
|
#[test]
|
|
fn channel_list_decodes_roster() {
|
|
let body = hx("47656e6572616c001802000000011000000000000001021000000000000000");
|
|
match messages::parse_server(messages::opcode::SMSG_CHANNEL_LIST, &body).unwrap() {
|
|
ServerPacket::ChannelList {
|
|
channel,
|
|
flags,
|
|
members,
|
|
} => {
|
|
assert_eq!(channel, "General");
|
|
assert_eq!(flags, 0x18);
|
|
assert_eq!(members, vec![(0x1001, 0x01), (0x1002, 0x00)]);
|
|
}
|
|
_ => panic!("expected ChannelList"),
|
|
}
|
|
}
|
|
|
|
/// `SMSG_CHAT_PLAYER_NOT_FOUND` (cstring name, VERIFIED vmangos `Server/Packets/Chat.cpp:26-29`) and
|
|
/// `SMSG_CHAT_WRONG_FACTION` (empty body, `Server/Packets/Chat.cpp:16-18`).
|
|
#[test]
|
|
fn chat_player_not_found_and_wrong_faction_decode() {
|
|
let body = hx("47686f73746e616d6500");
|
|
match messages::parse_server(messages::opcode::SMSG_CHAT_PLAYER_NOT_FOUND, &body).unwrap() {
|
|
ServerPacket::ChatPlayerNotFound { name } => assert_eq!(name, "Ghostname"),
|
|
_ => panic!("expected ChatPlayerNotFound"),
|
|
}
|
|
assert!(matches!(
|
|
decode(
|
|
messages::parse_server(messages::opcode::SMSG_CHAT_PLAYER_NOT_FOUND, &body).unwrap()
|
|
)[..],
|
|
[SessionEvent::ChatPlayerNotFound { .. }]
|
|
));
|
|
|
|
let p = messages::parse_server(messages::opcode::SMSG_CHAT_WRONG_FACTION, &[]).unwrap();
|
|
assert!(matches!(p, ServerPacket::ChatWrongFaction));
|
|
assert!(matches!(decode(p)[..], [SessionEvent::ChatWrongFaction]));
|
|
}
|
|
|
|
/// `SMSG_NOTIFICATION` (VERIFIED vmangos `WorldSession::SendNotification`,
|
|
/// `Server/WorldSession.cpp:900-915`: `data << szStr`): one cstring, pre-formatted server-side —
|
|
/// the "You do not know that language" reply is how a wrong-tongue send fails (silently, until
|
|
/// this packet was rendered).
|
|
#[test]
|
|
fn notification_decodes() {
|
|
let body = hx("596f7520646f206e6f74206b6e6f772074686174206c616e677561676500");
|
|
match messages::parse_server(messages::opcode::SMSG_NOTIFICATION, &body).unwrap() {
|
|
ServerPacket::Notification { ref text } => {
|
|
assert_eq!(text, "You do not know that language")
|
|
}
|
|
other => panic!("expected Notification, got {}", other.name()),
|
|
}
|
|
match &decode(messages::parse_server(messages::opcode::SMSG_NOTIFICATION, &body).unwrap())[..] {
|
|
[SessionEvent::Notification { text }] => assert_eq!(text, "You do not know that language"),
|
|
other => panic!("notification decode: {} events", other.len()),
|
|
}
|
|
}
|
|
|
|
/// `SMSG_PLAYED_TIME` (VERIFIED vmangos `WorldPackets::Misc::PlayedTime::AppendBodyTo`,
|
|
/// `Server/Packets/Misc.cpp:278-282`): `u32` total + `u32` level, both seconds.
|
|
#[test]
|
|
fn played_time_decodes() {
|
|
let body = hx("40e20100100e0000"); // total = 123456, level = 3600
|
|
let p = messages::parse_server(messages::opcode::SMSG_PLAYED_TIME, &body).unwrap();
|
|
assert!(matches!(
|
|
p,
|
|
ServerPacket::PlayedTime {
|
|
total: 123_456,
|
|
level: 3600,
|
|
}
|
|
));
|
|
assert!(matches!(
|
|
decode(p)[..],
|
|
[SessionEvent::PlayedTime {
|
|
total: 123_456,
|
|
level: 3600,
|
|
}]
|
|
));
|
|
}
|
|
|
|
/// `MSG_RANDOM_ROLL`'s server→client broadcast shape (VERIFIED vmangos
|
|
/// `WorldSession::HandleRandomRollOpcode`, `Handlers/GroupHandler.cpp:394-422`): `u32` min + `u32`
|
|
/// max + `u32` roll + a **raw** `u64` guid.
|
|
#[test]
|
|
fn random_roll_broadcast_decodes() {
|
|
let body = hx("01000000640000002a0000009999000000000000");
|
|
let p = messages::parse_server(messages::opcode::MSG_RANDOM_ROLL, &body).unwrap();
|
|
assert!(matches!(
|
|
p,
|
|
ServerPacket::RandomRoll {
|
|
min: 1,
|
|
max: 100,
|
|
roll: 42,
|
|
guid: 0x9999,
|
|
}
|
|
));
|
|
assert!(matches!(
|
|
decode(p)[..],
|
|
[SessionEvent::RandomRoll {
|
|
min: 1,
|
|
max: 100,
|
|
roll: 42,
|
|
guid: 0x9999,
|
|
}]
|
|
));
|
|
}
|
|
|
|
/// The `$`-macro gate is a byte-level fact, not a design choice — pin it.
|
|
///
|
|
/// **The reference has TWO gates, and we want the WIRE one** (decision 0759 corrects 0754). The
|
|
/// `SMSG_MESSAGECHAT` parser `0x49d560` routes `0x0B 0x0C 0x0D 0x1A 0x5A` into the expanding branch
|
|
/// at `0x49da3d` (`0x49d5e4-0x49d606`) and `0x52 0x53 0x54` into the one at `0x49d961` — **eight**.
|
|
/// The seven-value chain at `0x49cf36-0x49cf5d` is the *pending-chat re-expansion* gate and omits
|
|
/// `0x5A`; 0754 cited that one by mistake, leaving boss emotes unexpanded.
|
|
///
|
|
/// Three plausible-and-wrong edits this guards against: dropping `RAID_BOSS_EMOTE` (0x5A) because
|
|
/// the other gate omits it, adding `RAID_BOSS_WHISPER` (0x59) because it looks like its sibling, and
|
|
/// dropping the BG_SYSTEM trio (0x52-0x54) because they look like plain system lines. Player chat is
|
|
/// never expanded.
|
|
#[test]
|
|
fn the_macro_expanded_chat_types_are_the_reference_wire_gate() {
|
|
let mut got = messages::MACRO_EXPANDED_TYPES;
|
|
got.sort_unstable();
|
|
assert_eq!(got, [0x0B, 0x0C, 0x0D, 0x1A, 0x52, 0x53, 0x54, 0x5A]);
|
|
// 0x59 RAID_BOSS_WHISPER takes its own branch (`0x49d610`), never expands, and is remapped to a
|
|
// plain whisper — it must never creep in alongside 0x5A.
|
|
for t in [
|
|
messages::CHAT_MSG_SAY,
|
|
messages::CHAT_MSG_YELL,
|
|
messages::CHAT_MSG_WHISPER,
|
|
messages::CHAT_MSG_GUILD,
|
|
messages::CHAT_MSG_CHANNEL,
|
|
messages::CHAT_MSG_SYSTEM,
|
|
messages::CHAT_MSG_RAID_BOSS_WHISPER,
|
|
] {
|
|
assert!(
|
|
!messages::MACRO_EXPANDED_TYPES.contains(&t),
|
|
"type {t:#04x} must reach the frame verbatim"
|
|
);
|
|
}
|
|
}
|