diff --git a/crates/benilla-protocol/src/messages/update_object/movement.rs b/crates/benilla-protocol/src/messages/update_object/movement.rs index 12cc4282..876d3411 100644 --- a/crates/benilla-protocol/src/messages/update_object/movement.rs +++ b/crates/benilla-protocol/src/messages/update_object/movement.rs @@ -36,127 +36,66 @@ impl ObjectType { } } -// MovementBlock update_flag bits (UpdateObject-only; the shared MOVEMENT_FLAG_* live in `super`). const UPDATE_FLAG_TRANSPORT: u8 = 0x02; const UPDATE_FLAG_MELEE_ATTACKING: u8 = 0x04; const UPDATE_FLAG_HIGH_GUID: u8 = 0x08; const UPDATE_FLAG_ALL: u8 = 0x10; const UPDATE_FLAG_LIVING: u8 = 0x20; const UPDATE_FLAG_HAS_POSITION: u8 = 0x40; -// SplineFlag final-destination bits (checked angle → target → point). const SPLINE_FLAG_FINAL_POINT: u32 = 0x1_0000; const SPLINE_FLAG_FINAL_TARGET: u32 = 0x2_0000; const SPLINE_FLAG_FINAL_ANGLE: u32 = 0x4_0000; -/// `MoveSplineFlag::Flying` — and in vmangos 1.12 that bit alone *is* `Mask_CatmullRom` -/// (`MoveSplineFlag.h:48,77`), the same split [`crate::ServerPacket::MonsterMove`] reads: set ⇒ a 3-D -/// flight path that keeps its own Z, clear ⇒ a ground walk whose Z the client re-derives from terrain. +/// `MoveSplineFlag::Flying`; in vmangos it alone is `Mask_CatmullRom` (`MoveSplineFlag.h:48,77`). const SPLINE_FLAG_FLYING: u32 = 0x200; -/// `SPLINEFLAG_RUNMODE` — see [`super::super::monster_move`]'s copy for why its *absence* is what -/// matters: a spline without it forces walk mode on for the unit it moves. const SPLINE_FLAG_RUNMODE: u32 = 0x100; -/// `MoveSplineFlag::Cyclic` (`MoveSplineFlag.h:59`) — the path loops back on itself forever. +/// `MoveSplineFlag::Cyclic` (`MoveSplineFlag.h:59`). const SPLINE_FLAG_CYCLIC: u32 = 0x10_0000; -/// The **live spline a unit is already riding** when it streams into view — the `LIVING` block's -/// `MOVEFLAG_SPLINE_ENABLED` tail (vmangos `Object.cpp:535` → `PacketBuilder::WriteCreate`, -/// `packet_builder.cpp:152`). Without it, a creature that was walking when we first saw it stands -/// frozen at its create pose until the server's *next* `SMSG_MONSTER_MOVE` — which then teleports it -/// to wherever it actually got to (decision 0708). -/// -/// [`Self::path`] is the **travel-order polyline**, the same shape `MonsterMove` decodes to. The wire -/// carries the server's raw internal control array (`MoveSpline::getPath()` = `Spline::getPoints()`), -/// which is *not* that: vmangos builds every spline — linear ones included — through -/// `SplineBase::InitCatmullRom` ("we should use catmullrom initializer even for linear mode!", -/// `spline.cpp:52`), so the array is `[phantom, p₀, …, pₙ, tail]` whose walkable range is -/// `index_lo = 1 ..= index_hi = len − 2` (`spline.cpp:238-270`): the phantom head is `p₀` extrapolated -/// one segment *backwards*, and the tail duplicates the destination (or, cyclic, closes the loop). -/// We hand over exactly that range. +/// The spline a `LIVING` unit is already riding when it streams in (`packet_builder.cpp:152`). +/// vmangos builds every spline as Catmull-Rom (`spline.cpp:52`), so the wire array is +/// `[phantom, p₀, …, pₙ, tail]` and [`Self::path`] is its walkable range `1 ..= len − 2`. #[derive(Debug, Clone)] pub struct CreateSpline { - /// The full travel-order polyline in raw WoW coords: `[start, …waypoints…, endpoint]`. + /// The travel-order polyline in raw WoW coords, start to endpoint. pub path: Vec<[f32; 3]>, /// The server's spline id (`MoveSpline::GetId`), as on `MonsterMove`. pub id: u32, - /// Milliseconds of this spline the server has **already ridden** (`MoveSpline::timePassed`): the - /// unit is `time_passed_ms / duration_ms` of the way along `path`, so a client that starts the - /// ride at `now − time_passed_ms` lands exactly where the server has it. + /// Ms of the path already ridden (`MoveSpline::timePassed`); start the ride at now minus this. pub time_passed_ms: u32, - /// The whole path's duration in ms (`MoveSpline::Duration` = the spline's length, which vmangos - /// initialises in *time* units at one constant velocity — `CommonInitializer`, - /// `MoveSpline.cpp:125-135`). Time is therefore uniform in arc length along the whole polyline, - /// which is what makes the `time_passed` fraction above exact. + /// Duration in ms at one constant speed, time uniform in arc length (`MoveSpline.cpp:125-135`). pub duration_ms: u32, /// `MoveSplineFlag::Flying`: a 3-D flight path (keep the spline's Z); clear for a ground walk. pub flying: bool, - /// `MoveSplineFlag::Cyclic` — the server loops this path forever. The app rides it once; a looping - /// follow is unbuilt (the same gap `MonsterMove`'s cyclic paths have). + /// `MoveSplineFlag::Cyclic`: the server loops this path forever. pub cyclic: bool, - /// `MoveSplineFlag::Runmode` — and its absence forces walk mode on (decision 1758). + /// `MoveSplineFlag::Runmode`; its absence forces walk mode on. pub run_mode: bool, } /// A `LIVING` block's live mover state: the `MOVEMENTFLAGS` word and the swim pitch that rides it. -/// See [`MovementBlock::mover`] for why both are surfaced rather than parsed and dropped. #[derive(Debug, Clone, Copy, PartialEq)] pub struct MoverState { - /// The raw `MOVEMENTFLAGS` word (`CMovement+0x40`) — direction bits, swim, falling, the granted - /// modes. + /// The raw `MOVEMENTFLAGS` word (`CMovement+0x40`). pub flags: u32, - /// The **swim pitch** (radians, +up), present on the wire iff `MOVEFLAG_SWIMMING`; `0.0` (level) - /// otherwise. + /// Swim pitch in radians, +up; on the wire only with `MOVEFLAG_SWIMMING`, else `0.0`. pub pitch: f32, } -/// The decoded fields a movement block carries: the pose (from its `LIVING`/`HAS_POSITION` flag), -/// for a `LIVING` block the unit's 6 movement speeds, its live [`CreateSpline`] and (if -/// `ON_TRANSPORT`) its rider pose, and (if `UPDATE_FLAG_TRANSPORT`) a transport GameObject's path -/// progress. The rest of the block is parsed and discarded to stay aligned. +/// The decoded fields of a movement block; the rest is read and discarded to stay aligned. pub struct MovementBlock { pub position: Option<(Vector3d, f32)>, - /// A `LIVING` block's **live `MOVEMENTFLAGS` word** and its **swim pitch** — the mover state this - /// unit is *already in* at the moment it streams into view. `None` for a `HAS_POSITION`-only - /// block (a GameObject has no mover). - /// - /// **The reference applies both, byte-VERIFIED** (wow-5875-re §5, - /// `system/collision/scratch/create-block-swim-pitch.md`). The create-block apply `0x619320` - /// reaches `0x618c30`, which merges the block's flags into `CMovement+0x40` under the same - /// `0x75a07dff` mask a relayed `MSG_MOVE_*` uses (`0x618df3`; `SWIMMING`, `FORWARD` and - /// `BACKWARD` are all inside it) and then calls the pose commit `0x7c6420`, whose - /// `fld [edx+0x34]` / `fst [ecx+0x20]` copies **this pitch** into the mover pitch field - /// unconditionally, alongside the position and facing. So a unit that streams in mid-swim is - /// pitched on its **first drawn frame** — the apply runs before `0x613e10` has even built the - /// model or registered its per-frame render callback. - /// - /// Dropping the pair is why a player who came into view mid-swim rendered *level* until their - /// next relay packet: the body-pitch render law reads exactly these two values and had neither. - /// - /// The pitch is `0.0` (level) whenever `MOVEFLAG_SWIMMING` is clear, and that is the - /// reference's own value, not our convenience: the block field is explicitly zeroed at - /// `0x47ec4a` on the non-swimming leg, so the unconditional commit stores a real 0.0 rather - /// than stale memory. + /// A `LIVING` block's mover state. The reference merges these flags under the relay's + /// `0x75a07dff` mask (`0x618c30`) and commits the pitch at once (`0x7c6420`), so a unit + /// streaming in mid-swim is pitched on its first frame. pub mover: Option, - /// A `LIVING` block's 6 movement speeds (yd/s) in wire order `[walk, run, run_back, swim, swim_back, - /// turn_rate]`; `None` for a `HAS_POSITION`-only block (GameObjects, which don't move under their - /// own power). The animation selector keys walk-vs-run on `walk` (boundary 2× it — RF-0057); the - /// net bridge extrapolates a remote mover between packets at `run`/`run_back`/`swim` + `turn_rate`. + /// A `LIVING` block's speeds in wire order `[walk, run, run_back, swim, swim_back, turn_rate]`. pub speeds: Option<[f32; 6]>, - /// A transport GameObject's (`UPDATE_FLAG_TRANSPORT`, 0x02) path progress: the server's ms clock in - /// the path domain (vmangos `Object.cpp:590-605` — `GenericTransport::GetPathProgress()`, the same - /// `m_pathProgress` `ShipTransport::Update`/`CalculateSegmentPos` key off, `Transports/Transport.cpp: - /// 285-301`). `None` for every non-transport object: vmangos raises `UPDATEFLAG_TRANSPORT` only in - /// the transport GO paths — `ShipTransport`'s ctor (`Transport.cpp:39`) and the type-11 elevator - /// branch of `GameObject::Create` (`GameObject.cpp:246`); units/players never set it - /// (`Unit.cpp:88`). This is decision 0438's - /// **cycle anchor**: `anchor = this value`, `t₀ = Instant::now()` on create, `progress = anchor + - /// elapsed_ms` thereafter. + /// A ship's or elevator's path progress in ms at create, the anchor its cycle runs from + /// (`Object.cpp:590-605`); no other object sets `UPDATE_FLAG_TRANSPORT`. pub transport_progress: Option, - /// A `LIVING` block's rider pose on a transport (`MOVEFLAG_ON_TRANSPORT`, 0x0200_0000) — `Some` when this - /// unit/player is standing on a boat/zeppelin/elevator, its position local to that transport's frame - /// (decision 0438 "Riding is the mover's platform frame" — an observed rider carried this way - /// re-anchors through the transport's live matrix each frame, never treating `pos` as world-space). + /// A `LIVING` block's rider pose (`MOVEFLAG_ON_TRANSPORT`), local to the transport's frame. pub transport: Option, - /// The path this unit is **already walking** at create time (`MOVEFLAG_SPLINE_ENABLED`) — see - /// [`CreateSpline`]. `None` for a unit standing still, and for every non-`LIVING` block. + /// The path this unit is already walking at create (`MOVEFLAG_SPLINE_ENABLED`). pub spline: Option, } @@ -178,17 +117,13 @@ impl MovementBlock { position = Some((living_position, living_orientation)); if flags & MOVEMENT_FLAG_ON_TRANSPORT != 0 { - // A FULL u64, not packed: the LIVING block serializes through the same vmangos - // `MovementInfo::Write` as the MSG_MOVE relays (`Object.cpp:524` `*data << m`), - // whose `data << t_guid` is the plain-u64 `ObjectGuid` operator. + // A full u64, not packed, as in `MovementInfo::Write` (`Object.cpp:524`). transport = Some(TransportPose { guid: read_u64_le(r)?, pos: Vector3d::read(r)?, orientation: read_f32_le(r)?, }); } - // The swim-pitch tail — a lone `f32` present iff `MOVEFLAG_SWIMMING`, the same gate - // (and the same position in the layout) the `MSG_MOVE_*` body uses. let pitch = if flags & MOVEMENT_FLAG_SWIMMING != 0 { read_f32_le(r)? } else { @@ -204,9 +139,6 @@ impl MovementBlock { if flags & MOVEMENT_FLAG_SPLINE_ELEVATION != 0 { let _ = read_f32_le(r)?; } - // The 6 movement speeds (RF-0058 order): [0]=walk, [1]=run, [2]=run-back, [3]=swim, - // [4]=swim-back, [5]=turn-rate. The animation selector keys walk-vs-run on the walk speed - // (boundary = 2× it — RF-0057); the net bridge extrapolates a remote mover with the rest. let mut s = [0.0f32; 6]; for slot in &mut s { *slot = read_f32_le(r)?; @@ -214,10 +146,7 @@ impl MovementBlock { speeds = Some(s); if flags & MOVEMENT_FLAG_SPLINE_ENABLED != 0 { let spline_flags = read_u32_le(r)?; - // The dictated final facing, in vmangos's own angle → target → point order - // (`packet_builder.cpp:162-167`). Parsed for alignment only: a unit still riding a - // path faces its travel tangent every frame, exactly as `MonsterMove`'s facing snap is - // overwritten while a path runs (`net::objects::monster_move`). + // The final facing (`packet_builder.cpp:162-167`); a unit on a path faces along it. if spline_flags & SPLINE_FLAG_FINAL_ANGLE != 0 { let _ = read_f32_le(r)?; } else if spline_flags & SPLINE_FLAG_FINAL_TARGET != 0 { @@ -234,11 +163,9 @@ impl MovementBlock { let v = Vector3d::read(r)?; nodes.push([v.x, v.y, v.z]); } - // The final destination trails the array; it is `path`'s last real point again - // (`MoveSpline::FinalDestination` = `getPoint(last())`), or zero on a cyclic path. + // The final destination repeats the path's last point, or is zero on a cyclic path. let _final_node = Vector3d::read(r)?; - // Drop the server's two virtual control points — see [`CreateSpline`]. Fewer than - // four nodes cannot hold a two-point path, so there is nothing to ride. + // Drop the two virtual control points; under four nodes there is no path to ride. spline = (nodes.len() >= 4).then(|| CreateSpline { path: nodes[1..nodes.len() - 1].to_vec(), id, diff --git a/crates/benilla-protocol/src/messages/vendor.rs b/crates/benilla-protocol/src/messages/vendor.rs index 80012a8c..fa12b707 100644 --- a/crates/benilla-protocol/src/messages/vendor.rs +++ b/crates/benilla-protocol/src/messages/vendor.rs @@ -1,17 +1,11 @@ -//! Vendor messages — `CMSG_LIST_INVENTORY`/buy/sell (opcodes 414-421, vmangos `Opcodes_1_12_1.h`, -//! VERIFIED). Bodies from vmangos `Item.{h,cpp}` + the hand-serialized `ItemHandler.cpp`. Lives -//! beside, not inside, [`super::items`] (the item-*template* query pair): vendor rows carry their -//! own wire shape and the buy/sell result vocabulary, unrelated to -//! `SMSG_ITEM_QUERY_SINGLE_RESPONSE`. +//! Vendor messages: the inventory list, buy and sell (opcodes 414-421), buyback and repair. use std::io; use crate::wire::{capacity_hint, read_u32_le, read_u64_le, read_u8}; -/// One vendor row (`SMSG_LIST_INVENTORY`, vmangos `ItemHandler.cpp:741-810`). `slot` (the wire's -/// `muid`) is the row's 1-based position in the vendor's list — **not** what buying uses; buying -/// addresses the item by [`VendorItem::entry`] (see [`buy_item`]). `current_count` of -/// `0xFFFF_FFFF` means unlimited stock; `price` is already reputation-discounted server-side. +/// One vendor row (`ItemHandler.cpp:741-810`): `slot` is the 1-based list position, not what a buy +/// sends; `current_count` `0xFFFF_FFFF` is unlimited; `price` is already reputation-discounted. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct VendorItem { pub slot: u32, @@ -26,7 +20,7 @@ pub struct VendorItem { pub buy_count: u32, } -/// `BuyResult` (vmangos `ItemDefines.h:120-141`) — the `u8` reason on `SMSG_BUY_FAILED`. +/// `BuyResult` (vmangos `ItemDefines.h:120-141`): the `u8` reason on `SMSG_BUY_FAILED`. pub mod buy_result { pub const CANT_FIND_ITEM: u8 = 0; pub const ITEM_ALREADY_SOLD: u8 = 1; @@ -39,9 +33,7 @@ pub mod buy_result { pub const REPUTATION_REQUIRE: u8 = 12; } -/// `SellResult` (vmangos `ItemDefines.h:120-141`) — the `u8` reason on `SMSG_SELL_ITEM`. A -/// *successful* sell sends no packet at all (the item vanishes + coinage rises via -/// `UPDATE_OBJECT`); only the error path (`SendSellError`, `Player.cpp:11723`) uses this shape. +/// `SellResult` (vmangos `ItemDefines.h:120-141`): the `u8` reason on `SMSG_SELL_ITEM`. pub mod sell_result { pub const CANT_FIND_ITEM: u8 = 1; pub const CANT_SELL_ITEM: u8 = 2; @@ -51,16 +43,13 @@ pub mod sell_result { pub const ONLY_EMPTY_BAG: u8 = 6; } -/// Body of `CMSG_LIST_INVENTORY` (vmangos `Item.cpp:94`): one full 8-byte vendor guid. The server -/// gates on `UNIT_NPC_FLAG_VENDOR` + the player being alive (`ItemHandler.cpp:693`). +/// Body of `CMSG_LIST_INVENTORY`: the vendor guid; the dead get no list (`ItemHandler.cpp:693`). pub fn list_inventory(vendor_guid: u64) -> Vec { vendor_guid.to_le_bytes().to_vec() } -/// Body of `CMSG_BUY_ITEM` (vmangos `Item.cpp:104-110`): `u64 vendorGuid, u32 item` (the template -/// **entry**, not the vendor row's `muid`), `u8 count` (stacks to buy), `u8 unk1` (always 0). -/// Auto-places into the first free bag slot — the specific-bag-slot variant is -/// [`buy_item_in_slot`]. Handler → `BuyItemFromVendor` (`ItemHandler.cpp:688`). +/// Body of `CMSG_BUY_ITEM` (vmangos `Item.cpp:104-110`): `u64 vendorGuid`, `u32` template entry +/// (not the row's `muid`), `u8 count` (stacks), `u8 unk1` (0); it fills the first free slot. pub fn buy_item(vendor_guid: u64, entry: u32, count: u8) -> Vec { let mut body = Vec::with_capacity(14); body.extend_from_slice(&vendor_guid.to_le_bytes()); @@ -70,18 +59,9 @@ pub fn buy_item(vendor_guid: u64, entry: u32, count: u8) -> Vec { body } -/// Body of `CMSG_BUY_ITEM_IN_SLOT` (vmangos `Item.cpp:113-120`): `u64 vendorGuid, u32 item` -/// (the template **entry**, like [`buy_item`]), `u64 bagGuid`, `u8 bagSlot`, `u8 count`. -/// -/// The buy-into-a-named-slot variant, and the packet the **merchant cursor** sends: the reference -/// puts a vendor row on the cursor as payload mode 5 (`PickupMerchantItem 0x4fb760`) and the -/// three container drop handlers each call the sender `0x5e1f30` with `count = 1` -/// (wow-re `system/ui/scratch/merchant-cursor-law.md` §5). Clicking a row is the auto-place -/// [`buy_item`]; dragging it into a slot is this. -/// -/// `bag_guid` is the **container object's** guid, or the player's own for the backpack and the -/// equipment slots; `bag_slot` is the destination within it. Handler → `BuyItemFromVendor`, the -/// same one `CMSG_BUY_ITEM` reaches, with the slot pair filled instead of `NULL_BAG`/`NULL_SLOT`. +/// Body of `CMSG_BUY_ITEM_IN_SLOT` (vmangos `Item.cpp:113-120`): `u64 vendorGuid`, `u32` template +/// entry, `u64 bagGuid` (ours for the backpack), `u8 bagSlot`, `u8 count`; the reference sends it +/// with `count = 1` for a vendor row dropped into a slot (`0x5e1f30`). pub fn buy_item_in_slot( vendor_guid: u64, entry: u32, @@ -98,8 +78,8 @@ pub fn buy_item_in_slot( body } -/// Body of `CMSG_SELL_ITEM` (vmangos `Item.cpp:87-92`): `u64 vendorGuid, u64 itemGuid, u8 count` -/// (0 = sell the whole stack). Handler `ItemHandler.cpp:442`. +/// Body of `CMSG_SELL_ITEM` (vmangos `Item.cpp:87-92`): `u64 vendorGuid, u64 itemGuid, u8 count`, +/// where 0 sells the whole stack. pub fn sell_item(vendor_guid: u64, item_guid: u64, count: u8) -> Vec { let mut body = Vec::with_capacity(17); body.extend_from_slice(&vendor_guid.to_le_bytes()); @@ -108,10 +88,8 @@ pub fn sell_item(vendor_guid: u64, item_guid: u64, count: u8) -> Vec { body } -/// Body of `CMSG_BUYBACK_ITEM` (VERIFIED wow-re ui/scratch/buyback-data-path.md, `0x4fb950`: -/// `u64 vendorGuid, u32 slot` — the slot is the **absolute** player-array buyback slot 69–80 -/// (`BUYBACK_SLOT_START` + index), sent raw with no re-base; vmangos `HandleBuybackItem` reads it -/// the same way). +/// Body of `CMSG_BUYBACK_ITEM` (`0x4fb950`): `u64 vendorGuid, u32 slot`, the absolute buyback +/// slot 69-80 (`BUYBACK_SLOT_START` + index), not re-based. pub fn buyback_item(vendor_guid: u64, slot: u32) -> Vec { let mut body = Vec::with_capacity(12); body.extend_from_slice(&vendor_guid.to_le_bytes()); @@ -119,9 +97,7 @@ pub fn buyback_item(vendor_guid: u64, slot: u32) -> Vec { body } -/// Body of `CMSG_REPAIR_ITEM` (VERIFIED wow-re ui/scratch/repair-machinery.md, all 4 client send -/// sites: `u64 vendorGuid, u64 itemGuid`; `itemGuid == 0` = repair everything. vmangos -/// `HandleRepairItemOpcode` corroborates). +/// Body of `CMSG_REPAIR_ITEM`: `u64 vendorGuid, u64 itemGuid`; item guid 0 repairs everything. pub fn repair_item(vendor_guid: u64, item_guid: u64) -> Vec { let mut body = Vec::with_capacity(16); body.extend_from_slice(&vendor_guid.to_le_bytes()); @@ -130,12 +106,8 @@ pub fn repair_item(vendor_guid: u64, item_guid: u64) -> Vec { } /// Read `SMSG_LIST_INVENTORY` (vmangos `ItemHandler.cpp:741-810`): `u64 vendorGuid, u8 count` -/// (≤ `MAX_VENDOR_ITEMS` 128) then `count` rows of [`VendorItem`]. The empty-vendor case is -/// `count = 0` followed by one trailing `u8` — both producers write a literal `uint8(0)` -/// ("Vendor has no inventory": `ItemHandler.cpp:728-733` for a creature with no item lists, -/// `:806-809` for one whose every item is hidden from this player). It carries no other value -/// on this wire, so it is consumed and dropped — consumed *explicitly*, so an empty list leaves -/// no tail for the decode-length check to report (decision 2265 §B1). +/// (at most 128), then `count` rows. An empty list is followed by one `u8` 0 +/// (`ItemHandler.cpp:728-733`, `:806-809`), read here so no tail is left over. pub(super) fn read_list_inventory(r: &mut &[u8]) -> io::Result<(u64, Vec)> { let vendor_guid = read_u64_le(r)?; let count = read_u8(r)?; @@ -159,11 +131,8 @@ pub(super) fn read_list_inventory(r: &mut &[u8]) -> io::Result<(u64, Vec io::Result<(u64, u32, u32, u32)> { Ok(( read_u64_le(r)?, @@ -174,8 +143,7 @@ pub(super) fn read_buy_item(r: &mut &[u8]) -> io::Result<(u64, u32, u32, u32)> { } /// Read `SMSG_SELL_ITEM` (vmangos `Item.cpp:183-188`): `u64 vendorGuid, u64 itemGuid, u8 reason` -/// (a [`sell_result`] code). Only the error path sends this packet at all — a successful sell is -/// silent, visible only as the item's removal + coinage rising via `UPDATE_OBJECT`. +/// (a [`sell_result`] code). Only a failed sell sends it; a sale shows only in `UPDATE_OBJECT`. pub(super) fn read_sell_item(r: &mut &[u8]) -> io::Result<(u64, u64, u8)> { Ok((read_u64_le(r)?, read_u64_le(r)?, read_u8(r)?)) } diff --git a/crates/benilla-protocol/src/messages/world_state.rs b/crates/benilla-protocol/src/messages/world_state.rs index b03bf81f..a4e861e7 100644 --- a/crates/benilla-protocol/src/messages/world_state.rs +++ b/crates/benilla-protocol/src/messages/world_state.rs @@ -1,56 +1,28 @@ -//! World states — the server's key/value scoreboard table (`SMSG_INIT_WORLD_STATES` `0x2C2`, -//! `SMSG_UPDATE_WORLD_STATE` `0x2C3`). It is what the NPC-text `$w`/`$e` tokens read, and it -//! is what a battleground scoreboard is built from; benilla uses the first today. +//! World states, the server's key/value table (`SMSG_INIT_WORLD_STATES` `0x2C2`, +//! `SMSG_UPDATE_WORLD_STATE` `0x2C3`) that NPC-text `$w`/`$e` tokens and scoreboards read. //! -//! Both wires are settled from two independent sources that agree: -//! -//! - **The reference binary** (wow-re `system/ui/scratch/questtext-macro-expander.md` §3): the one -//! handler `0x48f690` holds the only two callers of the table setter `0x4c5870`, registered -//! against exactly these two opcodes at `0x48f515-0x48f52f`. `0x2C2` is *two leading dwords* -//! (handed to `0x4c5650`), a `u16` count, then `count × (id:u32, value:u32)`; `0x2C3` is a -//! single such pair. **`0x4c5650` is carved** (2026-08-25, wow-re -//! `system/ui/scratch/worldstate-ui-law.md`): it CLEARS the table, stores the two dwords as the -//! world-state UI's display filter, and rebuilds that list — all before the pair loop runs. See -//! [`InitWorldStates::zone`]. -//! - **vmangos's writers** (`Player::SendInitWorldStates`, `Player.cpp:8219-8293`, and -//! `WorldPackets::Misc::UpdateWorldState::AppendBodyTo`, `Misc.cpp:685-694`): `u32 mapId`, -//! `u32 zoneId` (the zone dword is `#if SUPPORTED_CLIENT_BUILD > CLIENT_BUILD_1_11_2`, so 1.12.1 -//! gets it), the `u16` count back-patched at the end, then the pairs; and `u32 field, u32 value` -//! respectively (also build-gated — a pre-1.8.4 client would read `u16`s). -//! -//! **The count includes a `(0, 0)` terminator.** vmangos starts `count = 1` and appends -//! `uint32(0) << uint32(0)` last, *"to prevent repeating audio bug"* — so the pair run is read -//! verbatim, terminator and all, exactly as the reference's count loop does. There is no -//! terminator special case on either side and we add none: id 0 simply lands in the table as 0. -//! -//! Ids and values are **raw dwords** on the wire and stay raw here. That matters: the reference's -//! `$e` token reads the table at the *negated* key, so a text asking for `$2077e` looks up -//! `0xFFFFF7E3` — a key only reachable if nothing has re-interpreted the field on the way in. +//! On `0x2C2` the reference clears the table and takes map and zone as its UI filter (`0x4c5650`) +//! before the pairs run. vmangos counts a trailing `(0, 0)` pair (`Player.cpp:8219-8293`), which +//! is read as data. Ids and values stay raw dwords: `$e` reads the table at the negated key, so +//! `$2077e` looks up `0xFFFFF7E3`. use std::io; use crate::wire::{read_u16_le, read_u32_le}; -/// `SMSG_INIT_WORLD_STATES` — the whole table for a zone, sent on login and on every zone change -/// (vmangos `Player::UpdateZone` → `SendInitWorldStates`). +/// `SMSG_INIT_WORLD_STATES`: the whole table for a zone, sent on login and every zone change. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct InitWorldStates { - /// The map the states are scoped to — the client's `[0xb71e84]`, and the first half of the - /// filter its world-state UI list is built against (see the module doc). + /// The map the states are scoped to; the reference keeps it at `[0xb71e84]`. pub map: u32, - /// The second leading dword. Named `zone` for the field vmangos fills it from; the client - /// stores it as `[0xb71ea8]` and matches it against **`WorldStateUI.dbc`'s `AreaID`** when it - /// decides which always-up rows a scope admits, so "whatever the server put here" is exactly - /// the comparand either way. + /// The zone vmangos fills in; the reference keeps it at `[0xb71ea8]` and matches it against + /// `WorldStateUI.dbc`'s `AreaID`. pub zone: u32, /// `(id, value)` verbatim, including the trailing `(0, 0)` terminator. pub states: Vec<(u32, u32)>, } /// Read `SMSG_INIT_WORLD_STATES`: `u32 map, u32 zone, u16 count`, then `count` `(id, value)` pairs. -/// -/// The count is trusted only as far as the body allows — a truncated run stops at the short read -/// like every other reader here, rather than pre-allocating on a hostile length. pub(in crate::messages) fn read_init_world_states(r: &mut &[u8]) -> io::Result { let map = read_u32_le(r)?; let zone = read_u32_le(r)?; @@ -71,8 +43,6 @@ pub(in crate::messages) fn read_update_world_state(r: &mut &[u8]) -> io::Result< mod tests { use super::*; - /// A vmangos-shaped `SMSG_INIT_WORLD_STATES`: map, zone, a count that *includes* the trailing - /// `(0, 0)` terminator, then the pairs. The terminator is data, not a sentinel we strip. #[test] fn init_reads_every_pair_including_the_terminator() { let mut body = Vec::new(); @@ -89,7 +59,6 @@ mod tests { assert_eq!(got.states, vec![(2264, 7), (2263, 0), (0, 0)]); } - /// A count longer than the body is a short read, not a panic or a giant allocation. #[test] fn init_truncated_run_errors() { let mut body = Vec::new(); @@ -100,8 +69,7 @@ mod tests { assert!(read_init_world_states(&mut body.as_slice()).is_err()); } - /// The update wire is the bare pair, and both halves stay raw dwords — a value with the top bit - /// set survives as-is, so the `%d` render downstream is the only place a sign appears. + /// A top-bit value stays unsigned; only the `%d` render downstream gives it a sign. #[test] fn update_reads_a_raw_dword_pair() { let mut body = Vec::new(); diff --git a/crates/benilla-protocol/src/wire.rs b/crates/benilla-protocol/src/wire.rs index 9023af02..52fe6df9 100644 --- a/crates/benilla-protocol/src/wire.rs +++ b/crates/benilla-protocol/src/wire.rs @@ -1,11 +1,8 @@ -//! Low-level wire read/write helpers shared by the auth ([`crate::auth`]) and world -//! ([`crate::world`]) protocols. Everything is little endian unless noted; these mirror the exact -//! field encodings the 1.12 client uses. +//! Wire read/write helpers shared by the auth and world protocols; little endian unless noted. use std::io::{self, Read, Write}; -/// WoW packs an 8-byte GUID as a one-byte mask (which of the 8 bytes are non-zero) followed by only -/// those non-zero bytes, low to high — used throughout the world protocol for object references. +/// Read a packed GUID: a mask byte naming the non-zero bytes, then only those bytes, low to high. pub fn read_packed_guid(r: &mut impl Read) -> io::Result { let mask = read_u8(r)?; let mut guid = 0u64; @@ -92,19 +89,8 @@ pub fn read_cstring(r: &mut impl Read) -> io::Result { Ok(String::from_utf8_lossy(&bytes).into_owned()) } -/// Bound a wire-derived element count before it becomes a `Vec::with_capacity` hint. -/// -/// A count read off the wire is server- (and so bug- and attacker-) controlled, and -/// `Vec::with_capacity(count as usize)` on a raw `u32` is the one failure the decoders' `io::Result` -/// totality does not cover: an allocation that cannot be satisfied is `handle_alloc_error` → -/// `abort()` — no unwind, no [`crate::Poll::Skipped`], no tally (decision 2265 §B1). The returned -/// hint bounds only the **up-front** allocation: the `Vec` still grows past it when the body really -/// carries more rows, and a lying count still fails on the short read of the body, which is -/// `u16`-bounded and read whole before parse. `cap` is the protocol's own bound where one exists -/// (cited at the call site), or a generous sane one where none does. -/// -/// Every `with_capacity` under `messages/` whose argument is a wire count goes through this; the -/// source-scan test beside it (`wire_count_capacity_hints_are_capped`) keeps that true. +/// Bound a wire-derived count before it becomes a `Vec::with_capacity` hint: a failed allocation +/// aborts rather than erroring. `cap` is the protocol's own bound where one exists. pub fn capacity_hint(count: impl TryInto, cap: usize) -> usize { count .try_into() @@ -137,11 +123,8 @@ impl Vector3d { } } -/// Decode a movement-spline packed point (`SMSG_MONSTER_MOVE` packs every point after the first this -/// way): **signed** two's-complement fields — 11 bits x, 11 bits y, 10 bits z — in quarter-yard -/// units (the server packs `round(v * 4)` per axis; vmangos `ByteBuffer::appendPackXYZ`). The -/// decoded vector is the offset from the spline's **destination** back to this waypoint -/// (`destination - waypoint`; vmangos `PacketBuilder::WriteLinearPath`), not an absolute position. +/// Decode a `SMSG_MONSTER_MOVE` packed point: signed 11-bit x, 11-bit y, 10-bit z in quarter yards +/// (`ByteBuffer::appendPackXYZ`), the offset `destination - waypoint`, not a position. pub fn packed_to_vector3d(p: i32) -> Vector3d { // Sign-extend each field: shift it to the top of the i32, then arithmetic-shift back down. Vector3d { @@ -155,8 +138,7 @@ pub fn packed_to_vector3d(p: i32) -> Vector3d { mod tests { use super::*; - /// The producer's encoder, transcribed from vmangos `ByteBuffer::appendPackXYZ`: - /// `packed |= ((int)lroundf(v * 4.0f) & mask) << shift` with masks 0x7FF/0x7FF/0x3FF. + /// vmangos's encoder, `ByteBuffer::appendPackXYZ`. fn pack_xyz(x: f32, y: f32, z: f32) -> i32 { let mut packed = 0u32; packed |= ((x * 4.0).round() as i32 & 0x7FF) as u32; @@ -167,9 +149,7 @@ mod tests { #[test] fn packed_point_roundtrips_signed_quarter_yards() { - // Field ranges: x/y are 11-bit signed (−256 .. 255.75 yd), z 10-bit signed (−128 .. 127.75 yd). - // Quarter-yard multiples are exact in f32, so the round-trip must be exact — including the - // negative offsets the old unsigned decode destroyed. + // x and y span −256..255.75 yd, z −128..127.75 yd; quarter yards are exact in f32. for &(x, y, z) in &[ (0.0f32, 0.0f32, 0.0f32), (1.25, -2.5, 3.75), @@ -190,14 +170,8 @@ mod tests { assert_eq!(capacity_hint(u64::MAX, 1024), 1024); } - /// **The rule, enforced instead of remembered** (decision 2265 §B1). - /// - /// A `Vec::with_capacity` whose argument is a raw wire count aborts the process — not the - /// packet — the day a server (or a decoder bug) puts a huge number there: an allocation - /// failure never unwinds, so it never becomes `Poll::Skipped`. There is no runtime signal to - /// test for, which is exactly why the check is structural: every `with_capacity(` under - /// `messages/` whose argument is a bare variable (cast or not) must go through - /// [`capacity_hint`] or carry a `.min(`, or be named in [`EXEMPT`] with the reason. + /// Every `with_capacity` under `messages/` whose argument is a bare variable goes through + /// [`capacity_hint`], carries a `.min(`, or is listed in [`EXEMPT`]. #[test] fn wire_count_capacity_hints_are_capped() { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/messages"); @@ -226,15 +200,11 @@ mod tests { ); } - /// `with_capacity` arguments under `messages/` that look like bare variables but are **not** - /// wire counts, each with the reason. Keyed `(path under src/messages/, argument text)`. + /// Bare-variable arguments that are not wire counts, as `(path under src/messages/, argument)`. const EXEMPT: &[(&str, &str)] = &[]; - /// The classifier, pinned on synthetic arguments so a regression in it cannot pass silently - /// as "nothing to flag". #[test] fn the_scan_flags_a_bare_wire_count_and_passes_a_bounded_one() { - // Flagged: a bare variable, with or without the cast, however it is wrapped. for arg in [ "count as usize", "count", @@ -245,7 +215,6 @@ mod tests { ] { assert!(uncapped_wire_count(arg), "should flag `{arg}`"); } - // Passes: literals, length arithmetic, a constant, and the two bounded forms. for arg in [ "12", "name.len() + 1", @@ -259,7 +228,6 @@ mod tests { ] { assert!(!uncapped_wire_count(arg), "should pass `{arg}`"); } - // The extractor: the balanced argument of every call on the line, with its line number. let src = "let a = Vec::with_capacity(count as usize);\nlet b = Vec::with_capacity((n as usize).min(4));\n"; assert_eq!( capacity_arguments(src), @@ -270,9 +238,7 @@ mod tests { ); } - /// Is this `with_capacity` argument a raw wire count? A bare identifier (optionally cast - /// `as usize`, optionally parenthesised, optionally `usize::from(...)`-wrapped) that is not a - /// `const` (all caps) and carries neither `.min(` nor `capacity_hint(`. + /// A lowercase identifier, however cast or wrapped, with no `.min(` or `capacity_hint(`. fn uncapped_wire_count(arg: &str) -> bool { if arg.contains(".min(") || arg.contains("capacity_hint(") { return false; @@ -330,7 +296,6 @@ mod tests { out } - /// Every `.rs` file under `root`, recursively. fn rust_files(root: &std::path::Path) -> Vec { let mut out = Vec::new(); let mut stack = vec![root.to_path_buf()]; diff --git a/crates/benilla-protocol/src/world/mod.rs b/crates/benilla-protocol/src/world/mod.rs index 56db9e81..9ee98433 100644 --- a/crates/benilla-protocol/src/world/mod.rs +++ b/crates/benilla-protocol/src/world/mod.rs @@ -1,18 +1,8 @@ -//! Phase 4: world server (`mangosd`) connection. +//! The world server (`mangosd`) connection, opened with the realm logon's SRP6 session key. //! -//! After the realmd logon ([`crate::logon`]) hands us the SRP6 session key, we connect to the world -//! server, prove we know that key, then stream object updates. The handshake: -//! -//! 1. Read `SMSG_AUTH_CHALLENGE` (unencrypted) → server seed. -//! 2. Send `CMSG_AUTH_SESSION` (unencrypted): build, uppercased account name, a client seed, and a -//! SHA1 digest binding the session key to both seeds (computed by `benilla-srp`). -//! 3. **1.12 header obfuscation turns on right after** `CMSG_AUTH_SESSION` — every subsequent packet -//! header (4-byte server / 6-byte client) is encrypted; bodies stay plaintext. The first encrypted -//! packet we read is `SMSG_AUTH_RESPONSE`. -//! 4. `CMSG_CHAR_ENUM` → `SMSG_CHAR_ENUM`, then `CMSG_PLAYER_LOGIN(guid)`, then the world streams -//! `SMSG_UPDATE_OBJECT`. -//! -//! Headers are (de)crypted by [`benilla_srp::vanilla_header`]; bodies by [`crate::messages`]. +//! `SMSG_AUTH_CHALLENGE` (server seed) and `CMSG_AUTH_SESSION` (build, uppercased account, client +//! seed, SHA1 over key and seeds) travel plain; every header after them is encrypted (4-byte +//! server, 6-byte client), bodies never, starting with `SMSG_AUTH_RESPONSE`. use std::io::{Read, Write}; use std::net::TcpStream; @@ -31,14 +21,10 @@ pub use reader::WorldReader; pub use session::{WardenRequired, WorldAuthReject, WorldSession}; pub use writer::WorldWriter; -/// Default `mangosd` world-server port — the stock one, which our vmangos deploy maps straight -/// through. Normally the realm list reply carries the port (the deploy's -/// `VMANGOS_REALMLIST_PORT=8085` matches); this constant is the fallback for probes/examples that -/// dial the world server directly. +/// The stock `mangosd` port, for probes that dial the world server without a realm list. pub const WORLD_PORT: u16 = 8085; -/// Read one server packet from `stream`: decrypt the 4-byte header, read the body, parse by opcode. -/// `decrypter` is `None` for the (single) unencrypted `SMSG_AUTH_CHALLENGE`. +/// Read, decrypt and parse one packet; `decrypter` is `None` only for `SMSG_AUTH_CHALLENGE`. pub(super) fn recv_packet( stream: &mut TcpStream, decrypter: Option<&mut DecrypterHalf>, @@ -61,8 +47,7 @@ pub(super) fn recv_packet( messages::parse_server(opcode, &body).map_err(|e| anyhow!("parsing opcode {opcode:#x}: {e}")) } -/// Write one client packet: an (optionally encrypted) 6-byte header + plaintext body. The header size -/// field counts the 4-byte opcode plus the body, but not the size field itself. +/// Write one client packet: a 6-byte header, its size counting opcode and body, then the body. pub(super) fn send_packet( stream: &mut TcpStream, encrypter: Option<&mut EncrypterHalf>, @@ -79,12 +64,7 @@ pub(super) fn send_packet( if let Some(e) = encrypter { e.encrypt(&mut header); } - // **One write, not two** (decision 0617). Header-then-body was a self-inflicted Nagle stall: the - // 6-byte header goes out as its own segment, and the body — small, and now behind unacknowledged - // data — is held by the kernel until the server ACKs it. With Nagle now off (see - // [`WorldSession::connect`]) that stall is gone either way, but two segments per packet is still - // two syscalls and two headers for a ≤40-byte message, and the server cannot parse the packet - // until both land. Joined here so one movement report is one segment. + // One write, not two: header and body leave as one segment. let mut packet = Vec::with_capacity(header.len() + body.len()); packet.extend_from_slice(&header); packet.extend_from_slice(body); diff --git a/crates/benilla-protocol/src/world/movement.rs b/crates/benilla-protocol/src/world/movement.rs index b708209f..a16d4a3a 100644 --- a/crates/benilla-protocol/src/world/movement.rs +++ b/crates/benilla-protocol/src/world/movement.rs @@ -1,18 +1,10 @@ use crate::messages::MovementInfo; use crate::wire::Vector3d; -/// `MovementFlags::FORWARD` — the only movement flag benilla sets. pub(super) const MOVEMENT_FLAG_FORWARD: u32 = 0x1; -/// A monotonic **client-uptime** millisecond stamp for the `MovementInfo` time field — the kind the real -/// 1.12 client sends (`GetTickCount()`, ms since the client started), **not** wall-clock. vmangos reads it -/// into `MovementInfo::ctime` and uses it only two ways (VERIFIED, decision 0057): a non-zero guard -/// (`ctime == 0` reads as "not a client packet" and *pauses* its movement extrapolation) and *deltas* -/// (`getMSTimeDiff`, so the epoch cancels). It never compares the value to its own clock, and it -/// overwrites the field with its server clock before relaying to nearby players — so this value never -/// reaches observers. Hence: match the real client's clock kind, wrap cleanly as a full `u32` (~49.7 days, -/// which `getMSTimeDiff` handles), and keep it non-zero. (The old value was wall-clock masked to 31 bits, -/// which folded backwards every ~24.85 days — a delta glitch the clean `u32` wrap avoids.) +/// Ms since start for the `MovementInfo` time, as the 1.12 client's `GetTickCount()`, never 0: +/// vmangos pauses extrapolation on 0 and otherwise uses only deltas, so a full `u32` wrap is safe. pub(super) fn client_uptime_ms() -> u32 { use std::sync::OnceLock; use std::time::Instant; @@ -20,7 +12,7 @@ pub(super) fn client_uptime_ms() -> u32 { (START.get_or_init(Instant::now).elapsed().as_millis() as u32).max(1) } -/// Build a `MovementInfo` stamped with the client-uptime time ([`client_uptime_ms`]). +/// Build a `MovementInfo` stamped with [`client_uptime_ms`]. pub(super) fn movement_info(pos: [f32; 3], orientation: f32, flags: u32) -> MovementInfo { MovementInfo { flags, @@ -31,11 +23,9 @@ pub(super) fn movement_info(pos: [f32; 3], orientation: f32, flags: u32) -> Move z: pos[2], }, orientation, - // benilla never sets ON_TRANSPORT outbound yet (boarding/riding is decision 0438 phase 2) — the - // writer only emits the transport tail while the flag is set, so this is always dropped. + // Written only under ON_TRANSPORT, which benilla does not send. transport: None, - // Level swim (0) by default; the writer only emits the pitch tail while SWIMMING is set, and the - // controller supplies the live swim pitch through `send_movement` when it does. + // Written only under SWIMMING; `send_movement` supplies the live pitch then. pitch: 0.0, fall_time: 0, jump: None, @@ -48,9 +38,6 @@ mod tests { #[test] fn client_uptime_ms_is_nonzero_and_monotonic() { - // The load-bearing property: vmangos treats `ctime == 0` as "not a client packet" and pauses its - // extrapolation, so our stamp must never be zero (the bug the old wall-clock value worked around). - // It must also be non-decreasing so `getMSTimeDiff` deltas stay sane. let a = client_uptime_ms(); let b = client_uptime_ms(); assert!(a >= 1, "stamp is non-zero: {a}"); diff --git a/crates/benilla-protocol/src/world/reader.rs b/crates/benilla-protocol/src/world/reader.rs index 480f574f..35e0b716 100644 --- a/crates/benilla-protocol/src/world/reader.rs +++ b/crates/benilla-protocol/src/world/reader.rs @@ -8,8 +8,7 @@ use crate::messages::{self, ServerPacket}; use super::recv_packet; -/// Read half of a split [`WorldSession`](super::WorldSession) — owns a cloned socket + the decrypter. Lives on the network -/// thread, streaming decoded [`crate::SessionEvent`]s (via [`Self::poll`]). +/// The read half of a split [`WorldSession`](super::WorldSession): cloned socket and decrypter. pub struct WorldReader { pub(super) stream: TcpStream, pub(super) decrypter: DecrypterHalf, @@ -21,13 +20,8 @@ impl WorldReader { recv_packet(&mut self.stream, Some(&mut self.decrypter)) } - /// Read one packet and decode it into a [`crate::Poll`]: the typed [`crate::SessionEvent`]s it - /// produced, or [`crate::Poll::Skipped`] for an unparseable one. Errors only on a real socket - /// failure (disconnect). The running world model lives in the ECS — the reader is stateless, - /// turning bytes into events and nothing more. - /// - /// `recv_packet` reads the whole body into a buffer before parsing, so a parse error leaves the - /// stream aligned — we skip that packet rather than tear down the session. + /// Read one packet and decode it into a [`crate::Poll`]; errors only when the socket fails. The + /// whole body is read before parsing, so an unparseable packet is skipped, the stream aligned. pub fn poll(&mut self) -> Result { let mut header = [0u8; 4]; if let Err(e) = self.stream.read_exact(&mut header) { @@ -47,9 +41,6 @@ impl WorldReader { events: crate::decode(packet), tail, }), - // Include the raw body (capped) so an unparseable packet can be decoded by hand — a parse - // bug is otherwise invisible past "failed to fill whole buffer". The opcode rides - // separately so the net thread can feed the app's dropped-packet tally. Err(e) => Ok(crate::Poll::Skipped { opcode, reason: format!( @@ -62,8 +53,7 @@ impl WorldReader { } } -/// Space-separated hex of the first `max` bytes of `body` (with a `…` when truncated) — the diagnostic -/// tail on a [`crate::Poll::Skipped`] reason so an unparseable packet's layout can be decoded by hand. +/// Hex of the first `max` bytes of `body`, `…` when truncated, for decoding a packet by hand. fn hex_preview(body: &[u8], max: usize) -> String { use std::fmt::Write; let mut s = String::new(); diff --git a/crates/benilla-protocol/src/world/session.rs b/crates/benilla-protocol/src/world/session.rs index b05b6717..9a8e529d 100644 --- a/crates/benilla-protocol/src/world/session.rs +++ b/crates/benilla-protocol/src/world/session.rs @@ -11,45 +11,21 @@ use super::reader::WorldReader; use super::writer::WorldWriter; use super::{recv_packet, send_packet}; -/// Socket read timeout for the handshake phase only (connect → `player_login`), where each step -/// waits for one specific server reply and silence means the session is dead (decision 0065). -/// Generous — a local vmangos answers in milliseconds; this only bounds the pathological stall. +/// Read timeout through `player_login`, where each step awaits one reply. const HANDSHAKE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); -/// The read bound while **queued** — a different question from the handshake's. -/// -/// [`HANDSHAKE_READ_TIMEOUT`] is 10 s because every handshake step should answer promptly. Sitting -/// in a login queue is the one step that should not: the server speaks when our place moves, which -/// on a full realm can be minutes apart. Keeping the handshake bound would abort every queue that -/// mattered. The real client bounds this not at all (it is event-driven and simply waits), but an -/// unbounded blocking read here would make a server that dies mid-queue indistinguishable from a -/// long wait, so the wait is generous rather than infinite. +/// Read timeout in the login queue, where updates can be minutes apart. Deviation: the reference +/// waits forever; a bound tells a server that died mid-queue from a long wait. const QUEUE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300); -/// The server requires Warden, which benilla does not implement — raised by -/// [`WorldSession::connect`] and recoverable through the `anyhow` chain with -/// `err.downcast_ref::()`, so the login screen can say so plainly. -/// -/// VERIFIED (vmangos `src/game/Anticheat/WardenAnticheat/Warden.cpp`): every Warden request arms a -/// response clock (`BeginTimeoutClock`, `Warden.ClientResponseDelay` — default 30 s in -/// `World.cpp`), and `Warden::Update` kicks unconditionally when it expires — not subject to the -/// penalty config, and regardless of module/maiev mode. So a client that cannot answer cannot stay -/// in the world: refusing at the handshake is the honest outcome, not a policy choice. +/// The server requires Warden. Deviation: benilla does not implement it; vmangos ships with it +/// off (`Warden.WinEnabled`, `Warden.OSXEnabled`). vmangos kicks a client that leaves a Warden +/// request unanswered for 30 s (`Warden.cpp`), so the connect refuses. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct WardenRequired; -/// The **world server** refused the session (`SMSG_AUTH_RESPONSE` with anything but -/// [`messages::AUTH_OK`]). -/// -/// Typed, and carrying its raw code, for the same reason [`crate::AuthReject`] is: the screen owes -/// the player the client's own authored words for what happened, and it cannot look them up from a -/// formatted string. Before this the code was interpolated into a `bail!` and thrown away, so every -/// world-side refusal — expired session, server shutting down, already logging in — arrived at the -/// login screen as the same generic "Unable to connect". -/// -/// **Its codes are `messages`' `AUTH_*` block, NOT [`crate::AuthReject`]'s.** See that block for -/// why the distinction is load-bearing: the two enums overlap numerically and mean unrelated -/// things. +/// The world server refused the session. Its code is from `messages`' `AUTH_*` block, not +/// [`crate::AuthReject`]'s, whose numbers overlap with unrelated meanings. #[derive(Debug, Clone, Copy)] pub struct WorldAuthReject { pub code: u8, @@ -82,31 +58,23 @@ impl std::error::Error for WardenRequired {} pub struct WorldSession { stream: TcpStream, crypto: HeaderCrypto, - /// The roster's guid → race, remembered from [`Self::char_enum`] so [`Self::player_login`] - /// can derive the character's faction tongue without the caller's help. + /// Roster guid to race, so [`Self::player_login`] can pick the chat language. roster_races: std::collections::HashMap, - /// The language chat sends speak — the logged-in character's faction tongue - /// ([`messages::faction_language`]), set automatically by [`Self::player_login`]. Load-bearing: - /// vmangos drops any chat whose language the character doesn't know (dot-commands included) — - /// hardcoded Common silently ate every Horde character's chat and commands once. + /// The language chat sends speak, the character's faction tongue: vmangos drops chat, even + /// dot-commands, in a language the character does not know. chat_language: u32, - /// The rested billing minutes the admitting `SMSG_AUTH_RESPONSE` carried, or `0` when its body - /// was too short for the billing group. Read by [`Self::billing_time_rested`]; see decision - /// 1820 for why `0` rather than the client's own uninitialised-global behaviour. + /// The rested billing minutes the admitting `SMSG_AUTH_RESPONSE` carried. Deviation: `0` when + /// the body is too short, where the reference leaves its global as it was; this field lives + /// per connection, with nothing earlier to keep. billing_time_rested: u32, - /// `SMSG_TUTORIAL_FLAGS` if it landed during the login handshake rather than in the world - /// stream (decision 1976) — handed to the world entry the way the billing minutes are. + /// `SMSG_TUTORIAL_FLAGS` when it lands during the handshake rather than in the world stream. tutorial_flags: Option>, - /// `SMSG_ADDON_INFO`'s per-record `status` bytes, in arrival order — `None` until the server - /// answers our addon block, which is the state that keeps the Lua index space empty - /// (decision 2175). Read by [`Self::take_addon_info`]. + /// `SMSG_ADDON_INFO`'s per-record status bytes in order; `None` until the server answers. addon_info: Option>, } impl WorldSession { - /// Connect to the world server and complete the auth handshake, leaving header obfuscation - /// enabled. `username` must be the same account used at logon; `session_key` is what - /// [`crate::logon`] returned. + /// Connect and complete the auth handshake with the logon's account and session key. pub fn connect( addr: impl ToSocketAddrs, username: &str, @@ -115,19 +83,9 @@ impl WorldSession { Self::connect_queued(addr, username, session_key, &mut |_| true) } - /// [`Self::connect`], reporting our place in the **login queue** as it moves. - /// - /// `on_queue` fires once per `AUTH_WAIT_QUEUE` the server sends — `None` when the packet - /// carried no readable position — and the call returns only once we are admitted (`AUTH_OK`), - /// refused, or the socket fails. The queue is a wait, not an outcome, so it is a callback - /// rather than a return value: the screen has to be able to show the position *while* the - /// handshake is still parked here. - /// - /// **Returning `false` abandons the queue** and fails the connect. That is the only way out of - /// a wait that can last minutes: every other stage of the handshake is bounded tightly enough - /// that a cancel is honoured at its next boundary, and a queue is not. Abandoning is checked - /// once per packet rather than per frame — the reference tears the socket down on the next - /// tick, which is finer-grained, but it costs a player at most one server update. + /// [`Self::connect`], calling `on_queue` with each queue position; `false` abandons the queue. + /// Deviation: checked per queue packet where the reference checks each tick, so a cancel can + /// lag one server update. pub fn connect_queued( addr: impl ToSocketAddrs, username: &str, @@ -136,33 +94,20 @@ impl WorldSession { ) -> Result { let mut queued = false; let mut stream = TcpStream::connect(addr).context("connecting to world server")?; - // **Nagle off** (decision 0617). VERIFIED in the reference client: `0x5bca60` calls - // `setsockopt(s, 6 /* IPPROTO_TCP */, 1 /* TCP_NODELAY */, &1, 4)` unconditionally on its game - // socket (WSOCK32 ordinal 21, IAT slot `0x7ff6f8`; a sibling helper at `0x43dda0` toggles the - // same option from a bool). Rust leaves Nagle *on*, which is exactly wrong for this protocol: - // every packet we send is a sub-MSS write on a latency-critical stream, so the kernel holds - // each one until the server ACKs the last — coalescing our movement stream into delayed-ACK- - // sized clumps. The de-jitter chain on the observing client then sizes its buffer to that - // self-inflicted lateness (decision 0615). Fatal on failure: the option cannot fail on a - // healthy connected socket, so an error here means the next handshake write is doomed anyway — - // better one named failure than a session that silently plays at delayed-ACK cadence. + // Nagle off: the reference sets `TCP_NODELAY` on its game socket (`0x5bca60`). stream .set_nodelay(true) .context("disabling Nagle (TCP_NODELAY) on the world socket")?; - // Handshake reads wait for one specific reply each — silence is failure, so bound them - // (decision 0065). A server that stays connected but never answers must not wedge the net - // thread forever. Cleared in `into_split` for the streaming phase (a quiet world is legal). + // `into_split` clears this for the streaming phase, where a quiet world is legal. stream .set_read_timeout(Some(HANDSHAKE_READ_TIMEOUT)) .context("setting handshake read timeout")?; - // 1. SMSG_AUTH_CHALLENGE (unencrypted) carries the server seed. let server_seed = match recv_packet(&mut stream, None)? { ServerPacket::AuthChallenge { server_seed } => server_seed, other => bail!("expected SMSG_AUTH_CHALLENGE, got {}", other.name()), }; - // 2. Bind the session key to (server_seed, our client_seed) → proof + header crypto. let username_n = NormalizedString::new(username).map_err(|e| anyhow!("invalid username: {e}"))?; let seed = ProofSeed::new(); @@ -170,10 +115,8 @@ impl WorldSession { let (client_proof, crypto) = seed.into_client_header_crypto(&username_n, session_key, server_seed); - // 3. CMSG_AUTH_SESSION goes out unencrypted; obfuscation begins immediately after. - // The addon block is the tail of this packet and is NOT optional: cmangos-classic kicks a - // session whose addon size is zero, which is what benilla used to send (B277, decision - // 1497). `STOCK_SECURE_ADDONS` is what a stock 1.12.1 install reports. + // Sent plain; header encryption starts right after. The addon block is required (cmangos + // kicks a zero-size one); `STOCK_SECURE_ADDONS` is what a stock 1.12.1 install reports. let body = messages::auth_session( u32::from(crate::CLIENT_BUILD), &username.to_uppercase(), @@ -194,15 +137,7 @@ impl WorldSession { addon_info: None, }; - // 4. Wait for SMSG_AUTH_RESPONSE. Usually the first encrypted packet, but not always first - // on the wire, so skip interleaved packets (as `char_enum` does) rather than demanding - // AUTH_RESPONSE lead. Each read is still bounded by the handshake timeout, so a server that - // never answers still fails per decision 0065. - // - // SMSG_WARDEN_DATA among them ends the connection here: it means the server runs Warden, - // whose response clock kicks us ~30 s later no matter what else we do ([`WardenRequired`]). - // Refusing at the handshake trades an unplayable 30-second kick/reconnect cycle for one - // honest message at the login screen. + // AUTH_RESPONSE is not always first, so others are skipped; Warden data ends the connect. loop { match session.recv()? { ServerPacket::AuthResponse { @@ -210,16 +145,11 @@ impl WorldSession { billing_time_rested, .. } if result == messages::AUTH_OK => { - // The admitting packet is the only one that counts: a queue packet carries the - // group too, but the client re-reads it on every AUTH_RESPONSE and the last one - // wins, which is this one. + // The reference keeps the last AUTH_RESPONSE's billing group. session.billing_time_rested = billing_time_rested.unwrap_or(0); break; } - // **Queued, not refused** — the realm is full and we keep our place in line. Report - // the position and go back to reading; the server re-sends as we move up and ends - // the wait with an `AUTH_OK`. Treating this as an ending (which it was until now) - // meant benilla could not log into a busy server at all. + // Queued, not refused: the server re-sends as we move up and ends with `AUTH_OK`. ServerPacket::AuthResponse { result, queue_position, @@ -244,8 +174,7 @@ impl WorldSession { _ => continue, } } - // Admitted. The rest of the handshake is prompt again, so it gets the prompt bound back — - // leaving the queue's 300 s in place would make a stalled roster step look like a long wait. + // Admitted: the rest of the handshake is prompt again. if queued { session .set_read_timeout(Some(HANDSHAKE_READ_TIMEOUT)) @@ -255,24 +184,17 @@ impl WorldSession { Ok(session) } - /// The account's accumulated **rested billing minutes**, from the `SMSG_AUTH_RESPONSE` that - /// admitted this session — what `GetBillingTimeRested()` reports (decision 1820). - /// - /// Against vmangos this is always `0`: the server hardcodes `uint32(0)` for the field - /// (`World.cpp:331`, and `:380` for the queue packet), guarded on a build newer than 1.7.1, - /// which 1.12 is. It is read from the wire rather than assumed so that a server which does - /// populate it is reported honestly. + /// Rested billing minutes (`GetBillingTimeRested`); always 0 from vmangos (`World.cpp:331`). pub fn billing_time_rested(&self) -> u32 { self.billing_time_rested } - /// The tutorial bank captured during the login handshake, if any (decision 1976). + /// The tutorial bank captured during the login handshake, if any. pub fn take_tutorial_flags(&mut self) -> Option> { self.tutorial_flags.take() } - /// Set a read timeout on the underlying socket (e.g. so a debug read-loop can stop when the - /// world goes quiet). `None` clears it (blocking reads). + /// Set the socket's read timeout; `None` makes reads block. pub fn set_read_timeout(&self, timeout: Option) -> Result<()> { self.stream .set_read_timeout(timeout) @@ -282,20 +204,15 @@ impl WorldSession { /// Read + decrypt + parse one server packet. pub fn recv(&mut self) -> Result { let packet = recv_packet(&mut self.stream, Some(self.crypto.decrypter()))?; - // **Captured here rather than in a loop arm**, because it is not one loop's business: - // `SMSG_ADDON_INFO` lands somewhere between `CMSG_AUTH_SESSION` and the roster, and which - // of the handshake's three read loops sees it is the server's timing, not our contract. - // `recv` is the one place all of them go through (decision 2175). + // `SMSG_ADDON_INFO` can reach any of the handshake's read loops, so it is caught here. if let ServerPacket::AddonInfo { statuses } = &packet { self.addon_info = Some(statuses.clone()); } Ok(packet) } - /// The `SMSG_ADDON_INFO` verdict, taken once — `None` when the server never answered our addon - /// block, which is a real state and not a failure: vmangos only replies when - /// `BuildAddonPacket` accepts the block, and a rejection leaves the session alive and silent - /// (`WorldSocket.cpp:447`). The reference behaves the same way — no reply, no Lua index space. + /// The `SMSG_ADDON_INFO` statuses, taken once; `None` is real, as vmangos stays silent when it + /// rejects the addon block (`WorldSocket.cpp:447`). pub fn take_addon_info(&mut self) -> Option> { self.addon_info.take() } @@ -310,8 +227,7 @@ impl WorldSession { ) } - /// Request the character list and return it (remembering each character's race, so - /// [`Self::player_login`] can pick the right chat tongue). + /// Request and return the character list, remembering each race for the chat language. pub fn char_enum(&mut self) -> Result> { self.send(opcode::CMSG_CHAR_ENUM, &[])?; loop { @@ -320,13 +236,11 @@ impl WorldSession { self.roster_races = characters.iter().map(|c| (c.guid, c.race)).collect(); return Ok(characters); } - // Warden can land either side of SMSG_AUTH_RESPONSE depending on when the server - // arms it, so the roster step refuses it too — same reason as `connect`. + // Warden can arm on either side of SMSG_AUTH_RESPONSE, so this step refuses it too. ServerPacket::Other { opcode: opcode::SMSG_WARDEN_DATA, } => return Err(WardenRequired.into()), - // The tutorial bank, if the server sends it this early (1976): kept for the world - // entry — skipped here it would be lost to the roster loop. + // Kept for the world entry when the server sends it this early. ServerPacket::TutorialFlags(flags) => { self.tutorial_flags = Some(flags.bytes); continue; @@ -337,9 +251,7 @@ impl WorldSession { } } - /// Create a character (the create screen's request, or the create-if-empty starter that unblocks - /// `PLAYER_LOGIN` on a fresh account). Returns the `SMSG_CHAR_CREATE` result byte (`WorldResult`) - /// for the caller to inspect ([`messages::CHAR_CREATE_SUCCESS`], a `CHAR_NAME_*` code, …). + /// Create a character; returns the `SMSG_CHAR_CREATE` result byte. pub fn create_character(&mut self, req: &messages::CharCreateReq) -> Result { self.send(opcode::CMSG_CHAR_CREATE, &messages::char_create(req))?; loop { @@ -350,9 +262,7 @@ impl WorldSession { } } - /// Delete a character (`CMSG_CHAR_DELETE`, full u64 guid — only valid at character select, - /// never while in-world). Returns the `SMSG_CHAR_DELETE` result byte - /// ([`messages::CHAR_DELETE_SUCCESS`] on success) for the caller to inspect. + /// Delete a character at character select; returns the `SMSG_CHAR_DELETE` result byte. pub fn delete_character(&mut self, guid: u64) -> Result { self.send(opcode::CMSG_CHAR_DELETE, &messages::full_guid(guid))?; loop { @@ -363,11 +273,7 @@ impl WorldSession { } } - /// Send `CMSG_PLAYER_LOGIN` to enter the world as the given character. Also adopts the - /// character's faction tongue for every subsequent chat send (session and split writer alike): - /// vmangos drops chat — dot-commands included — spoken in a language the character doesn't - /// know, so the tongue must follow the pick. Falls back to Common when the guid wasn't in the - /// enumerated roster (no [`Self::char_enum`] this connection). + /// Enter the world as `guid`, adopting its faction tongue for chat (Common if not enumerated). pub fn player_login(&mut self, guid: u64) -> Result<()> { self.chat_language = self .roster_races @@ -378,19 +284,17 @@ impl WorldSession { self.send(opcode::CMSG_PLAYER_LOGIN, &messages::full_guid(guid)) } - /// Declare `guid` as the unit this client controls. vmangos drops all `MSG_MOVE_*` until the - /// client is a *confirmed mover*, which this establishes (the real client sends it at login). + /// Declare the unit we move, as the 1.12 client does at login; vmangos drops moves until then. pub fn set_active_mover(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_SET_ACTIVE_MOVER, &messages::full_guid(guid)) } - /// Acknowledge a triggered cinematic as finished (`CMSG_COMPLETE_CINEMATIC`, empty body) — the - /// unsplit twin of [`WorldWriter::complete_cinematic`], for the CLI/probe path. + /// Acknowledge a triggered cinematic as finished (`CMSG_COMPLETE_CINEMATIC`, empty body). pub fn complete_cinematic(&mut self) -> Result<()> { self.send(opcode::CMSG_COMPLETE_CINEMATIC, &[]) } - /// Walk our player forward `info`-style: begin moving (`MSG_MOVE_START_FORWARD`). + /// Start walking forward (`MSG_MOVE_START_FORWARD`). pub fn start_forward(&mut self, pos: [f32; 3], orientation: f32) -> Result<()> { self.send( opcode::MSG_MOVE_START_FORWARD, @@ -414,11 +318,7 @@ impl WorldSession { ) } - /// Answer a `SMSG_FORCE_*_SPEED_CHANGE` at rest — the unsplit twin of - /// [`WorldWriter::force_speed_change_ack`], for the `--speed` probe: echo the mover guid + - /// counter + exact speed with a stationary `MovementInfo`. A wrong shape (packed guid, missing - /// counter, off-by-0.01 speed) is rejected by the server's pending-change matcher — the probe's - /// pass is the server logging no mismatch and the next change arriving with counter+1. + /// Ack a `SMSG_FORCE_*_SPEED_CHANGE` at rest with the full guid, counter and exact speed. pub fn force_speed_ack( &mut self, kind: messages::SpeedKind, @@ -434,11 +334,7 @@ impl WorldSession { ) } - /// Acknowledge a finished server spline (`CMSG_MOVE_SPLINE_DONE`) — the unsplit twin of - /// [`WorldWriter::move_spline_done`], for the `--charge` probe: after the server drives us with a - /// self `SMSG_MONSTER_MOVE`, echo its `spline_id` at the endpoint (at rest) so the server clears - /// its spline-pending state and relocates us. A malformed body throws in the server's parser and - /// drops the session — so a surviving stream is the live proof the wire is right. + /// Ack a finished self `SMSG_MONSTER_MOVE` at its endpoint (`CMSG_MOVE_SPLINE_DONE`). pub fn move_spline_done( &mut self, pos: [f32; 3], @@ -451,14 +347,12 @@ impl WorldSession { ) } - /// Ask a player's name (`CMSG_NAME_QUERY`) — the unsplit twin of [`WorldWriter::name_query`], - /// for the CLI/probe path. The answer arrives in the stream as `SMSG_NAME_QUERY_RESPONSE`. + /// Ask a player's name (`CMSG_NAME_QUERY`), answered by `SMSG_NAME_QUERY_RESPONSE`. pub fn name_query(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_NAME_QUERY, &messages::full_guid(guid)) } - /// Ask a creature template's name (`CMSG_CREATURE_QUERY`) — the unsplit twin of - /// [`WorldWriter::creature_query`]. + /// Ask a creature template's name (`CMSG_CREATURE_QUERY`). pub fn creature_query(&mut self, entry: u32, guid: u64) -> Result<()> { self.send( opcode::CMSG_CREATURE_QUERY, @@ -466,8 +360,7 @@ impl WorldSession { ) } - /// Cast a spell (`CMSG_CAST_SPELL`) — the unsplit twin of [`WorldWriter::cast_spell`], for the - /// CLI/probe path. `target: None` = self/implicit; the verdict arrives as `SMSG_CAST_RESULT`. + /// Cast a spell (`CMSG_CAST_SPELL`), `None` targeting self; answered by `SMSG_CAST_RESULT`. pub fn cast_spell(&mut self, spell_id: u32, target: Option) -> Result<()> { self.send( opcode::CMSG_CAST_SPELL, @@ -475,9 +368,7 @@ impl WorldSession { ) } - /// Cast a spell at a **ground point** (`CMSG_CAST_SPELL` with `TARGET_FLAG_DEST_LOCATION` + - /// WoW world coords) — the unsplit twin of [`WorldWriter::cast_spell_at_dest`], for the - /// `--spells` probe's dest-cast round trip (decision 0792). + /// Cast a spell at a ground point in world coords (`TARGET_FLAG_DEST_LOCATION`). pub fn cast_spell_at_dest(&mut self, spell_id: u32, dest: [f32; 3]) -> Result<()> { self.send( opcode::CMSG_CAST_SPELL, @@ -485,10 +376,7 @@ impl WorldSession { ) } - /// Cast an OPEN_LOCK spell at a **GameObject** (`CMSG_CAST_SPELL`) — the unsplit twin of - /// [`WorldWriter::cast_spell_gameobject`], for a probe's live chest-loot verification (decision - /// 0239): cast e.g. 3365 "Opening" / 2575 "Mining" at a lockable GO and expect - /// `SMSG_LOOT_RESPONSE`. + /// Cast an OPEN_LOCK spell (e.g. 3365 Opening, 2575 Mining) at a GameObject. pub fn cast_spell_gameobject(&mut self, spell_id: u32, go_guid: u64) -> Result<()> { self.send( opcode::CMSG_CAST_SPELL, @@ -496,8 +384,7 @@ impl WorldSession { ) } - /// Ask an item template (`CMSG_ITEM_QUERY_SINGLE`) — the unsplit twin of - /// [`WorldWriter::item_query`]. Answered by `SMSG_ITEM_QUERY_SINGLE_RESPONSE`. + /// Ask an item template (`CMSG_ITEM_QUERY_SINGLE`). pub fn item_query(&mut self, entry: u32, guid: u64) -> Result<()> { self.send( opcode::CMSG_ITEM_QUERY_SINGLE, @@ -505,8 +392,7 @@ impl WorldSession { ) } - /// Use an item by bag position (`CMSG_USE_ITEM`) — the unsplit twin of - /// [`WorldWriter::use_item`], for the probe's live use-item verification. + /// Use an item by bag position (`CMSG_USE_ITEM`). pub fn use_item(&mut self, bag_index: u8, slot: u8, spell_slot: u8) -> Result<()> { self.send( opcode::CMSG_USE_ITEM, @@ -519,8 +405,7 @@ impl WorldSession { ) } - /// Open an item by bag position (`CMSG_OPEN_ITEM`) — the unsplit twin of - /// [`WorldWriter::open_item`], for the probe's live open-item verification. + /// Open an item by bag position (`CMSG_OPEN_ITEM`). pub fn open_item(&mut self, bag_index: u8, slot: u8) -> Result<()> { self.send( opcode::CMSG_OPEN_ITEM, @@ -528,8 +413,7 @@ impl WorldSession { ) } - /// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`) — the unsplit twin of - /// [`WorldWriter::auto_equip_item`], for the probe's live equip verification. + /// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`). pub fn auto_equip_item(&mut self, bag_index: u8, slot: u8) -> Result<()> { self.send( opcode::CMSG_AUTOEQUIP_ITEM, @@ -537,8 +421,7 @@ impl WorldSession { ) } - /// Swap two player-array slots (`CMSG_SWAP_INV_ITEM`) — the unsplit twin of - /// [`WorldWriter::swap_inv_item`], for the probe's live backpack-move verification. + /// Swap two player-array slots (`CMSG_SWAP_INV_ITEM`). pub fn swap_inv_item(&mut self, src_slot: u8, dst_slot: u8) -> Result<()> { self.send( opcode::CMSG_SWAP_INV_ITEM, @@ -546,9 +429,7 @@ impl WorldSession { ) } - /// Ask a vendor's stock (`CMSG_LIST_INVENTORY`) — the unsplit twin of - /// [`WorldWriter::list_inventory`], for the probe's live vendor verification. Answered by - /// `SMSG_LIST_INVENTORY` (a `VendorInventory` event). + /// Ask a vendor's stock (`CMSG_LIST_INVENTORY`), answered by `SMSG_LIST_INVENTORY`. pub fn list_inventory(&mut self, vendor_guid: u64) -> Result<()> { self.send( opcode::CMSG_LIST_INVENTORY, @@ -556,8 +437,7 @@ impl WorldSession { ) } - /// Buy from a vendor (`CMSG_BUY_ITEM`) — the unsplit twin of [`WorldWriter::buy_item`]; `entry` - /// is the item template id, not the vendor row's `muid`. + /// Buy from a vendor (`CMSG_BUY_ITEM`); `entry` is the item template, not the row's `muid`. pub fn buy_item(&mut self, vendor_guid: u64, entry: u32, count: u8) -> Result<()> { self.send( opcode::CMSG_BUY_ITEM, @@ -565,7 +445,7 @@ impl WorldSession { ) } - /// `CMSG_BUY_ITEM_IN_SLOT` — buy into a named container slot, the merchant cursor's drop. + /// Buy into a named container slot (`CMSG_BUY_ITEM_IN_SLOT`), the merchant cursor's drop. pub fn buy_item_in_slot( &mut self, vendor_guid: u64, @@ -580,8 +460,7 @@ impl WorldSession { ) } - /// Sell an item to a vendor (`CMSG_SELL_ITEM`) — the unsplit twin of [`WorldWriter::sell_item`]; - /// `count` 0 = the whole stack. + /// Sell an item to a vendor (`CMSG_SELL_ITEM`); `count` 0 sells the whole stack. pub fn sell_item(&mut self, vendor_guid: u64, item_guid: u64, count: u8) -> Result<()> { self.send( opcode::CMSG_SELL_ITEM, @@ -589,8 +468,7 @@ impl WorldSession { ) } - /// Buy a sold item back (`CMSG_BUYBACK_ITEM`) — the unsplit twin of - /// [`WorldWriter::buyback_item`]; `slot` is the absolute buyback slot 69–80. + /// Buy a sold item back (`CMSG_BUYBACK_ITEM`); `slot` is the absolute buyback slot 69-80. pub fn buyback_item(&mut self, vendor_guid: u64, slot: u32) -> Result<()> { self.send( opcode::CMSG_BUYBACK_ITEM, @@ -598,8 +476,7 @@ impl WorldSession { ) } - /// Repair at a vendor (`CMSG_REPAIR_ITEM`) — the unsplit twin of [`WorldWriter::repair_item`]; - /// `item_guid` 0 = repair everything. + /// Repair at a vendor (`CMSG_REPAIR_ITEM`); `item_guid` 0 repairs everything. pub fn repair_item(&mut self, vendor_guid: u64, item_guid: u64) -> Result<()> { self.send( opcode::CMSG_REPAIR_ITEM, @@ -607,8 +484,7 @@ impl WorldSession { ) } - /// Right-click a pure banker (`CMSG_BANKER_ACTIVATE`) — opens the bank window, answered by - /// `SMSG_SHOW_BANK` (a `ShowBank` event). Decision 0604. + /// Open the bank at a banker (`CMSG_BANKER_ACTIVATE`), answered by `SMSG_SHOW_BANK`. pub fn banker_activate(&mut self, banker_guid: u64) -> Result<()> { self.send( opcode::CMSG_BANKER_ACTIVATE, @@ -616,10 +492,7 @@ impl WorldSession { ) } - /// Buy the next bank bag slot (`CMSG_BUY_BANK_SLOT`) — the server buys slot - /// `purchased_count + 1` and debits the price itself; success is silent (the - /// `PLAYER_BYTES_2` bank-bag-count byte advances via `UPDATE_OBJECT`), failure answers - /// `SMSG_BUY_BANK_SLOT_RESULT` (a `BuyBankSlotResult` event). Decision 0604. + /// Buy the next bank bag slot; success is silent, failure answers `SMSG_BUY_BANK_SLOT_RESULT`. pub fn buy_bank_slot(&mut self, banker_guid: u64) -> Result<()> { self.send( opcode::CMSG_BUY_BANK_SLOT, @@ -627,8 +500,7 @@ impl WorldSession { ) } - /// Deposit a bag item into the bank (`CMSG_AUTOBANK_ITEM`) — moves the item at player-array - /// position `(bag, slot)` into the first free bank slot. Decision 0604. + /// Deposit the item at `(bag, slot)` into the first free bank slot (`CMSG_AUTOBANK_ITEM`). pub fn autobank_item(&mut self, bag: u8, slot: u8) -> Result<()> { self.send( opcode::CMSG_AUTOBANK_ITEM, @@ -636,9 +508,7 @@ impl WorldSession { ) } - /// Auto-move at the bank (`CMSG_AUTOSTORE_BANK_ITEM`) — withdraws `(bag, slot)` to inventory - /// when it names a bank position, else deposits it (vmangos `HandleAutoStoreBankItemOpcode`, - /// `ItemHandler.cpp:971`). Decision 0604. + /// Withdraw a bank position, or deposit any other (`CMSG_AUTOSTORE_BANK_ITEM`). pub fn autostore_bank_item(&mut self, bag: u8, slot: u8) -> Result<()> { self.send( opcode::CMSG_AUTOSTORE_BANK_ITEM, @@ -646,8 +516,7 @@ impl WorldSession { ) } - /// Ask an NPC's questgiver dialog status (`CMSG_QUESTGIVER_STATUS_QUERY`) — answered by - /// `SMSG_QUESTGIVER_STATUS`, the overhead `!`/`?` marker's value. + /// Ask an NPC's overhead `!`/`?` status (`CMSG_QUESTGIVER_STATUS_QUERY`). pub fn questgiver_status_query(&mut self, npc: u64) -> Result<()> { self.send( opcode::CMSG_QUESTGIVER_STATUS_QUERY, @@ -655,8 +524,7 @@ impl WorldSession { ) } - /// Open a questgiver dialog (`CMSG_QUESTGIVER_HELLO`) — the unsplit twin for the probe's live - /// quest verification. The same `SendPreparedGossip` server path as gossip-hello. + /// Open a questgiver dialog (`CMSG_QUESTGIVER_HELLO`), the server's gossip-hello path. pub fn questgiver_hello(&mut self, npc: u64) -> Result<()> { self.send( opcode::CMSG_QUESTGIVER_HELLO, @@ -664,8 +532,7 @@ impl WorldSession { ) } - /// Ask a quest's detail panel (`CMSG_QUESTGIVER_QUERY_QUEST`) — answered by - /// `SMSG_QUESTGIVER_QUEST_DETAILS`. + /// Ask a quest's detail panel (`CMSG_QUESTGIVER_QUERY_QUEST`). pub fn questgiver_query_quest(&mut self, npc: u64, quest: u32) -> Result<()> { self.send( opcode::CMSG_QUESTGIVER_QUERY_QUEST, @@ -673,8 +540,7 @@ impl WorldSession { ) } - /// Accept a quest (`CMSG_QUESTGIVER_ACCEPT_QUEST`) — adds it to the log; the server closes the - /// gossip window. + /// Accept a quest (`CMSG_QUESTGIVER_ACCEPT_QUEST`); the server closes the gossip window. pub fn questgiver_accept_quest(&mut self, npc: u64, quest: u32) -> Result<()> { self.send( opcode::CMSG_QUESTGIVER_ACCEPT_QUEST, @@ -682,8 +548,7 @@ impl WorldSession { ) } - /// Ask for a quest's turn-in progress panel (`CMSG_QUESTGIVER_COMPLETE_QUEST`) — answered by - /// `SMSG_QUESTGIVER_REQUEST_ITEMS` (or OFFER_REWARD when there are no required items). + /// Ask a quest's turn-in panel: `REQUEST_ITEMS`, or `OFFER_REWARD` when nothing is required. pub fn questgiver_complete_quest(&mut self, npc: u64, quest: u32) -> Result<()> { self.send( opcode::CMSG_QUESTGIVER_COMPLETE_QUEST, @@ -691,8 +556,7 @@ impl WorldSession { ) } - /// Advance from progress to the reward panel (`CMSG_QUESTGIVER_REQUEST_REWARD`) — answered by - /// `SMSG_QUESTGIVER_OFFER_REWARD`. + /// Advance to the reward panel (`CMSG_QUESTGIVER_REQUEST_REWARD`). pub fn questgiver_request_reward(&mut self, npc: u64, quest: u32) -> Result<()> { self.send( opcode::CMSG_QUESTGIVER_REQUEST_REWARD, @@ -700,8 +564,7 @@ impl WorldSession { ) } - /// Choose a reward and finish the quest (`CMSG_QUESTGIVER_CHOOSE_REWARD`, `reward` = choice - /// index) — answered by `SMSG_QUESTGIVER_QUEST_COMPLETE` + the XP/money/item grants. + /// Choose reward index `reward` and finish the quest (`CMSG_QUESTGIVER_CHOOSE_REWARD`). pub fn questgiver_choose_reward(&mut self, npc: u64, quest: u32, reward: u32) -> Result<()> { self.send( opcode::CMSG_QUESTGIVER_CHOOSE_REWARD, @@ -709,25 +572,17 @@ impl WorldSession { ) } - /// Ask a quest's full template (`CMSG_QUEST_QUERY`) — the unsplit twin of - /// [`WorldWriter::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). + /// Ask a quest's full template by id alone (`CMSG_QUEST_QUERY`). pub fn quest_query(&mut self, quest_id: u32) -> Result<()> { self.send(opcode::CMSG_QUEST_QUERY, &messages::quest_query(quest_id)) } - /// Ask the server for its wall clock (`CMSG_QUERY_TIME`) — the unsplit twin of - /// [`WorldWriter::query_time`]; answered by `SMSG_QUERY_TIME_RESPONSE`, one `u32` of - /// unix-epoch seconds. That is the epoch a timed quest's deadline is written in, so no - /// countdown can be read off the descriptor without it (decision 1150). + /// Ask the server's unix-seconds clock, the epoch of timed-quest deadlines (`CMSG_QUERY_TIME`). pub fn query_time(&mut self) -> Result<()> { self.send(opcode::CMSG_QUERY_TIME, &messages::query_time()) } - /// Abandon a quest-log slot (`CMSG_QUESTLOG_REMOVE_QUEST`) — the unsplit twin of - /// [`WorldWriter::questlog_remove_quest`]; no ack SMSG, the server clears the - /// `PLAYER_QUEST_LOG` slot fields directly. + /// Abandon a quest-log slot; no reply, the server clears the `PLAYER_QUEST_LOG` fields. pub fn questlog_remove_quest(&mut self, slot: u8) -> Result<()> { self.send( opcode::CMSG_QUESTLOG_REMOVE_QUEST, @@ -735,10 +590,7 @@ impl WorldSession { ) } - /// Send a `/say` line — the unsplit twin of [`WorldWriter::send_chat`]; the probe CLI drives GM - /// dot-commands (`.go xyz …`) through it. Same body rules as the writer: spoken in the - /// character's own tongue (set by [`Self::player_login`]) — the server rejects `Universal` - /// from clients, and rejects a tongue the character doesn't know. + /// Send a `/say` line, GM dot-commands included, in the character's own tongue. pub fn send_chat(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -746,10 +598,7 @@ impl WorldSession { ) } - /// Send a chat line on an arbitrary lane — the generalised twin of [`Self::send_chat`] - /// (which is this with `CHAT_TYPE_SAY`). `target` is the whisper target on - /// `CHAT_TYPE_WHISPER` and the channel name on `CHAT_TYPE_CHANNEL`, `None` elsewhere. Spoken - /// in the character's own tongue, with the same server-side rules. + /// Send a chat line on any lane; `target` is the whisper target or channel name. pub fn send_chat_kind( &mut self, chat_type: u32, @@ -762,8 +611,7 @@ impl WorldSession { ) } - /// Join a channel — the unsplit twin of [`WorldWriter::join_channel`]; `password` is empty for - /// a channel that has none. + /// Join a channel; `password` is empty for a channel that has none. pub fn join_channel(&mut self, name: &str, password: &str) -> Result<()> { self.send( opcode::CMSG_JOIN_CHANNEL, @@ -771,13 +619,12 @@ impl WorldSession { ) } - /// Leave a channel — the unsplit twin of [`WorldWriter::leave_channel`]. + /// Leave a channel. pub fn leave_channel(&mut self, name: &str) -> Result<()> { self.send(opcode::CMSG_LEAVE_CHANNEL, &messages::leave_channel(name)) } - /// Invite a player to our group by name (`CMSG_GROUP_INVITE`); they answer with - /// `SMSG_GROUP_INVITE` and, if they want in, [`Self::group_accept`]. + /// Invite a player to our group by name (`CMSG_GROUP_INVITE`). pub fn group_invite(&mut self, member_name: &str) -> Result<()> { self.send( opcode::CMSG_GROUP_INVITE, @@ -790,23 +637,14 @@ impl WorldSession { self.send(opcode::CMSG_GROUP_ACCEPT, &messages::group_accept()) } - /// Leave/disband our group (`CMSG_GROUP_DISBAND`, empty body) — probes tear the group down so - /// the next run starts ungrouped. + /// Leave or disband our group (`CMSG_GROUP_DISBAND`, empty body). pub fn group_disband(&mut self) -> Result<()> { self.send(opcode::CMSG_GROUP_DISBAND, &messages::group_disband()) } - /// Send an **addon message** — a `CMSG_MESSAGECHAT` on `chat_type`'s lane with - /// [`messages::LANGUAGE_ADDON`] in the language field, which is what makes it addon traffic - /// rather than speech (decision 1029). `target` is the channel name on - /// [`messages::CHAT_TYPE_CHANNEL`] and `None` on the group/guild lanes; `text` is the raw - /// payload (`prefix`, TAB, the addon's own encoding) — this verb does not compose it. - /// - /// Probe-only, and deliberately: benilla runs FrameXML, not third-party addons, so it has - /// nothing to *say* over this lane. It exists so the receive gate can be proved against the - /// live server (`addon_chat_probe`) rather than argued from folk memory of the lane. The - /// server drops the whole message unless `AddonChannel` is on and `chat_type` is one of the - /// lanes `IsLanguageAllowedForChatType` permits (see [`messages::LANGUAGE_ADDON`]). + /// Send an addon message: [`messages::LANGUAGE_ADDON`] as the language, `target` the channel + /// name on [`messages::CHAT_TYPE_CHANNEL`]. vmangos drops it unless `AddonChannel` is on and + /// the lane allows it. Probe-only: benilla runs no third-party addons. pub fn send_addon_message( &mut self, chat_type: u32, @@ -819,8 +657,7 @@ impl WorldSession { ) } - /// Echo a same-map teleport ack — the unsplit twin of [`WorldWriter::teleport_ack`] (without it - /// the server freezes our movement after a `.go`). + /// Echo a same-map teleport ack; without it the server freezes our movement. pub fn teleport_ack(&mut self, guid: u64, counter: u32) -> Result<()> { self.send( opcode::MSG_MOVE_TELEPORT_ACK, @@ -828,19 +665,12 @@ impl WorldSession { ) } - /// Echo a cross-map worldport ack — the unsplit twin of [`WorldWriter::worldport_ack`] - /// (`MSG_MOVE_WORLDPORT_ACK`, empty body). Without it the server never runs - /// `HandleMoveWorldportAckOpcode`, so nothing on the destination map is ever streamed: no self - /// create block, and none of the arrival's own side effects (the `--mount-tele` probe's whole - /// subject — the mount strip a map that forbids mounting performs right there). + /// Ack a cross-map worldport (empty body); without it nothing on the new map is streamed. pub fn worldport_ack(&mut self) -> Result<()> { self.send(opcode::MSG_MOVE_WORLDPORT_ACK, &[]) } - /// **Acknowledge a granted mover mode** — root, water-walk, feather-fall or hover (the ack'd - /// family; decision 0866) — the unsplit twin of [`WorldWriter::move_mode_ack`], for the - /// `--death` probe: the server roots us at death, unroots and grants walk-on-water at release, - /// and applies nothing until we echo the counter (+ our current `pose`) back. + /// Ack a granted mover mode with the counter and current `pose`; nothing applies until then. pub fn move_mode_ack( &mut self, guid: u64, @@ -858,30 +688,22 @@ impl WorldSession { ) } - /// Release the spirit (`CMSG_REPOP_REQUEST`, empty body) — the unsplit twin of - /// [`WorldWriter::repop_request`], for the `--death` probe's live release verification. + /// Release the spirit (`CMSG_REPOP_REQUEST`, empty body). pub fn repop_request(&mut self) -> Result<()> { self.send(opcode::CMSG_REPOP_REQUEST, &[]) } - /// Ask where our corpse is (`MSG_CORPSE_QUERY`, empty request) — the unsplit twin of - /// [`WorldWriter::corpse_query`], for the `--death` probe. Answered by the same opcode (a - /// [`SessionEvent::CorpseQuery`](crate::SessionEvent::CorpseQuery)). + /// Ask where our corpse is (`MSG_CORPSE_QUERY`, empty body), answered on the same opcode. pub fn corpse_query(&mut self) -> Result<()> { self.send(opcode::MSG_CORPSE_QUERY, &[]) } - /// Self-resurrect (`CMSG_SELF_RES`, empty body) — the unsplit twin of - /// [`WorldWriter::self_res`], for the `--self-res` probe. The server casts whatever - /// `PLAYER_SELF_RES_SPELL` holds and zeroes the field. + /// Self-resurrect; the server casts `PLAYER_SELF_RES_SPELL` and zeroes it. pub fn self_res(&mut self) -> Result<()> { self.send(opcode::CMSG_SELF_RES, &[]) } - /// Ask the graveyard's Spirit Healer for the immediate res (`CMSG_SPIRIT_HEALER_ACTIVATE`, - /// full guid) — the unsplit twin of [`WorldWriter::spirit_healer_activate`], for the - /// `--spirit` probe's live durability-loss verification. The server resurrects at 50% health - /// and pushes the 25% durability loss as item values-deltas. + /// Take the Spirit Healer's resurrection: 50% health and a 25% durability loss. pub fn spirit_healer_activate(&mut self, npc: u64) -> Result<()> { self.send( opcode::CMSG_SPIRIT_HEALER_ACTIVATE, @@ -889,36 +711,27 @@ impl WorldSession { ) } - /// Start melee auto-attack (`CMSG_ATTACKSWING`, full guid) — the unsplit twin of - /// [`WorldWriter::attack_swing`]. Echoed as `SMSG_ATTACKSTART`; each completed swing arrives as - /// `SMSG_ATTACKERSTATEUPDATE`. + /// Start melee auto-attack (`CMSG_ATTACKSWING`), echoed as `SMSG_ATTACKSTART`. pub fn attack_swing(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_ATTACKSWING, &messages::attack_swing(guid)) } - /// Set our target (`CMSG_SET_SELECTION`, full guid) — the unsplit twin of - /// [`WorldWriter::set_selection`], for probe fidelity (the client selects before it casts). + /// Set our target (`CMSG_SET_SELECTION`, full guid); the client selects before it casts. pub fn set_selection(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_SET_SELECTION, &messages::full_guid(guid)) } - /// Open a loot window (`CMSG_LOOT`) — the unsplit twin of [`WorldWriter::loot`], for the - /// probe's live loot verification. Answered by `SMSG_LOOT_RESPONSE` (either shape). + /// Open a loot window (`CMSG_LOOT`), answered by `SMSG_LOOT_RESPONSE`. pub fn loot(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_LOOT, &messages::loot(guid)) } - /// Use a world GameObject (`CMSG_GAMEOBJ_USE`) — the unsplit twin of [`WorldWriter::gameobj_use`], - /// for a probe's live GO-interaction verification (decision 0236 phase 3's chest-loot capture). - /// The server answers on the type's own system (`SMSG_LOOT_RESPONSE` for a chest, gossip/quest for - /// a questgiver GO) or not at all. + /// Use a world GameObject; the server answers by type, or not at all. pub fn gameobj_use(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_GAMEOBJ_USE, &messages::gameobj_use(guid)) } - /// Ask for a GameObject template's head (`CMSG_GAMEOBJECT_QUERY`) — the unsplit twin of - /// [`WorldWriter::gameobject_query`], for the CLI/probe path. Answered by - /// `SMSG_GAMEOBJECT_QUERY_RESPONSE`. + /// Ask a GameObject template (`CMSG_GAMEOBJECT_QUERY`). pub fn gameobject_query(&mut self, entry: u32, guid: u64) -> Result<()> { self.send( opcode::CMSG_GAMEOBJECT_QUERY, @@ -926,9 +739,7 @@ impl WorldSession { ) } - /// Take one loot-window row (`CMSG_AUTOSTORE_LOOT_ITEM`) — the unsplit twin of - /// [`WorldWriter::autostore_loot_item`]; `loot_slot` is the wire's 0-based row index from the - /// `SMSG_LOOT_RESPONSE` this answers. + /// Take one loot row; `loot_slot` is the 0-based row of the `SMSG_LOOT_RESPONSE`. pub fn autostore_loot_item(&mut self, loot_slot: u8) -> Result<()> { self.send( opcode::CMSG_AUTOSTORE_LOOT_ITEM, @@ -936,24 +747,19 @@ impl WorldSession { ) } - /// Take the loot's coin pile (`CMSG_LOOT_MONEY`, empty body) — the unsplit twin of - /// [`WorldWriter::loot_money`]. Answered by `SMSG_LOOT_MONEY_NOTIFY` + `SMSG_LOOT_CLEAR_MONEY`. + /// Take the loot's coin (`CMSG_LOOT_MONEY`, empty body). pub fn loot_money(&mut self) -> Result<()> { self.send(opcode::CMSG_LOOT_MONEY, &messages::loot_money()) } - /// Close the loot window (`CMSG_LOOT_RELEASE`) — the unsplit twin of - /// [`WorldWriter::loot_release`]; the server ignores `guid` and releases its own stored loot - /// target. Answered by `SMSG_LOOT_RELEASE_RESPONSE`. + /// Close the loot window; the server ignores `guid` and releases its own loot target. pub fn loot_release(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_LOOT_RELEASE, &messages::loot_release(guid)) } - /// Split into independent read/write halves (separate cloned sockets + crypto halves) so a reader - /// thread can stream updates while another path sends our movement. Call after [`Self::player_login`]. + /// Split into a reader and a writer on cloned sockets, after [`Self::player_login`]. pub fn into_split(self) -> Result<(WorldReader, WorldWriter)> { - // The handshake is over — clear its read timeout (decision 0065): the streaming phase reads - // block indefinitely (a quiet world is legal; connection liveness is the caller's loop). + // A quiet world is legal, so streaming reads block without a timeout. self.stream .set_read_timeout(None) .context("clearing handshake read timeout")?; @@ -976,10 +782,7 @@ impl WorldSession { )) } - /// Cleanly leave the world back to character-select: send `CMSG_LOGOUT_REQUEST` and wait for - /// `SMSG_LOGOUT_COMPLETE` (instant for GM / rested). The server persists the character on logout, - /// so a follow-up [`Self::char_enum`] reflects the saved position. Requires a read timeout (so the - /// poll loop can give up); returns once logged out or `timeout` elapses. + /// Log out to character select, waiting up to `timeout`; needs a socket read timeout. pub fn logout(&mut self, timeout: std::time::Duration) -> Result<()> { self.send(opcode::CMSG_LOGOUT_REQUEST, &[])?; let deadline = std::time::Instant::now() + timeout; @@ -992,7 +795,7 @@ impl WorldSession { denied = reason != messages::LOGOUT_SUCCESS; } Ok(_) => {} - Err(_) => {} // read-timeout tick — keep polling until the deadline + Err(_) => {} // a read-timeout tick; poll until the deadline } } if denied { diff --git a/crates/benilla-protocol/src/world/writer/action_bar.rs b/crates/benilla-protocol/src/world/writer/action_bar.rs index d9dfb6f2..0ce26acc 100644 --- a/crates/benilla-protocol/src/world/writer/action_bar.rs +++ b/crates/benilla-protocol/src/world/writer/action_bar.rs @@ -1,13 +1,5 @@ -//! The action bar's `WorldWriter` send — one verb, mirroring [`crate::messages::action_bar`]. Split -//! out of [`super::spells`] by decision 0640. -//! -//! One verb is the whole family because the bar is **client-authoritative** (decisions 0216 §7 / -//! 0218 §4): the server stores the slots and hands them back at login and never edits them in -//! normal play. So every pickup, place and hop the player makes is exactly one of these — a -//! drag-swap is two sends, never atomic. -//! -//! The second verb is the mirror image of the first: [`WorldWriter::set_actionbar_toggles`] posts a -//! byte the client is **not** allowed to write locally. +//! The action bar sends. Its contents are client-authoritative: the server stores the slots, +//! hands them back at login and never edits them in play. use anyhow::Result; @@ -16,12 +8,8 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Set (or clear, `packed == 0`) one action-bar slot (`CMSG_SET_ACTION_BUTTON`, layout in - /// [`messages::set_action_button`]) — decision 0216 §7/0218 §4: the bar is - /// client-authoritative, so this is the ONLY wire traffic a local pickup/place/hop generates, - /// one send per slot mutation (a drag-swap is two sends, never atomic). No dedicated answer - /// packet — `SMSG_ACTION_BUTTONS` only ever re-arrives on a server-side edit (a GM command, a - /// macro-menu save), never as our own edit's echo. + /// Set one action-bar slot, or clear it with `packed == 0` (`CMSG_SET_ACTION_BUTTON`). A + /// drag-swap is two sends, never atomic; the server never echoes our own edit. pub fn set_action_button(&mut self, button: u8, packed: u32) -> Result<()> { self.send( opcode::CMSG_SET_ACTION_BUTTON, @@ -29,21 +17,9 @@ impl WorldWriter { ) } - /// Post the four extra bars' visibility byte (`CMSG_SET_ACTIONBAR_TOGGLES`, layout in - /// [`messages::set_actionbar_toggles`]) — `PLAYER_FIELD_BYTES` byte 2, VERIFIED wow-re - /// `system/ui/scratch/action-bar-toggles.md`. - /// - /// **The opposite arrangement to [`Self::set_action_button`]**, and the difference is the whole - /// point. The bar's *contents* are client-authoritative; the bar's *visibility* is - /// server-owned: the real client never stores this byte itself (§4.1 — the one `+0x102a` - /// instruction image-wide is a read), so the value only becomes true when the server's - /// `SMSG_UPDATE_OBJECT` echoes it back into the descriptor. There is no answer packet and no - /// field-change notification (§4.2), which is why the reference UI keeps its own optimistic - /// copy in Lua and re-reads the field exactly once, at `PLAYER_ENTERING_WORLD`. - /// - /// Sending while disconnected is a **silent no-op**, not an error, matching the reference: the - /// binding gates nothing and drops the packet at `0x5ab637` (no connection) or `0x5379ab` - /// (connection state ≠ 6), returning zero Lua values either way, so nothing upstream can tell. + /// Post the extra bars' visibility byte, `PLAYER_FIELD_BYTES` byte 2. The server owns it: the + /// reference never writes it locally, it holds once `SMSG_UPDATE_OBJECT` echoes it, and a + /// disconnected send is dropped silently (`0x5ab637`). pub fn set_actionbar_toggles(&mut self, toggles: u8) -> Result<()> { self.send( opcode::CMSG_SET_ACTIONBAR_TOGGLES, diff --git a/crates/benilla-protocol/src/world/writer/area_trigger.rs b/crates/benilla-protocol/src/world/writer/area_trigger.rs index a0bd3212..1edb0128 100644 --- a/crates/benilla-protocol/src/world/writer/area_trigger.rs +++ b/crates/benilla-protocol/src/world/writer/area_trigger.rs @@ -1,4 +1,4 @@ -//! The one area-trigger send. Body in [`crate::messages::area_trigger`], whose scope this mirrors. +//! The area-trigger send. use anyhow::Result; @@ -7,14 +7,8 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Report that we just walked into an `AreaTrigger.dbc` volume (`CMSG_AREATRIGGER`, layout in - /// [`messages::area_trigger`]) — the client's whole part in the trigger system: it owns the - /// geometry and names the id, and the **server** decides what that trigger means (a teleport, - /// a quest's explore objective, the inn's rested state, a battleground's entrance list). - /// - /// There is no dedicated success reply: a teleport arrives as the ordinary - /// `SMSG_TRANSFER_PENDING` + `SMSG_NEW_WORLD` pair or a same-map `MSG_MOVE_TELEPORT_ACK`, a - /// refusal as `SMSG_AREA_TRIGGER_MESSAGE`, and most triggers answer with nothing at all. + /// Report walking into an `AreaTrigger.dbc` volume (`CMSG_AREATRIGGER`). There is no success + /// reply; a refusal is `SMSG_AREA_TRIGGER_MESSAGE`, and most triggers answer nothing. pub fn area_trigger(&mut self, trigger_id: u32) -> Result<()> { self.send( opcode::CMSG_AREATRIGGER, diff --git a/crates/benilla-protocol/src/world/writer/attack.rs b/crates/benilla-protocol/src/world/writer/attack.rs index bb75aa67..64d1cc03 100644 --- a/crates/benilla-protocol/src/world/writer/attack.rs +++ b/crates/benilla-protocol/src/world/writer/attack.rs @@ -1,11 +1,5 @@ -//! The auto-attack family's `WorldWriter` sends — start and stop the melee swing, and stop the -//! ranged auto-repeat. Bodies in [`crate::messages::attack`], whose scope this mirrors (the two -//! stops are bodyless). Split out of [`super::spells`] by decision 0640, when the messages side -//! grew an `attack` family of its own. -//! -//! Melee and ranged sit together because they are two halves of one toggle: the client runs at most -//! one auto-attack, and switching weapons hands off between `CMSG_ATTACKSTOP` and -//! `CMSG_CANCEL_AUTO_REPEAT_SPELL`. +//! The auto-attack sends. The client runs at most one auto-attack, so switching between melee and +//! ranged hands off between `CMSG_ATTACKSTOP` and `CMSG_CANCEL_AUTO_REPEAT_SPELL`. use anyhow::Result; @@ -14,8 +8,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Start melee auto-attack on `guid` (`CMSG_ATTACKSWING`, a full 8-byte guid — vmangos - /// `AttackSwing::ReadFromWorldPacket`). Echoed back as `SMSG_ATTACKSTART`. + /// Start melee auto-attack (`CMSG_ATTACKSWING`, full guid), echoed as `SMSG_ATTACKSTART`. pub fn attack_swing(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_ATTACKSWING, &messages::attack_swing(guid)) } @@ -25,8 +18,7 @@ impl WorldWriter { self.send(opcode::CMSG_ATTACKSTOP, &[]) } - /// Stop our ranged auto-repeat (`CMSG_CANCEL_AUTO_REPEAT_SPELL`, empty body) — the ack every - /// local cancel sends (the client's one send site `0x6ea0c6`, inside the cancel `0x6ea080`). + /// Stop our ranged auto-repeat; the reference sends it on every local cancel (`0x6ea080`). pub fn cancel_auto_repeat(&mut self) -> Result<()> { self.send(opcode::CMSG_CANCEL_AUTO_REPEAT_SPELL, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/auction.rs b/crates/benilla-protocol/src/world/writer/auction.rs index ddbaf276..64e6c6ed 100644 --- a/crates/benilla-protocol/src/world/writer/auction.rs +++ b/crates/benilla-protocol/src/world/writer/auction.rs @@ -1,17 +1,7 @@ -//! The auction house's `WorldWriter` sends (decision 1511 P0): the auctioneer greeting, the three -//! list pages (browse / your bids / your auctions), and the three market verbs — list an item, -//! bid or buy out, cancel. Bodies in [`crate::messages`]'s `auction` builders (layout VERIFIED -//! against vmangos `Handlers/AuctionHouseHandler.cpp` + `Server/Packets/AuctionHouse.cpp`). Split -//! out per `writer/mod.rs`'s rule — the module a body builder lives in names the module its send -//! lives in. -//! -//! Two things every caller here has to know. **The auctioneer guid rides on every send**, not -//! just the hello: the server re-validates the 5 yd interact distance on each one independently -//! (`GetCheckedAuctionHouseForAuctioneer`), so there is no session to hold and a walk-away -//! invalidates the next verb rather than the window. And **several refusals are silent** — a sell -//! with a zero bid or duration, a bid the player cannot afford, a cancel whose 5% cut they cannot -//! pay, and any list request while one is already in flight all get *no packet at all*. A UI that -//! blocks waiting for an ack hangs. +//! The auction house sends. Each carries the auctioneer guid, and the server re-checks the 5 yd +//! range on each (`GetCheckedAuctionHouseForAuctioneer`). Sell, bid and cancel answer +//! `SMSG_AUCTION_COMMAND_RESULT`, but some refusals are silent: a zero bid or duration, an +//! unaffordable bid or cancel cut, and a list request while one is in flight. use anyhow::Result; @@ -20,10 +10,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Greet an auctioneer (`MSG_AUCTION_HELLO`, layout in [`messages::auction_hello`]) — one - /// auctioneer guid. **Two-way opcode**: the reply carries the guid back plus the - /// `AuctionHouse.dbc` house id (an `AuctionHello` event), and it is that reply which opens the - /// window. + /// Greet an auctioneer; the reply, carrying the `AuctionHouse.dbc` id, opens the window. pub fn auction_hello(&mut self, auctioneer: u64) -> Result<()> { self.send( opcode::MSG_AUCTION_HELLO, @@ -31,11 +18,8 @@ impl WorldWriter { ) } - /// Ask a Browse page (`CMSG_AUCTION_LIST_ITEMS`, layout in - /// [`messages::auction_list_items`]) — the search's filters as the server reads them, with - /// [`messages::auction_filter`]'s sentinels for the ones left unset. **No sort rides the - /// wire**: sorting the returned page is entirely ours. `list_from` pages by - /// [`messages::AUCTION_PAGE_SIZE`]. Answered by `SMSG_AUCTION_LIST_RESULT`. + /// Ask a Browse page; unset filters are [`messages::auction_filter`]'s sentinels, no sort rides + /// the wire, and `list_from` pages by [`messages::AUCTION_PAGE_SIZE`]. pub fn auction_list_items( &mut self, auctioneer: u64, @@ -66,9 +50,7 @@ impl WorldWriter { ) } - /// Ask the Auctions tab page (`CMSG_AUCTION_LIST_OWNER_ITEMS`, layout in - /// [`messages::auction_list_owner_items`]) — our own listings. Answered by - /// `SMSG_AUCTION_OWNER_LIST_RESULT`. + /// Ask a page of our own auctions (`CMSG_AUCTION_LIST_OWNER_ITEMS`). pub fn auction_list_owner_items(&mut self, auctioneer: u64, list_from: u32) -> Result<()> { self.send( opcode::CMSG_AUCTION_LIST_OWNER_ITEMS, @@ -76,11 +58,8 @@ impl WorldWriter { ) } - /// Ask the Bid tab page (`CMSG_AUCTION_LIST_BIDDER_ITEMS`, layout in - /// [`messages::auction_list_bidder_items`]). `auction_ids` is a **refresh set**, not a filter: - /// the server prepends a record for each id named and then appends every auction we are the - /// current bidder on. Pass `&[]` for a plain page. Answered by - /// `SMSG_AUCTION_BIDDER_LIST_RESULT`. + /// Ask a Bid tab page (`CMSG_AUCTION_LIST_BIDDER_ITEMS`). `auction_ids` is a refresh set, not + /// a filter: the server lists those first, then every auction we are the current bidder on. pub fn auction_list_bidder_items( &mut self, auctioneer: u64, @@ -93,12 +72,7 @@ impl WorldWriter { ) } - /// List an item for auction (`CMSG_AUCTION_SELL_ITEM`, layout in - /// [`messages::auction_sell_item`]) — the Create Auction pane's Create button. - /// `etime_minutes` must be one of [`messages::auction_duration`]'s three values (120/480/1440); - /// the deposit is charged immediately out of the player's purse. Answered by - /// `SMSG_AUCTION_COMMAND_RESULT` (action [`messages::auction_action::STARTED`]) — or by - /// silence, if the bid or duration is zero or the deposit is unaffordable. + /// List an item; `etime_minutes` is 120, 480 or 1440, and the deposit is charged at once. pub fn auction_sell_item( &mut self, auctioneer: u64, @@ -113,11 +87,7 @@ impl WorldWriter { ) } - /// Bid on — or buy out — an auction (`CMSG_AUCTION_PLACE_BID`, layout in - /// [`messages::auction_place_bid`]). One verb for both: the server reads a `price` at or above - /// a nonzero buyout *as* a buyout; there is no separate opcode and no flag. Answered by - /// `SMSG_AUCTION_COMMAND_RESULT` (action [`messages::auction_action::BID_PLACED`]), or by - /// silence if the player cannot afford the price. + /// Bid on an auction; a `price` at or above a nonzero buyout is the buyout. pub fn auction_place_bid( &mut self, auctioneer: u64, @@ -130,11 +100,7 @@ impl WorldWriter { ) } - /// Cancel one of our own auctions (`CMSG_AUCTION_REMOVE_ITEM`, layout in - /// [`messages::auction_remove_item`]) — the Auctions tab's Cancel button. The deposit is - /// **forfeit**, and if the auction already carries a bid the 5% cut comes out of the seller's - /// pocket (and the cancel is refused *silently* when they cannot pay it). Answered by - /// `SMSG_AUCTION_COMMAND_RESULT` (action [`messages::auction_action::REMOVED`]). + /// Cancel one of our auctions; the deposit is forfeit, and with a bid the seller pays 5%. pub fn auction_remove_item(&mut self, auctioneer: u64, auction_id: u32) -> Result<()> { self.send( opcode::CMSG_AUCTION_REMOVE_ITEM, diff --git a/crates/benilla-protocol/src/world/writer/bank.rs b/crates/benilla-protocol/src/world/writer/bank.rs index 914e046a..7187f95e 100644 --- a/crates/benilla-protocol/src/world/writer/bank.rs +++ b/crates/benilla-protocol/src/world/writer/bank.rs @@ -1,11 +1,5 @@ -//! The bank window's `WorldWriter` sends — open the bank, buy the next bag slot, deposit, -//! withdraw. Bodies in [`crate::messages::bank`], whose scope this mirrors. Split out of -//! `writer/mod.rs` (decision 0636). -//! -//! There is no dedicated withdraw opcode in the sense the names suggest: vmangos routes -//! `CMSG_AUTOBANK_ITEM` / `CMSG_AUTOSTORE_BANK_ITEM` by whether the source `(bag, slot)` *is* a -//! bank position, so it tolerates either direction and the pair is really "move it across the bank -//! boundary, server picks the destination" (decision 0604). +//! The bank sends. vmangos routes `CMSG_AUTOBANK_ITEM` and `CMSG_AUTOSTORE_BANK_ITEM` by whether +//! the source is a bank position, so either moves an item across the bank boundary. use anyhow::Result; @@ -14,8 +8,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Open the bank (`CMSG_BANKER_ACTIVATE`, layout in [`messages::banker_activate`]) — one - /// 8-byte banker guid. Answered by `SMSG_SHOW_BANK` (decision 0604). + /// Open the bank (`CMSG_BANKER_ACTIVATE`, full banker guid), answered by `SMSG_SHOW_BANK`. pub fn banker_activate(&mut self, banker_guid: u64) -> Result<()> { self.send( opcode::CMSG_BANKER_ACTIVATE, @@ -23,9 +16,7 @@ impl WorldWriter { ) } - /// Buy the next bank-bag slot (`CMSG_BUY_BANK_SLOT`, layout in [`messages::buy_bank_slot`]). - /// No packet on success (the PLAYER_BYTES_2 count + coinage deltas are the confirmation); - /// refusal answers `SMSG_BUY_BANK_SLOT_RESULT`. + /// Buy the next bank-bag slot; success is silent, failure answers `SMSG_BUY_BANK_SLOT_RESULT`. pub fn buy_bank_slot(&mut self, banker_guid: u64) -> Result<()> { self.send( opcode::CMSG_BUY_BANK_SLOT, @@ -33,8 +24,7 @@ impl WorldWriter { ) } - /// Deposit an item into the bank (`CMSG_AUTOBANK_ITEM`, layout in - /// [`messages::autobank_item`]): the wire `(bag, slot)` of the source item. + /// Deposit the item at wire `(bag, slot)` into the bank (`CMSG_AUTOBANK_ITEM`). pub fn autobank_item(&mut self, bag: u8, slot: u8) -> Result<()> { self.send( opcode::CMSG_AUTOBANK_ITEM, @@ -42,9 +32,7 @@ impl WorldWriter { ) } - /// Withdraw a bank item into the bags (`CMSG_AUTOSTORE_BANK_ITEM`, layout in - /// [`messages::autostore_bank_item`]): the wire `(bag, slot)` of the bank item (vmangos - /// routes by whether the source is a bank position, so it tolerates either direction). + /// Withdraw the bank item at wire `(bag, slot)` into the bags (`CMSG_AUTOSTORE_BANK_ITEM`). pub fn autostore_bank_item(&mut self, bag: u8, slot: u8) -> Result<()> { self.send( opcode::CMSG_AUTOSTORE_BANK_ITEM, diff --git a/crates/benilla-protocol/src/world/writer/battlefield.rs b/crates/benilla-protocol/src/world/writer/battlefield.rs index c79767ec..6568cdc9 100644 --- a/crates/benilla-protocol/src/world/writer/battlefield.rs +++ b/crates/benilla-protocol/src/world/writer/battlefield.rs @@ -1,4 +1,4 @@ -//! The battleground queue's sends (decision 1963). +//! The battleground queue's sends. use anyhow::Result; @@ -7,8 +7,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Answer a ready battleground (`CMSG_BATTLEFIELD_PORT`): the slot's map id and whether to - /// enter — `AcceptBattlefieldPort(index, accept)`'s packet. + /// Enter or decline a ready battleground by map id, as `AcceptBattlefieldPort` sends. pub fn battlefield_port(&mut self, map_id: u32, accept: bool) -> Result<()> { self.send( opcode::CMSG_BATTLEFIELD_PORT, @@ -16,14 +15,12 @@ impl WorldWriter { ) } - /// Ask for the scoreboard (`MSG_PVP_LOG_DATA`, empty) — `RequestBattlefieldScoreData()`'s - /// packet; the 5000 ms throttle is the caller's (decision 1972). + /// Ask the scoreboard (`RequestBattlefieldScoreData`); the caller throttles it to 5000 ms. pub fn request_battlefield_score_data(&mut self) -> Result<()> { self.send(opcode::MSG_PVP_LOG_DATA, &[]) } - /// Leave the battleground (`CMSG_LEAVE_BATTLEFIELD`): `LeaveBattlefield()`'s packet, sent - /// only once the scoreboard's "ended" byte has arrived (decision 1972). + /// Leave the battleground, sent only once the scoreboard's "ended" byte has arrived. pub fn leave_battlefield(&mut self, map_id: u32) -> Result<()> { self.send( opcode::CMSG_LEAVE_BATTLEFIELD, @@ -31,8 +28,7 @@ impl WorldWriter { ) } - /// Reopen a queued battleground's instance list (`CMSG_BATTLEFIELD_LIST`): - /// `ShowBattlefieldList(index)`'s packet, the queued slot's map (decision 1974). + /// Reopen a queued slot's instance list by map, as `ShowBattlefieldList` sends. pub fn battlefield_list(&mut self, map_id: u32) -> Result<()> { self.send( opcode::CMSG_BATTLEFIELD_LIST, @@ -40,8 +36,7 @@ impl WorldWriter { ) } - /// Join through the battlemaster the list came from (`CMSG_BATTLEMASTER_JOIN`) — - /// `JoinBattlefield`'s packet when the cached guid is non-zero (decision 1974). + /// Join through the list's battlemaster, as `JoinBattlefield` sends for a non-zero guid. pub fn battlemaster_join( &mut self, battlemaster: u64, @@ -55,8 +50,7 @@ impl WorldWriter { ) } - /// Join without a battlemaster (`CMSG_BATTLEFIELD_JOIN`) — `JoinBattlefield`'s packet when - /// the list arrived with a zero guid (decision 1974). + /// Join without a battlemaster, as `JoinBattlefield` sends when the list had a zero guid. pub fn battlefield_join( &mut self, map_id: u32, @@ -69,14 +63,12 @@ impl WorldWriter { ) } - /// Ask for every queue slot's state (`CMSG_BATTLEFIELD_STATUS`, empty) — the reference's - /// world-enter reset sends it once per entry (decision 1974). + /// Ask every queue slot's state; the reference sends it once per world entry. pub fn battlefield_status(&mut self) -> Result<()> { self.send(opcode::CMSG_BATTLEFIELD_STATUS, &[]) } - /// Ask for the teammates' positions (`MSG_BATTLEGROUND_PLAYER_POSITIONS`, empty) — - /// `RequestBattlefieldPositions()`'s packet; the 5000 ms throttle is the caller's (1980). + /// Ask teammates' positions (`RequestBattlefieldPositions`); the caller throttles to 5000 ms. pub fn request_battlefield_positions(&mut self) -> Result<()> { self.send(opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/binder.rs b/crates/benilla-protocol/src/world/writer/binder.rs index c9929256..5f97bb75 100644 --- a/crates/benilla-protocol/src/world/writer/binder.rs +++ b/crates/benilla-protocol/src/world/writer/binder.rs @@ -1,9 +1,5 @@ -//! The innkeeper bind answer — the client's one send in the bind family (decision 1331). -//! -//! There is no "bind me here" verb: the flow starts with the *server's* `SMSG_BINDER_CONFIRM` -//! (raised by selecting the innkeeper's gossip line), and this is the Yes. The guid is the one -//! that arrived in the confirm; vmangos resolves it back to a live innkeeper in interact range -//! (`HandleBinderActivateOpcode`), so it is load-bearing rather than an echo. +//! The innkeeper bind answer: the Yes to the server's `SMSG_BINDER_CONFIRM`, carrying its guid, +//! which vmangos resolves to an innkeeper in range (`HandleBinderActivateOpcode`). use anyhow::Result; @@ -12,9 +8,8 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Accept an innkeeper's bind offer (`CMSG_BINDER_ACTIVATE`) — the `CONFIRM_BINDER` dialog's - /// Accept. The server answers by casting spell 3286 on us, which lands as - /// `SMSG_BINDPOINTUPDATE` + `SMSG_PLAYERBOUND`; declining sends nothing at all. + /// Accept an innkeeper's bind offer (`CMSG_BINDER_ACTIVATE`). The server casts spell 3286 on + /// us, landing as `SMSG_BINDPOINTUPDATE` and `SMSG_PLAYERBOUND`; declining sends nothing. pub fn binder_activate(&mut self, binder_guid: u64) -> Result<()> { self.send( opcode::CMSG_BINDER_ACTIVATE, diff --git a/crates/benilla-protocol/src/world/writer/channel.rs b/crates/benilla-protocol/src/world/writer/channel.rs index dba039d1..357e2329 100644 --- a/crates/benilla-protocol/src/world/writer/channel.rs +++ b/crates/benilla-protocol/src/world/writer/channel.rs @@ -1,8 +1,4 @@ -//! The channel family's `WorldWriter` sends — join/leave/list/moderation (bodies in -//! [`crate::messages`]'s `join_channel`/`leave_channel`/`channel_*` builders, all cstring-only -//! shapes; layout VERIFIED against vmangos `Server/Packets/Channel.h`/`.cpp`). Split out of -//! `writer/mod.rs` (decision 0288 phase 1) — the channel administration commands are one clearly -//! separable concern among the writer's many domains. +//! The channel sends: join, leave, list and moderation, all with cstring-only bodies. use anyhow::Result; @@ -11,12 +7,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Join a channel (`CMSG_JOIN_CHANNEL`, layout in [`messages::join_channel`]): `password` is an - /// empty string for a channel that has none. Zone channels (General/Trade/LocalDefense) are - /// joined by the CLIENT too — in 1.12 the client walks ChatChannels.dbc on login/zone-in and - /// sends this same packet with the composed name ("General - Elwynn Forest"); the server only - /// tracks membership by name (decision 0288 phase 6 drives that walk). Answered by - /// `SMSG_CHANNEL_NOTIFY`'s `YOU_JOINED`/`WRONG_PASSWORD`/`BANNED`/… notice. + /// Join a channel; the 1.12 client joins zone channels this way ("General - Elwynn Forest"). pub fn join_channel(&mut self, name: &str, password: &str) -> Result<()> { self.send( opcode::CMSG_JOIN_CHANNEL, @@ -24,19 +15,17 @@ impl WorldWriter { ) } - /// Leave a channel (`CMSG_LEAVE_CHANNEL`, layout in [`messages::leave_channel`]). + /// Leave a channel (`CMSG_LEAVE_CHANNEL`). pub fn leave_channel(&mut self, name: &str) -> Result<()> { self.send(opcode::CMSG_LEAVE_CHANNEL, &messages::leave_channel(name)) } - /// Ask a channel's member roster (`CMSG_CHANNEL_LIST`, layout in [`messages::channel_list`]), - /// the `/chatlist` / `/list` command. Answered by `SMSG_CHANNEL_LIST`. + /// Ask a channel's members (`CMSG_CHANNEL_LIST`, `/chatlist`), answered by `SMSG_CHANNEL_LIST`. pub fn channel_list(&mut self, name: &str) -> Result<()> { self.send(opcode::CMSG_CHANNEL_LIST, &messages::channel_list(name)) } - /// Set a channel's password (`CMSG_CHANNEL_PASSWORD`, layout in - /// [`messages::channel_password`]) — owner-only server-side. + /// Set a channel's password (`CMSG_CHANNEL_PASSWORD`); owner only. pub fn channel_password(&mut self, name: &str, password: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_PASSWORD, @@ -44,8 +33,7 @@ impl WorldWriter { ) } - /// Transfer channel ownership (`CMSG_CHANNEL_SET_OWNER`, layout in - /// [`messages::channel_set_owner`]) — owner-only server-side. + /// Transfer channel ownership (`CMSG_CHANNEL_SET_OWNER`); owner only. pub fn channel_set_owner(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_SET_OWNER, @@ -53,14 +41,12 @@ impl WorldWriter { ) } - /// Ask who owns a channel (`CMSG_CHANNEL_OWNER`, layout in [`messages::channel_owner`]) — the - /// `/console` "who owns" query. Answered by `SMSG_CHANNEL_NOTIFY`'s `CHANNEL_OWNER` notice. + /// Ask who owns a channel (`CMSG_CHANNEL_OWNER`), answered by a `CHANNEL_OWNER` notify. pub fn channel_owner(&mut self, name: &str) -> Result<()> { self.send(opcode::CMSG_CHANNEL_OWNER, &messages::channel_owner(name)) } - /// Grant moderator (`CMSG_CHANNEL_MODERATOR`, layout in [`messages::channel_moderator`]) — - /// owner-only server-side. + /// Grant moderator (`CMSG_CHANNEL_MODERATOR`); owner only. pub fn channel_moderator(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_MODERATOR, @@ -68,7 +54,7 @@ impl WorldWriter { ) } - /// Revoke moderator (`CMSG_CHANNEL_UNMODERATOR`, layout in [`messages::channel_unmoderator`]). + /// Revoke moderator (`CMSG_CHANNEL_UNMODERATOR`). pub fn channel_unmoderator(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_UNMODERATOR, @@ -76,8 +62,7 @@ impl WorldWriter { ) } - /// Mute a player on the channel (`CMSG_CHANNEL_MUTE`, layout in [`messages::channel_mute`]) — - /// moderator-only server-side. + /// Mute a player on the channel (`CMSG_CHANNEL_MUTE`); moderator only. pub fn channel_mute(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_MUTE, @@ -85,7 +70,7 @@ impl WorldWriter { ) } - /// Unmute a player (`CMSG_CHANNEL_UNMUTE`, layout in [`messages::channel_unmute`]). + /// Unmute a player (`CMSG_CHANNEL_UNMUTE`). pub fn channel_unmute(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_UNMUTE, @@ -93,9 +78,7 @@ impl WorldWriter { ) } - /// Invite a player to a moderated/private channel (`CMSG_CHANNEL_INVITE`, layout in - /// [`messages::channel_invite`]); level-gated server-side - /// (`CONFIG_UINT32_CHANNEL_INVITE_MIN_LEVEL`). + /// Invite a player to a channel, gated on `CONFIG_UINT32_CHANNEL_INVITE_MIN_LEVEL`. pub fn channel_invite(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_INVITE, @@ -103,8 +86,7 @@ impl WorldWriter { ) } - /// Kick a player off the channel (`CMSG_CHANNEL_KICK`, layout in [`messages::channel_kick`]) — - /// moderator-only server-side. + /// Kick a player off the channel (`CMSG_CHANNEL_KICK`); moderator only. pub fn channel_kick(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_KICK, @@ -112,8 +94,7 @@ impl WorldWriter { ) } - /// Ban a player from the channel (`CMSG_CHANNEL_BAN`, layout in [`messages::channel_ban`]) — - /// moderator-only server-side. + /// Ban a player from the channel (`CMSG_CHANNEL_BAN`); moderator only. pub fn channel_ban(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_BAN, @@ -121,7 +102,7 @@ impl WorldWriter { ) } - /// Unban a player (`CMSG_CHANNEL_UNBAN`, layout in [`messages::channel_unban`]). + /// Unban a player (`CMSG_CHANNEL_UNBAN`). pub fn channel_unban(&mut self, name: &str, player: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_UNBAN, @@ -129,8 +110,7 @@ impl WorldWriter { ) } - /// Toggle join/leave announcements (`CMSG_CHANNEL_ANNOUNCEMENTS`, layout in - /// [`messages::channel_announcements`]) — owner/moderator-only server-side. + /// Toggle join/leave announcements (`CMSG_CHANNEL_ANNOUNCEMENTS`); owner or moderator only. pub fn channel_announcements(&mut self, name: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_ANNOUNCEMENTS, @@ -138,8 +118,7 @@ impl WorldWriter { ) } - /// Toggle moderation (speak-restricted-to-moderators) mode (`CMSG_CHANNEL_MODERATE`, layout in - /// [`messages::channel_moderate`]) — owner-only server-side. + /// Toggle moderated mode, where only moderators speak (`CMSG_CHANNEL_MODERATE`); owner only. pub fn channel_moderate(&mut self, name: &str) -> Result<()> { self.send( opcode::CMSG_CHANNEL_MODERATE, diff --git a/crates/benilla-protocol/src/world/writer/chat.rs b/crates/benilla-protocol/src/world/writer/chat.rs index 95e0a128..f0cfcde6 100644 --- a/crates/benilla-protocol/src/world/writer/chat.rs +++ b/crates/benilla-protocol/src/world/writer/chat.rs @@ -1,16 +1,5 @@ -//! The chat frame's `WorldWriter` sends — every `CMSG_MESSAGECHAT` flavour the slash commands -//! reach (say/yell/emote/whisper/party/raid/guild/officer/the two raid-leader forms/the two -//! battleground forms/AFK/DND/channel), the ignore-list notify, and the three non-`MESSAGECHAT` -//! slash commands whose *answer* is a chat line (`/played`, `/random`, the DBC-indexed `/wave`). -//! Bodies in [`crate::messages`]'s `messagechat*`/`played_time`/`random_roll`/`text_emote` -//! builders; the inbound half is [`crate::messages::chat`]. Split out of `writer/mod.rs` -//! (decision 0636), mirroring [`super::channel`], which owns the channel *administration* verbs -//! (join/leave/moderation) — only the channel *send* lives here. -//! -//! One fact governs the whole family: **every send must speak [`WorldWriter::chat_language`]**, the -//! logged-in character's own tongue. vmangos rejects `Universal` from clients and rejects a tongue -//! the character doesn't know, dropping the whole message — which silently ate every Horde -//! character's dot-commands while this was hardcoded Common. +//! The chat sends. Chat speaks [`WorldWriter::chat_language`]: vmangos drops `Universal` from +//! clients and any tongue the character does not know. use anyhow::Result; @@ -19,11 +8,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Send a chat line as `/say`. Used to issue server **dot commands** (`.tele Westfall`, …) — - /// vmangos parses anything beginning with `.` as a GM command on the way in, but only *after* - /// the language gate: the send must speak [`Self::chat_language`], the character's own tongue - /// (**vmangos rejects `Universal` from clients**, and rejects a tongue the character doesn't - /// know — which silently ate every Horde character's commands while this was hardcoded Common). + /// Send a `/say` line; dot-commands go out this way, parsed after the language gate. pub fn send_chat(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -31,8 +16,7 @@ impl WorldWriter { ) } - /// Send a `/yell` line (`CHAT_MSG_YELL`) — same body shape as [`Self::send_chat`], a different - /// wire chat type (VERIFIED vmangos `SharedDefines.h:1199`). + /// Send a `/yell` line (`CHAT_MSG_YELL`, `SharedDefines.h:1199`). pub fn send_yell(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -40,9 +24,7 @@ impl WorldWriter { ) } - /// Send a custom `/emote ` line (`CHAT_MSG_EMOTE`, VERIFIED vmangos - /// `SharedDefines.h:1202`) — distinct from [`Self::text_emote`]'s DBC-indexed `/wave`: the - /// server renders it verbatim as `"PlayerName "`. + /// Send a custom `/emote` line (`CHAT_MSG_EMOTE`), shown verbatim as `"PlayerName "`. pub fn send_emote_chat(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -50,10 +32,7 @@ impl WorldWriter { ) } - /// Send a `/whisper ` line (`CHAT_MSG_WHISPER`): body in - /// [`messages::messagechat_whisper`], the one `CMSG_MESSAGECHAT` shape that carries a name - /// ahead of the message (VERIFIED vmangos `Server/Packets/Chat.cpp:3-12`). A bad `target` - /// answers `SMSG_CHAT_PLAYER_NOT_FOUND`, unmodelled here — silent from the client's own POV. + /// Send a `/whisper` line; the body carries the name before the message (`Chat.cpp:3-12`). pub fn send_whisper(&mut self, target: &str, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -61,10 +40,7 @@ impl WorldWriter { ) } - /// Send a `/p` party line (`CHAT_MSG_PARTY`) — same body shape as [`Self::send_chat`] (VERIFIED - /// vmangos `Handlers/ChatHandler.cpp:472-493`: the server rebroadcasts to the group, no group - /// membership needed on the wire — the server enforces it and silently drops the send if we're - /// not grouped). + /// Send a `/p` party line, dropped silently when ungrouped (`ChatHandler.cpp:472-493`). pub fn send_party(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -72,8 +48,7 @@ impl WorldWriter { ) } - /// Send a `/ra` raid line (`CHAT_MSG_RAID`) — requires a raid group server-side - /// (`Handlers/ChatHandler.cpp:514-536`). + /// Send a `/ra` raid line (`CHAT_MSG_RAID`); needs a raid (`ChatHandler.cpp:514-536`). pub fn send_raid(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -81,8 +56,7 @@ impl WorldWriter { ) } - /// Send a `/g` guild line (`CHAT_MSG_GUILD`) — requires guild membership server-side - /// (`Handlers/ChatHandler.cpp:494-503`). + /// Send a `/g` guild line (`CHAT_MSG_GUILD`); needs a guild (`ChatHandler.cpp:494-503`). pub fn send_guild(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -90,8 +64,7 @@ impl WorldWriter { ) } - /// Send a `/o` guild-officer line (`CHAT_MSG_OFFICER`) — requires guild membership server-side - /// (`Handlers/ChatHandler.cpp:504-513`). + /// Send a `/o` officer line (`CHAT_MSG_OFFICER`); needs a guild (`ChatHandler.cpp:504-513`). pub fn send_officer(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -99,8 +72,7 @@ impl WorldWriter { ) } - /// Send a `/rl` raid-leader line (`CHAT_MSG_RAID_LEADER`) — leader-only server-side - /// (`Handlers/ChatHandler.cpp:538-559`; VERIFIED active for 5875, `> CLIENT_BUILD_1_10_2`). + /// Send a `/rl` raid-leader line, leader only (`ChatHandler.cpp:538-559`). pub fn send_raid_leader(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -108,8 +80,7 @@ impl WorldWriter { ) } - /// Send a `/rw` raid-warning line (`CHAT_MSG_RAID_WARNING`) — leader/assistant-only server-side - /// (`Handlers/ChatHandler.cpp:561-576`). + /// Send a `/rw` raid warning, leader or assistant only (`ChatHandler.cpp:561-576`). pub fn send_raid_warning(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -121,8 +92,7 @@ impl WorldWriter { ) } - /// Send a battleground raid line (`CHAT_MSG_BATTLEGROUND`) — requires a BG group server-side - /// (`Handlers/ChatHandler.cpp:579-593`; VERIFIED active for 5875, `> CLIENT_BUILD_1_11_2`). + /// Send a battleground line; needs a battleground group (`ChatHandler.cpp:579-593`). pub fn send_battleground(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -134,8 +104,7 @@ impl WorldWriter { ) } - /// Send a battleground-leader line (`CHAT_MSG_BATTLEGROUND_LEADER`) — BG-group-leader-only - /// server-side (`Handlers/ChatHandler.cpp:595-609`). + /// Send a battleground-leader line, leader only (`ChatHandler.cpp:595-609`). pub fn send_battleground_leader(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -147,9 +116,7 @@ impl WorldWriter { ) } - /// Toggle AFK (`CHAT_MSG_AFK`, `/afk [message]`) — `message` may be empty (a bare toggle); when - /// non-empty it becomes the AFK auto-reply text (vmangos `Handlers/ChatHandler.cpp:611-630`: - /// `masterPlr->afkMsg`). Setting AFK clears DND server-side (mutually exclusive). + /// Toggle AFK, `message` the auto-reply if any; it clears DND (`ChatHandler.cpp:611-630`). pub fn send_afk(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -157,8 +124,7 @@ impl WorldWriter { ) } - /// Toggle DND (`CHAT_MSG_DND`, `/dnd [message]`) — same shape as [`Self::send_afk`] - /// (`Handlers/ChatHandler.cpp:632-648`); mutually exclusive with AFK server-side. + /// Toggle DND (`CHAT_MSG_DND`), which clears AFK (`ChatHandler.cpp:632-648`). pub fn send_dnd(&mut self, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -166,26 +132,9 @@ impl WorldWriter { ) } - /// **Send an addon broadcast** (`SendAddonMessage`, decision 1235) — the one `CMSG_MESSAGECHAT` - /// in this module that does **not** speak [`Self::chat_language`]. - /// - /// `chat_type` is one of the client's four addon lanes (`CHAT_TYPE_PARTY`/`RAID`/`GUILD`/ - /// `BATTLEGROUND`); `text` is the already-composed `prefix` TAB `message` payload — this verb - /// does not compose it, because the Lua binding does and the tab's position is the only thing - /// the far client's splitter has to go on. - /// - /// The language field carries [`messages::LANGUAGE_ADDON`] (`0xFFFFFFFF`), and that sentinel - /// is the entire difference between addon data and speech: 1.12.1 has no addon opcode. - /// VERIFIED in `WoW.exe` (5875), wow-re `system/ui/scratch/addon-chat-law.md` §5 — the binding - /// `0x49f920` writes opcode `0x95` (`0x49facf`), the chat type (`0x49fad8`), then - /// `or ebx,-0x1` / `push ebx` (`0x49fab9`/`0x49fadd`) into the u32 language write at - /// `0x49fae1`, then the message CString at `0x49faf0`. Corroborated end-to-end against a live - /// vmangos by `examples/addon_chat_probe` (decision 1029): the sentinel and the tab both - /// survive the relay untouched. - /// - /// Server-side the line is exempt from the `KnowsLanguage` gate, from flood control and from - /// `SanitizeChatMessage`, and gated instead by the `AddonChannel` config — see - /// [`messages::LANGUAGE_ADDON`] for the whole treatment. + /// Send an addon message (`SendAddonMessage`). Its language, [`messages::LANGUAGE_ADDON`], is + /// the only mark of addon data (`0x49f920`); vmangos gates it on `AddonChannel` and skips the + /// language gate, flood control and sanitizing. pub fn send_addon_message(&mut self, chat_type: u32, text: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -193,9 +142,7 @@ impl WorldWriter { ) } - /// Send a `/1`-style channel line (`CHAT_MSG_CHANNEL`) — `channel` is the channel **name** - /// (`Handlers/ChatHandler.cpp:255-327`; body in [`messages::messagechat_channel`]). Requires - /// membership; the server silently drops it if we're not on the channel. + /// Send a channel line by name, dropped unless we are on it (`ChatHandler.cpp:255-327`). pub fn send_channel(&mut self, channel: &str, message: &str) -> Result<()> { self.send( opcode::CMSG_MESSAGECHAT, @@ -203,31 +150,22 @@ impl WorldWriter { ) } - /// Tell the server we've added `guid` to our ignore list (`CMSG_CHAT_IGNORED`, a raw 8-byte - /// guid — VERIFIED vmangos `WorldPackets::Misc::ChatIgnored::ReadFromWorldPacket`, - /// `Server/Packets/Misc.cpp:127-130`). The server whispers that player a `CHAT_MSG_IGNORED` - /// self-notice ("So-and-so is now ignoring you") — `Handlers/ChatHandler.cpp:755-763`. + /// Tell the server we ignore `guid`; that player gets a `CHAT_MSG_IGNORED` notice. pub fn chat_ignored(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_CHAT_IGNORED, &messages::full_guid(guid)) } - /// Ask our played time (`CMSG_PLAYED_TIME`, empty body, layout in [`messages::played_time`]) — - /// the `/played` command. Answered by `SMSG_PLAYED_TIME` (total + since-last-level-up seconds). + /// Ask our played time (`CMSG_PLAYED_TIME`), answered by `SMSG_PLAYED_TIME`. pub fn played_time(&mut self) -> Result<()> { self.send(opcode::CMSG_PLAYED_TIME, &messages::played_time()) } - /// Roll `/random [min] [max]` (`MSG_RANDOM_ROLL`, layout in [`messages::random_roll`]): the - /// server validates `min <= max <= 10000` and broadcasts the result (to our group if we're in - /// one, else just to us) as the same opcode's server→client shape — `min, max, roll, guid` - /// (decoded by the codec into a `RandomRoll` event). + /// Roll `/random`; the server checks `min <= max <= 10000` and answers on the same opcode. pub fn random_roll(&mut self, min: u32, max: u32) -> Result<()> { self.send(opcode::MSG_RANDOM_ROLL, &messages::random_roll(min, max)) } - /// Perform a chat emote (`CMSG_TEXT_EMOTE`: EmotesText id + target guid, 0 = untargeted). - /// The server echoes `SMSG_TEXT_EMOTE` to everyone in range **including us**, so our own - /// emote's sound/anim arrive through the same receive path as everyone else's. + /// Perform a text emote (EmotesText id, target or 0), echoed to everyone in range, us included. pub fn text_emote(&mut self, text_id: u32, target: u64) -> Result<()> { self.send( opcode::CMSG_TEXT_EMOTE, diff --git a/crates/benilla-protocol/src/world/writer/death.rs b/crates/benilla-protocol/src/world/writer/death.rs index 993b6e6d..ae6c6b0b 100644 --- a/crates/benilla-protocol/src/world/writer/death.rs +++ b/crates/benilla-protocol/src/world/writer/death.rs @@ -1,13 +1,5 @@ -//! The death/corpse-run family's `WorldWriter` sends — release the spirit, find the corpse, -//! reclaim it, self-resurrect off a soulstone, or take the spirit healer's res; plus the answer -//! to someone else's res offer. -//! Bodies in [`crate::messages`]'s `reclaim_corpse`/`spirit_healer_activate`/`resurrect_response` -//! builders (`repop_request`, `corpse_query` and `self_res` are bodyless). Split out of -//! `writer/mod.rs` (decision 0636), mirroring [`crate::messages::death`]. -//! -//! Every one of these is server-gated on a death state the client only *believes* it is in -//! (ghost/unreleased/delay-elapsed/in-range), so a refusal is normal and is not always a packet — -//! the confirmations arrive as ordinary descriptor deltas. +//! The death and corpse-run sends. Each is server-gated on a death state the client only believes +//! it is in, so refusals are normal and not always a packet; success arrives as descriptor deltas. use anyhow::Result; @@ -16,32 +8,23 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Release the spirit (`CMSG_REPOP_REQUEST`, empty body — decision 0308 slice 1): valid only - /// while dead and unreleased (the server refuses it alive or already-ghost). The server - /// answers with the ghost form (aura 8326 → the ghost flags), the corpse object, unroot, - /// water-walk, `SMSG_CORPSE_RECLAIM_DELAY`, and the graveyard teleport. + /// Release the spirit while dead and unreleased; the server answers with ghost aura 8326, the + /// corpse, `SMSG_CORPSE_RECLAIM_DELAY` and the graveyard teleport. pub fn repop_request(&mut self) -> Result<()> { self.send(opcode::CMSG_REPOP_REQUEST, &[]) } - /// Ask where our corpse is (`MSG_CORPSE_QUERY`, empty request): answered by the same opcode - /// (the [`SessionEvent::CorpseQuery`](crate::SessionEvent::CorpseQuery) feed for the map - /// markers + the corpse-run range gate). + /// Ask where our corpse is (`MSG_CORPSE_QUERY`, empty body), answered on the same opcode. pub fn corpse_query(&mut self) -> Result<()> { self.send(opcode::MSG_CORPSE_QUERY, &[]) } - /// Self-resurrect (`CMSG_SELF_RES`, empty body — decision 1746): the DEATH popup's second - /// button, offered only while `PLAYER_SELF_RES_SPELL` is non-zero. The server casts that - /// spell on us and zeroes the field; like the reclaim, the success is ordinary descriptor - /// deltas (alive, health/mana per the spell) with no answer packet of its own. + /// Self-resurrect while `PLAYER_SELF_RES_SPELL` is set; the server casts it, with no reply. pub fn self_res(&mut self) -> Result<()> { self.send(opcode::CMSG_SELF_RES, &[]) } - /// Reclaim our corpse (`CMSG_RECLAIM_CORPSE` — the RECOVER_CORPSE popup's Accept): the corpse's - /// guid. Server gates: ghost, the reclaim delay elapsed, within 39 yd. Success comes back as - /// ordinary descriptor deltas (alive, ghost flags clear) + the corpse-to-bones swap. + /// Reclaim our corpse as a ghost, past the reclaim delay, within 39 yd. pub fn reclaim_corpse(&mut self, corpse_guid: u64) -> Result<()> { self.send( opcode::CMSG_RECLAIM_CORPSE, @@ -49,9 +32,7 @@ impl WorldWriter { ) } - /// Take the spirit healer's resurrection (`CMSG_SPIRIT_HEALER_ACTIVATE` — the XP_LOSS - /// confirm's final Accept): res at 50%, 25% durability loss, resurrection sickness at - /// level ≥ 11. `npc` is the spirit healer's guid (from `SMSG_SPIRIT_HEALER_CONFIRM`). + /// Take the spirit healer's res: 50% health, 25% durability loss, sickness from level 11. pub fn spirit_healer_activate(&mut self, npc: u64) -> Result<()> { self.send( opcode::CMSG_SPIRIT_HEALER_ACTIVATE, @@ -59,8 +40,7 @@ impl WorldWriter { ) } - /// Answer a resurrection offer (`CMSG_RESURRECT_RESPONSE` — the RESURRECT popup's - /// Accept/Decline): the offerer's guid + the accept byte. + /// Accept or decline a resurrection offer (`CMSG_RESURRECT_RESPONSE`). pub fn resurrect_response(&mut self, caster: u64, accept: bool) -> Result<()> { self.send( opcode::CMSG_RESURRECT_RESPONSE, @@ -68,9 +48,7 @@ impl WorldWriter { ) } - /// Ask a battleground spirit healer for its clock (`CMSG_AREA_SPIRIT_HEALER_QUERY`) — sent - /// when the client adopts a new healer; answered by `SMSG_AREA_SPIRIT_HEALER_TIME` - /// (decision 1963). + /// Ask a new battleground spirit healer's clock, answered by `SMSG_AREA_SPIRIT_HEALER_TIME`. pub fn area_spirit_healer_query(&mut self, healer: u64) -> Result<()> { self.send( opcode::CMSG_AREA_SPIRIT_HEALER_QUERY, @@ -78,8 +56,7 @@ impl WorldWriter { ) } - /// Queue for the healer's next wave (`CMSG_AREA_SPIRIT_HEALER_QUEUE`) — - /// `AcceptAreaSpiritHeal()`'s packet, carrying the cached healer (decision 1963). + /// Queue for the healer's next wave, as `AcceptAreaSpiritHeal` sends. pub fn area_spirit_healer_queue(&mut self, healer: u64) -> Result<()> { self.send( opcode::CMSG_AREA_SPIRIT_HEALER_QUEUE, diff --git a/crates/benilla-protocol/src/world/writer/duel.rs b/crates/benilla-protocol/src/world/writer/duel.rs index f2d652de..2b8bae4f 100644 --- a/crates/benilla-protocol/src/world/writer/duel.rs +++ b/crates/benilla-protocol/src/world/writer/duel.rs @@ -1,10 +1,6 @@ -//! The duel family's `WorldWriter` sends — the only two the client ever makes (decision 0633). -//! -//! There is no "start a duel" opcode: the challenge goes out as an ordinary `CMSG_CAST_SPELL` of -//! the spellbook spell whose `Effect[0]` is `SPELL_EFFECT_DUEL`, which is why nothing here builds -//! one. Both bodies carry the duel-arbiter guid, byte-verified against WoW.exe's own -//! `AcceptDuel 0x4d4830` / `CancelDuel 0x4d48b0` (bodies in [`crate::messages`]'s `duel_*` -//! builders). +//! The duel sends. There is no start-duel opcode: a challenge is a `CMSG_CAST_SPELL` of the spell +//! whose `Effect[0]` is `SPELL_EFFECT_DUEL`. Both bodies carry the arbiter guid (`AcceptDuel` +//! `0x4d4830`, `CancelDuel` `0x4d48b0`). use anyhow::Result; @@ -13,8 +9,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Accept a duel challenge (`CMSG_DUEL_ACCEPTED`) — the popup's Accept, and the challenger's - /// own immediate auto-accept of the request it just triggered. + /// Accept a duel (`CMSG_DUEL_ACCEPTED`); the challenger also auto-accepts its own request. pub fn duel_accepted(&mut self, arbiter: u64) -> Result<()> { self.send( opcode::CMSG_DUEL_ACCEPTED, @@ -22,9 +17,7 @@ impl WorldWriter { ) } - /// Decline or abandon a duel (`CMSG_DUEL_CANCELLED`) — the popup's Decline, a cancel during - /// the countdown, and `/forfeit` once the duel is under way. The server reads the intent from - /// the duel's own state, not from the packet. + /// Decline, cancel or forfeit a duel; the server reads which from the duel's state. pub fn duel_cancelled(&mut self, arbiter: u64) -> Result<()> { self.send( opcode::CMSG_DUEL_CANCELLED, diff --git a/crates/benilla-protocol/src/world/writer/gameobject.rs b/crates/benilla-protocol/src/world/writer/gameobject.rs index 87f33f90..2a483abe 100644 --- a/crates/benilla-protocol/src/world/writer/gameobject.rs +++ b/crates/benilla-protocol/src/world/writer/gameobject.rs @@ -1,11 +1,4 @@ -//! The GameObject family's `WorldWriter` sends — the single player-facing USE verb and the -//! ask-once template head. Bodies in [`crate::messages::gameobject`], whose scope this mirrors. -//! Split out of `writer/mod.rs` (decision 0636). -//! -//! Two verbs is the whole family by design (decision 0236): the client does not know or care what -//! kind of GameObject it just clicked — one `CMSG_GAMEOBJ_USE` goes out and the *server* fans it -//! out by GO type. Everything the client needs to draw the thing and pick its cursor comes from the -//! template query. +//! The GameObject sends; one `CMSG_GAMEOBJ_USE` serves every kind, dispatched by type. use anyhow::Result; @@ -14,20 +7,12 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Use a world GameObject (`CMSG_GAMEOBJ_USE`, layout in [`messages::gameobj_use`]) — the single - /// player-facing verb for any usable GO (decision 0236): a full guid naming the chest/door/quest - /// object/lever. The server fans it out by GO type — a chest answers with `SMSG_LOOT_RESPONSE` - /// (the loot window), a questgiver GO with the gossip/quest packets, a door with a - /// `GAMEOBJECT_STATE` flip via `UPDATE_OBJECT` — or refuses silently (out of range, locked, no - /// quest). There is no dedicated success reply. + /// Use a world GameObject; the server answers by type or refuses silently, with no ack. pub fn gameobj_use(&mut self, guid: u64) -> Result<()> { self.send(opcode::CMSG_GAMEOBJ_USE, &messages::gameobj_use(guid)) } - /// Ask for a GameObject template's type/display/name/`data[24]` head (`CMSG_GAMEOBJECT_QUERY`: - /// entry + guid — the ask-once template lookup, decision 0236). The `entry` is the one embedded - /// in the GameObject's guid bits 24–47 ([`crate::guid::entry`]), the same convention as - /// [`WorldWriter::creature_query`]. Answered by `SMSG_GAMEOBJECT_QUERY_RESPONSE`. + /// Ask a GameObject template; `entry` is the guid's bits 24-47 ([`crate::guid::entry`]). pub fn gameobject_query(&mut self, entry: u32, guid: u64) -> Result<()> { self.send( opcode::CMSG_GAMEOBJECT_QUERY, @@ -35,12 +20,7 @@ impl WorldWriter { ) } - /// Ask for one page of a book (`CMSG_PAGE_TEXT_QUERY`, layout in - /// [`messages::page_text_query`]) — the ask-once page cache a readable reaches when the reader - /// opens on it (decision 1105). `guid` names the asking object (the book item or the - /// `GAMEOBJECT_TYPE_TEXT` world object); the server resolves the page from the id alone. - /// Answered by `SMSG_PAGE_TEXT_QUERY_RESPONSE` — once per page of the whole chain, not just - /// the one asked for. + /// Ask a book page; the server resolves it by id alone and answers each page of the chain. pub fn page_text_query(&mut self, page_id: u32, guid: u64) -> Result<()> { self.send( opcode::CMSG_PAGE_TEXT_QUERY, diff --git a/crates/benilla-protocol/src/world/writer/gm_ticket.rs b/crates/benilla-protocol/src/world/writer/gm_ticket.rs index a012b609..89e93124 100644 --- a/crates/benilla-protocol/src/world/writer/gm_ticket.rs +++ b/crates/benilla-protocol/src/world/writer/gm_ticket.rs @@ -1,19 +1,9 @@ -//! The Help window's five sends — the whole client half of the GM trouble-ticket flow -//! (decision 1673). +//! The GM ticket sends. The client holds no ticket state; the server's answers are the only truth. //! -//! Three of the five have empty bodies, which is the shape of the feature: the client holds no -//! ticket state of its own, so "show me my ticket", "abandon it" and "is the queue up?" are each a -//! bare opcode and the server's answer is the only truth. The two that carry a body — file and -//! edit — are byte-verified against the real client's own senders (`0x5ef740` / `0x5efac0`); see -//! [`crate::messages::gm_ticket`] for why the category is a single byte in both. -//! -//! **Nothing here may be retried on a timeout.** vmangos answers several refusals with silence -//! rather than a response code (queue off, under `GMTickets.MinLevel`, category >= 11 — -//! `GMTicketHandler.cpp:91,106-113`), and it treats more than two `CMSG_GMTICKET_UPDATETEXT` in a -//! single world tick as flooding, whose default sanction is a **kick** -//! (`WorldSession.cpp:1316-1342`, `Antiflood.Sanction = 8`). So a client that re-sent an edit -//! because no answer came would disconnect itself. The Help window asks once per click, exactly as -//! the reference does. +//! Never retry one on a timeout: vmangos refuses several silently (queue off, under +//! `GMTickets.MinLevel`, category >= 11, `GMTicketHandler.cpp:91,106-113`), and more than two +//! `CMSG_GMTICKET_UPDATETEXT` in one world tick is flooding, kicked by default +//! (`WorldSession.cpp:1316-1342`). use anyhow::Result; @@ -22,12 +12,9 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// File a new ticket (`CMSG_GMTICKET_CREATE`) — the Help window's Submit. - /// - /// `category` is a `GMTicketCategory.dbc` id (1..10, the clicked row); `map`/`pos` are where - /// the player is standing, which is what lets a GM `.tele` to the reported spot. Answered by - /// `SMSG_GMTICKET_CREATE` — **or by nothing at all**, on the several vmangos paths that refuse - /// silently (module doc). + /// File a ticket (`CMSG_GMTICKET_CREATE`, sender `0x5ef740`): `category` is a + /// `GMTicketCategory.dbc` id 1-10, `map` and `pos` where the player stands. Answered by + /// `SMSG_GMTICKET_CREATE`, or by silence. pub fn gm_ticket_create( &mut self, category: u8, @@ -41,11 +28,7 @@ impl WorldWriter { ) } - /// Edit the open ticket's text (`CMSG_GMTICKET_UPDATETEXT`) — the Help window's "Save Changes". - /// - /// Carries the category byte the client carries even though vmangos reads and discards it; the - /// category of an existing ticket cannot actually be changed this way. Answered by - /// `SMSG_GMTICKET_UPDATETEXT`. **Rate-limited server-side** — see the module doc. + /// Edit the open ticket's text (sender `0x5efac0`); vmangos discards the category byte. pub fn gm_ticket_updatetext(&mut self, category: u8, text: &str) -> Result<()> { self.send( opcode::CMSG_GMTICKET_UPDATETEXT, @@ -53,26 +36,18 @@ impl WorldWriter { ) } - /// Ask for the open ticket (`CMSG_GMTICKET_GETTICKET`, empty body) — `GetGMTicket()`. - /// - /// The client's own poll: once on world entry and every 10 minutes while the ticket status - /// toast is up. Answered by `SMSG_GMTICKET_GETTICKET`, and on vmangos **also** by an - /// unsolicited `SMSG_QUERY_TIME_RESPONSE` — its handler calls `SendQueryTimeResponse()` as its - /// first statement (`GMTicketHandler.cpp:34`), so two packets come back for this one request. + /// Ask for the open ticket, on world entry and every 10 minutes with the toast up; vmangos also + /// sends an unsolicited `SMSG_QUERY_TIME_RESPONSE` (`GMTicketHandler.cpp:34`). pub fn gm_ticket_get(&mut self) -> Result<()> { self.send(opcode::CMSG_GMTICKET_GETTICKET, &[]) } - /// Abandon the open ticket (`CMSG_GMTICKET_DELETETICKET`, empty body) — the - /// `HELP_TICKET_ABANDON_CONFIRM` dialog's Yes. Answered by `SMSG_GMTICKET_DELETETICKET` - /// carrying 9, or — with no ticket to delete — by nothing at all on vmangos. + /// Abandon the open ticket; the reply carries 9, and vmangos sends none if there is no ticket. pub fn gm_ticket_delete(&mut self) -> Result<()> { self.send(opcode::CMSG_GMTICKET_DELETETICKET, &[]) } - /// Ask whether the petition queue is taking tickets (`CMSG_GMTICKET_SYSTEMSTATUS`, empty body) - /// — `GetGMStatus()`, which the Help window calls from its own OnShow. Answered by - /// `SMSG_GMTICKETSYSTEMSTATUS`. + /// Ask whether the ticket queue is open, as `GetGMStatus` does when the Help window shows. pub fn gm_ticket_system_status(&mut self) -> Result<()> { self.send(opcode::CMSG_GMTICKET_SYSTEMSTATUS, &[]) } diff --git a/crates/benilla-protocol/src/world/writer/gossip.rs b/crates/benilla-protocol/src/world/writer/gossip.rs index cb286606..4a9ca716 100644 --- a/crates/benilla-protocol/src/world/writer/gossip.rs +++ b/crates/benilla-protocol/src/world/writer/gossip.rs @@ -1,11 +1,5 @@ -//! The gossip family's `WorldWriter` sends — open a menu, choose an option, and fetch a menu's -//! greeting text. Bodies in [`crate::messages::gossip`], whose scope this mirrors. Split out of -//! `writer/mod.rs` (decision 0636). -//! -//! `CMSG_GOSSIP_HELLO` is the front door to every other NPC service window — vendor, trainer, -//! bank, taxi, quest — because the server passes `UNIT_NPC_FLAG_NONE` for this opcode (vmangos -//! `CanInteractWithNPC`, `Player.cpp:347`), so it works on any interactable creature, not only -//! gossip-flagged ones. +//! The gossip sends. `CMSG_GOSSIP_HELLO` opens every NPC service window: vmangos accepts it for any +//! interactable creature (`CanInteractWithNPC` with `UNIT_NPC_FLAG_NONE`, `Player.cpp:347`). use anyhow::Result; @@ -14,18 +8,12 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Open a gossip menu on an NPC (`CMSG_GOSSIP_HELLO`, layout in [`messages::gossip_hello`]) — - /// works on any interactable creature, not only gossip-flagged ones (vmangos - /// `CanInteractWithNPC`, `Player.cpp:347`, passes `UNIT_NPC_FLAG_NONE` for this opcode). - /// Answered by `SMSG_GOSSIP_MESSAGE` (a `GossipMenu` event). + /// Open a gossip menu on an NPC (`CMSG_GOSSIP_HELLO`), answered by `SMSG_GOSSIP_MESSAGE`. pub fn gossip_hello(&mut self, npc_guid: u64) -> Result<()> { self.send(opcode::CMSG_GOSSIP_HELLO, &messages::gossip_hello(npc_guid)) } - /// Choose a gossip option (`CMSG_GOSSIP_SELECT_OPTION`, layout in - /// [`messages::gossip_select_option`]): `gossip_list_id` is the option's echoed `index`; `code` - /// carries a password only for a `coded` option, omitted entirely otherwise. The server answers - /// either a fresh `SMSG_GOSSIP_MESSAGE` (a sub-menu) or `SMSG_GOSSIP_COMPLETE`. + /// Choose a gossip option by its echoed `index`; `code` is sent only for a coded option. pub fn gossip_select_option( &mut self, npc_guid: u64, @@ -38,9 +26,7 @@ impl WorldWriter { ) } - /// Ask for a gossip menu's greeting text (`CMSG_NPC_TEXT_QUERY`, layout in - /// [`messages::npc_text_query`]) — sent on receiving a gossip menu's `text_id`. Answered by - /// `SMSG_NPC_TEXT_UPDATE` (an `NpcGreeting` event); ask-once cacheable like an item template. + /// Ask a gossip menu's greeting text (`CMSG_NPC_TEXT_QUERY`); ask once and cache. pub fn npc_text_query(&mut self, text_id: u32, guid: u64) -> Result<()> { self.send( opcode::CMSG_NPC_TEXT_QUERY, diff --git a/crates/benilla-protocol/src/world/writer/group.rs b/crates/benilla-protocol/src/world/writer/group.rs index f7ee6f9c..29dcb4d6 100644 --- a/crates/benilla-protocol/src/world/writer/group.rs +++ b/crates/benilla-protocol/src/world/writer/group.rs @@ -1,9 +1,4 @@ -//! The group/party family's `WorldWriter` sends — invite/accept/decline/kick/leader/disband, loot -//! method, party-stats asks, raid subgroup management, minimap pings, raid-target icons, and ready -//! checks (bodies in [`crate::messages`]'s `group_*`/`loot_method`/`minimap_ping`/`raid_target_*`/ -//! `ready_check_*` builders; layout VERIFIED against vmangos `Server/Packets/Group.{h,cpp}`, -//! `Handlers/GroupHandler.cpp`). Split out of `writer/mod.rs`, mirroring [`super::channel`] — the -//! group commands are one clearly separable concern among the writer's many domains. +//! The group and raid sends. use anyhow::Result; @@ -12,8 +7,7 @@ use crate::messages::{self, opcode}; use super::WorldWriter; impl WorldWriter { - /// Invite a player to group (`CMSG_GROUP_INVITE`, layout in [`messages::group_invite`]). - /// Answered by `SMSG_PARTY_COMMAND_RESULT`; the invited player sees `SMSG_GROUP_INVITE`. + /// Invite a player to group, answered by `SMSG_PARTY_COMMAND_RESULT`. pub fn group_invite(&mut self, member_name: &str) -> Result<()> { self.send( opcode::CMSG_GROUP_INVITE, @@ -26,13 +20,12 @@ impl WorldWriter { self.send(opcode::CMSG_GROUP_ACCEPT, &messages::group_accept()) } - /// Decline a pending group invite (`CMSG_GROUP_DECLINE`, empty body); the inviter sees - /// `SMSG_GROUP_DECLINE`. + /// Decline a pending group invite; the inviter gets `SMSG_GROUP_DECLINE`. pub fn group_decline(&mut self) -> Result<()> { self.send(opcode::CMSG_GROUP_DECLINE, &messages::group_decline()) } - /// Kick a group member by name (`CMSG_GROUP_UNINVITE`, layout in [`messages::group_uninvite`]). + /// Kick a group member by name (`CMSG_GROUP_UNINVITE`). pub fn group_uninvite(&mut self, member_name: &str) -> Result<()> { self.send( opcode::CMSG_GROUP_UNINVITE, @@ -40,7 +33,7 @@ impl WorldWriter { ) } - /// Kick a group member by guid (`CMSG_GROUP_UNINVITE_GUID`) — the raid-frame right-click kick. + /// Kick a group member by guid (`CMSG_GROUP_UNINVITE_GUID`), the raid frame's kick. pub fn group_uninvite_guid(&mut self, guid: u64) -> Result<()> { self.send( opcode::CMSG_GROUP_UNINVITE_GUID, @@ -48,7 +41,7 @@ impl WorldWriter { ) } - /// Hand off group leadership (`CMSG_GROUP_SET_LEADER`, a full guid — the 1.12 wire form). + /// Hand off group leadership (`CMSG_GROUP_SET_LEADER`, a full guid in 1.12). pub fn group_set_leader(&mut self, guid: u64) -> Result<()> { self.send( opcode::CMSG_GROUP_SET_LEADER, @@ -56,10 +49,9 @@ impl WorldWriter { ) } - /// Set the group's loot method (`CMSG_LOOT_METHOD`, layout in [`messages::loot_method`]): - /// `method` is `0` free-for-all / `1` round-robin / `2` master / `3` group / `4` - /// need-before-greed; `loot_master` is the master looter's guid (ignored unless `method == - /// 2`); `threshold` is an `ItemQualities` value. + /// Set the group's loot method (`CMSG_LOOT_METHOD`): `method` 0 free-for-all, 1 round-robin, + /// 2 master (the only one that reads `loot_master`), 3 group, 4 need-before-greed; + /// `threshold` is an `ItemQualities` value. pub fn loot_method(&mut self, method: u32, loot_master: u64, threshold: u32) -> Result<()> { self.send( opcode::CMSG_LOOT_METHOD, @@ -72,8 +64,7 @@ impl WorldWriter { self.send(opcode::CMSG_GROUP_DISBAND, &messages::group_disband()) } - /// Ask a party/raid member's full live stats (`CMSG_REQUEST_PARTY_MEMBER_STATS`, a full guid). - /// Answered by `SMSG_PARTY_MEMBER_STATS_FULL` (or its offline-miss shape). + /// Ask a member's live stats, answered by `SMSG_PARTY_MEMBER_STATS_FULL`. pub fn request_party_member_stats(&mut self, guid: u64) -> Result<()> { self.send( opcode::CMSG_REQUEST_PARTY_MEMBER_STATS, @@ -81,8 +72,7 @@ impl WorldWriter { ) } - /// Move a raid member to a different subgroup (`CMSG_GROUP_CHANGE_SUB_GROUP`, layout in - /// [`messages::group_change_sub_group`]) — raid-only drag-drop between subgroup columns. + /// Move a raid member to another subgroup (`CMSG_GROUP_CHANGE_SUB_GROUP`). pub fn group_change_sub_group(&mut self, name: &str, group_nr: u8) -> Result<()> { self.send( opcode::CMSG_GROUP_CHANGE_SUB_GROUP, @@ -90,8 +80,7 @@ impl WorldWriter { ) } - /// Swap two raid members' subgroups (`CMSG_GROUP_SWAP_SUB_GROUP`, layout in - /// [`messages::group_swap_sub_group`]). + /// Swap two raid members' subgroups (`CMSG_GROUP_SWAP_SUB_GROUP`). pub fn group_swap_sub_group(&mut self, name: &str, swap_with: &str) -> Result<()> { self.send( opcode::CMSG_GROUP_SWAP_SUB_GROUP, @@ -99,8 +88,7 @@ impl WorldWriter { ) } - /// Convert the party into a raid (`CMSG_GROUP_RAID_CONVERT`, empty body) — leader-only, - /// one-way. + /// Convert the party into a raid (`CMSG_GROUP_RAID_CONVERT`, empty body); leader only, one-way. pub fn group_raid_convert(&mut self) -> Result<()> { self.send( opcode::CMSG_GROUP_RAID_CONVERT, @@ -108,8 +96,7 @@ impl WorldWriter { ) } - /// Grant or revoke raid-assistant on a member (`CMSG_GROUP_ASSISTANT_LEADER`, layout in - /// [`messages::group_assistant_leader`]). + /// Grant or revoke raid assistant (`CMSG_GROUP_ASSISTANT_LEADER`). pub fn group_assistant_leader(&mut self, guid: u64, grant: bool) -> Result<()> { self.send( opcode::CMSG_GROUP_ASSISTANT_LEADER, @@ -117,14 +104,12 @@ impl WorldWriter { ) } - /// Ping the minimap for the group (`MSG_MINIMAP_PING`, layout in [`messages::minimap_ping`]) — - /// the server stamps our guid and relays it to the rest of the group. + /// Ping the minimap (`MSG_MINIMAP_PING`); the server adds our guid and relays it to the group. pub fn minimap_ping(&mut self, x: f32, y: f32) -> Result<()> { self.send(opcode::MSG_MINIMAP_PING, &messages::minimap_ping(x, y)) } - /// Set (or clear, `guid == 0`) one raid-target icon (`MSG_RAID_TARGET_UPDATE`, layout in - /// [`messages::raid_target_set`]). + /// Set one raid-target icon, or clear it with `guid == 0` (`MSG_RAID_TARGET_UPDATE`). pub fn raid_target_set(&mut self, icon: u8, guid: u64) -> Result<()> { self.send( opcode::MSG_RAID_TARGET_UPDATE, @@ -132,8 +117,7 @@ impl WorldWriter { ) } - /// Ask the current raid-target icon set (`MSG_RAID_TARGET_UPDATE`, layout in - /// [`messages::raid_target_request`]). Answered by the server's full-list shape. + /// Ask the raid-target icons (`MSG_RAID_TARGET_UPDATE`), answered with the full list. pub fn raid_target_request(&mut self) -> Result<()> { self.send( opcode::MSG_RAID_TARGET_UPDATE, @@ -141,13 +125,12 @@ impl WorldWriter { ) } - /// Start a raid ready check (`MSG_RAID_READY_CHECK`, empty body) — leader-only. + /// Start a raid ready check (`MSG_RAID_READY_CHECK`, empty body); leader only. pub fn ready_check_start(&mut self) -> Result<()> { self.send(opcode::MSG_RAID_READY_CHECK, &messages::ready_check_start()) } - /// Answer a raid ready check (`MSG_RAID_READY_CHECK`, layout in - /// [`messages::ready_check_answer`]). + /// Answer a raid ready check (`MSG_RAID_READY_CHECK`). pub fn ready_check_answer(&mut self, ready: bool) -> Result<()> { self.send( opcode::MSG_RAID_READY_CHECK, @@ -155,8 +138,7 @@ impl WorldWriter { ) } - /// Ask for our saved-instance list (`CMSG_REQUEST_RAID_INFO`, empty body — decision 1549's - /// Raid Info panel). Answered by `SMSG_RAID_INSTANCE_INFO`. + /// Ask our saved-instance list, answered by `SMSG_RAID_INSTANCE_INFO`. pub fn request_raid_info(&mut self) -> Result<()> { self.send( opcode::CMSG_REQUEST_RAID_INFO,