diff --git a/crates/benilla-protocol/src/messages/guild.rs b/crates/benilla-protocol/src/messages/guild.rs index 3e054f19..0bbc5183 100644 --- a/crates/benilla-protocol/src/messages/guild.rs +++ b/crates/benilla-protocol/src/messages/guild.rs @@ -1,100 +1,49 @@ -//! The guild family — the roster, the guild query, invitations, rank administration and the event -//! broadcast (opcodes `0x54`/`0x55`, `0x81`–`0x93`, `0x231`–`0x235`, `0x2FC`). +//! The guild wire messages: roster, guild query, invitations, rank administration and the event +//! broadcast (opcodes `0x54`/`0x55`, `0x81`-`0x93`, `0x231`-`0x235`, `0x2FC`). //! -//! The server side is pinned at the bytes by vmangos `Server/Packets/Guild.{h,cpp}` (every -//! `ReadFromWorldPacket` / `AppendBodyTo`), `Guild/Guild.{h,cpp}` (`SendGuildRoster`, -//! `SendQueryResponse`, `BroadcastEvent`, and the enums) and `Handlers/GuildHandler.cpp` (which -//! `NullClientPacket` handlers take an empty body). -//! -//! The client side of `SMSG_GUILD_EVENT` is VERIFIED in wow-re -//! (`system/object-layer/scratch/rf77-smsg-chat-wire-order.md`, RF-0077 corrected): opcode `0x92` -//! registers handler `0x5e7180` (`0x5e337a mov ecx,0x92; call 0x5ab650`), which reads `u8 -//! eventType`, `u8 strCount` (≤ 3 — three stack buffers), `strCount` × CString, and then — **only** -//! in the `0xc`/`0xd` arms of its `switch` (jump table `0x5e74dc`) — a trailing `u64` guid via -//! `0x4190b0`. That is the read [`read_guild_event`] implements, and it is *narrower* than what -//! vmangos writes (see that function's doc). -//! -//! The client side of `SMSG_GUILD_ROSTER` and `SMSG_GUILD_QUERY_RESPONSE` is likewise VERIFIED in -//! wow-re (`system/ui/scratch/guild-roster-wire.md`), by a §5 trio deriving each independently. -//! The two do **not** live in the guild-init block the event handler does: `0x8a` is handler -//! `0x4d0ad0` (registered `0x4d0a0e`) in a **`GuildInfo.cpp`** TU, and `0x55` is `0x555290` -//! (registered `0x555073`) in **dbcache**, delegating to decoder `0x62f260` — which wow-re already -//! held bit-exact as `PRIMITIVE:dbcache_guild`. Per-field addresses are on the two readers below. -//! -//! **The parse trap in this family is `SMSG_GUILD_ROSTER`'s `f32 lastOnlineTime`**: it rides on the -//! member's presence byte, so reading it unconditionally desynchronises every *later* member — -//! exactly the shape [`super::social::read_friend_status`] already has, and written the same way -//! (an explicit condition on the field the wire keys off, never a "read if bytes remain"). -//! -//! **One reference behaviour we deliberately do not reproduce: capped string reads.** The client's -//! `0x4191b0` takes a per-field capacity (counting the NUL, so the real cap is `cap - 1`) — roster -//! name `0x30`, notes `0x80`, motd `0x200`, info text `0x7d0`, query guild name `0x60`, rank name -//! `0x40`. On overflow it does not truncate gracefully: it slams the cursor to `size + 1` -//! (`0x4192aa`) and NULs `dest[0]` (`0x4192be`), so the field arrives **empty** *and every later -//! read in that packet silently no-ops*. [`read_cstring`] is unbounded, which is strictly safer and -//! cannot poison a packet — and it costs no fidelity, because every one of those caps is far above -//! the content limit the server enforces (a 15-character rank name into a 64-byte buffer, a -//! 128-character MOTD into 512). The caps are recorded here because they explain a class of -//! "the guild pane went blank" behaviour in the reference, not because we want them. +//! Deviation: string reads are unbounded. The reference `0x4191b0` caps each field, counting the +//! NUL (roster name `0x30`, notes `0x80`, MOTD `0x200`, info `0x7d0`, guild name `0x60`, rank name +//! `0x40`), and on overflow empties it and no-ops every later read in the packet; every cap is far +//! above the server's own length limits, so a legitimate packet reads the same. use std::io::{self, Read}; use crate::wire::{capacity_hint, read_cstring, read_f32_le, read_u32_le, read_u64_le, read_u8}; -/// A guild always has at least this many ranks — vmangos `GUILD_RANKS_MIN_COUNT` (`Guild/Guild.h:31`); -/// the five defaults are GM / Officer / Veteran / Member / Initiate ([`guild_default_rank`]). +/// Minimum rank count: the five defaults in [`guild_default_rank`] (`Guild/Guild.h:31`). pub const GUILD_RANKS_MIN_COUNT: usize = 5; -/// …and at most this many — vmangos `GUILD_RANKS_MAX_COUNT` (`Guild/Guild.h:32`). Load-bearing for -/// the parse, not just a policy number: `SMSG_GUILD_QUERY_RESPONSE` always carries **exactly** -/// this many rank-name cstrings, padding unused ranks with empty strings -/// (`Guild/Guild.cpp:868-872`, whose own comment is "show always 10 ranks"). +/// Maximum rank count (`Guild/Guild.h:32`), and the exact number of rank names +/// `SMSG_GUILD_QUERY_RESPONSE` carries, unused ones empty (`Guild/Guild.cpp:868-872`). pub const GUILD_RANKS_MAX_COUNT: usize = 10; -/// Rank-name cap, in *characters* — vmangos `GUILD_RANK_MAX_LENGTH` (`Guild/Guild.h:36`). -/// -/// These five caps are the reference UI's edit-box `maxLetters`, and vmangos re-checks the three -/// it can (`utf8length`, not byte length). Over-long is **not** a polite refusal on the rank ones: -/// `HandleGuildRankOpcode` / `HandleGuildAddRankOpcode` (`Handlers/GuildHandler.cpp:580-584`, -/// `:600-604`) run `ProcessAnticheatAction(… CHEAT_ACTION_KICK)`. The builders below do **not** -/// truncate — silently shortening a rank name or a note would send the wrong text, which is its -/// own bug — so the caller enforces the cap at the edit box, as the reference UI does. +/// Rank-name cap in characters (`Guild/Guild.h:36`). The builders do not truncate, and an +/// over-long rank name gets the session kicked (`Handlers/GuildHandler.cpp:580-584`, `:600-604`). pub const GUILD_RANK_MAX_LENGTH: usize = 15; -/// Guild-name cap, in characters — vmangos `GUILD_NAME_MAX_LENGTH` (`Guild/Guild.h:37`). +/// Guild-name cap, in characters (`Guild/Guild.h:37`). pub const GUILD_NAME_MAX_LENGTH: usize = 24; -/// Public/officer note cap, in characters — vmangos `GUILD_NOTE_MAX_LENGTH` (`Guild/Guild.h:38`). +/// Public/officer note cap, in characters (`Guild/Guild.h:38`). pub const GUILD_NOTE_MAX_LENGTH: usize = 31; -/// Guild info text cap, in characters — vmangos `GUILD_INFO_MAX_LENGTH` (`Guild/Guild.h:39`). +/// Guild info text cap, in characters (`Guild/Guild.h:39`). pub const GUILD_INFO_MAX_LENGTH: usize = 500; -/// MOTD cap, in characters — vmangos `GUILD_MOTD_MAX_LENGTH` (`Guild/Guild.h:40`). +/// MOTD cap, in characters (`Guild/Guild.h:40`). pub const GUILD_MOTD_MAX_LENGTH: usize = 128; -/// The five ranks every guild is created with and cannot delete — vmangos `GuildDefaultRanks` -/// (`Guild/Guild.h:44-54`). Rank ids are **0-based with 0 = guild master**, and the ordering is the -/// authority hierarchy: promote does `rank--`, demote does `rank++` (vmangos's own comments). +/// The five ranks every guild is created with and cannot delete (`Guild/Guild.h:44-54`). Ids are +/// 0-based with 0 = guild master; promote does `rank--`, demote `rank++`. pub mod guild_default_rank { - /// `GR_GUILDMASTER` — rank 0. `HandleGuildRankOpcode` force-overwrites this rank's rights with - /// `GR_RIGHT_ALL` whatever the client sends, "to prevent loss of leader rights". + /// `GR_GUILDMASTER`; the server forces this rank's rights to `GR_RIGHT_ALL` whatever we send. pub const GUILDMASTER: u32 = 0; - /// `GR_OFFICER`. pub const OFFICER: u32 = 1; - /// `GR_VETERAN`. pub const VETERAN: u32 = 2; - /// `GR_MEMBER`. pub const MEMBER: u32 = 3; - /// `GR_INITIATE` — the lowest default rank. + /// `GR_INITIATE`, the lowest default rank. pub const INITIATE: u32 = 4; } -/// One rank's rights bitmask — vmangos `GuildRankRights` (`Guild/Guild.h:56-74`), **with -/// `GR_RIGHT_EMPTY` factored out**. -/// -/// vmangos writes every named right as `GR_RIGHT_EMPTY | bit` (`GR_RIGHT_GCHATLISTEN = 0x41`, -/// not `0x01`) because its test is `(rights & right) != GR_RIGHT_EMPTY` — the `0x40` is a sentinel -/// that makes that comparison work, not a right. The meaningful bit is the other half, and that is -/// what is named here; [`EMPTY`] is kept so the vmangos constants can be reconstructed and -/// recognised. +/// One rank's rights bitmask (`Guild/Guild.h:56-74`) with the `GR_RIGHT_EMPTY` bit factored out: +/// vmangos spells each right `0x40 | bit` for its test `(rights & right) != GR_RIGHT_EMPTY`. pub mod guild_rank_right { - /// `GR_RIGHT_EMPTY` (`0x40`) — the sentinel described in the module doc. Carries no permission. + /// `GR_RIGHT_EMPTY`, the sentinel vmangos ORs into every right; it grants nothing. pub const EMPTY: u32 = 0x0000_0040; /// Read guild chat (`GR_RIGHT_GCHATLISTEN`, vmangos `0x41`). pub const GCHAT_LISTEN: u32 = 0x0000_0001; @@ -116,27 +65,21 @@ pub mod guild_rank_right { pub const SET_MOTD: u32 = 0x0000_1000; /// Edit any member's public note (`GR_RIGHT_EPNOTE`, vmangos `0x2040`). pub const EDIT_PUBLIC_NOTE: u32 = 0x0000_2000; - /// **See** officer notes at all (`GR_RIGHT_VIEWOFFNOTE`, vmangos `0x4040`). Without it the - /// server blanks every `officerNote` in the roster it sends us - /// (`Guild/Guild.cpp:821`, `:844`) — the empty string is a *permission* signal, not "no note". + /// See officer notes (`GR_RIGHT_VIEWOFFNOTE`, vmangos `0x4040`); without it the server blanks + /// every roster `officerNote` (`Guild/Guild.cpp:821`, `:844`). pub const VIEW_OFFICER_NOTE: u32 = 0x0000_4000; /// Edit officer notes (`GR_RIGHT_EOFFNOTE`, vmangos `0x8040`). pub const EDIT_OFFICER_NOTE: u32 = 0x0000_8000; /// Edit the guild information text (`GR_RIGHT_MODIFY_GUILD_INFO`, vmangos `0x10040`). pub const MODIFY_GUILD_INFO: u32 = 0x0001_0000; - /// `GR_RIGHT_ALL` — what the server force-writes onto rank 0 regardless of what we send. - /// Unlike the named rights above, this is a **raw wire value, sentinel and all**: it includes - /// [`EMPTY`], and it lights bits (`0x0000_0E00`, `0x000E_0000`) that no 1.12 right owns. Test - /// a rank against the named bits, never against this. + /// `GR_RIGHT_ALL`, forced onto rank 0 by the server. A raw vmangos value: it includes + /// [`EMPTY`] and bits no 1.12 right owns, so test ranks against the named bits, never this. pub const ALL: u32 = 0x000F_F1FF; } -/// The thirteen rank-right checkboxes **in the order the FrameXML API indexes them**. -/// -/// `GuildControlGetRankFlags()` returns a flat list and `GuildControlSetRankFlag(i, on)` takes an -/// index into it, so **index order is the API contract** — and it is deliberately *not* bit order: -/// Promote/Demote (indices 5/6 here) sit above Invite/Remove (7/8) in bit value. Anything that -/// computes the bit as `1 << (i - 1)` is wrong; this table is the mapping. +/// The thirteen rank-right checkboxes in the order `GuildControlGetRankFlags()` and +/// `GuildControlSetRankFlag(i, on)` index them, which is not bit order: Promote/Demote (5/6) come +/// before Invite/Remove (7/8), whose bits are lower. pub const GUILD_RANK_RIGHT_ORDER: [u32; 13] = [ guild_rank_right::GCHAT_LISTEN, guild_rank_right::GCHAT_SPEAK, @@ -153,104 +96,81 @@ pub const GUILD_RANK_RIGHT_ORDER: [u32; 13] = [ guild_rank_right::MODIFY_GUILD_INFO, ]; -/// A roster member's presence byte — vmangos `GuildRosterPresenceFlags` (`Guild/Guild.h:139-144`). -/// -/// `0` (no bit at all) is the only value the wire treats specially: it is what makes the member's -/// `f32 lastOnlineTime` present. The AFK/DND bits ride alongside `ONLINE`, exactly as the friend -/// list's [`super::social::friend_status`] does. -/// -/// The reference client tests these as its own chat flags, and **DND is tested first** -/// (`0x4d13e4` before `0x4d13fb`, wow-re `system/ui/scratch/guild-roster-wire.md`) — so a member -/// flagged both AFK and DND displays as DND. That precedence is a display rule, not a wire rule, -/// but it belongs with the bits. +/// A roster member's presence byte (`Guild/Guild.h:139-144`); only `0` changes the wire, adding +/// the `f32 lastOnlineTime`. The reference tests DND before AFK (`0x4d13e4`, `0x4d13fb`), so a +/// member flagged both shows as DND. pub mod guild_presence { - /// Offline — and **only** this value carries the `lastOnlineTime` float. + /// Offline; only this value carries the `lastOnlineTime` float. pub const OFFLINE: u8 = 0x00; - /// `GRF_ONLINE`. Value INFERRED from vmangos: the reference client only ever tests the whole - /// byte against zero, so nothing in the binary pins this bit specifically. + /// `GRF_ONLINE` (vmangos value; the reference only tests the whole byte against zero). pub const ONLINE: u8 = 0x01; - /// `GRF_AFK` — set alongside [`ONLINE`]. VERIFIED as the client's own `CHAT_FLAG_AFK`. + /// `GRF_AFK`, set alongside [`ONLINE`]; the reference's own `CHAT_FLAG_AFK`. pub const AFK: u8 = 0x02; - /// `GRF_DND` — set alongside [`ONLINE`]. VERIFIED as the client's own `CHAT_FLAG_DND`, and - /// tested ahead of [`AFK`]. + /// `GRF_DND`, set alongside [`ONLINE`]; the reference's own `CHAT_FLAG_DND`. pub const DND: u8 = 0x04; } -/// `SMSG_GUILD_EVENT`'s leading byte — vmangos `GuildEvents` (`Guild/Guild.h:121-137`), which is -/// also the reference client's own `switch` (14 arms, jump table `0x5e74dc`; wow-re RF-0077). +/// `SMSG_GUILD_EVENT`'s leading byte (`Guild/Guild.h:121-137`), the arms of the reference +/// handler's `switch` (jump table `0x5e74dc`). pub mod guild_event { - /// `GE_PROMOTION` — params: promoter, promoted, new rank name. + /// `GE_PROMOTION`, params: promoter, promoted, new rank name. pub const PROMOTION: u8 = 0x00; - /// `GE_DEMOTION` — params: demoter, demoted, new rank name. + /// `GE_DEMOTION`, params: demoter, demoted, new rank name. pub const DEMOTION: u8 = 0x01; - /// `GE_MOTD` — param: the new message of the day. The reference client routes this one - /// through the profanity filter (`0x703f50(0x11c,…)`, CVar-gated), not the normal event path. + /// `GE_MOTD`, param: the new MOTD. The reference routes it through the CVar-gated profanity + /// filter (`0x703f50`), not the normal event path. pub const MOTD: u8 = 0x02; - /// `GE_JOINED` — param: the joiner's name. + /// `GE_JOINED`, param: the joiner's name. pub const JOINED: u8 = 0x03; - /// `GE_LEFT` — param: the leaver's name. + /// `GE_LEFT`, param: the leaver's name. pub const LEFT: u8 = 0x04; - /// `GE_REMOVED` — params: the removed player, then who removed them. + /// `GE_REMOVED`, params: the removed player, then who removed them. pub const REMOVED: u8 = 0x05; - /// `GE_LEADER_IS` — param: the current leader. + /// `GE_LEADER_IS`, param: the current leader. pub const LEADER_IS: u8 = 0x06; - /// `GE_LEADER_CHANGED` — params: old leader, new leader. + /// `GE_LEADER_CHANGED`, params: old leader, new leader. pub const LEADER_CHANGED: u8 = 0x07; - /// `GE_DISBANDED` — no params. + /// `GE_DISBANDED`, no params. pub const DISBANDED: u8 = 0x08; - /// `GE_TABARDCHANGE`. The reference client falls through to its default arm for this one. + /// `GE_TABARDCHANGE`; the reference handles it in its default arm. pub const TABARD_CHANGE: u8 = 0x09; - /// `GE_UPDATE_RANK_NAME` — params: rank id, new rank name. The reference client writes it - /// straight into the roster cache (`0x560e30`) and fires **no** FrameScript event. + /// `GE_UPDATE_RANK_NAME`, params: rank id, new rank name. The reference writes it into the + /// roster cache (`0x560e30`) and fires no FrameScript event. pub const UPDATE_RANK_NAME: u8 = 0x0A; - /// `GE_UPDATE_ROSTER` — "re-ask for the roster". Silent in the reference client; vmangos never - /// sends it (it re-sends the whole `SMSG_GUILD_ROSTER` instead). + /// `GE_UPDATE_ROSTER`: silent in the reference, and vmangos never sends it. pub const UPDATE_ROSTER: u8 = 0x0B; - /// `GE_SIGNED_ON` — param: the guildmate's name. **Carries the trailing guid.** + /// `GE_SIGNED_ON`, param: the guildmate's name; carries the trailing guid. pub const SIGNED_ON: u8 = 0x0C; - /// `GE_SIGNED_OFF` — param: the guildmate's name. **Carries the trailing guid.** + /// `GE_SIGNED_OFF`, param: the guildmate's name; carries the trailing guid. pub const SIGNED_OFF: u8 = 0x0D; } -/// `SMSG_GUILD_COMMAND_RESULT`'s leading `u32` — which *verb* the result is about (vmangos -/// `Typecommand`, `Guild/Guild.h:76-85`). -/// -/// It is a much coarser tag than the opcode that provoked it: vmangos answers "you're not in a -/// guild" to `CMSG_GUILD_INFO`, `_ROSTER`, `_LEAVE`, `_DISBAND` and half a dozen others all as -/// [`CREATE`], and every permission refusal as [`INVITE`]. So the pair -/// `(command, result)` names the *message*, not the request — vmangos's own comment notes that -/// values 2, 4-13 and 15-18 "have no effect" here. +/// `SMSG_GUILD_COMMAND_RESULT`'s leading `u32`, the verb the result is about +/// (`Guild/Guild.h:76-85`). Coarser than the request: vmangos reports "not in a guild" for most +/// verbs as [`CREATE`] and every permission refusal as [`INVITE`]. pub mod guild_command { - /// `GUILD_CREATE_S` — also vmangos's catch-all for "you are not in a guild". + /// `GUILD_CREATE_S`, also vmangos's catch-all for "you are not in a guild". pub const CREATE: u32 = 0x00; - /// `GUILD_INVITE_S` — also vmangos's catch-all for a permission refusal. + /// `GUILD_INVITE_S`, also vmangos's catch-all for a permission refusal. pub const INVITE: u32 = 0x01; - /// `GUILD_QUIT_S` — leaving/being removed. + /// `GUILD_QUIT_S`: leaving or being removed. pub const QUIT: u32 = 0x03; - /// `GUILD_FOUNDER_S`. pub const FOUNDER: u32 = 0x0E; - /// `GUILD_UNK19`. pub const UNK19: u32 = 0x13; - /// `GUILD_UNK20`. pub const UNK20: u32 = 0x14; } -/// `SMSG_GUILD_COMMAND_RESULT`'s trailing `u32` — vmangos `CommandErrors` -/// (`Guild/Guild.h:96-119`). The string field between the two carries whatever `%s` the matching -/// `ERR_GUILD_*` GlobalString needs (a player name, a guild name, or empty). -/// -/// Note [`LEADER_LEAVE`] and [`PERMISSIONS`] are the **same** value `0x08`: which one it means is -/// decided by the [`guild_command`] tag beside it (`QUIT` → the leader-can't-leave message, -/// anything else → the permission refusal). That collision is the reason the command tag has to be -/// carried through to the consumer rather than dropped. +/// `SMSG_GUILD_COMMAND_RESULT`'s trailing `u32` (`Guild/Guild.h:96-119`). [`LEADER_LEAVE`] and +/// [`PERMISSIONS`] share `0x08`: with command `QUIT` it is the leader-cannot-leave message, +/// otherwise the permission refusal. pub mod guild_command_error { - /// `ERR_PLAYER_NO_MORE_IN_GUILD` — vmangos's own comment: "no message/error". + /// `ERR_PLAYER_NO_MORE_IN_GUILD`: "no message/error" per vmangos. pub const PLAYER_NO_MORE_IN_GUILD: u32 = 0x00; /// `ERR_GUILD_INTERNAL`. pub const INTERNAL: u32 = 0x01; - /// `ERR_ALREADY_IN_GUILD` — *you* are. + /// `ERR_ALREADY_IN_GUILD`: you are. pub const ALREADY_IN_GUILD: u32 = 0x02; - /// `ERR_ALREADY_IN_GUILD_S` — *they* are (the string names them). + /// `ERR_ALREADY_IN_GUILD_S`: they are (the string names them). pub const ALREADY_IN_GUILD_S: u32 = 0x03; /// `ERR_INVITED_TO_GUILD`. pub const INVITED_TO_GUILD: u32 = 0x04; @@ -260,18 +180,17 @@ pub mod guild_command_error { pub const NAME_INVALID: u32 = 0x06; /// `ERR_GUILD_NAME_EXISTS_S`. pub const NAME_EXISTS_S: u32 = 0x07; - /// `ERR_GUILD_LEADER_LEAVE` — the guild master cannot `/gquit` while anyone else remains. - /// Same value as [`PERMISSIONS`]; disambiguated by [`guild_command::QUIT`]. + /// `ERR_GUILD_LEADER_LEAVE`: the guild master cannot `/gquit` while anyone else remains. pub const LEADER_LEAVE: u32 = 0x08; - /// `ERR_GUILD_PERMISSIONS` — see [`LEADER_LEAVE`]. + /// `ERR_GUILD_PERMISSIONS`, sharing `0x08` with [`LEADER_LEAVE`]. pub const PERMISSIONS: u32 = 0x08; - /// `ERR_GUILD_PLAYER_NOT_IN_GUILD` — *you* aren't. + /// `ERR_GUILD_PLAYER_NOT_IN_GUILD`: you aren't. pub const PLAYER_NOT_IN_GUILD: u32 = 0x09; - /// `ERR_GUILD_PLAYER_NOT_IN_GUILD_S` — *they* aren't. + /// `ERR_GUILD_PLAYER_NOT_IN_GUILD_S`: they aren't. pub const PLAYER_NOT_IN_GUILD_S: u32 = 0x0A; /// `ERR_GUILD_PLAYER_NOT_FOUND_S`. pub const PLAYER_NOT_FOUND_S: u32 = 0x0B; - /// `ERR_GUILD_NOT_ALLIED` — cross-faction, refused by config. + /// `ERR_GUILD_NOT_ALLIED`: cross-faction, refused by config. pub const NOT_ALLIED: u32 = 0x0C; /// `ERR_GUILD_RANK_TOO_HIGH_S`. pub const RANK_TOO_HIGH_S: u32 = 0x0D; @@ -279,46 +198,30 @@ pub mod guild_command_error { pub const RANK_TOO_LOW_S: u32 = 0x0E; /// `ERR_GUILD_RANKS_LOCKED` (`0x0F`/`0x10` are unused at 5875). pub const RANKS_LOCKED: u32 = 0x11; - /// `ERR_GUILD_RANK_IN_USE` — can't delete the lowest rank while somebody holds it. + /// `ERR_GUILD_RANK_IN_USE`: the lowest rank cannot be deleted while somebody holds it. pub const RANK_IN_USE: u32 = 0x12; /// `ERR_GUILD_IGNORING_YOU_S`. pub const IGNORING_YOU_S: u32 = 0x13; - /// `ERR_GUILD_UNK20` — vmangos: "for Typecommand 0x05 only". + /// `ERR_GUILD_UNK20`: "for Typecommand 0x05 only" per vmangos. pub const UNK20: u32 = 0x14; } -/// `SMSG_GUILD_QUERY_RESPONSE` — one guild's public identity: its name, its ten rank names, and -/// its tabard. -/// -/// This is an ask-once cache fill, the guild twin of the name query: the roster and every chat -/// line reference a guild by id, and this is what turns that id into text. +/// `SMSG_GUILD_QUERY_RESPONSE`: one guild's name, ten rank names and tabard, by guild id. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct GuildQueryResponse { /// The guild id we asked about. pub guild_id: u32, - /// The guild's name — and, **empty, the "no such guild" answer**. The reference client tests - /// exactly this (`0x5552ae test al,al` on the decoded record's name, choosing the cache - /// *insert* `0x561070` or the cache *remove* `0x561390`); there is no separate not-found flag, - /// and the full record is consumed either way. A consumer must therefore treat an empty name - /// as "this id names nothing", not as "a guild with a blank name". + /// The guild's name; empty means no such guild, and the reference then drops the id from its + /// cache instead of inserting it (`0x5552ae`). pub name: String, - /// **Always exactly [`GUILD_RANKS_MAX_COUNT`] entries**, indexed by rank id (0 = guild - /// master); ranks the guild has not created are empty strings, not absent. A guild has between - /// [`GUILD_RANKS_MIN_COUNT`] and [`GUILD_RANKS_MAX_COUNT`] real ranks, so the tail of this - /// array is normally empty — "how many ranks exist" is the count of leading non-empty entries, - /// or better, the roster's `rank_rights` length. + /// Indexed by rank id, uncreated ranks empty; the live rank count is the roster's + /// `rank_rights` length. pub rank_names: [String; GUILD_RANKS_MAX_COUNT], - /// Tabard emblem symbol index — the file index the body composite formats into - /// `Textures\GuildEmblems\Emblem_