comments: protocol, the server packet enum and its parser
1,082 comment lines become 487; the code is unchanged. The creature query doc said the display id was dropped; CreatureQueryInfo keeps it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,15 @@
|
||||
//! The **page-text** query pair (decision 1105) — the ask-once cache behind every readable *book*.
|
||||
//!
|
||||
//! Distinct from the mail letter's `CMSG_ITEM_TEXT_QUERY` ([`super::mail`]), which fetches one
|
||||
//! whole player-written body by an *instance* text id. A `PageText.wdb` id names a single **page**,
|
||||
//! and pages **chain**: each answer carries the next page's id (`0` = last). Two things read from
|
||||
//! this cache, and both reach it the same way — the client opens its reader on an object guid and
|
||||
//! asks the object for its page id:
|
||||
//!
|
||||
//! - a readable **item template** (`ItemPrototype::PageText`) — a book in your bags;
|
||||
//! - a **`GAMEOBJECT_TYPE_TEXT`** world object (its template `data[0]`) — a book, plaque or sign
|
||||
//! lying in the world.
|
||||
//!
|
||||
//! The client's request carries the asking object's guid after the page id (VERIFIED: the cache's
|
||||
//! request writer `0x564730` appends the 8-byte guid at `0x56485d` under the per-cache "requests
|
||||
//! carry a guid" flag `[cache+0x38]` — the very address vmangos cites in
|
||||
//! `QueryPageText::ReadFromWorldPacket` when it reads the tail as optional). vmangos resolves the
|
||||
//! page from the id alone and skips the guid.
|
||||
//! The page-text query pair, the ask-once cache behind every readable book: a `PageText.wdb` id
|
||||
//! names one page, and each answer carries the next page's id (`0` = last). The first id comes
|
||||
//! from an item template's `PageText` or a `GAMEOBJECT_TYPE_TEXT` object's `data[0]`; mail bodies
|
||||
//! use `CMSG_ITEM_TEXT_QUERY` instead.
|
||||
|
||||
use std::io;
|
||||
|
||||
use crate::wire::{read_cstring, read_u32_le};
|
||||
|
||||
/// Body of `CMSG_PAGE_TEXT_QUERY` (opcode `0x005A`/90 — VERIFIED vmangos
|
||||
/// `Server/Protocol/Opcodes_1_12_1.h`): `u32 pageId` then the asking object's `u64 guid` (VERIFIED
|
||||
/// `QueryPageText::ReadFromWorldPacket`, which reads the guid only `if (rpos() < size())` and
|
||||
/// discards it; the real client writes it — see the module doc).
|
||||
/// Body of `CMSG_PAGE_TEXT_QUERY` (`0x005A`): `u32 pageId`, then the asking object's `u64 guid`,
|
||||
/// which the 1.12 client appends (`0x564730`, at `0x56485d`) and vmangos reads only if present,
|
||||
/// then discards (`QueryPageText::ReadFromWorldPacket`).
|
||||
pub fn page_text_query(page_id: u32, guid: u64) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(12);
|
||||
body.extend_from_slice(&page_id.to_le_bytes());
|
||||
@@ -31,13 +17,10 @@ pub fn page_text_query(page_id: u32, guid: u64) -> Vec<u8> {
|
||||
body
|
||||
}
|
||||
|
||||
/// Read `SMSG_PAGE_TEXT_QUERY_RESPONSE` (VERIFIED vmangos `Server/Packets/Query.cpp`,
|
||||
/// `PageTextQueryResponse::AppendBodyTo`): `u32 pageId, cstr text, u32 nextPageId`.
|
||||
///
|
||||
/// vmangos answers **one query with the whole chain** — `HandlePageTextQueryOpcode` loops on
|
||||
/// `next_page`, sending one of these per page — so asking for page 1 lands every page of the book
|
||||
/// unsolicited. A page the server doesn't know answers `"Item page missing."` with `nextPageId = 0`,
|
||||
/// which terminates the chain rather than erroring.
|
||||
/// Read `SMSG_PAGE_TEXT_QUERY_RESPONSE`: `u32 pageId, cstr text, u32 nextPageId`
|
||||
/// (`PageTextQueryResponse::AppendBodyTo`). vmangos answers one query with the whole chain, one
|
||||
/// packet per page (`HandlePageTextQueryOpcode`), and an unknown page with `"Item page missing."`
|
||||
/// and `nextPageId = 0`.
|
||||
pub(super) fn read_page_text_query_response(r: &mut &[u8]) -> io::Result<(u32, String, u32)> {
|
||||
Ok((read_u32_le(r)?, read_cstring(r)?, read_u32_le(r)?))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
//! 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.
|
||||
//! The opcode to [`ServerPacket`] dispatch, one match arm per opcode.
|
||||
|
||||
use std::io::{self, Read};
|
||||
|
||||
@@ -19,9 +16,8 @@ use super::{
|
||||
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.
|
||||
/// Read a `SMSG_FORCE_*_SPEED_CHANGE` body: `[packed guid][u32 counter][f32 speed]` for all six
|
||||
/// kinds (vmangos `SendSpeedChangeToController`, the `> 1_9_4` branch).
|
||||
fn read_force_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
|
||||
Ok(ServerPacket::ForceSpeedChange {
|
||||
guid: read_packed_guid(r)?,
|
||||
@@ -31,30 +27,21 @@ fn read_force_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPack
|
||||
})
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
/// Read an `SMSG_COMPRESSED_MOVES` body: `u32` uncompressed size, then deflated
|
||||
/// `[u8 size][u16 opcode][body]` records, `size` counting the opcode (`MovementData::AddPacket`).
|
||||
/// Each record goes back through [`parse_server`], and one bad record fails the whole batch; only
|
||||
/// the `MSG_MOVE_*` relays can appear (vmangos `ObjectViewersMovementDeliverer`).
|
||||
fn read_compressed_moves(r: &mut &[u8]) -> io::Result<Vec<ServerPacket>> {
|
||||
let uncompressed = read_u32_le(r)? as usize;
|
||||
let mut buf = Vec::with_capacity(uncompressed.min(64 * 1024));
|
||||
// `bufread`, not `read`: see the compressed update object's arm — the cursor must stop at
|
||||
// the stream's end for the tail to see what follows it.
|
||||
// `bufread`, not `read`: the cursor must stop at the stream's end for the tail to be seen.
|
||||
flate2::bufread::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.
|
||||
// `size` spans opcode and body, so under 2 means the stream is misframed.
|
||||
let body_len = size.checked_sub(2).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -92,9 +79,8 @@ fn read_compressed_moves(r: &mut &[u8]) -> io::Result<Vec<ServerPacket>> {
|
||||
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).
|
||||
/// Read a `SMSG_SPLINE_SET_*_SPEED` body: `[packed guid][f32 speed]`, no counter, no ack
|
||||
/// (vmangos `SendSpeedChangeToAll`, the `> 1_8_4` layout).
|
||||
fn read_spline_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
|
||||
Ok(ServerPacket::SplineSpeedChange {
|
||||
guid: read_packed_guid(r)?,
|
||||
@@ -103,9 +89,8 @@ fn read_spline_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPac
|
||||
})
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Read a `MSG_MOVE_SET_*_SPEED` body: `[packed guid][MovementInfo][f32 speed]`, no ack
|
||||
/// (vmangos `SendSpeedChangeToObservers`).
|
||||
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)?;
|
||||
@@ -124,21 +109,10 @@ fn read_move_set_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerP
|
||||
})
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// True for an opcode the server relays as `[packed guid][MovementInfo]`, one client handler
|
||||
/// (`0x603bb0`): the echoed input stream (vmangos `HandleMovementOpcodes`), and the observer leg
|
||||
/// of root, hover, feather-fall, water-walk and teleport, with no ack and its direction in the
|
||||
/// flags word. `MSG_MOVE_TELEPORT` (197) has no counter, unlike `MSG_MOVE_TELEPORT_ACK` (199).
|
||||
const fn is_movement_relay(o: u16) -> bool {
|
||||
matches!(
|
||||
o,
|
||||
@@ -163,15 +137,10 @@ const fn is_movement_relay(o: u16) -> bool {
|
||||
| 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.
|
||||
// Another unit's knockback: the four trailing launch floats go unread, since the
|
||||
// `MovementInfo` comes from the victim's ack and its jump tail already is that quad.
|
||||
| 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.
|
||||
// Another unit's mode change: the mode rides the `MovementInfo` flags word.
|
||||
| opcode::MSG_MOVE_ROOT
|
||||
| opcode::MSG_MOVE_UNROOT
|
||||
| opcode::MSG_MOVE_HOVER
|
||||
@@ -181,11 +150,7 @@ const fn is_movement_relay(o: u16) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// 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):
|
||||
/// `SMSG_ADDON_INFO`'s records, with no count, names or trailer, as `0x51da70` reads each:
|
||||
///
|
||||
/// ```text
|
||||
/// u8 status ; 2 -> [rec+0x29] = 1 (hidden from the Lua index space)
|
||||
@@ -200,13 +165,8 @@ const fn is_movement_relay(o: u16) -> bool {
|
||||
/// 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.
|
||||
/// Retail sends the minimal form, 12 records of `{2, 1, 0, 0u32, 0}`. A truncated record ends the
|
||||
/// walk and keeps the whole records before it, so `statuses[i]` is always record i's.
|
||||
fn read_addon_info(r: &mut &[u8]) -> Vec<u8> {
|
||||
let mut statuses = Vec::new();
|
||||
while !r.is_empty() {
|
||||
@@ -230,31 +190,21 @@ fn read_addon_info(r: &mut &[u8]) -> Vec<u8> {
|
||||
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..])
|
||||
}
|
||||
|
||||
/// Decode one server packet body into its [`ServerPacket`]. The body is the whole length-framed
|
||||
/// packet, so a decoder that reads fewer bytes than the server wrote succeeds all the same; that
|
||||
/// gap is what [`parse_server_with_tail`] reports, and this is its `(packet, _)` projection.
|
||||
/// Decode one server packet body into its [`ServerPacket`], ignoring any unread tail that
|
||||
/// [`parse_server_with_tail`] would report.
|
||||
pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
parse_server_with_tail(opcode, body).map(|(packet, _)| packet)
|
||||
}
|
||||
|
||||
/// [`parse_server`], also reporting the **tail**: how many of `body`'s bytes the decoder left
|
||||
/// unconsumed. A body is length-framed, so a decoder shorter than the server's layout does not
|
||||
/// fail — it silently reads a prefix, and the next field the server added is invisible from
|
||||
/// outside. A non-zero tail is the instrument for that drift (decision 2265 §B1): the reader
|
||||
/// surfaces it on [`crate::Poll::Events`], the app announces it once per opcode. It is a count,
|
||||
/// never a failure — a working decoder with a trailing field it has no use for stays working.
|
||||
/// An unknown opcode ([`ServerPacket::Other`]) consumes nothing by definition and reports `0`,
|
||||
/// so the tally does not double-count what the dropped-packet tally already names.
|
||||
/// [`parse_server`], plus how many body bytes the decoder left unread: a count, never a failure,
|
||||
/// that exposes a decoder shorter than the server's layout. [`ServerPacket::Other`] reports `0`.
|
||||
pub fn parse_server_with_tail(opcode: u16, body: &[u8]) -> io::Result<(ServerPacket, usize)> {
|
||||
let mut r = body;
|
||||
// `inner_tail` is the one arm that decodes through a second stream (the compressed update
|
||||
// object): the outer cursor ends at the zlib stream's end, and what the INFLATED bytes left
|
||||
// unread is that packet's tail.
|
||||
// The inflated leftover of the compressed update object, the one arm with a second stream.
|
||||
let mut inner_tail = 0;
|
||||
let packet = parse_server_body(opcode, &mut r, &mut inner_tail)?;
|
||||
let tail = match packet {
|
||||
@@ -264,9 +214,7 @@ pub fn parse_server_with_tail(opcode: u16, body: &[u8]) -> io::Result<(ServerPac
|
||||
Ok((packet, tail))
|
||||
}
|
||||
|
||||
/// The opcode → variant dispatch over a slice cursor. `cursor` is advanced past what the arm read
|
||||
/// **only on success** (an error's partial read is nobody's business); the arms themselves keep
|
||||
/// the plain `&[u8]` local every helper is typed over.
|
||||
/// The opcode dispatch; `cursor` advances past what the arm read only on success.
|
||||
fn parse_server_body(
|
||||
opcode: u16,
|
||||
cursor: &mut &[u8],
|
||||
@@ -278,40 +226,18 @@ fn parse_server_body(
|
||||
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.
|
||||
// The client's grammar (`0x5b41b0`): `u8 code`; then, for OK or WAIT_QUEUE with 5+
|
||||
// bytes left, `u32 billingTimeRemaining, u8 billingPlanFlags, u32 billingTimeRested`;
|
||||
// then, for WAIT_QUEUE, `u32 position`. The 5 guarding a 9-byte group is the client's
|
||||
// and mis-parses a 6..=9 byte body; matched on purpose. Deviation: a body too short
|
||||
// for the position gives `None`, because the client redisplays a stale position.
|
||||
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.
|
||||
// The only source of `GetBillingTimeRested()` (the client's writer is `0x5b421a`).
|
||||
billing_time_rested = Some(read_u32_le(&mut r)?);
|
||||
}
|
||||
ServerPacket::AuthResponse {
|
||||
@@ -322,8 +248,7 @@ fn parse_server_body(
|
||||
}
|
||||
opcode::SMSG_CHAR_ENUM => {
|
||||
let count = read_u8(&mut r)?;
|
||||
// Hint bounded by the realm's character cap: vmangos clamps `CharactersPerRealm` to
|
||||
// 10 (`World.cpp:629`, `setConfigMinMax(…, 10, 1, 10)`).
|
||||
// vmangos clamps `CharactersPerRealm` to 10 (`World.cpp:629`).
|
||||
let mut characters = Vec::with_capacity(capacity_hint(count, 10));
|
||||
for _ in 0..count {
|
||||
characters.push(Character::read(&mut r)?);
|
||||
@@ -350,16 +275,8 @@ fn parse_server_body(
|
||||
},
|
||||
opcode::SMSG_COMPRESSED_UPDATE_OBJECT => {
|
||||
let _decompressed_size = read_u32_le(&mut r)?;
|
||||
// Through `&mut r`, so the cursor advances over the compressed bytes. Handed `r` by
|
||||
// value the decoder reads a COPY of the slice, and the whole zlib body then reads
|
||||
// as unconsumed — 803 bytes on the decode-length instrument's first live login
|
||||
// (2266 §B1). The tail that means drift here is the inflated stream's, reported
|
||||
// through `inner_tail`.
|
||||
//
|
||||
// And through `bufread`, not `read` (2343): `read::ZlibDecoder` wraps its source in a
|
||||
// 32 KiB buffer and fills it from the slice, so the outer cursor always ended at the
|
||||
// BODY's end and a byte after the zlib stream could never show as a tail. The
|
||||
// `bufread` decoder reads the slice as its own buffer and consumes exactly the stream.
|
||||
// `&mut r` so the cursor advances over the zlib bytes (by value it reads a copy), and
|
||||
// `bufread`, not `read`, whose 32 KiB buffer would swallow bytes after the stream.
|
||||
let mut decoder = flate2::bufread::ZlibDecoder::new(&mut r);
|
||||
let mut decompressed = Vec::new();
|
||||
decoder.read_to_end(&mut decompressed)?;
|
||||
@@ -410,10 +327,8 @@ fn parse_server_body(
|
||||
}
|
||||
}
|
||||
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).
|
||||
// `u32 newMapId`, then `{u32 transportEntry, u32 oldMapId}` only when riding a
|
||||
// transport (`Misc.cpp:493-501`), which makes the following NEW_WORLD boat-local.
|
||||
let map = read_u32_le(&mut r)?;
|
||||
let transport = if r.is_empty() {
|
||||
None
|
||||
@@ -428,10 +343,8 @@ fn parse_server_body(
|
||||
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.
|
||||
// Packed DateTime, LSB up: minute:6, hour:5, weekday:3, day:6, month:4, year:5. The
|
||||
// day serial assumes 31-day months and 372-day years, monotonic but not calendar-true.
|
||||
let datetime = read_u32_le(&mut r)?;
|
||||
let timescale = read_f32_le(&mut r)?;
|
||||
let (day, month, year) = (
|
||||
@@ -446,12 +359,7 @@ fn parse_server_body(
|
||||
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.
|
||||
// Wall-clock unix seconds (`QueryHandler.cpp:418-423`), not the game clock above.
|
||||
opcode::SMSG_QUERY_TIME_RESPONSE => ServerPacket::QueryTimeResponse {
|
||||
unix_time: read_u32_le(&mut r)?,
|
||||
},
|
||||
@@ -466,8 +374,7 @@ fn parse_server_body(
|
||||
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.
|
||||
// The ticket response opcodes share one 4-byte body; only the opcode names the verb.
|
||||
opcode::SMSG_GMTICKET_GETTICKET => ServerPacket::GmTicketAnswer {
|
||||
ticket: gm_ticket::read_gm_ticket(&mut r)?.map(Box::new),
|
||||
},
|
||||
@@ -489,8 +396,7 @@ fn parse_server_body(
|
||||
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.
|
||||
// The summon question; the client's whole answer is the guid back.
|
||||
opcode::SMSG_SUMMON_REQUEST => {
|
||||
let ask = summon::read_summon_request(&mut r)?;
|
||||
ServerPacket::SummonRequest {
|
||||
@@ -499,8 +405,7 @@ fn parse_server_body(
|
||||
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).
|
||||
// Two-way `MSG_`: inbound is the question (guid, cost); our answer is the guid alone.
|
||||
opcode::MSG_TALENT_WIPE_CONFIRM => {
|
||||
let ask = progression::read_talent_wipe_confirm(&mut r)?;
|
||||
ServerPacket::TalentWipeConfirm {
|
||||
@@ -615,13 +520,10 @@ fn parse_server_body(
|
||||
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).
|
||||
// `emoteNum` picks neither sentence nor voice kit on the reference's receive side.
|
||||
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.
|
||||
// The target's name, length-prefixed including its NUL (a lone `0x00` for no
|
||||
// target); trimmed at the first NUL, so no target reads as "", the untargeted form.
|
||||
let namelen = read_u32_le(&mut r)? as usize;
|
||||
let mut name = Vec::with_capacity(capacity_hint(namelen, 64));
|
||||
for _ in 0..namelen {
|
||||
@@ -743,8 +645,7 @@ fn parse_server_body(
|
||||
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).
|
||||
// Empty bodies on both sides: vmangos writes nothing and the reference reads nothing.
|
||||
opcode::SMSG_PET_NAME_INVALID => ServerPacket::PetNameInvalid,
|
||||
opcode::SMSG_PET_BROKEN => ServerPacket::PetBroken,
|
||||
opcode::SMSG_PET_ACTION_SOUND => {
|
||||
@@ -766,9 +667,8 @@ fn parse_server_body(
|
||||
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.
|
||||
// The empty swing refusals. `SMSG_ATTACKSWING_NOTSTANDING` (`0x147`) is absent on purpose:
|
||||
// the reference never registers it and vmangos never sends it.
|
||||
opcode::SMSG_ATTACKSWING_NOTINRANGE => {
|
||||
ServerPacket::AttackSwingError(AttackSwingError::NotInRange)
|
||||
}
|
||||
@@ -778,7 +678,7 @@ fn parse_server_body(
|
||||
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.
|
||||
// The family's fourth arm, registered with the spell handlers: same empty body, same act.
|
||||
opcode::SMSG_CANCEL_COMBAT => ServerPacket::CancelCombat,
|
||||
opcode::SMSG_FEIGN_DEATH_RESISTED => ServerPacket::FeignDeathResisted,
|
||||
opcode::SMSG_AI_REACTION => {
|
||||
@@ -822,7 +722,7 @@ fn parse_server_body(
|
||||
seconds,
|
||||
}
|
||||
}
|
||||
// One body, two tables — the opcode picks, exactly as `0x6e9950`'s `cmp edi,0x267` does.
|
||||
// One body, two tables: the opcode picks, as `0x6e9950`'s `cmp edi,0x267` does.
|
||||
opcode::SMSG_SET_FLAT_SPELL_MODIFIER | opcode::SMSG_SET_PCT_SPELL_MODIFIER => {
|
||||
let (mask_bit, op, value) = spells::read_set_spell_modifier(&mut r)?;
|
||||
ServerPacket::SpellModifier {
|
||||
@@ -888,7 +788,7 @@ fn parse_server_body(
|
||||
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.
|
||||
// One body, two sentences: only the opcode tells them apart.
|
||||
opcode::SMSG_PROCRESIST => {
|
||||
ServerPacket::ProcResist(combat_log::read_spell_outcome_log(&mut r)?)
|
||||
}
|
||||
@@ -1116,11 +1016,8 @@ fn parse_server_body(
|
||||
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.
|
||||
// The acked mode family, all eight `packed guid, u32 counter`
|
||||
// (`MovementPacketSender.cpp:342-366`); unacked, the server never applies the change.
|
||||
opcode::SMSG_FORCE_MOVE_ROOT
|
||||
| opcode::SMSG_FORCE_MOVE_UNROOT
|
||||
| opcode::SMSG_MOVE_WATER_WALK
|
||||
@@ -1148,11 +1045,8 @@ fn parse_server_body(
|
||||
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.
|
||||
// The observer mode family: twelve bare packed guids for any unit, nothing to ack
|
||||
// (vmangos `SendMovementFlagChangeToAll`; the reference's one handler `0x603c80`).
|
||||
opcode::SMSG_SPLINE_MOVE_ROOT
|
||||
| opcode::SMSG_SPLINE_MOVE_UNROOT
|
||||
| opcode::SMSG_SPLINE_MOVE_WATER_WALK
|
||||
@@ -1176,7 +1070,7 @@ fn parse_server_body(
|
||||
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
|
||||
// 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),
|
||||
@@ -1187,12 +1081,9 @@ fn parse_server_body(
|
||||
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.
|
||||
// `packed guid, u32 counter`, then `vcos, vsin, speedXY, speedZ`
|
||||
// (`MovementPacketSender.cpp:261-277`): the jump tail the ack echoes, but direction-first
|
||||
// where `JumpInfo` serializes `zspeed` first, so the reads are spelled out.
|
||||
opcode::SMSG_MOVE_KNOCK_BACK => {
|
||||
let guid = read_packed_guid(&mut r)?;
|
||||
let counter = read_u32_le(&mut r)?;
|
||||
@@ -1252,7 +1143,7 @@ fn parse_server_body(
|
||||
}
|
||||
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.
|
||||
// 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,
|
||||
@@ -1264,17 +1155,8 @@ fn parse_server_body(
|
||||
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.
|
||||
// `HandleCreatureQueryOpcode`: seven `u32`s (type_flags, type, pet_family, rank,
|
||||
// unk, pet_spell_list_id, display_id), then civilian and racial_leader as `u8`s.
|
||||
let type_flags = read_u32_le(&mut r)?;
|
||||
let creature_type = read_u32_le(&mut r)?;
|
||||
let pet_family = read_u32_le(&mut r)?;
|
||||
@@ -1300,9 +1182,8 @@ fn parse_server_body(
|
||||
}
|
||||
}
|
||||
}
|
||||
// `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.
|
||||
// `u32 petNumber, cstring name, u32 nameTimestamp` (`Pet.cpp:79-84`); the timestamp only
|
||||
// ages the reference's on-disk pet-name cache.
|
||||
opcode::SMSG_PET_NAME_QUERY_RESPONSE => {
|
||||
let pet_number = read_u32_le(&mut r)?;
|
||||
let name = read_cstring(&mut r)?;
|
||||
@@ -1329,7 +1210,6 @@ fn parse_server_body(
|
||||
let guid = gameobject::read_gameobject_despawn_anim(&mut r)?;
|
||||
ServerPacket::GameObjectDespawnAnim { guid }
|
||||
}
|
||||
// Decision 2339's three — the ones vmangos sends that the reference answers.
|
||||
opcode::SMSG_OPEN_CONTAINER => ServerPacket::OpenContainer {
|
||||
item: items::read_open_container(&mut r)?,
|
||||
},
|
||||
@@ -1339,31 +1219,23 @@ fn parse_server_body(
|
||||
opcode::SMSG_STANDSTATE_UPDATE => ServerPacket::StandStateUpdate {
|
||||
state: pose::read_stand_state_update(&mut r)?,
|
||||
},
|
||||
// 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)?,
|
||||
},
|
||||
@@ -1420,8 +1292,7 @@ fn parse_server_body(
|
||||
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.
|
||||
// The lockout family, read in the client's own field order.
|
||||
opcode::SMSG_RAID_INSTANCE_MESSAGE => ServerPacket::RaidInstanceMessage {
|
||||
message: instance::read_raid_instance_message(&mut r)?,
|
||||
},
|
||||
@@ -1440,8 +1311,6 @@ fn parse_server_body(
|
||||
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 {
|
||||
@@ -1465,18 +1334,11 @@ fn parse_server_body(
|
||||
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.
|
||||
// `0x2D6` carries our 8-byte request and the 50-byte reply; only the reply reaches here.
|
||||
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)?)
|
||||
}
|
||||
@@ -1487,8 +1349,6 @@ fn parse_server_body(
|
||||
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)?,
|
||||
},
|
||||
@@ -1499,10 +1359,6 @@ fn parse_server_body(
|
||||
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)?)
|
||||
}
|
||||
@@ -1519,10 +1375,7 @@ fn parse_server_body(
|
||||
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).
|
||||
// Guild founding; the two `MSG_` opcodes read a different body from the one they write.
|
||||
opcode::SMSG_PETITION_SHOWLIST => {
|
||||
ServerPacket::PetitionShowList(petition::read_petition_show_list(&mut r)?)
|
||||
}
|
||||
@@ -1544,9 +1397,6 @@ fn parse_server_body(
|
||||
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)?,
|
||||
@@ -1559,8 +1409,6 @@ fn parse_server_body(
|
||||
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)?,
|
||||
@@ -1569,20 +1417,15 @@ fn parse_server_body(
|
||||
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).
|
||||
// A full 8-byte guid, not packed (vmangos `HandleMountSpecialAnimOpcode`).
|
||||
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`).
|
||||
// A packed guid, unlike the flourish, then `u8 allowMove` (`Misc.cpp:677-682`).
|
||||
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 {
|
||||
@@ -1603,9 +1446,6 @@ fn parse_server_body(
|
||||
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)?,
|
||||
},
|
||||
@@ -1629,11 +1469,8 @@ fn parse_server_body(
|
||||
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.
|
||||
// The three list results share one frame and a 64-byte record; the reader is bounded by
|
||||
// the buffer too, since vmangos's browse fast path can count a record it never writes.
|
||||
opcode::MSG_AUCTION_HELLO => {
|
||||
let (auctioneer, house_id) = auction::read_auction_hello(&mut r)?;
|
||||
ServerPacket::AuctionHello {
|
||||
@@ -1671,8 +1508,7 @@ fn parse_server_body(
|
||||
total_count,
|
||||
}
|
||||
}
|
||||
// The two notifications carry different field orders and the owner one has no house id —
|
||||
// two readers, never one.
|
||||
// Different field orders, and no house id on the owner's: two readers, never one.
|
||||
opcode::SMSG_AUCTION_BIDDER_NOTIFICATION => ServerPacket::AuctionBidderNotification(
|
||||
auction::read_auction_bidder_notification(&mut r)?,
|
||||
),
|
||||
@@ -1688,16 +1524,13 @@ fn parse_server_body(
|
||||
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`].
|
||||
// The world-state table, source of the `$<n>w`/`$<n>e` NPC-text tokens.
|
||||
opcode::SMSG_INIT_WORLD_STATES => {
|
||||
ServerPacket::InitWorldStates(world_state::read_init_world_states(&mut r)?)
|
||||
}
|
||||
@@ -1705,9 +1538,7 @@ fn parse_server_body(
|
||||
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.
|
||||
// Another player's move, relayed under the mover's opcode (creatures use monster moves).
|
||||
o if is_movement_relay(o) => {
|
||||
let guid = read_packed_guid(&mut r)?;
|
||||
let info = movement::read_movement_info(&mut r)?;
|
||||
@@ -1737,8 +1568,6 @@ fn parse_server_body(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The tail is the decoder's unconsumed remainder: a known-good body reports 0, the same body
|
||||
/// with one byte appended reports 1, and neither is an error.
|
||||
#[test]
|
||||
fn a_trailing_byte_is_reported_as_tail_one_and_never_as_a_failure() {
|
||||
// `SMSG_SET_FACTION_VISIBLE`: exactly one `u32 repListId`.
|
||||
@@ -1760,14 +1589,9 @@ mod tests {
|
||||
ServerPacket::SetFactionVisible { list_id: 21 }
|
||||
));
|
||||
assert_eq!(tail, 1);
|
||||
// …and the projection every existing caller uses is unchanged by it.
|
||||
assert!(parse_server(opcode::SMSG_SET_FACTION_VISIBLE, &longer).is_ok());
|
||||
}
|
||||
|
||||
/// The compressed update object decodes through a second stream. Its zlib bytes are consumed
|
||||
/// through the cursor (a copy of the slice read the whole body as a tail — 803 bytes on the
|
||||
/// instrument's first live login), and the tail it reports is what the INFLATED stream left
|
||||
/// unread — the one leftover that means the inner layout drifted.
|
||||
#[test]
|
||||
fn the_compressed_update_object_reports_the_inflated_streams_tail_not_its_zlib_bytes() {
|
||||
use std::io::Write;
|
||||
@@ -1793,8 +1617,7 @@ mod tests {
|
||||
.expect("a trailing inflated byte is not a failure");
|
||||
assert_eq!(tail, 1, "the inflated stream's leftover is the tail");
|
||||
|
||||
// A byte AFTER the zlib stream, in the packet body: the outer tail (2343). The `read`
|
||||
// decoder's 32 KiB buffer swallowed it and this read 0.
|
||||
// Bytes after the zlib stream, still in the packet body, are the outer tail.
|
||||
let mut outer = body(&empty);
|
||||
outer.extend([0xAB, 0xCD]);
|
||||
let (_, tail) = parse_server_with_tail(opcode::SMSG_COMPRESSED_UPDATE_OBJECT, &outer)
|
||||
@@ -1805,8 +1628,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// An unknown opcode consumes nothing by definition; reporting its whole body as a tail would
|
||||
/// double-count what the dropped-packet tally already names.
|
||||
#[test]
|
||||
fn an_unknown_opcode_reports_no_tail() {
|
||||
let (packet, tail) =
|
||||
@@ -1815,8 +1636,6 @@ mod tests {
|
||||
assert_eq!(tail, 0);
|
||||
}
|
||||
|
||||
/// **The falsifier for decision 2265 §B1**: a wire count of `0xFFFF_FFFF` followed by one
|
||||
/// valid row must come back as a short-read `Err` — never reserve the count, never abort.
|
||||
#[test]
|
||||
fn a_lying_faction_count_is_a_short_read_not_an_allocation() {
|
||||
let mut body = 0xFFFF_FFFFu32.to_le_bytes().to_vec();
|
||||
|
||||
Reference in New Issue
Block a user