Files
benilla-octo/crates/benilla-protocol/src/messages/packet.rs
T
SamandClaude Opus 5 8d3d805ec8 the guild window opens: roster, ranks, notes and the message of the day
- the social window's third tab is live — the roster in both its views, the member card with its promote/demote/remove verbs, the rank editor's thirteen permissions, the guild information board, and the invite, leave, kick and note dialogs behind them
- setting your hearthstone at an innkeeper now asks before it takes and says so afterwards, and the innkeeper's gossip lines get their own icons back
- unavailable talents grey out, and every greyed icon in the client now goes through the same luminance fold the original used
- the buff tooltip's remaining-time line reads gold with the client's own hour/minute ladder, the macro editor takes a shift-click to paste a spell, trainer rows repaint after a purchase, and a long book page stops flickering its ellipsis

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 17:38:54 +07:00

1380 lines
73 KiB
Rust

//! The decoded [`ServerPacket`] — one variant per server opcode benilla models, with the wire
//! provenance on each, plus the logging [`ServerPacket::name`]. The opcode→variant dispatch lives
//! in the sibling `parse` module; the per-domain payload readers in the domain children.
use crate::wire::Vector3d;
use super::{
ActionButton, AttackerState, CastOutcome, ChannelNotify, Character, ChatMessage,
CorpseLocation, DamageShield, EnvironmentalDamageLog, ExplorationXp, FriendEntry,
FriendStatusUpdate, GameObjectQueryInfo, GossipOption, GroupLootInfo, GroupMemberEntry,
GuildCommandResult, GuildEventNotice, GuildInfo, GuildQueryResponse, GuildRoster,
InitWorldStates, ItemInfo, ItemPushResult, JumpInfo, LevelUpInfo, LootAllPassed, LootItem,
LootRoll, LootRollWon, LootStartRoll, MailListEntry, MirrorTimerStart, MoveMode, Object,
PartyMemberStatsInfo, PeriodicAuraLog, PetMode, PetSpells, QuestComplete, QuestDetails,
QuestGiverList, QuestOfferReward, QuestOption, QuestRequestItems, QuestTemplate,
ResurrectRequestBody, SpeedKind, SpellChainTargets, SpellCooldown, SpellDamageLog,
SpellEnergizeLog, SpellGo, SpellHealLog, SpellLogMiss, SpellStart, TaxiMask, TradeStatus,
TradeStatusExtended, TrainerSpell, TransportPose, VendorItem, WhoResults, XpGain,
};
/// The **final facing** a `SMSG_MONSTER_MOVE` dictates (its `moveType`): the unit snaps to face this
/// when the move is applied — the real client stores it straight into the unit's movement facing, a
/// hard snap, not a smooth turn (wow-re object-layer: `0x6018f0` → `0x7c6f30 mov [esi+0x1c],eax`).
/// This is how a creature re-orients without walking (a scripted/emote/aggro face), and benilla used
/// to **discard** it (the "mob won't turn to face me" gap). `Angle` is a raw WoW orientation; `Spot`
/// / `Target` resolve to an angle in the app (atan2 from the unit to the point, or to the target
/// unit's live position — the client snapshots the target's position at receipt, it doesn't track).
#[derive(Debug, Clone, Copy)]
pub enum MonsterMoveFacing {
/// A plain move (`moveType` 0) or a stop — no dictated facing; the unit faces its travel direction.
None,
/// `moveType 2` — face a world point (raw WoW coords).
Spot([f32; 3]),
/// `moveType 3` — face a unit by guid (resolved to its position when applied).
Target(u64),
/// `moveType 4` — face a raw orientation (radians, WoW convention).
Angle(f32),
}
/// One creature template's UI-visible head (see [`ServerPacket::CreatureQueryResponse`]).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreatureQueryInfo {
pub name: String,
/// The NPC subtitle ("Stable Master") — the unit tooltip's second line.
pub subname: String,
/// `CreatureType.dbc` id (Beast/Humanoid/… — the level line's type word + the TAB filter).
pub creature_type: u32,
/// `CreatureFamily.dbc` id (Wolf/Cat/Imp/… — `UnitCreatureFamily`'s row, and through that row's
/// pet-food mask, `GetPetFoodTypes`'s diet list; decision 1062). `0` for anything that is not a
/// tameable beast or a warlock minion — which is most of the table, and the reason both
/// consumers treat a missing row as nil rather than as an error.
pub pet_family: u32,
/// Elite rank: 0 normal, 1 elite, 2 rare-elite, 3 world boss, 4 rare — the unit tooltip's
/// rank word `{"", Elite, Elite, Boss, ""}` (decision 0276's byte-verified table).
pub rank: u32,
/// The template type flags. Bit `0x10` (HIDE_FACTION_TOOLTIP) suppresses the unit tooltip's
/// faction-name line (the client's `0x612610` gate on the cached record's `+0x14`).
pub type_flags: u32,
/// The civilian flag (the tooltip's green CIVILIAN line; dishonorable-kill marking).
pub civilian: bool,
/// The racial-leader flag (the tooltip's white LEADER line; `0x6125c0` on the record's `+0x31`).
pub racial_leader: bool,
}
/// A decoded server packet (only the opcodes benilla handles; everything else is [`Self::Other`]).
pub enum ServerPacket {
AuthChallenge {
server_seed: u32,
},
AuthResponse {
result: u8,
},
CharEnum {
characters: Vec<Character>,
},
CharCreate {
result: u8,
},
CharDelete {
result: u8,
},
UpdateObject {
objects: Vec<Object>,
},
/// `SMSG_COMPRESSED_MOVES` — a **batch** of whole movement packets in one zlib envelope, each
/// already parsed into its own [`ServerPacket`] (decision 0624).
///
/// vmangos flips a session onto this container once it has been sent
/// `Compression.Movement.Count` (300) movement packets inside a ten-second window
/// (`WorldSession::SendMovementPacket`), and flips back when the rate drops — so it is not an
/// exotic case but the **normal** carrier for any nearby player moving at frame cadence, which
/// since decision 0617 is what our own sender produces. Body: `u32` uncompressed size, then a
/// deflate stream of `[u8 size][u16 opcode][body]` records where `size` counts the opcode's two
/// bytes (`MovementData::AddPacket`).
CompressedMoves {
packets: Vec<ServerPacket>,
},
/// `SMSG_DESTROY_OBJECT` — one object ceased to exist server-side (a corpse decaying ahead of
/// respawn, a despawn), body a plain `u64` guid (vmangos `DestroyObject::AppendBodyTo`). Distinct
/// from the update-object `OutOfRange` block (*we* left its range; the real client keeps those
/// staged for cheap re-create): destroy frees the object outright (handler `0xaa → 0x4674a0`).
DestroyObject {
guid: u64,
},
/// `SMSG_TRIGGER_CINEMATIC` — play the cinematic sequence (body: the `CinematicSequences.dbc`
/// id, `u32`). Sent on a character's first-ever login (the race intro,
/// `HandleCharEnum`'s played-time-0 branch) and by GameObject type-13 cameras. While one runs
/// unacked, vmangos anchors visibility to the flying camera (see [`super::opcode`]'s note) — the
/// client must answer with `CMSG_COMPLETE_CINEMATIC` (empty body) when done (or to skip).
TriggerCinematic {
cinematic_id: u32,
},
MonsterMove {
guid: u64,
start: Vector3d,
/// The server's per-move spline counter — echoed back in `CMSG_MOVE_SPLINE_DONE` when the
/// spline drives our own player (Charge/knockback/taxi); ignored for a creature's walk.
spline_id: u32,
/// The **full** ordered path the unit traverses, in travel order `[start, …waypoints…, endpoint]`
/// (absolute WoW coords) — every intermediate waypoint decoded, not just the endpoint. The client
/// walks all of them at constant (arc-length) speed; a straight hop is the two-point degenerate.
/// Empty for a `Stop`/degenerate move. (Ground paths pack the waypoints as `¼`-yd offsets from the
/// endpoint; a flying `Mask_CatmullRom` path sends them absolute — decoded in `monster_move`.)
path: Vec<Vector3d>,
/// The dictated final facing (`moveType` 2/3/4), applied as a snap. [`MonsterMoveFacing::None`]
/// for a plain move — the unit then faces its travel direction along the path.
facing: MonsterMoveFacing,
stop: bool,
duration_ms: u32,
/// The path is a 3-D **flight** (`SPLINE_FLAG_FLYING` = `Mask_CatmullRom`): keep the spline's own Z.
/// Clear ⇒ a ground walk whose Z the client re-derives from the terrain (the app terrain-clamps it).
flying: bool,
},
/// A relayed *player* movement packet (`MSG_MOVE_*`, see [`super::parse::is_movement_relay`]): the mover's
/// authoritative pose plus its live CMovement `moveFlags`. The app snaps the entity to `position`
/// and extrapolates from `flags` between packets. `opcode` is kept for logging/diagnostics.
/// `fall_time` + `jump` carry the airborne clock + ballistic launch params when `JUMPING` is set, so
/// a jump replays as one arc (decision 0053). `transport` is `Some` while `MOVEFLAG_ON_TRANSPORT` is
/// set: the mover's local pose on the named transport (decision 0438 — an observed rider's boarding
/// tail, composed through the transport's live matrix rather than read as world coordinates).
PlayerMove {
guid: u64,
opcode: u16,
flags: u32,
position: Vector3d,
orientation: f32,
/// The swim pitch (radians, +up) from the `MovementInfo` tail — the mover's 3D travel
/// angle while `MOVEFLAG_SWIMMING` is set (`0.0` otherwise), so observers integrate the
/// vertical between packets the way the client's swim velocity basis does (`0x7c5880`).
pitch: f32,
/// The `MovementInfo` time word. vmangos stamps it with **its own ms clock at receipt**
/// (`MovementInfo::stime`, `Object.cpp` `Write`), so every relayed move shares one server
/// clock — the scheduling basis for the reference's deferred replay (decision 0601).
time: u32,
fall_time: u32,
jump: Option<JumpInfo>,
transport: Option<TransportPose>,
},
Teleport {
guid: u64,
counter: u32,
position: Vector3d,
orientation: f32,
},
NewWorld {
map: u32,
position: Vector3d,
orientation: f32,
},
/// The far-teleport preamble (`SMSG_TRANSFER_PENDING`): the destination map and, when the
/// player is riding a transport through the transfer, `(transportEntry, oldMapId)`. The
/// block's presence decides how the follow-up [`Self::NewWorld`] coordinates read —
/// boat-local vs world (decision 0455).
TransferPending {
map: u32,
transport: Option<(u32, u32)>,
},
/// `SMSG_TRANSFER_ABORTED`: the announced transfer will not happen (map full, no instance).
TransferAborted {
reason: u8,
},
LoginVerifyWorld {
map: u32,
position: Vector3d,
orientation: f32,
},
TimeSpeed {
hours: u8,
minutes: u8,
/// Monotonic day count flattened from the packed date (`year·372 + month·31 + day`) —
/// the celestial layer's moon-phase precession input.
day_serial: u32,
timescale: f32,
},
/// `SMSG_QUERY_TIME_RESPONSE`: the server's wall clock in **unix-epoch seconds**, answering our
/// `CMSG_QUERY_TIME`. Not the in-game clock — that is [`Self::TimeSpeed`]'s packed date, a
/// different quantity entirely. This one exists so the client can read the *absolute* stamps
/// the server writes into descriptor fields: today the quest-log slot's timed-quest deadline
/// (decision 1150).
QueryTimeResponse {
unix_time: u32,
},
/// The player's hearthstone bind point (`SMSG_BINDPOINTUPDATE`, at login + on re-bind):
/// position + map + the AreaTable id the `$z` token names.
BindPoint {
position: Vector3d,
map: u32,
area: u32,
},
/// `SMSG_BINDER_CONFIRM` — an innkeeper is *asking* whether to make this your home
/// (decision 1331). The body is the innkeeper's guid, which must come back in
/// `CMSG_BINDER_ACTIVATE` for anything to happen: this packet binds nothing on its own.
BinderConfirm {
binder: u64,
},
/// `SMSG_PLAYERBOUND` — the bind took: who bound us and the AreaTable id we are now bound in.
/// Arrives beside [`Self::BindPoint`], which carries the same area id plus the position; this
/// one exists for the "X is now your home" acknowledgement.
PlayerBound {
binder: u64,
area: u32,
},
/// `SMSG_SET_PROFICIENCY` (at login + on train): the subclass bitmask the player can equip
/// for one item class (2 weapons / 4 armor) — the client's `0xc4d4a0[class]` store, the item
/// tooltip's slot-line proficiency red (vmangos `Skill.h`: u8 itemClass + u32 mask).
SetProficiency {
item_class: u8,
subclass_mask: u32,
},
/// `SMSG_INITIALIZE_FACTIONS` — the player's reputation store, sent once at login: `(flags,
/// standing)` per reputation-list slot (vanilla sends all 64), indexed by `Faction.dbc`'s
/// `reputationIndex`. The wire standing **excludes** the DBC base value — the client adds the
/// race/class-matched `BaseRepValue` before ranking (hated → exalted).
InitializeFactions {
standings: Vec<(u8, i32)>,
},
/// `SMSG_SET_FACTION_STANDING` — mid-session standing deltas: `(reputationListId, standing)`
/// per changed slot (vmangos `ReputationMgr::SendState` batches every `needSend` faction).
/// Same standing convention as [`Self::InitializeFactions`] (DBC base excluded).
SetFactionStanding {
standings: Vec<(u32, i32)>,
},
/// `SMSG_SET_FACTION_VISIBLE` — one reputation-list slot just became visible in the pane
/// (vmangos `ReputationMgr::SendVisible`, pushed the first time the player meets the faction).
/// It carries **no standing**: the slot's existing standing is already correct, and this only
/// lifts `FACTION_FLAG_VISIBLE` on it.
SetFactionVisible {
list_id: u32,
},
/// `SMSG_NAME_QUERY_RESPONSE` — a player character's identity, answering `CMSG_NAME_QUERY`:
/// guid, name, realm (1.12.1 carries it, empty on a single realm), race/gender/class as `u32`s
/// (VERIFIED vmangos `NameQueryResponse::AppendBodyTo`). An unknown guid answers with an empty
/// name (`SendNameQueryOpcodeFromDBCallBack` with no DB row).
NameQueryResponse {
guid: u64,
name: String,
race: u32,
gender: u32,
class: u32,
},
/// `SMSG_CREATURE_QUERY_RESPONSE` — a creature template's display data, answering
/// `CMSG_CREATURE_QUERY`. We surface what the UI consumes ([`CreatureQueryInfo`]: name,
/// subname, `CreatureType.dbc` id, the `CreatureFamily.dbc` id, the elite **rank**, the type
/// flags, and the civilian/racial-leader pair); only `unk`, the pet spell-list id and the
/// display id are still parsed for alignment and dropped. A **miss** (unknown entry) is a lone `u32` of
/// `entry | 0x8000_0000` → `None` (VERIFIED vmangos `HandleCreatureQueryOpcode`, both
/// branches).
CreatureQueryResponse {
entry: u32,
/// `Some` on a hit; `None` when the server flagged the entry unknown.
info: Option<CreatureQueryInfo>,
},
/// `SMSG_PET_NAME_QUERY_RESPONSE` — a pet's display name, answering `CMSG_PET_NAME_QUERY`.
/// Keyed by **pet number**, not a template entry (a pet guid carries none — see
/// [`crate::guid::pet_number`]); that is also how the real client's own pet-name cache is keyed.
/// The `nameTimestamp` tail is parsed for alignment and dropped: it exists to age out that
/// on-disk cache, which we have no equivalent of.
PetNameQueryResponse {
pet_number: u32,
name: String,
},
/// `SMSG_GAMEOBJECT_QUERY_RESPONSE` — a GameObject template's type/display/name/`data[24]` head,
/// answering `CMSG_GAMEOBJECT_QUERY` (decision 0236's ask-once template lookup, the GO twin of
/// [`Self::CreatureQueryResponse`]). `data[24]` is the raw type-specific tail (e.g. a chest's
/// lockId) — parsed verbatim, resolved by whichever later consumer knows the GO's type. A miss
/// (unknown entry) is a lone `u32` of `entry | 0x8000_0000` → `None`, same shape as the
/// creature/item miss.
GameObjectQueryResponse {
entry: u32,
info: Option<GameObjectQueryInfo>,
},
/// `SMSG_PAGE_TEXT_QUERY_RESPONSE` — one page of a book, answering `CMSG_PAGE_TEXT_QUERY`
/// (the ask-once page cache every readable reaches: a readable item template's `PageText` and a
/// `GAMEOBJECT_TYPE_TEXT` object's `data[0]`, decision 1105). Pages **chain** —
/// `next_page_id == 0` is the last one — and vmangos answers a single query with one of these
/// per page of the whole chain.
PageTextQueryResponse {
page_id: u32,
text: String,
next_page_id: u32,
},
/// `SMSG_GAMEOBJECT_CUSTOM_ANIM` — a GameObject plays a one-shot Custom animation. Payload
/// VERIFIED vmangos `GameObject::SendGameObjectCustomAnim`: `u64 guid, u32 animId`. The
/// client arms GO substate `8 + animId` (AnimationData 153..156, `animId >= 4` rejected) —
/// the fishing bobber's bite splash is `animId 0` (decision 1086).
GameObjectCustomAnim {
guid: u64,
anim_id: u32,
},
/// `SMSG_FISH_NOT_HOOKED` — the fishing channel ended (expiry, or clicked before the splash)
/// with nothing hooked. Empty body (VERIFIED vmangos `GameObject::Update`/`Use`); the red
/// `ERR_FISH_NOT_HOOKED` toast (decision 1086).
FishNotHooked,
/// `SMSG_FISH_ESCAPED` — the hooked fish got away (the skill roll failed on the click).
/// Empty body (VERIFIED vmangos `GameObject::Use`); the red `ERR_FISH_ESCAPED` toast
/// (decision 1086).
FishEscaped,
/// `SMSG_PLAY_SOUND` — a 2D sound-kit id, map/zone-wide (BG events, scripts). Payload
/// VERIFIED vmangos `Map::PlayDirectSoundToMap`: one `u32 soundId` (SoundEntries).
PlaySound {
sound_id: u32,
},
/// `SMSG_PLAY_MUSIC` — a music kit for the music channel (scripts, e.g. Karazhan opera).
/// Payload VERIFIED vmangos: one `u32 musicId` (SoundEntries).
PlayMusic {
music_id: u32,
},
/// `SMSG_PLAY_OBJECT_SOUND` — a 3D kit at a source object (fishing-bobber splash 3355,
/// distance-dependent scripts). Payload VERIFIED vmangos `WorldObject::PlayDistanceSound`:
/// `u32 soundId, u64 sourceGuid`.
PlayObjectSound {
sound_id: u32,
guid: u64,
},
/// `SMSG_WEATHER` — the zone's weather state. Payload VERIFIED vmangos
/// `Weather::SendWeatherForPlayersInZone` (1.12 shape): `u32 type, f32 grade,
/// u32 soundId (a SoundEntries kit — 8533..8558, the rain/snow/sandstorm loops; 0 = clear),
/// u8 instant`. `type`/`grade`/`instant` also feed the weather *visuals* when they exist.
Weather {
weather_type: u32,
grade: f32,
sound_id: u32,
instant: bool,
},
/// `SMSG_TEXT_EMOTE` — a nearby unit performed a chat emote (`/wave`). Payload VERIFIED
/// vmangos `EmoteChatBuilder`: `u64 guid, u32 textEmote (EmotesText.dbc), u32 emoteNum,
/// u32 namelen, char name[namelen]` (the target's name; namelen ≥ 1, a lone NUL when there was
/// no target).
///
/// **`target_name` is load-bearing and used to be dropped here** (decision 1274 / B156): it is
/// what selects the emote's sentence FORM — untargeted vs targeted vs "…at you" — in
/// `benilla_formats::EmoteTextCatalog`, and it is the raw wire string, never round-tripped
/// through a name cache, exactly as the reference passes it. `emoteNum` really is
/// dropped: the receive side reads it off the wire and never consumes it (wow-re
/// `ui/scratch/text-emote-composition.md` §3).
TextEmote {
guid: u64,
text_emote: u32,
target_name: String,
},
/// `SMSG_EMOTE` — a unit plays an anim emote. Payload VERIFIED vmangos
/// `Unit::HandleEmote`: `u32 emoteId (Emotes.dbc), u64 guid`.
Emote {
guid: u64,
emote_id: u32,
},
/// `SMSG_ITEM_QUERY_SINGLE_RESPONSE` — an item template's display head, answering
/// `CMSG_ITEM_QUERY_SINGLE` (layout in [`super::items`]); `None` info = the miss shape. `ItemInfo` now
/// carries the full 1.12.1 item template (decision 0274 P1) — boxed so this one wide-but-rare
/// variant doesn't inflate every other (tiny, hot) `ServerPacket` (`clippy::large_enum_variant`).
ItemQueryResponse {
entry: u32,
info: Option<Box<ItemInfo>>,
},
/// `SMSG_MESSAGECHAT` — one inbound chat line (say/yell/system/NPC/channel; layout in
/// [`super::chat`]). System lines (type `0x0A`) are how GM dot-commands answer.
MessageChat(ChatMessage),
/// `SMSG_CHANNEL_NOTIFY` — a channel join/leave/error/moderation notice (layout in
/// [`super::channel::read_channel_notify`], decision 0288).
ChannelNotify(ChannelNotify),
/// `SMSG_CHANNEL_LIST` — a channel's member roster, answering our `CMSG_CHANNEL_LIST` (layout in
/// [`super::channel::read_channel_list`]): `(guid, memberFlags)` per row.
ChannelList {
channel: String,
flags: u8,
members: Vec<(u64, u8)>,
},
/// `SMSG_CHAT_PLAYER_NOT_FOUND` — a whisper target wasn't found online (vmangos
/// `Server/Packets/Chat.cpp:26-29`).
ChatPlayerNotFound {
name: String,
},
/// `SMSG_CHAT_WRONG_FACTION` — a cross-faction whisper was refused; empty body (vmangos
/// `Server/Packets/Chat.cpp:16-18`).
ChatWrongFaction,
/// `SMSG_NOTIFICATION` — a server notice ("You do not know that language", …); one cstring
/// (vmangos `WorldSession::SendNotification`, `Server/WorldSession.cpp:900-915`).
Notification {
text: String,
},
/// `SMSG_AREA_TRIGGER_MESSAGE` — why an area trigger refused us ("You must be at least level 58
/// to enter…"); `u32 length` + one cstring (vmangos `WorldSession::SendAreaTriggerMessage`,
/// `Server/WorldSession.cpp:882-898`).
AreaTriggerMessage {
text: String,
},
/// `SMSG_PLAYED_TIME` — answers our `CMSG_PLAYED_TIME` (`/played`): total played time + time
/// since the last level-up, both in seconds (layout in [`super::chat::read_played_time`]).
PlayedTime {
total: u32,
level: u32,
},
/// `MSG_RANDOM_ROLL` — the server's `/random` broadcast (layout in
/// [`super::chat::read_random_roll`]): the rolled range, the result, and the roller's guid.
RandomRoll {
min: u32,
max: u32,
roll: u32,
guid: u64,
},
/// `SMSG_INVENTORY_CHANGE_FAILURE` — the server refused an inventory operation
/// (equip/store/split; layout in [`super::items::read_inventory_change_failure`]). `reason` is the
/// `InventoryResult` code; `required_level` rides only on the level refusal (reason 1).
InventoryChangeFailure {
reason: u8,
required_level: Option<u32>,
item_guid: u64,
/// The destination bag's ABSOLUTE player slot (255 = the player's own array).
bag_slot: u8,
},
/// `SMSG_INITIAL_SPELLS` — the player's spell book + active cooldowns, once at login
/// (layout in [`super::spellbook::read_initial_spells`]).
InitialSpells {
spell_ids: Vec<u16>,
cooldowns: Vec<SpellCooldown>,
},
/// `SMSG_ACTION_BUTTONS` — the player's saved action bar, once at login: the occupied slots
/// of the 120-slot wire array (layout in [`super::action_bar::read_action_buttons`]).
ActionButtons {
buttons: Vec<ActionButton>,
},
/// `SMSG_LEARNED_SPELL` — a spell was added to the book after login (trainer/quest/level-up;
/// layout in [`super::spellbook::read_learned_spell`]). The first post-login spell-book mutation (0237).
LearnedSpell {
spell_id: u16,
},
/// `SMSG_SUPERCEDED_SPELL` — a rank-up replaced its predecessor in the book + action bar
/// (layout in [`super::spellbook::read_superceded_spell`]).
SupercededSpell {
old_spell_id: u16,
new_spell_id: u16,
},
/// `SMSG_CAST_RESULT` — the server's verdict on our `CMSG_CAST_SPELL`.
CastResult {
spell_id: u32,
outcome: CastOutcome,
},
/// `SMSG_PET_SPELLS` — the pet action bar's whole state, or (with a zero `pet_guid`) its
/// teardown (layout in [`super::pet::read_pet_spells`], decision 0982).
PetSpells(PetSpells),
/// `SMSG_PET_MODE` — the pet's react/command state alone, no bar behind it
/// (layout in [`super::pet::read_pet_mode`]).
PetMode(PetMode),
/// `SMSG_PET_ACTION_FEEDBACK` — one reason byte for a refused pet order.
PetActionFeedback {
reason: u8,
},
/// `SMSG_PET_CAST_FAILED` — the pet's cast refusal, in `SMSG_CAST_RESULT`'s vocabulary.
PetCastFailed {
spell_id: u32,
outcome: CastOutcome,
},
/// `SMSG_ATTACKSTART` — a unit began melee auto-attack (including our own echo).
AttackStart {
attacker: u64,
victim: u64,
},
/// `SMSG_ATTACKSTOP` — a unit stopped melee auto-attack.
AttackStop {
attacker: u64,
victim: u64,
},
/// `SMSG_ATTACKERSTATEUPDATE` — one completed melee swing (decision 0073: the attacker's swing
/// animation trigger).
AttackerState(AttackerState),
/// `SMSG_AI_REACTION` — a creature flared aggro (2 HOSTILE) or a stealth pre-aggro alert
/// (0 ALERT) at someone (layout in [`super::attack::read_ai_reaction`]; decision 0277).
AiReaction {
unit: u64,
reaction: u32,
},
/// `SMSG_SPELL_START` — a non-triggered cast began, instants included (decision 0099 phase 1:
/// the precast trigger; layout in [`super::spells::read_spell_start`]).
SpellStart(SpellStart),
/// `SMSG_SPELL_GO` — the cast launched: hit/miss lists + (for a ranged spell) the ammo block.
/// The server schedules impact itself off `Spell.dbc` Speed — nothing about missile travel
/// rides this packet (layout in [`super::spells::read_spell_go`]).
SpellGo(SpellGo),
SpellChainTargets(SpellChainTargets),
/// `SMSG_SPELL_FAILED_OTHER` — an observed cast was interrupted/cancelled (vmangos
/// `Spell::SendInterrupted`); our own cast's failure is [`Self::CastResult`] instead.
SpellFailedOther {
caster: u64,
spell_id: u32,
},
/// `SMSG_SPELL_DELAYED` — pushback: our own cast took damage and the server extended its timer
/// by `delay_ms` (vmangos `Spell::Delayed`). The cast bar slides its window out by the same, so
/// a hit pushes the bar back instead of letting it finish early (decision 0256).
SpellDelayed {
caster: u64,
delay_ms: u32,
},
/// `SMSG_CANCEL_AUTO_REPEAT` — stop our own ranged auto-repeat visual; self-only, empty body
/// (vmangos `WorldPackets::Misc::CancelAutoRepeat`). Consumed by the local cancel funnel
/// (`net/apply/spells.rs::cancel_auto_repeat`, decision 0406). vmangos DOES send it —
/// `SpellCaster::InterruptSpell` → `Player::SendAutoRepeatCancel`, on every player autorepeat
/// interrupt, target death included (corrected 2026-08-05; the earlier "zero send sites" note
/// here was wrong).
CancelAutoRepeat,
/// `SMSG_SPELL_COOLDOWN` — server-pushed cooldowns for the player or pet, by caster guid
/// (layout + the `cooldown_ms == 0` "use Spell.dbc" fork in [`super::spellbook::read_spell_cooldown`];
/// decision 0137 phase 4).
SpellCooldownList {
caster: u64,
/// `(spell_id, cooldown_ms)` — `0` ms = the spell's own DBC recovery/category times.
cooldowns: Vec<(u32, u32)>,
},
/// `SMSG_ITEM_COOLDOWN` — put an item (by instance guid) on the client's fixed 30 s use
/// cooldown for its on-use spell (layout in [`super::spellbook::read_item_cooldown`]).
ItemCooldown {
item_guid: u64,
spell_id: u32,
},
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` — how long one item's TEMPORARY enchant has left (layout in
/// [`super::items::read_item_enchant_time`]). The tooltip's countdown has no other source
/// (decision 0920).
ItemEnchantTime {
item_guid: u64,
slot: u32,
seconds: u32,
},
/// `SMSG_COOLDOWN_EVENT` — start an on-hold (`SPELL_ATTR_COOLDOWN_ON_EVENT`) cooldown's
/// parked timers now (layout in [`super::spellbook::read_cooldown_event`]).
CooldownEvent {
spell_id: u32,
caster: u64,
},
/// `SMSG_CLEAR_COOLDOWN` — remove one spell's cooldown record outright (same body shape as
/// [`Self::CooldownEvent`]).
ClearCooldown {
spell_id: u32,
caster: u64,
},
/// `SMSG_COOLDOWN_CHEAT` — wipe every cooldown for the named unit (the GM reset).
CooldownCheat {
caster: u64,
},
/// `MSG_CHANNEL_START` — our own channeled cast opened (self-only; no guid on the wire). The
/// cast bar's channel-open edge (decision 0137).
ChannelStart {
spell_id: u32,
duration_ms: u32,
},
/// `MSG_CHANNEL_UPDATE` — our own channel's time left; `0` = over (natural end and interrupt
/// alike). Self-only (decision 0137).
ChannelUpdate {
remaining_ms: u32,
},
/// `SMSG_UPDATE_AURA_DURATION` — how long one of **our own** auras has left, keyed by its
/// `UNIT_FIELD_AURA` slot. Self-only, never sent for a permanent aura, and it arrives *before*
/// the descriptor delta that says which spell occupies the slot — so a consumer buffers it by
/// slot and joins on `(slot, spell_id)` (decision 0255).
UpdateAuraDuration {
slot: u8,
remaining_ms: u32,
},
/// `SMSG_PLAY_SPELL_VISUAL` — play a spell-visual kit on a unit outside the normal cast
/// sequence, at the client's hardcoded stage 0 (the eat/drink kit cadence, mid-channel kit
/// swaps — decision 0280).
PlaySpellVisual {
unit: u64,
kit_id: u32,
},
/// `SMSG_SPELLNONMELEEDAMAGELOG` — non-melee (spell) damage dealt (decision 0137 phase 2's
/// floating-combat-text data feed; layout in [`super::combat_log::read_spell_damage_log`]).
SpellDamageLog(SpellDamageLog),
/// `SMSG_PERIODICAURALOG` — periodic (DoT/HoT/regen) aura ticks (decision 0137 phase 2; layout
/// in [`super::combat_log::read_periodic_aura_log`]).
PeriodicAuraLog(PeriodicAuraLog),
/// `SMSG_SPELLHEALLOG` — a direct heal landing (decision 0578's center-combat-text feed;
/// layout in [`super::combat_log::read_spell_heal_log`]).
SpellHealLog(SpellHealLog),
/// `SMSG_SPELLENERGIZELOG` — an instant power gain (decision 0578; layout in
/// [`super::combat_log::read_spell_energize_log`]).
SpellEnergizeLog(SpellEnergizeLog),
/// `SMSG_SPELLDAMAGESHIELD` — a damage-shield (Thorns-style) return hit (decision 0137 phase 2;
/// layout in [`super::combat_log::read_damage_shield`]).
DamageShield(DamageShield),
/// `SMSG_ENVIRONMENTALDAMAGELOG` — environmental damage taken (fall/drowning/…; layout in
/// [`super::combat_log::read_environmental_damage_log`]).
EnvironmentalDamageLog(EnvironmentalDamageLog),
/// `SMSG_SPELLLOGMISS` — a spell cast's per-target miss list (decision 0137 phase 2; layout in
/// [`super::combat_log::read_spell_log_miss`]).
SpellLogMiss(SpellLogMiss),
/// `SMSG_LOG_XPGAIN` — an XP award, kill or non-kill (decision 0137 phase 2; layout in
/// [`super::progression::read_xp_gain`]).
XpGain(XpGain),
/// `SMSG_EXPLORATION_EXPERIENCE` — a first visit to an area: the discovered area id + its XP
/// award (decision 0828; layout in [`super::progression::read_exploration_xp`]).
ExplorationXp(ExplorationXp),
/// `SMSG_LEVELUP_INFO` — our own ding, self-addressed only (decision 0304; layout in
/// [`super::progression::read_level_up_info`]).
LevelUp(LevelUpInfo),
/// `SMSG_QUESTGIVER_STATUS` — the questgiver dialog status for one NPC (`!`/`?` marker), a
/// [`super::quest::dialog_status`] value. A world-marker concern (out of the panel slice, decision
/// 0088): parsed + surfaced per guid now, rendered later.
QuestGiverStatus {
npc: u64,
status: u32,
},
/// `SMSG_QUESTGIVER_QUEST_LIST` — the greeting panel: an NPC's offered/active quest rows
/// (layout in [`super::quest::read_questgiver_quest_list`]).
QuestGiverQuestList(QuestGiverList),
/// `SMSG_QUESTGIVER_QUEST_DETAILS` — the accept panel: full quest text + rewards on offer
/// (layout in [`super::quest::read_questgiver_quest_details`]).
QuestGiverDetails(QuestDetails),
/// `SMSG_QUESTGIVER_REQUEST_ITEMS` — the progress panel: the "bring me these" text + required
/// items and the completability flag (layout in [`super::quest::read_questgiver_request_items`]).
QuestGiverRequestItems(QuestRequestItems),
/// `SMSG_QUESTGIVER_OFFER_REWARD` — the reward panel: turn-in text + rewards to grant (layout in
/// [`super::quest::read_questgiver_offer_reward`]).
QuestGiverOfferReward(QuestOfferReward),
/// `SMSG_QUESTGIVER_QUEST_COMPLETE` — the turn-in result: XP/money granted + fixed items
/// (layout in [`super::quest::read_questgiver_quest_complete`]).
QuestGiverComplete(QuestComplete),
/// `SMSG_QUESTGIVER_QUEST_INVALID` (vmangos `Quest.cpp:126`) — the accept/query attempt was
/// rejected before a details/failed panel could be built; `msg` is a client message code.
QuestGiverInvalid {
msg: u32,
},
/// `SMSG_QUESTGIVER_QUEST_FAILED` (vmangos `Quest.cpp:110`) — a `CMSG_QUESTGIVER_ACCEPT_QUEST`
/// (or an in-progress requirement) failed; `reason` is a client `QuestFailedReason` code.
QuestGiverFailed {
quest_id: u32,
reason: u32,
},
/// `SMSG_QUEST_QUERY_RESPONSE` — the full quest template, answering `CMSG_QUEST_QUERY`; feeds
/// the quest-log detail view (layout + wire-trap notes on [`QuestTemplate`]). Boxed: at 400+
/// bytes (four fixed-count arrays plus five strings) it would otherwise dwarf every other
/// `ServerPacket` variant and bloat the whole enum.
QuestQueryResponse(Box<QuestTemplate>),
/// `SMSG_QUESTLOG_FULL` — the log has no free slot for a new quest; empty body (vmangos
/// `Quest.cpp:87`).
QuestLogFull,
/// `SMSG_QUESTUPDATE_COMPLETE` — every objective on this quest is now complete (vmangos
/// `Quest.cpp:91`); the log slot's state byte gets `QUEST_STATE_COMPLETE`.
QuestUpdateComplete {
quest_id: u32,
},
/// `SMSG_QUESTUPDATE_FAILED` — the quest failed outright (vmangos `Quest.cpp:116`).
QuestUpdateFailed {
quest_id: u32,
},
/// `SMSG_QUESTUPDATE_FAILEDTIMER` — a timed quest's clock ran out (vmangos `Quest.cpp:121`).
QuestUpdateFailedTimer {
quest_id: u32,
},
/// `SMSG_QUESTUPDATE_ADD_KILL` — a kill/use toast for a creature-or-gameobject objective
/// (vmangos `Quest.cpp:144`); `entry` carries the same raw creature/GO encoding as
/// [`super::quest::QuestObjective::creature_or_go`], and mirrors (doesn't replace) the durable
/// `PLAYER_QUEST_LOG` counter field.
QuestUpdateAddKill {
quest_id: u32,
entry: u32,
count: u32,
required: u32,
guid: u64,
},
/// `SMSG_QUESTUPDATE_ADD_ITEM` — an item-collection toast (vmangos `Quest.cpp:138`).
QuestUpdateAddItem {
item_id: u32,
count: u32,
},
/// `SMSG_GOSSIP_MESSAGE` — a gossip menu opened on an NPC, answering our `CMSG_GOSSIP_HELLO` (or
/// riding a `CMSG_GOSSIP_SELECT_OPTION` reply that re-opens the menu). Payload VERIFIED vmangos
/// `GossipDef.cpp:180-225` (the 1.12 shape — no box-money field, that's TBC+); `text_id` drives a
/// follow-up `CMSG_NPC_TEXT_QUERY` for the greeting body. `quests` carries the quest-option rows
/// riding the same packet (wired to the questgiver panels — decision 0088).
GossipMessage {
npc: u64,
text_id: u32,
options: Vec<GossipOption>,
quests: Vec<QuestOption>,
},
/// `SMSG_GOSSIP_COMPLETE` — the gossip window closes (vmangos `Npc.cpp:90`); an empty body.
GossipComplete,
/// `SMSG_NPC_TEXT_UPDATE` — answers `CMSG_NPC_TEXT_QUERY`: always 8 weighted text blocks
/// (vmangos `GossipDef.cpp:298-369`), carried here **undecided**. Which line greets you needs
/// the NPC's gender and a die roll, so it is drawn when the frame opens, not when the packet
/// lands — [`super::gossip::select_greeting`].
NpcText {
text_id: u32,
blocks: Vec<super::gossip::NpcTextBlock>,
},
/// `SMSG_LIST_INVENTORY` — a vendor's stock, answering `CMSG_LIST_INVENTORY` (vmangos
/// `ItemHandler.cpp:741-810`). Empty stock sends `count = 0` plus a trailing error byte the
/// parser leaves unconsumed.
VendorList {
vendor: u64,
items: Vec<VendorItem>,
},
/// `SMSG_BUY_ITEM` — the vendor stock update after a purchase (vmangos `Item.cpp:190-196`); the
/// purchased item itself arrives via `UPDATE_OBJECT` + `SMSG_ITEM_PUSH_RESULT`, already handled.
BuyItem {
vendor: u64,
slot: u32,
new_count: u32,
purchase_count: u32,
},
/// `SMSG_SELL_ITEM` — the **error** path only (vmangos `Item.cpp:183-188`, `SendSellError`
/// `Player.cpp:11723`); a successful sell sends nothing, visible only as the item vanishing +
/// coinage rising via `UPDATE_OBJECT`. `reason` is a [`super::vendor::sell_result`] code.
SellItemResult {
vendor: u64,
item_guid: u64,
reason: u8,
},
/// `SMSG_BUY_FAILED` — the server refused a purchase (vmangos `Item.h:277`). `reason` is a
/// [`super::vendor::buy_result`] code.
BuyFailed {
vendor: u64,
item_entry: u32,
reason: u8,
},
/// `SMSG_SHOW_BANK` — the bank window opens, answering our `CMSG_BANKER_ACTIVATE` (vmangos
/// `Npc.cpp:94`) — or arriving unprompted for the `GOSSIP_OPTION_BANKER` gossip option
/// (`SendShowBank`, `Player.cpp:12426`); the handler must not assume it always follows our own
/// activate. The vault itself is already streamed via the ordinary player descriptor
/// (decision 0604) — this only opens the window.
ShowBank {
banker: u64,
},
/// `SMSG_BUY_BANK_SLOT_RESULT` — a bank-slot purchase was refused (vmangos `Item.cpp:137-140`).
/// `result` is a [`super::bank::bank_slot_result`] code; a *successful* buy sends no packet at
/// all, visible only as the `PLAYER_BYTES_2` bank-bag-count byte advancing + the coinage drop
/// (decision 0604).
BuyBankSlotResult {
result: u32,
},
/// `SMSG_TRAINER_LIST` — a class/profession trainer's service list, reached through the gossip
/// trainer option (layout in [`super::trainer::read_trainer_list`], decision 0237). `trainer_type` is
/// the window-framing kind (0 class · 1 mount · 2 tradeskill · 3 pet); `title` is the greeting.
TrainerList {
trainer: u64,
trainer_type: u32,
services: Vec<TrainerSpell>,
title: String,
},
/// `SMSG_TRAINER_BUY_SUCCEEDED` — the trainer taught the service (vmangos `SendTrainingSuccess`);
/// confirmation + sound only — the learned spell arrives via `SMSG_LEARNED_SPELL`, and the
/// green→gray repaint needs a `CMSG_TRAINER_LIST` re-request.
TrainerBuySucceeded {
trainer: u64,
spell_id: u32,
},
/// `SMSG_TRAINER_BUY_FAILED` — the trainer refused (vmangos `SendTrainingFailure`). `error` is a
/// [`super::trainer::train_fail`] code (0 unavailable · 1 not-enough-money · 2 not-enough-skill).
TrainerBuyFailed {
trainer: u64,
spell_id: u32,
error: u32,
},
/// `SMSG_LOOT_RESPONSE`, normal shape — a loot window opened, answering `CMSG_LOOT` (layout in
/// [`super::loot::read_loot_response`]). `loot_type` is a [`super::loot::loot_type`] code; `items` includes
/// any quest-item rows riding the same list.
LootResponse {
guid: u64,
loot_type: u8,
gold: u32,
items: Vec<LootItem>,
},
/// `SMSG_LOOT_RESPONSE`, error shape — the server refused to open the loot window (didn't
/// kill it, too far, not standing, …). `error` is a [`super::loot::loot_error`] code.
LootError {
guid: u64,
error: u8,
},
/// `SMSG_LOOT_RELEASE_RESPONSE` — the loot window closes, answering `CMSG_LOOT_RELEASE`.
/// `result` is always `1` (vmangos never sends another value).
LootReleaseResponse {
guid: u64,
result: u8,
},
/// `SMSG_LOOT_REMOVED` — one loot-window row was taken, by anyone.
LootRemoved {
slot: u8,
},
/// `SMSG_LOOT_MONEY_NOTIFY` — our share of the loot's coin pile, answering `CMSG_LOOT_MONEY`.
LootMoneyNotify {
amount: u32,
},
/// `SMSG_LOOT_CLEAR_MONEY` — the coin line disappears for every current looter; empty body.
LootClearMoney,
/// `SMSG_LOOT_START_ROLL` — a group roll opened on one drop (layout in [`LootStartRoll`]);
/// drives a `GroupLootFrame`.
LootStartRoll(LootStartRoll),
/// `SMSG_LOOT_ROLL` — one roller's vote or dice result (layout, and the overloaded
/// `(roll_number, roll_type)` pair, in [`LootRoll`]).
LootRoll(LootRoll),
/// `SMSG_LOOT_ROLL_WON` — a group roll resolved (layout in [`LootRollWon`]).
LootRollWon(LootRollWon),
/// `SMSG_LOOT_ALL_PASSED` — everyone passed; the roll closes and the item returns to the
/// corpse for ordinary looting (layout in [`LootAllPassed`]).
LootAllPassed(LootAllPassed),
/// `SMSG_ITEM_PUSH_RESULT` — an item landed in our bags (looted or received from an NPC);
/// drives the "You receive loot: …" chat line (layout in [`ItemPushResult`]).
ItemPushResult(ItemPushResult),
/// `MSG_CORPSE_QUERY`'s answer — where our corpse is (layout + the two-map split in
/// [`CorpseLocation`]); also pushed unprompted as not-found at bones-conversion.
CorpseQuery(CorpseLocation),
/// `SMSG_CORPSE_RECLAIM_DELAY` — ms until the corpse can be reclaimed (sent at release +
/// at login-while-dead).
CorpseReclaimDelay {
delay_ms: u32,
},
/// `SMSG_DURABILITY_DAMAGE_DEATH` — the 10% natural-death durability loss happened (empty
/// body); the red error-line cue.
DurabilityDamageDeath,
/// `SMSG_RESURRECT_REQUEST` — a resurrection offer (layout in [`ResurrectRequestBody`]).
ResurrectRequest(ResurrectRequestBody),
/// `SMSG_SPIRIT_HEALER_CONFIRM` — the spirit healer asks for the XP_LOSS-style confirm;
/// carries the NPC's guid (the eventual `CMSG_SPIRIT_HEALER_ACTIVATE` target).
SpiritHealerConfirm {
npc: u64,
},
/// **A granted mover mode changed** — the ack'd movement-mode family (decision 0866): root,
/// water-walk, feather-fall or hover, granted or revoked on our mover. `apply` is the direction.
/// Must be acked with the echoed `counter` ([`MoveMode::ack_opcode`]) or the server never applies
/// the change and observers never see it.
MoveMode {
guid: u64,
counter: u32,
mode: MoveMode,
apply: bool,
},
/// `SMSG_LOGOUT_COMPLETE` — the world session is over; we are back at character select.
LogoutComplete,
/// `SMSG_LOGOUT_RESPONSE` — the server's answer to `CMSG_LOGOUT_REQUEST`, and the ONLY thing
/// that decides whether logging out is instant or a countdown. Body `{u32 reason, u8 instant}`
/// (vmangos `WorldPackets::Misc::LogoutResponse`; the real client reads the same pair — wow-re
/// `system/net/ledger.tsv` 0x5b4630 `Handle(0x4c) — {u32, u8}`).
///
/// `reason` non-zero is a REFUSAL and no logout starts (vmangos `HandleLogoutRequestOpcode`:
/// 1 in combat, 3 jumping/falling, 2 GM-frozen). `instant` is set when the server logs you out
/// on the spot — resting (an inn or a city), on a taxi, or a GM account — in which case
/// `LogoutComplete` follows immediately; otherwise a 20-second server-side timer runs, which is
/// what the client's CAMP/QUIT dialog counts down.
LogoutResponse {
reason: u32,
instant: bool,
},
/// `SMSG_LOGOUT_CANCEL_ACK` — the server dropped a pending logout at our `CMSG_LOGOUT_CANCEL`.
/// Empty body; it is what takes the countdown dialog back down (`LOGOUT_CANCEL`).
LogoutCancelAck,
/// `SMSG_PONG` — the echo of our `CMSG_PING`'s sequence number (the keepalive's return leg;
/// the io layer matches it against the ping clock to measure the round-trip).
Pong {
sequence: u32,
},
/// A `SMSG_FORCE_*_SPEED_CHANGE` — the server changed one of our mover's six speeds (aura,
/// mount, GM `.modify speed`) and awaits the matching `CMSG_FORCE_*_SPEED_CHANGE_ACK` carrying
/// this `counter` + the exact `speed` back (see the opcode block's protocol note). `guid` is
/// the mover (packed on the wire); `speed` is flat yd/s (rad/s for the turn rate).
ForceSpeedChange {
guid: u64,
kind: SpeedKind,
counter: u32,
speed: f32,
},
/// `SMSG_GROUP_INVITE` — someone invited us to their group (layout in
/// [`super::group::read_group_invite`]).
GroupInvite {
inviter: String,
},
/// `SMSG_GROUP_DECLINE` — an invite we sent was declined.
GroupDecline {
name: String,
},
/// `SMSG_GROUP_UNINVITE` — we were removed from our group (kicked or left); empty body.
GroupUninvited,
/// `SMSG_GROUP_SET_LEADER` — the group's leader changed.
GroupLeaderChanged {
name: String,
},
/// `SMSG_GROUP_DESTROYED` — the group disbanded outright; empty body.
GroupDestroyed,
/// `SMSG_GROUP_LIST` — the full roster, sent on every membership change (layout in
/// [`super::group::read_group_list`]). `members` excludes the recipient's own row (`own_flags`
/// carries theirs); `loot` is `None` for the empty "you left" shape and whenever the list has
/// no other members.
GroupList {
group_type: u8,
own_flags: u8,
members: Vec<GroupMemberEntry>,
leader: u64,
loot: Option<GroupLootInfo>,
},
/// `SMSG_PARTY_COMMAND_RESULT` — the server's verdict on a group command we issued
/// (invite/leave): `operation` a [`super::group::party_operation`] code, `result` a
/// [`super::group::party_result`] code.
PartyCommandResult {
operation: u32,
member: String,
result: u32,
},
/// `SMSG_PARTY_MEMBER_STATS` / `_FULL` — a party/raid member's live stats for the frame
/// (layout in [`super::group::read_party_member_stats`]); `full` distinguishes the delta form
/// from the ask-once full form (our own [`super::group::request_party_member_stats`], or the
/// offline-miss reply). Boxed: [`PartyMemberStatsInfo`]'s ~20 optional fields would otherwise
/// dwarf every other variant here.
PartyMemberStats {
guid: u64,
full: bool,
info: Box<PartyMemberStatsInfo>,
},
/// `MSG_MINIMAP_PING` — someone pinged the minimap for the group.
MinimapPing {
guid: u64,
x: f32,
y: f32,
},
/// `MSG_RAID_TARGET_UPDATE`, delta shape — one raid-target icon changed; `guid == 0` clears it.
RaidTargetSet {
icon: u8,
guid: u64,
},
/// `MSG_RAID_TARGET_UPDATE`, full-list shape — the whole current icon set (only currently-set
/// icons are present).
RaidTargetList {
entries: Vec<(u8, u64)>,
},
/// `MSG_RAID_READY_CHECK`, empty body — the raid leader started a ready check.
ReadyCheckRequest,
/// `MSG_RAID_READY_CHECK`, non-empty body — one member's answer, forwarded to the leader only.
ReadyCheckAnswer {
guid: u64,
ready: u8,
},
/// `SMSG_DUEL_REQUESTED` — a duel challenge. Sent to challenger and challenged alike; which
/// one we are is `challenger == our guid` (decision 0633).
DuelRequested {
arbiter: u64,
challenger: u64,
},
/// `SMSG_DUEL_OUTOFBOUNDS` — we left the 75 yd bubble around the duel flag; 10 s to return.
DuelOutOfBounds,
/// `SMSG_DUEL_INBOUNDS` — we came back inside (70 yd, the hysteresis edge).
DuelInBounds,
/// `SMSG_DUEL_COMPLETE` — the duel is over. `started` is false only when it ended before it
/// began (declined/cancelled), which is what earns the "Duel cancelled." line.
DuelComplete {
started: bool,
},
/// `SMSG_DUEL_WINNER` — the outcome line, broadcast to everyone nearby.
DuelWinner {
fled: bool,
winner: String,
loser: String,
},
/// `SMSG_DUEL_COUNTDOWN` — start the "Duel starting: N" tick. Already converted from the
/// wire's milliseconds to whole seconds ([`super::duel::read_duel_countdown`]).
DuelCountdown {
seconds: u32,
},
/// `SMSG_START_MIRROR_TIMER` — start or wholly re-state one mirror timer (the breath /
/// fatigue bars, decision 0874). The family has no update opcode: every change re-sends this.
MirrorTimerStart(MirrorTimerStart),
/// `SMSG_PAUSE_MIRROR_TIMER` — freeze/unfreeze a running timer. vmangos never sends it.
MirrorTimerPause {
kind: u32,
paused: bool,
},
/// `SMSG_STOP_MIRROR_TIMER` — that timer is over; hide its bar.
MirrorTimerStop {
kind: u32,
},
/// `SMSG_FRIEND_LIST` — the whole friend list, guids + presence (decision 0668). Pushed
/// unasked at login and on every `CMSG_FRIEND_LIST`; always complete, never a delta.
FriendList {
friends: Vec<FriendEntry>,
},
/// `SMSG_IGNORE_LIST` — the whole ignore list: guids and nothing else.
IgnoreList {
guids: Vec<u64>,
},
/// `SMSG_FRIEND_STATUS` — one result about one player: the ack for an add/remove, or the
/// login/logout broadcast every friend-lister receives.
FriendStatus(FriendStatusUpdate),
/// `SMSG_WHO` — the `/who` answer: up to 49 rows plus the true match total.
WhoResults(WhoResults),
/// `SMSG_GUILD_QUERY_RESPONSE` — one guild's public identity: name, its ten rank names, tabard.
/// The guild twin of the name query: an ask-once cache fill keyed by guild id.
GuildQueryResponse(GuildQueryResponse),
/// `SMSG_GUILD_ROSTER` — the whole guild: MOTD, info text, every rank's rights, every member.
/// Always a complete snapshot (the server re-sends it for any change), never a delta.
GuildRoster(GuildRoster),
/// `SMSG_GUILD_EVENT` — one thing that happened in the guild, as an event id plus its
/// already-formatted string arguments.
GuildEvent(GuildEventNotice),
/// `SMSG_GUILD_COMMAND_RESULT` — the verdict on a guild verb we sent. The command tag beside
/// the code is load-bearing, not decorative (result `0x08` means two different things).
GuildCommandResult(GuildCommandResult),
/// `SMSG_GUILD_INVITE` — somebody asked us into their guild. Answered by `CMSG_GUILD_ACCEPT` /
/// `CMSG_GUILD_DECLINE`, neither of which echoes anything back, so the pending invite is
/// client-side state.
GuildInvite {
inviter: String,
guild: String,
},
/// `SMSG_GUILD_DECLINE` — the player we invited turned it down. Sent only to the inviter.
GuildDecline {
name: String,
},
/// `SMSG_GUILD_INFO` — the "founded on / N members / N accounts" summary; a separate ask from
/// the roster, with no overlapping fields.
GuildInfo(GuildInfo),
/// A `SMSG_SPLINE_SET_*_SPEED` — a speed change on a unit we don't control (a creature, or a
/// player mid-spline): `[packed guid][f32 speed]`, no counter, no ack (decision 0441 — how an
/// observed unit's mounted speed reaches us).
SplineSpeedChange {
guid: u64,
kind: SpeedKind,
speed: f32,
},
/// A `MSG_MOVE_SET_*_SPEED` — a freely-moving *player's* speed change (the common observer
/// case: someone mounts up nearby): `[packed guid][MovementInfo][f32 speed]` — a speed change
/// AND a fresh authoritative pose in one packet (decision 0441).
MoveSetSpeed {
guid: u64,
kind: SpeedKind,
flags: u32,
position: Vector3d,
orientation: f32,
/// The swim pitch from the `MovementInfo` tail (see [`Self::PlayerMove::pitch`]).
pitch: f32,
/// The `MovementInfo` time word (see [`Self::PlayerMove::time`]).
time: u32,
fall_time: u32,
jump: Option<JumpInfo>,
/// The rider's platform frame when ON_TRANSPORT is set (a speed change aboard a boat
/// carries the same MovementInfo as any relay — decision 0438's frame law applies).
transport: Option<TransportPose>,
speed: f32,
},
/// `SMSG_MOUNTRESULT` / `SMSG_DISMOUNTRESULT` — the server's answer to a (dis)mount attempt
/// (`mount` distinguishes them): one raw result code (vmangos `UnitMountResult` /
/// `UnitDismountResult`; OK = 10 / 3). Decision 0441; error lines are a P2 trimming.
MountResult {
mount: bool,
code: u32,
},
/// `SMSG_MOUNTSPECIAL_ANIM` — a nearby mounted player hit the flourish (one raw u64 guid;
/// VERIFIED vmangos `HandleMountSpecialAnimOpcode`, `MovementHandler.cpp:969-970`). The
/// sender is excluded from the broadcast, so this only ever names someone else's mount.
MountSpecialAnim {
guid: u64,
},
/// `SMSG_CLIENT_CONTROL_UPDATE` — packed mover guid + `u8` allowMove (VERIFIED vmangos
/// `Server/Packets/Misc.cpp:677-682`). Always addressed to us; `mover` is the unit it speaks
/// about, which is our own guid when the server is taking control *away*.
ClientControlUpdate {
mover: u64,
allow_move: bool,
},
/// `SMSG_SHOWTAXINODES` — the taxi map (vmangos `SendTaxiMenu`, `TaxiHandler.cpp:82-96`;
/// layout in [`super::taxi::read_show_taxi_nodes`], decision 0484). `window` is the
/// window-framing constant vmangos always writes `1`; `flightmaster` names the NPC the menu
/// opened on (its taxi mount resolves through the `benilla-formats` `TaxiNodes` catalog);
/// `nearest_node` is the node the flight master itself sits at; `known` is the full
/// known-node bitmask.
ShowTaxiNodes {
window: u32,
flightmaster: u64,
nearest_node: u32,
known: TaxiMask,
},
/// `SMSG_TAXINODE_STATUS` — answers `CMSG_TAXINODE_STATUS_QUERY`, and also rides a
/// first-visit "learn" (vmangos `SendLearnNewTaxiNode`, `TaxiHandler.cpp:117-138`): `guid`
/// names the flight master asked about (plain, not packed); `known` is whether the nearest
/// node to it is in our taxi mask.
TaxiNodeStatus {
guid: u64,
known: bool,
},
/// `SMSG_ACTIVATETAXIREPLY` — answers `CMSG_ACTIVATETAXI`/`CMSG_ACTIVATETAXIEXPRESS`: `code`
/// is a [`super::taxi_reply`] value (`0` OK, everything else a refusal — no flight starts).
ActivateTaxiReply {
code: u32,
},
/// `SMSG_NEW_TAXI_PATH` — empty body. Rides a first-visit "learn" alongside
/// [`Self::TaxiNodeStatus`] (vmangos `SendLearnNewTaxiNode`); the client has nothing to read
/// from it, but the wire is modelled rather than silently dropped.
NewTaxiPath,
/// `SMSG_MAIL_LIST_RESULT` — the inbox page, answering `CMSG_GET_MAIL_LIST` (layout in
/// [`super::mail::read_mail_list_result`], decision 0544 P0).
MailList {
mails: Vec<MailListEntry>,
},
/// `SMSG_SEND_MAIL_RESULT` — the verdict on a mail action (send/take-money/take-item/return/
/// delete), keyed by `action` ([`super::mail::mail_action`]) and `error`
/// ([`super::mail::mail_error`]); `equip_error`/`item` are the two mutually-exclusive
/// conditional tails (layout in [`super::mail::read_send_mail_result`]).
SendMailResult {
mail_id: u32,
action: u32,
error: u32,
equip_error: Option<u32>,
item: Option<(u32, u32)>,
},
/// `SMSG_ITEM_TEXT_QUERY_RESPONSE` — a letter's body text, answering `CMSG_ITEM_TEXT_QUERY`
/// (the ask-once fetch a mail's nonzero `item_text_id` triggers).
ItemTextQueryResponse {
text_id: u32,
text: String,
},
/// `SMSG_RECEIVED_MAIL` — a mail arrived (instant for text-only, on the delivery timer's
/// expiry otherwise). `seconds` is the delay until it is "waiting", in the countdown's units:
/// vmangos only ever sends `0.0` ("now"), but the real client reads a float here and runs it
/// through the countdown's set-value ladder (decision 0913).
ReceivedMail {
seconds: f32,
},
/// `MSG_QUERY_NEXT_MAIL_TIME`'s reply (same opcode as our empty-body request): `0.0` = unread
/// mail waiting, `-86400.0` = none.
NextMailTime {
seconds: f32,
},
/// `SMSG_TRADE_STATUS` — one pulse of the trade state machine (open/accept/cancel/complete/
/// the refusal reasons); the tail-carrying statuses hold their payload in [`TradeStatus`]
/// (layout in [`super::trade::read_trade_status`], decision 0592 P0).
TradeStatus {
status: TradeStatus,
},
/// `SMSG_TRADE_STATUS_EXTENDED` — the item/gold snapshot for one window side, pushed whenever
/// that side's offer changes (layout in [`super::trade::read_trade_status_extended`]). Boxed:
/// the seven-slot item array is ~460 bytes, and it would otherwise bloat every `ServerPacket`
/// (the same reason `QuestQueryResponse`/`PartyMemberStats` box their payloads).
TradeStatusExtended {
state: Box<TradeStatusExtended>,
},
/// `SMSG_INIT_WORLD_STATES` — the whole world-state table for a zone, pushed on login and on
/// every zone change (layout in [`super::world_state::read_init_world_states`]).
InitWorldStates(InitWorldStates),
/// `SMSG_UPDATE_WORLD_STATE` — one `(id, value)` write into that table.
UpdateWorldState {
id: u32,
value: u32,
},
Other {
opcode: u16,
},
}
impl ServerPacket {
/// A short human name for logging/tallying (the opcode in hex for unmodelled packets).
pub fn name(&self) -> String {
match self {
ServerPacket::AuthChallenge { .. } => "SMSG_AUTH_CHALLENGE".into(),
ServerPacket::AuthResponse { .. } => "SMSG_AUTH_RESPONSE".into(),
ServerPacket::CharEnum { .. } => "SMSG_CHAR_ENUM".into(),
ServerPacket::CharCreate { .. } => "SMSG_CHAR_CREATE".into(),
ServerPacket::CharDelete { .. } => "SMSG_CHAR_DELETE".into(),
ServerPacket::UpdateObject { .. } => "SMSG_UPDATE_OBJECT".into(),
ServerPacket::CompressedMoves { .. } => "SMSG_COMPRESSED_MOVES".into(),
ServerPacket::DestroyObject { .. } => "SMSG_DESTROY_OBJECT".into(),
ServerPacket::TriggerCinematic { .. } => "SMSG_TRIGGER_CINEMATIC".into(),
ServerPacket::MonsterMove { .. } => "SMSG_MONSTER_MOVE".into(),
ServerPacket::PlayerMove { opcode, .. } => format!("MSG_MOVE relay ({opcode:#06x})"),
ServerPacket::Teleport { .. } => "MSG_MOVE_TELEPORT_ACK".into(),
ServerPacket::NewWorld { .. } => "SMSG_NEW_WORLD".into(),
ServerPacket::TransferPending { .. } => "SMSG_TRANSFER_PENDING".into(),
ServerPacket::TransferAborted { .. } => "SMSG_TRANSFER_ABORTED".into(),
ServerPacket::LoginVerifyWorld { .. } => "SMSG_LOGIN_VERIFY_WORLD".into(),
ServerPacket::TimeSpeed { .. } => "SMSG_LOGIN_SETTIMESPEED".into(),
ServerPacket::QueryTimeResponse { .. } => "SMSG_QUERY_TIME_RESPONSE".into(),
ServerPacket::BindPoint { .. } => "SMSG_BINDPOINTUPDATE".into(),
ServerPacket::BinderConfirm { .. } => "SMSG_BINDER_CONFIRM".into(),
ServerPacket::PlayerBound { .. } => "SMSG_PLAYERBOUND".into(),
ServerPacket::SetProficiency { .. } => "SMSG_SET_PROFICIENCY".into(),
ServerPacket::InitializeFactions { .. } => "SMSG_INITIALIZE_FACTIONS".into(),
ServerPacket::SetFactionStanding { .. } => "SMSG_SET_FACTION_STANDING".into(),
ServerPacket::SetFactionVisible { .. } => "SMSG_SET_FACTION_VISIBLE".into(),
ServerPacket::NameQueryResponse { .. } => "SMSG_NAME_QUERY_RESPONSE".into(),
ServerPacket::CreatureQueryResponse { .. } => "SMSG_CREATURE_QUERY_RESPONSE".into(),
ServerPacket::PetNameQueryResponse { .. } => "SMSG_PET_NAME_QUERY_RESPONSE".into(),
ServerPacket::GameObjectQueryResponse { .. } => "SMSG_GAMEOBJECT_QUERY_RESPONSE".into(),
ServerPacket::PageTextQueryResponse { .. } => "SMSG_PAGE_TEXT_QUERY_RESPONSE".into(),
ServerPacket::GameObjectCustomAnim { .. } => "SMSG_GAMEOBJECT_CUSTOM_ANIM".into(),
ServerPacket::FishNotHooked => "SMSG_FISH_NOT_HOOKED".into(),
ServerPacket::FishEscaped => "SMSG_FISH_ESCAPED".into(),
ServerPacket::PlaySound { .. } => "SMSG_PLAY_SOUND".into(),
ServerPacket::PlayMusic { .. } => "SMSG_PLAY_MUSIC".into(),
ServerPacket::PlayObjectSound { .. } => "SMSG_PLAY_OBJECT_SOUND".into(),
ServerPacket::Weather { .. } => "SMSG_WEATHER".into(),
ServerPacket::TextEmote { .. } => "SMSG_TEXT_EMOTE".into(),
ServerPacket::Emote { .. } => "SMSG_EMOTE".into(),
ServerPacket::ItemQueryResponse { .. } => "SMSG_ITEM_QUERY_SINGLE_RESPONSE".into(),
ServerPacket::InventoryChangeFailure { .. } => "SMSG_INVENTORY_CHANGE_FAILURE".into(),
ServerPacket::MessageChat(_) => "SMSG_MESSAGECHAT".into(),
ServerPacket::ChannelNotify(_) => "SMSG_CHANNEL_NOTIFY".into(),
ServerPacket::ChannelList { .. } => "SMSG_CHANNEL_LIST".into(),
ServerPacket::ChatPlayerNotFound { .. } => "SMSG_CHAT_PLAYER_NOT_FOUND".into(),
ServerPacket::ChatWrongFaction => "SMSG_CHAT_WRONG_FACTION".into(),
ServerPacket::Notification { .. } => "SMSG_NOTIFICATION".into(),
ServerPacket::AreaTriggerMessage { .. } => "SMSG_AREA_TRIGGER_MESSAGE".into(),
ServerPacket::PlayedTime { .. } => "SMSG_PLAYED_TIME".into(),
ServerPacket::RandomRoll { .. } => "MSG_RANDOM_ROLL".into(),
ServerPacket::InitialSpells { .. } => "SMSG_INITIAL_SPELLS".into(),
ServerPacket::ActionButtons { .. } => "SMSG_ACTION_BUTTONS".into(),
ServerPacket::LearnedSpell { .. } => "SMSG_LEARNED_SPELL".into(),
ServerPacket::SupercededSpell { .. } => "SMSG_SUPERCEDED_SPELL".into(),
ServerPacket::CastResult { .. } => "SMSG_CAST_RESULT".into(),
ServerPacket::PetSpells(_) => "SMSG_PET_SPELLS".into(),
ServerPacket::PetMode(_) => "SMSG_PET_MODE".into(),
ServerPacket::PetActionFeedback { .. } => "SMSG_PET_ACTION_FEEDBACK".into(),
ServerPacket::PetCastFailed { .. } => "SMSG_PET_CAST_FAILED".into(),
ServerPacket::AttackStart { .. } => "SMSG_ATTACKSTART".into(),
ServerPacket::AttackStop { .. } => "SMSG_ATTACKSTOP".into(),
ServerPacket::AttackerState(_) => "SMSG_ATTACKERSTATEUPDATE".into(),
ServerPacket::AiReaction { .. } => "SMSG_AI_REACTION".into(),
ServerPacket::SpellStart(_) => "SMSG_SPELL_START".into(),
ServerPacket::SpellGo(_) => "SMSG_SPELL_GO".into(),
ServerPacket::SpellChainTargets(_) => "SMSG_SPELL_UPDATE_CHAIN_TARGETS".into(),
ServerPacket::SpellFailedOther { .. } => "SMSG_SPELL_FAILED_OTHER".into(),
ServerPacket::SpellDelayed { .. } => "SMSG_SPELL_DELAYED".into(),
ServerPacket::CancelAutoRepeat => "SMSG_CANCEL_AUTO_REPEAT".into(),
ServerPacket::SpellCooldownList { .. } => "SMSG_SPELL_COOLDOWN".into(),
ServerPacket::ItemCooldown { .. } => "SMSG_ITEM_COOLDOWN".into(),
ServerPacket::ItemEnchantTime { .. } => "SMSG_ITEM_ENCHANT_TIME_UPDATE".into(),
ServerPacket::CooldownEvent { .. } => "SMSG_COOLDOWN_EVENT".into(),
ServerPacket::ClearCooldown { .. } => "SMSG_CLEAR_COOLDOWN".into(),
ServerPacket::CooldownCheat { .. } => "SMSG_COOLDOWN_CHEAT".into(),
ServerPacket::ChannelStart { .. } => "MSG_CHANNEL_START".into(),
ServerPacket::ChannelUpdate { .. } => "MSG_CHANNEL_UPDATE".into(),
ServerPacket::UpdateAuraDuration { .. } => "SMSG_UPDATE_AURA_DURATION".into(),
ServerPacket::PlaySpellVisual { .. } => "SMSG_PLAY_SPELL_VISUAL".into(),
ServerPacket::SpellDamageLog(_) => "SMSG_SPELLNONMELEEDAMAGELOG".into(),
ServerPacket::PeriodicAuraLog(_) => "SMSG_PERIODICAURALOG".into(),
ServerPacket::SpellHealLog(_) => "SMSG_SPELLHEALLOG".into(),
ServerPacket::SpellEnergizeLog(_) => "SMSG_SPELLENERGIZELOG".into(),
ServerPacket::DamageShield(_) => "SMSG_SPELLDAMAGESHIELD".into(),
ServerPacket::EnvironmentalDamageLog(_) => "SMSG_ENVIRONMENTALDAMAGELOG".into(),
ServerPacket::SpellLogMiss(_) => "SMSG_SPELLLOGMISS".into(),
ServerPacket::XpGain(_) => "SMSG_LOG_XPGAIN".into(),
ServerPacket::ExplorationXp(_) => "SMSG_EXPLORATION_EXPERIENCE".into(),
ServerPacket::LevelUp(_) => "SMSG_LEVELUP_INFO".into(),
ServerPacket::QuestGiverStatus { .. } => "SMSG_QUESTGIVER_STATUS".into(),
ServerPacket::QuestGiverQuestList(_) => "SMSG_QUESTGIVER_QUEST_LIST".into(),
ServerPacket::QuestGiverDetails(_) => "SMSG_QUESTGIVER_QUEST_DETAILS".into(),
ServerPacket::QuestGiverRequestItems(_) => "SMSG_QUESTGIVER_REQUEST_ITEMS".into(),
ServerPacket::QuestGiverOfferReward(_) => "SMSG_QUESTGIVER_OFFER_REWARD".into(),
ServerPacket::QuestGiverComplete(_) => "SMSG_QUESTGIVER_QUEST_COMPLETE".into(),
ServerPacket::QuestGiverInvalid { .. } => "SMSG_QUESTGIVER_QUEST_INVALID".into(),
ServerPacket::QuestGiverFailed { .. } => "SMSG_QUESTGIVER_QUEST_FAILED".into(),
ServerPacket::QuestQueryResponse(_) => "SMSG_QUEST_QUERY_RESPONSE".into(),
ServerPacket::QuestLogFull => "SMSG_QUESTLOG_FULL".into(),
ServerPacket::QuestUpdateComplete { .. } => "SMSG_QUESTUPDATE_COMPLETE".into(),
ServerPacket::QuestUpdateFailed { .. } => "SMSG_QUESTUPDATE_FAILED".into(),
ServerPacket::QuestUpdateFailedTimer { .. } => "SMSG_QUESTUPDATE_FAILEDTIMER".into(),
ServerPacket::QuestUpdateAddKill { .. } => "SMSG_QUESTUPDATE_ADD_KILL".into(),
ServerPacket::QuestUpdateAddItem { .. } => "SMSG_QUESTUPDATE_ADD_ITEM".into(),
ServerPacket::GossipMessage { .. } => "SMSG_GOSSIP_MESSAGE".into(),
ServerPacket::GossipComplete => "SMSG_GOSSIP_COMPLETE".into(),
ServerPacket::NpcText { .. } => "SMSG_NPC_TEXT_UPDATE".into(),
ServerPacket::VendorList { .. } => "SMSG_LIST_INVENTORY".into(),
ServerPacket::BuyItem { .. } => "SMSG_BUY_ITEM".into(),
ServerPacket::SellItemResult { .. } => "SMSG_SELL_ITEM".into(),
ServerPacket::BuyFailed { .. } => "SMSG_BUY_FAILED".into(),
ServerPacket::ShowBank { .. } => "SMSG_SHOW_BANK".into(),
ServerPacket::BuyBankSlotResult { .. } => "SMSG_BUY_BANK_SLOT_RESULT".into(),
ServerPacket::TrainerList { .. } => "SMSG_TRAINER_LIST".into(),
ServerPacket::TrainerBuySucceeded { .. } => "SMSG_TRAINER_BUY_SUCCEEDED".into(),
ServerPacket::TrainerBuyFailed { .. } => "SMSG_TRAINER_BUY_FAILED".into(),
ServerPacket::LootResponse { .. } => "SMSG_LOOT_RESPONSE".into(),
ServerPacket::LootError { .. } => "SMSG_LOOT_RESPONSE (error)".into(),
ServerPacket::LootReleaseResponse { .. } => "SMSG_LOOT_RELEASE_RESPONSE".into(),
ServerPacket::LootRemoved { .. } => "SMSG_LOOT_REMOVED".into(),
ServerPacket::LootMoneyNotify { .. } => "SMSG_LOOT_MONEY_NOTIFY".into(),
ServerPacket::LootClearMoney => "SMSG_LOOT_CLEAR_MONEY".into(),
ServerPacket::LootStartRoll(_) => "SMSG_LOOT_START_ROLL".into(),
ServerPacket::LootRoll(_) => "SMSG_LOOT_ROLL".into(),
ServerPacket::LootRollWon(_) => "SMSG_LOOT_ROLL_WON".into(),
ServerPacket::LootAllPassed(_) => "SMSG_LOOT_ALL_PASSED".into(),
ServerPacket::ItemPushResult(_) => "SMSG_ITEM_PUSH_RESULT".into(),
ServerPacket::CorpseQuery(_) => "MSG_CORPSE_QUERY".into(),
ServerPacket::CorpseReclaimDelay { .. } => "SMSG_CORPSE_RECLAIM_DELAY".into(),
ServerPacket::DurabilityDamageDeath => "SMSG_DURABILITY_DAMAGE_DEATH".into(),
ServerPacket::ResurrectRequest(_) => "SMSG_RESURRECT_REQUEST".into(),
ServerPacket::SpiritHealerConfirm { .. } => "SMSG_SPIRIT_HEALER_CONFIRM".into(),
ServerPacket::MoveMode { mode, apply, .. } => match (mode, apply) {
(MoveMode::Root, true) => "SMSG_FORCE_MOVE_ROOT".into(),
(MoveMode::Root, false) => "SMSG_FORCE_MOVE_UNROOT".into(),
(MoveMode::WaterWalk, true) => "SMSG_MOVE_WATER_WALK".into(),
(MoveMode::WaterWalk, false) => "SMSG_MOVE_LAND_WALK".into(),
(MoveMode::FeatherFall, true) => "SMSG_MOVE_FEATHER_FALL".into(),
(MoveMode::FeatherFall, false) => "SMSG_MOVE_NORMAL_FALL".into(),
(MoveMode::Hover, true) => "SMSG_MOVE_SET_HOVER".into(),
(MoveMode::Hover, false) => "SMSG_MOVE_UNSET_HOVER".into(),
},
ServerPacket::LogoutComplete => "SMSG_LOGOUT_COMPLETE".into(),
ServerPacket::LogoutResponse { .. } => "SMSG_LOGOUT_RESPONSE".into(),
ServerPacket::LogoutCancelAck => "SMSG_LOGOUT_CANCEL_ACK".into(),
ServerPacket::Pong { .. } => "SMSG_PONG".into(),
ServerPacket::ForceSpeedChange { kind, .. } => {
format!("SMSG_FORCE_{kind:?}_SPEED_CHANGE")
}
ServerPacket::GroupInvite { .. } => "SMSG_GROUP_INVITE".into(),
ServerPacket::GroupDecline { .. } => "SMSG_GROUP_DECLINE".into(),
ServerPacket::GroupUninvited => "SMSG_GROUP_UNINVITE".into(),
ServerPacket::GroupLeaderChanged { .. } => "SMSG_GROUP_SET_LEADER".into(),
ServerPacket::GroupDestroyed => "SMSG_GROUP_DESTROYED".into(),
ServerPacket::GroupList { .. } => "SMSG_GROUP_LIST".into(),
ServerPacket::PartyCommandResult { .. } => "SMSG_PARTY_COMMAND_RESULT".into(),
ServerPacket::PartyMemberStats { full: true, .. } => {
"SMSG_PARTY_MEMBER_STATS_FULL".into()
}
ServerPacket::PartyMemberStats { full: false, .. } => "SMSG_PARTY_MEMBER_STATS".into(),
ServerPacket::MinimapPing { .. } => "MSG_MINIMAP_PING".into(),
ServerPacket::RaidTargetSet { .. } | ServerPacket::RaidTargetList { .. } => {
"MSG_RAID_TARGET_UPDATE".into()
}
ServerPacket::ReadyCheckRequest | ServerPacket::ReadyCheckAnswer { .. } => {
"MSG_RAID_READY_CHECK".into()
}
ServerPacket::DuelRequested { .. } => "SMSG_DUEL_REQUESTED".into(),
ServerPacket::DuelOutOfBounds => "SMSG_DUEL_OUTOFBOUNDS".into(),
ServerPacket::DuelInBounds => "SMSG_DUEL_INBOUNDS".into(),
ServerPacket::DuelComplete { .. } => "SMSG_DUEL_COMPLETE".into(),
ServerPacket::DuelWinner { .. } => "SMSG_DUEL_WINNER".into(),
ServerPacket::DuelCountdown { .. } => "SMSG_DUEL_COUNTDOWN".into(),
ServerPacket::MirrorTimerStart(..) => "SMSG_START_MIRROR_TIMER".into(),
ServerPacket::MirrorTimerPause { .. } => "SMSG_PAUSE_MIRROR_TIMER".into(),
ServerPacket::MirrorTimerStop { .. } => "SMSG_STOP_MIRROR_TIMER".into(),
ServerPacket::FriendList { .. } => "SMSG_FRIEND_LIST".into(),
ServerPacket::IgnoreList { .. } => "SMSG_IGNORE_LIST".into(),
ServerPacket::FriendStatus(..) => "SMSG_FRIEND_STATUS".into(),
ServerPacket::WhoResults(..) => "SMSG_WHO".into(),
ServerPacket::GuildQueryResponse(..) => "SMSG_GUILD_QUERY_RESPONSE".into(),
ServerPacket::GuildRoster(..) => "SMSG_GUILD_ROSTER".into(),
ServerPacket::GuildEvent(..) => "SMSG_GUILD_EVENT".into(),
ServerPacket::GuildCommandResult(..) => "SMSG_GUILD_COMMAND_RESULT".into(),
ServerPacket::GuildInvite { .. } => "SMSG_GUILD_INVITE".into(),
ServerPacket::GuildDecline { .. } => "SMSG_GUILD_DECLINE".into(),
ServerPacket::GuildInfo(..) => "SMSG_GUILD_INFO".into(),
ServerPacket::SplineSpeedChange { kind, .. } => {
format!("SMSG_SPLINE_SET_{kind:?}_SPEED")
}
ServerPacket::MoveSetSpeed { kind, .. } => format!("MSG_MOVE_SET_{kind:?}_SPEED"),
ServerPacket::MountResult { mount: true, .. } => "SMSG_MOUNTRESULT".into(),
ServerPacket::MountResult { mount: false, .. } => "SMSG_DISMOUNTRESULT".into(),
ServerPacket::MountSpecialAnim { .. } => "SMSG_MOUNTSPECIAL_ANIM".into(),
ServerPacket::ClientControlUpdate { .. } => "SMSG_CLIENT_CONTROL_UPDATE".into(),
ServerPacket::ShowTaxiNodes { .. } => "SMSG_SHOWTAXINODES".into(),
ServerPacket::TaxiNodeStatus { .. } => "SMSG_TAXINODE_STATUS".into(),
ServerPacket::ActivateTaxiReply { .. } => "SMSG_ACTIVATETAXIREPLY".into(),
ServerPacket::NewTaxiPath => "SMSG_NEW_TAXI_PATH".into(),
ServerPacket::MailList { .. } => "SMSG_MAIL_LIST_RESULT".into(),
ServerPacket::SendMailResult { .. } => "SMSG_SEND_MAIL_RESULT".into(),
ServerPacket::ItemTextQueryResponse { .. } => "SMSG_ITEM_TEXT_QUERY_RESPONSE".into(),
ServerPacket::ReceivedMail { .. } => "SMSG_RECEIVED_MAIL".into(),
ServerPacket::NextMailTime { .. } => "MSG_QUERY_NEXT_MAIL_TIME".into(),
ServerPacket::TradeStatus { .. } => "SMSG_TRADE_STATUS".into(),
ServerPacket::TradeStatusExtended { .. } => "SMSG_TRADE_STATUS_EXTENDED".into(),
ServerPacket::InitWorldStates(_) => "SMSG_INIT_WORLD_STATES".into(),
ServerPacket::UpdateWorldState { .. } => "SMSG_UPDATE_WORLD_STATE".into(),
ServerPacket::Other { opcode } => format!("opcode {opcode:#06x}"),
}
}
}