Files
benilla-octo/crates/benilla-protocol/tests/guild.rs
T
Sam SungandClaude Opus 5.5 e739c8237b Comments in benilla-protocol: the second read, citations on the pinned revision and restored facts (#375)
The second read of `benilla-protocol`'s comments, the last step #41 asked for: that crate's comments were rewritten to the rule before second readers existed. Four readers compared every file's comments against their state before the rewrite and against the code as it is now.

Fixes #41.

- **Citations.** vmangos line citations are re-pointed to the pinned revision: off-by-a-few lines (`Pet.cpp`, `Guild.cpp`, `Petition.cpp`, `LootHandler.cpp`, `ChatHandler.cpp`), and paths the rewrite dropped where a bare file name is ambiguous (`Server/Packets/Spell.cpp` against `Spells/Spell.cpp`, the two `Item.cpp`, `Group.cpp` and `Chat.cpp`). The gossip-hello citation is `NPCHandler.cpp:347`.
- **Facts restored.** The move-time-skipped chain (`0x603b40` → `0x601560` → `0x61ab90`), the addon-message TAB surviving vmangos's chat sanitizer, the zone-under-attack sender, `SMSG_ADDON_INFO` answering the auth session's addon block, the officer-note permission gate, and `SMSG_ITEM_COOLDOWN` on equipping an item with an on-use spell (the old text said a proc).
- **Deviation labels.** A label now carries its reason, or goes when the behaviour is only unbuilt (the survey window, the group XP bonus, the NPC-text language garble).
- **Two examples.** `park_probe` and `login_refusal_probe` took a hard-coded local account; they now read `WOW_USER` and `WOW_PASS`.
- **Verification.** Every file strips to identical code apart from the two examples; `cargo fmt --check` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

664 lines
23 KiB
Rust

//! The guild wire: the query and roster caches, invitations, member verbs, rank administration,
//! the event broadcast and command results, laid out per vmangos.
mod common;
use benilla_protocol::events::{decode, SessionEvent};
use benilla_protocol::messages::{
self, guild_command, guild_command_error, guild_event, guild_presence, guild_rank_right,
opcode, GUILD_RANKS_MAX_COUNT,
};
use benilla_protocol::ServerPacket;
use common::hx;
#[test]
fn cmsg_bodies_golden() {
// CMSG_GUILD_QUERY (vmangos Server/Packets/Guild.cpp:8-11): one u32, the guild id.
assert_eq!(
messages::guild_query(0x1234),
hx("34120000"),
"CMSG_GUILD_QUERY body"
);
// The one-cstring verbs (Guild.cpp:3-6, 13-16, 18-21, 23-26, 28-31, 33-36, 73-76, 44-49).
assert_eq!(
messages::guild_create("Legacy"),
hx("4c656761637900"),
"CMSG_GUILD_CREATE body"
);
assert_eq!(
messages::guild_invite("Bob"),
hx("426f6200"),
"CMSG_GUILD_INVITE body"
);
assert_eq!(
messages::guild_promote("Bob"),
hx("426f6200"),
"CMSG_GUILD_PROMOTE body"
);
assert_eq!(
messages::guild_demote("Bob"),
hx("426f6200"),
"CMSG_GUILD_DEMOTE body"
);
assert_eq!(
messages::guild_remove("Bob"),
hx("426f6200"),
"CMSG_GUILD_REMOVE body"
);
assert_eq!(
messages::guild_leader("Bob"),
hx("426f6200"),
"CMSG_GUILD_LEADER body"
);
assert_eq!(
messages::guild_add_rank("Grunt"),
hx("4772756e7400"),
"CMSG_GUILD_ADD_RANK body"
);
assert_eq!(
messages::guild_info_text("We raid."),
hx("576520726169642e00"),
"CMSG_GUILD_INFO_TEXT body"
);
// CMSG_GUILD_MOTD (Guild.cpp:38-42): one cstring.
assert_eq!(
messages::guild_motd("Hello"),
hx("48656c6c6f00"),
"CMSG_GUILD_MOTD body"
);
// The empty verbs (vmangos Opcodes.cpp:213, 214, 216, 218, 222, 224, 655: NullClientPacket).
assert_eq!(
messages::guild_accept(),
Vec::<u8>::new(),
"CMSG_GUILD_ACCEPT body"
);
assert_eq!(
messages::guild_decline(),
Vec::<u8>::new(),
"CMSG_GUILD_DECLINE body"
);
assert_eq!(
messages::guild_info(),
Vec::<u8>::new(),
"CMSG_GUILD_INFO body"
);
assert_eq!(
messages::guild_roster(),
Vec::<u8>::new(),
"CMSG_GUILD_ROSTER body"
);
assert_eq!(
messages::guild_leave(),
Vec::<u8>::new(),
"CMSG_GUILD_LEAVE body"
);
assert_eq!(
messages::guild_disband(),
Vec::<u8>::new(),
"CMSG_GUILD_DISBAND body"
);
assert_eq!(
messages::guild_del_rank(),
Vec::<u8>::new(),
"CMSG_GUILD_DEL_RANK body"
);
// CMSG_GUILD_RANK (Guild.cpp:78-83): u32 rankId, u32 rights, cstring rankName.
let rights = guild_rank_right::GCHAT_LISTEN
| guild_rank_right::GCHAT_SPEAK
| guild_rank_right::OFFCHAT_LISTEN
| guild_rank_right::OFFCHAT_SPEAK
| guild_rank_right::INVITE
| guild_rank_right::PROMOTE
| guild_rank_right::DEMOTE;
assert_eq!(rights, 0x0000_019F, "the officer rights mask");
assert_eq!(
messages::guild_rank(2, rights, "Veteran"),
hx(concat!("02000000", "9f010000", "5665746572616e00")),
"CMSG_GUILD_RANK body"
);
// CMSG_GUILD_SET_PUBLIC_NOTE / _OFFICER_NOTE (Guild.cpp:61-65 / 67-71): name, then note.
assert_eq!(
messages::guild_set_public_note("Bob", "hi"),
hx(concat!("426f6200", "686900")),
"CMSG_GUILD_SET_PUBLIC_NOTE body"
);
assert_eq!(
messages::guild_set_officer_note("Bob", "hi"),
hx(concat!("426f6200", "686900")),
"CMSG_GUILD_SET_OFFICER_NOTE body"
);
}
/// An empty MOTD is a one-byte empty cstring; vmangos (`Server/Packets/Guild.cpp:38-42`) reads a
/// zero-byte body as a clear too.
#[test]
fn guild_motd_clears_with_an_empty_cstring_not_an_empty_body() {
assert_eq!(
messages::guild_motd(""),
hx("00"),
"CMSG_GUILD_MOTD body, cleared"
);
assert_eq!(messages::guild_motd("").len(), 1, "one NUL, not zero bytes");
}
/// `SMSG_GUILD_QUERY_RESPONSE` always carries ten rank-name cstrings, not a counted list (vmangos
/// `Server/Packets/Guild.cpp:118-131`); the distinct emblem values after them catch a short read.
#[test]
fn guild_query_response_always_carries_ten_rank_names() {
let mut body = Vec::new();
body.extend_from_slice(&7u32.to_le_bytes()); // guildId
body.extend_from_slice(b"Legacy of Steel\0"); // guildName
for name in [
b"Guild Master".as_slice(),
b"Officer".as_slice(),
b"Veteran".as_slice(),
b"Member".as_slice(),
b"Initiate".as_slice(),
] {
body.extend_from_slice(name);
body.push(0);
}
// The five uncreated ranks: an empty string each, a bare NUL, not absent.
body.extend_from_slice(&[0u8; GUILD_RANKS_MAX_COUNT - 5]);
body.extend_from_slice(&1u32.to_le_bytes()); // emblemStyle
body.extend_from_slice(&2u32.to_le_bytes()); // emblemColor
body.extend_from_slice(&3u32.to_le_bytes()); // borderStyle
body.extend_from_slice(&4u32.to_le_bytes()); // borderColor
body.extend_from_slice(&5u32.to_le_bytes()); // backgroundColor
let packet = messages::parse_server(opcode::SMSG_GUILD_QUERY_RESPONSE, &body).unwrap();
let response = match &packet {
ServerPacket::GuildQueryResponse(r) => r.clone(),
other => panic!("expected GuildQueryResponse, got {}", other.name()),
};
assert_eq!(response.guild_id, 7);
assert_eq!(response.name, "Legacy of Steel");
assert_eq!(response.rank_names[0], "Guild Master");
assert_eq!(response.rank_names[4], "Initiate");
assert_eq!(
&response.rank_names[5..],
&["", "", "", "", ""],
"the five uncreated ranks are empty strings on the wire"
);
assert_eq!(response.emblem_style, 1, "emblemStyle — the desync canary");
assert_eq!(response.emblem_color, 2);
assert_eq!(response.border_style, 3);
assert_eq!(response.border_color, 4);
assert_eq!(response.background_color, 5);
match decode(packet).as_slice() {
[SessionEvent::GuildQueryResponse(r)] => assert_eq!(r.name, "Legacy of Steel"),
other => panic!("guild query response decode: {other:?}"),
}
}
/// "No such guild" is a complete `SMSG_GUILD_QUERY_RESPONSE` with an empty name: the reference
/// branches on it (`0x5552ae`) and still consumes the whole record.
#[test]
fn guild_query_response_reports_no_such_guild_as_an_empty_name() {
let mut body = Vec::new();
body.extend_from_slice(&404u32.to_le_bytes()); // the id we asked about
body.push(0); // guildName: empty == "this id names nothing"
body.extend_from_slice(&[0u8; GUILD_RANKS_MAX_COUNT]); // ten empty rank names, still sent
for _ in 0..5 {
body.extend_from_slice(&0u32.to_le_bytes()); // the emblem block, still sent
}
match &messages::parse_server(opcode::SMSG_GUILD_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::GuildQueryResponse(response) => {
assert_eq!(response.guild_id, 404);
assert_eq!(response.name, "", "the empty name IS the not-found signal");
assert!(response.rank_names.iter().all(String::is_empty));
assert_eq!(response.background_color, 0, "the full record was consumed");
}
other => panic!("expected GuildQueryResponse, got {}", other.name()),
}
}
/// Append one `SMSG_GUILD_ROSTER` member (vmangos `Server/Packets/Guild.cpp:155-172`); the caller
/// passes `last_online` itself, `Some` only for an offline member.
fn push_member(
body: &mut Vec<u8>,
guid: u64,
presence: u8,
name: &str,
rank_id: u32,
level: u8,
class: u8,
zone: u32,
last_online: Option<f32>,
public_note: &str,
officer_note: &str,
) {
body.extend_from_slice(&guid.to_le_bytes());
body.push(presence);
body.extend_from_slice(name.as_bytes());
body.push(0);
body.extend_from_slice(&rank_id.to_le_bytes());
body.push(level);
body.push(class);
body.extend_from_slice(&zone.to_le_bytes());
if let Some(days) = last_online {
body.extend_from_slice(&days.to_le_bytes());
}
body.extend_from_slice(public_note.as_bytes());
body.push(0);
body.extend_from_slice(officer_note.as_bytes());
body.push(0);
}
/// An `SMSG_GUILD_ROSTER` head (Server/Packets/Guild.cpp:143-153): member count, MOTD, info text,
/// then the counted rank-rights array.
fn roster_head(member_count: u32, motd: &str, info: &str, rank_rights: &[u32]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&member_count.to_le_bytes());
body.extend_from_slice(motd.as_bytes());
body.push(0);
body.extend_from_slice(info.as_bytes());
body.push(0);
body.extend_from_slice(&(rank_rights.len() as u32).to_le_bytes());
for rights in rank_rights {
body.extend_from_slice(&rights.to_le_bytes());
}
body
}
/// `SMSG_GUILD_ROSTER`'s `f32 lastOnlineTime` is sent only when the presence byte is 0 (vmangos
/// `Server/Packets/Guild.cpp:164-165`); the third member, after a wider offline one, checks the
/// parse stays in step.
#[test]
fn guild_roster_conditional_float_keeps_later_members_in_sync() {
let mut body = roster_head(
3,
"Raid at 8",
"We are a guild.",
&[guild_rank_right::ALL, 0x0000_019F, 0x0000_0003],
);
push_member(
&mut body,
0x1111,
guild_presence::ONLINE,
"Alice",
0,
60,
2,
1519,
None,
"the GM",
"trusted",
);
push_member(
&mut body,
0x2222,
guild_presence::OFFLINE,
"Bob",
1,
58,
4,
1497,
Some(3.5),
"on holiday",
"back next week",
);
push_member(
&mut body,
0x3333,
guild_presence::ONLINE | guild_presence::AFK,
"Carol",
3,
42,
9,
141,
None,
"levelling",
"promote soon",
);
let packet = messages::parse_server(opcode::SMSG_GUILD_ROSTER, &body).unwrap();
let roster = match &packet {
ServerPacket::GuildRoster(r) => r.clone(),
other => panic!("expected GuildRoster, got {}", other.name()),
};
assert_eq!(roster.motd, "Raid at 8");
assert_eq!(roster.info, "We are a guild.");
assert_eq!(
roster.rank_rights,
vec![guild_rank_right::ALL, 0x0000_019F, 0x0000_0003],
"rank_rights' length is the guild's real rank count"
);
assert_eq!(roster.members.len(), 3);
let alice = &roster.members[0];
assert!(alice.is_online());
assert_eq!(alice.name, "Alice");
assert_eq!(alice.last_online_days, 0.0, "online: the float is not sent");
assert_eq!(alice.public_note, "the GM");
assert_eq!(alice.officer_note, "trusted");
let bob = &roster.members[1];
assert!(!bob.is_online());
assert_eq!(bob.name, "Bob");
assert_eq!(bob.last_online_days, 3.5, "offline: the float IS sent");
assert_eq!(bob.public_note, "on holiday");
assert_eq!(bob.officer_note, "back next week");
let carol = &roster.members[2];
assert_eq!(carol.guid, 0x3333, "third member guid — the desync canary");
assert_eq!(carol.name, "Carol");
assert_eq!(carol.presence, guild_presence::ONLINE | guild_presence::AFK);
assert!(carol.is_online());
assert_eq!(carol.rank_id, 3);
assert_eq!(carol.level, 42);
assert_eq!(carol.class, 9);
assert_eq!(carol.zone, 141);
assert_eq!(carol.last_online_days, 0.0);
assert_eq!(carol.public_note, "levelling");
assert_eq!(carol.officer_note, "promote soon");
match decode(packet).as_slice() {
[SessionEvent::GuildRoster(r)] => assert_eq!(r.members.len(), 3),
other => panic!("guild roster decode: {other:?}"),
}
}
#[test]
fn guild_roster_with_no_members_is_just_the_head() {
let body = roster_head(0, "", "", &[guild_rank_right::ALL]);
let packet = messages::parse_server(opcode::SMSG_GUILD_ROSTER, &body).unwrap();
match &packet {
ServerPacket::GuildRoster(roster) => {
assert!(roster.members.is_empty());
assert_eq!(roster.motd, "");
assert_eq!(roster.info, "");
assert_eq!(roster.rank_rights, vec![guild_rank_right::ALL]);
}
other => panic!("expected GuildRoster, got {}", other.name()),
}
}
/// A viewer without `GR_RIGHT_VIEWOFFNOTE` gets `""` for every officer note (vmangos
/// `Guild/Guild.cpp:819`, `:846`): the cstring's NUL is still on the wire.
#[test]
fn guild_roster_reads_the_empty_officer_notes_of_an_unprivileged_viewer() {
let mut body = roster_head(2, "motd", "info", &[guild_rank_right::ALL, 0x0000_0003]);
push_member(
&mut body,
0x1111,
guild_presence::OFFLINE,
"Alice",
0,
60,
2,
1519,
Some(0.25),
"note A",
"",
);
push_member(
&mut body,
0x2222,
guild_presence::ONLINE,
"Bob",
1,
58,
4,
1497,
None,
"note B",
"",
);
let packet = messages::parse_server(opcode::SMSG_GUILD_ROSTER, &body).unwrap();
match &packet {
ServerPacket::GuildRoster(roster) => {
assert_eq!(roster.members.len(), 2);
assert_eq!(roster.members[0].officer_note, "");
assert_eq!(
roster.members[1].name, "Bob",
"the second row still lines up"
);
assert_eq!(roster.members[1].public_note, "note B");
assert_eq!(roster.members[1].officer_note, "");
}
other => panic!("expected GuildRoster, got {}", other.name()),
}
}
/// The float's condition is the whole presence byte against zero, as the reference tests it
/// (`0x4d0c12`), never `presence & ONLINE`; `0x08`, a bit no 1.12 flag owns, still means online.
#[test]
fn roster_presence_is_tested_whole_byte_not_masked_against_online() {
let mut body = roster_head(2, "", "", &[guild_rank_right::ALL]);
push_member(
&mut body, 0x1111, 0x08, // an unnamed presence bit: online, so no float
"Alice", 0, 60, 2, 1519, None, "first", "",
);
push_member(
&mut body,
0x2222,
guild_presence::ONLINE,
"Bob",
1,
58,
4,
1497,
None,
"second",
"",
);
match &messages::parse_server(opcode::SMSG_GUILD_ROSTER, &body).unwrap() {
ServerPacket::GuildRoster(roster) => {
assert!(
roster.members[0].is_online(),
"an unnamed presence bit still means online"
);
assert_eq!(roster.members[0].last_online_days, 0.0);
assert_eq!(
roster.members[1].name, "Bob",
"a masked test would have desynchronised here"
);
assert_eq!(roster.members[1].public_note, "second");
}
other => panic!("expected GuildRoster, got {}", other.name()),
}
}
/// `SMSG_GUILD_EVENT`'s trailing guid rides on the event id: the reference (`0x5e7180`) reads it
/// only for 0x0c and 0x0d, though vmangos also writes one for `GE_JOINED` and `GE_LEFT`
/// (`Handlers/GuildHandler.cpp:218`, `:405`).
#[test]
fn guild_event_trailing_guid_rides_on_the_event_id() {
// GE_SIGNED_ON (0x0c): one param and the guid (Server/Packets/Guild.cpp:133-141).
let mut body = vec![guild_event::SIGNED_ON, 1];
body.extend_from_slice(b"Alice\0");
body.extend_from_slice(&0xDEAD_BEEFu64.to_le_bytes());
let packet = messages::parse_server(opcode::SMSG_GUILD_EVENT, &body).unwrap();
match &packet {
ServerPacket::GuildEvent(notice) => {
assert_eq!(notice.event, guild_event::SIGNED_ON);
assert_eq!(notice.params, vec!["Alice".to_string()]);
assert_eq!(notice.guid, Some(0xDEAD_BEEF));
}
other => panic!("expected GuildEvent, got {}", other.name()),
}
match decode(packet).as_slice() {
[SessionEvent::GuildEvent(notice)] => assert_eq!(notice.guid, Some(0xDEAD_BEEF)),
other => panic!("guild event decode: {other:?}"),
}
// GE_MOTD (0x02): one param, no guid.
let mut body = vec![guild_event::MOTD, 1];
body.extend_from_slice(b"Raid at 8\0");
let packet = messages::parse_server(opcode::SMSG_GUILD_EVENT, &body).unwrap();
match &packet {
ServerPacket::GuildEvent(notice) => {
assert_eq!(notice.event, guild_event::MOTD);
assert_eq!(notice.params, vec!["Raid at 8".to_string()]);
assert_eq!(notice.guid, None);
}
other => panic!("expected GuildEvent, got {}", other.name()),
}
// GE_JOINED (0x03): vmangos appends a guid here, which the reference never reads.
let mut body = vec![guild_event::JOINED, 1];
body.extend_from_slice(b"Carol\0");
body.extend_from_slice(&0x1234u64.to_le_bytes());
match &messages::parse_server(opcode::SMSG_GUILD_EVENT, &body).unwrap() {
ServerPacket::GuildEvent(notice) => {
assert_eq!(notice.event, guild_event::JOINED);
assert_eq!(notice.params, vec!["Carol".to_string()]);
assert_eq!(
notice.guid, None,
"the client reads the guid for 0x0c/0x0d only — this one is trailing slack"
);
}
other => panic!("expected GuildEvent, got {}", other.name()),
}
// GE_PROMOTION (0x00): the three-param maximum (promoter, promoted, new rank name).
let mut body = vec![guild_event::PROMOTION, 3];
body.extend_from_slice(b"Alice\0Bob\0Officer\0");
match &messages::parse_server(opcode::SMSG_GUILD_EVENT, &body).unwrap() {
ServerPacket::GuildEvent(notice) => {
assert_eq!(notice.params, vec!["Alice", "Bob", "Officer"]);
assert_eq!(notice.guid, None);
}
other => panic!("expected GuildEvent, got {}", other.name()),
}
// GE_DISBANDED (0x08): no params, no guid, the two-byte minimum body.
let packet = messages::parse_server(
opcode::SMSG_GUILD_EVENT,
&hx(concat!("08", "00")), // event, paramCount
)
.unwrap();
match &packet {
ServerPacket::GuildEvent(notice) => {
assert_eq!(notice.event, guild_event::DISBANDED);
assert!(notice.params.is_empty());
assert_eq!(notice.guid, None);
}
other => panic!("expected GuildEvent, got {}", other.name()),
}
}
/// `SMSG_GUILD_COMMAND_RESULT` (Server/Packets/Guild.cpp:96-101): `u32` command, a cstring in the
/// middle, `u32` result.
#[test]
fn guild_command_result_wire() {
let mut body = Vec::new();
body.extend_from_slice(&guild_command::INVITE.to_le_bytes());
body.extend_from_slice(b"Bob\0");
body.extend_from_slice(&guild_command_error::ALREADY_IN_GUILD_S.to_le_bytes());
let packet = messages::parse_server(opcode::SMSG_GUILD_COMMAND_RESULT, &body).unwrap();
match &packet {
ServerPacket::GuildCommandResult(result) => {
assert_eq!(result.command, guild_command::INVITE);
assert_eq!(result.name, "Bob");
assert_eq!(result.result, guild_command_error::ALREADY_IN_GUILD_S);
}
other => panic!("expected GuildCommandResult, got {}", other.name()),
}
match decode(packet).as_slice() {
[SessionEvent::GuildCommandResult(result)] => assert_eq!(result.name, "Bob"),
other => panic!("guild command result decode: {other:?}"),
}
// Result 0x08 is ERR_GUILD_LEADER_LEAVE under QUIT and ERR_GUILD_PERMISSIONS otherwise
// (vmangos Guild/Guild.h:106-107); the command tag tells them apart.
assert_eq!(
guild_command_error::LEADER_LEAVE,
guild_command_error::PERMISSIONS
);
let mut body = Vec::new();
body.extend_from_slice(&guild_command::QUIT.to_le_bytes());
body.extend_from_slice(b"\0");
body.extend_from_slice(&guild_command_error::LEADER_LEAVE.to_le_bytes());
match &messages::parse_server(opcode::SMSG_GUILD_COMMAND_RESULT, &body).unwrap() {
ServerPacket::GuildCommandResult(result) => {
assert_eq!(result.command, guild_command::QUIT);
assert_eq!(
result.name, "",
"the empty middle cstring is still consumed"
);
assert_eq!(result.result, 0x08);
}
other => panic!("expected GuildCommandResult, got {}", other.name()),
}
}
/// `SMSG_GUILD_INFO` (Server/Packets/Guild.cpp:103-111): the guild name, then day/month/year and
/// the member and account counts, five `u32`s in that order.
#[test]
fn guild_info_wire() {
let mut body = Vec::new();
body.extend_from_slice(b"Legacy of Steel\0");
body.extend_from_slice(&11u32.to_le_bytes()); // createdDay
body.extend_from_slice(&9u32.to_le_bytes()); // createdMonth
body.extend_from_slice(&2004u32.to_le_bytes()); // createdYear
body.extend_from_slice(&57u32.to_le_bytes()); // memberCount
body.extend_from_slice(&41u32.to_le_bytes()); // accountCount
let packet = messages::parse_server(opcode::SMSG_GUILD_INFO, &body).unwrap();
match &packet {
ServerPacket::GuildInfo(info) => {
assert_eq!(info.name, "Legacy of Steel");
assert_eq!(info.created_day, 11);
assert_eq!(info.created_month, 9);
assert_eq!(info.created_year, 2004);
assert_eq!(info.member_count, 57);
assert_eq!(
info.account_count, 41,
"accounts, not characters — the second count"
);
}
other => panic!("expected GuildInfo, got {}", other.name()),
}
match decode(packet).as_slice() {
[SessionEvent::GuildInfo(info)] => assert_eq!(info.account_count, 41),
other => panic!("guild info decode: {other:?}"),
}
}
/// `SMSG_GUILD_INVITE` (Server/Packets/Guild.cpp:85-89) is inviter then guild, two cstrings;
/// `SMSG_GUILD_DECLINE` (`:91-94`) is one cstring, the decliner.
#[test]
fn guild_invite_and_decline_wire() {
let packet = messages::parse_server(
opcode::SMSG_GUILD_INVITE,
&hx(concat!("416c69636500", "4c656761637900")),
)
.unwrap();
match &packet {
ServerPacket::GuildInvite { inviter, guild } => {
assert_eq!(inviter, "Alice", "the inviter comes first");
assert_eq!(guild, "Legacy");
}
other => panic!("expected GuildInvite, got {}", other.name()),
}
match decode(packet).as_slice() {
[SessionEvent::GuildInvite { inviter, guild }] => {
assert_eq!(inviter, "Alice");
assert_eq!(guild, "Legacy");
}
other => panic!("guild invite decode: {other:?}"),
}
let packet = messages::parse_server(opcode::SMSG_GUILD_DECLINE, &hx("426f6200")).unwrap();
match &packet {
ServerPacket::GuildDecline { name } => assert_eq!(name, "Bob"),
other => panic!("expected GuildDecline, got {}", other.name()),
}
match decode(packet).as_slice() {
[SessionEvent::GuildDecline { name }] => assert_eq!(name, "Bob"),
other => panic!("guild decline decode: {other:?}"),
}
}