Files
benilla-octo/crates/benilla-protocol/src/messages/parse.rs
T
SamandClaude Fable 5.1 cf3189e422 a refused login says why, the deep ocean darkens, and the blade trails
- a refused character login left the player on a loading screen with no world behind it and no way off — reported as sitting at character select with no message, and the symptom was worse than the report: `SMSG_CHARACTER_LOGIN_FAILED` (0x41) had a name and no parse arm, so the client never heard it, and it was not at character select when the refusal landed, because the IO thread sends `CMSG_PLAYER_LOGIN` and announces the connection in the same breath (the entry's head start, 0777), so `ClientState::InWorld` was already set and the loading cover was up waiting for a snap the server had just declined to send; the wire half parses the opcode into `ServerPacket::CharacterLoginFailed` / `SessionEvent::CharacterLoginFailed` and carries the byte RAW, since vmangos sends a bare `1` and mangos-classic its own `CharLoginFailReasons`, and a wire layer that interpreted it would be picking a server; the refusal is an entry being REVOKED, not a failure to enter — `Cycle::LoginRefused` runs the same `SessionEnd::LoggedOut` teardown a `/logout` does, the cover cuts to black, `back_on_login_refused` takes the select screen back with the relist's roster underneath, and clearing `pending_pick` is load-bearing: it is 0065's reconnect memory and the relist is auto-answered with it, so keeping it would re-enter the refused character forever from behind a black screen; the byte is a 1-BASED REASON INDEX, not a status code — wow-re read `OnCharLoginResult` (`0x5aad70`): `dec eax; cmp eax,5; ja` into a six-entry jump table mapping 1..=6 onto `CHAR_LOGIN_*` 0x3e/0x3f/0x40/0x42/0x43/0x44 and SKIPPING 0x41 `CHAR_LOGIN_FAILED`, reachable only as the default, so the tempting `0x3d + byte` is right for four rows and wrong for the rest; `char_login_refusal_text` is that table, resolved against the player's own `GlueStrings.lua` and asserted against it (vmangos's `1` therefore reads "World server is down" — the server's dialect, and what the reference would say); the select screen runs the shared `login::drive_dialog` rather than a second copy of the reference's one `GlueDialog`, which found two holes — `to_select_on_roster` closed ANY dialog on an arriving roster while the refusal's own relist produces one a second later, so it would have wiped the message (the original bug one layer up; now scoped to status/queued), and `preflight`'s descriptor latch outlived the session that armed it, announcing "the avatar never streamed in" from character select, now disarmed on any disconnect; a live probe against vmangos pins the three facts this rests on — the byte arrives as 0x01, the roster still serves after it, a retry enters the world on the same socket — and `WOW_REFUSE_LOGIN=1` refuses the first pick of a run by sending a non-player guid, the one guard that needs no server state, so a path no real account can stage by hand is walkable on demand (2071)
- the ocean's depth ramp was never a placeholder, and its deep row is darker than we drew it: four files called the ADT ocean's `V = byte/255` a stand-in "pending its own RE" — the director's report had the sea fading to deep colour about 6x too slowly — on the claim that ocean takes a non-LUT UV path via `0x68d890`, which is the MAGMA vert-fill, as `benilla_adt::LiquidVertex::texcoords` had said by that same address all along; the ocean fill is `0x68d690` and it reads a depth LUT exactly as the river fill `0x68d790` does, `0x68c4c0` building both side by side (`0xc81768 = min(d/42, 1)` river, `0xc7fcd8 = min(d/255, 1)` ocean), the two fills differing in exactly 3 of 246 bytes — one `fld` displacement — so there is no second ocean-only depth term, and wow-re's cross-check found `/255` bit-exact for all 256 entries; the divisors differ because the authored bytes do — a census over every MCLQ block on Azeroth + Kalimdor, each wet vertex's depth byte against its own geometric depth, reads a river at 8.96 byte/yd (reproducing the independently-measured ≈8.5, the control) and the sea at 1.72, 5.2x gentler, so `/255` saturates at ~148 yd and `/42` at ~4.7 yd, the same ramp in yards and not a 6x slower one, while 83.4% of ocean vertices carry byte 255 outright, so the open sea is authored fully deep and the ramp is only a shore band; a test pins both divisors against the raw MCLQ bytes (swapping the ocean onto `/42` fails it), `examples/liquid_depth_census` is the measurement with named shore-band pins, `WOW_OCEAN_DEPTH_DIV` is the A/B, and the alpha-ramp deferral closes with it — `0xc7fbc0`'s `1.6·(i/63)^8` is the sky glare falloff, bound at `0x685257` inside a branch proven dead, so the linear `127 + 2·row` alpha shipped was right; and the finding nobody asked for: `0x68a830` fills the 8×64 swatch as an exact integer accumulator in BYTE space, `row(i) = c0 + floor(i·(c1−c0)/64)`, so the ramp never reaches the deep endpoint (row 63 is ~98.4% of the way), and an ocean-only tail `[0x68a9c0, 0x68aa36)` — gated on last row AND selector 0, river being selector 1 — runs RGB→HSV, `V ×= 0.9` (`0x3f666666` at `0x8102ec`), HSV→RGB and forces that row's alpha to 255, which reduces to `floor(0.9·byte)` per channel (99.03% bit-exact over all 2^24 triples, max 1/255); byte 255 ⇒ `V = 1.0` ⇒ row 63 exactly, so the darkened row IS the open sea, and benilla had rendered deep ocean ~10% too bright over most of the water in the world — an error that survived every regression diff because no capture scenario holds any ocean; `liquid.wgsl`'s `swatch_row` / `swatch_at` port the accumulator, the LINEAR/LINEAR CLAMP sampling (so the darkening ramps in across the final 1/64 of V rather than stepping) and the ocean-only tail, on the ADT lane only — the WMO arms keep the plain lerp, their opacity being a different 256-entry ramp — pinned by a test against wow-re's published byte vectors (row 62 = 12,36,41; row 63 = 11,35,40, green one ABOVE the deep endpoint; after the tail 9,31,36) that also asserts the WGSL still carries the formula; rivers move at most 7/255 from the row quantization (2069, 2074)
- the weapon swing trail — `SpellVisualKit` CharProc type 8, which benilla had dropped on the floor with the rest of the dispatcher's unmodelled cases: 34 of 1772 kits carry it, 27 are reached from a live `Spell.dbc` chain, and 16 of those name NO emitter slot at all, so the ribbon is their entire visual — kit 324 alone reaches 218 spells (Heroic Strike, Overpower, Mortal Strike, Bloodthirst, Rend) and rendered nothing beyond the swing animation, with 506 (Revenge, Riposte, Mongoose Bite), 557 (Sunder Armor), 646 (Hamstring, Wing Clip), 399 (Sinister Strike), 822 (Garrote) and 823 (Feint, Disengage) in the same shape; the proc draws nothing — `0x60d80a` latches a packed colour and a duration on the unit, and the unit's next `PlayAnimation` (`0x5fe48e`, the fields' only reader) hands both to its two weapon-hand trail objects and clears the duration, so an arm fires exactly once and a second proc on a live trail RESETS its ring rather than stacking; each object rings 128 samples of its weapon model's `$WTB`/`$WTT` markers (455 of the 571 weapon models author both) and draws them as a strip that fades from opaque at the OLDEST end of the arc to ≈0 at the blade, the alpha being a clock and not just an opacity; `weapon_trail.rs` transcribes the arithmetic rather than approximating it — the truncating `_ftol` decode (not the small-int idiom every other integer param takes, which would yield colour 0 and no trail at all), `fadeStep`'s low-byte floor (`cmp al,1`, so a ≈768 ms hitch takes a floor of 1 instead of its own huge value), the alpha-derived termination, and the ring's writer/reader modulus mismatch (`& 127` stored, `% 127` read — shipped 5875 behaviour, reachable only by Whirlwind's 10 000 ms spin); the draw state was dispatched rather than guessed and the guess was wrong in three of seven places: the callback's six EGxRs writes say `0x07 = 2` (ordinary SRC_ALPHA / INV_SRC_ALPHA — additive is mode 3, the difference between a ribbon that whites out crossing itself and a coloured smear) and `0x10 = 0` (depth test OFF: the arc draws over the world and over the swinging character, never eaten by his own shoulder), so `EffectDrawSpec::no_depth_test` is a new pipeline-key bit on the shared effect lane with every other producer explicitly false, where the state had only existed as the global `WOW_PARTICLE_NODEPTH` debug lever; the third — lighting ON over a vertex format with no normal — is a verified state that is not yet a verified appearance and ships unlit as a named deviation with the question out; the consume edge is `AnimDriver::started_anim`, not the one-shot alone, because `0x5fe2f0` is the image's SINGLE animation entry point, locomotion included — a unit that simply starts running consumes the arm, which is how Charge (kit 44, 38 spells, anim id −1) trails at all; kit 324's 600 ms duration is on screen for 1250 ms at 60 Hz, since `fadeStep` is `trunc(dt·alpha/300)` and shrinks with the alpha it decays, twice wow-re's own estimate; the warm pass gets the depth-test-off pipeline as the one combination that ships (alpha, unlit, no rasterizer settle) rather than as another factor of the cross product — 36 more warm draws per camera for pipelines nothing asks for — so the first Heroic Strike does not compile one live; and `benilla-extract charprocs` grows the trail census that found the 34/27/16 split (2076)
- a zero injury column plays nothing: benilla walked down the wound-grunt family when the selected column was 0 — crushing to critical to ordinary — on a comment with no citation ("crushing rows are often 0 in data"), a true observation about the data and a false inference about the client; `0x623490` tests the selected id once (`0x6234e6 test ebx,ebx ; je 0x62350e`, the shared epilogue) and no fallback is even reachable — the row pointer dies at `0x6234e4`, so neither the bus pick `0x623b10` nor the play `0x458890` ever sees the row to re-read a column from, and the CGPlayer twin `0x62f880` is identical; it mattered because the class-2 chance roll had just landed beside it (2073): a crushing blow skips the roll (class 9 carries threshold 100) AND fell through to the ordinary kit, so it was louder than an ordinary hit on the same creature by two routes; `sound/combat.rs` now plays the selected column or nothing — crushing → column 5, critical → 4, ordinary → 3 — and a crushing blow on a creature with no crushing kit, which is most of the 406 rows (`Skeleton`, `Silithid`, `MountainGiant` among them), is silent, the reference's behaviour rather than an omission (2075)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:09:51 +07:00

1602 lines
76 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The opcode→[`ServerPacket`] dispatch: [`parse_server`] decodes one server packet body (already
//! decrypted + sized) into its variant, delegating per-domain payloads to the domain children
//! (`spells`, `quest`, `loot`, …). One match arm per opcode — which opcode carries what stays
//! readable in one place.
use std::io::{self, Read};
use crate::wire::{
read_cstring, read_f32_le, read_packed_guid, read_u16_le, read_u32_le, read_u64_le, read_u8,
Vector3d,
};
use super::{
action_bar, area_trigger, attack, auction, bank, battlefield, binder, broadcast, channel, chat,
combat_log, death, duel, gameobject, gm_ticket, gossip, group, guild, instance, items, loot,
mail, meeting_stone, mirror_timer, monster_move, movement, opcode, page_text, pet, petition,
progression, pvp, quest, social, spellbook, spells, stable, summon, tabard, taxi, trade,
trainer, tutorial, update_object, vendor, world_state, AttackSwingError, Character,
CreatureQueryInfo, JumpInfo, MoveMode, ServerPacket, SpeedKind, SplineMode,
};
/// Read one `SMSG_FORCE_*_SPEED_CHANGE` body — `[packed mover guid][u32 counter][f32 speed]`,
/// identical across all six kinds (VERIFIED vmangos `SendSpeedChangeToController`, the 5875
/// `> 1_9_4` branch). The kind is the opcode's; the caller's arm names it.
fn read_force_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
Ok(ServerPacket::ForceSpeedChange {
guid: read_packed_guid(r)?,
kind,
counter: read_u32_le(r)?,
speed: read_f32_le(r)?,
})
}
/// Read an `SMSG_COMPRESSED_MOVES` body: `[u32 uncompressed size][deflate stream]`, the stream a
/// run of `[u8 size][u16 opcode][body]` records where `size` counts the opcode's own two bytes
/// (VERIFIED vmangos `MovementData::AddPacket`/`BuildPacket`). Each record is a whole movement
/// packet, so it goes straight back through [`parse_server`] — the batch is a *transport*, not a
/// message shape, and nothing downstream needs to know a move arrived batched.
///
/// A record that fails to parse fails the **whole** batch (surfacing as one `Poll::Skipped` naming
/// the inner opcode) rather than being dropped quietly. Losing a batch is worse than losing a
/// packet, but silence here is what cost days: an unhandled inner opcode has to be loud. Only the
/// `MSG_MOVE_*` relays vmangos routes through `ObjectViewersMovementDeliverer` can appear, and we
/// model all of them (`the_batch_carries_every_relayed_move_opcode`).
fn read_compressed_moves(r: &mut impl Read) -> io::Result<Vec<ServerPacket>> {
let uncompressed = read_u32_le(r)? as usize;
let mut buf = Vec::with_capacity(uncompressed.min(64 * 1024));
flate2::read::ZlibDecoder::new(r).read_to_end(&mut buf)?;
let mut rest = buf.as_slice();
let mut packets = Vec::new();
while !rest.is_empty() {
let size = read_u8(&mut rest)? as usize;
let opcode = read_u16_le(&mut rest)?;
// `size` spans opcode + body; anything under two bytes cannot even hold the opcode we
// just read, so the stream is misframed and every later record would be garbage.
let body_len = size.checked_sub(2).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("compressed-moves record size {size} < 2"),
)
})?;
if rest.len() < body_len {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"compressed-moves record {opcode:#06x} wants {body_len}B, {}B left",
rest.len()
),
));
}
let (body, tail) = rest.split_at(body_len);
rest = tail;
// A batch inside a batch would recurse without bound; vmangos never nests one.
if opcode == opcode::SMSG_COMPRESSED_MOVES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"compressed-moves nested inside compressed-moves",
));
}
packets.push(parse_server(opcode, body).map_err(|e| {
io::Error::new(
e.kind(),
format!(
"compressed-moves record {opcode:#06x} ({}): {e}",
super::opcode_name(opcode).unwrap_or("?")
),
)
})?);
}
Ok(packets)
}
/// Read one `SMSG_SPLINE_SET_*_SPEED` body — `[packed guid][f32 speed]` (VERIFIED vmangos
/// `MovementPacketSender::SendSpeedChangeToAll` + the mid-spline observer branch, the 5875
/// `> 1_8_4` layout). Observer-only: no counter, no ack (decision 0441).
fn read_spline_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
Ok(ServerPacket::SplineSpeedChange {
guid: read_packed_guid(r)?,
kind,
speed: read_f32_le(r)?,
})
}
/// Read one `MSG_MOVE_SET_*_SPEED` body — `[packed guid][MovementInfo][f32 speed]` (VERIFIED
/// vmangos `SendSpeedChangeToObservers`, the finalized-spline branch): a freely-moving player's
/// speed change, carrying a fresh pose alongside (decision 0441). No ack.
fn read_move_set_speed(kind: SpeedKind, r: &mut impl Read) -> io::Result<ServerPacket> {
let guid = read_packed_guid(r)?;
let info = movement::read_movement_info(r)?;
Ok(ServerPacket::MoveSetSpeed {
guid,
kind,
flags: info.flags,
position: info.position,
orientation: info.orientation,
pitch: info.pitch,
time: info.timestamp,
fall_time: info.fall_time,
jump: info.jump,
transport: info.transport,
speed: read_f32_le(r)?,
})
}
/// True for a relayed movement opcode — one the server rebroadcasts as `[packed guid][MovementInfo]`.
/// Two groups, one wire shape and one client handler (`0x603bb0`, wow-re `re/net/opcode-handlers.tsv`
/// — 30 rows pointing at it, of which these are 23):
///
/// - the **echoed input stream**, every opcode bound to vmangos `HandleMovementOpcodes` (VERIFIED
/// `Opcodes.cpp`): another player's walking, turning, jumping, swimming, facing;
/// - the **observer leg of the movement-mode family** (decision 2061): root/unroot, hover,
/// feather-fall, water-walk and the near-teleport, broadcast by
/// `MovementPacketSender::Send{MovementFlagChange,Teleport}ToObservers` once the mover's own
/// client has acked. Same body, no ack, and the apply/unapply direction rides the `MovementInfo`
/// flags word rather than the opcode (see [`super::opcode`]'s block).
///
/// Excludes `MSG_MOVE_TELEPORT_ACK` / `MSG_MOVE_WORLDPORT_ACK`, which share the family but carry
/// different bodies and are decoded by their own arms — note `MSG_MOVE_TELEPORT` (197) is the
/// observer's teleport and is NOT `MSG_MOVE_TELEPORT_ACK` (199): no counter dword, so it parses here.
const fn is_movement_relay(o: u16) -> bool {
matches!(
o,
opcode::MSG_MOVE_START_FORWARD
| opcode::MSG_MOVE_START_BACKWARD
| opcode::MSG_MOVE_STOP
| opcode::MSG_MOVE_START_STRAFE_LEFT
| opcode::MSG_MOVE_START_STRAFE_RIGHT
| opcode::MSG_MOVE_STOP_STRAFE
| opcode::MSG_MOVE_JUMP
| opcode::MSG_MOVE_START_TURN_LEFT
| opcode::MSG_MOVE_START_TURN_RIGHT
| opcode::MSG_MOVE_STOP_TURN
| opcode::MSG_MOVE_START_PITCH_UP
| opcode::MSG_MOVE_START_PITCH_DOWN
| opcode::MSG_MOVE_STOP_PITCH
| opcode::MSG_MOVE_SET_RUN_MODE
| opcode::MSG_MOVE_SET_WALK_MODE
| opcode::MSG_MOVE_FALL_LAND
| opcode::MSG_MOVE_START_SWIM
| opcode::MSG_MOVE_STOP_SWIM
| opcode::MSG_MOVE_SET_FACING
| opcode::MSG_MOVE_SET_PITCH
| opcode::MSG_MOVE_HEARTBEAT
// Somebody else was knocked back (decision 1702). It carries four extra floats after the
// `MovementInfo` — the launch quad — which this reader simply doesn't reach, and doesn't
// need to: the server built that `MovementInfo` from the victim's own ack, so its jump
// tail already IS the quad, and the arc replays from it like any other relayed jump.
| opcode::MSG_MOVE_KNOCK_BACK
// The observer leg of the movement-mode family (decision 2061): somebody else was
// rooted, levitated (hover + feather fall + water walk), or blinked. The mode lands in
// the `MovementInfo` flags word this body already carries, so no arm of its own is
// needed — only the opcode's admission to the family.
| opcode::MSG_MOVE_ROOT
| opcode::MSG_MOVE_UNROOT
| opcode::MSG_MOVE_HOVER
| opcode::MSG_MOVE_FEATHER_FALL
| opcode::MSG_MOVE_WATER_WALK
| opcode::MSG_MOVE_TELEPORT
)
}
/// Parse a server packet body (already decrypted + sized) by opcode.
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,
}
}
other => ServerPacket::Other { opcode: other },
})
}