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

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

1658 lines
78 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The opcode→[`ServerPacket`] dispatch: [`parse_server`] decodes one server packet body (already
//! decrypted + sized) into its variant, delegating per-domain payloads to the domain children
//! (`spells`, `quest`, `loot`, …). One match arm per opcode — which opcode carries what stays
//! readable in one place.
use std::io::{self, Read};
use crate::wire::{
read_cstring, read_f32_le, read_packed_guid, read_u16_le, read_u32_le, read_u64_le, read_u8,
Vector3d,
};
use super::{
action_bar, area_trigger, attack, auction, bank, battlefield, binder, broadcast, channel, chat,
combat_log, death, duel, gameobject, gm_ticket, gossip, group, guild, instance, items, loot,
mail, meeting_stone, mirror_timer, monster_move, movement, opcode, page_text, pet, petition,
progression, pvp, quest, social, spellbook, spells, stable, summon, tabard, taxi, trade,
trainer, tutorial, update_object, vendor, world_state, AttackSwingError, Character,
CreatureQueryInfo, JumpInfo, MoveMode, ServerPacket, SpeedKind, SplineMode,
};
/// Read one `SMSG_FORCE_*_SPEED_CHANGE` body — `[packed mover guid][u32 counter][f32 speed]`,
/// identical across all six kinds (VERIFIED vmangos `SendSpeedChangeToController`, the 5875
/// `> 1_9_4` branch). The kind is the opcode's; the caller's arm names it.
fn read_force_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
Ok(ServerPacket::ForceSpeedChange {
guid: read_packed_guid(r)?,
kind,
counter: read_u32_le(r)?,
speed: read_f32_le(r)?,
})
}
/// Read an `SMSG_COMPRESSED_MOVES` body: `[u32 uncompressed size][deflate stream]`, the stream a
/// run of `[u8 size][u16 opcode][body]` records where `size` counts the opcode's own two bytes
/// (VERIFIED vmangos `MovementData::AddPacket`/`BuildPacket`). Each record is a whole movement
/// packet, so it goes straight back through [`parse_server`] — the batch is a *transport*, not a
/// message shape, and nothing downstream needs to know a move arrived batched.
///
/// A record that fails to parse fails the **whole** batch (surfacing as one `Poll::Skipped` naming
/// the inner opcode) rather than being dropped quietly. Losing a batch is worse than losing a
/// packet, but silence here is what cost days: an unhandled inner opcode has to be loud. Only the
/// `MSG_MOVE_*` relays vmangos routes through `ObjectViewersMovementDeliverer` can appear, and we
/// model all of them (`the_batch_carries_every_relayed_move_opcode`).
fn read_compressed_moves(r: &mut impl Read) -> io::Result<Vec<ServerPacket>> {
let uncompressed = read_u32_le(r)? as usize;
let mut buf = Vec::with_capacity(uncompressed.min(64 * 1024));
flate2::read::ZlibDecoder::new(r).read_to_end(&mut buf)?;
let mut rest = buf.as_slice();
let mut packets = Vec::new();
while !rest.is_empty() {
let size = read_u8(&mut rest)? as usize;
let opcode = read_u16_le(&mut rest)?;
// `size` spans opcode + body; anything under two bytes cannot even hold the opcode we
// just read, so the stream is misframed and every later record would be garbage.
let body_len = size.checked_sub(2).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("compressed-moves record size {size} < 2"),
)
})?;
if rest.len() < body_len {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"compressed-moves record {opcode:#06x} wants {body_len}B, {}B left",
rest.len()
),
));
}
let (body, tail) = rest.split_at(body_len);
rest = tail;
// A batch inside a batch would recurse without bound; vmangos never nests one.
if opcode == opcode::SMSG_COMPRESSED_MOVES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"compressed-moves nested inside compressed-moves",
));
}
packets.push(parse_server(opcode, body).map_err(|e| {
io::Error::new(
e.kind(),
format!(
"compressed-moves record {opcode:#06x} ({}): {e}",
super::opcode_name(opcode).unwrap_or("?")
),
)
})?);
}
Ok(packets)
}
/// Read one `SMSG_SPLINE_SET_*_SPEED` body — `[packed guid][f32 speed]` (VERIFIED vmangos
/// `MovementPacketSender::SendSpeedChangeToAll` + the mid-spline observer branch, the 5875
/// `> 1_8_4` layout). Observer-only: no counter, no ack (decision 0441).
fn read_spline_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
Ok(ServerPacket::SplineSpeedChange {
guid: read_packed_guid(r)?,
kind,
speed: read_f32_le(r)?,
})
}
/// Read one `MSG_MOVE_SET_*_SPEED` body — `[packed guid][MovementInfo][f32 speed]` (VERIFIED
/// vmangos `SendSpeedChangeToObservers`, the finalized-spline branch): a freely-moving player's
/// speed change, carrying a fresh pose alongside (decision 0441). No ack.
fn read_move_set_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
let guid = read_packed_guid(r)?;
let info = movement::read_movement_info(r)?;
Ok(ServerPacket::MoveSetSpeed {
guid,
kind,
flags: info.flags,
position: info.position,
orientation: info.orientation,
pitch: info.pitch,
time: info.timestamp,
fall_time: info.fall_time,
jump: info.jump,
transport: info.transport,
speed: read_f32_le(r)?,
})
}
/// True for a relayed movement opcode — one the server rebroadcasts as `[packed guid][MovementInfo]`.
/// Two groups, one wire shape and one client handler (`0x603bb0`, wow-re `re/net/opcode-handlers.tsv`
/// — 30 rows pointing at it, of which these are 23):
///
/// - the **echoed input stream**, every opcode bound to vmangos `HandleMovementOpcodes` (VERIFIED
/// `Opcodes.cpp`): another player's walking, turning, jumping, swimming, facing;
/// - the **observer leg of the movement-mode family** (decision 2061): root/unroot, hover,
/// feather-fall, water-walk and the near-teleport, broadcast by
/// `MovementPacketSender::Send{MovementFlagChange,Teleport}ToObservers` once the mover's own
/// client has acked. Same body, no ack, and the apply/unapply direction rides the `MovementInfo`
/// flags word rather than the opcode (see [`super::opcode`]'s block).
///
/// Excludes `MSG_MOVE_TELEPORT_ACK` / `MSG_MOVE_WORLDPORT_ACK`, which share the family but carry
/// different bodies and are decoded by their own arms — note `MSG_MOVE_TELEPORT` (197) is the
/// observer's teleport and is NOT `MSG_MOVE_TELEPORT_ACK` (199): no counter dword, so it parses here.
const fn is_movement_relay(o: u16) -> bool {
matches!(
o,
opcode::MSG_MOVE_START_FORWARD
| opcode::MSG_MOVE_START_BACKWARD
| opcode::MSG_MOVE_STOP
| opcode::MSG_MOVE_START_STRAFE_LEFT
| opcode::MSG_MOVE_START_STRAFE_RIGHT
| opcode::MSG_MOVE_STOP_STRAFE
| opcode::MSG_MOVE_JUMP
| opcode::MSG_MOVE_START_TURN_LEFT
| opcode::MSG_MOVE_START_TURN_RIGHT
| opcode::MSG_MOVE_STOP_TURN
| opcode::MSG_MOVE_START_PITCH_UP
| opcode::MSG_MOVE_START_PITCH_DOWN
| opcode::MSG_MOVE_STOP_PITCH
| opcode::MSG_MOVE_SET_RUN_MODE
| opcode::MSG_MOVE_SET_WALK_MODE
| opcode::MSG_MOVE_FALL_LAND
| opcode::MSG_MOVE_START_SWIM
| opcode::MSG_MOVE_STOP_SWIM
| opcode::MSG_MOVE_SET_FACING
| opcode::MSG_MOVE_SET_PITCH
| opcode::MSG_MOVE_HEARTBEAT
// Somebody else was knocked back (decision 1702). It carries four extra floats after the
// `MovementInfo` — the launch quad — which this reader simply doesn't reach, and doesn't
// need to: the server built that `MovementInfo` from the victim's own ack, so its jump
// tail already IS the quad, and the arc replays from it like any other relayed jump.
| opcode::MSG_MOVE_KNOCK_BACK
// The observer leg of the movement-mode family (decision 2061): somebody else was
// rooted, levitated (hover + feather fall + water walk), or blinked. The mode lands in
// the `MovementInfo` flags word this body already carries, so no arm of its own is
// needed — only the opcode's admission to the family.
| opcode::MSG_MOVE_ROOT
| opcode::MSG_MOVE_UNROOT
| opcode::MSG_MOVE_HOVER
| opcode::MSG_MOVE_FEATHER_FALL
| opcode::MSG_MOVE_WATER_WALK
| opcode::MSG_MOVE_TELEPORT
)
}
/// Parse a server packet body (already decrypted + sized) by opcode.
/// `SMSG_ADDON_INFO`'s record stream — **no count, no names, no trailer** (decision 2175).
///
/// The grammar is the one `AddOn_ReadAddonInfoReply 0x51da70` reads, per record (wow-re
/// `system/net/scratch/cmsg-auth-session-addon-block.md` §6):
///
/// ```text
/// u8 status ; 2 -> [rec+0x29] = 1 (hidden from the Lua index space)
/// ; 0 -> [rec+0x24] = 2 (rejected)
/// ; else -> verify the .toc/Bindings.xml signature
/// u8 infoProvided ; persisted into the .pub and echoed next logon
/// if infoProvided:
/// u8 keyProvided
/// if keyProvided: u8[256] modulus
/// u32 revision
/// u8 urlProvided
/// if urlProvided: u8[256] url
/// ```
///
/// The retail capture is the minimal form — 12 x 8 bytes of `{2, 1, 0, 0u32, 0}`.
///
/// **A truncated record ends the walk and keeps the records before it**, rather than failing the
/// packet. Only a record that parsed whole contributes a status, so `statuses[i]` is always
/// record *i*'s and the pairing against what we sent cannot slip. The stream is self-delimiting only if every field is read exactly right, so a grammar
/// error here would otherwise turn a working login into an unparseable packet; the statuses we did
/// read are still true, and a short read is visible as a count that does not match what we sent.
fn read_addon_info(r: &mut &[u8]) -> Vec<u8> {
let mut statuses = Vec::new();
while !r.is_empty() {
let Ok(status) = read_u8(r) else { break };
let Ok(info_provided) = read_u8(r) else { break };
if info_provided != 0 {
let Ok(key_provided) = read_u8(r) else { break };
if key_provided != 0 && skip(r, 256).is_none() {
break;
}
if read_u32_le(r).is_err() {
break;
}
}
let Ok(url_provided) = read_u8(r) else { break };
if url_provided != 0 && skip(r, 256).is_none() {
break;
}
statuses.push(status);
}
statuses
}
/// Advance `r` by `n` bytes, or `None` when it holds fewer.
fn skip(r: &mut &[u8], n: usize) -> Option<()> {
(r.len() >= n).then(|| *r = &r[n..])
}
pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
let mut r = body;
Ok(match opcode {
opcode::SMSG_AUTH_CHALLENGE => ServerPacket::AuthChallenge {
server_seed: read_u32_le(&mut r)?,
},
opcode::SMSG_AUTH_RESPONSE => {
// **The client's own grammar** (VERIFIED, handler `0x5b41b0`; wow-re
// `system/glue/scratch/login-failure-dialogs.md`):
//
// u8 code
// if (code == AUTH_OK || code == AUTH_WAIT_QUEUE) && remaining >= 5:
// u32 billingTimeRemaining, u8 billingPlanFlags, u32 billingTimeRested
// if code == AUTH_WAIT_QUEUE:
// u32 position
//
// The client **branches on the remaining length**, which is what makes both shapes the
// mangos family sends parse correctly: the short `{u8, u32}` leaves 4 bytes (`4 < 5`,
// billing skipped, position at body offset 1) and the long
// `{u8, u32, u8, u32, u32}` leaves 13 (billing read, position at offset 10).
//
// The threshold is **5 guarding a 9-byte group** — not a size check, just "is there
// more here than a bare position?". Copied rather than tidied: a body of 6..=9 bytes
// hits it and mis-parses, and matching the client's mistakes is the point of a
// faithful parser.
//
// ONE DELIBERATE DIVERGENCE: where a truncated body leaves the client's position
// *unwritten* — silently redisplaying the previous queue position from an
// uninitialised process-lifetime global — ours reports `None`. Rendering a stale
// position as current is a bug we decline to reproduce.
let result = read_u8(&mut r)?;
let queued = result == super::AUTH_WAIT_QUEUE;
let mut billing_time_rested = None;
if (result == super::AUTH_OK || queued) && r.len() >= 5 {
let _billing_time_remaining = read_u32_le(&mut r)?;
let _billing_plan_flags = read_u8(&mut r)?;
// Kept rather than dropped since 1820: this is the only place the value ever
// arrives, and `GetBillingTimeRested()` is a plain read of it. The client parks it
// in a process-lifetime global (`[ClientServices+0x1af8]`, sole writer `0x5b421a`);
// ours rides the session, which has the same lifetime and cannot go stale across a
// reconnect the way that global does.
billing_time_rested = Some(read_u32_le(&mut r)?);
}
ServerPacket::AuthResponse {
result,
queue_position: queued.then(|| read_u32_le(&mut r).ok()).flatten(),
billing_time_rested,
}
}
opcode::SMSG_CHAR_ENUM => {
let count = read_u8(&mut r)?;
let mut characters = Vec::with_capacity(count as usize);
for _ in 0..count {
characters.push(Character::read(&mut r)?);
}
ServerPacket::CharEnum { characters }
}
opcode::SMSG_CHAR_DELETE => ServerPacket::CharDelete {
result: read_u8(&mut r)?,
},
opcode::SMSG_CHAR_CREATE => ServerPacket::CharCreate {
result: read_u8(&mut r)?,
},
opcode::SMSG_CHARACTER_LOGIN_FAILED => ServerPacket::CharacterLoginFailed {
result: read_u8(&mut r)?,
},
opcode::SMSG_UPDATE_OBJECT => ServerPacket::UpdateObject {
objects: update_object::read_update_object(&mut r)?,
},
opcode::SMSG_DESTROY_OBJECT => ServerPacket::DestroyObject {
guid: read_u64_le(&mut r)?,
},
opcode::SMSG_TRIGGER_CINEMATIC => ServerPacket::TriggerCinematic {
cinematic_id: read_u32_le(&mut r)?,
},
opcode::SMSG_COMPRESSED_UPDATE_OBJECT => {
let _decompressed_size = read_u32_le(&mut r)?;
let mut decoder = flate2::read::ZlibDecoder::new(r);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
let mut dr = decompressed.as_slice();
ServerPacket::UpdateObject {
objects: update_object::read_update_object(&mut dr)?,
}
}
opcode::SMSG_COMPRESSED_MOVES => ServerPacket::CompressedMoves {
packets: read_compressed_moves(&mut r)?,
},
opcode::MSG_MOVE_TIME_SKIPPED => {
let (guid, lag_ms) = movement::read_move_time_skipped(&mut r)?;
ServerPacket::MoveTimeSkipped { guid, lag_ms }
}
opcode::SMSG_MONSTER_MOVE => monster_move::read_monster_move(&mut r, false)?,
opcode::SMSG_MONSTER_MOVE_TRANSPORT => monster_move::read_monster_move(&mut r, true)?,
opcode::MSG_MOVE_TELEPORT_ACK => {
let guid = read_packed_guid(&mut r)?;
let counter = read_u32_le(&mut r)?;
let info = movement::read_movement_info(&mut r)?;
ServerPacket::Teleport {
guid,
counter,
position: info.position,
orientation: info.orientation,
}
}
opcode::SMSG_NEW_WORLD => {
let map = read_u32_le(&mut r)?;
let position = Vector3d::read(&mut r)?;
let orientation = read_f32_le(&mut r)?;
ServerPacket::NewWorld {
map,
position,
orientation,
}
}
opcode::SMSG_LOGIN_VERIFY_WORLD => {
let map = read_u32_le(&mut r)?;
let position = Vector3d::read(&mut r)?;
let orientation = read_f32_le(&mut r)?;
ServerPacket::LoginVerifyWorld {
map,
position,
orientation,
}
}
opcode::SMSG_TRANSFER_PENDING => {
// `u32 newMapId` +, iff the player rides a transport through the transfer,
// `{u32 transportEntry, u32 oldMapId}` (VERIFIED vmangos `Misc.cpp:493-501`). The
// block's PRESENCE is load-bearing: it decides whether the follow-up NEW_WORLD's
// coordinates are boat-local or world (decision 0455).
let map = read_u32_le(&mut r)?;
let transport = if r.is_empty() {
None
} else {
let entry = read_u32_le(&mut r)?;
let old_map = read_u32_le(&mut r)?;
Some((entry, old_map))
};
ServerPacket::TransferPending { map, transport }
}
opcode::SMSG_TRANSFER_ABORTED => ServerPacket::TransferAborted {
reason: read_u8(&mut r)?,
},
opcode::SMSG_LOGIN_SETTIMESPEED => {
// The vanilla packed DateTime bit layout (LSB up): minute:6, hour:5, weekday:3,
// day-of-month:6, month:4, year:5. The day serial flattens it with the packed
// convention's fixed 31-day months / 372-day years — monotonic across the dates a
// server actually serves, which is all the celestial moon-phase precession needs.
let datetime = read_u32_le(&mut r)?;
let timescale = read_f32_le(&mut r)?;
let (day, month, year) = (
(datetime >> 14) & 0x3F,
(datetime >> 20) & 0x0F,
(datetime >> 24) & 0x1F,
);
ServerPacket::TimeSpeed {
hours: ((datetime >> 6) & 0x1F) as u8,
minutes: (datetime & 0x3F) as u8,
day_serial: year * 372 + month * 31 + day,
timescale,
}
}
// The OTHER clock (decision 1150) — kept beside SETTIMESPEED precisely because they are
// easy to confuse: this one is wall-clock UNIX seconds (vmangos
// `WorldSession::SendQueryTimeResponse`, `Handlers/QueryHandler.cpp:418-423` — a bare
// `(uint32)time(nullptr)`), the epoch every absolute stamp the server writes into a
// descriptor field is expressed in. SETTIMESPEED above is the in-game day/night clock and
// says nothing about it.
opcode::SMSG_QUERY_TIME_RESPONSE => ServerPacket::QueryTimeResponse {
unix_time: read_u32_le(&mut r)?,
},
opcode::SMSG_BINDPOINTUPDATE => {
// vmangos BindpointUpdate::AppendBodyTo (Packets/Misc.cpp): x, y, z, mapId, areaId.
let position = Vector3d::read(&mut r)?;
let map = read_u32_le(&mut r)?;
let area = read_u32_le(&mut r)?;
ServerPacket::BindPoint {
position,
map,
area,
}
}
// The GM trouble-ticket answers (decision 1673). The three response opcodes share one
// 4-byte body and therefore one reader; only the opcode says which verb was answered.
opcode::SMSG_GMTICKET_GETTICKET => ServerPacket::GmTicketAnswer {
ticket: gm_ticket::read_gm_ticket(&mut r)?.map(Box::new),
},
opcode::SMSG_GMTICKET_CREATE => ServerPacket::GmTicketCreated {
response: gm_ticket::read_gm_ticket_response(&mut r)?,
},
opcode::SMSG_GMTICKET_UPDATETEXT => ServerPacket::GmTicketUpdated {
response: gm_ticket::read_gm_ticket_response(&mut r)?,
},
opcode::SMSG_GMTICKET_DELETETICKET => ServerPacket::GmTicketDeleted {
response: gm_ticket::read_gm_ticket_response(&mut r)?,
},
opcode::SMSG_GMTICKET_SYSTEMSTATUS => ServerPacket::GmTicketSystemStatus {
status: gm_ticket::read_gm_ticket_system_status(&mut r)?,
},
opcode::SMSG_GM_TICKET_STATUS_UPDATE => ServerPacket::GmTicketStatusUpdate {
status: gm_ticket::read_gm_ticket_response(&mut r)?,
},
opcode::SMSG_BINDER_CONFIRM => ServerPacket::BinderConfirm {
binder: binder::read_binder_confirm(&mut r)?,
},
// The summon question (decision 1747). Three fields, and the client's whole answer is
// the guid back — see `super::summon` for the byte pin on both bodies.
opcode::SMSG_SUMMON_REQUEST => {
let ask = summon::read_summon_request(&mut r)?;
ServerPacket::SummonRequest {
summoner: ask.summoner,
zone: ask.zone,
delay_ms: ask.delay_ms,
}
}
// The talent twin of the confirm above, on a two-way `MSG_` opcode: this direction is the
// question (guid + cost); the answer we send back carries the guid alone (decision 1580).
opcode::MSG_TALENT_WIPE_CONFIRM => {
let ask = progression::read_talent_wipe_confirm(&mut r)?;
ServerPacket::TalentWipeConfirm {
trainer: ask.trainer,
cost: ask.cost,
}
}
opcode::SMSG_PET_UNLEARN_CONFIRM => {
let ask = pet::read_pet_unlearn_confirm(&mut r)?;
ServerPacket::PetUnlearnConfirm {
trainer: ask.trainer,
cost: ask.cost,
}
}
opcode::SMSG_RAID_GROUP_ONLY => {
let boot = instance::read_raid_group_only(&mut r)?;
ServerPacket::RaidGroupOnly {
delay_ms: boot.delay_ms,
reason: boot.reason,
}
}
opcode::SMSG_AREA_SPIRIT_HEALER_TIME => {
let t = death::read_area_spirit_healer_time(&mut r)?;
ServerPacket::AreaSpiritHealerTime {
healer: t.healer,
ms: t.ms,
}
}
opcode::SMSG_BATTLEFIELD_STATUS => {
ServerPacket::BattlefieldStatus(battlefield::read_battlefield_status(&mut r)?)
}
opcode::MSG_PVP_LOG_DATA => {
ServerPacket::PvpLogData(battlefield::read_pvp_log_data(&mut r)?)
}
opcode::SMSG_BATTLEFIELD_LIST => {
ServerPacket::BattlefieldList(battlefield::read_battlefield_list(&mut r)?)
}
opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS => {
ServerPacket::BattlefieldPositions(battlefield::read_battlefield_positions(&mut r)?)
}
opcode::MSG_TABARDVENDOR_ACTIVATE => {
ServerPacket::TabardVendorActivate(tabard::read_tabard_vendor_activate(&mut r)?)
}
opcode::MSG_SAVE_GUILD_EMBLEM => {
ServerPacket::SaveGuildEmblemResult(tabard::read_save_guild_emblem_result(&mut r)?)
}
opcode::SMSG_GROUP_JOINED_BATTLEGROUND => ServerPacket::GroupJoinedBattleground {
result: crate::wire::read_u32_le(&mut r)?,
},
opcode::SMSG_BATTLEGROUND_PLAYER_JOINED => ServerPacket::BattlegroundPlayer {
guid: crate::wire::read_u64_le(&mut r)?,
joined: true,
},
opcode::SMSG_BATTLEGROUND_PLAYER_LEFT => ServerPacket::BattlegroundPlayer {
guid: crate::wire::read_u64_le(&mut r)?,
joined: false,
},
opcode::SMSG_MEETINGSTONE_SETQUEUE => {
let q = meeting_stone::read_meeting_stone_set_queue(&mut r)?;
ServerPacket::MeetingStoneSetQueue {
area: q.area,
status: q.status,
}
}
opcode::SMSG_MEETINGSTONE_SUCCESS => {
ServerPacket::MeetingStoneNotice(crate::messages::MeetingStoneNotice::Success)
}
opcode::SMSG_MEETINGSTONE_IN_PROGRESS => {
ServerPacket::MeetingStoneNotice(crate::messages::MeetingStoneNotice::InProgress)
}
opcode::SMSG_MEETINGSTONE_MEMBER_ADDED => ServerPacket::MeetingStoneNotice(
meeting_stone::read_meeting_stone_member_added(&mut r)?,
),
opcode::SMSG_MEETINGSTONE_JOIN_FAILED => {
ServerPacket::MeetingStoneNotice(meeting_stone::read_meeting_stone_join_failed(&mut r)?)
}
opcode::SMSG_TUTORIAL_FLAGS => {
ServerPacket::TutorialFlags(tutorial::read_tutorial_flags(&mut r)?)
}
opcode::SMSG_PLAYERBOUND => {
let bound = binder::read_player_bound(&mut r)?;
ServerPacket::PlayerBound {
binder: bound.binder,
area: bound.area,
}
}
opcode::SMSG_SET_PROFICIENCY => {
// vmangos SetProficiency::AppendBodyTo (Packets/Skill.cpp): u8 itemClass + u32 mask.
let item_class = read_u8(&mut r)?;
let subclass_mask = read_u32_le(&mut r)?;
ServerPacket::SetProficiency {
item_class,
subclass_mask,
}
}
opcode::SMSG_PLAY_SOUND => ServerPacket::PlaySound {
sound_id: read_u32_le(&mut r)?,
},
opcode::SMSG_PLAY_MUSIC => ServerPacket::PlayMusic {
music_id: read_u32_le(&mut r)?,
},
opcode::SMSG_PLAY_OBJECT_SOUND => ServerPacket::PlayObjectSound {
sound_id: read_u32_le(&mut r)?,
guid: read_u64_le(&mut r)?,
},
opcode::SMSG_WEATHER => ServerPacket::Weather {
weather_type: read_u32_le(&mut r)?,
grade: read_f32_le(&mut r)?,
sound_id: read_u32_le(&mut r)?,
instant: read_u8(&mut r)? != 0,
},
opcode::SMSG_TEXT_EMOTE => {
let guid = read_u64_le(&mut r)?;
let text_emote = read_u32_le(&mut r)?;
// Read and dropped on purpose — `emoteNum` selects neither the sentence nor the voice
// kit on the receive side (wow-re `ui/scratch/text-emote-composition.md` §3).
let _emote_num = read_u32_le(&mut r)?;
// The target's name, length-prefixed **including its NUL** (vmangos writes a lone
// `0x00` and `namelen == 1` when there was no target). Trimmed at the first NUL, so an
// untargeted emote arrives as the empty string — which is precisely the "untargeted"
// bit of the sentence-form selector, not a missing value.
let namelen = read_u32_le(&mut r)? as usize;
let mut name = Vec::with_capacity(namelen.min(64));
for _ in 0..namelen {
name.push(read_u8(&mut r)?);
}
let end = name.iter().position(|&b| b == 0).unwrap_or(name.len());
let target_name = String::from_utf8_lossy(&name[..end]).into_owned();
ServerPacket::TextEmote {
guid,
text_emote,
target_name,
}
}
opcode::SMSG_EMOTE => {
let emote_id = read_u32_le(&mut r)?;
let guid = read_u64_le(&mut r)?;
ServerPacket::Emote { guid, emote_id }
}
opcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE => {
let (entry, info) = items::read_item_query_response(&mut r)?;
ServerPacket::ItemQueryResponse {
entry,
info: info.map(Box::new),
}
}
opcode::SMSG_MESSAGECHAT => ServerPacket::MessageChat(chat::read_message_chat(&mut r)?),
opcode::SMSG_CHANNEL_NOTIFY => {
ServerPacket::ChannelNotify(channel::read_channel_notify(&mut r)?)
}
opcode::SMSG_CHANNEL_LIST => {
let (name, flags, members) = channel::read_channel_list(&mut r)?;
ServerPacket::ChannelList {
channel: name,
flags,
members,
}
}
opcode::SMSG_CHAT_PLAYER_NOT_FOUND => ServerPacket::ChatPlayerNotFound {
name: chat::read_chat_player_not_found(&mut r)?,
},
opcode::SMSG_CHAT_WRONG_FACTION => ServerPacket::ChatWrongFaction,
opcode::SMSG_NOTIFICATION => ServerPacket::Notification {
text: chat::read_notification(&mut r)?,
},
opcode::SMSG_AREA_TRIGGER_MESSAGE => ServerPacket::AreaTriggerMessage {
text: area_trigger::read_area_trigger_message(&mut r)?,
},
opcode::SMSG_SERVER_MESSAGE => {
let (message_type, text) = broadcast::read_server_message(&mut r)?;
ServerPacket::ServerMessage { message_type, text }
}
opcode::SMSG_ZONE_UNDER_ATTACK => ServerPacket::ZoneUnderAttack {
area_id: broadcast::read_zone_under_attack(&mut r)?,
},
opcode::SMSG_DEFENSE_MESSAGE => {
let (zone_id, text) = broadcast::read_defense_message(&mut r)?;
ServerPacket::DefenseMessage { zone_id, text }
}
opcode::SMSG_CHAT_RESTRICTED => ServerPacket::ChatRestricted,
opcode::SMSG_PLAYED_TIME => {
let (total, level) = chat::read_played_time(&mut r)?;
ServerPacket::PlayedTime { total, level }
}
opcode::MSG_RANDOM_ROLL => {
let (min, max, roll, guid) = chat::read_random_roll(&mut r)?;
ServerPacket::RandomRoll {
min,
max,
roll,
guid,
}
}
opcode::SMSG_INVENTORY_CHANGE_FAILURE => {
let (reason, required_level, item_guid, bag_slot) =
items::read_inventory_change_failure(&mut r)?;
ServerPacket::InventoryChangeFailure {
reason,
required_level,
item_guid,
bag_slot,
}
}
opcode::SMSG_INITIAL_SPELLS => {
let (spell_ids, cooldowns) = spellbook::read_initial_spells(&mut r)?;
ServerPacket::InitialSpells {
spell_ids,
cooldowns,
}
}
opcode::SMSG_ACTION_BUTTONS => ServerPacket::ActionButtons {
buttons: action_bar::read_action_buttons(&mut r)?,
},
opcode::SMSG_LEARNED_SPELL => ServerPacket::LearnedSpell {
spell_id: spellbook::read_learned_spell(&mut r)?,
},
opcode::SMSG_REMOVED_SPELL => ServerPacket::RemovedSpell {
spell_id: spellbook::read_removed_spell(&mut r)?,
},
opcode::SMSG_SUPERCEDED_SPELL => {
let (old_spell_id, new_spell_id) = spellbook::read_superceded_spell(&mut r)?;
ServerPacket::SupercededSpell {
old_spell_id,
new_spell_id,
}
}
opcode::SMSG_CAST_RESULT => {
let (spell_id, outcome) = spells::read_cast_result(&mut r)?;
ServerPacket::CastResult { spell_id, outcome }
}
opcode::SMSG_PET_SPELLS => ServerPacket::PetSpells(pet::read_pet_spells(&mut r)?),
opcode::SMSG_PET_MODE => ServerPacket::PetMode(pet::read_pet_mode(&mut r)?),
opcode::SMSG_PET_ACTION_FEEDBACK => ServerPacket::PetActionFeedback {
reason: pet::read_pet_action_feedback(&mut r)?,
},
opcode::SMSG_PET_CAST_FAILED => {
let (spell_id, outcome) = pet::read_pet_cast_failed(&mut r)?;
ServerPacket::PetCastFailed { spell_id, outcome }
}
opcode::SMSG_PET_TAME_FAILURE => ServerPacket::PetTameFailure {
reason: pet::read_pet_tame_failure(&mut r)?,
},
// Both bodies really are empty — the opcode is the whole message on either (vmangos's
// `AppendBodyTo` writes nothing, and the reference's handlers read nothing).
opcode::SMSG_PET_NAME_INVALID => ServerPacket::PetNameInvalid,
opcode::SMSG_PET_BROKEN => ServerPacket::PetBroken,
opcode::SMSG_PET_ACTION_SOUND => {
let (pet_guid, talk) = pet::read_pet_action_sound(&mut r)?;
ServerPacket::PetActionSound { pet_guid, talk }
}
opcode::SMSG_PET_DISMISS_SOUND => {
let (model_id, position) = pet::read_pet_dismiss_sound(&mut r)?;
ServerPacket::PetDismissSound { model_id, position }
}
opcode::SMSG_ATTACKSTART => {
let (attacker, victim) = attack::read_attack_start(&mut r)?;
ServerPacket::AttackStart { attacker, victim }
}
opcode::SMSG_ATTACKSTOP => {
let (attacker, victim) = attack::read_attack_stop(&mut r)?;
ServerPacket::AttackStop { attacker, victim }
}
opcode::SMSG_ATTACKERSTATEUPDATE => {
ServerPacket::AttackerState(attack::read_attacker_state(&mut r)?)
}
// The swing refusals — empty bodies, nothing read. `SMSG_ATTACKSWING_NOTSTANDING` (`0x147`)
// is deliberately absent: the reference never registers it and vmangos never sends it, so
// it falls to the unknown-opcode arm exactly as it does in the real client's dispatcher.
opcode::SMSG_ATTACKSWING_NOTINRANGE => {
ServerPacket::AttackSwingError(AttackSwingError::NotInRange)
}
opcode::SMSG_ATTACKSWING_BADFACING => {
ServerPacket::AttackSwingError(AttackSwingError::BadFacing)
}
opcode::SMSG_ATTACKSWING_DEADTARGET | opcode::SMSG_ATTACKSWING_CANT_ATTACK => {
ServerPacket::AttackSwingError(AttackSwingError::DeadOrUnattackable)
}
// The family's fourth arm, from the spell TU's registration — same empty body, same act.
opcode::SMSG_CANCEL_COMBAT => ServerPacket::CancelCombat,
opcode::SMSG_FEIGN_DEATH_RESISTED => ServerPacket::FeignDeathResisted,
opcode::SMSG_AI_REACTION => {
let (unit, reaction) = attack::read_ai_reaction(&mut r)?;
ServerPacket::AiReaction { unit, reaction }
}
opcode::SMSG_SPELL_START => ServerPacket::SpellStart(spells::read_spell_start(&mut r)?),
opcode::SMSG_SPELL_GO => ServerPacket::SpellGo(spells::read_spell_go(&mut r)?),
opcode::SMSG_SPELL_UPDATE_CHAIN_TARGETS => {
ServerPacket::SpellChainTargets(spells::read_spell_chain_targets(&mut r)?)
}
opcode::SMSG_SPELL_FAILED_OTHER => {
let (caster, spell_id) = spells::read_spell_failed_other(&mut r)?;
ServerPacket::SpellFailedOther { caster, spell_id }
}
opcode::SMSG_SPELL_DELAYED => {
let (caster, delay_ms) = spells::read_spell_delayed(&mut r)?;
ServerPacket::SpellDelayed { caster, delay_ms }
}
opcode::SMSG_CANCEL_AUTO_REPEAT => ServerPacket::CancelAutoRepeat,
opcode::SMSG_SPELL_COOLDOWN => {
let (caster, cooldowns) = spellbook::read_spell_cooldown(&mut r)?;
ServerPacket::SpellCooldownList { caster, cooldowns }
}
opcode::SMSG_ITEM_COOLDOWN => {
let (item_guid, spell_id) = spellbook::read_item_cooldown(&mut r)?;
ServerPacket::ItemCooldown {
item_guid,
spell_id,
}
}
opcode::SMSG_ITEM_TIME_UPDATE => {
let (item_guid, seconds) = items::read_item_time(&mut r)?;
ServerPacket::ItemTime { item_guid, seconds }
}
opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE => {
let (item_guid, slot, seconds) = items::read_item_enchant_time(&mut r)?;
ServerPacket::ItemEnchantTime {
item_guid,
slot,
seconds,
}
}
opcode::SMSG_COOLDOWN_EVENT => {
let (spell_id, caster) = spellbook::read_cooldown_event(&mut r)?;
ServerPacket::CooldownEvent { spell_id, caster }
}
opcode::SMSG_CLEAR_COOLDOWN => {
let (spell_id, caster) = spellbook::read_cooldown_event(&mut r)?;
ServerPacket::ClearCooldown { spell_id, caster }
}
opcode::SMSG_COOLDOWN_CHEAT => ServerPacket::CooldownCheat {
caster: spellbook::read_cooldown_cheat(&mut r)?,
},
opcode::MSG_CHANNEL_START => {
let (spell_id, duration_ms) = spells::read_channel_start(&mut r)?;
ServerPacket::ChannelStart {
spell_id,
duration_ms,
}
}
opcode::MSG_CHANNEL_UPDATE => ServerPacket::ChannelUpdate {
remaining_ms: spells::read_channel_update(&mut r)?,
},
opcode::SMSG_UPDATE_AURA_DURATION => {
let (slot, remaining_ms) = spells::read_update_aura_duration(&mut r)?;
ServerPacket::UpdateAuraDuration { slot, remaining_ms }
}
opcode::SMSG_PLAY_SPELL_VISUAL => {
let (unit, kit_id) = spells::read_play_spell_visual(&mut r)?;
ServerPacket::PlaySpellVisual { unit, kit_id }
}
opcode::SMSG_SPELLNONMELEEDAMAGELOG => {
ServerPacket::SpellDamageLog(combat_log::read_spell_damage_log(&mut r)?)
}
opcode::SMSG_PERIODICAURALOG => {
ServerPacket::PeriodicAuraLog(combat_log::read_periodic_aura_log(&mut r)?)
}
opcode::SMSG_SPELLDAMAGESHIELD => {
ServerPacket::DamageShield(combat_log::read_damage_shield(&mut r)?)
}
opcode::SMSG_SPELLHEALLOG => {
ServerPacket::SpellHealLog(combat_log::read_spell_heal_log(&mut r)?)
}
opcode::SMSG_SPELLENERGIZELOG => {
ServerPacket::SpellEnergizeLog(combat_log::read_spell_energize_log(&mut r)?)
}
opcode::SMSG_ENVIRONMENTALDAMAGELOG => {
ServerPacket::EnvironmentalDamageLog(combat_log::read_environmental_damage_log(&mut r)?)
}
opcode::SMSG_SPELLLOGMISS => {
ServerPacket::SpellLogMiss(combat_log::read_spell_log_miss(&mut r)?)
}
opcode::SMSG_PARTYKILLLOG => {
ServerPacket::PartyKillLog(combat_log::read_party_kill_log(&mut r)?)
}
opcode::SMSG_SPELLINSTAKILLLOG => {
ServerPacket::SpellInstaKillLog(combat_log::read_spell_insta_kill_log(&mut r)?)
}
// One body, two sentences — the arm is the only thing that tells them apart.
opcode::SMSG_PROCRESIST => {
ServerPacket::ProcResist(combat_log::read_spell_outcome_log(&mut r)?)
}
opcode::SMSG_SPELLORDAMAGE_IMMUNE => {
ServerPacket::SpellOrDamageImmune(combat_log::read_spell_outcome_log(&mut r)?)
}
opcode::SMSG_SPELLDISPELLOG => {
ServerPacket::SpellDispelLog(combat_log::read_spell_dispel_log(&mut r)?)
}
opcode::SMSG_DISPEL_FAILED => {
ServerPacket::DispelFailed(combat_log::read_dispel_failed(&mut r)?)
}
opcode::SMSG_ENCHANTMENTLOG => {
ServerPacket::EnchantmentLog(combat_log::read_enchantment_log(&mut r)?)
}
opcode::SMSG_SPELLLOGEXECUTE => {
ServerPacket::SpellLogExecute(combat_log::read_spell_log_execute(&mut r)?)
}
opcode::SMSG_LOG_XPGAIN => ServerPacket::XpGain(progression::read_xp_gain(&mut r)?),
opcode::SMSG_EXPLORATION_EXPERIENCE => {
ServerPacket::ExplorationXp(progression::read_exploration_xp(&mut r)?)
}
opcode::SMSG_LEVELUP_INFO => {
ServerPacket::LevelUp(progression::read_level_up_info(&mut r)?)
}
opcode::SMSG_QUESTGIVER_STATUS => {
let (npc, status) = quest::read_questgiver_status(&mut r)?;
ServerPacket::QuestGiverStatus { npc, status }
}
opcode::SMSG_QUESTGIVER_QUEST_LIST => {
ServerPacket::QuestGiverQuestList(quest::read_questgiver_quest_list(&mut r)?)
}
opcode::SMSG_QUESTGIVER_QUEST_DETAILS => {
ServerPacket::QuestGiverDetails(quest::read_questgiver_quest_details(&mut r)?)
}
opcode::SMSG_QUESTGIVER_REQUEST_ITEMS => {
ServerPacket::QuestGiverRequestItems(quest::read_questgiver_request_items(&mut r)?)
}
opcode::SMSG_QUESTGIVER_OFFER_REWARD => {
ServerPacket::QuestGiverOfferReward(quest::read_questgiver_offer_reward(&mut r)?)
}
opcode::SMSG_QUESTGIVER_QUEST_COMPLETE => {
ServerPacket::QuestGiverComplete(quest::read_questgiver_quest_complete(&mut r)?)
}
opcode::SMSG_QUESTGIVER_QUEST_INVALID => ServerPacket::QuestGiverInvalid {
msg: quest::read_questgiver_quest_invalid(&mut r)?,
},
opcode::SMSG_QUESTGIVER_QUEST_FAILED => {
let (quest_id, reason) = quest::read_questgiver_quest_failed(&mut r)?;
ServerPacket::QuestGiverFailed { quest_id, reason }
}
opcode::SMSG_QUEST_QUERY_RESPONSE => {
ServerPacket::QuestQueryResponse(Box::new(quest::read_quest_query_response(&mut r)?))
}
opcode::SMSG_QUESTLOG_FULL => ServerPacket::QuestLogFull,
opcode::MSG_QUEST_PUSH_RESULT => {
ServerPacket::QuestPushResult(quest::read_quest_push_result(&mut r)?)
}
opcode::SMSG_QUEST_CONFIRM_ACCEPT => {
ServerPacket::QuestConfirmAccept(quest::read_quest_confirm_accept(&mut r)?)
}
opcode::SMSG_QUESTUPDATE_COMPLETE => ServerPacket::QuestUpdateComplete {
quest_id: quest::read_quest_update_complete(&mut r)?,
},
opcode::SMSG_QUESTUPDATE_FAILED => ServerPacket::QuestUpdateFailed {
quest_id: quest::read_quest_update_failed(&mut r)?,
},
opcode::SMSG_QUESTUPDATE_FAILEDTIMER => ServerPacket::QuestUpdateFailedTimer {
quest_id: quest::read_quest_update_failedtimer(&mut r)?,
},
opcode::SMSG_QUESTUPDATE_ADD_KILL => {
let (quest_id, entry, count, required, guid) =
quest::read_quest_update_add_kill(&mut r)?;
ServerPacket::QuestUpdateAddKill {
quest_id,
entry,
count,
required,
guid,
}
}
opcode::SMSG_QUESTUPDATE_ADD_ITEM => {
let (item_id, count) = quest::read_quest_update_add_item(&mut r)?;
ServerPacket::QuestUpdateAddItem { item_id, count }
}
opcode::SMSG_GOSSIP_MESSAGE => {
let (npc, text_id, options, quests) = gossip::read_gossip_message(&mut r)?;
ServerPacket::GossipMessage {
npc,
text_id,
options,
quests,
}
}
opcode::SMSG_GOSSIP_COMPLETE => ServerPacket::GossipComplete,
opcode::SMSG_GOSSIP_POI => ServerPacket::GossipPoi(gossip::read_gossip_poi(&mut r)?),
opcode::SMSG_NPC_TEXT_UPDATE => {
let (text_id, blocks) = gossip::read_npc_text_update(&mut r)?;
ServerPacket::NpcText { text_id, blocks }
}
opcode::SMSG_LIST_INVENTORY => {
let (vendor, items) = vendor::read_list_inventory(&mut r)?;
ServerPacket::VendorList { vendor, items }
}
opcode::SMSG_BUY_ITEM => {
let (vendor, slot, new_count, purchase_count) = vendor::read_buy_item(&mut r)?;
ServerPacket::BuyItem {
vendor,
slot,
new_count,
purchase_count,
}
}
opcode::SMSG_SELL_ITEM => {
let (vendor, item_guid, reason) = vendor::read_sell_item(&mut r)?;
ServerPacket::SellItemResult {
vendor,
item_guid,
reason,
}
}
opcode::SMSG_BUY_FAILED => {
let (vendor, item_entry, reason) = vendor::read_buy_failed(&mut r)?;
ServerPacket::BuyFailed {
vendor,
item_entry,
reason,
}
}
opcode::SMSG_SHOW_BANK => {
let banker = bank::read_show_bank(&mut r)?;
ServerPacket::ShowBank { banker }
}
opcode::SMSG_BUY_BANK_SLOT_RESULT => {
let result = bank::read_buy_bank_slot_result(&mut r)?;
ServerPacket::BuyBankSlotResult { result }
}
opcode::SMSG_TRAINER_LIST => {
let (trainer, trainer_type, services, title) = trainer::read_trainer_list(&mut r)?;
ServerPacket::TrainerList {
trainer,
trainer_type,
services,
title,
}
}
opcode::SMSG_TRAINER_BUY_SUCCEEDED => {
let (trainer, spell_id) = trainer::read_trainer_buy_succeeded(&mut r)?;
ServerPacket::TrainerBuySucceeded { trainer, spell_id }
}
opcode::SMSG_TRAINER_BUY_FAILED => {
let (trainer, spell_id, error) = trainer::read_trainer_buy_failed(&mut r)?;
ServerPacket::TrainerBuyFailed {
trainer,
spell_id,
error,
}
}
opcode::MSG_LIST_STABLED_PETS => {
let (npc, num_stable_slots, pets) = stable::read_list_stabled_pets(&mut r)?;
ServerPacket::ListStabledPets {
npc,
num_stable_slots,
pets,
}
}
opcode::SMSG_INVALIDATE_PLAYER => ServerPacket::InvalidatePlayer {
guid: read_u64_le(&mut r)?,
},
opcode::SMSG_STABLE_RESULT => {
let result = stable::read_stable_result(&mut r)?;
ServerPacket::StableResult { result }
}
opcode::SMSG_LOOT_RESPONSE => {
let (guid, body) = loot::read_loot_response(&mut r)?;
match body {
loot::LootResponseBody::Items {
loot_type,
gold,
items,
} => ServerPacket::LootResponse {
guid,
loot_type,
gold,
items,
},
loot::LootResponseBody::Error { error } => ServerPacket::LootError { guid, error },
}
}
opcode::SMSG_LOOT_RELEASE_RESPONSE => {
let (guid, result) = loot::read_loot_release_response(&mut r)?;
ServerPacket::LootReleaseResponse { guid, result }
}
opcode::SMSG_LOOT_REMOVED => ServerPacket::LootRemoved {
slot: loot::read_loot_removed(&mut r)?,
},
opcode::SMSG_LOOT_MONEY_NOTIFY => ServerPacket::LootMoneyNotify {
amount: loot::read_loot_money_notify(&mut r)?,
},
opcode::SMSG_LOOT_CLEAR_MONEY => ServerPacket::LootClearMoney,
opcode::SMSG_LOOT_START_ROLL => {
ServerPacket::LootStartRoll(loot::read_loot_start_roll(&mut r)?)
}
opcode::SMSG_LOOT_ROLL => ServerPacket::LootRoll(loot::read_loot_roll(&mut r)?),
opcode::SMSG_LOOT_ROLL_WON => ServerPacket::LootRollWon(loot::read_loot_roll_won(&mut r)?),
opcode::SMSG_LOOT_ALL_PASSED => {
ServerPacket::LootAllPassed(loot::read_loot_all_passed(&mut r)?)
}
opcode::SMSG_LOOT_MASTER_LIST => ServerPacket::LootMasterList {
candidates: loot::read_loot_master_list(&mut r)?,
},
opcode::SMSG_ITEM_PUSH_RESULT => {
ServerPacket::ItemPushResult(loot::read_item_push_result(&mut r)?)
}
opcode::MSG_CORPSE_QUERY => {
ServerPacket::CorpseQuery(death::read_corpse_query_response(&mut r)?)
}
opcode::SMSG_DURABILITY_DAMAGE_DEATH => ServerPacket::DurabilityDamageDeath,
opcode::SMSG_CORPSE_RECLAIM_DELAY => ServerPacket::CorpseReclaimDelay {
delay_ms: death::read_corpse_reclaim_delay(&mut r)?,
},
opcode::SMSG_RESURRECT_REQUEST => {
ServerPacket::ResurrectRequest(death::read_resurrect_request(&mut r)?)
}
opcode::SMSG_SPIRIT_HEALER_CONFIRM => ServerPacket::SpiritHealerConfirm {
npc: death::read_spirit_healer_confirm(&mut r)?,
},
// **The ack'd movement-mode family** (decision 0866) — root, water-walk, feather-fall and
// hover, granted to the controlling client. ONE wire shape for all eight opcodes:
// `packed guid + u32 counter` (VERIFIED vmangos `MovementPacketSender.cpp:342-366`, the
// `> CLIENT_BUILD_1_9_4` branch). The app must ack with the echoed counter, or the server
// never applies the change and observers never see it.
opcode::SMSG_FORCE_MOVE_ROOT
| opcode::SMSG_FORCE_MOVE_UNROOT
| opcode::SMSG_MOVE_WATER_WALK
| opcode::SMSG_MOVE_LAND_WALK
| opcode::SMSG_MOVE_FEATHER_FALL
| opcode::SMSG_MOVE_NORMAL_FALL
| opcode::SMSG_MOVE_SET_HOVER
| opcode::SMSG_MOVE_UNSET_HOVER => {
let (mode, apply) = match opcode {
opcode::SMSG_FORCE_MOVE_ROOT => (MoveMode::Root, true),
opcode::SMSG_FORCE_MOVE_UNROOT => (MoveMode::Root, false),
opcode::SMSG_MOVE_WATER_WALK => (MoveMode::WaterWalk, true),
opcode::SMSG_MOVE_LAND_WALK => (MoveMode::WaterWalk, false),
opcode::SMSG_MOVE_FEATHER_FALL => (MoveMode::FeatherFall, true),
opcode::SMSG_MOVE_NORMAL_FALL => (MoveMode::FeatherFall, false),
opcode::SMSG_MOVE_SET_HOVER => (MoveMode::Hover, true),
_ => (MoveMode::Hover, false),
};
let guid = read_packed_guid(&mut r)?;
let counter = read_u32_le(&mut r)?;
ServerPacket::MoveMode {
guid,
counter,
mode,
apply,
}
}
// **The observer movement-mode family** (decision 1780) — the spline-move twelve. Body is a
// BARE packed guid: no counter, no `MovementInfo`, and nothing to ack (VERIFIED vmangos
// `MovementPacketSender::SendMovementFlagChangeToAll`/`SendToggleRunWalkToAll`, and the
// reference's single handler `0x603c80`, which reads one packed guid and dispatches).
// `guid` is any unit, not our mover — the constants block in `opcode` carries the arm map.
opcode::SMSG_SPLINE_MOVE_ROOT
| opcode::SMSG_SPLINE_MOVE_UNROOT
| opcode::SMSG_SPLINE_MOVE_WATER_WALK
| opcode::SMSG_SPLINE_MOVE_LAND_WALK
| opcode::SMSG_SPLINE_MOVE_FEATHER_FALL
| opcode::SMSG_SPLINE_MOVE_NORMAL_FALL
| opcode::SMSG_SPLINE_MOVE_SET_HOVER
| opcode::SMSG_SPLINE_MOVE_UNSET_HOVER
| opcode::SMSG_SPLINE_MOVE_START_SWIM
| opcode::SMSG_SPLINE_MOVE_STOP_SWIM
| opcode::SMSG_SPLINE_MOVE_SET_RUN_MODE
| opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE => {
let (mode, apply) = match opcode {
opcode::SMSG_SPLINE_MOVE_ROOT => (SplineMode::Root, true),
opcode::SMSG_SPLINE_MOVE_UNROOT => (SplineMode::Root, false),
opcode::SMSG_SPLINE_MOVE_WATER_WALK => (SplineMode::WaterWalk, true),
opcode::SMSG_SPLINE_MOVE_LAND_WALK => (SplineMode::WaterWalk, false),
opcode::SMSG_SPLINE_MOVE_FEATHER_FALL => (SplineMode::FeatherFall, true),
opcode::SMSG_SPLINE_MOVE_NORMAL_FALL => (SplineMode::FeatherFall, false),
opcode::SMSG_SPLINE_MOVE_SET_HOVER => (SplineMode::Hover, true),
opcode::SMSG_SPLINE_MOVE_UNSET_HOVER => (SplineMode::Hover, false),
opcode::SMSG_SPLINE_MOVE_START_SWIM => (SplineMode::Swimming, true),
opcode::SMSG_SPLINE_MOVE_STOP_SWIM => (SplineMode::Swimming, false),
// The one inverted pair: RUN_MODE *clears* `MOVEFLAG_WALK_MODE`, because the
// reference's `0x617e80` feeds the opcode's bool to `SetRunMode 0x7c71c0`.
opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE => (SplineMode::WalkMode, true),
_ => (SplineMode::WalkMode, false),
};
ServerPacket::SplineMoveMode {
guid: read_packed_guid(&mut r)?,
mode,
apply,
}
}
// **The knockback the server aims at our mover** (decision 1702): `packed guid + u32
// counter`, then the launch quad `vcos, vsin, speedXY, speedZ` (VERIFIED vmangos
// `MovementPacketSender.cpp:261-277`, the `> CLIENT_BUILD_1_9_4` branch). The quad reads
// straight into a [`JumpInfo`] — it *is* the jump tail the ack must echo — but the wire
// order here is direction-first, where `JumpInfo`'s own serialization is `zspeed` first, so
// the four reads are named rather than delegated.
opcode::SMSG_MOVE_KNOCK_BACK => {
let guid = read_packed_guid(&mut r)?;
let counter = read_u32_le(&mut r)?;
let cos_angle = read_f32_le(&mut r)?;
let sin_angle = read_f32_le(&mut r)?;
let xy_speed = read_f32_le(&mut r)?;
let zspeed = read_f32_le(&mut r)?;
ServerPacket::KnockBack {
guid,
counter,
launch: JumpInfo {
zspeed,
cos_angle,
sin_angle,
xy_speed,
},
}
}
opcode::SMSG_INITIALIZE_FACTIONS => {
let count = read_u32_le(&mut r)?;
let mut standings = Vec::with_capacity(count as usize);
for _ in 0..count {
let flags = read_u8(&mut r)?;
let standing = read_u32_le(&mut r)? as i32;
standings.push((flags, standing));
}
ServerPacket::InitializeFactions { standings }
}
opcode::SMSG_SET_FACTION_STANDING => {
let count = read_u32_le(&mut r)?;
let mut standings = Vec::with_capacity(count as usize);
for _ in 0..count {
let list_id = read_u32_le(&mut r)?;
let standing = read_u32_le(&mut r)? as i32;
standings.push((list_id, standing));
}
ServerPacket::SetFactionStanding { standings }
}
opcode::SMSG_SET_FACTION_VISIBLE => ServerPacket::SetFactionVisible {
list_id: read_u32_le(&mut r)?,
},
opcode::SMSG_NAME_QUERY_RESPONSE => {
let guid = read_u64_le(&mut r)?;
let name = read_cstring(&mut r)?;
let _realm = read_cstring(&mut r)?; // cross-realm BG name; empty on a single realm
ServerPacket::NameQueryResponse {
guid,
name,
race: read_u32_le(&mut r)?,
gender: read_u32_le(&mut r)?,
class: read_u32_le(&mut r)?,
}
}
opcode::SMSG_CREATURE_QUERY_RESPONSE => {
let entry = read_u32_le(&mut r)?;
// A miss is the lone entry echoed with its top bit set — nothing follows.
if entry & 0x8000_0000 != 0 {
ServerPacket::CreatureQueryResponse {
entry: entry & 0x7FFF_FFFF,
info: None,
}
} else {
let name = read_cstring(&mut r)?;
for _ in 0..3 {
let _ = read_cstring(&mut r)?; // name2..name4, always empty in 5875
}
let subname = read_cstring(&mut r)?;
// The tail (VERIFIED vmangos `HandleCreatureQueryOpcode`, 5875 = every build
// guard included): type_flags, type, pet_family, RANK, unk, pet_spell_list_id,
// display_id (u32 ×7), then civilian + racial_leader (u8 ×2). We keep `type`
// (the `CreatureType.dbc` id — the TAB-target filter's input), `pet_family` (the
// `CreatureFamily.dbc` id behind `UnitCreatureFamily` and the diet tooltip,
// decision 1062), `rank` (the unit tooltip's Elite/Boss word, decision 0276's
// level-line law), `type_flags` (bit 0x10 hides its faction line), the
// `civilian`/`racial_leader` pair (its green CIVILIAN / white LEADER lines), and
// `display_id` — the template's model, which is the ONLY way to draw a creature
// that has no world object to read `UNIT_FIELD_DISPLAYID` off: a stabled pet
// (decision 1676). The `unk`/`pet_spell_list_id` pair stays alignment-only.
let type_flags = read_u32_le(&mut r)?;
let creature_type = read_u32_le(&mut r)?;
let pet_family = read_u32_le(&mut r)?;
let rank = read_u32_le(&mut r)?;
let _unk = read_u32_le(&mut r)?;
let _pet_spell_list = read_u32_le(&mut r)?;
let display_id = read_u32_le(&mut r)?;
let civilian = read_u8(&mut r)? != 0;
let racial_leader = read_u8(&mut r)? != 0;
ServerPacket::CreatureQueryResponse {
entry,
info: Some(CreatureQueryInfo {
name,
subname,
creature_type,
pet_family,
rank,
type_flags,
display_id,
civilian,
racial_leader,
}),
}
}
}
// `u32 petNumber`, cstring name, `u32 nameTimestamp` (VERIFIED vmangos
// `PetNameQueryResponse::AppendBodyTo`, `Server/Packets/Pet.cpp:79-84`). The timestamp ages
// out the reference's on-disk pet-name cache; we keep none, so it is alignment-only.
opcode::SMSG_PET_NAME_QUERY_RESPONSE => {
let pet_number = read_u32_le(&mut r)?;
let name = read_cstring(&mut r)?;
let _name_timestamp = read_u32_le(&mut r)?;
ServerPacket::PetNameQueryResponse { pet_number, name }
}
opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE => {
let (entry, info) = gameobject::read_gameobject_query_response(&mut r)?;
ServerPacket::GameObjectQueryResponse { entry, info }
}
opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE => {
let (page_id, text, next_page_id) = page_text::read_page_text_query_response(&mut r)?;
ServerPacket::PageTextQueryResponse {
page_id,
text,
next_page_id,
}
}
opcode::SMSG_GAMEOBJECT_CUSTOM_ANIM => {
let (guid, anim_id) = gameobject::read_gameobject_custom_anim(&mut r)?;
ServerPacket::GameObjectCustomAnim { guid, anim_id }
}
opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM => {
let guid = gameobject::read_gameobject_despawn_anim(&mut r)?;
ServerPacket::GameObjectDespawnAnim { guid }
}
// Both fishing verdicts are empty-bodied (the opcode IS the message).
opcode::SMSG_FISH_NOT_HOOKED => ServerPacket::FishNotHooked,
opcode::SMSG_FISH_ESCAPED => ServerPacket::FishEscaped,
opcode::SMSG_LOGOUT_COMPLETE => ServerPacket::LogoutComplete,
// `{u32 reason, u8 instant}` — the `instant` byte is what the CAMP/QUIT countdown hangs
// on (see the variant's own doc); it used to be dropped on the floor here.
opcode::SMSG_LOGOUT_RESPONSE => ServerPacket::LogoutResponse {
reason: read_u32_le(&mut r)?,
instant: read_u8(&mut r)? != 0,
},
opcode::SMSG_LOGOUT_CANCEL_ACK => ServerPacket::LogoutCancelAck,
// The keepalive echo: our CMSG_PING's sequence number back (vmangos `_HandlePing` — the
// body is the one u32 it read from us).
opcode::SMSG_PONG => ServerPacket::Pong {
sequence: read_u32_le(&mut r)?,
},
// The force-speed-change family — one arm per kind, one shared body reader (the wire shape
// and the mandatory ack protocol are documented on the opcode block).
opcode::SMSG_FORCE_WALK_SPEED_CHANGE => read_force_speed(SpeedKind::Walk, &mut r)?,
opcode::SMSG_FORCE_RUN_SPEED_CHANGE => read_force_speed(SpeedKind::Run, &mut r)?,
opcode::SMSG_FORCE_RUN_BACK_SPEED_CHANGE => read_force_speed(SpeedKind::RunBack, &mut r)?,
opcode::SMSG_FORCE_SWIM_SPEED_CHANGE => read_force_speed(SpeedKind::Swim, &mut r)?,
opcode::SMSG_FORCE_SWIM_BACK_SPEED_CHANGE => read_force_speed(SpeedKind::SwimBack, &mut r)?,
opcode::SMSG_FORCE_TURN_RATE_CHANGE => read_force_speed(SpeedKind::TurnRate, &mut r)?,
// The group/party family (see the opcode block's note; bodies in `group`).
opcode::SMSG_GROUP_INVITE => ServerPacket::GroupInvite {
inviter: group::read_group_invite(&mut r)?,
},
opcode::SMSG_GROUP_DECLINE => ServerPacket::GroupDecline {
name: group::read_group_decline(&mut r)?,
},
opcode::SMSG_GROUP_UNINVITE => ServerPacket::GroupUninvited,
opcode::SMSG_GROUP_SET_LEADER => ServerPacket::GroupLeaderChanged {
name: group::read_group_set_leader(&mut r)?,
},
opcode::SMSG_GROUP_DESTROYED => ServerPacket::GroupDestroyed,
opcode::SMSG_GROUP_LIST => {
let (group_type, own_flags, members, leader, loot) = group::read_group_list(&mut r)?;
ServerPacket::GroupList {
group_type,
own_flags,
members,
leader,
loot,
}
}
opcode::SMSG_PARTY_COMMAND_RESULT => {
let (operation, member, result) = group::read_party_command_result(&mut r)?;
ServerPacket::PartyCommandResult {
operation,
member,
result,
}
}
opcode::SMSG_PARTY_MEMBER_STATS | opcode::SMSG_PARTY_MEMBER_STATS_FULL => {
let (guid, info) = group::read_party_member_stats(&mut r)?;
ServerPacket::PartyMemberStats {
guid,
full: opcode == opcode::SMSG_PARTY_MEMBER_STATS_FULL,
info: Box::new(info),
}
}
opcode::MSG_MINIMAP_PING => {
let (guid, x, y) = group::read_minimap_ping(&mut r)?;
ServerPacket::MinimapPing { guid, x, y }
}
opcode::MSG_RAID_TARGET_UPDATE => match group::read_raid_target_update(&mut r)? {
group::RaidTargetUpdate::Delta { icon, guid } => {
ServerPacket::RaidTargetSet { icon, guid }
}
group::RaidTargetUpdate::List(entries) => ServerPacket::RaidTargetList { entries },
},
opcode::SMSG_RAID_INSTANCE_INFO => ServerPacket::RaidInstanceInfo {
entries: group::read_raid_instance_info(&mut r)?,
},
opcode::MSG_RAID_READY_CHECK => match group::read_ready_check(&mut r)? {
group::ReadyCheck::Started => ServerPacket::ReadyCheckRequest,
group::ReadyCheck::Answer { guid, ready } => {
ServerPacket::ReadyCheckAnswer { guid, ready }
}
},
// The instance/raid lockout family (decision 1748; bodies in `instance`). Every one of
// these is read here in the client's own field order.
opcode::SMSG_RAID_INSTANCE_MESSAGE => ServerPacket::RaidInstanceMessage {
message: instance::read_raid_instance_message(&mut r)?,
},
opcode::SMSG_INSTANCE_SAVE_CREATED => ServerPacket::InstanceSaveCreated {
flag: instance::read_u32_body(&mut r)?,
},
opcode::SMSG_INSTANCE_RESET => ServerPacket::InstanceReset {
map: instance::read_u32_body(&mut r)?,
},
opcode::SMSG_INSTANCE_RESET_FAILED => ServerPacket::InstanceResetFailed {
failure: instance::read_instance_reset_failed(&mut r)?,
},
opcode::SMSG_UPDATE_LAST_INSTANCE => ServerPacket::UpdateLastInstance {
map: instance::read_u32_body(&mut r)?,
},
opcode::SMSG_UPDATE_INSTANCE_OWNERSHIP => ServerPacket::UpdateInstanceOwnership {
owns: instance::read_u32_body(&mut r)?,
},
// The duel family (decision 0633; bodies in `duel`). Both empty-body arms carry their
// whole meaning in the opcode.
opcode::SMSG_DUEL_REQUESTED => {
let req = duel::read_duel_requested(&mut r)?;
ServerPacket::DuelRequested {
arbiter: req.arbiter,
challenger: req.challenger,
}
}
opcode::SMSG_DUEL_OUTOFBOUNDS => ServerPacket::DuelOutOfBounds,
opcode::SMSG_DUEL_INBOUNDS => ServerPacket::DuelInBounds,
opcode::SMSG_DUEL_COMPLETE => ServerPacket::DuelComplete {
started: duel::read_duel_complete(&mut r)?,
},
opcode::SMSG_DUEL_WINNER => {
let w = duel::read_duel_winner(&mut r)?;
ServerPacket::DuelWinner {
fled: w.fled,
winner: w.winner,
loser: w.loser,
}
}
opcode::SMSG_DUEL_COUNTDOWN => ServerPacket::DuelCountdown {
seconds: duel::read_duel_countdown(&mut r)?,
},
// The honor pair (decision 1512; bodies in `pvp`). MSG_INSPECT_HONOR_STATS is an `MSG_`:
// opcode 0x2D6 carries our 8-byte request AND the server's 50-byte reply. There is no
// ambiguity to resolve here — `parse_server` is fed only by the world *reader*, so an
// 0x2D6 body arriving at this function is always the reply. The request shape is built by
// `pvp::inspect_honor_stats` and goes straight out through `world::writer::pvp`; it never
// reaches this match.
opcode::MSG_INSPECT_HONOR_STATS => {
ServerPacket::InspectHonorStats(pvp::read_inspect_honor_stats(&mut r)?)
}
opcode::SMSG_PVP_CREDIT => ServerPacket::PvpCredit(pvp::read_pvp_credit(&mut r)?),
// The mirror timers (decision 0874; bodies in `mirror_timer`). START carries the timer's
// whole state and is re-sent on every change — there is no update opcode in the family.
opcode::SMSG_START_MIRROR_TIMER => {
ServerPacket::MirrorTimerStart(mirror_timer::read_start_mirror_timer(&mut r)?)
}
opcode::SMSG_PAUSE_MIRROR_TIMER => {
let (kind, paused) = mirror_timer::read_pause_mirror_timer(&mut r)?;
ServerPacket::MirrorTimerPause { kind, paused }
}
opcode::SMSG_STOP_MIRROR_TIMER => ServerPacket::MirrorTimerStop {
kind: mirror_timer::read_stop_mirror_timer(&mut r)?,
},
// The social family (decision 0668; bodies in `social`). Both list packets are
// replace-everything snapshots; the status packet is one result about one player.
opcode::SMSG_FRIEND_LIST => ServerPacket::FriendList {
friends: social::read_friend_list(&mut r)?,
},
opcode::SMSG_IGNORE_LIST => ServerPacket::IgnoreList {
guids: social::read_ignore_list(&mut r)?,
},
opcode::SMSG_FRIEND_STATUS => {
ServerPacket::FriendStatus(social::read_friend_status(&mut r)?)
}
opcode::SMSG_WHO => ServerPacket::WhoResults(social::read_who(&mut r)?),
// The guild family (bodies in `guild`). The two big ones are caches — a query response
// fills the guild-id→identity cache, the roster replaces the whole membership — and the
// three small ones are notifications. `SMSG_GUILD_ROSTER`'s member loop carries the
// family's one conditional field; see `guild::read_guild_roster`.
opcode::SMSG_GUILD_QUERY_RESPONSE => {
ServerPacket::GuildQueryResponse(guild::read_guild_query_response(&mut r)?)
}
opcode::SMSG_GUILD_ROSTER => ServerPacket::GuildRoster(guild::read_guild_roster(&mut r)?),
opcode::SMSG_GUILD_EVENT => ServerPacket::GuildEvent(guild::read_guild_event(&mut r)?),
opcode::SMSG_GUILD_COMMAND_RESULT => {
ServerPacket::GuildCommandResult(guild::read_guild_command_result(&mut r)?)
}
opcode::SMSG_GUILD_INVITE => {
let (inviter, guild) = guild::read_guild_invite(&mut r)?;
ServerPacket::GuildInvite { inviter, guild }
}
opcode::SMSG_GUILD_DECLINE => ServerPacket::GuildDecline {
name: guild::read_guild_decline(&mut r)?,
},
opcode::SMSG_GUILD_INFO => ServerPacket::GuildInfo(guild::read_guild_info(&mut r)?),
// The petition family (bodies in `petition`) — founding a guild, which at 1.12 is an
// entirely different wire from the guild family above. Note `SMSG_PETITION_SHOW_SIGNATURES`
// answers two different asks, and the two `MSG_` opcodes read a *different* body from the
// one they write (see `petition`'s own docs).
opcode::SMSG_PETITION_SHOWLIST => {
ServerPacket::PetitionShowList(petition::read_petition_show_list(&mut r)?)
}
opcode::SMSG_PETITION_SHOW_SIGNATURES => {
ServerPacket::PetitionShowSignatures(petition::read_petition_show_signatures(&mut r)?)
}
opcode::SMSG_PETITION_SIGN_RESULTS => {
ServerPacket::PetitionSignResults(petition::read_petition_sign_results(&mut r)?)
}
opcode::SMSG_PETITION_QUERY_RESPONSE => {
ServerPacket::PetitionQueryResponse(petition::read_petition_query_response(&mut r)?)
}
opcode::SMSG_TURN_IN_PETITION_RESULTS => ServerPacket::TurnInPetitionResults {
result: petition::read_turn_in_petition_results(&mut r)?,
},
opcode::MSG_PETITION_DECLINE => ServerPacket::PetitionDeclined {
player: petition::read_petition_decline(&mut r)?,
},
opcode::MSG_PETITION_RENAME => {
ServerPacket::PetitionRenamed(petition::read_petition_rename(&mut r)?)
}
// The observer speed legs (decision 0441): a unit we don't control changed speed — a
// creature or mid-spline player (SPLINE family), or a freely-moving player (MOVE_SET
// family, which carries a fresh pose too). No ack on either.
opcode::SMSG_SPLINE_SET_WALK_SPEED => read_spline_speed(SpeedKind::Walk, &mut r)?,
opcode::SMSG_SPLINE_SET_RUN_SPEED => read_spline_speed(SpeedKind::Run, &mut r)?,
opcode::SMSG_SPLINE_SET_RUN_BACK_SPEED => read_spline_speed(SpeedKind::RunBack, &mut r)?,
opcode::SMSG_SPLINE_SET_SWIM_SPEED => read_spline_speed(SpeedKind::Swim, &mut r)?,
opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED => read_spline_speed(SpeedKind::SwimBack, &mut r)?,
opcode::SMSG_SPLINE_SET_TURN_RATE => read_spline_speed(SpeedKind::TurnRate, &mut r)?,
opcode::MSG_MOVE_SET_WALK_SPEED => read_move_set_speed(SpeedKind::Walk, &mut r)?,
opcode::MSG_MOVE_SET_RUN_SPEED => read_move_set_speed(SpeedKind::Run, &mut r)?,
opcode::MSG_MOVE_SET_RUN_BACK_SPEED => read_move_set_speed(SpeedKind::RunBack, &mut r)?,
opcode::MSG_MOVE_SET_SWIM_SPEED => read_move_set_speed(SpeedKind::Swim, &mut r)?,
opcode::MSG_MOVE_SET_SWIM_BACK_SPEED => read_move_set_speed(SpeedKind::SwimBack, &mut r)?,
opcode::MSG_MOVE_SET_TURN_RATE => read_move_set_speed(SpeedKind::TurnRate, &mut r)?,
// Mount feedback (decision 0441): one u32 result code per attempt; error lines are a P2
// trimming — decoded so the wire is modelled, surfaced for the debug log.
opcode::SMSG_MOUNTRESULT => ServerPacket::MountResult {
mount: true,
code: read_u32_le(&mut r)?,
},
opcode::SMSG_DISMOUNTRESULT => ServerPacket::MountResult {
mount: false,
code: read_u32_le(&mut r)?,
},
// A nearby rider's flourish: one raw u64 guid (VERIFIED vmangos
// `HandleMountSpecialAnimOpcode` — `data << GetObjectGuid()`, full 8 bytes, not packed).
opcode::SMSG_MOUNTSPECIAL_ANIM => ServerPacket::MountSpecialAnim {
guid: read_u64_le(&mut r)?,
},
// The control handoff: PACKED guid (unlike the flourish above) + one byte (VERIFIED
// vmangos `Server/Packets/Misc.cpp:677-682` — `WriteAsPacked()`, then `uint8 allowMove`).
opcode::SMSG_CLIENT_CONTROL_UPDATE => ServerPacket::ClientControlUpdate {
mover: read_packed_guid(&mut r)?,
allow_move: read_u8(&mut r)? != 0,
},
// The taxi/flight-master family (decision 0484): the map (SHOWTAXINODES), a status
// answer/first-visit learn (TAXINODE_STATUS), the activate verdict, and the multi-hop
// path ack (NEW_TAXI_PATH, empty body).
opcode::SMSG_SHOWTAXINODES => {
let (window, flightmaster, nearest_node, known) = taxi::read_show_taxi_nodes(&mut r)?;
ServerPacket::ShowTaxiNodes {
window,
flightmaster,
nearest_node,
known,
}
}
opcode::SMSG_TAXINODE_STATUS => {
let (guid, known) = taxi::read_taxi_node_status(&mut r)?;
ServerPacket::TaxiNodeStatus {
guid,
known: known != 0,
}
}
opcode::SMSG_ACTIVATETAXIREPLY => ServerPacket::ActivateTaxiReply {
code: taxi::read_activate_taxi_reply(&mut r)?,
},
opcode::SMSG_NEW_TAXI_PATH => ServerPacket::NewTaxiPath,
// The mail arc (decision 0544 P0): the inbox list, the send/take/return/delete verdict,
// the letter-body ask-once fetch, and the arrival pair (RECEIVED_MAIL + the
// QUERY_NEXT_MAIL_TIME reply — same opcode as our empty-body request).
opcode::SMSG_MAIL_LIST_RESULT => ServerPacket::MailList {
mails: mail::read_mail_list_result(&mut r)?,
},
opcode::SMSG_SEND_MAIL_RESULT => {
let (mail_id, action, error, equip_error, item) = mail::read_send_mail_result(&mut r)?;
ServerPacket::SendMailResult {
mail_id,
action,
error,
equip_error,
item,
}
}
opcode::SMSG_ITEM_TEXT_QUERY_RESPONSE => {
let (text_id, text) = mail::read_item_text_query_response(&mut r)?;
ServerPacket::ItemTextQueryResponse { text_id, text }
}
opcode::SMSG_RECEIVED_MAIL => ServerPacket::ReceivedMail {
seconds: mail::read_received_mail(&mut r)?,
},
opcode::MSG_QUERY_NEXT_MAIL_TIME => ServerPacket::NextMailTime {
seconds: mail::read_query_next_mail_time(&mut r)?,
},
// The auction house arc (decision 1511 P0). MSG_AUCTION_HELLO is two-way — this is the
// REPLY (our request is the same opcode with just the guid), and it is what opens the
// window. The three list results share one frame and one 64-byte record; their reader is
// bounded by the buffer as well as by the wire `count`, because vmangos's browse fast path
// can count a record it then fails to write.
opcode::MSG_AUCTION_HELLO => {
let (auctioneer, house_id) = auction::read_auction_hello(&mut r)?;
ServerPacket::AuctionHello {
auctioneer,
house_id,
}
}
opcode::SMSG_AUCTION_COMMAND_RESULT => {
let (auction_id, action, error, tail) = auction::read_auction_command_result(&mut r)?;
ServerPacket::AuctionCommandResult {
auction_id,
action,
error,
tail,
}
}
opcode::SMSG_AUCTION_LIST_RESULT => {
let (auctions, total_count) = auction::read_auction_list_result(&mut r)?;
ServerPacket::AuctionListResult {
auctions,
total_count,
}
}
opcode::SMSG_AUCTION_OWNER_LIST_RESULT => {
let (auctions, total_count) = auction::read_auction_list_result(&mut r)?;
ServerPacket::AuctionOwnerListResult {
auctions,
total_count,
}
}
opcode::SMSG_AUCTION_BIDDER_LIST_RESULT => {
let (auctions, total_count) = auction::read_auction_list_result(&mut r)?;
ServerPacket::AuctionBidderListResult {
auctions,
total_count,
}
}
// The two notifications carry different field orders and the owner one has no house id —
// two readers, never one.
opcode::SMSG_AUCTION_BIDDER_NOTIFICATION => ServerPacket::AuctionBidderNotification(
auction::read_auction_bidder_notification(&mut r)?,
),
opcode::SMSG_AUCTION_OWNER_NOTIFICATION => ServerPacket::AuctionOwnerNotification(
auction::read_auction_owner_notification(&mut r)?,
),
opcode::SMSG_AUCTION_REMOVED_NOTIFICATION => {
let (auction_id, item_entry, random_property_id) =
auction::read_auction_removed_notification(&mut r)?;
ServerPacket::AuctionRemovedNotification {
auction_id,
item_entry,
random_property_id,
}
}
// The player-trade arc (decision 0592 P0): the state-machine status pulse and the
// item/gold snapshot for one window side.
opcode::SMSG_TRADE_STATUS => ServerPacket::TradeStatus {
status: trade::read_trade_status(&mut r)?,
},
opcode::SMSG_TRADE_STATUS_EXTENDED => ServerPacket::TradeStatusExtended {
state: Box::new(trade::read_trade_status_extended(&mut r)?),
},
// The world-state table — the `$<n>w`/`$<n>e` NPC-text tokens' source (and a battleground
// scoreboard's, later). Both opcodes write the one table; see [`super::world_state`].
opcode::SMSG_INIT_WORLD_STATES => {
ServerPacket::InitWorldStates(world_state::read_init_world_states(&mut r)?)
}
opcode::SMSG_UPDATE_WORLD_STATE => {
let (id, value) = world_state::read_update_world_state(&mut r)?;
ServerPacket::UpdateWorldState { id, value }
}
// A relayed player movement packet: `[packed mover guid][MovementInfo]`, same opcode the mover
// sent. This is how other players' walking/turning/strafing reaches us (creatures use
// SMSG_MONSTER_MOVE instead). Surface the pose + live move-flags; the app extrapolates between.
o if is_movement_relay(o) => {
let guid = read_packed_guid(&mut r)?;
let info = movement::read_movement_info(&mut r)?;
ServerPacket::PlayerMove {
guid,
opcode: o,
flags: info.flags,
position: info.position,
orientation: info.orientation,
pitch: info.pitch,
time: info.timestamp,
fall_time: info.fall_time,
jump: info.jump,
transport: info.transport,
}
}
opcode::SMSG_ADDON_INFO => ServerPacket::AddonInfo {
statuses: read_addon_info(&mut r),
},
other => ServerPacket::Other { opcode: other },
})
}