diff --git a/crates/benilla-protocol/src/world/writer/guild.rs b/crates/benilla-protocol/src/world/writer/guild.rs index d8d98ef1..9330b7ce 100644 --- a/crates/benilla-protocol/src/world/writer/guild.rs +++ b/crates/benilla-protocol/src/world/writer/guild.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/instance.rs b/crates/benilla-protocol/src/world/writer/instance.rs index 3dc42b02..9afa140c 100644 --- a/crates/benilla-protocol/src/world/writer/instance.rs +++ b/crates/benilla-protocol/src/world/writer/instance.rs @@ -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()) } diff --git a/crates/benilla-protocol/src/world/writer/items.rs b/crates/benilla-protocol/src/world/writer/items.rs index e9fccceb..eebf5c5d 100644 --- a/crates/benilla-protocol/src/world/writer/items.rs +++ b/crates/benilla-protocol/src/world/writer/items.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/lifecycle.rs b/crates/benilla-protocol/src/world/writer/lifecycle.rs index 8db74493..94d38a2f 100644 --- a/crates/benilla-protocol/src/world/writer/lifecycle.rs +++ b/crates/benilla-protocol/src/world/writer/lifecycle.rs @@ -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, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/loot.rs b/crates/benilla-protocol/src/world/writer/loot.rs index 771c09f6..eacb9ac8 100644 --- a/crates/benilla-protocol/src/world/writer/loot.rs +++ b/crates/benilla-protocol/src/world/writer/loot.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/mail.rs b/crates/benilla-protocol/src/world/writer/mail.rs index 3d10954e..1d2ef857 100644 --- a/crates/benilla-protocol/src/world/writer/mail.rs +++ b/crates/benilla-protocol/src/world/writer/mail.rs @@ -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, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/meeting_stone.rs b/crates/benilla-protocol/src/world/writer/meeting_stone.rs index 3728686e..62afd247 100644 --- a/crates/benilla-protocol/src/world/writer/meeting_stone.rs +++ b/crates/benilla-protocol/src/world/writer/meeting_stone.rs @@ -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, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/mod.rs b/crates/benilla-protocol/src/world/writer/mod.rs index 3563fd46..de0fa1d1 100644 --- a/crates/benilla-protocol/src/world/writer/mod.rs +++ b/crates/benilla-protocol/src/world/writer/mod.rs @@ -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>, - /// 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(..) { diff --git a/crates/benilla-protocol/src/world/writer/names.rs b/crates/benilla-protocol/src/world/writer/names.rs index f0455cd2..430b47e0 100644 --- a/crates/benilla-protocol/src/world/writer/names.rs +++ b/crates/benilla-protocol/src/world/writer/names.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/pet.rs b/crates/benilla-protocol/src/world/writer/pet.rs index 8d5224ce..7eec2614 100644 --- a/crates/benilla-protocol/src/world/writer/pet.rs +++ b/crates/benilla-protocol/src/world/writer/pet.rs @@ -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)) } diff --git a/crates/benilla-protocol/src/world/writer/petition.rs b/crates/benilla-protocol/src/world/writer/petition.rs index 6c023bea..2bb588c1 100644 --- a/crates/benilla-protocol/src/world/writer/petition.rs +++ b/crates/benilla-protocol/src/world/writer/petition.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/player_flags.rs b/crates/benilla-protocol/src/world/writer/player_flags.rs index 84fea4d8..9e102847 100644 --- a/crates/benilla-protocol/src/world/writer/player_flags.rs +++ b/crates/benilla-protocol/src/world/writer/player_flags.rs @@ -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, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/pose.rs b/crates/benilla-protocol/src/world/writer/pose.rs index 48946ad6..bdf1a520 100644 --- a/crates/benilla-protocol/src/world/writer/pose.rs +++ b/crates/benilla-protocol/src/world/writer/pose.rs @@ -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, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/progression.rs b/crates/benilla-protocol/src/world/writer/progression.rs index fc722336..aa7e9fb8 100644 --- a/crates/benilla-protocol/src/world/writer/progression.rs +++ b/crates/benilla-protocol/src/world/writer/progression.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/pvp.rs b/crates/benilla-protocol/src/world/writer/pvp.rs index 9cfeb34f..88aebb9e 100644 --- a/crates/benilla-protocol/src/world/writer/pvp.rs +++ b/crates/benilla-protocol/src/world/writer/pvp.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/quest.rs b/crates/benilla-protocol/src/world/writer/quest.rs index 03befbaa..2886f17c 100644 --- a/crates/benilla-protocol/src/world/writer/quest.rs +++ b/crates/benilla-protocol/src/world/writer/quest.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/reputation.rs b/crates/benilla-protocol/src/world/writer/reputation.rs index a4c51262..6940385c 100644 --- a/crates/benilla-protocol/src/world/writer/reputation.rs +++ b/crates/benilla-protocol/src/world/writer/reputation.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/selection.rs b/crates/benilla-protocol/src/world/writer/selection.rs index e23d09d8..e9bd50bc 100644 --- a/crates/benilla-protocol/src/world/writer/selection.rs +++ b/crates/benilla-protocol/src/world/writer/selection.rs @@ -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)) } diff --git a/crates/benilla-protocol/src/world/writer/self_movement.rs b/crates/benilla-protocol/src/world/writer/self_movement.rs index 887dfe6c..df66c9a1 100644 --- a/crates/benilla-protocol/src/world/writer/self_movement.rs +++ b/crates/benilla-protocol/src/world/writer/self_movement.rs @@ -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, ) -> 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, diff --git a/crates/benilla-protocol/src/world/writer/skills.rs b/crates/benilla-protocol/src/world/writer/skills.rs index 5a944ab6..b48ca204 100644 --- a/crates/benilla-protocol/src/world/writer/skills.rs +++ b/crates/benilla-protocol/src/world/writer/skills.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/social.rs b/crates/benilla-protocol/src/world/writer/social.rs index e5a76321..c342723a 100644 --- a/crates/benilla-protocol/src/world/writer/social.rs +++ b/crates/benilla-protocol/src/world/writer/social.rs @@ -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)) } diff --git a/crates/benilla-protocol/src/world/writer/spells.rs b/crates/benilla-protocol/src/world/writer/spells.rs index 14452bf4..84e36c11 100644 --- a/crates/benilla-protocol/src/world/writer/spells.rs +++ b/crates/benilla-protocol/src/world/writer/spells.rs @@ -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) -> 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()) } diff --git a/crates/benilla-protocol/src/world/writer/stable.rs b/crates/benilla-protocol/src/world/writer/stable.rs index 91496025..49c8bc41 100644 --- a/crates/benilla-protocol/src/world/writer/stable.rs +++ b/crates/benilla-protocol/src/world/writer/stable.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/summon.rs b/crates/benilla-protocol/src/world/writer/summon.rs index d755cbbd..15ca1e57 100644 --- a/crates/benilla-protocol/src/world/writer/summon.rs +++ b/crates/benilla-protocol/src/world/writer/summon.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/tabard.rs b/crates/benilla-protocol/src/world/writer/tabard.rs index 5502dda4..36ef37a5 100644 --- a/crates/benilla-protocol/src/world/writer/tabard.rs +++ b/crates/benilla-protocol/src/world/writer/tabard.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/taxi.rs b/crates/benilla-protocol/src/world/writer/taxi.rs index 1975f66d..3f88dfd9 100644 --- a/crates/benilla-protocol/src/world/writer/taxi.rs +++ b/crates/benilla-protocol/src/world/writer/taxi.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/trade.rs b/crates/benilla-protocol/src/world/writer/trade.rs index 1062c61b..3a052d33 100644 --- a/crates/benilla-protocol/src/world/writer/trade.rs +++ b/crates/benilla-protocol/src/world/writer/trade.rs @@ -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, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/trainer.rs b/crates/benilla-protocol/src/world/writer/trainer.rs index 1e306c75..5a6b544b 100644 --- a/crates/benilla-protocol/src/world/writer/trainer.rs +++ b/crates/benilla-protocol/src/world/writer/trainer.rs @@ -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, diff --git a/crates/benilla-protocol/src/world/writer/tutorial.rs b/crates/benilla-protocol/src/world/writer/tutorial.rs index 44be9e43..d223a3c4 100644 --- a/crates/benilla-protocol/src/world/writer/tutorial.rs +++ b/crates/benilla-protocol/src/world/writer/tutorial.rs @@ -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, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/vendor.rs b/crates/benilla-protocol/src/world/writer/vendor.rs index aaf61b80..6d5fac8c 100644 --- a/crates/benilla-protocol/src/world/writer/vendor.rs +++ b/crates/benilla-protocol/src/world/writer/vendor.rs @@ -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, diff --git a/crates/benilla-protocol/tests/auction.rs b/crates/benilla-protocol/tests/auction.rs index 67ee7a2c..a7b5d407 100644 --- a/crates/benilla-protocol/tests/auction.rs +++ b/crates/benilla-protocol/tests/auction.rs @@ -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, 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, 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()