Files
benilla-octo/crates/benilla-protocol/src/messages/parse.rs
T
SamandClaude Fable 5 d16d33dfca world books open, and anvils stop offering a cursor gear
- clicking a book in the world opens it and turns its pages: letters and world books share one reader
- the cursor's object rules are the client's own 31-row strategy table now, so decorative objects stop pretending to be usable
- trainer and tradeskill windows draw each row's icon by the reference's own law, and the trainer's filter dropdown toggles again
- water color follows the zone's light blend, so lakes stop snapping between area tints

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:03:35 +07:00

1068 lines
48 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, bank, channel, chat, combat_log, death, duel, gameobject,
gossip, group, items, loot, mail, mirror_timer, monster_move, movement, opcode, page_text, pet,
progression, quest, social, spellbook, spells, taxi, trade, trainer, update_object, vendor,
world_state, Character, CreatureQueryInfo, MoveMode, ServerPacket, SpeedKind,
};
/// 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 player-movement opcode — one the server rebroadcasts as
/// `[packed guid][MovementInfo]` (every opcode bound to vmangos `HandleMovementOpcodes`, VERIFIED
/// `Opcodes.cpp`). Excludes `MSG_MOVE_TELEPORT_ACK` / `MSG_MOVE_WORLDPORT_ACK`, which share the family
/// but carry different bodies and are decoded by their own arms.
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
)
}
/// 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 => ServerPacket::AuthResponse {
result: read_u8(&mut r)?,
},
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_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::SMSG_MONSTER_MOVE => monster_move::read_monster_move(&mut r)?,
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,
}
}
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,
}
}
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)?;
let _emote_num = read_u32_le(&mut r)?;
let namelen = read_u32_le(&mut r)?;
for _ in 0..namelen {
read_u8(&mut r)?; // the target's name — chat consumes it later; audio keys on ids
}
ServerPacket::TextEmote { guid, text_emote }
}
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_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_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_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)?)
}
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_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_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::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_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::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_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,
}
}
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_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), and the
// `civilian`/`racial_leader` pair (its green CIVILIAN / white LEADER lines); the
// rest 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,
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 }
}
// 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::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 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 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 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 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 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 },
})
}