//! 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 { 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> { 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 { 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 { 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 { 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 `$w`/`$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 }, }) }