comments: protocol, the world writer's sends
1,096 comment lines become 372 across 31 files; the code is unchanged. Dropped as false or stale: - the module table, which missed 8 of the 44 modules; - "cacheable forever" for names, which a pet rename breaks; - "only failures get a packet", though SMSG_BUY_ITEM answers a buy; - a go_target parameter that does not exist; - "three" cast shapes, where there are five. vmangos line citations now name the function or the current lines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,6 @@
|
||||
//! The guild family's `WorldWriter` sends — the two cache asks, the invitation dance, the member
|
||||
//! verbs, and rank administration.
|
||||
//!
|
||||
//! Three shapes of the family are worth knowing before calling any of these:
|
||||
//!
|
||||
//! - **Almost nothing is acked individually.** Promote, demote, remove, the leader handover, the
|
||||
//! MOTD, both notes and every rank verb answer with a fresh `SMSG_GUILD_ROSTER` (and, for the
|
||||
//! rank verbs, an `SMSG_GUILD_QUERY_RESPONSE` too) — the server re-sends the whole snapshot
|
||||
//! rather than a delta. A refusal instead comes back as `SMSG_GUILD_COMMAND_RESULT`. So the
|
||||
//! caller's model updates when the roster lands, never optimistically at the send.
|
||||
//! - **Members are addressed by NAME here**, unlike the friend list's remove-by-guid: every verb
|
||||
//! below that targets a player takes the character name, and the server normalises its case.
|
||||
//! - **Rank ids are 0-based with 0 = guild master**, and authority *decreases* as the id rises —
|
||||
//! [`WorldWriter::guild_promote`] moves a member to `rank - 1`.
|
||||
//! The guild sends. Members are addressed by name, and rank 0 is the guild master. The server acks
|
||||
//! a member or rank change with a whole fresh `SMSG_GUILD_ROSTER` and refuses one with
|
||||
//! `SMSG_GUILD_COMMAND_RESULT`, so state updates when the roster lands, not at the send.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -20,67 +9,52 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask for a guild's public identity by id (`CMSG_GUILD_QUERY`) — name, rank names, tabard.
|
||||
/// The ask-once cache fill behind every "which guild is that?": a roster row, a `/who` hit and
|
||||
/// a guild chat line all reference a guild by id alone.
|
||||
/// `CMSG_GUILD_QUERY`: a guild's name, rank names and tabard by id, for the ask-once cache.
|
||||
pub fn guild_query(&mut self, guild_id: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_QUERY, &messages::guild_query(guild_id))
|
||||
}
|
||||
|
||||
/// Found a guild by name (`CMSG_GUILD_CREATE`). vmangos registers this opcode `STATUS_NEVER`:
|
||||
/// on a 1.12 realm founding runs through the charter/petition flow instead, so this send is
|
||||
/// here for completeness of the family and draws no reply.
|
||||
/// `CMSG_GUILD_CREATE`: `STATUS_NEVER` in vmangos, so no reply; guilds are founded by charter.
|
||||
pub fn guild_create(&mut self, name: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_CREATE, &messages::guild_create(name))
|
||||
}
|
||||
|
||||
/// Invite a character into our guild by name (`CMSG_GUILD_INVITE`). They get an
|
||||
/// `SMSG_GUILD_INVITE` popup; we get an `SMSG_GUILD_COMMAND_RESULT` only if it was refused
|
||||
/// (already guilded, ignoring us, wrong faction, no permission).
|
||||
/// `CMSG_GUILD_INVITE`: the invitee gets `SMSG_GUILD_INVITE`; we hear back only on a refusal.
|
||||
pub fn guild_invite(&mut self, name: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_INVITE, &messages::guild_invite(name))
|
||||
}
|
||||
|
||||
/// Accept the guild invitation we are holding (`CMSG_GUILD_ACCEPT`, empty body). Which
|
||||
/// invitation is the server's pending state, not a field — so there is nothing to pass, and
|
||||
/// nothing to get wrong except sending it when no invite is outstanding (a silent no-op).
|
||||
/// `CMSG_GUILD_ACCEPT`, empty: takes the server's pending invite; a silent no-op without one.
|
||||
pub fn guild_accept(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_ACCEPT, &messages::guild_accept())
|
||||
}
|
||||
|
||||
/// Turn down the guild invitation we are holding (`CMSG_GUILD_DECLINE`, empty body). The
|
||||
/// inviter is told by `SMSG_GUILD_DECLINE`; we hear nothing back.
|
||||
/// `CMSG_GUILD_DECLINE`, empty: the inviter gets `SMSG_GUILD_DECLINE`; we hear nothing.
|
||||
pub fn guild_decline(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_DECLINE, &messages::guild_decline())
|
||||
}
|
||||
|
||||
/// Ask for our guild's founding date and member/account counts (`CMSG_GUILD_INFO`, empty
|
||||
/// body), answered by `SMSG_GUILD_INFO`. A different ask from the roster, sharing no fields.
|
||||
/// `CMSG_GUILD_INFO`, empty: answered by `SMSG_GUILD_INFO`, the founding date and counts.
|
||||
pub fn guild_info(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_INFO, &messages::guild_info())
|
||||
}
|
||||
|
||||
/// Ask for the whole guild roster (`CMSG_GUILD_ROSTER`, empty body). The server also pushes
|
||||
/// `SMSG_GUILD_ROSTER` unasked after every change it makes, so this is a refresh — the guild
|
||||
/// pane's opener — and never the only way the roster arrives.
|
||||
/// `CMSG_GUILD_ROSTER`, empty: a refresh; the server also pushes the roster after every change.
|
||||
pub fn guild_roster(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_ROSTER, &messages::guild_roster())
|
||||
}
|
||||
|
||||
/// Promote a member one rank (`CMSG_GUILD_PROMOTE`): the server does `rank - 1`, *towards*
|
||||
/// guild master. Answered by a fresh roster, or refused with `RANK_TOO_HIGH_S`/`PERMISSIONS`.
|
||||
/// `CMSG_GUILD_PROMOTE`: the server moves the member to `rank - 1`, toward guild master.
|
||||
pub fn guild_promote(&mut self, name: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_PROMOTE, &messages::guild_promote(name))
|
||||
}
|
||||
|
||||
/// Demote a member one rank (`CMSG_GUILD_DEMOTE`): `rank + 1`, away from guild master.
|
||||
/// `CMSG_GUILD_DEMOTE`: `rank + 1`, away from guild master.
|
||||
pub fn guild_demote(&mut self, name: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_DEMOTE, &messages::guild_demote(name))
|
||||
}
|
||||
|
||||
/// Leave our guild (`CMSG_GUILD_LEAVE`, empty body). Refused with
|
||||
/// `(QUIT, LEADER_LEAVE)` while we are the guild master and anyone else remains — hand over
|
||||
/// with [`Self::guild_leader`] first, or [`Self::guild_disband`].
|
||||
/// `CMSG_GUILD_LEAVE`, empty: refused (`LEADER_LEAVE`) for a guild master while others remain.
|
||||
pub fn guild_leave(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_LEAVE, &messages::guild_leave())
|
||||
}
|
||||
@@ -90,32 +64,24 @@ impl WorldWriter {
|
||||
self.send(opcode::CMSG_GUILD_REMOVE, &messages::guild_remove(name))
|
||||
}
|
||||
|
||||
/// Disband the guild (`CMSG_GUILD_DISBAND`, empty body). Guild master only, and irreversible —
|
||||
/// every member gets a `GE_DISBANDED` event.
|
||||
/// `CMSG_GUILD_DISBAND`, empty: guild master only; every member gets `GE_DISBANDED`.
|
||||
pub fn guild_disband(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_DISBAND, &messages::guild_disband())
|
||||
}
|
||||
|
||||
/// Hand the guild to another member (`CMSG_GUILD_LEADER`). Guild master only; both names ride
|
||||
/// back to everyone as a `GE_LEADER_CHANGED` event.
|
||||
/// `CMSG_GUILD_LEADER`: guild master only; everyone gets both names in `GE_LEADER_CHANGED`.
|
||||
pub fn guild_leader(&mut self, name: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_LEADER, &messages::guild_leader(name))
|
||||
}
|
||||
|
||||
/// Set the message of the day (`CMSG_GUILD_MOTD`). Passing `""` clears it — see
|
||||
/// [`messages::guild_motd`] for why we send the one-byte empty cstring rather than an empty
|
||||
/// body, which the server also accepts.
|
||||
/// `CMSG_GUILD_MOTD`: `""` clears the message of the day.
|
||||
pub fn guild_motd(&mut self, motd: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_MOTD, &messages::guild_motd(motd))
|
||||
}
|
||||
|
||||
/// Rewrite one rank's name **and** rights in a single packet (`CMSG_GUILD_RANK`).
|
||||
///
|
||||
/// There is no partial form: a caller changing only the name must still send the rank's
|
||||
/// current rights, and vice versa. Guild master only; `rights` is ignored and replaced with
|
||||
/// [`messages::guild_rank_right::ALL`] for rank 0; and a `name` over
|
||||
/// [`messages::GUILD_RANK_MAX_LENGTH`] characters gets the session **kicked** by vmangos's
|
||||
/// anticheat rather than refused, so the caller caps it.
|
||||
/// `CMSG_GUILD_RANK`: a rank's name and rights together, with no partial form. Guild master
|
||||
/// only; rank 0 always gets all rights, and a name over [`messages::GUILD_RANK_MAX_LENGTH`]
|
||||
/// gets the session kicked by vmangos, so the caller caps it.
|
||||
pub fn guild_rank(&mut self, rank_id: u32, rights: u32, name: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_GUILD_RANK,
|
||||
@@ -123,22 +89,18 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Append a rank at the bottom of the ladder (`CMSG_GUILD_ADD_RANK`). It starts with guild
|
||||
/// chat listen + speak and nothing else. Silently ignored once the guild already has
|
||||
/// [`messages::GUILD_RANKS_MAX_COUNT`] ranks — the reference UI hides the button there.
|
||||
/// `CMSG_GUILD_ADD_RANK`: a new bottom rank with only guild chat rights; ignored at
|
||||
/// [`messages::GUILD_RANKS_MAX_COUNT`] ranks.
|
||||
pub fn guild_add_rank(&mut self, name: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_ADD_RANK, &messages::guild_add_rank(name))
|
||||
}
|
||||
|
||||
/// Delete the **lowest** rank (`CMSG_GUILD_DEL_RANK`, empty body). There is no rank id on the
|
||||
/// wire: it is always the last one, which is why the reference UI only ever offers to remove
|
||||
/// the bottom row. Refused with `RANK_IN_USE` while a member still holds it.
|
||||
/// `CMSG_GUILD_DEL_RANK`, empty: always the lowest rank; refused with `RANK_IN_USE` while held.
|
||||
pub fn guild_del_rank(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_GUILD_DEL_RANK, &messages::guild_del_rank())
|
||||
}
|
||||
|
||||
/// Set a member's public note (`CMSG_GUILD_SET_PUBLIC_NOTE`); needs
|
||||
/// [`messages::guild_rank_right::EDIT_PUBLIC_NOTE`].
|
||||
/// `CMSG_GUILD_SET_PUBLIC_NOTE`: needs [`messages::guild_rank_right::EDIT_PUBLIC_NOTE`].
|
||||
pub fn guild_set_public_note(&mut self, name: &str, note: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_GUILD_SET_PUBLIC_NOTE,
|
||||
@@ -146,9 +108,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Set a member's officer note (`CMSG_GUILD_SET_OFFICER_NOTE`); needs
|
||||
/// [`messages::guild_rank_right::EDIT_OFFICER_NOTE`]. Note that *seeing* officer notes is a
|
||||
/// separate right — a roster whose officer notes are all empty may mean we cannot view them.
|
||||
/// `CMSG_GUILD_SET_OFFICER_NOTE`: needs [`messages::guild_rank_right::EDIT_OFFICER_NOTE`].
|
||||
pub fn guild_set_officer_note(&mut self, name: &str, note: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_GUILD_SET_OFFICER_NOTE,
|
||||
@@ -156,8 +116,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Set the guild information text (`CMSG_GUILD_INFO_TEXT`) — the long free-text pane, not the
|
||||
/// MOTD and not `SMSG_GUILD_INFO`'s counts. It rides back on the roster as its `info` field.
|
||||
/// `CMSG_GUILD_INFO_TEXT`: the free-text info pane; it comes back as the roster's `info`.
|
||||
pub fn guild_info_text(&mut self, text: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_GUILD_INFO_TEXT,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
//! The instance/raid lockout family's one outbound verb (decision 1748).
|
||||
//!
|
||||
//! Everything else in the family is the server telling us something; the only thing the player can
|
||||
//! *do* is press "Reset all instances" on their own portrait, which is this.
|
||||
//! The instance lockout family's one send: "Reset all instances" from the player's portrait.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -10,20 +7,9 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Reset every dungeon we are the owner of (`CMSG_RESET_INSTANCES`, empty body) — the
|
||||
/// `CONFIRM_RESET_INSTANCES` dialog's Yes.
|
||||
///
|
||||
/// The answer is per instance, not one verdict: the server walks every bind and sends an
|
||||
/// `SMSG_INSTANCE_RESET` for each map that reset, so a mixed outcome is normal — and **no
|
||||
/// answer at all is the ordinary case**, because a character with nothing resettable gets
|
||||
/// nothing back. Refusals only exist on the group path: vmangos `Group::ResetInstances` sends
|
||||
/// `INSTANCERESET_FAIL_OFFLINE` (2187) and `INSTANCERESET_FAIL_GENERAL` (2205), while the solo
|
||||
/// path `Player::ResetInstances` sends successes only. `INSTANCERESET_FAIL_ZONING` is a code
|
||||
/// the client can render and this server never sends.
|
||||
///
|
||||
/// Both sides also refuse a RAID here — `entry->mapType == MAP_RAID` is skipped outright —
|
||||
/// which is the server's face of the same party-dungeon rule `CanShowResetInstances()` gates
|
||||
/// the menu row on.
|
||||
/// `CMSG_RESET_INSTANCES`, empty: the `CONFIRM_RESET_INSTANCES` dialog's Yes. The server sends
|
||||
/// one `SMSG_INSTANCE_RESET` per map it reset and nothing when none could; raids are skipped,
|
||||
/// and only the group path refuses (vmangos `Group::ResetInstances`).
|
||||
pub fn reset_instances(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_RESET_INSTANCES, &messages::reset_instances())
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
//! The bag/equipment family's `WorldWriter` sends — the item-template ask, use, auto-equip, the
|
||||
//! ammo fork, the three move/swap/split shapes, and destroy. Bodies in
|
||||
//! [`crate::messages::items`], whose scope this mirrors. Split out of `writer/mod.rs`
|
||||
//! (decision 0636).
|
||||
//!
|
||||
//! Three distinct swap opcodes exist because the addressable space differs, not the intent:
|
||||
//! `CMSG_SWAP_INV_ITEM` only ever names two slots in the player's own grid, while `CMSG_SWAP_ITEM`
|
||||
//! and `CMSG_SPLIT_ITEM` take a `(bag, slot)` pair on each side so either endpoint may be an
|
||||
//! equipped bag (decision 0216 §6). Every one of them refuses the same way —
|
||||
//! `SMSG_INVENTORY_CHANGE_FAILURE` — and succeeds silently, as values deltas on both slots.
|
||||
//! The bag and equipment sends. A move succeeds silently, as values deltas on both slots, and is
|
||||
//! refused with `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -16,9 +8,7 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask an item template's display head (`CMSG_ITEM_QUERY_SINGLE`: entry + item guid, 0 for
|
||||
/// template-only asks). Answered by `SMSG_ITEM_QUERY_SINGLE_RESPONSE` (an `ItemTemplate`
|
||||
/// event) — the T2 container groundwork.
|
||||
/// `CMSG_ITEM_QUERY_SINGLE`: an item template by entry; `guid` is 0 for a template-only ask.
|
||||
pub fn item_query(&mut self, entry: u32, guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_ITEM_QUERY_SINGLE,
|
||||
@@ -26,10 +16,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Use an item by bag position (`CMSG_USE_ITEM`, layout in [`messages::use_item`]) — eat the
|
||||
/// food, drink the potion, hearthstone home. `go_target` aims the use at a GameObject, which is
|
||||
/// how a KEY opens a locked door (decision 0769). The server answers with the effect (values
|
||||
/// deltas, a stack decrement/destroy) or `SMSG_CAST_RESULT` on refusal.
|
||||
/// `CMSG_USE_ITEM`: use the item at a bag position; a GameObject `target` is how a key opens a
|
||||
/// locked door. Refused with `SMSG_CAST_RESULT`.
|
||||
pub fn use_item(
|
||||
&mut self,
|
||||
bag_index: u8,
|
||||
@@ -43,11 +31,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Open an item by bag position (`CMSG_OPEN_ITEM`, layout in [`messages::open_item`]) — crack
|
||||
/// the clam, empty the picked lockbox, unwrap the gift. The right-click fork for an
|
||||
/// `crate::ItemInfo::openable` item; the server answers with `SMSG_LOOT_RESPONSE` on the
|
||||
/// item's **own** guid (so the loot window opens over a thing in the bag), or an equip error
|
||||
/// on a refusal (still locked, dead, flying).
|
||||
/// `CMSG_OPEN_ITEM`: open a clam, lockbox or gift; the loot window comes back on the item's
|
||||
/// own guid.
|
||||
pub fn open_item(&mut self, bag_index: u8, slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_OPEN_ITEM,
|
||||
@@ -55,10 +40,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Wrap an item in gift paper (`CMSG_WRAP_ITEM`, layout in [`messages::wrap_item`]) — the
|
||||
/// completion of the local wrap cursor a `ITEM_FLAG_WRAPPER` item's right-click arms. The
|
||||
/// paper's `(bag, slot)` leads, the target's follows. Success is silent (field updates on the
|
||||
/// target, one paper destroyed); every refusal comes back as `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
/// `CMSG_WRAP_ITEM`: wrap an item in the `ITEM_FLAG_WRAPPER` paper; success is silent and
|
||||
/// consumes one paper.
|
||||
pub fn wrap_item(
|
||||
&mut self,
|
||||
gift_bag: u8,
|
||||
@@ -72,9 +55,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`, layout in [`messages::auto_equip_item`]) — the
|
||||
/// server picks the destination slot. Success arrives as inventory-slot values deltas (and the
|
||||
/// visible-item change everyone renders); refusal as `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
/// `CMSG_AUTOEQUIP_ITEM`: equip a bag item; the server picks the slot.
|
||||
pub fn auto_equip_item(&mut self, bag_index: u8, slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUTOEQUIP_ITEM,
|
||||
@@ -82,19 +63,13 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Load ammo into the ammo slot (`CMSG_SET_AMMO`, layout in [`messages::set_ammo`]) — the
|
||||
/// client's own auto-equip fork for ammo-class items (wow-re `cursor-dragdrop-slots.md`).
|
||||
/// Addressed by item *entry*, not a bag slot; the stack stays in the bag and `PLAYER_AMMO_ID`
|
||||
/// starts referencing it. A wrong/absent ranged weapon refuses via
|
||||
/// `SMSG_INVENTORY_CHANGE_FAILURE`. Decision 0526.
|
||||
/// `CMSG_SET_AMMO`: what the 1.12 client sends to auto-equip ammo. It names an item entry, not
|
||||
/// a slot; the stack stays in the bag and `PLAYER_AMMO_ID` points at it.
|
||||
pub fn set_ammo(&mut self, entry: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_SET_AMMO, &messages::set_ammo(entry))
|
||||
}
|
||||
|
||||
/// Swap two of the player's own inventory slots (`CMSG_SWAP_INV_ITEM`, layout in
|
||||
/// [`messages::swap_inv_item`]) — the wire for a backpack-internal pick/place/swap (both slots
|
||||
/// are `INVENTORY_SLOT_ITEM_START`+i). An empty destination is a move; the server settles both
|
||||
/// slots with values deltas, or refuses via `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
/// `CMSG_SWAP_INV_ITEM`: two slots of the player's own grid; an empty destination is a move.
|
||||
pub fn swap_inv_item(&mut self, src_slot: u8, dst_slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SWAP_INV_ITEM,
|
||||
@@ -102,11 +77,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// The general bag↔bag move (`CMSG_SWAP_ITEM`, layout in [`messages::swap_item`]): either
|
||||
/// endpoint may be an equipped bag (unlike [`Self::swap_inv_item`], which only ever addresses
|
||||
/// the player's own grid) — the wire for a whole-space bag-window pick/place/swap (decision
|
||||
/// 0216 §6, slice 2). An empty destination is a move, same as `swap_inv_item`; refusal answers
|
||||
/// `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
/// `CMSG_SWAP_ITEM`: a `(bag, slot)` on each side, so either end may be in an equipped bag.
|
||||
pub fn swap_item(
|
||||
&mut self,
|
||||
dst_bag: u8,
|
||||
@@ -120,11 +91,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Auto-store an item into a bag (`CMSG_AUTOSTORE_BAG_ITEM`, layout in
|
||||
/// [`messages::auto_store_bag_item`]): take `(src_bag, src_slot)` and put it anywhere inside
|
||||
/// `dst_bag` — **the client names no destination slot; the server picks it**. The wire for
|
||||
/// `PutItemInBag`'s auto-store leg and the whole of `PutItemInBackpack` (wow-re
|
||||
/// `bag-verbs-law.md`). Refusal answers `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
/// `CMSG_AUTOSTORE_BAG_ITEM`: into `dst_bag` at a slot the server picks; the wire of
|
||||
/// `PutItemInBackpack` and of `PutItemInBag`'s auto-store.
|
||||
pub fn auto_store_bag_item(&mut self, src_bag: u8, src_slot: u8, dst_bag: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUTOSTORE_BAG_ITEM,
|
||||
@@ -132,10 +100,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Split a stack (`CMSG_SPLIT_ITEM`, layout in [`messages::split_item`]): carry `count` off
|
||||
/// `(src_bag, src_slot)` onto `(dst_bag, dst_slot)` — either endpoint may be an equipped bag
|
||||
/// (unlike [`Self::swap_inv_item`]). Success settles both slots via values deltas; refusal
|
||||
/// answers `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
/// `CMSG_SPLIT_ITEM`: move `count` off a stack; either end may be in an equipped bag.
|
||||
pub fn split_item(
|
||||
&mut self,
|
||||
src_bag: u8,
|
||||
@@ -150,9 +115,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Destroy a bag item (`CMSG_DESTROYITEM`, layout in [`messages::destroy_item`]): `count` 0 =
|
||||
/// the whole stack. The delete-confirm popup's `OnAccept` (decision 0216 §3) — no dedicated
|
||||
/// answer packet; the item's disappearance is the ordinary field-update stream.
|
||||
/// `CMSG_DESTROYITEM`: `count` 0 destroys the whole stack; there is no reply packet.
|
||||
pub fn destroy_item(&mut self, bag: u8, slot: u8, count: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_DESTROYITEM,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
//! The session-lifecycle `WorldWriter` sends — the three the client makes about *being logged in*
|
||||
//! rather than about anything in the world: the keepalive, the leave-the-world request, and the
|
||||
//! cinematic ack that gates the world stream on entering it. Split out of `writer/mod.rs`
|
||||
//! (decision 0636).
|
||||
//!
|
||||
//! All three are effectively bodyless (`ping` carries only its two counters), and all three are
|
||||
//! **cadence/obligation** sends rather than player intents: skip the ping and the socket dies,
|
||||
//! skip the cinematic ack and the world around the body despawns.
|
||||
//! The sends about being logged in rather than about the world: the keepalive, logout, the server
|
||||
//! clock and the cinematic acks.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -14,70 +8,45 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Send the ~30 s keepalive (`CMSG_PING`): `sequence` is the ++counter the server echoes back
|
||||
/// as `SMSG_PONG`, `last_rtt_ms` the previous round-trip measurement (the real client's
|
||||
/// lastRtt; the server stores it as our reported latency). Cadence discipline is the caller's:
|
||||
/// vmangos kicks a socket whose pings repeat faster than 27 s apart (`_HandlePing`'s
|
||||
/// overspeed count), so this is a timer send, never a retry.
|
||||
/// `CMSG_PING`, every ~30 s: the server echoes `sequence` in `SMSG_PONG` and stores
|
||||
/// `last_rtt_ms` as our latency. vmangos kicks pings under 27 s apart, so never retry one.
|
||||
pub fn ping(&mut self, sequence: u32, last_rtt_ms: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_PING, &messages::ping(sequence, last_rtt_ms))
|
||||
}
|
||||
|
||||
/// Ask to leave the world back to character select (`CMSG_LOGOUT_REQUEST`, empty body). The
|
||||
/// server answers `SMSG_LOGOUT_RESPONSE` (a refusal while in combat) and, once the logout
|
||||
/// completes (instant for a resting/GM character), `SMSG_LOGOUT_COMPLETE` — which the stream
|
||||
/// surfaces as [`SessionEvent::LoggedOut`](crate::SessionEvent::LoggedOut).
|
||||
/// `CMSG_LOGOUT_REQUEST`, empty: answered by `SMSG_LOGOUT_RESPONSE`, then
|
||||
/// `SMSG_LOGOUT_COMPLETE` when the logout ends (at once for a resting or GM character).
|
||||
pub fn logout_request(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_LOGOUT_REQUEST, &[])
|
||||
}
|
||||
|
||||
/// The FORCED logout (`CMSG_PLAYER_LOGOUT`, empty) — `ForceLogout()`'s packet, the same
|
||||
/// dispatcher as [`Self::logout_request`] with the pending-logout latch bypassed
|
||||
/// (decision 1963).
|
||||
/// `CMSG_PLAYER_LOGOUT`, empty: the forced logout the 1.12 client's `ForceLogout()` sends.
|
||||
pub fn player_logout(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_PLAYER_LOGOUT, &[])
|
||||
}
|
||||
|
||||
/// Call off a pending logout (`CMSG_LOGOUT_CANCEL`, empty body) — the CAMP/QUIT dialog's Cancel
|
||||
/// (decision 0674). Only meaningful while the server's 20-second timer is running (a non-instant
|
||||
/// [`logout_request`](Self::logout_request)); the server drops the timer, unroots the character
|
||||
/// `CMSG_LOGOUT_CANCEL`, empty: the server stops its 20 s logout timer, unroots the character
|
||||
/// and answers `SMSG_LOGOUT_CANCEL_ACK`.
|
||||
pub fn logout_cancel(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_LOGOUT_CANCEL, &[])
|
||||
}
|
||||
|
||||
/// Ask the server for its wall clock (`CMSG_QUERY_TIME`, empty body); answered with
|
||||
/// `SMSG_QUERY_TIME_RESPONSE`, one `u32` of unix-epoch seconds.
|
||||
///
|
||||
/// A cadence/obligation send like [`ping`](Self::ping), not a player intent: the server writes
|
||||
/// *absolute* stamps in its own epoch into descriptor fields — a timed quest's deadline is
|
||||
/// `time(nullptr) + limitTime` — and nothing on the wire ever restates them as a duration. So
|
||||
/// every countdown the client draws is only as right as its last sample of this clock; benilla
|
||||
/// takes one on entering the world (decision 1150).
|
||||
/// `CMSG_QUERY_TIME`, empty: answered with the server's unix time as one `u32`. Descriptor
|
||||
/// deadlines, such as a timed quest's, are absolute server time, so countdowns need this clock.
|
||||
pub fn query_time(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_QUERY_TIME, &messages::query_time())
|
||||
}
|
||||
|
||||
/// Acknowledge a triggered cinematic as finished (`CMSG_COMPLETE_CINEMATIC`, empty body) — the
|
||||
/// packet the real client sends when the cinematic ends or the player ESCs out. Must answer
|
||||
/// every `SMSG_TRIGGER_CINEMATIC` ([`SessionEvent::CinematicTriggered`]
|
||||
/// (crate::SessionEvent::CinematicTriggered)): while one runs unacked, vmangos anchors object
|
||||
/// visibility to the flying cinematic camera and the world around the body despawns.
|
||||
/// `CMSG_COMPLETE_CINEMATIC`, empty: sent when a cinematic ends or is escaped, and owed for
|
||||
/// every `SMSG_TRIGGER_CINEMATIC`; until then vmangos anchors visibility to the cinematic
|
||||
/// camera and the world around the body despawns.
|
||||
pub fn complete_cinematic(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_COMPLETE_CINEMATIC, &[])
|
||||
}
|
||||
|
||||
/// Announce the cinematic shot being armed (`CMSG_NEXT_CINEMATIC_CAMERA`, empty body) — see
|
||||
/// the opcode's own note. Sent once per shot, **including the first**, so a stock single-camera
|
||||
/// race intro sends exactly one; the run still ends with exactly one
|
||||
/// [`complete_cinematic`](Self::complete_cinematic).
|
||||
///
|
||||
/// The reference's send is inside the shot arm `0x48edf0` (`0x48ef11 push 0xfb` → the
|
||||
/// `0x418190` builder → `0x5ab630` flush), immediately before that shot's narration starts —
|
||||
/// *not* inside the shot advance `0x48efe0`, where benilla first placed it. That misreading
|
||||
/// mattered on the shipped path rather than only on a private server's: every 1.12
|
||||
/// `CinematicSequences` row carries exactly one camera, so "between cameras" meant benilla
|
||||
/// sent this packet **never**.
|
||||
/// `CMSG_NEXT_CINEMATIC_CAMERA`, empty: sent as each shot is armed, the first included, just
|
||||
/// before its narration (reference: shot arm `0x48edf0`, not the advance `0x48efe0`). Every
|
||||
/// 1.12 `CinematicSequences` row has one camera, so a race intro sends exactly one.
|
||||
pub fn next_cinematic_camera(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_NEXT_CINEMATIC_CAMERA, &[])
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
//! The loot window's `WorldWriter` sends — open, take a row, take the coin pile, close, and vote
|
||||
//! in a group roll. Bodies in [`crate::messages::loot`], whose scope this mirrors. Split out of
|
||||
//! `writer/mod.rs` (decision 0636).
|
||||
//!
|
||||
//! Two addressing quirks worth keeping together: [`WorldWriter::loot_release`] passes a guid the
|
||||
//! server *ignores* (it releases whatever loot guid it has stored for us), and
|
||||
//! [`WorldWriter::loot_roll`] is addressed by the `(looted_target, item_slot)` pair the roll was
|
||||
//! opened with — never by the client-internal `rollID` the reference UI keys its frames on.
|
||||
//! The loot window sends: open, take, coin, close, roll, and the master looter's give.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -14,18 +7,12 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Open a loot window (`CMSG_LOOT`, layout in [`messages::loot`]) — a full guid naming the
|
||||
/// corpse/creature/player to loot. Answered by `SMSG_LOOT_RESPONSE` (a `LootResponse` event
|
||||
/// on success, `LootError` on refusal).
|
||||
/// `CMSG_LOOT`: open the loot of a corpse, creature or player (`SMSG_LOOT_RESPONSE`).
|
||||
pub fn loot(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_LOOT, &messages::loot(guid))
|
||||
}
|
||||
|
||||
/// Take one loot-window row (`CMSG_AUTOSTORE_LOOT_ITEM`, layout in
|
||||
/// [`messages::autostore_loot_item`]): `loot_slot` is the wire's 0-based row index. The server
|
||||
/// auto-places the item into the first free bag slot; success arrives via the normal
|
||||
/// item-create/values path plus `SMSG_LOOT_REMOVED` clearing the row and
|
||||
/// `SMSG_ITEM_PUSH_RESULT` naming what landed.
|
||||
/// `CMSG_AUTOSTORE_LOOT_ITEM`: take the 0-based row `loot_slot` into the first free bag slot.
|
||||
pub fn autostore_loot_item(&mut self, loot_slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUTOSTORE_LOOT_ITEM,
|
||||
@@ -33,25 +20,19 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Take the loot's coin pile (`CMSG_LOOT_MONEY`, empty body). Answered by
|
||||
/// `SMSG_LOOT_MONEY_NOTIFY` (our share) then `SMSG_LOOT_CLEAR_MONEY` (the coin line clears for
|
||||
/// every looter) plus the coinage rising via `UPDATE_OBJECT`.
|
||||
/// `CMSG_LOOT_MONEY`, empty: answered by `SMSG_LOOT_MONEY_NOTIFY` with our share, then
|
||||
/// `SMSG_LOOT_CLEAR_MONEY` to every looter.
|
||||
pub fn loot_money(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_LOOT_MONEY, &messages::loot_money())
|
||||
}
|
||||
|
||||
/// Close the loot window (`CMSG_LOOT_RELEASE`, layout in [`messages::loot_release`]); the
|
||||
/// server ignores `guid` and releases whatever loot guid it has stored for us instead.
|
||||
/// Answered by `SMSG_LOOT_RELEASE_RESPONSE` (a `LootReleaseResponse` event).
|
||||
/// `CMSG_LOOT_RELEASE`: the server ignores `guid` and releases the loot it has stored for us.
|
||||
pub fn loot_release(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_LOOT_RELEASE, &messages::loot_release(guid))
|
||||
}
|
||||
|
||||
/// Cast a group-loot vote (`CMSG_LOOT_ROLL`, layout in [`messages::loot_roll`]) — the roll is
|
||||
/// addressed by the `(looted_target, item_slot)` pair `SMSG_LOOT_START_ROLL` opened it with,
|
||||
/// never by the client-internal `rollID`. `roll_type` is a [`messages::roll_vote`] value; the
|
||||
/// server drops anything `>= 3` without a reply. Answered by an `SMSG_LOOT_ROLL` broadcast of
|
||||
/// our vote, then the resolution (`SMSG_LOOT_ROLL_WON` / `SMSG_LOOT_ALL_PASSED`).
|
||||
/// `CMSG_LOOT_ROLL`: addressed by the `(looted_target, item_slot)` of `SMSG_LOOT_START_ROLL`,
|
||||
/// not the UI's `rollID`; a [`messages::roll_vote`] of 3 or more is dropped unanswered.
|
||||
pub fn loot_roll(&mut self, looted_target: u64, item_slot: u32, roll_type: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_LOOT_ROLL,
|
||||
@@ -59,15 +40,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Hand one loot-window row to a group member (`CMSG_LOOT_MASTER_GIVE`, layout in
|
||||
/// [`messages::loot_master_give`]) — the master looter's replacement for taking the row
|
||||
/// themselves. `loot_guid` is the open loot source and `slot` the wire's 0-based row index,
|
||||
/// the same one [`WorldWriter::autostore_loot_item`] would carry.
|
||||
///
|
||||
/// Success looks like anyone else's loot from here: `SMSG_LOOT_REMOVED` clears the row for
|
||||
/// every looter, and the *recipient* (not us) gets the item-create traffic. A refusal comes
|
||||
/// back on `SMSG_LOOT_RESPONSE`'s error shape carrying a `MASTER_*`
|
||||
/// [`messages::loot_error`] code.
|
||||
/// `CMSG_LOOT_MASTER_GIVE`: the master looter hands 0-based row `slot` of `loot_guid` to a
|
||||
/// member; a refusal is an `SMSG_LOOT_RESPONSE` error with a `MASTER_*` loot error code.
|
||||
pub fn loot_master_give(&mut self, loot_guid: u64, slot: u8, player_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_LOOT_MASTER_GIVE,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
//! The mail arc's `WorldWriter` sends (decision 0544 P0): the inbox open/refresh, send, take-money/
|
||||
//! take-item/mark-read/return/delete/copy-letter verbs, the letter-body ask-once fetch, and the login
|
||||
//! unread-mail query. Bodies in [`crate::messages`]'s `mail` builders (layout VERIFIED against
|
||||
//! vmangos `Handlers/MailHandler.cpp` + `Server/Packets/Mail.cpp`). Split out of `writer/mod.rs`
|
||||
//! the same way `channel`/`group` are — one clearly separable concern among the writer's domains.
|
||||
//! The mailbox sends. A verb on one mail is acked by `SMSG_SEND_MAIL_RESULT` carrying its action.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -11,9 +7,7 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask the mailbox's inbox page (`CMSG_GET_MAIL_LIST`, layout in [`messages::get_mail_list`]) —
|
||||
/// the window's open verb and its refresh: one mailbox guid. Answered by
|
||||
/// `SMSG_MAIL_LIST_RESULT` (a `MailList` event).
|
||||
/// `CMSG_GET_MAIL_LIST`: opens and refreshes the inbox; answered by `SMSG_MAIL_LIST_RESULT`.
|
||||
pub fn get_mail_list(&mut self, mailbox: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_GET_MAIL_LIST,
|
||||
@@ -21,11 +15,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a mail (`CMSG_SEND_MAIL`, layout in [`messages::send_mail`]) — the Send tab:
|
||||
/// recipient/subject/body, an optional item attachment (`item_guid == 0` = none), money, and
|
||||
/// COD. `stationery`/`package` ride the wire but are discarded server-side (player mail is
|
||||
/// always stored `MAIL_STATIONERY_DEFAULT`). Answered by `SMSG_SEND_MAIL_RESULT` (a
|
||||
/// `SendMailResult` event).
|
||||
/// `CMSG_SEND_MAIL`: `item_guid` 0 attaches nothing. The server discards `stationery` and
|
||||
/// `package`, storing player mail as `MAIL_STATIONERY_DEFAULT`.
|
||||
pub fn send_mail(
|
||||
&mut self,
|
||||
mailbox: u64,
|
||||
@@ -46,9 +37,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Take a mail's attached money (`CMSG_MAIL_TAKE_MONEY`, layout in
|
||||
/// [`messages::mail_take_money`]). Answered by `SMSG_SEND_MAIL_RESULT`
|
||||
/// (action [`messages::mail_action::MONEY_TAKEN`]).
|
||||
/// `CMSG_MAIL_TAKE_MONEY`: acked with [`messages::mail_action::MONEY_TAKEN`].
|
||||
pub fn mail_take_money(&mut self, mailbox: u64, mail_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MAIL_TAKE_MONEY,
|
||||
@@ -56,9 +45,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Take a mail's attached item (`CMSG_MAIL_TAKE_ITEM`, layout in
|
||||
/// [`messages::mail_take_item`]). Answered by `SMSG_SEND_MAIL_RESULT`
|
||||
/// (action [`messages::mail_action::ITEM_TAKEN`]).
|
||||
/// `CMSG_MAIL_TAKE_ITEM`: acked with [`messages::mail_action::ITEM_TAKEN`].
|
||||
pub fn mail_take_item(&mut self, mailbox: u64, mail_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MAIL_TAKE_ITEM,
|
||||
@@ -66,9 +53,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Mark a mail read (`CMSG_MAIL_MARK_AS_READ`, layout in [`messages::mail_mark_as_read`]) —
|
||||
/// sent when a letter opens. **No response packet**: the app flips the local `checked` READ
|
||||
/// bit and repaints.
|
||||
/// `CMSG_MAIL_MARK_AS_READ`: sent when a letter opens. There is no reply, so the caller sets
|
||||
/// the read bit itself.
|
||||
pub fn mail_mark_as_read(&mut self, mailbox: u64, mail_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MAIL_MARK_AS_READ,
|
||||
@@ -76,9 +62,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Return a mail to its sender (`CMSG_MAIL_RETURN_TO_SENDER`, layout in
|
||||
/// [`messages::mail_return_to_sender`]). Answered by `SMSG_SEND_MAIL_RESULT`
|
||||
/// (action [`messages::mail_action::RETURNED`]).
|
||||
/// `CMSG_MAIL_RETURN_TO_SENDER`: acked with [`messages::mail_action::RETURNED`].
|
||||
pub fn mail_return_to_sender(&mut self, mailbox: u64, mail_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MAIL_RETURN_TO_SENDER,
|
||||
@@ -86,9 +70,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Delete a mail (`CMSG_MAIL_DELETE`, layout in [`messages::mail_delete`]) — the taken-letter
|
||||
/// close and the explicit Delete button alike. Answered by `SMSG_SEND_MAIL_RESULT`
|
||||
/// (action [`messages::mail_action::DELETED`]).
|
||||
/// `CMSG_MAIL_DELETE`: acked with [`messages::mail_action::DELETED`].
|
||||
pub fn mail_delete(&mut self, mailbox: u64, mail_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MAIL_DELETE,
|
||||
@@ -96,9 +78,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Make a permanent copy of a letter's body (`CMSG_MAIL_CREATE_TEXT_ITEM`, layout in
|
||||
/// [`messages::mail_create_text_item`]) — the open letter's "Take Attachments" letter button.
|
||||
/// Answered by `SMSG_SEND_MAIL_RESULT` (action [`messages::mail_action::MADE_PERMANENT`]).
|
||||
/// `CMSG_MAIL_CREATE_TEXT_ITEM`: a permanent copy of the letter; acked with
|
||||
/// [`messages::mail_action::MADE_PERMANENT`].
|
||||
pub fn mail_create_text_item(&mut self, mailbox: u64, mail_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MAIL_CREATE_TEXT_ITEM,
|
||||
@@ -106,9 +87,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch a letter's body text (`CMSG_ITEM_TEXT_QUERY`, layout in [`messages::item_text_query`])
|
||||
/// — sent once per mail whose `item_text_id != 0` (ask-once cacheable). Answered by
|
||||
/// `SMSG_ITEM_TEXT_QUERY_RESPONSE` (a `MailItemText` event).
|
||||
/// `CMSG_ITEM_TEXT_QUERY`: a letter's body, asked once per mail with a nonzero `item_text_id`.
|
||||
pub fn item_text_query(&mut self, text_id: u32, mail_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_ITEM_TEXT_QUERY,
|
||||
@@ -116,9 +95,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask whether unread mail is waiting (`MSG_QUERY_NEXT_MAIL_TIME`, EMPTY body — same opcode
|
||||
/// answers with one `f32`: `0.0` unread waiting, `-86400.0` none). Sent once at login to seed
|
||||
/// `HasNewMail()`/the minimap letter icon.
|
||||
/// `MSG_QUERY_NEXT_MAIL_TIME`, empty: the reply is one `f32`, `0.0` when unread mail waits and
|
||||
/// `-86400.0` when none. Sent at login to seed `HasNewMail()`.
|
||||
pub fn query_next_mail_time(&mut self) -> Result<()> {
|
||||
self.send(opcode::MSG_QUERY_NEXT_MAIL_TIME, &[])
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The meeting-stone queue's sends (decisions 1963/2283).
|
||||
//! The meeting-stone queue's sends.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -7,10 +7,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Join the meeting-stone queue (`CMSG 0x292`, `u64 goGuid`) — what a right-click on a
|
||||
/// `GAMEOBJECT_TYPE_MEETINGSTONE` sends once that type's own use-slot validator has passed
|
||||
/// its four client-side refusals (decision 2283). Never `CMSG_GAMEOBJ_USE`: the server has no
|
||||
/// type-23 case for the shared opener.
|
||||
/// `CMSG_MEETINGSTONE_JOIN`: a meeting stone's right-click, after the client's own checks.
|
||||
/// Never `CMSG_GAMEOBJ_USE`: the server has no case for type 23 there.
|
||||
pub fn meeting_stone_join(&mut self, go_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MEETINGSTONE_JOIN,
|
||||
@@ -18,8 +16,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Leave the meeting-stone queue (`CMSG 0x293`, empty) — `CancelMeetingStoneRequest()`'s
|
||||
/// packet, sent by the party leader (or a player in no party).
|
||||
/// `CMSG_MEETINGSTONE_LEAVE`, empty: `CancelMeetingStoneRequest()`, from the party leader or a
|
||||
/// player in no party.
|
||||
pub fn meeting_stone_leave(&mut self) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MEETINGSTONE_LEAVE,
|
||||
@@ -27,8 +25,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask for the meeting-stone status (`CMSG 0x296`, empty) — the enter-world query the
|
||||
/// reference sends once per world session (decision 1974).
|
||||
/// `CMSG_MEETINGSTONE_STATUS_QUERY`, empty: the reference sends it once per world session.
|
||||
pub fn meeting_stone_status_query(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_MEETINGSTONE_STATUS_QUERY, &[])
|
||||
}
|
||||
|
||||
@@ -1,53 +1,5 @@
|
||||
//! The world session's **write half** — [`WorldWriter`] and every outbound verb the client has: one
|
||||
//! method per thing the player can *do*, which is not quite one per opcode (the fifteen chat
|
||||
//! flavours all ride `CMSG_MESSAGECHAT`, and four cast shapes ride `CMSG_CAST_SPELL`).
|
||||
//!
|
||||
//! This file holds only the type, its state, and the one private [`WorldWriter::send`] every verb
|
||||
//! funnels through; the verbs themselves live in per-family sibling modules. The families **mirror
|
||||
//! [`crate::messages`]'s own decomposition**, so the module a body builder lives in names the module
|
||||
//! its send lives in (`messages::vendor::buy_item` ↔ `writer::vendor`'s `buy_item`) — the split rule
|
||||
//! is mechanical rather than a matter of judgment, which is what keeps it from silting back up
|
||||
//! (decision 0636; [`channel`]/[`group`]/[`mail`]/[`trade`]/[`duel`] were peeled off ahead of it,
|
||||
//! starting with decision 0288 phase 1).
|
||||
//!
|
||||
//! | module | the family |
|
||||
//! |---|---|
|
||||
//! | [`self_movement`] | our own mover: the `MSG_MOVE_*` stream + every mover ack |
|
||||
//! | [`lifecycle`] | being logged in: ping, logout, the cinematic ack |
|
||||
//! | [`selection`] | the two sends that set our target: select, inspect |
|
||||
//! | [`pvp`] | the honor system's one ask: another player's honor stats |
|
||||
//! | [`names`] | the ask-once name lookups: player, creature, pet |
|
||||
//! | [`chat`] | the chat frame's wire: every `MESSAGECHAT` flavour + `/played`, `/random`, `/wave` |
|
||||
//! | [`channel`] | channel administration: join/leave/list/moderation |
|
||||
//! | [`spells`] | casting, the four cancels, and the aura cancel |
|
||||
//! | [`attack`] | the auto-attack toggle: melee start/stop, ranged auto-repeat stop |
|
||||
//! | [`action_bar`] | the one client-authoritative slot write |
|
||||
//! | [`pet`] | the pet bar's four intents: press, autocast, call off, drag |
|
||||
//! | [`progression`] | the talent spend |
|
||||
//! | [`skills`] | the skills pane's abandon |
|
||||
//! | [`pose`] | the client-volunteered body state: sheath, stand, the mounted flourish |
|
||||
//! | [`items`] | bags and equipment: use, equip, swap/split/destroy, the template ask |
|
||||
//! | [`loot`] | the loot window: open, take, coin, close, roll |
|
||||
//! | [`death`] | the corpse run: release, corpse query, reclaim, spirit healer, res answer |
|
||||
//! | [`gm_ticket`] | the Help window: file, edit, ask, abandon, queue status |
|
||||
//! | [`gossip`] | the front door to every NPC service window |
|
||||
//! | [`vendor`] | buy, sell, buy back, repair |
|
||||
//! | [`bank`] | open, buy a slot, deposit, withdraw |
|
||||
//! | [`trainer`] | the service list refresh + the purchase |
|
||||
//! | [`stable`] | the hunter stable: the list refresh, stable/unstable/swap, buy a slot |
|
||||
//! | [`taxi`] | the flight master: status, map, the two flight verbs |
|
||||
//! | [`quest`] | the questgiver dialog walk + the two quest-log verbs |
|
||||
//! | [`gameobject`] | the one USE verb + the template ask |
|
||||
//! | [`group`] | party/raid: invite, kick, leader, loot method, icons, ready checks |
|
||||
//! | [`guild`] | the guild: the two cache asks, invitations, member verbs, rank administration |
|
||||
//! | [`instance`] | the one lockout verb: reset all instances |
|
||||
//! | [`trade`] | the player-trade dance |
|
||||
//! | [`duel`] | accept and cancel |
|
||||
//! | [`mail`] | the mailbox |
|
||||
//! | [`auction`] | the auction house: the hello, the three list pages, sell/bid/cancel |
|
||||
//! | [`player_flags`] | the empty-bodied `PLAYER_FLAGS` toggles: PvP, show-helm, show-cloak |
|
||||
//! | [`social`] | friends, ignores, and `/who` |
|
||||
//! | [`area_trigger`] | the one "I walked into trigger N" report |
|
||||
//! The world session's write half: [`WorldWriter`] and one method per thing the player can do.
|
||||
//! Each family module mirrors its namesake in `crate::messages`, which builds the bodies.
|
||||
|
||||
use std::net::TcpStream;
|
||||
|
||||
@@ -101,39 +53,23 @@ mod trainer;
|
||||
mod tutorial;
|
||||
mod vendor;
|
||||
|
||||
/// Write half of a split [`WorldSession`](super::WorldSession) — owns a cloned socket + the encrypter. Used to send our own
|
||||
/// movement (`MSG_MOVE_*`). The active player must already be the confirmed mover (see
|
||||
/// [`WorldSession::set_active_mover`](super::WorldSession::set_active_mover)). Coordinates are raw WoW yards; `orientation` is radians.
|
||||
/// Write half of a split [`WorldSession`](super::WorldSession): a cloned socket and the encrypter.
|
||||
/// Movement sends need the active player confirmed as mover first
|
||||
/// ([`WorldSession::set_active_mover`](super::WorldSession::set_active_mover)). Positions are raw
|
||||
/// WoW yards; `orientation` is radians.
|
||||
pub struct WorldWriter {
|
||||
pub(super) stream: TcpStream,
|
||||
pub(super) encrypter: EncrypterHalf,
|
||||
/// Every packet that has actually reached the socket since the last drain, as
|
||||
/// `(opcode, body length)` — armed by [`Self::watch_sends`], `None` (and free) otherwise.
|
||||
///
|
||||
/// The **outbound twin of the app's inbound `in` trace**. Inbound, every packet is accounted
|
||||
/// for by name the moment it arrives; outbound there was nothing of the kind — the mover's
|
||||
/// `snd` lines record a *decision* taken before the command is even queued, and the `wire` tag
|
||||
/// records only failures, so "did we actually send X?" could be answered only by inference
|
||||
/// from a silence. That is a poor instrument for a client whose fidelity is partly *what it
|
||||
/// puts on the wire*: decision 1872 landed a whole feature — the GameObject questgiver query —
|
||||
/// whose entire observable effect is a send, and the only way to see it from inside was to add
|
||||
/// state to a game resource.
|
||||
///
|
||||
/// Recorded **after** a successful write, never before, so a line here means a transmission
|
||||
/// and not an intention.
|
||||
/// Every packet that reached the socket since the last drain, as `(opcode, body length)`;
|
||||
/// `None` until [`Self::watch_sends`]. Pushed only after a successful write.
|
||||
pub(super) sent: Option<Vec<(u16, usize)>>,
|
||||
/// The language every chat send carries — the logged-in character's faction tongue,
|
||||
/// inherited from the session at split (set by `WorldSession::player_login` off the roster's
|
||||
/// race). Load-bearing: vmangos drops the whole message (dot-commands included) when the
|
||||
/// character doesn't know the language — a Horde character sending Common gets only an
|
||||
/// `SMSG_NOTIFICATION` back, never the say / the command.
|
||||
/// The character's faction language, set at login from its race; every chat send carries it.
|
||||
/// vmangos drops the whole message, dot-commands included, when the character does not know it.
|
||||
pub(super) chat_language: u32,
|
||||
}
|
||||
|
||||
impl WorldWriter {
|
||||
/// Frame, encrypt, and write one packet. Private, and the sole write path — every verb in the
|
||||
/// family modules goes through here, so there is exactly one place framing/encryption happens.
|
||||
/// Visible to them because a private item is in scope throughout its module's descendants.
|
||||
/// Frame, encrypt and write one packet: the sole write path every verb goes through.
|
||||
fn send(&mut self, opcode: u16, body: &[u8]) -> Result<()> {
|
||||
let sent = send_packet(&mut self.stream, Some(&mut self.encrypter), opcode, body);
|
||||
if sent.is_ok() {
|
||||
@@ -144,14 +80,12 @@ impl WorldWriter {
|
||||
sent
|
||||
}
|
||||
|
||||
/// Start recording what reaches the socket — see [`Self::sent`]. Idempotent; a writer that is
|
||||
/// already watching keeps whatever it has not yet handed over.
|
||||
/// Start recording what reaches the socket; a second call keeps what is not yet drained.
|
||||
pub fn watch_sends(&mut self) {
|
||||
self.sent.get_or_insert_with(Vec::new);
|
||||
}
|
||||
|
||||
/// Hand over (and clear) everything recorded since the last call — `(opcode, body length)` per
|
||||
/// packet, in send order. A no-op on a writer that is not watching.
|
||||
/// Hand over and clear the recorded `(opcode, body length)` pairs, in send order.
|
||||
pub fn drain_sent(&mut self, mut each: impl FnMut(u16, usize)) {
|
||||
if let Some(log) = &mut self.sent {
|
||||
for (opcode, len) in log.drain(..) {
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
//! The name-lookup `WorldWriter` sends — the three ask-once queries that turn a guid into
|
||||
//! something displayable: a player's name/race/gender/class, a creature template's name/subname,
|
||||
//! and a pet's given name. Bodies in [`crate::messages`]'s `full_guid`/`creature_query`/
|
||||
//! `pet_name_query` builders. Split out of `writer/mod.rs` (decision 0636).
|
||||
//!
|
||||
//! Three verbs rather than one because the guid's own bits decide which can answer: a creature's
|
||||
//! guid embeds its template entry ([`crate::guid::entry`]) while a pet's embeds a pet number
|
||||
//! ([`crate::guid::pet_number`]), so [`WorldWriter::creature_query`] cannot name a pet and
|
||||
//! [`WorldWriter::pet_name_query`] is the only thing that can. Every answer is cacheable forever
|
||||
//! (a name never changes under a guid), which is what makes these the ask-once family.
|
||||
//! The ask-once lookups that turn a guid into a player, creature or pet name.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -16,15 +7,12 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask for a player character's name/race/gender/class (`CMSG_NAME_QUERY`, a full 8-byte guid —
|
||||
/// vmangos `QueryPlayerName::ReadFromWorldPacket`). Answered by `SMSG_NAME_QUERY_RESPONSE`.
|
||||
/// `CMSG_NAME_QUERY`: a player's name, race, gender and class, by full 8-byte guid.
|
||||
pub fn name_query(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_NAME_QUERY, &messages::full_guid(guid))
|
||||
}
|
||||
|
||||
/// Ask for a creature template's name/subname (`CMSG_CREATURE_QUERY`: entry + guid). The `entry`
|
||||
/// is the one embedded in the creature's guid bits 24–47 ([`crate::guid::entry`]). Answered by
|
||||
/// `SMSG_CREATURE_QUERY_RESPONSE`.
|
||||
/// `CMSG_CREATURE_QUERY`: a template's name and subname; `entry` is guid bits 24 to 47.
|
||||
pub fn creature_query(&mut self, entry: u32, guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_CREATURE_QUERY,
|
||||
@@ -32,10 +20,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask for a pet's name (`CMSG_PET_NAME_QUERY`: pet number + guid). A pet's guid holds a pet
|
||||
/// number where a creature's holds its template entry ([`crate::guid::pet_number`]), so
|
||||
/// [`Self::creature_query`] cannot name one — this is the only query that can. Answered by
|
||||
/// `SMSG_PET_NAME_QUERY_RESPONSE`, or by silence if the pet is gone.
|
||||
/// `CMSG_PET_NAME_QUERY`: the only way to name a pet, whose guid holds a pet number, not an
|
||||
/// entry. A pet that is gone gets no answer.
|
||||
pub fn pet_name_query(&mut self, pet_number: u32, guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PET_NAME_QUERY,
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
//! The pet's `WorldWriter` sends — mirroring [`crate::messages::pet`] (decisions 0982, 0988, 1066).
|
||||
//!
|
||||
//! Six verbs, and the thing to know about the bar's four is that **no reply packet answers any of
|
||||
//! them**. The first three are intents the client has already applied to its own state before sending
|
||||
//! (wow-re §10.1/§10.2: the reaction, command and autocast paths all write `[0xb71468]` or the
|
||||
//! slot word and fire `PET_BAR_UPDATE` *before* the packet leaves). The bar's contents change only
|
||||
//! when a fresh `SMSG_PET_SPELLS` arrives — a summon, a swap, a learned spell — never as a reply
|
||||
//! to one of these.
|
||||
//!
|
||||
//! [`WorldWriter::pet_cancel_aura`] is the exception worth naming: it too gets no reply, but it is
|
||||
//! the one verb the client does *not* pre-apply, because what it changes is the pet's own
|
||||
//! descriptor. The aura leaves in a `UNIT_FIELD_AURA` delta, and the slot's icon follows from
|
||||
//! that — so the round trip is visible, just not as an answer.
|
||||
//!
|
||||
//! The menu's two ([`WorldWriter::pet_abandon`], [`WorldWriter::pet_rename`]) invert that: neither
|
||||
//! is pre-applied, because what each asks for is not a UI state the server ignores but a change to
|
||||
//! the world that it may refuse — and the answer arrives as the pet leaving, or as a bumped name
|
||||
//! timestamp in its descriptor.
|
||||
//!
|
||||
//! Every one carries the pet's guid: the server re-checks ownership on each
|
||||
//! (`PetHandler.cpp`'s `GetCharmerOrOwnerGuid` gate) and silently drops anything naming a unit we
|
||||
//! don't control.
|
||||
//! The pet sends. No reply answers a bar send: the 1.12 client applies the press to its own state
|
||||
//! (`0xb71468`, `PET_BAR_UPDATE`) before sending, and the bar changes only on a fresh
|
||||
//! `SMSG_PET_SPELLS`. The server drops a send naming a pet we do not control (`PetHandler.cpp`).
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -28,10 +9,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Press one pet bar slot (`CMSG_PET_ACTION`, layout in [`messages::pet_action`]) — the one
|
||||
/// verb behind all three slot classes, because the server dispatches on the **type byte inside
|
||||
/// the word we echo**: a command token runs `HandlePetCommand`, a reaction token sets the react
|
||||
/// state, a spell casts. `target` is the unit the action should aim at, or `0`.
|
||||
/// `CMSG_PET_ACTION`: press a bar slot. The server dispatches on the type byte of the echoed
|
||||
/// word (command, reaction or spell); `target_guid` is 0 for none.
|
||||
pub fn pet_action(&mut self, pet_guid: u64, packed: u32, target_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PET_ACTION,
|
||||
@@ -39,8 +18,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Call the pet off its target (`CMSG_PET_STOP_ATTACK`, layout in
|
||||
/// [`messages::pet_stop_attack`]) — the Attack button's second press.
|
||||
/// `CMSG_PET_STOP_ATTACK`: the Attack button's second press.
|
||||
pub fn pet_stop_attack(&mut self, pet_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PET_STOP_ATTACK,
|
||||
@@ -48,13 +26,9 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel one of the **pet's** auras (`CMSG_PET_CANCEL_AURA`, layout in
|
||||
/// [`messages::pet_cancel_aura`]) — a pet bar spell click that lands on a spell already running
|
||||
/// on the pet (wow-re §10.1, `0x4bd25f`; the predicate is `0x4bcea0`).
|
||||
///
|
||||
/// The click that sends this sends **nothing else**: the reference returns straight after,
|
||||
/// so the ordinary `CMSG_PET_ACTION` never leaves. That "and return" is the whole behaviour —
|
||||
/// clicking a lit pet buff takes it off rather than re-casting it.
|
||||
/// `CMSG_PET_CANCEL_AURA`: a bar click on a spell already on the pet sends only this, not
|
||||
/// `CMSG_PET_ACTION` (reference: `0x4bd25f`, predicate `0x4bcea0`). Not pre-applied: the aura
|
||||
/// leaves by a `UNIT_FIELD_AURA` delta.
|
||||
pub fn pet_cancel_aura(&mut self, pet_guid: u64, spell_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PET_CANCEL_AURA,
|
||||
@@ -62,13 +36,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Write one or two pet bar slots (`CMSG_PET_SET_ACTION`, layout — and the reason this one
|
||||
/// opcode carries two different verbs — in [`messages::pet_set_action`]).
|
||||
///
|
||||
/// **The autocast toggle is this send**, with one entry: the pressed slot and its word with
|
||||
/// bit 30 flipped. The drag is the same send with one or two. Unlike the player bar's
|
||||
/// [`WorldWriter::set_action_button`], a swap here IS atomic — the server tells the forms
|
||||
/// apart by body size and applies both entries together.
|
||||
/// `CMSG_PET_SET_ACTION`: one entry toggles autocast (the slot's word with bit 30 flipped); a
|
||||
/// drag sends one or two, and the server applies a two-entry swap atomically.
|
||||
pub fn pet_set_action(&mut self, pet_guid: u64, entries: &[(u32, u32)]) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PET_SET_ACTION,
|
||||
@@ -76,14 +45,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Flip one pet **spellbook** entry's autocast (`CMSG_PET_SPELL_AUTOCAST`, layout in
|
||||
/// [`messages::pet_spell_autocast`]) — `ToggleSpellAutocast`'s send (decision 1032).
|
||||
///
|
||||
/// The fifth verb, and the one easy to mistake for the fourth: the pet BAR's autocast right
|
||||
/// click is [`WorldWriter::pet_set_action`] with one entry (a slot and its word), while this
|
||||
/// names a **spell id**, because the book it comes from has no slots. Same bit, same
|
||||
/// server-side effect; different opcode, different body, different handler. Like the rest of
|
||||
/// this file: no reply.
|
||||
/// `CMSG_PET_SPELL_AUTOCAST`: `ToggleSpellAutocast` from the pet spellbook, by spell id. The
|
||||
/// bar's autocast click is `pet_set_action` instead.
|
||||
pub fn pet_spell_autocast(
|
||||
&mut self,
|
||||
pet_guid: u64,
|
||||
@@ -96,24 +59,14 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Give the pet up (`CMSG_PET_ABANDON`, layout in [`messages::pet_abandon`]) — the right-click
|
||||
/// menu's Abandon **and** its Dismiss, which are one message (decision 1066).
|
||||
///
|
||||
/// Unlike everything above it this one is not pre-applied and does not need to be: what answers
|
||||
/// it is the pet leaving — `SMSG_PET_SPELLS` with a zero guid tears the bar down, and the
|
||||
/// object's own removal takes the frame with it. Optimism would only race that.
|
||||
/// `CMSG_PET_ABANDON`: both Abandon and Dismiss. Not pre-applied: the pet leaving answers it,
|
||||
/// with `SMSG_PET_SPELLS` carrying a zero guid.
|
||||
pub fn pet_abandon(&mut self, pet_guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_PET_ABANDON, &messages::pet_abandon(pet_guid))
|
||||
}
|
||||
|
||||
/// Rename the pet (`CMSG_PET_RENAME`, layout in [`messages::pet_rename`]) — the `RENAME_PET` →
|
||||
/// `PETRENAMECONFIRM` popup chain's payload.
|
||||
///
|
||||
/// Also not pre-applied, and here that is load-bearing: the server validates the name
|
||||
/// (`ObjectMgr::CheckPetName`, the reserved-name list) and may refuse it outright, so a client
|
||||
/// that wrote the new name locally would show a name the world does not agree with. What
|
||||
/// arrives on success is a bumped `UNIT_FIELD_PET_NAME_TIMESTAMP` in the pet's descriptor,
|
||||
/// which is what makes the name cache re-ask.
|
||||
/// `CMSG_PET_RENAME`: not pre-applied, as the server may refuse the name
|
||||
/// (`ObjectMgr::CheckPetName`). Success bumps `UNIT_FIELD_PET_NAME_TIMESTAMP`.
|
||||
pub fn pet_rename(&mut self, pet_guid: u64, name: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PET_RENAME,
|
||||
@@ -121,8 +74,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Unlearn the pet's skills at its trainer (`CMSG_PET_UNLEARN`) — `ConfirmPetUnlearn()`'s
|
||||
/// packet, carrying the latched trainer guid (decision 1963).
|
||||
/// `CMSG_PET_UNLEARN`: `ConfirmPetUnlearn()`, carrying the latched trainer guid.
|
||||
pub fn pet_unlearn(&mut self, trainer: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_PET_UNLEARN, &messages::pet_unlearn(trainer))
|
||||
}
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
//! The petition family's `WorldWriter` sends — buying a guild charter, showing it, signing it,
|
||||
//! offering it, renaming it and turning it in (decision 1672).
|
||||
//!
|
||||
//! [`super::guild`] is *being* in a guild; this is *founding* one, and at 1.12 the two share only
|
||||
//! their error channel. Three shapes bind every caller here:
|
||||
//!
|
||||
//! - **A charter is addressed by its ITEM guid**, not by a petition id — sign, offer, rename,
|
||||
//! decline and turn-in all take the item. The petition *id* appears in exactly one place,
|
||||
//! [`WorldWriter::petition_query`], and it is not derivable from the item without either a
|
||||
//! preceding `SMSG_PETITION_SHOW_SIGNATURES` or a read of the item's enchantment slot 0
|
||||
//! (see [`crate::messages::petition`]'s module doc).
|
||||
//! - **Success is mostly silent.** Buying answers with nothing but the new item; offering answers
|
||||
//! the *target*, not us; renaming echoes only if it took. What comes back on a refusal is an
|
||||
//! `SMSG_GUILD_COMMAND_RESULT`, borrowed from the guild family — so a caller that only listens
|
||||
//! for petition opcodes will see a failed verb as a hang.
|
||||
//! - **Every one of these can be refused silently** by a range or ownership check the client
|
||||
//! cannot see (`GetNPCIfCanInteractWith`, "do we hold that item", "are we the owner"). None of
|
||||
//! them is safe to treat as applied at the send.
|
||||
//! The guild charter sends. A charter is addressed by its item guid; only `petition_query` takes
|
||||
//! the petition id. Success is mostly silent, a refusal comes back as the guild family's
|
||||
//! `SMSG_GUILD_COMMAND_RESULT`, and a failed range or ownership check drops a send unanswered.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -24,13 +9,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask a petitioner NPC what charters it sells (`CMSG_PETITION_SHOWLIST`), answered by
|
||||
/// `SMSG_PETITION_SHOWLIST`.
|
||||
///
|
||||
/// This is the **second** way that answer arrives: selecting the NPC's
|
||||
/// `GOSSIP_OPTION_PETITIONER` row makes the server close the gossip menu and push the same
|
||||
/// packet unasked (`Player.cpp:12428-12431`). So a client that only ever talks through gossip
|
||||
/// never needs this send — it is here for the direct re-open, and because the opcode exists.
|
||||
/// `CMSG_PETITION_SHOWLIST`: answered by `SMSG_PETITION_SHOWLIST`, which the petitioner's
|
||||
/// gossip option also pushes unasked (`Player.cpp:12428`).
|
||||
pub fn petition_show_list(&mut self, npc: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PETITION_SHOWLIST,
|
||||
@@ -38,18 +18,9 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Buy a guild charter from a petitioner NPC (`CMSG_PETITION_BUY`) under the given name.
|
||||
///
|
||||
/// **Nothing acknowledges a successful buy.** The server takes the money, stores item
|
||||
/// [`messages::CHARTER_ITEM_ENTRY`] and sends only the ordinary `SMSG_ITEM_PUSH_RESULT`
|
||||
/// (`Handlers/PetitionsHandler.cpp:130`). A refusal, by contrast, is loud and arrives on three
|
||||
/// *different* opcodes depending on which check failed: `SMSG_GUILD_COMMAND_RESULT` for a
|
||||
/// taken or invalid name, `SMSG_BUY_FAILED` for too little money, `SMSG_INVENTORY_CHANGE_FAILURE`
|
||||
/// for a full bag.
|
||||
///
|
||||
/// The name is capped at [`messages::CHARTER_NAME_MAX_LENGTH`] characters by the server's own
|
||||
/// validator; the caller enforces that at the edit box, as the reference does, rather than
|
||||
/// truncating here.
|
||||
/// `CMSG_PETITION_BUY`, name capped by the caller: success sends only `SMSG_ITEM_PUSH_RESULT`
|
||||
/// (`PetitionsHandler.cpp:130`); a refusal is `SMSG_GUILD_COMMAND_RESULT` (name),
|
||||
/// `SMSG_BUY_FAILED` (money) or `SMSG_INVENTORY_CHANGE_FAILURE` (full bag).
|
||||
pub fn petition_buy(&mut self, npc: u64, name: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PETITION_BUY,
|
||||
@@ -57,12 +28,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Open a charter and see who has signed it (`CMSG_PETITION_SHOW_SIGNATURES`), answered by
|
||||
/// `SMSG_PETITION_SHOW_SIGNATURES`.
|
||||
///
|
||||
/// Dropped **silently** if we are already in a guild or do not hold that item
|
||||
/// (`Handlers/PetitionsHandler.cpp:140`, `:143`), so a caller must not wait on the answer as
|
||||
/// though it were guaranteed.
|
||||
/// `CMSG_PETITION_SHOW_SIGNATURES`: dropped unanswered if we are in a guild or do not hold the
|
||||
/// item (`PetitionsHandler.cpp:140`).
|
||||
pub fn petition_show_signatures(&mut self, item: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PETITION_SHOW_SIGNATURES,
|
||||
@@ -70,14 +37,9 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Sign somebody's charter (`CMSG_PETITION_SIGN`), answered by `SMSG_PETITION_SIGN_RESULTS` —
|
||||
/// which the server sends to the **owner too**, identical, with the signer's guid in both
|
||||
/// copies.
|
||||
///
|
||||
/// Not every refusal comes back on that opcode: an already-guilded or cross-faction signer is
|
||||
/// answered with an `SMSG_GUILD_COMMAND_RESULT` instead
|
||||
/// (`Handlers/PetitionsHandler.cpp:252`, `:258`).
|
||||
/// `arg` is the byte the optional Lua argument rides on; the client's own default is `1`.
|
||||
/// `CMSG_PETITION_SIGN`: `SMSG_PETITION_SIGN_RESULTS` goes to us and the owner alike; a
|
||||
/// guilded or cross-faction signer gets `SMSG_GUILD_COMMAND_RESULT` instead
|
||||
/// (`PetitionsHandler.cpp:252`). `arg` carries the optional Lua argument, 1 by default.
|
||||
pub fn petition_sign(&mut self, item: u64, arg: i8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PETITION_SIGN,
|
||||
@@ -85,12 +47,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Show our charter to another player so they can sign it (`CMSG_OFFER_PETITION`).
|
||||
///
|
||||
/// The success path answers **them**, not us: the server sends *the target* an
|
||||
/// `SMSG_PETITION_SHOW_SIGNATURES` (`Handlers/PetitionsHandler.cpp:390-397`). We hear only
|
||||
/// about refusals, as `SMSG_GUILD_COMMAND_RESULT`. So there is nothing local to update on the
|
||||
/// send, and no answer to wait for.
|
||||
/// `CMSG_OFFER_PETITION`: the target gets `SMSG_PETITION_SHOW_SIGNATURES`; we hear back only on
|
||||
/// a refusal (`PetitionsHandler.cpp:390`).
|
||||
pub fn offer_petition(&mut self, item: u64, player: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_OFFER_PETITION,
|
||||
@@ -98,17 +56,9 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Turn a completed charter in to a guild registrar (`CMSG_TURN_IN_PETITION`), answered by
|
||||
/// `SMSG_TURN_IN_PETITION_RESULTS`.
|
||||
///
|
||||
/// Only the charter's **owner** may do this; anyone else is refused silently
|
||||
/// (`Handlers/PetitionsHandler.cpp:432`). And a guild-name collision answers with an
|
||||
/// `SMSG_GUILD_COMMAND_RESULT` and **no results packet at all** (`:445`) — so the absence of a
|
||||
/// result is itself an outcome, not a dropped packet.
|
||||
///
|
||||
/// On success the guild is created with every signer as a member, and the charter item is
|
||||
/// destroyed — which arrives as an ordinary inventory update, plus the guild family's own
|
||||
/// roster push.
|
||||
/// `CMSG_TURN_IN_PETITION`: answered by `SMSG_TURN_IN_PETITION_RESULTS`, except that a
|
||||
/// non-owner gets nothing (`PetitionsHandler.cpp:432`) and a taken guild name gets only
|
||||
/// `SMSG_GUILD_COMMAND_RESULT` (`:445`).
|
||||
pub fn turn_in_petition(&mut self, item: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_TURN_IN_PETITION,
|
||||
@@ -116,12 +66,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask for a petition's record by id (`CMSG_PETITION_QUERY`), answered by
|
||||
/// `SMSG_PETITION_QUERY_RESPONSE` — the **only** packet that carries the proposed guild's name
|
||||
/// and the signature requirement.
|
||||
///
|
||||
/// The `item` field is read and then ignored by vmangos, which looks the petition up by id
|
||||
/// alone (`Handlers/PetitionsHandler.cpp:171-185`); it is sent at its true value anyway.
|
||||
/// `CMSG_PETITION_QUERY`: the only source of the proposed guild name and signature count.
|
||||
/// vmangos ignores `item` and looks the petition up by id (`PetitionsHandler.cpp:171`).
|
||||
pub fn petition_query(&mut self, petition_id: u32, item: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PETITION_QUERY,
|
||||
@@ -129,11 +75,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Rename a charter (`MSG_PETITION_RENAME`), echoed back **only if it took**; a rejected name
|
||||
/// comes back as an `SMSG_GUILD_COMMAND_RESULT` and no echo.
|
||||
///
|
||||
/// vmangos does **no ownership check** here — anyone holding the item may rename it
|
||||
/// (`Handlers/PetitionsHandler.cpp:187-216`).
|
||||
/// `MSG_PETITION_RENAME`: echoed only on success, else `SMSG_GUILD_COMMAND_RESULT`. vmangos
|
||||
/// lets anyone holding the item rename it (`PetitionsHandler.cpp:187`).
|
||||
pub fn petition_rename(&mut self, item: u64, name: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_PETITION_RENAME,
|
||||
@@ -141,9 +84,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Decline a charter someone offered us (`MSG_PETITION_DECLINE`). We send the charter's item
|
||||
/// guid; the server forwards **our** guid to its owner, which is why one opcode has two
|
||||
/// bodies.
|
||||
/// `MSG_PETITION_DECLINE`: we send the item guid; the owner gets ours on the same opcode.
|
||||
pub fn petition_decline(&mut self, item: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_PETITION_DECLINE,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
//! The **`PLAYER_FLAGS` toggles** — the three empty-bodied sends by which the player asks the
|
||||
//! server to flip one bit of their own `PLAYER_FLAGS`: the PvP flag (decision 0646), and the two
|
||||
//! equipment-display preferences, show-helm and show-cloak (decision 1472).
|
||||
//!
|
||||
//! What makes these one family, and unlike [`super::pose`]'s client-volunteered body state: **the
|
||||
//! server owns the bit, and the only answer is the descriptor.** Nothing here has an ack, nothing
|
||||
//! here has a body — the packet *is* the verb — and each one's effect comes back as a field in the
|
||||
//! next `SMSG_UPDATE_OBJECT`. So there is no body builder in [`crate::messages`] to mirror, and a
|
||||
//! caller that wants a *specific* state rather than a flip must compare against the flag it is
|
||||
//! already holding and send only on a difference.
|
||||
//! The empty-bodied toggles that ask the server to flip a bit of our own `PLAYER_FLAGS`. There is
|
||||
//! no ack, only the next descriptor update, so to reach a given state send only when it differs.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -16,24 +8,19 @@ use crate::messages::opcode;
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask the server to flip our own PvP flag (`CMSG_TOGGLE_PVP`, empty body) — `/pvp` and the
|
||||
/// unit popup's PvP row. There is no ack and no immediate local effect: flagging *on* comes
|
||||
/// back as the `UNIT_FIELD_FLAGS` PvP bit within the next descriptor update, while flagging
|
||||
/// *off* only clears the preference — the flag itself survives until vmangos' 300 s drop
|
||||
/// timer expires (`Player::UpdatePvP`). The client predicts neither.
|
||||
/// `CMSG_TOGGLE_PVP`: flagging on shows as the `UNIT_FIELD_FLAGS` PvP bit; flagging off clears
|
||||
/// only the wish, and the flag drops after vmangos's 300 s timer (`Player::UpdatePvP`).
|
||||
pub fn toggle_pvp(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_TOGGLE_PVP, &[])
|
||||
}
|
||||
|
||||
/// Ask the server to flip our show-helm preference (`CMSG_TOGGLE_HELM`, empty body) — the
|
||||
/// Options window's *Show Helm* row, through the VM's intent queue. The answer is
|
||||
/// `PLAYER_FLAGS`' `HIDE_HELM` bit in the next descriptor update, which is what every client
|
||||
/// in range dresses our body from (decision 1472).
|
||||
/// `CMSG_TOGGLE_HELM`: answered by the `PLAYER_FLAGS` `HIDE_HELM` bit, which every client in
|
||||
/// range dresses our body from.
|
||||
pub fn toggle_helm(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_TOGGLE_HELM, &[])
|
||||
}
|
||||
|
||||
/// The cloak half of [`Self::toggle_helm`] (`CMSG_TOGGLE_CLOAK`, empty body).
|
||||
/// `CMSG_TOGGLE_CLOAK`: the cloak half of [`Self::toggle_helm`].
|
||||
pub fn toggle_cloak(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_TOGGLE_CLOAK, &[])
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
//! The three **client-volunteered** pose `WorldWriter` sends — sheath, stand state, and the
|
||||
//! mounted flourish. Bodies in [`crate::messages`]'s `set_sheathed`/`stand_state_change` builders
|
||||
//! (the flourish is bodyless). Split out of `writer/mod.rs` (decision 0636).
|
||||
//!
|
||||
//! What makes these one family, and unlike every other send here: **the client decides, the server
|
||||
//! only relays.** Nothing validates them and nothing answers them — the sheath byte and the stand
|
||||
//! byte land in our own `UNIT_FIELD_BYTES_2`/`BYTES_1` and every observer reads the pose from
|
||||
//! there (decisions 0080/0080c), and the flourish is a pure broadcast the sender plays locally at
|
||||
//! send time. So the *only* consequence of getting one wrong is that other players see the wrong
|
||||
//! body.
|
||||
//! The client-volunteered pose sends: sheath, stand state and the mount flourish. None gets a
|
||||
//! reply; other clients read the pose from our descriptor.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -16,19 +8,15 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Tell the server our sheath state (`CMSG_SETSHEATHED`): `state` 0 unarmed/stowed, 1 melee
|
||||
/// drawn, 2 ranged drawn. Purely client-volunteered (body in [`messages::set_sheathed`]) — it
|
||||
/// becomes our own `UNIT_FIELD_BYTES_2` sheath byte, which other clients read. Sent by the
|
||||
/// manual toggle, the attack-start auto-draw, and every reconcile force (byte-verified: the
|
||||
/// setter `0x611cf0` sends it whenever `bFireEvent` — wow-re `sheath-policy.md`,
|
||||
/// decision 0080).
|
||||
/// `CMSG_SETSHEATHED`: `state` 0 stowed, 1 melee drawn, 2 ranged drawn, stored as our
|
||||
/// `UNIT_FIELD_BYTES_2` sheath byte. The reference setter `0x611cf0` sends it whenever
|
||||
/// `bFireEvent` is set.
|
||||
pub fn set_sheathed(&mut self, state: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_SETSHEATHED, &messages::set_sheathed(state))
|
||||
}
|
||||
|
||||
/// Tell the server our stand state (`CMSG_STANDSTATECHANGE`): 0 stand · 1 sit · 3 sleep ·
|
||||
/// 8 kneel (the only values vmangos accepts). Client-volunteered like sheath — the echo into
|
||||
/// `UNIT_FIELD_BYTES_1` byte 0 drives every observer's sit/stand pose (decision 0080c).
|
||||
/// `CMSG_STANDSTATECHANGE`: vmangos accepts only 0 stand, 1 sit, 3 sleep and 8 kneel, stored
|
||||
/// as `UNIT_FIELD_BYTES_1` byte 0.
|
||||
pub fn stand_state_change(&mut self, state: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_STANDSTATECHANGE,
|
||||
@@ -36,10 +24,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// The mounted space-bar flourish (`CMSG_MOUNTSPECIAL_ANIM`, EMPTY body — VERIFIED vmangos
|
||||
/// `HandleMountSpecialAnimOpcode`). The sender plays its own MountSpecial(94) locally at
|
||||
/// send time and self-suppresses the broadcast echo (decision 0441 P2 — whether the echo
|
||||
/// arrives is a server-config detail).
|
||||
/// `CMSG_MOUNTSPECIAL_ANIM`, empty: the mounted space-bar flourish. The sender plays
|
||||
/// `MountSpecial` (94) itself at send time and ignores the broadcast echo.
|
||||
pub fn mount_special(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_MOUNTSPECIAL_ANIM, &[])
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
//! The character-progression `WorldWriter` send — the talent spend, mirroring
|
||||
//! [`crate::messages::progression`]. Split out of [`super::spells`] by decision 0640.
|
||||
//!
|
||||
//! The rest of that family is inbound only: XP awards and the level-up summary arrive as packets,
|
||||
//! and the points they grant go back out through this one verb — plus the one that gives them all
|
||||
//! back, the respec answer (decision 1580). The skills half of "what I have learned" has its own
|
||||
//! opcode and its own file, [`super::skills`].
|
||||
//! The talent sends: spending points and confirming a respec.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -13,10 +7,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Spend talent points (`CMSG_LEARN_TALENT`, layout in [`messages::learn_talent`]): the
|
||||
/// `Talent.dbc` row id + the requested rank (0-based, learn-up-to). No dedicated reply — the
|
||||
/// server validates silently; success arrives as the rank spell's learn effects plus the
|
||||
/// refreshed `PLAYER_CHARACTER_POINTS1` (decision 0304).
|
||||
/// `CMSG_LEARN_TALENT`: a `Talent.dbc` row and the 0-based rank to learn up to. No reply:
|
||||
/// success is the rank spell's learn plus a fresh `PLAYER_CHARACTER_POINTS1`.
|
||||
pub fn learn_talent(&mut self, talent_id: u32, requested_rank: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_LEARN_TALENT,
|
||||
@@ -24,12 +16,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Answer a class trainer's respec question (`MSG_TALENT_WIPE_CONFIRM` outbound, layout in
|
||||
/// [`messages::talent_wipe_confirm`]) — the `CONFIRM_TALENT_WIPE` dialog's Accept, and the only
|
||||
/// packet in the flow that unlearns anything: the question that raised the dialog arrived on
|
||||
/// this same opcode and changed nothing. The server answers by resetting the talents and having
|
||||
/// the trainer cast 14867, which lands as the un-learn of every rank spell plus the refreshed
|
||||
/// `PLAYER_CHARACTER_POINTS1`; declining sends nothing at all (decision 1580).
|
||||
/// `MSG_TALENT_WIPE_CONFIRM`: the `CONFIRM_TALENT_WIPE` dialog's Accept; declining sends
|
||||
/// nothing. The server resets the talents and has the trainer cast spell 14867.
|
||||
pub fn talent_wipe_confirm(&mut self, trainer_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_TALENT_WIPE_CONFIRM,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
//! The one honor-system send: asking for another player's honor stats (decision 1512). The
|
||||
//! family's other message, `SMSG_PVP_CREDIT`, is inbound only.
|
||||
//!
|
||||
//! **Why its own module rather than [`super::selection`]** — that module is "the two sends that
|
||||
//! set our selection", and this send does not: `HandleInspectOpcode` opens with
|
||||
//! `SetSelectionGuid` (vmangos `MiscHandler.cpp:944`) while `HandleInspectHonorStatsOpcode` does
|
||||
//! not (`MiscHandler.cpp:962-972`), so filing it there would make that module's own doc false. It
|
||||
//! also follows `writer/mod.rs`'s stated, mechanical split rule: a send lives in the module named
|
||||
//! after the [`crate::messages`] module its body builder lives in, and this body builder is
|
||||
//! `messages::pvp::inspect_honor_stats`.
|
||||
//! The honor send: another player's honor stats. Unlike inspect, it does not set our selection
|
||||
//! (`MiscHandler.cpp:962`).
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -16,14 +8,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask for a player's honor stats (`MSG_INSPECT_HONOR_STATS`, a raw 8-byte guid — body and
|
||||
/// server gates documented on [`messages::inspect_honor_stats`]). The reply rides the **same**
|
||||
/// opcode and surfaces as [`crate::events::SessionEvent::InspectHonorStats`].
|
||||
///
|
||||
/// There is no failure reply: each of the server's three refusals (target offline, beyond
|
||||
/// `INSPECT_DISTANCE`, a valid attack target) is a silent `return`. A caller must therefore
|
||||
/// treat the ask as fire-and-forget and keep whatever it already had on screen, exactly as the
|
||||
/// reference pane does — it re-asks on show and simply shows nothing until an answer lands.
|
||||
/// `MSG_INSPECT_HONOR_STATS`: the reply rides the same opcode. An offline, distant or hostile
|
||||
/// target gets no reply at all, so keep what is on screen until one lands.
|
||||
pub fn inspect_honor_stats(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_INSPECT_HONOR_STATS,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
//! The quest family's `WorldWriter` sends — the questgiver dialog walk (status probe → detail →
|
||||
//! accept → progress → reward panel → complete) and the two quest-log verbs. Bodies in
|
||||
//! [`crate::messages::quest`], whose `giver`/`log` split this mirrors as one file. Split out of
|
||||
//! `writer/mod.rs` (decision 0636).
|
||||
//!
|
||||
//! The dialog is a **client-driven state machine**: each panel the player advances to is another
|
||||
//! send, and the server only ever answers the panel that was asked for. Note the two ways a quest's
|
||||
//! text is fetched — [`WorldWriter::questgiver_query_quest`] needs an NPC guid (it's the click on a
|
||||
//! greeting row), while [`WorldWriter::quest_query`] takes only the id and is the quest log's own
|
||||
//! ask-once source.
|
||||
//! The quest sends: the questgiver dialog, the quest log and sharing. The dialog is client-driven:
|
||||
//! each panel is its own send, and the server answers only the panel asked for.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -16,10 +8,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Greet a questgiver (`CMSG_QUESTGIVER_HELLO`, layout in [`messages::questgiver_hello`]) —
|
||||
/// answered with `SMSG_QUESTGIVER_QUEST_LIST` (or the gossip menu, server's choice). Sent by
|
||||
/// the *end* of a quest session on a non-gossip NPC, which is how the reference puts the
|
||||
/// player back in the giver's list after a decline (decision 1738).
|
||||
/// `CMSG_QUESTGIVER_HELLO`: answered by the quest list or the gossip menu. The reference also
|
||||
/// sends it when a quest session on a non-gossip NPC ends, to return to the giver's list.
|
||||
pub fn questgiver_hello(&mut self, npc: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTGIVER_HELLO,
|
||||
@@ -27,9 +17,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask a quest's detail panel (`CMSG_QUESTGIVER_QUERY_QUEST`, layout in
|
||||
/// [`messages::questgiver_query_quest`]) — the click a greeting/gossip quest row makes.
|
||||
/// Answered by `SMSG_QUESTGIVER_QUEST_DETAILS` (a `QuestDetail` event).
|
||||
/// `CMSG_QUESTGIVER_QUERY_QUEST`: a quest row's click, asking for the details panel.
|
||||
pub fn questgiver_query_quest(&mut self, npc: u64, quest: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTGIVER_QUERY_QUEST,
|
||||
@@ -37,9 +25,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Accept a quest (`CMSG_QUESTGIVER_ACCEPT_QUEST`, layout in
|
||||
/// [`messages::questgiver_accept_quest`]) — the detail panel's Accept button. Adds it to the log;
|
||||
/// the server closes the gossip window (`SMSG_GOSSIP_COMPLETE`).
|
||||
/// `CMSG_QUESTGIVER_ACCEPT_QUEST`: answered by `SMSG_GOSSIP_COMPLETE` closing the window.
|
||||
pub fn questgiver_accept_quest(&mut self, npc: u64, quest: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTGIVER_ACCEPT_QUEST,
|
||||
@@ -47,9 +33,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask a quest's turn-in progress panel (`CMSG_QUESTGIVER_COMPLETE_QUEST`, layout in
|
||||
/// [`messages::questgiver_complete_quest`]) — answered by `SMSG_QUESTGIVER_REQUEST_ITEMS`
|
||||
/// (a `QuestProgress` event), or OFFER_REWARD when there are no required items.
|
||||
/// `CMSG_QUESTGIVER_COMPLETE_QUEST`: answered by the progress panel, or by the reward panel
|
||||
/// when no items are required.
|
||||
pub fn questgiver_complete_quest(&mut self, npc: u64, quest: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTGIVER_COMPLETE_QUEST,
|
||||
@@ -57,9 +42,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Advance from the progress panel to the reward panel (`CMSG_QUESTGIVER_REQUEST_REWARD`, layout
|
||||
/// in [`messages::questgiver_request_reward`]) — the progress panel's Continue button. Answered
|
||||
/// by `SMSG_QUESTGIVER_OFFER_REWARD` (a `QuestOffer` event).
|
||||
/// `CMSG_QUESTGIVER_REQUEST_REWARD`: the progress panel's Continue, answered by the reward
|
||||
/// panel.
|
||||
pub fn questgiver_request_reward(&mut self, npc: u64, quest: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTGIVER_REQUEST_REWARD,
|
||||
@@ -67,10 +51,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Choose a reward and finish the quest (`CMSG_QUESTGIVER_CHOOSE_REWARD`, layout in
|
||||
/// [`messages::questgiver_choose_reward`]; `reward` = choice index) — the reward panel's Complete
|
||||
/// button. Answered by `SMSG_QUESTGIVER_QUEST_COMPLETE` (a `QuestComplete` event) + the
|
||||
/// XP/money/item grants via `UPDATE_OBJECT`.
|
||||
/// `CMSG_QUESTGIVER_CHOOSE_REWARD`: `reward` is the choice index; answered by
|
||||
/// `SMSG_QUESTGIVER_QUEST_COMPLETE`.
|
||||
pub fn questgiver_choose_reward(&mut self, npc: u64, quest: u32, reward: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTGIVER_CHOOSE_REWARD,
|
||||
@@ -78,17 +60,12 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask a quest's full template (`CMSG_QUEST_QUERY`, layout in [`messages::quest_query`]) — the
|
||||
/// quest-log detail pane's ask-once source, distinct from [`Self::questgiver_query_quest`]
|
||||
/// (which needs an NPC guid, not just the quest id). Answered by `SMSG_QUEST_QUERY_RESPONSE`
|
||||
/// (a `QuestTemplate` event).
|
||||
/// `CMSG_QUEST_QUERY`: a quest template by id alone, the quest log's ask-once source.
|
||||
pub fn quest_query(&mut self, quest_id: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_QUEST_QUERY, &messages::quest_query(quest_id))
|
||||
}
|
||||
|
||||
/// Ask an NPC's questgiver dialog status (`CMSG_QUESTGIVER_STATUS_QUERY`, layout in
|
||||
/// [`messages::questgiver_status_query`]) — the overhead `!`/`?` marker's value, answered by
|
||||
/// `SMSG_QUESTGIVER_STATUS`.
|
||||
/// `CMSG_QUESTGIVER_STATUS_QUERY`: the NPC's overhead `!` or `?` marker.
|
||||
pub fn questgiver_status_query(&mut self, npc: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTGIVER_STATUS_QUERY,
|
||||
@@ -96,9 +73,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Abandon a quest-log slot (`CMSG_QUESTLOG_REMOVE_QUEST`, layout in
|
||||
/// [`messages::questlog_remove_quest`]) — no ack SMSG; the server clears the `PLAYER_QUEST_LOG`
|
||||
/// slot fields directly.
|
||||
/// `CMSG_QUESTLOG_REMOVE_QUEST`: no ack; the server clears the `PLAYER_QUEST_LOG` slot fields.
|
||||
pub fn questlog_remove_quest(&mut self, slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUESTLOG_REMOVE_QUEST,
|
||||
@@ -106,11 +81,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Share the quest with the party (`CMSG_PUSHQUESTTOPARTY`, layout in
|
||||
/// [`messages::push_quest_to_party`]) — the quest log's *Share Quest* button (decision 1733).
|
||||
/// No member guid: the server walks the group itself and answers with one
|
||||
/// `MSG_QUEST_PUSH_RESULT` per member (often two — the `SHARING_QUEST` opener, then the
|
||||
/// outcome).
|
||||
/// `CMSG_PUSHQUESTTOPARTY`: the server walks the group and answers with `MSG_QUEST_PUSH_RESULT`
|
||||
/// per member, often two (`SHARING_QUEST`, then the outcome).
|
||||
pub fn push_quest_to_party(&mut self, quest_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PUSHQUESTTOPARTY,
|
||||
@@ -118,9 +90,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Answer the escort confirm with Yes (`CMSG_QUEST_CONFIRM_ACCEPT`, layout in
|
||||
/// [`messages::quest_confirm_accept`]) — a `QUEST_FLAGS_PARTY_ACCEPT` quest a party member
|
||||
/// started. Saying **No** sends nothing at all, so there is no counterpart method.
|
||||
/// `CMSG_QUEST_CONFIRM_ACCEPT`: Yes to a party member's `QUEST_FLAGS_PARTY_ACCEPT` quest; No
|
||||
/// sends nothing.
|
||||
pub fn quest_confirm_accept(&mut self, quest_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_QUEST_CONFIRM_ACCEPT,
|
||||
@@ -128,11 +99,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Report our verdict on a quest a party member shared with us (`MSG_QUEST_PUSH_RESULT`,
|
||||
/// layout in [`messages::quest_push_result`]) — the receiver's leg of the share, relayed by the
|
||||
/// server to the sharer. `sharer` is the giver guid the shared
|
||||
/// `SMSG_QUESTGIVER_QUEST_DETAILS` arrived under (a *player* guid, which is what makes that
|
||||
/// panel a share).
|
||||
/// `MSG_QUEST_PUSH_RESULT`: our answer to a shared quest, relayed to `sharer`, the player guid
|
||||
/// the shared details panel arrived under.
|
||||
pub fn quest_push_result(&mut self, sharer: u64, msg: messages::QuestShareMsg) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_QUEST_PUSH_RESULT,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
//! The reputation pane's `WorldWriter` sends, mirroring [`crate::messages::reputation`]: the at-war
|
||||
//! toggle, the inactive toggle, and the watched faction. Split out of `writer/mod.rs` (decision 0636).
|
||||
//!
|
||||
//! All three address a faction by its **reputation-list slot**, and none is acked — see the messages
|
||||
//! module for the shapes and for why the watched slot is signed.
|
||||
//! The reputation pane's sends. Each names a faction by its reputation-list slot; none is acked.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -11,9 +7,7 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Declare or withdraw war on a faction (`CMSG_SET_FACTION_ATWAR`, layout in
|
||||
/// [`messages::set_faction_at_war`]) — the pane's crossed-swords checkbox. vmangos DROPS this
|
||||
/// while the player is in combat, so a flip mid-fight is silently nothing.
|
||||
/// `CMSG_SET_FACTION_ATWAR`: vmangos drops it while the player is in combat.
|
||||
pub fn set_faction_at_war(&mut self, rep_list_id: u32, at_war: bool) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SET_FACTION_ATWAR,
|
||||
@@ -21,8 +15,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Move a faction to (or out of) the pane's inactive bucket (`CMSG_SET_FACTION_INACTIVE`,
|
||||
/// layout in [`messages::set_faction_inactive`]).
|
||||
/// `CMSG_SET_FACTION_INACTIVE`: move a faction into or out of the pane's inactive group.
|
||||
pub fn set_faction_inactive(&mut self, rep_list_id: u32, inactive: bool) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SET_FACTION_INACTIVE,
|
||||
@@ -30,9 +23,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Watch a faction on the main bar, or [`messages::WATCHED_FACTION_NONE`] to stop
|
||||
/// (`CMSG_SET_WATCHED_FACTION`, layout in [`messages::set_watched_faction`]). The answer comes
|
||||
/// back as a `PLAYER_FIELD_WATCHED_FACTION_INDEX` descriptor update, not an ack.
|
||||
/// `CMSG_SET_WATCHED_FACTION`: [`messages::WATCHED_FACTION_NONE`] stops watching; the answer is
|
||||
/// a `PLAYER_FIELD_WATCHED_FACTION_INDEX` update.
|
||||
pub fn set_watched_faction(&mut self, rep_list_id: i32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SET_WATCHED_FACTION,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//! The two `WorldWriter` sends that set our **selection** — picking a unit, and inspecting a
|
||||
//! player. Both carry a raw 8-byte guid ([`crate::messages::full_guid`]), and both land in the
|
||||
//! server's `UNIT_FIELD_TARGET` for us: `CMSG_INSPECT` sets the selection as a side effect
|
||||
//! (vmangos `MiscHandler.cpp:945`), which is why the real client sends it even though the inspect
|
||||
//! window paints from already-streamed PUBLIC fields. Split out of `writer/mod.rs`
|
||||
//! (decision 0636).
|
||||
//! The two sends that set our selection, picking a unit and inspecting a player, both carrying a
|
||||
//! raw 8-byte guid.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -12,20 +8,14 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Set (or clear) our current target: `CMSG_SET_SELECTION` carrying a **full 8-byte GUID** (verified
|
||||
/// vmangos `SetSelection::ReadFromWorldPacket` — `recv_data >> guid` reads a raw `uint64`, not a
|
||||
/// packed guid). `guid == 0` clears the selection. The real client sends this the moment the local
|
||||
/// player picks a unit; the server records it in the player's `UNIT_FIELD_TARGET` and relays it to
|
||||
/// nearby observers.
|
||||
/// `CMSG_SET_SELECTION`: a raw `u64`, not a packed guid (`SetSelection::ReadFromWorldPacket`);
|
||||
/// 0 clears it. The server stores it in our `UNIT_FIELD_TARGET` for observers.
|
||||
pub fn set_selection(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_SET_SELECTION, &messages::full_guid(guid))
|
||||
}
|
||||
|
||||
/// Ask to inspect a player (`CMSG_INSPECT`, a full 8-byte guid — vmangos
|
||||
/// `WorldPackets::Misc::Inspect`). Server-side this *also* sets our selection to the target
|
||||
/// (`MiscHandler.cpp:945`), which is why the real client sends it even though the inspect
|
||||
/// window paints from already-streamed PUBLIC fields. The `SMSG_INSPECT` reply echoes the guid
|
||||
/// and nothing else — we neither wait for it nor parse it (decision 0631).
|
||||
/// `CMSG_INSPECT`: it also sets our selection (`MiscHandler.cpp:945`), so the reference sends
|
||||
/// it though the window paints from streamed fields; the `SMSG_INSPECT` reply is just the guid.
|
||||
pub fn inspect(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_INSPECT, &messages::full_guid(guid))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
//! Our own mover's `WorldWriter` sends — the self-movement stream (`MSG_MOVE_*`) and the acks the
|
||||
//! server demands of a player mover (speed change, near-teleport, worldport, the granted
|
||||
//! movement-mode family, spline-done). Bodies in [`crate::messages`]'s `movement`/`move_*`/`teleport_ack`/
|
||||
//! `force_speed_ack` builders; the `MovementInfo` stamping is
|
||||
//! [`crate::world::movement`]'s. Split out of `writer/mod.rs` (decision 0636), the family the
|
||||
//! writer type itself was originally built for.
|
||||
//!
|
||||
//! What unites these beyond the opcode range: every one of them carries a full `MovementInfo`
|
||||
//! snapshot (or is the empty echo of one), and every ack is **mandatory** — un-acked, the server
|
||||
//! either force-resolves the change on a timeout and flags its anticheat, or completes the
|
||||
//! teleport ~20 s late.
|
||||
//! Our own mover's sends: the `MSG_MOVE_*` stream and the acks the server demands of a player
|
||||
//! mover. Every ack is mandatory: unacked, the server force-resolves the change on a timeout and
|
||||
//! flags its anticheat, or completes a teleport about 20 s late.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -18,13 +10,9 @@ use crate::world::movement::{client_uptime_ms, movement_info};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Send one self-movement packet: a `MSG_MOVE_*` `opcode` carrying a `MovementInfo` with the given
|
||||
/// `flags` + pose. The caller (the controller) chooses the opcode per movement-axis transition
|
||||
/// (start/stop forward/back/strafe/turn), the periodic heartbeat, and the facing update — exactly as
|
||||
/// the real client does — and the `flags` it passes are the live CMovement bits the server relays to
|
||||
/// nearby players. `flags` must only set bits whose `MovementInfo` tail we serialize — the base
|
||||
/// directional/turn/walk bits, `JUMPING` (with its `jump` tail), `SWIMMING` (with its `pitch`
|
||||
/// tail), and `ON_TRANSPORT` (with its `transport` local-frame tail — decision 0438 phase 2).
|
||||
/// One `MSG_MOVE_*` packet, its opcode picked per movement transition as the reference does.
|
||||
/// `flags` may set only bits with a serialized tail: directional, turn and walk bits, and
|
||||
/// `JUMPING`, `SWIMMING` and `ON_TRANSPORT` with `jump`, `pitch` and `transport`.
|
||||
pub fn send_movement(
|
||||
&mut self,
|
||||
opcode: u16,
|
||||
@@ -37,9 +25,7 @@ impl WorldWriter {
|
||||
transport: Option<TransportPose>,
|
||||
) -> Result<()> {
|
||||
let mut info = movement_info(pos, orientation, flags);
|
||||
// Each conditional tail is gated on its flag by the serializer, so the flag and the value must
|
||||
// agree (they share `flags`): `SWIMMING` ⇒ the swim pitch, `JUMPING` ⇒ the ballistic launch
|
||||
// tail, `ON_TRANSPORT` ⇒ the rider's local pose.
|
||||
// The serializer writes each tail only when its flag is set, so flag and value must agree.
|
||||
info.pitch = pitch;
|
||||
info.fall_time = fall_time;
|
||||
info.jump = jump;
|
||||
@@ -47,35 +33,21 @@ impl WorldWriter {
|
||||
self.send(opcode, &messages::movement(&info))
|
||||
}
|
||||
|
||||
/// Claim `guid` as the unit whose movement we are sending (`CMSG_SET_ACTIVE_MOVER`, a full u64).
|
||||
///
|
||||
/// **The mover handshake is client-driven — no server packet sets it.** The server states who
|
||||
/// we *may* drive (`SMSG_CLIENT_CONTROL_UPDATE`) and then waits; until this reply lands,
|
||||
/// `Player::GetConfirmedMover` resolves nothing for the new mover and **every** `MSG_MOVE_*` we
|
||||
/// send for it is discarded (vmangos `MovementHandler.cpp:844-884`, which also rejects a claim
|
||||
/// on a unit it never handed us). Login makes this claim for our own body; possession simply
|
||||
/// re-makes it for somebody else's.
|
||||
/// `CMSG_SET_ACTIVE_MOVER`, full guid: the client's claim after `SMSG_CLIENT_CONTROL_UPDATE`.
|
||||
/// Until it lands, every `MSG_MOVE_*` for that unit is discarded (`MovementHandler.cpp:844`);
|
||||
/// sent at login for our own body and again on possession.
|
||||
pub fn set_active_mover(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_SET_ACTIVE_MOVER, &messages::full_guid(guid))
|
||||
}
|
||||
|
||||
/// Vote on the far-sight view the server has already chosen (`CMSG_FAR_SIGHT`, one `u8`).
|
||||
///
|
||||
/// The real client sends this from exactly two sites, both inside its far-sight engage/release
|
||||
/// function `0x5ee290`: `1` when the view attaches (`5ee3cb`/`5ee3dd`), `0` when it releases
|
||||
/// (`5ee4f3`/`5ee504`). It names no object — the server resolves that from `PLAYER_FARSIGHT`,
|
||||
/// which only the server writes.
|
||||
/// `CMSG_FAR_SIGHT`, one `u8`: 1 when the view attaches, 0 when it releases (reference:
|
||||
/// `0x5ee290`). It names no object; the server reads its own `PLAYER_FARSIGHT`.
|
||||
pub fn far_sight(&mut self, engage: bool) -> Result<()> {
|
||||
self.send(opcode::CMSG_FAR_SIGHT, &[u8::from(engage)])
|
||||
}
|
||||
|
||||
/// Give up `guid` as our mover (`CMSG_MOVE_NOT_ACTIVE_MOVER`): the full u64 guid being
|
||||
/// released, then a `MovementInfo` at its parting pose.
|
||||
///
|
||||
/// The pose is the point. vmangos clears its confirmed-mover state and **re-broadcasts a stop
|
||||
/// under the old guid** built from what we send here (`MovementHandler.cpp:886-965`), so
|
||||
/// skipping it strands every observer on that unit's last relayed pose — a possessed creature
|
||||
/// left sliding where the possession ended.
|
||||
/// `CMSG_MOVE_NOT_ACTIVE_MOVER`: the released full guid, then its parting pose, which vmangos
|
||||
/// re-broadcasts to observers as a stop (`MovementHandler.cpp:886`).
|
||||
pub fn move_not_active_mover(
|
||||
&mut self,
|
||||
guid: u64,
|
||||
@@ -91,11 +63,8 @@ impl WorldWriter {
|
||||
self.send(opcode::CMSG_MOVE_NOT_ACTIVE_MOVER, &body)
|
||||
}
|
||||
|
||||
/// Report that our movement clock skipped `lag_ms` (`CMSG_MOVE_TIME_SKIPPED`, layout in
|
||||
/// [`messages::move_time_skipped`]) for the named mover. The server folds the number into its
|
||||
/// own copy of our movement clock — and, when we have just boarded a transport, answers by
|
||||
/// re-sending that transport's create update (the 1.12 boarding fix; see
|
||||
/// [`opcode::CMSG_MOVE_TIME_SKIPPED`]).
|
||||
/// `CMSG_MOVE_TIME_SKIPPED`: the server adds `lag_ms` to its copy of our movement clock and,
|
||||
/// just after we board a transport, re-sends that transport's create update.
|
||||
pub fn move_time_skipped(&mut self, guid: u64, lag_ms: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MOVE_TIME_SKIPPED,
|
||||
@@ -103,11 +72,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Acknowledge that a server-authored spline (Charge/knockback/taxi — an `SMSG_MONSTER_MOVE`
|
||||
/// addressed to our own guid) finished: `CMSG_MOVE_SPLINE_DONE` with a `MovementInfo` at the
|
||||
/// ride's endpoint and the `spline_id` we were driven by. The server sets `SplineDonePending` for
|
||||
/// a player mover and validates this against its newest spline id, then relocates us and
|
||||
/// re-broadcasts a stop/heartbeat to observers — so it must be sent, at rest, once the ride ends.
|
||||
/// `CMSG_MOVE_SPLINE_DONE`: owed, at rest, when an `SMSG_MONSTER_MOVE` on our own guid ends.
|
||||
/// The server checks `spline_id` against its newest, then relocates us and tells observers.
|
||||
pub fn move_spline_done(
|
||||
&mut self,
|
||||
flags: u32,
|
||||
@@ -122,16 +88,13 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Echo a cross-map worldport ack: confirms `SMSG_NEW_WORLD` so the server resumes its object
|
||||
/// stream on the new continent (`MSG_MOVE_WORLDPORT_ACK` has an empty body).
|
||||
/// `MSG_MOVE_WORLDPORT_ACK`, empty: confirms `SMSG_NEW_WORLD` so the object stream resumes.
|
||||
pub fn worldport_ack(&mut self) -> Result<()> {
|
||||
self.send(opcode::MSG_MOVE_WORLDPORT_ACK, &[])
|
||||
}
|
||||
|
||||
/// Answer a `SMSG_FORCE_*_SPEED_CHANGE` (`CMSG_FORCE_*_SPEED_CHANGE_ACK`, picked by `kind`):
|
||||
/// echo the mover `guid` + `counter` + the exact `speed` the server sent, carrying our live
|
||||
/// `MovementInfo` (same field set as [`Self::send_movement`] — the server relocates us to it).
|
||||
/// Mandatory: unacked, the server force-resolves the change after ~4 s and flags its anticheat.
|
||||
/// `CMSG_FORCE_*_SPEED_CHANGE_ACK`: echoes `counter` and the exact `speed` with our live pose,
|
||||
/// which the server relocates us to. Unacked, it force-resolves after ~4 s and flags anticheat.
|
||||
pub fn force_speed_change_ack(
|
||||
&mut self,
|
||||
kind: messages::SpeedKind,
|
||||
@@ -157,14 +120,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Echo a same-map teleport ack so the server completes the near-teleport **immediately**
|
||||
/// (relocates us + streams the surrounding objects). Without a valid ack the teleport only
|
||||
/// finishes on a ~20s server-side fallback, so the destination's objects appear ~20s late.
|
||||
///
|
||||
/// vanilla 1.12 / vmangos expect a **full 8-byte guid** for this opcode (the lone movement opcode
|
||||
/// that does — the `MovementInfo` ones are correctly packed). With a packed guid the server's
|
||||
/// `ByteBuffer` overruns and drops the packet (confirmed by a vmangos capture: opcode `0xC7` →
|
||||
/// `ByteBufferException`, then the teleport completing 20s later).
|
||||
/// `MSG_MOVE_TELEPORT_ACK`: completes a near-teleport at once; without it the server finishes
|
||||
/// on a ~20 s fallback. The guid is a full 8 bytes: a packed one overruns vmangos's reader.
|
||||
pub fn teleport_ack(&mut self, guid: u64, counter: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_MOVE_TELEPORT_ACK,
|
||||
@@ -172,19 +129,10 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// **Acknowledge a granted mover mode** — root, water-walk, feather-fall or hover (the ack'd
|
||||
/// family; decision 0866): full guid + the echoed counter + our current `MovementInfo`, plus the
|
||||
/// trailing `u32 apply` for every mode but root ([`MoveMode::ack_carries_apply`]). Un-acked, the
|
||||
/// server never applies the change and observers never see it; a wrong/zero counter trips its
|
||||
/// cheat log (`HandleMoveRootAck`).
|
||||
///
|
||||
/// `flags` is the caller's live wire word and must already carry the applied mode's bit. Two
|
||||
/// server rules constrain it: an apply-ack without the bit is a KICK for root
|
||||
/// (`HandleMoveRootAck:715`), and **moving bits must never accompany `MOVEFLAG_ROOT`** — they
|
||||
/// freeze the real client (vmangos `MovementInfo.h`) and trip `CHEAT_TYPE_ROOT_MOVE`. Turn bits
|
||||
/// are not moving bits (`MOVEFLAG_MASK_MOVING` excludes them) and are fine alongside root.
|
||||
///
|
||||
/// `pose` is the mover's live `(position, orientation)` — the pair `World::self_pose` returns.
|
||||
/// Ack a granted root, water-walk, feather-fall or hover: full guid, echoed counter and our
|
||||
/// pose, plus a trailing `u32 apply` for all but root. Unacked, the server never applies it.
|
||||
/// `flags` must carry the mode's bit (a root apply-ack without it is a kick, vmangos
|
||||
/// `MovementHandler.cpp:722-729`) and no moving bit beside `MOVEFLAG_ROOT`; turn bits are fine.
|
||||
pub fn move_mode_ack(
|
||||
&mut self,
|
||||
guid: u64,
|
||||
@@ -202,16 +150,9 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// **Acknowledge the knockback we are now flying** (`CMSG_MOVE_KNOCK_BACK_ACK`, decision 1702):
|
||||
/// full guid + the echoed counter + our `MovementInfo` at the moment of launch, whose jump tail
|
||||
/// is the server's own `launch` quad echoed back.
|
||||
///
|
||||
/// The echo is not politeness — it is the server's whole validation. `flags` must carry
|
||||
/// `MOVEFLAG_JUMPING`, and `launch` must be bit-for-bit what arrived (vmangos matches all four
|
||||
/// floats within `0.01`); miss either and `FindPendingMovementKnockbackChange` fails, the ack is
|
||||
/// logged as `OnWrongAckData`, and no observer ever sees the knockback — the server builds their
|
||||
/// `MSG_MOVE_KNOCK_BACK` from the `MovementInfo` sent here. `fall_time` is the launch's fresh
|
||||
/// clock (0).
|
||||
/// `CMSG_MOVE_KNOCK_BACK_ACK`: our pose at launch, with the server's `launch` echoed as the
|
||||
/// jump tail. `flags` must carry `MOVEFLAG_JUMPING` and `launch` must match within 0.01, or
|
||||
/// vmangos rejects the ack and observers never see the knockback.
|
||||
pub fn knock_back_ack(
|
||||
&mut self,
|
||||
guid: u64,
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
//! The skills pane's `WorldWriter` send — one verb, mirroring [`crate::messages::skills`]: the
|
||||
//! abandon. Split out of `writer/mod.rs` (decision 0636).
|
||||
//!
|
||||
//! There is no "learn a skill" counterpart and no skill-up send: skills are granted server-side
|
||||
//! (by a trainer purchase, a quest, or a use-based tick) and arrive as `PLAYER_SKILL_INFO` field
|
||||
//! updates, so abandoning is the only skill verb the client ever initiates.
|
||||
//! The skills pane's one send, the abandon; skills are granted server-side as field updates.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -12,9 +7,7 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Unlearn a whole skill line (`CMSG_UNLEARN_SKILL`, layout in [`messages::unlearn_skill`]) —
|
||||
/// the skills pane's abandon. No ack: the server's `SetSkill(id, 0, 0)` comes back as a
|
||||
/// `PLAYER_SKILL_INFO` field update.
|
||||
/// `CMSG_UNLEARN_SKILL`: no ack; the removal comes back as a `PLAYER_SKILL_INFO` update.
|
||||
pub fn unlearn_skill(&mut self, skill_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_UNLEARN_SKILL,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
//! The social family's `WorldWriter` sends — friends, ignores, and `/who` (decision 0668).
|
||||
//!
|
||||
//! Note the asymmetry the wire imposes and the UI has to live with: you **add** by name and
|
||||
//! **remove** by guid (`CMSG_ADD_FRIEND` takes a cstring, `CMSG_DEL_FRIEND` a guid). That is not
|
||||
//! an inconsistency to paper over — it is why the client keeps both lists keyed by guid and
|
||||
//! resolves display names through the name cache (see [`crate::messages::social`]'s module doc).
|
||||
//! A "remove Bob" verb therefore has to find Bob's guid in the list *before* it can send anything.
|
||||
//! The social sends: friends, ignores and `/who`. The wire adds by name and removes by guid, so a
|
||||
//! removal must find the guid in the list first.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -13,21 +8,18 @@ use crate::messages::{self, opcode, WhoRequest};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask for the friend list again (`CMSG_FRIEND_LIST`, empty body) — the FrameXML's
|
||||
/// `ShowFriends()`. The server also pushes it unasked at login, so this is a refresh, never
|
||||
/// the only way the list arrives.
|
||||
/// `CMSG_FRIEND_LIST`, empty: `ShowFriends()`'s refresh; the server also pushes it at login.
|
||||
pub fn friend_list(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_FRIEND_LIST, &messages::friend_list())
|
||||
}
|
||||
|
||||
/// Befriend a character by name (`CMSG_ADD_FRIEND`). The answer is an `SMSG_FRIEND_STATUS`
|
||||
/// carrying one of the `FRIEND_ADDED_*` / refusal codes — never a silent success.
|
||||
/// `CMSG_ADD_FRIEND`: always answered by `SMSG_FRIEND_STATUS`, `FRIEND_ADDED_*` or a refusal.
|
||||
pub fn add_friend(&mut self, name: &str) -> Result<()> {
|
||||
self.send(opcode::CMSG_ADD_FRIEND, &messages::add_friend(name))
|
||||
}
|
||||
|
||||
/// The client's LFG slots and comment (`CMSG_SET_LOOKING_FOR_GROUP`). Nothing answers it —
|
||||
/// the reference has no handler for the opcode, and the readback is purely local (1961).
|
||||
/// `CMSG_SET_LOOKING_FOR_GROUP`: the LFG slots and comment. vmangos leaves the opcode unhandled
|
||||
/// (`Opcodes.cpp:603`), so the readback is local.
|
||||
pub fn set_looking_for_group(&mut self, slots: [u32; 3], comment: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SET_LOOKING_FOR_GROUP,
|
||||
@@ -50,9 +42,8 @@ impl WorldWriter {
|
||||
self.send(opcode::CMSG_DEL_IGNORE, &messages::del_ignore(guid))
|
||||
}
|
||||
|
||||
/// Run a `/who` (`CMSG_WHO`). The server answers at most one query per session at a time
|
||||
/// (`ReceivedWhoRequest` gates it) and runs it as an async task, so the `SMSG_WHO` lands a
|
||||
/// tick or two later — a second query fired while one is in flight is dropped, not queued.
|
||||
/// `CMSG_WHO`: the server runs one query per session at a time, as an async task, so the reply
|
||||
/// lags a tick or two and a second query sent meanwhile is dropped (`ReceivedWhoRequest`).
|
||||
pub fn who(&mut self, request: &WhoRequest) -> Result<()> {
|
||||
self.send(opcode::CMSG_WHO, &messages::who(request))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
//! The cast family's `WorldWriter` sends — the three `CMSG_CAST_SPELL` shapes (a unit or self, a
|
||||
//! GameObject, an item), plus the three cancels a cast can need: the in-flight cast, the channel,
|
||||
//! and the aura it left behind. Bodies in [`crate::messages::spells`], whose scope this mirrors;
|
||||
//! the two spell-id cancels are a bare `u32` written inline. Split out of `writer/mod.rs`
|
||||
//! (decision 0636), narrowed to the cast itself by decision 0640.
|
||||
//!
|
||||
//! 0636 kept auto-attack, the action bar and the talent spend here on the grounds that all of them
|
||||
//! are "the ability system's wire". That was the weakest grouping in it, and once 0640 split
|
||||
//! `messages/spells.rs` the mirror both allowed and recommended the finer cut: they are now
|
||||
//! [`super::attack`], [`super::action_bar`] and [`super::progression`].
|
||||
//!
|
||||
//! The aura cancel stays because the wire says it belongs to the spell, not to the aura:
|
||||
//! `CMSG_CANCEL_AURA` is addressed **by spell id, not by aura slot**.
|
||||
//! The cast sends: each `CMSG_CAST_SPELL` target shape, and the three cancels (cast, channel,
|
||||
//! aura), all addressed by spell id.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -19,9 +8,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Cast a spell (`CMSG_CAST_SPELL`): `target: None` = a self/implicit cast, `Some(guid)` = an
|
||||
/// explicit unit target (body in [`messages::cast_spell`]). The server answers
|
||||
/// `SMSG_CAST_RESULT` (and `SMSG_SPELL_START`/`GO` on success, unmodelled yet).
|
||||
/// `CMSG_CAST_SPELL`: `None` is a self or implicit cast, `Some` a unit target; answered by
|
||||
/// `SMSG_CAST_RESULT`.
|
||||
pub fn cast_spell(&mut self, spell_id: u32, target: Option<u64>) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_CAST_SPELL,
|
||||
@@ -29,19 +17,14 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel one of our own auras (`CMSG_CANCEL_AURA`, body in [`messages::cancel_aura`]) — the
|
||||
/// right-click-a-buff wire. Carries the **spell id**, not a slot; the server refuses passives,
|
||||
/// no-cancel spells and debuffs (decision 0257). No answer packet — the removal arrives as a
|
||||
/// `UNIT_FIELD_AURA` delta zeroing the slot.
|
||||
/// `CMSG_CANCEL_AURA`: by spell id, not slot; the server refuses passives, no-cancel spells and
|
||||
/// debuffs. No reply: the slot clears by a `UNIT_FIELD_AURA` delta.
|
||||
pub fn cancel_aura(&mut self, spell_id: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_CANCEL_AURA, &messages::cancel_aura(spell_id))
|
||||
}
|
||||
|
||||
/// Cast an OPEN_LOCK spell at a **GameObject** (`CMSG_CAST_SPELL`, body in
|
||||
/// [`messages::cast_spell_gameobject`]) — the right-click on a locked chest / mining vein / herb
|
||||
/// node (decision 0239). The server runs `EffectOpenLock` → for a chest, opens the loot
|
||||
/// (`SMSG_LOOT_RESPONSE`); the profession/skill gate is the server's. Answered by
|
||||
/// `SMSG_CAST_RESULT` on refusal.
|
||||
/// `CMSG_CAST_SPELL` at a GameObject: the open-lock cast on a chest, vein or herb. The server
|
||||
/// checks the skill, and a chest answers with its loot.
|
||||
pub fn cast_spell_gameobject(&mut self, spell_id: u32, go_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_CAST_SPELL,
|
||||
@@ -49,10 +32,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Cast an item-targeted spell (`CMSG_CAST_SPELL` with `TARGET_FLAG_ITEM` + the item's packed
|
||||
/// guid — [`messages::cast_spell_item`]): the enchant cast the CraftFrame's item pick
|
||||
/// completes (decision 0437 phase 3). The server resolves the item, checks reagents, applies
|
||||
/// the enchant; refusal answers `SMSG_CAST_RESULT`.
|
||||
/// `CMSG_CAST_SPELL` with `TARGET_FLAG_ITEM` and the item's packed guid: an enchant on the item
|
||||
/// picked in the craft frame.
|
||||
pub fn cast_spell_item(&mut self, spell_id: u32, item_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_CAST_SPELL,
|
||||
@@ -60,10 +41,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Cast a spell at a **ground point** (`CMSG_CAST_SPELL` with `TARGET_FLAG_DEST_LOCATION` +
|
||||
/// the destination Vec3 in WoW world coords — [`messages::cast_spell_at_dest`]): the
|
||||
/// targeting-cursor commit for a ground-targeted AOE (decision 0792). The server range/LOS
|
||||
/// checks the point in `Spell::CheckCast`; refusal answers `SMSG_CAST_RESULT`.
|
||||
/// `CMSG_CAST_SPELL` with `TARGET_FLAG_DEST_LOCATION`: a ground-targeted spell at a world
|
||||
/// point; the server checks range and line of sight (`Spell::CheckCast`).
|
||||
pub fn cast_spell_at_dest(&mut self, spell_id: u32, dest: [f32; 3]) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_CAST_SPELL,
|
||||
@@ -71,10 +50,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Cast a spell at a **source point** (`CMSG_CAST_SPELL` with `TARGET_FLAG_SOURCE_LOCATION` +
|
||||
/// the Vec3 in WoW world coords — [`messages::cast_spell_at_source`]): the targeting-cursor
|
||||
/// commit for a `Targets & 0x20` spell (decision 2218). The dest verb's twin, one bit over;
|
||||
/// the server centres the AoE on the point it carries.
|
||||
/// `CMSG_CAST_SPELL` with `TARGET_FLAG_SOURCE_LOCATION`, for a `Targets & 0x20` spell; the
|
||||
/// server centres the area effect on `src`, a world point.
|
||||
pub fn cast_spell_at_source(&mut self, spell_id: u32, src: [f32; 3]) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_CAST_SPELL,
|
||||
@@ -82,16 +59,14 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel a named in-flight cast (`CMSG_CANCEL_CAST`: one `u32` spell id — vmangos
|
||||
/// `HandleCancelCastOpcode`). Sent by the wand-only auto-repeat handoff (`0x6095b8`) and by
|
||||
/// the cast bar's local self-cancel (movement/Esc mid-cast, `benilla::ui_cast`).
|
||||
/// `CMSG_CANCEL_CAST`, one `u32` spell id: sent by the wand auto-repeat handoff (reference:
|
||||
/// `0x6095b8`) and when movement or Esc cancels a cast.
|
||||
pub fn cancel_cast(&mut self, spell_id: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_CANCEL_CAST, &spell_id.to_le_bytes())
|
||||
}
|
||||
|
||||
/// End our own running channel (`CMSG_CANCEL_CHANNELLING`: one `u32` spell id, which vmangos
|
||||
/// reads and ignores — the interrupt is unconditional; the real client still writes it). The
|
||||
/// channel half of the local self-cancel (`benilla::ui_cast`).
|
||||
/// `CMSG_CANCEL_CHANNELLING`: the 1.12 client writes the spell id, which vmangos ignores,
|
||||
/// interrupting unconditionally.
|
||||
pub fn cancel_channelling(&mut self, spell_id: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_CANCEL_CHANNELLING, &spell_id.to_le_bytes())
|
||||
}
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
//! The stable window's `WorldWriter` sends — the list refresh and the four mutations. Bodies in
|
||||
//! [`crate::messages::stable`], whose scope this mirrors.
|
||||
//!
|
||||
//! **Every verb names the stable master's guid**, because the server re-checks the interaction on
|
||||
//! each one (`WorldSession::CheckStableMaster`, VERIFIED vmangos `NPCHandler.cpp:584-607`): the guid
|
||||
//! must be an NPC with `UNIT_NPC_FLAG_STABLEMASTER` that the player can still interact with. A
|
||||
//! window left open while the player walks away therefore fails every button with
|
||||
//! [`messages::stable_result::ERR_STABLE`] rather than acting at a distance — which is why the app
|
||||
//! side range-guards the session rather than trusting the open window.
|
||||
//!
|
||||
//! **None of the four mutations is answered with a fresh list** — only a one-byte
|
||||
//! `SMSG_STABLE_RESULT` (VERIFIED: every `HandleStable*` path ends in `SendStableResult` and
|
||||
//! nothing else). So a success is a cue to re-ask with [`WorldWriter::list_stabled_pets`], the same
|
||||
//! shape as the trainer's post-purchase re-request.
|
||||
//! The stable window's sends. Each names the stable master, whose reach the server re-checks
|
||||
//! (`NPCHandler.cpp:584`), so an out-of-range window fails with `ERR_STABLE`. A mutation is
|
||||
//! answered only by a one-byte `SMSG_STABLE_RESULT`; on success, re-ask for the list.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -20,13 +9,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask (or re-ask) a stable master's pet list (`MSG_LIST_STABLED_PETS`, layout in
|
||||
/// [`messages::list_stabled_pets`]) — one 8-byte NPC guid.
|
||||
///
|
||||
/// This is the *refresh* verb. The window first *opens* off the server's own unprompted send of
|
||||
/// the same opcode, which the gossip stable option produces (`GOSSIP_OPTION_STABLEPET` →
|
||||
/// `SendStablePet`, VERIFIED vmangos `Player.cpp:12400-12402`) — exactly the trainer's and the
|
||||
/// vendor's arrangement. Answered by `MSG_LIST_STABLED_PETS` inbound.
|
||||
/// `MSG_LIST_STABLED_PETS`: the refresh; the window first opens on the server's unasked send
|
||||
/// from the gossip stable option (`Player.cpp:12400`).
|
||||
pub fn list_stabled_pets(&mut self, npc_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_LIST_STABLED_PETS,
|
||||
@@ -34,25 +18,14 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Put the current pet into a stable slot (`CMSG_STABLE_PET`, layout in
|
||||
/// [`messages::stable_pet`]): the NPC guid alone.
|
||||
///
|
||||
/// **There is no slot to name** — the server takes the first free one and refuses if that index
|
||||
/// exceeds the slots the player has bought (`HandleStablePet`, VERIFIED `NPCHandler.cpp:609-655`).
|
||||
/// Answers [`messages::stable_result::SUCCESS_STABLE`], or `ERR_STABLE` when the player is dead,
|
||||
/// has no live hunter pet, or has no free bought slot.
|
||||
/// `CMSG_STABLE_PET`: the server picks the first free bought slot (`NPCHandler.cpp:609`);
|
||||
/// a dead player, no live hunter pet or no free slot gets `ERR_STABLE`.
|
||||
pub fn stable_pet(&mut self, npc_guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_STABLE_PET, &messages::stable_pet(npc_guid))
|
||||
}
|
||||
|
||||
/// Summon a stabled pet as the current pet (`CMSG_UNSTABLE_PET`, layout in
|
||||
/// [`messages::unstable_pet`]): the NPC guid + the pet's own
|
||||
/// [`messages::StabledPet::pet_number`], never its slot.
|
||||
///
|
||||
/// Only valid with **no** current pet — vmangos refuses even when the existing pet is merely
|
||||
/// unsummoned and out of range (`HandleUnstablePet`, VERIFIED `NPCHandler.cpp:657-702`). With a
|
||||
/// pet already out, the verb is [`Self::stable_swap_pet`]. Answers
|
||||
/// [`messages::stable_result::SUCCESS_UNSTABLE`] or `ERR_STABLE`.
|
||||
/// `CMSG_UNSTABLE_PET`: by `pet_number`, never slot. vmangos refuses it while any current pet
|
||||
/// exists, even an unsummoned one (`NPCHandler.cpp:657`); with a pet out, swap instead.
|
||||
pub fn unstable_pet(&mut self, npc_guid: u64, pet_number: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_UNSTABLE_PET,
|
||||
@@ -60,14 +33,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Trade the current pet for a stabled one in a single step (`CMSG_STABLE_SWAP_PET`, layout in
|
||||
/// [`messages::stable_swap_pet`]): the NPC guid + the stabled pet's
|
||||
/// [`messages::StabledPet::pet_number`].
|
||||
///
|
||||
/// The current pet goes into the slot the named pet vacates (`HandleStableSwapPet`, VERIFIED
|
||||
/// `NPCHandler.cpp:735-789`). Requires a live hunter pet to be out. Answers
|
||||
/// [`messages::stable_result::SUCCESS_UNSTABLE`] — the *same* code a plain unstable returns, so
|
||||
/// the reply cannot tell the two verbs apart.
|
||||
/// `CMSG_STABLE_SWAP_PET`: the live pet takes the named pet's slot (`NPCHandler.cpp:735`).
|
||||
/// Success is `SUCCESS_UNSTABLE`, the same code as a plain unstable.
|
||||
pub fn stable_swap_pet(&mut self, npc_guid: u64, pet_number: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_STABLE_SWAP_PET,
|
||||
@@ -75,15 +42,9 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Buy the next stable slot (`CMSG_BUY_STABLE_SLOT`, layout in
|
||||
/// [`messages::buy_stable_slot`]): the NPC guid alone.
|
||||
///
|
||||
/// The *which* is implicit, exactly as it is for the bank's bag slots (decision 0604): the
|
||||
/// server buys `m_stableSlots + 1` and prices it from `StableSlotPrices.dbc` at that row
|
||||
/// (`HandleBuyStableSlot`, VERIFIED `NPCHandler.cpp:704-729`). Answers
|
||||
/// [`messages::stable_result::SUCCESS_BUY_SLOT`], `ERR_MONEY`, or — past the two slots 5875
|
||||
/// ships — `ERR_STABLE`. No packet reports the new count: the next list's `num_stable_slots`
|
||||
/// does.
|
||||
/// `CMSG_BUY_STABLE_SLOT`: the server buys the next slot at its `StableSlotPrices.dbc` price
|
||||
/// (`NPCHandler.cpp:704`); past the two 1.12 slots it answers `ERR_STABLE`. Only the next
|
||||
/// list carries the new count.
|
||||
pub fn buy_stable_slot(&mut self, npc_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_BUY_STABLE_SLOT,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//! The summon answer — the client's one send in the summon family (decision 1747).
|
||||
//!
|
||||
//! There is no "summon me" verb and no decline verb: the flow starts with the *server's*
|
||||
//! `SMSG_SUMMON_REQUEST` (a warlock's ritual, a meeting stone, a GM) and this is the Yes.
|
||||
//! Declining is silence — the server auto-declines when its own two-minute window runs out — which
|
||||
//! is why this file has exactly one function and its twin does not exist.
|
||||
//! The summon accept, the client's one summon send. Declining sends nothing: the server's
|
||||
//! two-minute window lapses.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -12,12 +8,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Accept a summon (`CMSG_SUMMON_RESPONSE`) — the `CONFIRM_SUMMON` dialog's Accept, carrying
|
||||
/// the guid the question arrived with.
|
||||
///
|
||||
/// The server answers by teleporting us to the summon point it recorded when it asked. It does
|
||||
/// **not** check the guid (vmangos reads it and calls `Player::SummonIfPossible(true)` off its
|
||||
/// own `m_summon_expire`), so what makes a late accept harmless is that timer, not this field.
|
||||
/// `CMSG_SUMMON_RESPONSE`: the `CONFIRM_SUMMON` Accept. vmangos ignores the guid and teleports
|
||||
/// us unless its own `m_summon_expire` has passed (`Player::SummonIfPossible`).
|
||||
pub fn summon_response(&mut self, summoner_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SUMMON_RESPONSE,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The tabard designer's senders (decision 1977; bodies in [`crate::messages::tabard`]).
|
||||
//! The tabard designer's sends, and the battlemaster greeting.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// `MSG_TABARDVENDOR_ACTIVATE` out — the NPC-click ladder's TABARDDESIGNER arm.
|
||||
/// `MSG_TABARDVENDOR_ACTIVATE`: the NPC-click ladder's `TABARDDESIGNER` arm.
|
||||
pub fn tabard_vendor_activate(&mut self, npc: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_TABARDVENDOR_ACTIVATE,
|
||||
@@ -15,7 +15,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// `MSG_SAVE_GUILD_EMBLEM` out — `TabardModel:Save()` past its fourteen pre-flight checks.
|
||||
/// `MSG_SAVE_GUILD_EMBLEM`: `TabardModel:Save()`, once its fourteen client-side checks pass.
|
||||
pub fn save_guild_emblem(&mut self, vendor: u64, design: [u32; 5]) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_SAVE_GUILD_EMBLEM,
|
||||
@@ -23,7 +23,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// `CMSG_BATTLEMASTER_HELLO` — the ladder's BATTLEMASTER arm.
|
||||
/// `CMSG_BATTLEMASTER_HELLO`: the NPC-click ladder's `BATTLEMASTER` arm.
|
||||
pub fn battlemaster_hello(&mut self, npc: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_BATTLEMASTER_HELLO,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
//! The flight-master family's `WorldWriter` sends — the known-status probe, the map open, and the
|
||||
//! two flight verbs. Bodies in [`crate::messages::taxi`], whose scope this mirrors. Split out of
|
||||
//! `writer/mod.rs` (decision 0636).
|
||||
//!
|
||||
//! Two flight opcodes, one discriminator (decision 0496 §TU-3): send `CMSG_ACTIVATETAXI` when a
|
||||
//! direct `TaxiPath` edge exists current→target, and `CMSG_ACTIVATETAXIEXPRESS` with the whole node
|
||||
//! chain when it doesn't — the real rule the verdict corrected from a hop-count guess. Both answer
|
||||
//! The flight master's sends. A flight is `CMSG_ACTIVATETAXI` when a direct `TaxiPath` edge joins
|
||||
//! the two nodes, else `CMSG_ACTIVATETAXIEXPRESS` with the whole chain; both are answered by
|
||||
//! `SMSG_ACTIVATETAXIREPLY`.
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -14,9 +9,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask a nearby flight master's known status (`CMSG_TAXINODE_STATUS_QUERY`, layout in
|
||||
/// [`messages::taxi_node_status_query`]): the flight master's guid, not ours. Answered by
|
||||
/// `SMSG_TAXINODE_STATUS` (a `TaxiNodeStatus` event, decision 0484).
|
||||
/// `CMSG_TAXINODE_STATUS_QUERY`: whether we know a nearby flight master's node; answered by
|
||||
/// `SMSG_TAXINODE_STATUS`.
|
||||
pub fn taxi_node_status_query(&mut self, flightmaster_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_TAXINODE_STATUS_QUERY,
|
||||
@@ -24,12 +18,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Open a flight master's taxi map (`CMSG_TAXIQUERYAVAILABLENODES`, layout in
|
||||
/// [`messages::taxi_query_available_nodes`], decision 0496 I4 — CONFIRMED as built: the
|
||||
/// interact ladder is first-match-wins low→high over `UNIT_NPC_FLAGS`, so only a pure
|
||||
/// flightmaster reaches here). A known node answers `SMSG_SHOWTAXINODES` (a `TaxiNodesShown`
|
||||
/// event); a never-visited node instead answers the first-visit learn pair (`NewTaxiPath` +
|
||||
/// `TaxiNodeStatus`) and opens no menu on this click.
|
||||
/// `CMSG_TAXIQUERYAVAILABLENODES`: a known node answers `SMSG_SHOWTAXINODES`; a new node
|
||||
/// answers with the first-visit learn pair instead, and no map opens on that click.
|
||||
pub fn taxi_query_available_nodes(&mut self, flightmaster_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_TAXIQUERYAVAILABLENODES,
|
||||
@@ -37,9 +27,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Fly a single hop (`CMSG_ACTIVATETAXI`, layout in [`messages::activate_taxi`]): the
|
||||
/// flight-master guid, the source node, the destination node. Answered by
|
||||
/// `SMSG_ACTIVATETAXIREPLY`; success continues into the mount + `SMSG_MONSTER_MOVE` flight.
|
||||
/// `CMSG_ACTIVATETAXI`: one hop; success mounts us and flies an `SMSG_MONSTER_MOVE` path.
|
||||
pub fn activate_taxi(
|
||||
&mut self,
|
||||
flightmaster_guid: u64,
|
||||
@@ -52,11 +40,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Fly a multi-hop chain in one send (`CMSG_ACTIVATETAXIEXPRESS`, layout in
|
||||
/// [`messages::activate_taxi_express`], decision 0496 §TU-3 — sent when no direct `TaxiPath`
|
||||
/// edge exists current→target, the real discriminator the verdict corrected from a hop-count
|
||||
/// guess): the flight-master guid, the route's combined fare, and the full node chain in
|
||||
/// order. Answered by `SMSG_ACTIVATETAXIREPLY`, same as [`Self::activate_taxi`].
|
||||
/// `CMSG_ACTIVATETAXIEXPRESS`: the route's combined fare and its whole node chain, in order.
|
||||
pub fn activate_taxi_express(
|
||||
&mut self,
|
||||
flightmaster_guid: u64,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
//! The player-trade arc's `WorldWriter` sends (decision 0592 P0): initiate, the auto-`BEGIN_TRADE`
|
||||
//! reply, the decline pair (busy/ignore), set/clear an item, set gold, and accept/unaccept/cancel.
|
||||
//! Bodies in [`crate::messages`]'s `trade` builders (layout VERIFIED against vmangos
|
||||
//! `Handlers/TradeHandler.cpp` + `Server/Packets/Trade.cpp`). Split out of `writer/mod.rs` the same
|
||||
//! way `channel`/`group`/`mail` are — one clearly separable concern among the writer's domains.
|
||||
//! The player trade sends.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -11,9 +7,7 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Open a trade with another player (`CMSG_INITIATE_TRADE`, layout in
|
||||
/// [`messages::initiate_trade`]): the target's guid. The server answers us on any refusal
|
||||
/// (`SMSG_TRADE_STATUS`) and, on success, sends the *target* a `BEGIN_TRADE`.
|
||||
/// `CMSG_INITIATE_TRADE`: the target gets `BEGIN_TRADE`; we hear back only on a refusal.
|
||||
pub fn initiate_trade(&mut self, target: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_INITIATE_TRADE,
|
||||
@@ -21,27 +15,23 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Acknowledge a received `BEGIN_TRADE` (`CMSG_BEGIN_TRADE`, EMPTY body) — the client's
|
||||
/// automatic reply that makes the server emit `OPEN_WINDOW` to both sides.
|
||||
/// `CMSG_BEGIN_TRADE`, empty: the automatic reply to `BEGIN_TRADE`, after which the server
|
||||
/// sends both sides `OPEN_WINDOW`.
|
||||
pub fn begin_trade(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_BEGIN_TRADE, &[])
|
||||
}
|
||||
|
||||
/// Decline a trade request as busy (`CMSG_BUSY_TRADE`, EMPTY body) — the server cancels it with
|
||||
/// `TRADE_STATUS_BUSY` to the initiator.
|
||||
/// `CMSG_BUSY_TRADE`, empty: the initiator gets `TRADE_STATUS_BUSY`.
|
||||
pub fn busy_trade(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_BUSY_TRADE, &[])
|
||||
}
|
||||
|
||||
/// Decline a trade request as ignored (`CMSG_IGNORE_TRADE`, EMPTY body) — the server cancels it
|
||||
/// with `TRADE_STATUS_IGNORE_YOU` to the initiator.
|
||||
/// `CMSG_IGNORE_TRADE`, empty: the initiator gets `TRADE_STATUS_IGNORE_YOU`.
|
||||
pub fn ignore_trade(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_IGNORE_TRADE, &[])
|
||||
}
|
||||
|
||||
/// Put a bag item into a trade slot (`CMSG_SET_TRADE_ITEM`, layout in
|
||||
/// [`messages::set_trade_item`]): the trade slot, and the item's inventory `bag`/`slot`.
|
||||
/// Clears the partner's accept and re-arms the 200 ms scam-prevention delay server-side.
|
||||
/// `CMSG_SET_TRADE_ITEM`: clears the partner's accept and re-arms the server's 200 ms delay.
|
||||
pub fn set_trade_item(&mut self, trade_slot: u8, bag: u8, slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SET_TRADE_ITEM,
|
||||
@@ -49,8 +39,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove an item from a trade slot (`CMSG_CLEAR_TRADE_ITEM`, layout in
|
||||
/// [`messages::clear_trade_item`]): the trade slot.
|
||||
/// `CMSG_CLEAR_TRADE_ITEM`: empty one trade slot.
|
||||
pub fn clear_trade_item(&mut self, trade_slot: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_CLEAR_TRADE_ITEM,
|
||||
@@ -58,8 +47,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Set the gold we are offering (`CMSG_SET_TRADE_GOLD`, layout in [`messages::set_trade_gold`]):
|
||||
/// copper. Clears the partner's accept and re-arms the scam-prevention delay server-side.
|
||||
/// `CMSG_SET_TRADE_GOLD`, in copper: clears the partner's accept and re-arms the 200 ms delay.
|
||||
pub fn set_trade_gold(&mut self, copper: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SET_TRADE_GOLD,
|
||||
@@ -67,20 +55,18 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Press Trade (`CMSG_ACCEPT_TRADE`, layout in [`messages::accept_trade`]) — accepting within
|
||||
/// 200 ms of the last change is bounced by the server as `TRADE_STATUS_BACK_TO_TRADE`; when
|
||||
/// both sides have accepted and the checks pass, the server swaps and sends both `COMPLETE`.
|
||||
/// `CMSG_ACCEPT_TRADE`: bounced (`TRADE_STATUS_BACK_TO_TRADE`) within 200 ms of a change; once
|
||||
/// both sides accept, the server swaps and sends both `COMPLETE`.
|
||||
pub fn accept_trade(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_ACCEPT_TRADE, &messages::accept_trade())
|
||||
}
|
||||
|
||||
/// Un-press Trade (`CMSG_UNACCEPT_TRADE`, EMPTY body) — drops our accept back to editing.
|
||||
/// `CMSG_UNACCEPT_TRADE`, empty: withdraws our accept.
|
||||
pub fn unaccept_trade(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_UNACCEPT_TRADE, &[])
|
||||
}
|
||||
|
||||
/// Cancel the trade (`CMSG_CANCEL_TRADE`, EMPTY body) — the Close/Cancel path; the server
|
||||
/// unwinds both sides with `TRADE_STATUS_TRADE_CANCELED`.
|
||||
/// `CMSG_CANCEL_TRADE`, empty: both sides get `TRADE_STATUS_TRADE_CANCELED`.
|
||||
pub fn cancel_trade(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_CANCEL_TRADE, &[])
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
//! The trainer window's `WorldWriter` sends — the service-list refresh and the purchase. Bodies in
|
||||
//! [`crate::messages::trainer`], whose scope this mirrors. Split out of `writer/mod.rs`
|
||||
//! (decision 0636).
|
||||
//!
|
||||
//! [`WorldWriter::trainer_list`] is the *refresh* verb, not the open: the window first appears off
|
||||
//! the gossip trainer option's own `SMSG_TRAINER_LIST`. It exists because the server does **not**
|
||||
//! auto-resend the list after a purchase (VERIFIED vmangos `NPCHandler.cpp:92-95`), so repainting
|
||||
//! the bought row green→gray takes a standalone re-request.
|
||||
//! The trainer window's sends: the list refresh and the purchase.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -14,13 +7,8 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask (or re-ask) a trainer's service list (`CMSG_TRAINER_LIST`, layout in
|
||||
/// [`messages::trainer_list`]) — one 8-byte trainer guid. The window first *opens* off the
|
||||
/// gossip trainer option's `SMSG_TRAINER_LIST`; this is the *refresh* verb, re-requested after a
|
||||
/// purchase to repaint the bought row green→gray (vmangos `HandleTrainerListOpcode` honors a
|
||||
/// standalone re-request while the player can still interact — VERIFIED `NPCHandler.cpp:92-95`,
|
||||
/// the server does not auto-resend on a buy). Answered by `SMSG_TRAINER_LIST` (a `TrainerList`
|
||||
/// event).
|
||||
/// `CMSG_TRAINER_LIST`: the refresh after a purchase, which the server does not resend
|
||||
/// (`NPCHandler.cpp:92`); the window first opens from the gossip option.
|
||||
pub fn trainer_list(&mut self, trainer_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_TRAINER_LIST,
|
||||
@@ -28,11 +16,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Buy (learn) a trainer service (`CMSG_TRAINER_BUY_SPELL`, layout in
|
||||
/// [`messages::trainer_buy_spell`]): the trainer guid + the service's spell id. Success answers
|
||||
/// `SMSG_TRAINER_BUY_SUCCEEDED` and delivers the spell via the learn effect's `SMSG_LEARNED_SPELL`
|
||||
/// (the green→gray repaint then needs a `CMSG_TRAINER_LIST` re-request); refusal answers
|
||||
/// `SMSG_TRAINER_BUY_FAILED` with a [`messages::train_fail`] code.
|
||||
/// `CMSG_TRAINER_BUY_SPELL`: answered by `SMSG_TRAINER_BUY_SUCCEEDED` and `SMSG_LEARNED_SPELL`,
|
||||
/// or by `SMSG_TRAINER_BUY_FAILED` with a [`messages::train_fail`] code.
|
||||
pub fn trainer_buy_spell(&mut self, trainer_guid: u64, spell_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_TRAINER_BUY_SPELL,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The tutorial system's sends (decision 1976).
|
||||
//! The tutorial system's sends.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -7,18 +7,18 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Acknowledge one tutorial (`CMSG_TUTORIAL_FLAG`): the 0-based id — `FlagTutorial`'s and the
|
||||
/// six auto-acknowledge sites' packet, sent only when the acknowledged bank's bit was clear.
|
||||
/// `CMSG_TUTORIAL_FLAG`, a 0-based id: sent by `FlagTutorial` and six auto-acknowledge sites,
|
||||
/// only when that tutorial's bit was clear.
|
||||
pub fn tutorial_flag(&mut self, id: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_TUTORIAL_FLAG, &messages::tutorial_flag(id))
|
||||
}
|
||||
|
||||
/// Mark every tutorial acknowledged (`CMSG_TUTORIAL_CLEAR`, empty) — `ClearTutorials()`.
|
||||
/// `CMSG_TUTORIAL_CLEAR`, empty: `ClearTutorials()`, marking every tutorial acknowledged.
|
||||
pub fn tutorial_clear(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_TUTORIAL_CLEAR, &[])
|
||||
}
|
||||
|
||||
/// Forget every tutorial (`CMSG_TUTORIAL_RESET`, empty) — `ResetTutorials()`.
|
||||
/// `CMSG_TUTORIAL_RESET`, empty: `ResetTutorials()`, forgetting every tutorial.
|
||||
pub fn tutorial_reset(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_TUTORIAL_RESET, &[])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
//! The vendor window's `WorldWriter` sends — list the stock, buy, sell, buy back, repair. Bodies in
|
||||
//! [`crate::messages::vendor`], whose scope this mirrors. Split out of `writer/mod.rs`
|
||||
//! (decision 0636).
|
||||
//!
|
||||
//! Note the two different item identities the family uses: buying names the item **template
|
||||
//! entry** (not the vendor row's `muid`), while selling and repairing name the **item guid** of the
|
||||
//! thing in our bags, and buyback names an absolute player-array slot 69–80. Only the two failures
|
||||
//! answer with a packet (`SMSG_BUY_FAILED` / `SMSG_SELL_ITEM`'s error shape); every success is
|
||||
//! ordinary `UPDATE_OBJECT` traffic.
|
||||
//! The vendor sends. Buying names an item template entry, not the vendor row's `muid`; selling and
|
||||
//! repairing name the item guid in our bags.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -15,9 +8,7 @@ use crate::messages::{self, opcode};
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Ask a vendor's stock (`CMSG_LIST_INVENTORY`, layout in [`messages::list_inventory`]) — the
|
||||
/// server requires `UNIT_NPC_FLAG_VENDOR` + the player alive (`ItemHandler.cpp:693`). Answered
|
||||
/// by `SMSG_LIST_INVENTORY` (a `VendorInventory` event).
|
||||
/// `CMSG_LIST_INVENTORY`: needs `UNIT_NPC_FLAG_VENDOR` and us alive (`ItemHandler.cpp:693`).
|
||||
pub fn list_inventory(&mut self, vendor_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_LIST_INVENTORY,
|
||||
@@ -25,10 +16,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Buy from a vendor (`CMSG_BUY_ITEM`, layout in [`messages::buy_item`]): `entry` is the item
|
||||
/// **template** id (not the vendor row's `muid`), `count` the number of stacks. Auto-places
|
||||
/// into the first free bag slot. Success updates the vendor stock (`SMSG_BUY_ITEM`) and
|
||||
/// delivers the item via the normal item-create path; refusal answers `SMSG_BUY_FAILED`.
|
||||
/// `CMSG_BUY_ITEM`: `count` stacks into the first free bag slot; answered by `SMSG_BUY_ITEM`
|
||||
/// with the new stock, or by `SMSG_BUY_FAILED`.
|
||||
pub fn buy_item(&mut self, vendor_guid: u64, entry: u32, count: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_BUY_ITEM,
|
||||
@@ -36,10 +25,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Buy into a **named** container slot (`CMSG_BUY_ITEM_IN_SLOT`, layout in
|
||||
/// [`messages::buy_item_in_slot`]) — the merchant cursor's drop, as against [`Self::buy_item`]'s
|
||||
/// auto-place from a row click. `bag_guid` is the container object's guid, or the player's own
|
||||
/// for the backpack and the equipment slots.
|
||||
/// `CMSG_BUY_ITEM_IN_SLOT`: the merchant cursor's drop. `bag_guid` is the container's guid, or
|
||||
/// the player's own for the backpack and the equipment slots.
|
||||
pub fn buy_item_in_slot(
|
||||
&mut self,
|
||||
vendor_guid: u64,
|
||||
@@ -54,10 +41,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Sell an item to a vendor (`CMSG_SELL_ITEM`, layout in [`messages::sell_item`]): `count` 0 =
|
||||
/// sell the whole stack. Success is silent (the item vanishes + coinage rises via
|
||||
/// `UPDATE_OBJECT`); refusal answers `SMSG_SELL_ITEM`'s error shape (a `VendorSellFailed`
|
||||
/// event).
|
||||
/// `CMSG_SELL_ITEM`: `count` 0 sells the whole stack. Success is silent; a refusal comes back
|
||||
/// as `SMSG_SELL_ITEM`'s error shape.
|
||||
pub fn sell_item(&mut self, vendor_guid: u64, item_guid: u64, count: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_SELL_ITEM,
|
||||
@@ -65,9 +50,8 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Buy a sold item back (`CMSG_BUYBACK_ITEM`, layout in [`messages::buyback_item`]): `slot` is
|
||||
/// the absolute player-array buyback slot 69–80. Success is the item re-creating + coinage
|
||||
/// falling via `UPDATE_OBJECT`; refusal answers `SMSG_BUY_FAILED`.
|
||||
/// `CMSG_BUYBACK_ITEM`: `slot` is the absolute player-array buyback slot, 69 to 80; a refusal
|
||||
/// is `SMSG_BUY_FAILED`.
|
||||
pub fn buyback_item(&mut self, vendor_guid: u64, slot: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_BUYBACK_ITEM,
|
||||
@@ -75,9 +59,7 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Repair at a repair-capable vendor (`CMSG_REPAIR_ITEM`, layout in
|
||||
/// [`messages::repair_item`]): `item_guid` 0 = repair everything. No dedicated answer packet —
|
||||
/// durability rises + coinage falls via `UPDATE_OBJECT`.
|
||||
/// `CMSG_REPAIR_ITEM`: `item_guid` 0 repairs everything; there is no reply packet.
|
||||
pub fn repair_item(&mut self, vendor_guid: u64, item_guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_REPAIR_ITEM,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
//! Oracle-free golden tests for the auction house arc's protocol layer (decision 1511 phase P0):
|
||||
//! byte-exact goldens for all seven CMSG bodies, the `MSG_AUCTION_HELLO` reply, the four
|
||||
//! `SMSG_AUCTION_COMMAND_RESULT` tail shapes, the one 64-byte record and one frame the three list
|
||||
//! results share (including that `total_count` rides *after* the records, and that a body shorter
|
||||
//! than its own `count` claims still yields the records it does carry), the two deliberately
|
||||
//! different notifications, and the removal notice. Same idioms as `tests/mail.rs` — `hx(...)`
|
||||
//! golden CMSG bodies, hand-built SMSG bodies round-tripped through `parse_server`, and a
|
||||
//! `decode()` bridge assertion.
|
||||
//! Golden tests for the auction house wire: every client body byte-exact, and hand-built server
|
||||
//! bodies through `parse_server` and `decode`.
|
||||
|
||||
use benilla_protocol::events::{decode, SessionEvent};
|
||||
use benilla_protocol::messages::{
|
||||
@@ -26,14 +20,13 @@ const AUCTIONEER: u64 = 0x00F1_3000_0000_0055;
|
||||
|
||||
#[test]
|
||||
fn auction_send_bodies_golden() {
|
||||
// MSG_AUCTION_HELLO: one full auctioneer guid, nothing else.
|
||||
// MSG_AUCTION_HELLO: the auctioneer's full guid and nothing else.
|
||||
assert_eq!(
|
||||
messages::auction_hello(AUCTIONEER),
|
||||
hx("550000000030f100"),
|
||||
"MSG_AUCTION_HELLO body"
|
||||
);
|
||||
|
||||
// CMSG_AUCTION_SELL_ITEM: u64 auctioneer, u64 itemGuid, u32 bid, u32 buyout, u32 etime.
|
||||
assert_eq!(
|
||||
messages::auction_sell_item(
|
||||
AUCTIONEER,
|
||||
@@ -76,9 +69,7 @@ fn auction_send_bodies_golden() {
|
||||
|
||||
#[test]
|
||||
fn auction_list_items_body_golden() {
|
||||
// CMSG_AUCTION_LIST_ITEMS with every filter set: u64 auctioneer, u32 listfrom, cstr name,
|
||||
// u8 levelmin, u8 levelmax, u32 slotId, u32 mainCategory, u32 subCategory, u32 quality,
|
||||
// u8 usable. TEN fields — no sort column, no sort count, no trailing padding on 5875.
|
||||
// Ten fields and nothing after them: 1.12 has no sort column, sort count or padding.
|
||||
assert_eq!(
|
||||
messages::auction_list_items(AUCTIONEER, 50, "Copper", 10, 20, 1, 2, 3, 4, 1),
|
||||
hx(concat!(
|
||||
@@ -90,15 +81,13 @@ fn auction_list_items_body_golden() {
|
||||
"01000000", // slotId
|
||||
"02000000", // mainCategory
|
||||
"03000000", // subCategory
|
||||
"04000000", // quality (a MINIMUM, not an equality)
|
||||
"04000000", // quality, a minimum, not an equality
|
||||
"01", // usable
|
||||
)),
|
||||
"CMSG_AUCTION_LIST_ITEMS body (filters set)"
|
||||
);
|
||||
|
||||
// The default browse: every filter at its sentinel and an EMPTY search name — which is just
|
||||
// the lone NUL byte between `listfrom` and `levelmin`, nothing more. This exact body is the
|
||||
// one that puts vmangos on its no-filter fast path.
|
||||
// Every filter at its sentinel: the body that takes vmangos's no-filter fast path.
|
||||
let body = messages::auction_list_items(
|
||||
AUCTIONEER,
|
||||
0,
|
||||
@@ -116,7 +105,7 @@ fn auction_list_items_body_golden() {
|
||||
hx(concat!(
|
||||
"550000000030f100", // auctioneer
|
||||
"00000000", // listfrom 0
|
||||
"00", // "" — the lone NUL, and the whole of the name field
|
||||
"00", // "": the lone NUL is the whole name field
|
||||
"00", // levelmin 0
|
||||
"00", // levelmax 0
|
||||
"ffffffff", // slotId ANY
|
||||
@@ -127,22 +116,19 @@ fn auction_list_items_body_golden() {
|
||||
)),
|
||||
"CMSG_AUCTION_LIST_ITEMS body (empty name, all sentinels)"
|
||||
);
|
||||
// 8 + 4 + 1 + 1 + 1 + 4*4 + 1 — the tightest possible browse body. Stated as a number so an
|
||||
// invented trailing field cannot slip in behind a hex string nobody re-counts.
|
||||
// 8 + 4 + 1 + 1 + 1 + 4 * 4 + 1: the smallest browse body.
|
||||
assert_eq!(body.len(), 32, "no trailing sort bytes ride this opcode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auction_list_bidder_items_body_golden() {
|
||||
// CMSG_AUCTION_LIST_BIDDER_ITEMS with NO refresh ids: u64 auctioneer, u32 listfrom, u32 0.
|
||||
// The count field is always present even when the list is empty.
|
||||
// u64 auctioneer, u32 listfrom, u32 id count: the count is present even with no ids.
|
||||
assert_eq!(
|
||||
messages::auction_list_bidder_items(AUCTIONEER, 0, &[]),
|
||||
hx("550000000030f1000000000000000000"),
|
||||
"CMSG_AUCTION_LIST_BIDDER_ITEMS body (no ids)"
|
||||
);
|
||||
|
||||
// ...and with several: the count, then that many u32 auction ids.
|
||||
assert_eq!(
|
||||
messages::auction_list_bidder_items(AUCTIONEER, 50, &[7, 4242, 0xDEAD_BEEF]),
|
||||
hx(concat!(
|
||||
@@ -159,7 +145,7 @@ fn auction_list_bidder_items_body_golden() {
|
||||
|
||||
#[test]
|
||||
fn auction_hello_reply_wire() {
|
||||
// MSG_AUCTION_HELLO's reply (same opcode as our request): u64 auctioneerGuid, u32 houseId.
|
||||
// The reply rides the request's opcode: u64 auctioneer, u32 houseId.
|
||||
let mut body = AUCTIONEER.to_le_bytes().to_vec();
|
||||
body.extend_from_slice(&6u32.to_le_bytes()); // houseId 6 (Orgrimmar), AuctionHouse.dbc 1..7
|
||||
match messages::parse_server(messages::opcode::MSG_AUCTION_HELLO, &body).unwrap() {
|
||||
@@ -169,7 +155,6 @@ fn auction_hello_reply_wire() {
|
||||
} => assert_eq!((auctioneer, house_id), (AUCTIONEER, 6)),
|
||||
other => panic!("auction hello, got {}", other.name()),
|
||||
}
|
||||
// The decode() bridge carries both fields through unchanged.
|
||||
let packet = messages::parse_server(messages::opcode::MSG_AUCTION_HELLO, &body).unwrap();
|
||||
match decode(packet).pop().unwrap() {
|
||||
SessionEvent::AuctionHello {
|
||||
@@ -202,8 +187,7 @@ fn parse_command(body: &[u8]) -> (u32, u32, u32, AuctionCommandTail) {
|
||||
|
||||
#[test]
|
||||
fn auction_command_result_bare_wire() {
|
||||
// The bare 3-u32 form: OK + STARTED carries NO tail (the server's OK arm writes one only for
|
||||
// BID_PLACED). This is the shape a successful listing and a successful cancel both take.
|
||||
// OK carries a tail only with BID_PLACED, so a successful listing or cancel is bare.
|
||||
let body = command_head(4242, auction_action::STARTED, auction_error::OK);
|
||||
assert_eq!(body.len(), 12);
|
||||
assert_eq!(
|
||||
@@ -216,7 +200,6 @@ fn auction_command_result_bare_wire() {
|
||||
)
|
||||
);
|
||||
|
||||
// OK + REMOVED, likewise bare.
|
||||
assert_eq!(
|
||||
parse_command(&command_head(
|
||||
4242,
|
||||
@@ -227,8 +210,7 @@ fn auction_command_result_bare_wire() {
|
||||
AuctionCommandTail::Empty
|
||||
);
|
||||
|
||||
// A named error with no tail of its own (NOT_ENOUGH_MONEY) — bare too, and `auction_id` is
|
||||
// `0` because the server had no AuctionEntry to name.
|
||||
// `auction_id` is 0 when the server had no auction to name.
|
||||
assert_eq!(
|
||||
parse_command(&command_head(
|
||||
0,
|
||||
@@ -243,14 +225,12 @@ fn auction_command_result_bare_wire() {
|
||||
)
|
||||
);
|
||||
|
||||
// An UNNAMED error code (6, 8, 9, 11, 12 have no vmangos enumerator) also decodes bare rather
|
||||
// than erroring — the real client falls through to a generic failure branch for these.
|
||||
// Codes 6, 8, 9, 11 and 12 have no vmangos name; the 1.12 client shows a generic failure.
|
||||
assert_eq!(
|
||||
parse_command(&command_head(0, auction_action::BID_PLACED, 9)).3,
|
||||
AuctionCommandTail::Empty
|
||||
);
|
||||
|
||||
// The decode() bridge, asserted on this shape.
|
||||
let packet = messages::parse_server(
|
||||
messages::opcode::SMSG_AUCTION_COMMAND_RESULT,
|
||||
&command_head(4242, auction_action::STARTED, auction_error::OK),
|
||||
@@ -277,8 +257,6 @@ fn auction_command_result_bare_wire() {
|
||||
|
||||
#[test]
|
||||
fn auction_command_result_bid_placed_tail_wire() {
|
||||
// OK **and** BID_PLACED: one trailing u32 newMinOutBid. Both fields have to match — an OK on
|
||||
// any other action carries nothing (asserted in the bare test above).
|
||||
let mut body = command_head(4242, auction_action::BID_PLACED, auction_error::OK);
|
||||
body.extend_from_slice(&617u32.to_le_bytes());
|
||||
assert_eq!(
|
||||
@@ -296,8 +274,7 @@ fn auction_command_result_bid_placed_tail_wire() {
|
||||
|
||||
#[test]
|
||||
fn auction_command_result_inventory_tail_wire() {
|
||||
// INVENTORY: one trailing u32 InventoryResult (EQUIP_ERR_*). Keyed on the ERROR alone — the
|
||||
// action here is STARTED, which carries no tail under OK.
|
||||
// The INVENTORY tail is keyed on the error alone, whatever the action.
|
||||
let mut body = command_head(0, auction_action::STARTED, auction_error::INVENTORY);
|
||||
body.extend_from_slice(&2u32.to_le_bytes()); // EQUIP_ERR_* code
|
||||
assert_eq!(
|
||||
@@ -313,8 +290,6 @@ fn auction_command_result_inventory_tail_wire() {
|
||||
|
||||
#[test]
|
||||
fn auction_command_result_higher_bid_tail_wire() {
|
||||
// HIGHER_BID: u64 newBidder, u32 newBid, u32 newMinOutBid — 16 trailing bytes, the longest of
|
||||
// the three tails and the one a reader most easily under-reads.
|
||||
let mut body = command_head(4242, auction_action::BID_PLACED, auction_error::HIGHER_BID);
|
||||
body.extend_from_slice(&0x0000_0000_0000_07D1u64.to_le_bytes()); // new bidder
|
||||
body.extend_from_slice(&25_000u32.to_le_bytes()); // new bid
|
||||
@@ -352,8 +327,7 @@ fn push_record(body: &mut Vec<u8>, e: &AuctionListEntry) {
|
||||
body.extend_from_slice(&e.time_left_ms.to_le_bytes());
|
||||
body.extend_from_slice(&e.bidder_guid.to_le_bytes());
|
||||
body.extend_from_slice(&e.current_bid.to_le_bytes());
|
||||
// The record's width is load-bearing (it is the reader's short-buffer bound) and decision
|
||||
// 1511's prose miscounts it as 60; pin it here so the two can never drift apart silently.
|
||||
// The record width is the reader's short-buffer bound.
|
||||
assert_eq!(body.len() - before, AUCTION_RECORD_BYTES);
|
||||
assert_eq!(AUCTION_RECORD_BYTES, 64);
|
||||
}
|
||||
@@ -398,14 +372,11 @@ fn parse_list(opcode: u16, body: &[u8]) -> (Vec<AuctionListEntry>, u32) {
|
||||
|
||||
#[test]
|
||||
fn auction_list_result_empty_wire() {
|
||||
// count 0, no records, then the trailing totalCount — which can still be nonzero (page 2 of a
|
||||
// one-page result set asked past the end).
|
||||
// Count 0, then a totalCount that is still nonzero when a page past the end is asked.
|
||||
let mut body = 0u32.to_le_bytes().to_vec();
|
||||
body.extend_from_slice(&37u32.to_le_bytes());
|
||||
assert_eq!(body.len(), 8);
|
||||
|
||||
// All three list opcodes share this frame and this record; assert on each so a future arm
|
||||
// cannot be wired to the wrong reader.
|
||||
for opcode in [
|
||||
messages::opcode::SMSG_AUCTION_LIST_RESULT,
|
||||
messages::opcode::SMSG_AUCTION_OWNER_LIST_RESULT,
|
||||
@@ -419,7 +390,6 @@ fn auction_list_result_empty_wire() {
|
||||
|
||||
#[test]
|
||||
fn auction_list_result_records_wire() {
|
||||
// Three rows through the real parse_server entry point, then the trailing totalCount.
|
||||
let rows = [
|
||||
unbid_row(1),
|
||||
AuctionListEntry {
|
||||
@@ -451,14 +421,12 @@ fn auction_list_result_records_wire() {
|
||||
assert_eq!(auctions, rows);
|
||||
assert_eq!(total_count, 129);
|
||||
|
||||
// Field semantics worth pinning rather than assuming: `min_increment` is 0 while nobody has
|
||||
// bid, `buyout` 0 means no buyout, and `start_bid` is NOT the current bid.
|
||||
// `min_increment` is 0 until a bid, `buyout` 0 means none, `start_bid` is not the current bid.
|
||||
assert_eq!(auctions[0].min_increment, 0);
|
||||
assert_eq!(auctions[0].current_bid, 0);
|
||||
assert_eq!(auctions[1].buyout, 0);
|
||||
assert_ne!(auctions[1].start_bid, auctions[1].current_bid);
|
||||
|
||||
// The decode() bridge carries the rows and the total through unchanged.
|
||||
let packet = messages::parse_server(messages::opcode::SMSG_AUCTION_LIST_RESULT, &body).unwrap();
|
||||
match decode(packet).pop().unwrap() {
|
||||
SessionEvent::AuctionListResult {
|
||||
@@ -471,7 +439,6 @@ fn auction_list_result_records_wire() {
|
||||
other => panic!("auction list result event, got {other:?}"),
|
||||
}
|
||||
|
||||
// The owner/bidder opcodes land on their own variants off the same bytes.
|
||||
let packet =
|
||||
messages::parse_server(messages::opcode::SMSG_AUCTION_OWNER_LIST_RESULT, &body).unwrap();
|
||||
assert!(matches!(
|
||||
@@ -488,10 +455,7 @@ fn auction_list_result_records_wire() {
|
||||
|
||||
#[test]
|
||||
fn auction_list_result_total_count_rides_after_the_records() {
|
||||
// `totalCount` is at the very END of the body, not beside the leading `count`. Build ONE
|
||||
// record whose every field differs from the total, so a reader that took the total from the
|
||||
// front (or from any record field) is caught: the leading count is 1, the total is 9999, and
|
||||
// no record field holds 9999.
|
||||
// Neither the leading count nor any record field is 9999, so only the trailing total can be.
|
||||
let row = AuctionListEntry {
|
||||
auction_id: 11,
|
||||
item_entry: 12,
|
||||
@@ -522,9 +486,7 @@ fn auction_list_result_total_count_rides_after_the_records() {
|
||||
|
||||
#[test]
|
||||
fn auction_list_record_round_trips_negative_signed_fields() {
|
||||
// `random_property_id` and `spell_charges` are SIGNED (the server casts int32 through uint32).
|
||||
// A negative random property id is a random *suffix* ("of the Bear"); a negative charge count
|
||||
// means "N charges then destroy". Read unsigned, both become ~4.29-billion nonsense.
|
||||
// Both are int32: a negative property id is a suffix; negative charges destroy when spent.
|
||||
let row = AuctionListEntry {
|
||||
auction_id: 77,
|
||||
item_entry: 7_078,
|
||||
@@ -550,8 +512,7 @@ fn auction_list_record_round_trips_negative_signed_fields() {
|
||||
assert_eq!(auctions[0].spell_charges, -5);
|
||||
assert_eq!(auctions[0].suffix_factor, 143);
|
||||
|
||||
// And the unclamped time_left: an expired-but-unswept auction wraps rather than clamping to
|
||||
// 0. The reader must pass it through faithfully; judging it is the consumer's job.
|
||||
// An expired but unswept auction's time left wraps instead of clamping; it passes through.
|
||||
let mut expired = unbid_row(78);
|
||||
expired.time_left_ms = u32::MAX - 500; // (expire - now) * 1000 on a negative difference
|
||||
let mut body = 1u32.to_le_bytes().to_vec();
|
||||
@@ -563,9 +524,7 @@ fn auction_list_record_round_trips_negative_signed_fields() {
|
||||
|
||||
#[test]
|
||||
fn auction_list_result_survives_a_count_larger_than_the_records() {
|
||||
// vmangos's browse fast path increments `count` for a record it then fails to write (a stale
|
||||
// auction whose item row is gone writes ZERO bytes and still counts). A body claiming 3 and
|
||||
// carrying 2 must yield those 2 rather than failing the whole page.
|
||||
// vmangos's browse fast path counts a stale auction whose item is gone but writes no bytes.
|
||||
let rows = [unbid_row(1), unbid_row(2)];
|
||||
let mut body = 3u32.to_le_bytes().to_vec(); // the server's inflated count
|
||||
for row in &rows {
|
||||
@@ -577,8 +536,7 @@ fn auction_list_result_survives_a_count_larger_than_the_records() {
|
||||
assert_eq!(auctions, rows, "the records that DID arrive come back");
|
||||
assert_eq!(total_count, 2);
|
||||
|
||||
// The harsher variant: truncated past the last record, so even the trailing total is missing.
|
||||
// Still not an error — the total falls back to what we read.
|
||||
// With the trailing total missing too, the total falls back to the records read.
|
||||
let mut body = 3u32.to_le_bytes().to_vec();
|
||||
for row in &rows {
|
||||
push_record(&mut body, row);
|
||||
@@ -587,8 +545,7 @@ fn auction_list_result_survives_a_count_larger_than_the_records() {
|
||||
assert_eq!(auctions, rows);
|
||||
assert_eq!(total_count, 2, "falls back to the records actually read");
|
||||
|
||||
// And a count that is pure nonsense (a hostile/desynced body) must not allocate on its word
|
||||
// alone or error — it yields the one record the buffer holds.
|
||||
// A nonsense count yields the one record the buffer holds.
|
||||
let mut body = u32::MAX.to_le_bytes().to_vec();
|
||||
push_record(&mut body, &rows[0]);
|
||||
let (auctions, _) = parse_list(messages::opcode::SMSG_AUCTION_LIST_RESULT, &body);
|
||||
@@ -597,13 +554,11 @@ fn auction_list_result_survives_a_count_larger_than_the_records() {
|
||||
|
||||
#[test]
|
||||
fn auction_bidder_notification_wire() {
|
||||
// SMSG_AUCTION_BIDDER_NOTIFICATION: u32 houseId, u32 auctionId, u64 bidderGuid,
|
||||
// u32 bidOrZero, u32 outBid, u32 itemEntry, i32 randomPropertyId — houseId FIRST and the
|
||||
// guid THIRD, unlike the owner notification.
|
||||
// House id first and the guid third, unlike the owner notification.
|
||||
let mut body = 6u32.to_le_bytes().to_vec(); // houseId
|
||||
body.extend_from_slice(&4242u32.to_le_bytes()); // auctionId
|
||||
body.extend_from_slice(&0x0000_0000_0000_0505u64.to_le_bytes()); // bidderGuid
|
||||
body.extend_from_slice(&15_000u32.to_le_bytes()); // bidOrZero — nonzero: OUTBID
|
||||
body.extend_from_slice(&15_000u32.to_le_bytes()); // bidOrZero, nonzero: outbid
|
||||
body.extend_from_slice(&750u32.to_le_bytes()); // outBid
|
||||
body.extend_from_slice(&12_640u32.to_le_bytes()); // itemEntry
|
||||
body.extend_from_slice(&(-19i32).to_le_bytes()); // randomPropertyId, signed
|
||||
@@ -623,8 +578,7 @@ fn auction_bidder_notification_wire() {
|
||||
other => panic!("auction bidder notification, got {}", other.name()),
|
||||
}
|
||||
|
||||
// bidOrZero == 0 means WON, not "no bid" — the one field whose zero is a *state*, and the
|
||||
// reason this notification cannot share a struct with the owner one.
|
||||
// bidOrZero 0 means won, not no bid.
|
||||
let mut won = body.clone();
|
||||
won[16..20].copy_from_slice(&0u32.to_le_bytes());
|
||||
match messages::parse_server(messages::opcode::SMSG_AUCTION_BIDDER_NOTIFICATION, &won).unwrap()
|
||||
@@ -637,9 +591,7 @@ fn auction_bidder_notification_wire() {
|
||||
other => panic!("auction bidder notification (won), got {}", other.name()),
|
||||
}
|
||||
|
||||
// The two notifications must never share a reader. Feed these same bytes to the OWNER opcode
|
||||
// (28 bytes needed, 32 available, so it parses rather than EOF-ing) and every field lands
|
||||
// somewhere else: the house id is eaten as the auction id and the guid slides four bytes.
|
||||
// The owner reader takes 28 of these 32 bytes and misplaces every field.
|
||||
match messages::parse_server(messages::opcode::SMSG_AUCTION_OWNER_NOTIFICATION, &body).unwrap()
|
||||
{
|
||||
ServerPacket::AuctionOwnerNotification(n) => {
|
||||
@@ -663,9 +615,7 @@ fn auction_bidder_notification_wire() {
|
||||
|
||||
#[test]
|
||||
fn auction_owner_notification_wire() {
|
||||
// SMSG_AUCTION_OWNER_NOTIFICATION: u32 auctionId, u32 bid, u32 outBid, u64 bidderGuid,
|
||||
// u32 itemEntry, i32 randomPropertyId — NO houseId, and the guid sits FOURTH. Deliberately a
|
||||
// different shape from the bidder notification above.
|
||||
// No house id and the guid fourth, unlike the bidder notification.
|
||||
let mut body = 4242u32.to_le_bytes().to_vec(); // auctionId
|
||||
body.extend_from_slice(&15_000u32.to_le_bytes()); // bid
|
||||
body.extend_from_slice(&750u32.to_le_bytes()); // outBid
|
||||
@@ -688,7 +638,7 @@ fn auction_owner_notification_wire() {
|
||||
other => panic!("auction owner notification, got {}", other.name()),
|
||||
}
|
||||
|
||||
// An all-zero bidder guid is the "sold" signal, not a missing bidder.
|
||||
// An all-zero bidder guid means sold, not a missing bidder.
|
||||
let mut sold = body.clone();
|
||||
sold[12..20].copy_from_slice(&0u64.to_le_bytes());
|
||||
match messages::parse_server(messages::opcode::SMSG_AUCTION_OWNER_NOTIFICATION, &sold).unwrap()
|
||||
|
||||
Reference in New Issue
Block a user