- 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>
1658 lines
78 KiB
Rust
1658 lines
78 KiB
Rust
//! 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 },
|
||
})
|
||
}
|