diff --git a/crates/benilla-protocol/src/messages/pet.rs b/crates/benilla-protocol/src/messages/pet.rs index 9bfcb92d..39ff4257 100644 --- a/crates/benilla-protocol/src/messages/pet.rs +++ b/crates/benilla-protocol/src/messages/pet.rs @@ -1,174 +1,96 @@ -//! Pet messages — the pet action bar's state packet and the client verbs that drive it -//! (decisions 0982, 0988). Mirrored on the outbound side by `world::writer::pet`. -//! -//! The **shape of the system** is worth stating once, because it is neither the action bar's nor -//! the one it looks like. The bar's **contents** are the server's: `SMSG_PET_SPELLS` carries all -//! ten slots and nothing else does. The bar's **state** — the lit command, the lit reaction, the -//! autocast bit, "the pet is attacking" — is the **client's**, applied on the press and never -//! confirmed. Not as an optimisation: the server does not answer these packets at all, and the -//! real client writes its own state and repaints before the send (wow-re §10.1/§10.2). A client -//! that waited would show a bar that never lights. -//! -//! Compare the player's 120-slot bar, which is client-authoritative and merely *echoed* to the -//! server ([`super::action_bar`], decisions 0216 §7/0218 §4): different split, same lesson — know -//! which half of a surface each end owns before writing either. -//! -//! `SMSG_PET_SPELLS` carries the whole bar in one body, and the same body serves a hunter/warlock -//! pet, a possessed minion and a charmed creature (vmangos `Player::PetSpellInitialize` / -//! `PossessSpellInitialize` / `CharmSpellInitialize`, `Objects/Player.cpp:17519-17672` — three -//! builders, one byte layout: the possess builder's `int32 duration` + `uint32 0` occupies exactly -//! the pet builder's `uint32 0` + four state bytes). Its **guid-only** form (an 8-byte body -//! carrying a zero guid — `Player::RemovePetActionBar`) is the teardown: the bar goes away. +//! Pet messages: the pet action bar's state packet and the client verbs that drive it. The bar's +//! contents are the server's; its state (lit command, lit reaction, autocast) is the client's, +//! applied on the press, because the server never answers these verbs. use std::io::{self, Read}; use crate::wire::{capacity_hint, read_u16_le, read_u32_le, read_u64_le, read_u8, Vector3d}; -/// Pet action-bar slots (vmangos `MAX_UNIT_ACTION_BAR_INDEX` = `ACTION_BAR_INDEX_END(10) - -/// ACTION_BAR_INDEX_START(0)`, `Objects/UnitDefines.h:781-787`); the client's own -/// `NUM_PET_ACTION_SLOTS` is the same 10 (`PetActionBarFrame.lua:4`). +/// Pet bar slots: vmangos `MAX_UNIT_ACTION_BAR_INDEX`, FrameXML's `NUM_PET_ACTION_SLOTS`. pub const PET_ACTION_SLOTS: usize = 10; -/// `ActiveStates` — the value the **server** puts in a packed word's top byte (vmangos -/// `Objects/UnitDefines.h:724-732`). These are what you BUILD a word from; they are **not** what -/// you decode one into (see [`PetActionEntry::kind`], and the note below). -/// -/// A passive spell: shown, never clickable, no autocast. +/// vmangos `ActiveStates` (`UnitDefines.h:724-732`): the server's top byte of a packed word; the +/// client decodes the masked [`PetActionEntry::kind`] instead. Passive: shown, never clickable. pub const PET_ACT_PASSIVE: u8 = 0x01; -/// A castable spell with autocast OFF — `0x01 | ` [`PET_AUTOCAST_ALLOWED`]`>>24`. +/// A castable spell with autocast off: `0x01 | ` [`PET_AUTOCAST_ALLOWED`]`>>24`. pub const PET_ACT_DISABLED: u8 = 0x81; -/// A castable spell with autocast ON — `0x81 | ` [`PET_AUTOCAST_ON`]`>>24`. +/// A castable spell with autocast on: `0x81 | ` [`PET_AUTOCAST_ON`]`>>24`. pub const PET_ACT_ENABLED: u8 = 0xC1; -/// A command token — the action is a [`PET_COMMAND_STAY`]-family value, not a spell id. +/// A command token: the action is a [`PET_COMMAND_STAY`]-family value, not a spell id. pub const PET_ACT_COMMAND: u8 = 0x07; -/// A reaction token — the action is a [`PET_REACT_PASSIVE`]-family value, not a spell id. +/// A reaction token: the action is a [`PET_REACT_PASSIVE`]-family value, not a spell id. pub const PET_ACT_REACTION: u8 = 0x06; -/// The **client's** slot type, after its own `& 0x3F` mask (wow-re -/// `system/ui/scratch/pet-action-bar-api.md` §1, `0x4bdccd`). This is the vocabulary a decoded -/// word speaks, and it is deliberately SHORTER than `ActiveStates`': the mask drops bits 30/31, -/// which is precisely what collapses [`PET_ACT_PASSIVE`], [`PET_ACT_DISABLED`] and -/// [`PET_ACT_ENABLED`] — three server states — onto **one** client type, with the autocast pair -/// carried separately as [`PET_AUTOCAST_ALLOWED`]/[`PET_AUTOCAST_ON`]. -/// -/// `GetPetActionInfo`'s jump table sends types 1–5 down the spell branch, 6 to reaction, 7 to -/// command (§2). The two token constants are numerically the `ACT_` ones, so a `kind()` compare -/// against `PET_ACT_COMMAND`/`PET_ACT_REACTION` still reads correctly; the spell family does not, -/// which is why [`PetActionEntry::is_spell`] is a range and not a set of equalities. +/// First spell-branch slot type. The client's type is the top byte `& 0x3F` (`0x4bdccd`), which +/// folds all three spell `ActiveStates` onto 1; types 6 and 7 are reaction and command. pub const PET_TYPE_SPELL_FIRST: u8 = 1; /// The last type the client's jump table routes to the spell branch. pub const PET_TYPE_SPELL_LAST: u8 = 5; -/// Bit 31 of a packed word: this slot **may** autocast (wow-re §2.1, `0x4bdd65`). The client -/// additionally requires the spell to exist in `Spell.dbc` before reporting it. +/// Bit 31 of a packed word: the slot may autocast (`0x4bdd65`), given a `Spell.dbc` record. pub const PET_AUTOCAST_ALLOWED: u32 = 0x8000_0000; -/// Bit 30: autocast is currently **running** (wow-re §2.1, `0x4bdda4`). This is the bit -/// `TogglePetAutocast` flips in place before it sends (§10.2, `0x4bcbff`). +/// Bit 30: autocast is running (`0x4bdda4`); `TogglePetAutocast` flips it before the send. pub const PET_AUTOCAST_ON: u32 = 0x4000_0000; -/// `CommandStates` (vmangos `UnitDefines.h:755-761`) — the action value carried by a -/// [`PET_ACT_COMMAND`] slot, and what `CharmInfo::GetCommandState` reports. +/// vmangos `CommandStates` (`UnitDefines.h:755-761`): a [`PET_ACT_COMMAND`] slot's action. pub const PET_COMMAND_STAY: u32 = 0; pub const PET_COMMAND_FOLLOW: u32 = 1; pub const PET_COMMAND_ATTACK: u32 = 2; pub const PET_COMMAND_DISMISS: u32 = 3; -/// `ReactStates` (vmangos `UnitDefines.h:734-739`) — the action value carried by a -/// [`PET_ACT_REACTION`] slot, and what `CharmInfo::GetReactState` reports. +/// vmangos `ReactStates` (`UnitDefines.h:734-739`): a [`PET_ACT_REACTION`] slot's action. pub const PET_REACT_PASSIVE: u32 = 0; pub const PET_REACT_DEFENSIVE: u32 = 1; pub const PET_REACT_AGGRESSIVE: u32 = 2; -/// The bit that marks the pet's bar **disabled**, in [`PetSpells::state`]'s own dword. -/// -/// Two independent readings meet on this one bit, which is why it is worth stating: vmangos writes -/// `pet->IsEnabled() ? 0x0 : 0x8` as the state field's **fourth byte** (`Player.cpp:17536`), and -/// the client reads **bit 27** of the whole dword (wow-re §1/§4, `0x4bd08d`). `0x8 << 24` is bit -/// 27 — the same bit, reached from both ends. -/// -/// It does two things, both verified in the client: it forces the reaction compare's left side to -/// Passive (§2.2) and it fails the usability predicate (§4). A disabled bar still draws. +/// Bit 27 of [`PetSpells::state`], vmangos's fourth byte `0x8` (`Player.cpp:17536`): the bar is +/// disabled, its lit reaction reads Passive and it is unusable, but it still draws (`0x4bd08d`). pub const PET_STATE_BAR_DISABLED: u32 = 0x0800_0000; -/// The pet's `UNIT_FIELD_FLAGS` bits that make its bar unusable — vmangos -/// `UNIT_FLAG_STUNNED 0x00040000` / `UNIT_FLAG_CONFUSED 0x00400000` / `UNIT_FLAG_FLEEING -/// 0x00800000` (`UnitDefines.h:509-514`), which are exactly bits 18/22/23, the three the client's -/// usability predicate tests (wow-re §4 step 6, `0x4bd075`-`0x4bd08b`). -/// -/// i.e. **a stunned, confused or feared pet cannot be ordered** — and note `UNIT_FLAG_POSSESSED` -/// (bit 24) is deliberately NOT among them: a possessed unit is exactly the case where the bar -/// must still work. +/// The pet's `UNIT_FIELD_FLAGS` bits that make its bar unusable: stunned, confused, fleeing +/// (`0x4bd075`). `UNIT_FLAG_POSSESSED` is not among them: a possessed unit's bar must work. pub const PET_UNUSABLE_UNIT_FLAGS: u32 = 0x0004_0000 | 0x0040_0000 | 0x0080_0000; -/// The permanent-cooldown marker OR-ed into a pet cooldown's **category** duration (vmangos -/// `Unit::WritePetSpellsCooldown`, `Objects/Unit.cpp:11270-11271`) — the twin of the player -/// initial-spell list's top-bit convention. Strip it before using the number as a duration. +/// Permanent-cooldown marker OR-ed into a pet cooldown's category duration (vmangos +/// `Unit.cpp:11270-11271`); strip it before using the number as a duration. pub const PET_COOLDOWN_PERMANENT: u32 = 0x0800_0000; -/// One packed pet action word, decoded **the way the client decodes it** (wow-re §1, verified at -/// `0x4bdcc7`/`0x4bdccd`/`0x4bdce3`): -/// -/// ```text -/// kind = (packed >> 24) & 0x3F the 0x3F mask is load-bearing — see below -/// action = packed & 0xFFFF SIXTEEN bits, not 24 -/// bit 31 = autocast allowed bits 16..23 are never read by anything -/// bit 30 = autocast on -/// ``` -/// -/// The mask is the whole reason the server's five `ActiveStates` become the client's three -/// branches: `0x01`, `0x81` and `0xC1` all mask to type **1**, and what separated them — -/// castability and autocast — is read from bits 31/30 instead. Decoding `>> 24` unmasked (which -/// benilla did until this was pinned) happens to work for the exact bytes vmangos sends and -/// silently mis-sorts anything else. -/// -/// What `action` *means* is `kind`'s to say: a spell id on the spell branch, a `CommandStates` -/// for [`PET_ACT_COMMAND`], a `ReactStates` for [`PET_ACT_REACTION`]. The raw word is kept -/// because the client echoes it **verbatim** in a [`pet_action`] body — re-packing from the split -/// pair would be a second chance to get it wrong. +/// One packed pet action word, as the client decodes it (`0x4bdcc7`/`0x4bdccd`/`0x4bdce3`): the +/// action in bits 0-15, the type in 24-29, autocast allowed in 31 and on in 30; bits 16-23 are +/// never read. The raw word is kept because the client echoes it verbatim in [`pet_action`]. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct PetActionEntry { - /// The whole word, exactly as it arrived (what [`pet_action`] echoes). pub packed: u32, } impl PetActionEntry { - /// Bits 0–15: the spell id, command state or react state. + /// Bits 0-15: the spell id, command state or react state. pub fn action(self) -> u32 { self.packed & 0xFFFF } - /// The client's slot type — bits 24–29 (see the type doc for why the mask matters). + /// The client's slot type, bits 24-29. pub fn kind(self) -> u8 { ((self.packed >> 24) & 0x3F) as u8 } - /// Does this slot take the **spell** branch (client types 1–5), rather than the reaction or - /// command branch? A type outside 1–7 is neither: it falls into the client's own default arm, - /// which we treat as an empty slot rather than reproducing (wow-re §2.5 quirk 1). + /// Client types 1-5 take the spell branch. Deviation: a type outside 1-7 reads as nothing + /// rather than the reference's default arm, which under-pushes its returns. pub fn is_spell(self) -> bool { (PET_TYPE_SPELL_FIRST..=PET_TYPE_SPELL_LAST).contains(&self.kind()) } - /// Is the slot EMPTY? **The whole word is zero** — the client tests the dword, not a field - /// (`0x4bdcbd test ecx,ecx`). - /// - /// vmangos's own unused middle slots are `(0, ACT_DISABLED)` = `0x8100_0000`, which is *not* - /// zero and so takes the spell branch — where spell id 0 has no `Spell.dbc` record, so every - /// return including the autocast pair comes back nil anyway (§2.1's no-record sub-path). Two - /// routes, one empty-looking button; this is the client's route. + /// Empty is a zero word (`0x4bdcbd`). vmangos's unused slots, `0x8100_0000`, take the spell + /// branch instead and come back nil, since spell 0 has no `Spell.dbc` record. pub fn is_empty(self) -> bool { self.packed == 0 } - /// Bit 31 — this slot may autocast. (The client also requires the spell to resolve in - /// `Spell.dbc`; that half belongs to whoever holds the catalog.) + /// Bit 31; the client also requires a `Spell.dbc` record, which the catalog holder checks. pub fn autocast_allowed(self) -> bool { self.packed & PET_AUTOCAST_ALLOWED != 0 } - /// Bit 30 — autocast is currently running. + /// Bit 30: autocast is running. pub fn autocast_on(self) -> bool { self.packed & PET_AUTOCAST_ON != 0 } - /// The word with bit 30 set or cleared — exactly `TogglePetAutocast`'s own in-place flip - /// (wow-re §10.2, `0x4bcbff`: `if (bit30 clear) v |= 0x40000000 else v &= 0xBFFFFFFF`), which - /// the client writes back to the slot **before** sending and which is then what goes on the - /// wire. + /// Bit 30 set or cleared: `TogglePetAutocast`'s in-place flip (`0x4bcbff`), before the send. pub fn with_autocast(self, on: bool) -> Self { Self { packed: if on { @@ -186,9 +108,7 @@ impl From for PetActionEntry { } } -/// One entry of `SMSG_PET_SPELLS`' trailing cooldown block, decoded. The wire widths are -/// [`read_cooldown_block`]'s problem — the two senders disagree about them — so this is the -/// decoded shape only. +/// One decoded entry of `SMSG_PET_SPELLS`' trailing cooldown block. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PetSpellCooldown { pub spell_id: u32, @@ -198,80 +118,46 @@ pub struct PetSpellCooldown { pub category_cd_ms: u32, } -/// `SMSG_PET_SPELLS` in full — the pet action bar's entire state, in one body. +/// `SMSG_PET_SPELLS`: the pet action bar's entire state in one body. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PetSpells { - /// The controlled unit (pet, possessed minion or charmed creature). **Zero means the bar is - /// gone**: `Player::RemovePetActionBar` sends an 8-byte body holding a zero guid and nothing - /// else, and [`read_pet_spells`] answers that with an otherwise-default value. + /// The pet, possessed minion or charmed creature; zero means the bar is gone. pub pet_guid: u64, - /// The charm's remaining duration in ms (`0` for a real pet — only the possess/charm builders - /// fill it, from the `SPELL_AURA_MOD_POSSESS`/`MOD_CHARM` aura). + /// The possess or charm aura's remaining ms; 0 for a real pet. pub duration_ms: u32, - /// The pet's mode word, **stored verbatim** — react state in byte 0, command state in byte 1, - /// [`PET_STATE_BAR_DISABLED`] at bit 27. - /// - /// One dword rather than four bytes because that is what the client keeps (`[0xb71468]`, wow-re - /// §1) and because two of its readings depend on the *whole* word rather than a field: the - /// reaction compare zeroes itself on bit 27, and the command compare is an **unmasked** - /// `state >> 8`, so a set bit 27 puts it out of range of any action and no command can light - /// (§2.3). Splitting it into bytes loses both behaviours silently. + /// The mode word, one dword as the client keeps it (`[0xb71468]`): react state in byte 0, + /// command state in byte 1, [`PET_STATE_BAR_DISABLED`] at bit 27. Do not split it into bytes. pub state: u32, /// The ten bar slots, in bar order. pub bar: [PetActionEntry; PET_ACTION_SLOTS], - /// The pet's whole known-spell list — the pet spellbook, packed the same way (spell id + - /// its current autocast type). Empty for a temporary pet or a charm; only a permanent pet - /// gets one (`Pet::IsPermanentPetFor` gates the loop, `Player.cpp:17549`). + /// The pet spellbook, packed like the bar; only a permanent pet has one (`Player.cpp:17549`). pub spells: Vec, - /// The pet's running cooldowns. pub cooldowns: Vec, } impl PetSpells { - /// Byte 0 of [`Self::state`] — a [`PET_REACT_PASSIVE`]-family value. + /// Byte 0 of [`Self::state`], a [`PET_REACT_PASSIVE`]-family value. pub fn react_state(&self) -> u32 { self.state & 0xFF } - /// `state >> 8` — a [`PET_COMMAND_STAY`]-family value, **unmasked on purpose**. - /// - /// The client's own command compare is `(state >> 8) == action` with no `& 0xFF` - /// (`0x4bdf0f`), so when bit 27 is set this is a number no command state can equal and every - /// command button goes dark. Masking here would be "tidier" and would quietly delete that. + /// `state >> 8`, unmasked like the client's compare (`0x4bdf0f`): with bit 27 set it matches + /// no command state, so every command button goes dark. Masking it would lose that. pub fn command_state(&self) -> u32 { self.state >> 8 } - /// Bit 27 — the server says the bar is disabled. + /// Bit 27: the server has disabled the bar. pub fn bar_disabled(&self) -> bool { self.state & PET_STATE_BAR_DISABLED != 0 } } -/// Read `SMSG_PET_SPELLS` (VERIFIED vmangos `Player::PetSpellInitialize`, -/// `Objects/Player.cpp:17519-17561`, with `CharmInfo::BuildActionBar` `Unit.cpp:8782-8786` and -/// `Unit::WritePetSpellsCooldown` `Unit.cpp:11245-11280`): -/// -/// ```text -/// u64 petGuid -/// u32 duration (0 for a pet; the charm/possess remainder otherwise) -/// u32 state react | command<<8 | 0<<16 | enabledFlag<<24 — kept VERBATIM -/// u32 × 10 the bar, packed -/// u8 spellCount then that many u32, packed the same way -/// see read_cooldown_block — the one field the client and vmangos disagree on -/// ``` -/// -/// vmangos writes the state as four separate bytes and the client reads one dword -/// (`0x4bd9d3 GetInt32` → `[0xb71468]`); those are the same four bytes, and holding the dword is -/// what lets bit 27 and the unmasked `>> 8` behave (see [`PetSpells::state`]). -/// -/// The **8-byte teardown** form (`Player::RemovePetActionBar`, `Player.cpp:17672-17677`) is a lone -/// zero guid: this returns [`PetSpells::default`] for it, so a caller's only test is -/// `pet_guid == 0`. Anything short of a full body after a non-zero guid is a real parse error and -/// surfaces as one — a truncated bar is not a bar. +/// Read `SMSG_PET_SPELLS` (vmangos `Player::PetSpellInitialize`, `Player.cpp:17519-17561`): +/// `u64 petGuid, u32 duration, u32 state, u32 bar[10], u8 count, u32 spells[count]`, then the +/// cooldown block. A lone guid is the teardown; any longer body cut short is an error. pub(super) fn read_pet_spells(r: &mut &[u8]) -> io::Result { let pet_guid = read_u64_le(r)?; if r.is_empty() { - // The teardown. vmangos always writes a zero guid here; carrying whatever arrived keeps - // the reader honest, and the `pet_guid == 0` test is the caller's contract either way. + // The teardown; vmangos always writes a zero guid here. return Ok(PetSpells { pet_guid, ..Default::default() @@ -286,7 +172,7 @@ pub(super) fn read_pet_spells(r: &mut &[u8]) -> io::Result { } let spell_count = read_u8(r)?; - // A `u8` count with no tighter server bound (`Player.cpp:17471`, `uint8 addlist`). + // A `u8` count with no tighter server bound (`Player.cpp:17471`). let mut spells = Vec::with_capacity(capacity_hint(spell_count, usize::from(u8::MAX))); for _ in 0..spell_count { spells.push(read_u32_le(r)?.into()); @@ -304,28 +190,14 @@ pub(super) fn read_pet_spells(r: &mut &[u8]) -> io::Result { }) } -/// The trailing cooldown block — **the one place the real client and vmangos disagree about this -/// packet**, so it is read by measuring rather than by choosing a side. -/// -/// - The **client** reads `u8 count` then 12-byte entries `{u16 spellId, u16 category, u32, u32}` -/// (wow-re §8, getter widths verified at `0x4bda58`/`0x4bda6c`/`0x4bda82`; the arbitration is -/// §12.6). -/// - **vmangos** writes `u16 count` then 14-byte entries `{u32 spellId, u16 category, u32, u32}` -/// (`Unit::WritePetSpellsCooldown`, `Objects/Unit.cpp:11225-11258`) — the later-expansion -/// layout. A real 1.12 client talking to vmangos mis-parses this block; we are not going to -/// reproduce that bug just to match it. -/// -/// The block is the packet's **tail**, so the remaining length settles it outright: `n` client -/// entries leave `12n` bytes, `n` vmangos entries leave `1 + 14n` (the `+1` being the u16 count's -/// high byte, which the `u8` read did not consume). `12n == 1 + 14m` has no solution in -/// non-negative integers with `n == m`, and for `count == 0` both leave nothing to read. So the -/// discriminator is exact, not a heuristic — and the day vmangos is fixed, this keeps working. +/// The trailing cooldown block. The client reads `u8 count` then 12-byte entries +/// `{u16 spell, u16 category, u32, u32}` (`0x4bda58`); vmangos writes `u16 count` then 14-byte +/// entries with a `u32` spell (`Unit.cpp:11225-11258`). Deviation: both are read, told apart +/// exactly by the tail length (`12n` vs `1 + 14n`), since the 1.12 client mis-parses vmangos's. fn read_cooldown_block(r: &mut &[u8]) -> io::Result> { let count = usize::from(read_u8(r)?); - // The discriminator holds at `count == 0` too: vmangos's lone leftover is its `u16` count's - // high byte, the client's form leaves nothing. The byte is consumed rather than left behind - // so an empty block is not a tail for the decode-length check to announce (decision 2265 - // §B1); either way nothing else is read and no entry is invented. + // At `count == 0` vmangos still leaves its `u16` count's high byte; it is consumed so the + // decode-length check sees no tail. let vmangos = r.len() == 1 + 14 * count; if vmangos { let _high_byte = read_u8(r)?; @@ -333,7 +205,7 @@ fn read_cooldown_block(r: &mut &[u8]) -> io::Result> { if count == 0 { return Ok(Vec::new()); } - // `count` came off a `u8` (above); no tighter bound exists on either producer. + // `count` came off a `u8`; no tighter bound exists on either producer. let mut cooldowns = Vec::with_capacity(capacity_hint(count, usize::from(u8::MAX))); for _ in 0..count { let spell_id = if vmangos { @@ -351,20 +223,15 @@ fn read_cooldown_block(r: &mut &[u8]) -> io::Result> { Ok(cooldowns) } -/// `SMSG_PET_MODE` — the state-only refresh: the same four bytes `SMSG_PET_SPELLS` carries, with -/// no bar behind them. The server sends it when a command or reaction changes without the bar's -/// contents moving. +/// `SMSG_PET_MODE`: the state word without the bar, for a command or reaction change. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct PetMode { pub pet_guid: u64, - /// The same verbatim dword [`PetSpells::state`] holds — the client stores it through the very - /// same writer (`0x4bc930`) from both packets. + /// The same dword as [`PetSpells::state`]; the client stores both through `0x4bc930`. pub state: u32, } -/// Read `SMSG_PET_MODE` — `u64 petGuid, u32 state` (client handler `0x4bdb10`, wow-re §8; -/// vmangos's four state bytes at `WorldPackets::Pet::PetMode::AppendBodyTo`, -/// `Server/Packets/Pet.cpp:101-108`, are that dword). +/// Read `SMSG_PET_MODE`: `u64 petGuid, u32 state` (`0x4bdb10`; vmangos `Packets/Pet.cpp:101-108`). pub(super) fn read_pet_mode(r: &mut impl Read) -> io::Result { Ok(PetMode { pet_guid: read_u64_le(r)?, @@ -372,32 +239,18 @@ pub(super) fn read_pet_mode(r: &mut impl Read) -> io::Result { }) } -/// Read `SMSG_PET_ACTION_FEEDBACK` (VERIFIED vmangos `PetActionFeedback::AppendBodyTo`, -/// `Server/Packets/Pet.cpp:87-90`): one reason byte. The client turns it into the red error line -/// ("Your pet has no path to that location", …). +/// Read `SMSG_PET_ACTION_FEEDBACK` (vmangos `Packets/Pet.cpp:87-90`): one reason byte. pub(super) fn read_pet_action_feedback(r: &mut impl Read) -> io::Result { read_u8(r) } -/// Read `SMSG_PET_TAME_FAILURE` (VERIFIED vmangos `PetTameFailure::AppendBodyTo`, -/// `Server/Packets/Pet.cpp:122-125`): one reason byte. [`pet_tame_failure_key`] is what the -/// reference does with it. +/// Read `SMSG_PET_TAME_FAILURE` (vmangos `Packets/Pet.cpp:122-125`): one reason byte. pub(super) fn read_pet_tame_failure(r: &mut impl Read) -> io::Result { read_u8(r) } -/// The `SMSG_PET_TAME_FAILURE` reason -> the `GlobalStrings.lua` key whose text fills -/// `ERR_TAME_FAILED`'s lone `%s` — the reference's jump table at `0x6e6ac0`, read out of the -/// image dword for dword. -/// -/// `0x6e6a20` computes `reason - 1` and bounds it at `0xa`, so **`1..=11` index the table and -/// everything else takes the default arm** `0x6e6a87` -> `PETTAME_UNKNOWNERROR`. That default is -/// reachable in play: vmangos's own enum has a twelfth value (`PETTAME_UNKNOWNERROR = 12`) which -/// falls out of range and lands on it, arriving at the same string by the other road. -/// -/// The eleven pairs are byte-identical to vmangos's `PetTameFailureReason` -/// (`src/game/SharedDefines.h:1707`) — the two ends agree without either having been fitted to -/// the other. +/// The `GlobalStrings.lua` key filling `ERR_TAME_FAILED`'s `%s`: the reference's table +/// (`0x6e6ac0`) covers `1..=11`, and anything else, vmangos's 12 included, is the default arm. pub fn pet_tame_failure_key(reason: u8) -> &'static str { match reason { 1 => "PETTAME_INVALIDCREATURE", @@ -415,44 +268,26 @@ pub fn pet_tame_failure_key(reason: u8) -> &'static str { } } -/// `SMSG_PET_ACTION_SOUND`'s selector: the pet acknowledging an **order** (vmangos -/// `PET_TALK_SPECIAL_SPELL`, sent when a pet bar spell press is accepted — -/// `PetHandler.cpp:523`, `PetAI.cpp:336`). +/// Sound selector for an accepted order (vmangos `PET_TALK_SPECIAL_SPELL`, `PetHandler.cpp:523`). pub const PET_TALK_ORDER: u32 = 0; -/// `SMSG_PET_ACTION_SOUND`'s selector: the pet **attacking** (vmangos `PET_TALK_ATTACK`, sent -/// from `Unit.cpp:8941` when an attack order is taken). +/// Sound selector for an attack order (vmangos `PET_TALK_ATTACK`, `Unit.cpp:8941`). pub const PET_TALK_ATTACK: u32 = 1; -/// Read `SMSG_PET_ACTION_SOUND` (VERIFIED vmangos `PetActionSound::AppendBodyTo`, -/// `Server/Packets/Pet.cpp:96-100`, and the reference's own reader at `0x6040ca`/`0x6040db`): -/// `u64 petGuid` then the `u32` talk selector. -/// -/// The selector is NOT a `SoundEntries` id — it is [`PET_TALK_ORDER`] / [`PET_TALK_ATTACK`], and -/// the sound comes off the pet's own `CreatureSoundData` row. Worth saying because the opcode's -/// name invites the other reading, and a two-value selector and a kit id are indistinguishable -/// on the wire. +/// Read `SMSG_PET_ACTION_SOUND`: `u64 petGuid, u32 selector` (`0x6040ca`, vmangos +/// `Packets/Pet.cpp:96-100`). The selector is a `PET_TALK_*` value, not a `SoundEntries` id: the +/// sound comes off the pet's `CreatureSoundData` row. pub(super) fn read_pet_action_sound(r: &mut impl Read) -> io::Result<(u64, u32)> { Ok((read_u64_le(r)?, read_u32_le(r)?)) } -/// Read `SMSG_PET_DISMISS_SOUND` — `u32 creatureModelDataId` then `f32` x/y/z. -/// -/// VERIFIED off **the reference's own reader**, which is the only authority there is: `0x604140` -/// takes one `0x418e30` (u32) then three `0x419130` (f32) in that order, and vmangos has no -/// sender to cross-check against. The `+1.0` the handler adds to `z` afterwards is a *play* -/// detail, not a wire one, so it is not applied here. +/// Read `SMSG_PET_DISMISS_SOUND`: `u32 creatureModelDataId, f32 x, y, z` (reader `0x604140`; +/// vmangos never sends it). The handler's `+1.0` on `z` is a play detail, not applied here. pub(super) fn read_pet_dismiss_sound(r: &mut impl Read) -> io::Result<(u32, Vector3d)> { Ok((read_u32_le(r)?, Vector3d::read(r)?)) } -/// Read `SMSG_PET_CAST_FAILED` (VERIFIED vmangos `PetCastFailed::AppendBodyTo`, -/// `Server/Packets/Pet.cpp:127-132`): `u32 spellId, u8 status, u8 reason`. The pet's twin of -/// `SMSG_CAST_RESULT` — the same `SpellCastResult` vocabulary, decoded the same way (status `2` = -/// fail) so the one message table renders both. vmangos only ever sends the fail status here, but -/// reading it the shared way costs nothing and keeps the two decodes from drifting. -/// -/// The body stops at the reason: unlike `CastResult`, `PetCastFailed::AppendBodyTo` writes **no -/// argument words** at all, so a pet failure never carries a `%s` fill. +/// Read `SMSG_PET_CAST_FAILED` (vmangos `Packets/Pet.cpp:127-132`): `u32` spell, `u8` status, +/// `u8` reason, in `SMSG_CAST_RESULT`'s vocabulary (2 is a failure) but with no argument words. pub(super) fn read_pet_cast_failed(r: &mut impl Read) -> io::Result<(u32, super::CastOutcome)> { let spell_id = read_u32_le(r)?; let status = read_u8(r)?; @@ -467,14 +302,8 @@ pub(super) fn read_pet_cast_failed(r: &mut impl Read) -> io::Result<(u32, super: Ok((spell_id, outcome)) } -/// Body of `CMSG_PET_ACTION` (VERIFIED vmangos `WorldPackets::Pet::PetAction::ReadFromWorldPacket`, -/// `Server/Packets/Pet.cpp:9-14`; opcode 373 `Opcodes_1_12_1.h:374`): `u64 petGuid, u32 packedData, -/// u64 targetGuid`. -/// -/// **`packed` is the slot's own word, echoed** — the server re-splits it exactly as the client -/// packed it (`UNIT_ACTION_BUTTON_ACTION/TYPE` at `PetHandler.cpp:37-38`) and dispatches on the -/// type: `ACT_COMMAND` runs `HandlePetCommand`, `ACT_REACTION` sets the react state, and the three -/// spell types cast. `target` is `0` when the action needs none. +/// Body of `CMSG_PET_ACTION` (opcode 373, vmangos `Packets/Pet.cpp:9-14`): `u64` pet guid, the +/// slot's `u32` word echoed verbatim, `u64` target guid (0 when the action needs none). pub fn pet_action(pet_guid: u64, packed: u32, target_guid: u64) -> Vec { let mut body = Vec::with_capacity(20); body.extend_from_slice(&pet_guid.to_le_bytes()); @@ -483,26 +312,15 @@ pub fn pet_action(pet_guid: u64, packed: u32, target_guid: u64) -> Vec { body } -/// Body of `CMSG_PET_STOP_ATTACK` (VERIFIED vmangos `PetStopAttack::ReadFromWorldPacket`, -/// `Server/Packets/Pet.cpp:33-36`; opcode 746 `Opcodes_1_12_1.h:747`): the lone `u64` pet guid. -/// The Attack button's *second* press — `PetActionBarFrame.lua:258-262` calls `PetStopAttack()` -/// instead of `CastPetAction` when the attack is already running. +/// Body of `CMSG_PET_STOP_ATTACK` (opcode 746, vmangos `Packets/Pet.cpp:33-36`): the pet guid, +/// sent by the Attack button's second press (`PetActionBarFrame.lua:258-262`). pub fn pet_stop_attack(pet_guid: u64) -> Vec { pet_guid.to_le_bytes().to_vec() } -/// Body of `CMSG_PET_CANCEL_AURA` (opcode 619): `u64 petGuid` then `u32 spellId`. -/// -/// Both ends agree independently. wow-re read the send off the client -/// (`ui/scratch/pet-action-bar-api.md` §10.1, `0x4bd25f`–`0x4bd2ad`: `push 0x26b`, the guid pair, -/// the id); vmangos reads the same two fields in the same order -/// (`PetCancelAura::ReadFromWorldPacket`, `Server/Packets/Pet.cpp:27-31`). -/// -/// The guid is what distinguishes it from the player's `CMSG_CANCEL_AURA` — which carries a bare -/// spell id and so could only ever mean "mine". The handler re-checks that the guid is our pet or -/// our charm and drops the packet otherwise, and answers a **dead** pet with -/// `SendPetActionFeedback(FEEDBACK_PET_DEAD)` rather than silence (`HandlePetCancelAuraOpcode`, -/// `Handlers/SpellHandler.cpp:407-432`) — the same feedback channel the bar's other refusals use. +/// Body of `CMSG_PET_CANCEL_AURA` (opcode 619, sender `0x4bd25f`): `u64` pet guid, `u32` spell. +/// The server drops it unless the guid is our pet or charm and answers a dead pet with +/// `FEEDBACK_PET_DEAD` (vmangos `SpellHandler.cpp:407-432`). pub fn pet_cancel_aura(pet_guid: u64, spell_id: u32) -> Vec { let mut body = Vec::with_capacity(12); body.extend_from_slice(&pet_guid.to_le_bytes()); @@ -510,20 +328,8 @@ pub fn pet_cancel_aura(pet_guid: u64, spell_id: u32) -> Vec { body } -/// Body of `CMSG_PET_SPELL_AUTOCAST` (opcode 755): `u64 petGuid`, `u32 spellId`, `u8 state` — -/// **thirteen** bytes, and the trailing field is a byte (decision 1032). -/// -/// Both ends agree independently. The client builds it at `0x4bcd5f`–`0x4bcdb2`: `push 0x2f3`, the -/// guid pair through the u64 writer `0x418370`, the spell id through the u32 writer `0x418190`, -/// then `(word >> 30) & 0xFFFFFF01` through **`0x418070`**, the *byte* writer — which is also why -/// that mask ends in `01`. vmangos reads `guid, spellId, state` in that order with `state` a -/// `uint8` (`PetSpellAutocast::ReadFromWorldPacket`, `Server/Packets/Pet.cpp:44-49`). -/// -/// **This is the pet SPELLBOOK's autocast verb, not the pet bar's.** The bar's right click sends -/// [`pet_set_action`], whose body names a slot *position*; the book has no positions, so this one -/// names the spell. The server's handler is correspondingly different — it re-derives which slots -/// to update itself (`HandlePetSpellAutocastOpcode`, `PetHandler.cpp:451-478`), gating on -/// `pet->HasSpell(spellId) && Spells::IsAutocastable(spellId)`. No reply packet. +/// Body of `CMSG_PET_SPELL_AUTOCAST` (opcode 755, sender `0x4bcd5f`): `u64` guid, `u32` spell, +/// `u8` state. The pet spellbook's autocast verb, with no reply; the bar's is [`pet_set_action`]. pub fn pet_spell_autocast(pet_guid: u64, spell_id: u32, enabled: bool) -> Vec { let mut body = Vec::with_capacity(13); body.extend_from_slice(&pet_guid.to_le_bytes()); @@ -532,22 +338,11 @@ pub fn pet_spell_autocast(pet_guid: u64, spell_id: u32, enabled: bool) -> Vec Vec { let mut body = Vec::with_capacity(8 + 8 * entries.len()); body.extend_from_slice(&pet_guid.to_le_bytes()); @@ -558,24 +353,15 @@ pub fn pet_set_action(pet_guid: u64, entries: &[(u32, u32)]) -> Vec { body } -/// Body of `CMSG_PET_ABANDON` (VERIFIED vmangos `PetAbandon::ReadFromWorldPacket`, -/// `Server/Packets/Pet.cpp:16-19`; opcode 374): the lone `u64` pet guid. -/// -/// **The menu's Abandon and its Dismiss are this same message** — see [`super::opcode`]'s note. -/// Nothing in the body says which the player picked, and nothing needs to: the server forks on the -/// pet's own type, deleting a hunter pet and unsummoning anything else. The client's fork is purely -/// about which *word* the menu shows. +/// Body of `CMSG_PET_ABANDON` (opcode 374, vmangos `Packets/Pet.cpp:16-19`): the pet guid. The +/// menu's Abandon and Dismiss both send it; the server deletes a hunter pet and unsummons others. pub fn pet_abandon(pet_guid: u64) -> Vec { pet_guid.to_le_bytes().to_vec() } -/// Body of `CMSG_PET_RENAME` (VERIFIED vmangos `PetRename::ReadFromWorldPacket`, -/// `Server/Packets/Pet.cpp:21-25`; opcode 375): `u64 petGuid` then the name as a **C string**. -/// -/// The name is not length-prefixed and not padded — vmangos's `>> std::string` reads to the first -/// NUL. The 12-character cap is the popup's (`RENAME_PET`'s `maxLetters = 12`), not the wire's; the -/// server re-checks with `ObjectMgr::CheckPetName` and refuses with `SMSG_PET_NAME_INVALID` -/// rather than truncating. +/// Body of `CMSG_PET_RENAME` (opcode 375, vmangos `Packets/Pet.cpp:21-25`): the pet guid, then +/// the name NUL-terminated. The 12-letter cap is the popup's; the server refuses a bad name with +/// `SMSG_PET_NAME_INVALID` rather than truncating. pub fn pet_rename(pet_guid: u64, name: &str) -> Vec { let mut body = Vec::with_capacity(9 + name.len()); body.extend_from_slice(&pet_guid.to_le_bytes()); @@ -588,8 +374,7 @@ pub fn pet_rename(pet_guid: u64, name: &str) -> Vec { mod tests { use super::*; - /// A hunter pet's `SMSG_PET_SPELLS`, byte-for-byte as vmangos builds it: the default - /// 3 commands / 4 spell slots / 3 reactions bar, one known spell, one running cooldown. + /// A hunter pet's `SMSG_PET_SPELLS` as vmangos builds it, with one spell and one cooldown. fn pet_spells_body() -> Vec { let mut b = Vec::new(); b.extend_from_slice(&0xF140_0000_0000_002Au64.to_le_bytes()); // pet guid @@ -598,8 +383,7 @@ mod tests { b.push(PET_COMMAND_FOLLOW as u8); b.push(0); b.push(0); // enabled - // The bar, exactly CharmInfo::InitPetActionBar's default: attack/follow/stay, - // four spell slots (one filled with Claw 3010, autocast on), aggressive/def/passive. + // The bar is CharmInfo::InitPetActionBar's default, Claw 3010 autocasting. for packed in [ PET_COMMAND_ATTACK | (u32::from(PET_ACT_COMMAND) << 24), PET_COMMAND_FOLLOW | (u32::from(PET_ACT_COMMAND) << 24), @@ -616,7 +400,7 @@ mod tests { } b.push(1); // spell count b.extend_from_slice(&(3010u32 | (u32::from(PET_ACT_ENABLED) << 24)).to_le_bytes()); - // The cooldown block in VMANGOS's layout: u16 count, u32 spell id (14-byte entries). + // The cooldown block in vmangos's layout: u16 count, u32 spell id (14-byte entries). b.extend_from_slice(&1u16.to_le_bytes()); b.extend_from_slice(&3010u32.to_le_bytes()); b.extend_from_slice(&0u16.to_le_bytes()); // category @@ -625,8 +409,7 @@ mod tests { b } - /// The same packet with the cooldown block in the **client's** layout: u8 count, u16 spell id - /// (12-byte entries). Both must decode identically — see [`read_cooldown_block`]. + /// The same packet with the cooldown block in the client's layout: u8 count, 12-byte entries. fn pet_spells_body_client_cooldowns() -> Vec { let mut b = pet_spells_body(); b.truncate(b.len() - 16); // drop vmangos's u16 count + its one 14-byte entry @@ -651,22 +434,19 @@ mod tests { assert_eq!(p.command_state(), PET_COMMAND_FOLLOW); assert!(!p.bar_disabled()); - // The three branches, read off the same word shape. assert_eq!(p.bar[0].kind(), PET_ACT_COMMAND); assert_eq!(p.bar[0].action(), PET_COMMAND_ATTACK); assert!(!p.bar[0].is_spell()); assert_eq!(p.bar[9].kind(), PET_ACT_REACTION); assert_eq!(p.bar[9].action(), PET_REACT_PASSIVE); - // The mask at work: the server sent 0xC1, and the client's type is 1 — the autocast pair - // having moved out of the type byte into bits 31/30. + // The server sent 0xC1; the client's type is 1, with autocast in bits 31/30. assert_eq!(p.bar[3].kind(), PET_TYPE_SPELL_FIRST); assert_eq!(p.bar[3].action(), 3010); assert!(p.bar[3].is_spell() && !p.bar[3].is_empty()); assert!(p.bar[3].autocast_allowed() && p.bar[3].autocast_on()); - // vmangos's unused middle slot (0, ACT_DISABLED) is NOT the client's empty word — it takes - // the spell branch, where spell id 0 resolves to nothing. Same button, different route. + // vmangos's unused slot (0, ACT_DISABLED) is not empty; it takes the spell branch. assert!(!p.bar[4].is_empty()); assert!(p.bar[4].is_spell() && p.bar[4].action() == 0); assert!( @@ -687,8 +467,6 @@ mod tests { ); } - /// The cooldown block's two layouts decode to the same thing — the client's 12-byte form and - /// vmangos's 14-byte one, told apart by the tail length alone. #[test] fn both_cooldown_layouts_decode_identically() { let from_vmangos = read_pet_spells(&mut &pet_spells_body()[..]).unwrap(); @@ -700,9 +478,7 @@ mod tests { assert_eq!(from_client, from_vmangos); } - /// A pet with no cooldowns at all: the client's form ends the packet, vmangos's leaves its - /// count's high byte behind. Neither is an error and neither invents an entry — and the - /// high byte is consumed, so neither leaves a tail. + /// No cooldowns: the client's form is one byte, vmangos's two, and both are fully consumed. #[test] fn an_empty_cooldown_block_reads_either_way() { let mut client = &[0u8][..]; @@ -716,8 +492,7 @@ mod tests { ); } - /// The teardown: an 8-byte body of zero guid ⇒ a default value whose `pet_guid` is 0. This is - /// the ONLY signal that the bar has gone away, so it must not read as a parse error. + /// A zero-guid body is the only signal that the bar is gone, so it must not be an error. #[test] fn the_guid_only_body_is_the_teardown() { let body = 0u64.to_le_bytes(); @@ -727,8 +502,7 @@ mod tests { assert_eq!(p.pet_guid, 0); } - /// A truncated bar is a parse error, not a half-read bar — the failure mode that would - /// otherwise paint 4 real slots and 6 zeroes. + /// Cut after the first bar slot: an error, not a half-read bar. #[test] fn a_truncated_body_errors() { let body = pet_spells_body(); @@ -738,8 +512,7 @@ mod tests { #[test] fn pet_mode_and_the_client_verbs_golden() { - // SMSG_PET_MODE: guid + the same state dword, no bar behind it. vmangos's fourth byte 0x8 - // IS the client's bit 27 — the two ends of the same bit. + // SMSG_PET_MODE: vmangos's fourth state byte 0x8 is the client's bit 27. let mut body = 0x2Au64.to_le_bytes().to_vec(); body.extend_from_slice(&[2, 0, 0, 0x8]); let mut r = &body[..]; @@ -748,7 +521,6 @@ mod tests { assert_eq!(mode.state, 0x0800_0002); assert_eq!(mode.state & PET_STATE_BAR_DISABLED, PET_STATE_BAR_DISABLED); - // CMSG_PET_ACTION: guid, the slot word VERBATIM, target guid. let packed = 3010 | (u32::from(PET_ACT_ENABLED) << 24); assert_eq!( pet_action(0x2A, packed, 0x99), @@ -760,11 +532,8 @@ mod tests { .concat() ); - // CMSG_PET_STOP_ATTACK: the lone guid. assert_eq!(pet_stop_attack(0x2A), 0x2Au64.to_le_bytes()); - // CMSG_PET_CANCEL_AURA: guid then spell id, 12 bytes — the guid is the whole difference - // from the player's own CMSG_CANCEL_AURA, which is a bare u32. assert_eq!( pet_cancel_aura(0x2A, 2645), [ @@ -775,17 +544,13 @@ mod tests { ); assert_eq!(pet_cancel_aura(0x2A, 2645).len(), 12); - // CMSG_PET_SET_ACTION: the server tells one entry from two BY BODY SIZE, so the one-entry - // form is 16 bytes and the two-entry form is exactly 24. + // CMSG_PET_SET_ACTION: the server counts entries by body size, so 16 bytes or exactly 24. assert_eq!(pet_set_action(0x2A, &[(3, packed)]).len(), 16); assert_eq!(pet_set_action(0x2A, &[(3, packed), (4, 0)]).len(), 24); - // CMSG_PET_ABANDON: the lone guid — the same shape as stop-attack, a different verb. assert_eq!(pet_abandon(0x2A), 0x2Au64.to_le_bytes()); - // CMSG_PET_RENAME: guid then a NUL-TERMINATED name. The terminator is the whole framing — - // there is no length prefix and no padding, so dropping it would leave the server reading - // the name off the end of the body. + // CMSG_PET_RENAME: guid then a NUL-terminated name, the terminator being its only framing. assert_eq!( pet_rename(0x2A, "Bruce"), [&0x2Au64.to_le_bytes()[..], b"Bruce\0"].concat() @@ -797,8 +562,6 @@ mod tests { ); } - /// The autocast flip touches **bit 30 and nothing else** — the client's own in-place edit, and - /// the word it produces is exactly what `CMSG_PET_SET_ACTION` then carries. #[test] fn the_autocast_flip_moves_only_bit_30() { let on = PetActionEntry::from(3010 | (u32::from(PET_ACT_ENABLED) << 24)); @@ -811,14 +574,11 @@ mod tests { assert!(off.autocast_allowed() && !off.autocast_on()); assert_eq!(off.with_autocast(true), on); - // A passive spell can be asked to autocast and the word still says it may not — bit 31 is - // the server's to grant, and flipping 30 does not forge it. + // Bit 31 is the server's to grant; flipping bit 30 on a passive spell does not forge it. let passive = PetActionEntry::from(3010 | (u32::from(PET_ACT_PASSIVE) << 24)); assert!(!passive.with_autocast(true).autocast_allowed()); } - /// The `& 0x3F` mask, stated on its own: the server's five `ActiveStates` land on three client - /// branches, and the two dropped bits reappear as the autocast pair. #[test] fn the_type_mask_collapses_the_spell_states() { for act in [PET_ACT_PASSIVE, PET_ACT_DISABLED, PET_ACT_ENABLED] { @@ -836,17 +596,15 @@ mod tests { PET_ACT_REACTION ); - // A type outside 1-7 is the client's default arm; we treat it as nothing at all rather - // than reproduce its under-push (wow-re §2.5 quirk 1). + // A type outside 1-7 is neither a spell nor a token. let odd = PetActionEntry::from(3010 | (0x33u32 << 24)); assert!(!odd.is_spell()); assert!(odd.kind() != PET_ACT_COMMAND && odd.kind() != PET_ACT_REACTION); } } -/// `SMSG_PET_UNLEARN_CONFIRM` (VERIFIED at the bytes, wow-re `staticpopup-dialog-bindings.md` -/// §9, arm `0x5e4a26`): the pet trainer's guid and what unlearning costs, in copper — the -/// reference latches both and fires `CONFIRM_PET_UNLEARN(cost)`. Decision 1963. +/// `SMSG_PET_UNLEARN_CONFIRM` (`0x5e4a26`): the trainer's guid and the unlearn cost in copper, +/// which the reference latches before firing `CONFIRM_PET_UNLEARN(cost)`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PetUnlearnConfirm { pub trainer: u64, @@ -863,7 +621,7 @@ pub(super) fn read_pet_unlearn_confirm( }) } -/// Body of `CMSG_PET_UNLEARN` (VERIFIED, §9 `0x5dfba0`'s confirm arm): the latched trainer guid. +/// Body of `CMSG_PET_UNLEARN` (the confirm arm of `0x5dfba0`): the latched trainer guid. pub fn pet_unlearn(trainer: u64) -> Vec { trainer.to_le_bytes().to_vec() } diff --git a/crates/benilla-protocol/src/messages/petition.rs b/crates/benilla-protocol/src/messages/petition.rs index 595b94d7..c46f24ef 100644 --- a/crates/benilla-protocol/src/messages/petition.rs +++ b/crates/benilla-protocol/src/messages/petition.rs @@ -1,44 +1,8 @@ -//! The petition family — the guild-charter flow that *founds* a guild (opcodes `0x1BB`–`0x1C7` -//! plus `0x2C1`). -//! -//! [`super::guild`] is about *being* in a guild; this is about *making* one. At 1.12 those are -//! entirely different wires: `CMSG_GUILD_CREATE` is registered `STATUS_NEVER` on vmangos, and the -//! only path to a new guild is buy a charter → collect nine signatures → turn it in. -//! -//! The server side is pinned at the bytes by vmangos `Server/Packets/Petition.{h,cpp}` (the -//! `ReadFromWorldPacket` / `AppendBodyTo` pairs) and `Handlers/PetitionsHandler.cpp` (which -//! packets each handler answers with, and the order of its refusals). **Two of the five -//! server→client packets are not built through the packet classes at all** — -//! `SMSG_PETITION_SHOW_SIGNATURES` is hand-assembled at `PetitionsHandler.cpp:160-168` and -//! `:390-397` — so their layout is read off the handler, not off `Petition.cpp`. -//! -//! Four shapes of this family are worth knowing before touching any of it: -//! -//! - **The charter is an ITEM, and the petition id rides in its enchantment slot.** Buying stores -//! item [`CHARTER_ITEM_ENTRY`] and writes the petition id into `ITEM_FIELD_ENCHANTMENT` slot 0's -//! `id` dword (`PetitionsHandler.cpp:126`). That is not a hack the client is unaware of: the -//! reference's item tooltip **forces enchantment slot 0 to zero and skips the enchant lines -//! entirely** for any item whose template flags carry [`ITEM_FLAG_CHARTER`] -//! (`test ah,0x20 / jne 0x52c9ee`, wow-re `system/ui/scratch/tooltip-content-law.md:485-505`). -//! So slot 0 on a charter is a petition id by contract on both ends, and nothing may read it as -//! a `SpellItemEnchantment.dbc` row. -//! - **Almost nothing is acked positively.** A successful buy sends **no confirmation packet** — -//! the client learns of it only from the `SMSG_ITEM_PUSH_RESULT` for the new item -//! (`PetitionsHandler.cpp:130`). Rename echoes only on success (`:209-215`). The refusals come -//! back as `SMSG_GUILD_COMMAND_RESULT`, the *guild* family's error channel, reused here. -//! - **`SMSG_PETITION_SHOW_SIGNATURES` is the answer to two different questions.** It answers our -//! own `CMSG_PETITION_SHOW_SIGNATURES` (`:160-168`), and it is also what a *third party* is sent -//! when someone offers us their charter (`:390-397`, the `CMSG_OFFER_PETITION` path). One -//! inbound packet, two meanings, distinguished only by whether its `owner` is us. -//! - **The signature limit is enforced three times with three different numbers.** vmangos's -//! `MinPetitionSigns` config (clamped 0..=9, `World.cpp:665`) decides when a petition -//! `IsComplete()` — with `==`, not `>=` (`GuildMgr.h:124`); the sign handler *separately* -//! hard-caps at nine with the comment "Client hard limit at 9 signatures" -//! (`PetitionsHandler.cpp:269-271`); and `SMSG_PETITION_QUERY_RESPONSE` reports min = max = 9 -//! hardcoded (`:182-183`), regardless of the config. [`MAX_PETITION_SIGNATURES`] is that client -//! limit; a consumer wanting "how many does this petition need" reads the wire's -//! `max_signatures`, not the constant, because on a server with `MinPetitionSigns` lowered the -//! two disagree. +//! The petition family (opcodes `0x1BB`-`0x1C7`, `0x2C1`): the guild-charter flow. vmangos +//! registers `CMSG_GUILD_CREATE` as `STATUS_NEVER`, so a guild is founded only by buying a +//! charter, collecting nine signatures and turning it in. The charter item's enchantment slot 0 +//! holds the petition id (`PetitionsHandler.cpp:126`); the reference tooltip skips enchant lines +//! on a charter (`0x52c9ee`), so nothing may read that slot as an enchantment. use std::io::{self, Read}; @@ -46,111 +10,76 @@ use crate::wire::{ capacity_hint, read_cstring, read_i32_le, read_u16_le, read_u32_le, read_u64_le, read_u8, }; -/// The one item that is a guild charter — vmangos `GUILD_CHARTER` (`PetitionsHandler.cpp:37`), -/// also `Item::IsCharter`'s literal (`Item.h:158`, `GetEntry() == 5863u`). Confirmed against this -/// deployment's world DB: entry 5863 "Guild Charter", display 16161, flags `0x2000`. +/// The guild charter's item entry, vmangos `GUILD_CHARTER` (`PetitionsHandler.cpp:37`). pub const CHARTER_ITEM_ENTRY: u32 = 5863; -/// The charter's inventory display id — vmangos `CHARTER_DISPLAY_ID` (`PetitionsHandler.cpp:38`). -/// It rides in [`PetitionShowListEntry::charter_display_id`] as well as in the item template, and -/// the two agree; the packet's copy exists so a vendor list can be drawn before the item is owned. +/// The charter's display id, vmangos `CHARTER_DISPLAY_ID` (`PetitionsHandler.cpp:38`). pub const CHARTER_DISPLAY_ID: u32 = 16161; -/// `ITEM_FLAG_CHARTER` — the template-flag bit that makes an item a signable petition -/// (vmangos `ItemPrototype.h:77`). **The reference client keys on this same bit** to print the -/// green `ITEM_SIGNABLE` tooltip line and to suppress the enchantment lines -/// (wow-re `system/ui/scratch/tooltip-content-law.md:485-505`), so it is a shared contract rather -/// than a server-side convenience. +/// The template-flag bit of a signable petition (vmangos `ItemPrototype.h:77`); the reference +/// tooltip keys on it for the green `ITEM_SIGNABLE` line and to suppress enchantment lines. pub const ITEM_FLAG_CHARTER: u32 = 0x0000_2000; -/// The most signatures a 1.12 charter can hold — vmangos's own "Client hard limit at 9 -/// signatures" (`PetitionsHandler.cpp:269-271`), and the reference FrameXML's -/// `MAX_PETITION_SIGNATURES = 9` (`PetitionFrame.lua:1`), which sizes the window's name rows. -/// -/// This is the **row count**, not the requirement: how many a petition needs to be turned in is -/// [`PetitionQueryResponse::min_signatures`] off the wire, which a server may configure lower. +/// The most signatures a charter holds (`PetitionFrame.lua:1`, `PetitionsHandler.cpp:269`); the +/// requirement is [`PetitionQueryResponse::min_signatures`]. pub const MAX_PETITION_SIGNATURES: usize = 9; -/// Charter-name cap, in *characters* — vmangos `MAX_CHARTER_NAME` (`ObjectMgr.h:401`), enforced by -/// `ObjectMgr::IsValidCharterName` (`ObjectMgr.cpp:9322-9334`) as a UTF-8 length. The reference's -/// own edit boxes agree: `GuildRegistrarFrameEditBox letters="24"` and the `RENAME_GUILD` popup's -/// `maxLetters = 24`. Same number as [`super::GUILD_NAME_MAX_LENGTH`], and not a coincidence — a -/// charter name becomes a guild name. +/// Charter-name cap in characters (vmangos `MAX_CHARTER_NAME`, `ObjectMgr.h:401`); equal to +/// [`super::GUILD_NAME_MAX_LENGTH`] because the charter name becomes the guild's. pub const CHARTER_NAME_MAX_LENGTH: usize = 24; -/// `SMSG_PETITION_SIGN_RESULTS`' and `SMSG_TURN_IN_PETITION_RESULTS`' shared result code — -/// vmangos `PetitionSigns` (`Guild/Guild.h:147-154`). One enum, two packets, and the two use -/// **disjoint subsets** of it, which is why the arms are documented by packet. +/// The result code shared by `SMSG_PETITION_SIGN_RESULTS` and `SMSG_TURN_IN_PETITION_RESULTS` +/// (vmangos `PetitionSigns`, `Guild.h:147-154`); each packet uses its own subset. pub mod petition_result { - /// Signed, or turned in. Used by both packets. + /// Signed, or turned in; both packets. pub const OK: u32 = 0; - /// Sign only — this account has already signed this charter. vmangos checks uniqueness **per - /// account first**, then per character (`GuildMgr.cpp:388-400`), so a second character on one - /// account gets this, not a second signature. + /// Sign only: this account already signed. vmangos checks per account before per character + /// (`GuildMgr.cpp:388-400`), so an alt on the same account gets this. pub const ALREADY_SIGNED: u32 = 1; - /// Turn-in only — the would-be founder is already in a guild (`PetitionsHandler.cpp:427`). - /// Never sent by the sign path, which answers an already-guilded *signer* with an - /// `SMSG_GUILD_COMMAND_RESULT` instead (`:258`). + /// Turn-in only: the founder is already in a guild (`PetitionsHandler.cpp:427`); a guilded + /// signer gets `SMSG_GUILD_COMMAND_RESULT` instead (`:258`). pub const ALREADY_IN_GUILD: u32 = 2; - /// Sign only — you cannot sign your own charter (`PetitionsHandler.cpp:237`). + /// Sign only: you cannot sign your own charter (`PetitionsHandler.cpp:237`). pub const CANT_SIGN_OWN: u32 = 3; - /// Turn-in only — not enough signatures yet (`PetitionsHandler.cpp:438`). + /// Turn-in only: not enough signatures (`PetitionsHandler.cpp:438`); vmangos requires exactly + /// `MinPetitionSigns` (`GuildMgr.h:124`). pub const NEED_MORE: u32 = 4; - /// Not on the same realm. vmangos never sends it (cross-realm charters cannot arise on a - /// single-realm server); modelled because the reference has a string for it - /// (`ERR_PETITION_NOT_SAME_SERVER`) and the client's switch must have an arm. + /// Not on the same realm (`ERR_PETITION_NOT_SAME_SERVER`); vmangos never sends it. pub const NOT_SERVER: u32 = 5; } -/// One row of `SMSG_PETITION_SHOWLIST` — a charter a petitioner NPC will sell. -/// -/// vmangos always sends exactly one of these, and its own header says **"only 1 element is -/// supported in the client"** (`Petition.h:176`). We parse the counted list faithfully anyway: a -/// count is a count, and a reader that assumes one would desynchronise rather than degrade if it -/// were ever wrong. +/// One row of `SMSG_PETITION_SHOWLIST`: a charter the NPC sells. vmangos always sends exactly one +/// (`Petition.h:176`: "only 1 element is supported in the client"). #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PetitionShowListEntry { - /// Row index, 1-based (`PetitionsHandler.cpp:501`, a literal `1`). + /// Row index, 1-based (`PetitionsHandler.cpp:501`). pub index: u32, - /// The item entry sold — [`CHARTER_ITEM_ENTRY`] in practice. + /// The item entry sold, [`CHARTER_ITEM_ENTRY`] in practice. pub charter_entry: u32, - /// Its display id — [`CHARTER_DISPLAY_ID`] in practice. + /// Its display id, [`CHARTER_DISPLAY_ID`] in practice. pub charter_display_id: u32, - /// Price in **copper** (1000 = 10 silver on vmangos, `PetitionsHandler.cpp:39`). Signed on the - /// wire (`int32`), which is why it is signed here: a reader that narrows it to `u32` would be - /// asserting a range the sender does not. + /// Price in copper, an `int32` on the wire (vmangos charges 1000, `PetitionsHandler.cpp:39`). pub charter_cost: i32, - /// vmangos sends `1` and its header says the row **"must be `&1` to show it in the UI"** - /// (`Petition.h:169`). Kept raw rather than folded into a `bool`: that claim is a *comment* - /// about the reference client, not something vmangos itself enforces, and a consumer that - /// wants the filter can apply it knowingly. + /// vmangos sends 1; its header says a row "must be `&1` to show it in the UI" + /// (`Petition.h:169`). pub entry_flags: i32, } -/// `SMSG_PETITION_SHOWLIST` — a petitioner NPC's "here is what I sell" list. In 1.12 that is -/// always the single guild charter, and this packet is what puts the guild registrar's window on -/// screen. +/// `SMSG_PETITION_SHOWLIST`: the registrar NPC's charter list, which opens its window. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PetitionShowList { - /// The NPC we are talking to. Every later verb in the flow that names an NPC uses this guid. + /// The NPC; every later verb in the flow that names an NPC takes this guid. pub npc: u64, - /// The rows, in wire order. Normally exactly one; see [`PetitionShowListEntry`]. pub entries: Vec, } -/// Read `SMSG_PETITION_SHOWLIST` (VERIFIED vmangos `Server/Packets/Petition.cpp:115-127` -/// `PetitionShowList::AppendBodyTo`, filled at `Handlers/PetitionsHandler.cpp:482-507`): -/// `u64 npcGuid`, `u8 count`, then per row `u32 index`, `u32 charterEntry`, `u32 charterDisplayId`, -/// `i32 charterCost`, `i32 entryFlags`. -/// -/// The send is gated on `GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_PETITIONER)` -/// (`:484`; the flag is `0x200`, `Objects/UnitDefines.h:666`), so a list arriving at all is the -/// server's confirmation that we are in range of a real registrar. +/// Read `SMSG_PETITION_SHOWLIST` (vmangos `Packets/Petition.cpp:115-127`): `u64 npc, u8 count`, +/// then per row `u32 index, u32 entry, u32 displayId, i32 cost, i32 flags`. The request handler +/// sends it only for an NPC in range with `UNIT_NPC_FLAG_PETITIONER` (`PetitionsHandler.cpp:484`). pub(super) fn read_petition_show_list(r: &mut impl Read) -> io::Result { let npc = read_u64_le(r)?; let count = read_u8(r)?; - // vmangos sends exactly one charter (`PetitionsHandler.cpp:503`, `entries = { entry }`); - // 8 is generous. + // vmangos sends exactly one charter (`PetitionsHandler.cpp:503`); 8 is generous. let mut entries = Vec::with_capacity(capacity_hint(count, 8)); for _ in 0..count { entries.push(PetitionShowListEntry { @@ -164,48 +93,30 @@ pub(super) fn read_petition_show_list(r: &mut impl Read) -> io::Result, } -/// Read `SMSG_PETITION_SHOW_SIGNATURES` (VERIFIED vmangos `Handlers/PetitionsHandler.cpp:160-168` -/// and `:390-397`, which build it **by hand** rather than through a packet class): -/// `u64 itemGuid`, `u64 ownerGuid`, `u32 petitionId`, `u8 signatureCount`, then per signature -/// `u64 signerGuid` + `u32 0`. -/// -/// The 12-byte stride is corroborated by the sender's own size hint `8+8+4+1 + signs*12` and by -/// `Guild/GuildMgr.cpp:358-366` (`Petition::BuildSignatureData`). +/// Read `SMSG_PETITION_SHOW_SIGNATURES` (hand-built at vmangos `PetitionsHandler.cpp:160-168`, +/// `:390-397`): `u64 item, u64 owner, u32 petitionId, u8 count`, then `u64 signer, u32 0` each. pub(super) fn read_petition_show_signatures( r: &mut impl Read, ) -> io::Result { @@ -229,24 +140,19 @@ pub(super) fn read_petition_show_signatures( }) } -/// `SMSG_PETITION_SIGN_RESULTS` — the verdict on one signature attempt. -/// -/// **It is sent to both parties on success**: once to the signer (`PetitionsHandler.cpp:299`) and -/// again, identical, to the charter's owner if they are online (`:312-316`). Both copies name the -/// *signer* in [`Self::player`], so a receiver cannot tell the two apart from this packet alone — -/// only from whether it holds that charter. +/// `SMSG_PETITION_SIGN_RESULTS`: the verdict on one signature. A success goes, identical, to the +/// signer and the online owner (`PetitionsHandler.cpp:299`, `:312-316`); both name the signer. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PetitionSignResults { /// The charter item's guid. pub item: u64, - /// The **signer's** guid — in both copies of the packet, per the doc above. + /// The signer's guid, in both copies. pub player: u64, /// A [`petition_result`] code. pub result: u32, } -/// Read `SMSG_PETITION_SIGN_RESULTS` (VERIFIED vmangos `Server/Packets/Petition.cpp:69-74` -/// `PetitionSignResults::AppendBodyTo`): `u64 itemGuid`, `u64 playerGuid`, `u32 result`. +/// Read `SMSG_PETITION_SIGN_RESULTS` (vmangos `Packets/Petition.cpp:69-74`). pub(super) fn read_petition_sign_results(r: &mut impl Read) -> io::Result { Ok(PetitionSignResults { item: read_u64_le(r)?, @@ -255,64 +161,48 @@ pub(super) fn read_petition_sign_results(r: &mut impl Read) -> io::Result, - /// Which choice is pre-selected — `0` on vmangos. + /// The pre-selected choice, 0 on vmangos. pub default_choice: u32, } -/// Read `SMSG_PETITION_QUERY_RESPONSE` (VERIFIED vmangos `Server/Packets/Petition.cpp:81-102` -/// `PetitionQueryResponse::AppendBodyTo`, filled at `Handlers/PetitionsHandler.cpp:177-184`): -/// `u32 petitionId`, `u64 ownerGuid`, cstring name, cstring bodyText, `u32 flags`, -/// `u32 minSignatures`, `u32 maxSignatures`, `u32 deadline`, `u32 creation`, `u32 allowedGuildID`, -/// `u32 allowedClasses`, `u32 allowedRaces`, **`u16 allowedGender`**, `u32 allowedMinLevel`, -/// `u32 allowedMaxLevel`, `u32 choiceCount`, `choiceCount ×` cstring, `u32 defaultChoice`. -/// -/// The `u16` in the middle is the trap: see [`PetitionQueryResponse::allowed_gender`]. +/// Read `SMSG_PETITION_QUERY_RESPONSE` (vmangos `Packets/Petition.cpp:81-102`): the fields in +/// struct order, the choices as a `u32` count of C strings. pub(super) fn read_petition_query_response(r: &mut impl Read) -> io::Result { let petition_id = read_u32_le(r)?; let owner = read_u64_le(r)?; @@ -355,22 +245,16 @@ pub(super) fn read_petition_query_response(r: &mut impl Read) -> io::Result io::Result { Ok(PetitionRename { item: read_u64_le(r)?, @@ -378,56 +262,29 @@ pub(super) fn read_petition_rename(r: &mut impl Read) -> io::Result io::Result { read_u32_le(r) } -/// Read `MSG_PETITION_DECLINE`'s **inbound** form (VERIFIED vmangos -/// `Server/Packets/Petition.cpp:110-113` `PetitionDecline::AppendBodyTo`): one `u64`, the guid of -/// the player who declined. Delivered only to the charter's owner -/// (`Handlers/PetitionsHandler.cpp:321-338`). -/// -/// The outbound form of the same opcode carries the *item* guid instead -/// ([`petition_decline`]) — one opcode, two different bodies by direction, which is what `MSG_` -/// means here. +/// Read `MSG_PETITION_DECLINE` inbound (vmangos `Packets/Petition.cpp:110-113`): the decliner's +/// guid, sent to the owner only. The outbound form carries the item guid instead. pub(super) fn read_petition_decline(r: &mut impl Read) -> io::Result { read_u64_le(r) } -/// Body of `CMSG_PETITION_SHOWLIST` (VERIFIED vmangos `Server/Packets/Petition.cpp:3-6` -/// `PetitionShowListRequest::ReadFromWorldPacket`): one `u64`, the petitioner NPC's guid. -/// -/// Answered by `SMSG_PETITION_SHOWLIST`, or by nothing at all if the NPC is out of range or lacks -/// `UNIT_NPC_FLAG_PETITIONER` (`Handlers/PetitionsHandler.cpp:484`). The server also sends that -/// answer unasked when a `GOSSIP_OPTION_PETITIONER` row is selected (`Player.cpp:12428-12431`), so -/// this request is the *second* way in, not the only one. +/// Body of `CMSG_PETITION_SHOWLIST` (vmangos `Packets/Petition.cpp:3-6`): the NPC's guid. The +/// server also sends the list unasked for a `GOSSIP_OPTION_PETITIONER` row (`Player.cpp:12428`). pub fn petition_show_list(npc: u64) -> Vec { npc.to_le_bytes().to_vec() } -/// Body of `CMSG_PETITION_BUY` (VERIFIED vmangos `Server/Packets/Petition.cpp:47-67` -/// `PetitionBuy::ReadFromWorldPacket`) — **72 bytes, of which the server reads exactly two -/// fields**: the NPC guid at offset 0 and the name at offset 20. Everything else is skipped. -/// -/// The shape is a generic multi-choice petition struct that never shipped: `u64 npcGuid`, -/// `u32 0`, `u64 0`, cstring name, `10 × u32 0`, `u16 0`, `u8 0`, `u32 index`, `u32 0`. We send -/// zeros for every skipped field — including `index`, which the server's own comment calls -/// "unused" — because a field nobody reads has no correct non-zero value, and a zero is the one -/// choice that cannot encode an accident. -/// -/// The refusal ladder is worth knowing, because none of it is a petition packet: an already-taken -/// or invalid name answers `SMSG_GUILD_COMMAND_RESULT(GUILD_CREATE_S, name, …)` -/// (`Handlers/PetitionsHandler.cpp:78`, `:83`, `:93`), too little money answers `SMSG_BUY_FAILED` -/// (`:108`), a full bag answers `SMSG_INVENTORY_CHANGE_FAILURE` (`:116`) — and **success answers -/// nothing but the new item** (`:130`). +/// Body of `CMSG_PETITION_BUY` (vmangos `Packets/Petition.cpp:47-67`): `u64 npc, u32 0, u64 0`, +/// the name, `u32 0` x10, `u16 0, u8 0, u32 index, u32 0`. The server reads only NPC and name. +/// A bad name gets `SMSG_GUILD_COMMAND_RESULT`, no money `SMSG_BUY_FAILED`, a full bag +/// `SMSG_INVENTORY_CHANGE_FAILURE`; success is only the item push (`PetitionsHandler.cpp:78-130`). pub fn petition_buy(npc: u64, name: &str) -> Vec { let mut body = Vec::with_capacity(72 + name.len()); body.extend_from_slice(&npc.to_le_bytes()); @@ -437,30 +294,19 @@ pub fn petition_buy(npc: u64, name: &str) -> Vec { body.extend_from_slice(&[0u8; 40]); // 10 × u32 body.extend_from_slice(&0u16.to_le_bytes()); body.push(0); - body.extend_from_slice(&0u32.to_le_bytes()); // index — the server's own "unused" + body.extend_from_slice(&0u32.to_le_bytes()); // index, "unused" per the server body.extend_from_slice(&0u32.to_le_bytes()); body } -/// Body of `CMSG_PETITION_SHOW_SIGNATURES` (VERIFIED vmangos `Server/Packets/Petition.cpp:8-11` -/// `PetitionShowSignaturesRequest::ReadFromWorldPacket`): one `u64`, the charter **item's** guid. -/// -/// Answered with `SMSG_PETITION_SHOW_SIGNATURES` — or silently dropped if we are already in a -/// guild (`Handlers/PetitionsHandler.cpp:140`) or do not hold that item (`:143`). +/// Body of `CMSG_PETITION_SHOW_SIGNATURES` (vmangos `Packets/Petition.cpp:8-11`): the charter +/// item's guid, unanswered if we are guilded or lack the item (`PetitionsHandler.cpp:140-143`). pub fn petition_show_signatures(item: u64) -> Vec { item.to_le_bytes().to_vec() } -/// Body of `CMSG_PETITION_SIGN` (VERIFIED vmangos `Server/Packets/Petition.cpp:35-39` -/// `PetitionSign::ReadFromWorldPacket`): `u64 itemGuid` then one `i8` the server **skips**, whose -/// own vmangos comment is *"argument of `/run SignPetition(123)` is never used in the official -/// interface"*. -/// -/// **The client's default for that byte is `1`, not `0`** — `SignPetition`'s optional Lua argument -/// initialises `edi = 1` at `0x4f46d9` and the byte goes out through `Put8` at `0x4f4749` (wow-re -/// `system/ui/scratch/petition-charter-api.md`). Since the server reads and discards it, **nothing -/// observable depends on the value**, which is exactly why it shipped here as `0` and why only a -/// golden can tell the difference. +/// Body of `CMSG_PETITION_SIGN` (vmangos `Packets/Petition.cpp:35-39`): the item guid, then an +/// `i8` the server skips. `SignPetition` defaults it to 1 (`0x4f46d9`, sent at `0x4f4749`). pub fn petition_sign(item: u64, arg: i8) -> Vec { let mut body = Vec::with_capacity(9); body.extend_from_slice(&item.to_le_bytes()); @@ -468,13 +314,9 @@ pub fn petition_sign(item: u64, arg: i8) -> Vec { body } -/// Body of `CMSG_OFFER_PETITION` (VERIFIED vmangos `Server/Packets/Petition.cpp:41-45` -/// `OfferPetition::ReadFromWorldPacket`): `u64 itemGuid`, `u64 playerGuid` — the charter, and who -/// to show it to. -/// -/// On success the server sends `SMSG_PETITION_SHOW_SIGNATURES` **to the target**, not back to us -/// (`Handlers/PetitionsHandler.cpp:390-397`); we hear only about refusals, as -/// `SMSG_GUILD_COMMAND_RESULT`. +/// Body of `CMSG_OFFER_PETITION` (vmangos `Packets/Petition.cpp:41-45`): item guid, target guid. +/// Success sends the target `SMSG_PETITION_SHOW_SIGNATURES` (`PetitionsHandler.cpp:390-397`); we +/// hear only refusals, as `SMSG_GUILD_COMMAND_RESULT`. pub fn offer_petition(item: u64, player: u64) -> Vec { let mut body = Vec::with_capacity(16); body.extend_from_slice(&item.to_le_bytes()); @@ -482,24 +324,15 @@ pub fn offer_petition(item: u64, player: u64) -> Vec { body } -/// Body of `CMSG_TURN_IN_PETITION` (VERIFIED vmangos `Server/Packets/Petition.cpp:24-27` -/// `TurnInPetition::ReadFromWorldPacket`): one `u64`, the charter item's guid. -/// -/// Only the petition's **owner** may turn it in; anyone else is refused silently -/// (`Handlers/PetitionsHandler.cpp:432`). On success the guild is created with every signer as a -/// member at the lowest rank, the petition row is deleted, and *then* the item is destroyed — in -/// that order, because destroying a charter cascades into deleting its petition -/// (`Player.cpp:10811-10817`). +/// Body of `CMSG_TURN_IN_PETITION` (vmangos `Packets/Petition.cpp:24-27`): the charter item's +/// guid. Only the owner may turn it in; anyone else gets silence (`PetitionsHandler.cpp:432`). +/// Success founds the guild with every signer at the lowest rank. pub fn turn_in_petition(item: u64) -> Vec { item.to_le_bytes().to_vec() } -/// Body of `CMSG_PETITION_QUERY` (VERIFIED vmangos `Server/Packets/Petition.cpp:13-17` -/// `PetitionQuery::ReadFromWorldPacket`): `u32 petitionId`, `u64 itemGuid`. -/// -/// The item guid is **read and then ignored** by the handler (`Handlers/PetitionsHandler.cpp:171-185`), -/// which looks the petition up by id alone. Sent anyway, at its true value: a field the current -/// server does not use is not a field to fill with garbage. +/// Body of `CMSG_PETITION_QUERY` (vmangos `Packets/Petition.cpp:13-17`): `u32 petitionId`, +/// `u64 item`; the server looks up by id alone (`PetitionsHandler.cpp:171-185`). pub fn petition_query(petition_id: u32, item: u64) -> Vec { let mut body = Vec::with_capacity(12); body.extend_from_slice(&petition_id.to_le_bytes()); @@ -507,14 +340,8 @@ pub fn petition_query(petition_id: u32, item: u64) -> Vec { body } -/// Body of `MSG_PETITION_RENAME`'s **outbound** form (VERIFIED vmangos -/// `Server/Packets/Petition.cpp:29-33` `PetitionRename::ReadFromWorldPacket`): `u64 itemGuid`, -/// cstring newName. -/// -/// Echoed back as the same two fields on success ([`read_petition_rename`]); rejected names come -/// back as `SMSG_GUILD_COMMAND_RESULT` and no echo. The name is capped at -/// [`CHARTER_NAME_MAX_LENGTH`] characters by the server's own validator — not truncated here, for -/// the reason [`super::guild`]'s caps give: silently shortening sends the wrong text. +/// Body of `MSG_PETITION_RENAME` outbound (vmangos `Packets/Petition.cpp:29-33`): `u64` item, +/// cstring name. Not truncated to [`CHARTER_NAME_MAX_LENGTH`]; the server validates it. pub fn petition_rename(item: u64, name: &str) -> Vec { let mut body = Vec::with_capacity(9 + name.len()); body.extend_from_slice(&item.to_le_bytes()); @@ -522,15 +349,12 @@ pub fn petition_rename(item: u64, name: &str) -> Vec { body } -/// Body of `MSG_PETITION_DECLINE`'s **outbound** form (VERIFIED vmangos -/// `Server/Packets/Petition.cpp:19-22` `PetitionDecline::ReadFromWorldPacket`): one `u64`, the -/// charter item's guid. The server forwards our guid to the charter's owner -/// ([`read_petition_decline`]). +/// Body of `MSG_PETITION_DECLINE` outbound (vmangos `Packets/Petition.cpp:19-22`): the charter +/// item's guid; the server forwards our guid to the owner. pub fn petition_decline(item: u64) -> Vec { item.to_le_bytes().to_vec() } -/// Append a NUL-terminated string to `body`. fn push_cstring(body: &mut Vec, s: &str) { body.extend_from_slice(s.as_bytes()); body.push(0); @@ -540,9 +364,7 @@ fn push_cstring(body: &mut Vec, s: &str) { mod tests { use super::*; - /// `CMSG_PETITION_BUY`'s body is a fixed 72 bytes plus the name — the shape vmangos's reader - /// walks field by field. A body one byte short leaves the reader mid-field and the buy is - /// silently dropped, which is exactly the failure this pins. + /// A body one byte short leaves vmangos's reader mid-field and the buy is silently dropped. #[test] fn petition_buy_body_is_seventy_two_bytes_plus_the_name() { let body = petition_buy(0x1234_5678_9abc_def0, "Test"); @@ -555,8 +377,6 @@ mod tests { ); } - /// The charter constants are a shared contract between server and reference client, not - /// vmangos policy — the tooltip's signable branch keys on the same `0x2000`. #[test] fn charter_constants_match_the_world_data() { assert_eq!(CHARTER_ITEM_ENTRY, 5863); diff --git a/crates/benilla-protocol/src/messages/pose.rs b/crates/benilla-protocol/src/messages/pose.rs index ecccb145..08ff5858 100644 --- a/crates/benilla-protocol/src/messages/pose.rs +++ b/crates/benilla-protocol/src/messages/pose.rs @@ -1,47 +1,24 @@ -//! The two **client-volunteered pose** bodies — sheath state and stand state. Split out of -//! `messages/spells.rs` (`set_sheathed`) and `messages/client.rs` (`stand_state_change`) by -//! decision 0640, which is also what mirrors `world::writer::pose`. -//! -//! They were apart for no reason anyone recorded, and they are plainly one thing: the client -//! decides, the server stores whatever we send with no validation of its own, and the echo into our -//! `UNIT_FIELD_BYTES_2` / `UNIT_FIELD_BYTES_1` is what every observer's body reads (decisions -//! 0080 / 0080c). The server has no independent way to know a weapon is drawn or that we sat down — -//! so the only consequence of getting one wrong is that other players see the wrong body. -//! -//! `CMSG_MOUNTSPECIAL_ANIM`, the third member of that family, has an empty body and so needs no -//! builder here (see `world::writer::pose`). -//! -//! The stand state also has an **inbound** twin, [`read_stand_state_update`] (decision 2339): the -//! server's own `SMSG_STANDSTATE_UPDATE`, one byte, which the reference applies to the local player -//! through the same setter the volunteered change goes through. +//! The client-volunteered pose bodies, sheath and stand state: the server stores what we send +//! unvalidated and echoes it through our unit fields, which every observer's body reads. use std::io; use crate::wire::read_u8; -/// Body of `CMSG_SETSHEATHED` (vmangos `SetSheathed::ReadFromWorldPacket`: `recv_data >> sheathed`): -/// one `u32` sheath state (0 unarmed/stowed, 1 melee drawn, 2 ranged drawn). Purely -/// client-volunteered — `HandleSetSheathedOpcode` (`CombatHandler.cpp:80-87`) just stores whatever -/// we send via `Unit::SetSheath`, which lands in our own `UNIT_FIELD_BYTES_2` and relays to nearby -/// observers on the next values update; the server has no independent way to know a weapon is drawn. +/// Body of `CMSG_SETSHEATHED`: a `u32` sheath state (0 stowed, 1 melee, 2 ranged), stored +/// unvalidated into `UNIT_FIELD_BYTES_2` (vmangos `CombatHandler.cpp:80-87`). pub fn set_sheathed(state: u32) -> Vec { state.to_le_bytes().to_vec() } -/// Body of `CMSG_STANDSTATECHANGE` (vmangos `StandStateChange::ReadFromWorldPacket`: -/// `recv_data >> animState`): one `u32` stand state. The server accepts only -/// {0 STAND, 1 SIT, 3 SLEEP, 8 KNEEL} (`HandleStandStateChangeOpcode`, -/// `MiscHandler.cpp:437`) and applies it via `Unit::SetStandState` → `UNIT_FIELD_BYTES_1` -/// byte 0, which relays to every observer — the same client-volunteers → server-echoes → -/// fields-drive-everyone pattern as sheath (decision 0080c). +/// Body of `CMSG_STANDSTATECHANGE`: a `u32` stand state, of which the server accepts only +/// 0 stand, 1 sit, 3 sleep and 8 kneel (`MiscHandler.cpp:437`), into `UNIT_FIELD_BYTES_1` byte 0. pub fn stand_state_change(state: u32) -> Vec { state.to_le_bytes().to_vec() } -/// Read `SMSG_STANDSTATE_UPDATE` → the stand state. VERIFIED vmangos `Unit::SetStandState` -/// (`Objects/Unit.cpp:9541`) → `WorldPackets::Misc::StandStateUpdate::AppendBodyTo`, one `u8` -/// (`UnitStandStateType`: 0 STAND · 1 SIT · 3 SLEEP · 8 KNEEL …). No guid: the reference's -/// `0x603e50` applies it to the local player whatever unit the server meant (2339). +/// Read `SMSG_STANDSTATE_UPDATE`: one `u8` stand state (vmangos `Unit.cpp:9541`), with no guid; +/// the reference (`0x603e50`) applies it to the local player whatever unit the server meant. pub(super) fn read_stand_state_update(r: &mut &[u8]) -> io::Result { read_u8(r) } @@ -52,7 +29,7 @@ mod inbound_tests { #[test] fn stand_state_update_decodes() { - // One byte (VERIFIED vmangos `Misc::StandStateUpdate::AppendBodyTo`); a drink's sit is 1. + // One byte; a drink's sit is 1. match parse_server(opcode::SMSG_STANDSTATE_UPDATE, &[1]).unwrap() { ServerPacket::StandStateUpdate { state } => assert_eq!(state, 1), other => panic!("expected StandStateUpdate, got {}", other.name()), diff --git a/crates/benilla-protocol/src/messages/progression.rs b/crates/benilla-protocol/src/messages/progression.rs index e494e196..318153b7 100644 --- a/crates/benilla-protocol/src/messages/progression.rs +++ b/crates/benilla-protocol/src/messages/progression.rs @@ -1,31 +1,22 @@ -//! Character-progression messages — XP awarded, the level-up summary, and the talent spend. Split -//! out of `messages/spells.rs` (decision 0640). -//! -//! One decision produced all three (**0304**), and they are one arc on the wire: kill or complete -//! something → `SMSG_LOG_XPGAIN` → (on a ding) `SMSG_LEVELUP_INFO` → the points it granted go back -//! out as `CMSG_LEARN_TALENT`. Layouts VERIFIED against vmangos (cited per item). +//! Character-progression messages: XP, exploration, level-up, talent learning and talent wipe. use std::io::{self, Read}; use crate::wire::{read_f32_le, read_u32_le, read_u64_le, read_u8}; -/// One decoded `SMSG_LOG_XPGAIN` — an XP award, kill or non-kill (vmangos -/// `WorldPackets::Misc::LogXpGain::AppendBodyTo`, `Server/Packets/Misc.cpp:512-522` + -/// `Misc.h:779-790`). `victim` is `0` for non-kill xp (quest/exploration/GM command). +/// `SMSG_LOG_XPGAIN`: an XP award; `victim` is 0 for non-kill XP (vmangos `Misc.cpp:512-522`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct XpGain { pub victim: u64, pub total: u32, - /// The award before the rested bonus — only a kill carries it on the wire (rested applies - /// to kill XP only, so a non-kill packet's `base` is `= total` here). `total − base` is the - /// chat line's "(+N exp Rested bonus)" amount (decision 0304). + /// The award before the rested bonus; only a kill carries it, else it equals `total`. + /// `total - base` is the chat line's "(+N exp Rested bonus)". pub base: u32, pub kill: bool, } -/// Read `SMSG_LOG_XPGAIN`: victim raw `u64` (0 for non-kill) · totalXp u32 · xpType u8 (0 = kill, -/// 1 = non-kill) · iff `xpType == 0`: baseXp u32 · groupBonus f32 (read and dropped — composing -/// the "+N group bonus" line form waits for parties to exist; decision 0304 scope call). +/// Read `SMSG_LOG_XPGAIN`: `u64 victim, u32 total, u8 type` (0 kill, 1 other), then for a kill +/// `u32 base, f32 groupBonus`. Deviation: the group bonus is read and dropped. pub(super) fn read_xp_gain(r: &mut impl Read) -> io::Result { let victim = read_u64_le(r)?; let total = read_u32_le(r)?; @@ -44,32 +35,25 @@ pub(super) fn read_xp_gain(r: &mut impl Read) -> io::Result { }) } -/// One decoded `SMSG_EXPLORATION_EXPERIENCE` — a first visit to an area: the newly explored -/// `AreaTable.dbc` row **id** (not the explore-flag bit) and the XP the discovery granted -/// (vmangos `WorldPackets::Misc::ExplorationExperience`, `Server/Packets/Misc.h:838-846` + -/// `Misc.cpp:552-556`; filled by `Player::CheckAreaExploreAndOutdoor`, Player.cpp:6228-6341, -/// which sends it **even when xp is 0** — max level, or an area with no level). The XP itself -/// also rides a separate non-kill [`XpGain`]; this packet is what names the area. Decision 0828. +/// `SMSG_EXPLORATION_EXPERIENCE`: a first visit to an area, sent even for 0 XP +/// (`Player.cpp:6228-6341`). The XP also arrives as a non-kill [`XpGain`]; this names the area. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ExplorationXp { - /// The `AreaTable.dbc` row id of the area just discovered. + /// The `AreaTable.dbc` row id, not the explore-flag bit. pub area_id: u32, - /// The exploration XP awarded (0 at max level / unleveled areas). + /// The XP awarded; 0 at max level or for an area without a level. pub xp: u32, } -/// Read `SMSG_EXPLORATION_EXPERIENCE`: areaId u32 · xp u32 (8 bytes, `Misc.cpp:552-556`). +/// Read `SMSG_EXPLORATION_EXPERIENCE`: `u32 areaId, u32 xp` (vmangos `Misc.cpp:552-556`). pub(super) fn read_exploration_xp(r: &mut impl Read) -> io::Result { let area_id = read_u32_le(r)?; let xp = read_u32_le(r)?; Ok(ExplorationXp { area_id, xp }) } -/// Body of `CMSG_LEARN_TALENT` (vmangos `Server/Packets/Skill.h:10-19` + -/// `Player::LearnTalent`, Player.cpp:20807): `u32 talentId` (a `Talent.dbc` row id) + -/// `u32 requestedRank`, **0-based** — requesting rank k learns *up to* k (the server spends -/// `k − current + 1` points), so the click sends the current rank count itself (the next -/// 0-based rank). Decision 0304. +/// Body of `CMSG_LEARN_TALENT`: `u32` `Talent.dbc` id, `u32` 0-based rank. Requesting rank k +/// learns up to k (`Player.cpp:20807`), so a click sends the current rank count. pub fn learn_talent(talent_id: u32, requested_rank: u32) -> Vec { let mut body = Vec::with_capacity(8); body.extend_from_slice(&talent_id.to_le_bytes()); @@ -77,26 +61,18 @@ pub fn learn_talent(talent_id: u32, requested_rank: u32) -> Vec { body } -/// One decoded **inbound** `MSG_TALENT_WIPE_CONFIRM` — a class trainer asking whether to unlearn -/// every talent (vmangos `WorldPackets::Skill::TalentWipeConfirmResponse`, -/// `Server/Packets/Skill.h:39-52` + `Skill.cpp:19-23`, filled by `Player::SendTalentWipeConfirm`, -/// `Player.cpp:8338`). Decision 1580. +/// Inbound `MSG_TALENT_WIPE_CONFIRM`: a class trainer asks whether to unlearn every talent +/// (vmangos `Player.cpp:8338`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TalentWipeConfirm { - /// The trainer that asked — echoed back in the outbound answer, where it is load-bearing - /// (`HandleTalentWipeConfirmOpcode` resolves it to a live `UNIT_NPC_FLAG_TRAINER` in range and - /// drops the request otherwise), exactly like the innkeeper guid in [`super::binder`]. - /// - /// **Zero means "you have no talents to reset"** — vmangos's own comment on the field, and the - /// value its refusal path sends (`SkillHandler.cpp:52`, after a failed `ResetTalents`). There - /// is no trainer to ask about, so there is nothing to put on screen. + /// The asking trainer, echoed in the answer and required to be a trainer in range there. + /// Zero means "you have no talents to reset" (vmangos `SkillHandler.cpp:52`). pub trainer: u64, - /// What the reset costs, in copper — `Player::GetResetTalentsCost()`, which climbs with every - /// reset (the dialog's money frame, and the line "The cost will increase each time you do it"). + /// Cost in copper, climbing with every reset (`Player::GetResetTalentsCost`). pub cost: u32, } -/// Read the inbound `MSG_TALENT_WIPE_CONFIRM`: trainer guid u64 · cost u32 (`Skill.cpp:19-23`). +/// Read the inbound `MSG_TALENT_WIPE_CONFIRM`: `u64 trainer, u32 cost` (`Skill.cpp:19-23`). pub(super) fn read_talent_wipe_confirm(r: &mut impl Read) -> io::Result { Ok(TalentWipeConfirm { trainer: read_u64_le(r)?, @@ -104,35 +80,28 @@ pub(super) fn read_talent_wipe_confirm(r: &mut impl Read) -> io::Result Vec { trainer_guid.to_le_bytes().to_vec() } -/// One decoded `SMSG_LEVELUP_INFO` — our own ding, self-addressed only (vmangos -/// `WorldPackets::Misc::LevelUpInfo`, `Misc.h:793-802` + `Misc.cpp:524-532`; filled by -/// `Player::GiveLevel`, Player.cpp:3210-3218, and sent straight to the leveling session — no -/// guid). Decision 0304. +/// `SMSG_LEVELUP_INFO`: our own level-up, sent only to us with no guid (`Player.cpp:3210-3218`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LevelUpInfo { /// The level just reached. pub level: u32, /// Hit points gained. pub health: u32, - /// Power gains indexed mana, rage, focus, energy, happiness. The 1.12 server fills mana (or - /// zero for non-mana classes) and leaves the rest zero; `powers[0]` is the reference chat - /// line's mana arg. + /// Power gains: mana, rage, focus, energy, happiness. The server fills only mana, which is + /// the reference chat line's mana argument. pub powers: [u32; 5], - /// Stat gains indexed str, agi, stam, int, spirit — the reference chat's `SPELL_STAT0..4` - /// order, one line per positive entry. + /// Stat gains in `SPELL_STAT0..4` order (str, agi, stam, int, spirit), one chat line per + /// positive entry. pub stats: [u32; 5], } -/// Read `SMSG_LEVELUP_INFO`: level u32 · healthGain u32 · powerGains u32×5 · statGains u32×5 -/// (48 bytes, `Misc.cpp:524-532`). +/// Read `SMSG_LEVELUP_INFO`: `u32` level, health, 5 powers, 5 stats (`Misc.cpp:524-532`). pub(super) fn read_level_up_info(r: &mut impl Read) -> io::Result { let level = read_u32_le(r)?; let health = read_u32_le(r)?; @@ -156,8 +125,6 @@ pub(super) fn read_level_up_info(r: &mut impl Read) -> io::Result { mod tests { use super::*; - /// The respec pair, both directions: the question carries guid + cost, the answer carries the - /// guid alone. Asymmetric bodies on ONE opcode is the whole reason this is an `MSG_`. #[test] fn the_talent_wipe_pair_round_trips() { let trainer: u64 = 0xF130_0000_0000_2A01; @@ -173,9 +140,6 @@ mod tests { assert_eq!(talent_wipe_confirm(trainer), trainer.to_le_bytes().to_vec()); } - /// vmangos's "you have no talents to reset" reply — guid 0, and a cost that is still filled in. - /// It parses; what to DO with it is the app's call (decision 1580: there is no trainer to ask - /// about, so nothing goes on screen). #[test] fn a_zero_trainer_is_a_parseable_refusal() { let mut body = 0u64.to_le_bytes().to_vec(); diff --git a/crates/benilla-protocol/src/messages/pvp.rs b/crates/benilla-protocol/src/messages/pvp.rs index 07324082..860184a1 100644 --- a/crates/benilla-protocol/src/messages/pvp.rs +++ b/crates/benilla-protocol/src/messages/pvp.rs @@ -1,68 +1,35 @@ -//! The two honor-system wire messages (decision 1512): the inspect-honor request/reply pair and -//! the honor-gain credit. Every shape below is VERIFIED against vmangos source (file:line cites -//! per item); the goldens are hand-built against the server's own `AppendBodyTo` order. -//! -//! The *durable* honor state is not here — it rides the player descriptor, eleven PRIVATE fields -//! plus the PUBLIC current-rank byte ([`crate::messages::ObjectFields::player_pvp_rank`] and -//! friends). These two packets cover exactly what the descriptor cannot: **somebody else's** -//! honor stats (which never stream to us), and the *moment* a kill pays out. -//! -//! Not modelled here (known siblings, VERIFIED present in vmangos `Opcodes_1_12_1.h`): the -//! battleground family — `MSG_PVP_LOG_DATA` (736), `CMSG`/`SMSG_BATTLEFIELD_LIST` (572/573) and -//! `…_STATUS` (723/724) — which 0208/0631 defer wholesale, and `SMSG_ZONE_UNDER_ATTACK` (596), a -//! world broadcast rather than an honor message. +//! The honor wire messages: the inspect-honor request/reply pair and the honor-gain credit. +//! Durable honor state rides the player descriptor; these carry what it cannot, another player's +//! stats and the moment a kill pays out. use std::io; use crate::wire::{read_i32_le, read_u16_le, read_u32_le, read_u64_le, read_u8}; -/// Another player's honor stats — the `MSG_INSPECT_HONOR_STATS` **reply** (opcode `0x2D6`/726, -/// VERIFIED vmangos `Opcodes_1_12_1.h`; already named in `opcode_names`). A 50-byte body, all -/// little-endian with no padding, in the exact order -/// `WorldPackets::Misc::InspectHonorStatsResponse::AppendBodyTo` writes it -/// (`Server/Packets/Misc.cpp:301-325`). Every `#if SUPPORTED_CLIENT_BUILD >= CLIENT_BUILD_1_6_1` -/// block in that function is live for build 5875, so all three conditional members are present. -/// -/// This exists because the honor descriptor fields are PRIVATE: nothing about a *foreign* player's -/// kills, honor or progress bar is on the wire until we ask. The one exception is their current -/// rank, which streams publicly in `PLAYER_BYTES_3` and so is **not** in this packet — -/// [`crate::messages::ObjectFields::player_pvp_rank`] answers it off the target's own descriptor. -/// -/// The server builds every value straight out of the target's descriptor -/// (`Handlers/MiscHandler.cpp:962-1010`), so these fields are the inspected player's own -/// `PLAYER_FIELD_*` honor block, re-serialized — with two shape differences worth knowing: -/// `sessionKills` arrives as the raw packed dword rather than a split pair, and the three weekly -/// HK counts arrive as bare `u16`s with a separate (dead) dishonorable slot beside each. +/// Another player's honor stats, the `MSG_INSPECT_HONOR_STATS` reply (opcode 726): 50 bytes in +/// vmangos `Misc.cpp:301-325` order, from the target's `PLAYER_FIELD_*` honor block. Their +/// current rank is not here; it streams publicly in `PLAYER_BYTES_3`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InspectHonorStats { - /// Whose stats these are — echoed from our request (`MiscHandler.cpp:977`). The reply carries - /// no error shape at all: the server simply does not answer when the target is missing, out of - /// `INSPECT_DISTANCE` (10 yd), or a valid attack target, so a silent non-reply is the refusal. + /// Echoed from the request. There is no error reply: a missing, distant (10 yd) or hostile + /// target gets no answer at all (`MiscHandler.cpp:977`). pub player_guid: u64, - /// The target's **highest lifetime** honor rank (`GetHighestRank().rank`, - /// `MiscHandler.cpp:985`) — the *internal* rank 0..18, the same domain as - /// [`crate::messages::ObjectFields::player_honor_rank`], NOT the visual number the UI draws. - /// Their *current* rank is the PUBLIC descriptor byte, not this. + /// The highest lifetime rank (`MiscHandler.cpp:985`), internal 0..18 like + /// [`crate::messages::ObjectFields::player_honor_rank`], not the visual number. pub highest_rank: u8, - /// Today's kills as the raw packed `PLAYER_FIELD_SESSION_KILLS` dword — low `u16` honorable, - /// high `u16` dishonorable (`MiscHandler.cpp:988` reads the whole field with - /// `GetUInt32Value`). Split with [`InspectHonorStats::session_kills`]. + /// Today's kills, the packed `PLAYER_FIELD_SESSION_KILLS` dword (`MiscHandler.cpp:988`). pub session_kills: u32, - /// Yesterday's honorable kills (`GetUInt16Value(PLAYER_FIELD_YESTERDAY_KILLS, 0)`). + /// Yesterday's honorable kills, the low half of `PLAYER_FIELD_YESTERDAY_KILLS`. pub yesterday_hk: u16, - /// Written `0` by vmangos (`MiscHandler.cpp:994`, comment *"Unknown (deprecated, yesterday - /// dishonourable?)"*). Decoded and carried rather than skipped: whether the real client reads - /// these three slots at all is a question currently out with an RE orchestrator in - /// `wow-5875-re`, and a field we decode and ignore is honest where a silently-skipped one - /// would be a landmine if the verdict comes back "it reads them". + /// Always 0 from vmangos (`MiscHandler.cpp:994`); decoded, not skipped. pub unknown_old1: u16, /// Last week's honorable kills. pub last_week_hk: u16, - /// Written `0` by vmangos — see [`InspectHonorStats::unknown_old1`]. + /// Always 0 from vmangos. pub unknown_old2: u16, - /// This week's honorable kills (a 1.6.0 addition; live for 5875). + /// This week's honorable kills. pub this_week_hk: u16, - /// Written `0` by vmangos — see [`InspectHonorStats::unknown_old1`]. + /// Always 0 from vmangos. pub unknown_old3: u16, /// Lifetime honorable kills (`PLAYER_FIELD_LIFETIME_HONORBALE_KILLS`, vmangos's spelling). pub lifetime_hk: u32, @@ -72,35 +39,24 @@ pub struct InspectHonorStats { pub yesterday_honor: u32, /// Last week's honor points. pub last_week_honor: u32, - /// This week's honor points (a 1.6.0 addition; live for 5875). + /// This week's honor points. pub this_week_honor: u32, - /// Last week's **standing** — the numeric ladder position, not a rank - /// (`PLAYER_FIELD_LAST_WEEK_RANK`; `0` = unranked that week). + /// Last week's standing, the ladder position and not a rank; 0 if unranked that week. pub last_week_rank: u32, - /// The rank progress bar, `0..255` **within the target's current rank** (a 1.6.0 addition; - /// live for 5875). See [`crate::messages::ObjectFields::player_honor_rank_bar`] for the - /// server's computation and the negative-rank wrap. + /// Progress within the target's current rank, 0..255, computed as + /// [`crate::messages::ObjectFields::player_honor_rank_bar`] is. pub rank_bar: u8, } impl InspectHonorStats { - /// [`Self::session_kills`] split into `(honorable, dishonorable)` — the `TWO_SHORT` packing - /// the descriptor field uses (low half, then high half). The wire carries the dword whole - /// here, so unlike the descriptor accessor this split is ours to do. + /// The packed field split into `(honorable, dishonorable)`, low half then high half. pub fn session_kills(&self) -> (u16, u16) { (self.session_kills as u16, (self.session_kills >> 16) as u16) } } -/// Read the `MSG_INSPECT_HONOR_STATS` **reply** (see [`InspectHonorStats`]): 50 bytes in -/// `AppendBodyTo` order. A short body errors (`UnexpectedEof`) rather than yielding a -/// half-populated struct. -/// -/// The opcode is an **`MSG_`**: the same number 0x2D6 carries our request *and* the server's -/// reply, and the two bodies are different shapes (8 bytes vs 50). Nothing has to disambiguate -/// them, because direction does it: [`super::parse_server`] is fed only by the world *reader*, so -/// every 0x2D6 body it ever sees is a reply. Our own request never passes through here — it is -/// built by [`inspect_honor_stats`] and handed straight to the writer. +/// Read the `MSG_INSPECT_HONOR_STATS` reply. Our 8-byte request shares the opcode, but only +/// server packets reach this reader. pub(super) fn read_inspect_honor_stats(r: &mut &[u8]) -> io::Result { Ok(InspectHonorStats { player_guid: read_u64_le(r)?, @@ -122,66 +78,30 @@ pub(super) fn read_inspect_honor_stats(r: &mut &[u8]) -> io::Result> guid`, a raw `uint64`, not a packed guid). -/// -/// Server gates (`MiscHandler.cpp:962-972`) are the same three `CMSG_INSPECT` applies: the target -/// must be an online player, within `INSPECT_DISTANCE` (10.0), and not a valid attack target. Each -/// refusal is a silent `return` — there is no error reply, so a consumer must not block on an -/// answer that may never come. Unlike `CMSG_INSPECT`, this handler does **not** set our selection -/// (`HandleInspectOpcode` opens with `SetSelectionGuid`; this one does not), which is why the send -/// lives in `world::writer::pvp` rather than `writer::selection`. +/// Body of the `MSG_INSPECT_HONOR_STATS` request: the raw, unpacked `u64` guid (vmangos +/// `Misc.cpp:93-96`). A refused request gets no answer, and unlike `CMSG_INSPECT` it does not +/// set our selection (`MiscHandler.cpp:962-972`). pub fn inspect_honor_stats(guid: u64) -> Vec { guid.to_le_bytes().to_vec() } -/// An honor payout (`SMSG_PVP_CREDIT`, opcode `0x28C`/652 — VERIFIED vmangos -/// `Opcodes_1_12_1.h`; already named in `opcode_names`). A 16-byte body in the order -/// `WorldPackets::Misc::PvpCredit::AppendBodyTo` writes it (`Server/Packets/Misc.cpp:376-381`), -/// sent from `HonorMgr::SendPVPCredit` (`HonorMgr.cpp:1061-1093`) on every honor row the server -/// books — **including a dishonorable one**, which is why [`PvpCredit::honor`] is signed. -/// -/// This is the *only* notice of a specific payout: the descriptor's contribution fields move too, -/// but they are running totals with no attribution. The chat line and the floating combat text -/// both have to come from here. +/// `SMSG_PVP_CREDIT` (opcode 652): one honor payout, dishonorable ones included (vmangos +/// `HonorMgr.cpp:1061-1093`). The descriptor holds only totals; this is the only per-payout notice. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PvpCredit { - /// Honor points, truncated to an integer server-side (`static_cast(honor)`). - /// - /// **Signed, and genuinely negative for a dishonorable kill**: `HonorMgr::Add` computes - /// `honor = (type == DISHONORABLE) ? -cp : cp` (`HonorMgr.cpp:807`) and hands *that* to - /// `SendPVPCredit` (`HonorMgr.cpp:833`). A `u32` here would read a DK penalty as ~4 billion - /// honor. + /// Honor, truncated server-side; negative for a dishonorable kill (`HonorMgr.cpp:807`). pub honor: i32, - /// Who we killed. **`0` means there is no victim** — vmangos only fills the guid inside - /// `if (victim)` (`HonorMgr.cpp:1069-1071`), and it passes the *pre-fallback* `realSource`, so - /// an honor row booked with no source at all arrives with a zero guid. The UI needs a - /// victim-less phrasing rather than resolving a name for guid 0. + /// Who we killed; 0 for an honor row with no source (`HonorMgr.cpp:1069-1071`), which + /// needs a victim-less phrasing rather than a name lookup. pub victim_guid: u64, - /// The victim's **internal** rank — the same `[0..18]` domain as the descriptor's rank bytes - /// and [`InspectHonorStats::highest_rank`], and therefore a direct index into the - /// `PVP_RANK__` GlobalStrings. NOT the visual rank the badge texture uses; do the - /// `internal > 4 ? internal - 4 : -internal` conversion (`HonorMgr.cpp:991`) at the display - /// edge if a badge is wanted, and do not do it twice. - /// - /// VERIFIED `HonorMgr::SendPVPCredit` (`HonorMgr.cpp:1078-1089`): a **player** victim sends - /// `GetHonorMgr().GetRank().rank`, a **creature** victim sends `19` iff it is a racial leader - /// and otherwise leaves the field at its `0` default. - /// - /// One quirk to observe rather than imitate: for a player victim vmangos **floors the value at - /// 5** (`if (!rank) rank = (HONOR_RANK_COUNT - POSITIVE_HONOR_RANK_COUNT) + 1`, i.e. "at least - /// Scout"), with the comment *"Never display just 'HK:' without rank name"*. That is the - /// server working around a client that would otherwise print an empty rank name — it is - /// **server behaviour we receive**, not a rule we should re-implement: we render whatever - /// number arrives, and an unranked-victim `0` from some other server must still come out - /// sanely at the display edge. - /// - /// `i32` because that is the wire type; nothing vmangos sends here is negative. + /// The victim's internal rank (0..18), indexing the `PVP_RANK__` strings; the + /// badge's visual rank is `internal > 4 ? internal - 4 : -internal` (`HonorMgr.cpp:991`). + /// A creature sends 19 if a racial leader, else 0; vmangos floors a player victim at 5 + /// (`HonorMgr.cpp:1078-1089`), a server fallback rather than a rule to re-implement. pub victim_rank: i32, } -/// Read `SMSG_PVP_CREDIT` (see [`PvpCredit`]): `i32 honor`, `u64 victimGuid`, `i32 victimRank`. +/// Read `SMSG_PVP_CREDIT`: `i32 honor, u64 victimGuid, i32 victimRank`. pub(super) fn read_pvp_credit(r: &mut &[u8]) -> io::Result { Ok(PvpCredit { honor: read_i32_le(r)?, @@ -194,9 +114,6 @@ pub(super) fn read_pvp_credit(r: &mut &[u8]) -> io::Result { mod tests { use super::*; - /// The 50-byte reply, hand-built in `InspectHonorStatsResponse::AppendBodyTo` order with the - /// field boundaries commented — a level-60 target with a rank-11 lifetime best, kills in every - /// bucket, and a bar three-quarters of the way through the current rank. #[test] fn inspect_honor_stats_golden() { #[rustfmt::skip] @@ -205,11 +122,11 @@ mod tests { 0xB3, 0x2A, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // [8] u8 highestRank = 11 (internal) 0x0B, - // [9..13) u32 sessionKills = 0x0002_0011 — 17 HK low, 2 DK high + // [9..13) u32 sessionKills = 0x0002_0011: 17 HK low, 2 DK high 0x11, 0x00, 0x02, 0x00, // [13..15) u16 yesterdayHK = 41 0x29, 0x00, - // [15..17) u16 unknownOld1 = 0 (server always writes 0) + // [15..17) u16 unknownOld1 = 0 0x00, 0x00, // [17..19) u16 lastWeekHK = 420 0xA4, 0x01, @@ -229,7 +146,7 @@ mod tests { 0xEF, 0x20, 0x00, 0x00, // [41..45) u32 thisWeekHonor = 1250 0xE2, 0x04, 0x00, 0x00, - // [45..49) u32 lastWeekRank = 57 (the STANDING) + // [45..49) u32 lastWeekRank = 57 (the standing) 0x39, 0x00, 0x00, 0x00, // [49] u8 rankBar = 191 0xBF, @@ -258,13 +175,10 @@ mod tests { rank_bar: 191, } ); - // The packed today-kills dword splits the same way the descriptor's TWO_SHORT does. assert_eq!(stats.session_kills(), (17, 2)); } - /// A truncated reply must ERROR, not panic and not yield a half-populated struct — the wire is - /// hostile input. 49 bytes is the nastiest case: everything but the trailing `rankBar`, which - /// is exactly what a pre-1.6.1 server would send. + /// 49 bytes is everything but the trailing `rankBar`, which is what a pre-1.6.1 server sends. #[test] fn inspect_honor_stats_truncated_body_errors() { let full = [0u8; 50]; @@ -275,25 +189,21 @@ mod tests { "a {len}-byte inspect-honor body must be rejected" ); } - // …and the full length still parses, so the loop above is testing truncation and not a - // decoder that rejects everything. + // The full length parses, so the loop above really tests truncation. let mut r = full.as_slice(); assert!(read_inspect_honor_stats(&mut r).is_ok()); } - /// The request is the bare 8-byte guid — byte-exact against - /// `InspectHonorStats::ReadFromWorldPacket`'s single `recv_data >> guid`. #[test] fn inspect_honor_stats_request_golden() { assert_eq!( inspect_honor_stats(0x0000_0001_0000_2AB3), vec![0xB3, 0x2A, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00], ); - // No sentinel handling: a zero guid is encoded as-is (the server answers by not answering). + // A zero guid is encoded as-is. assert_eq!(inspect_honor_stats(0), vec![0u8; 8]); } - /// The 16-byte credit body, hand-built in `PvpCredit::AppendBodyTo` order. #[test] fn pvp_credit_golden() { #[rustfmt::skip] @@ -302,7 +212,7 @@ mod tests { 0x8F, 0x00, 0x00, 0x00, // [4..12) u64 victimGuid = 0x0000_0001_0000_2AB3 0xB3, 0x2A, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - // [12..16) i32 victimRank = 11 (INTERNAL rank — the PVP_RANK_11_ key) + // [12..16) i32 victimRank = 11 (internal rank, the PVP_RANK_11_ key) 0x0B, 0x00, 0x00, 0x00, ]; let mut r = body.as_slice(); @@ -318,9 +228,6 @@ mod tests { ); } - /// A **dishonorable** kill rides the same packet with a negative `honor` - /// (`HonorMgr.cpp:807`, `honor = -cp`) — the reason the field is `i32`. Decoded as `u32` this - /// body would read as 4 294 967 291 honor. #[test] fn pvp_credit_dishonorable_kill_is_negative_honor() { let mut body = Vec::with_capacity(16); @@ -334,10 +241,6 @@ mod tests { assert_eq!(credit.victim_rank, 5); } - /// The victim-less shape the UI has to phrase differently: vmangos fills the guid only inside - /// `if (victim)` and passes the pre-fallback `realSource`, so an honor row with no source - /// arrives as a zero guid with the rank left at its `0` default (which is also what a - /// non-racial-leader creature victim sends). #[test] fn pvp_credit_zero_guid_means_no_victim() { let mut body = Vec::with_capacity(16); @@ -351,7 +254,6 @@ mod tests { assert_eq!(credit.victim_rank, 0); } - /// A truncated credit body errors rather than panicking. #[test] fn pvp_credit_truncated_body_errors() { let full = [0u8; 16]; diff --git a/crates/benilla-protocol/src/messages/quest/giver.rs b/crates/benilla-protocol/src/messages/quest/giver.rs index 08ece1aa..823f30d9 100644 --- a/crates/benilla-protocol/src/messages/quest/giver.rs +++ b/crates/benilla-protocol/src/messages/quest/giver.rs @@ -1,34 +1,15 @@ -//! `SMSG_QUESTGIVER_*` / `CMSG_QUESTGIVER_*` — the "accept/turn-in a quest at an NPC" family -//! (opcodes 386-402, vmangos `Opcodes_1_12_1.h`, VERIFIED). This is the *giver panel* slice -//! (decision 0088): the four questgiver windows carry their own text, so it needs none of the full -//! `SMSG_QUEST_QUERY_RESPONSE` template — that lives in the sibling [`super::log`] slice. CMSG -//! bodies from vmangos `Server/Packets/Quest.cpp:6-78`; SMSG layouts from the same file's -//! `AppendBodyTo` writers (line citations inline). -//! -//! Wire traps this parser is written against the *byte stream* to survive (never the C++ control -//! flow): -//! - **QUEST_LIST icon is a `u32`** — `QuestListEntry`'s `icon` field is `uint32` (`Quest.h:270`), -//! not the `u8` the gossip-menu option icon uses. -//! - **OFFER_REWARD emotes are `{delay, emote}`** — reversed from DETAILS' `{emote, delay}` -//! (`Quest.cpp:271` vs `:308-309`). -//! - **DETAILS rewards parse is uniform across the HIDDEN_REWARDS fork** — the hidden branch writes -//! three `u32` zeros (`Quest.cpp:235-237`), which a count-prefixed reader consumes as -//! `choiceCount=0, rewCount=0, money=0`, identical in shape to an empty rewards block. The parser -//! can't see the quest flag, and it doesn't need to. +//! The questgiver family (opcodes 386-402): the NPC accept, progress and reward panels, which +//! carry their own text. Layouts from vmangos `Server/Packets/Quest.cpp`. use std::io; use crate::wire::{capacity_hint, read_cstring, read_i32_le, read_u32_le, read_u64_le, read_u8}; -/// `QUEST_EMOTE_COUNT` (vmangos `QuestDef.h:43`) — DETAILS always writes exactly this many emote -/// pairs; OFFER_REWARD writes a variable count (0..=4), so only DETAILS relies on the constant. +/// vmangos `QUEST_EMOTE_COUNT` (`QuestDef.h:43`): the emote pairs DETAILS always writes. pub const QUEST_EMOTE_COUNT: u32 = 4; -/// `DIALOG_STATUS_*` (vmangos `QuestDef.h:120-129`) — the questgiver dialog status a -/// `SMSG_QUESTGIVER_STATUS` carries per NPC guid, and the per-quest `icon` in a `QUEST_LIST` / -/// gossip quest entry. Drives the `!`/`?` over the NPC head (a world-marker concern, out of the -/// panel slice — parsed + stored per guid now, rendered later) and the greeting panel's -/// active-vs-available split. +/// vmangos `DIALOG_STATUS_*` (`QuestDef.h:120-129`): an NPC's `SMSG_QUESTGIVER_STATUS`, which +/// drives the `!`/`?` over its head, and a quest row's `icon`. pub mod dialog_status { pub const NONE: u32 = 0; pub const UNAVAILABLE: u32 = 1; @@ -40,10 +21,8 @@ pub mod dialog_status { pub const REWARD2: u32 = 7; } -/// One reward/choice item on a questgiver panel (`{itemId u32, count u32, displayId u32}` triple, -/// as written in DETAILS/OFFER_REWARD — vmangos `Quest.cpp:244-249` etc.). `display_id` → -/// `ItemDisplayInfo.dbc` icon, exactly like a vendor row; a `0` id means the server had no template -/// (writes a zero triple, `Quest.cpp:248-249`). +/// One reward or choice item on a questgiver panel (vmangos `Quest.cpp:244-249`). `display_id` +/// is an `ItemDisplayInfo.dbc` row; an all-zero triple means the server had no item template. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct QuestRewardItem { pub item_id: u32, @@ -51,13 +30,10 @@ pub struct QuestRewardItem { pub display_id: u32, } -/// One required item on the progress panel (`SMSG_QUESTGIVER_REQUEST_ITEMS`), same triple shape as -/// [`QuestRewardItem`] (vmangos `Quest.cpp:376-381`). +/// A required item on the progress panel, the same triple (vmangos `Quest.cpp:376-381`). pub type QuestRequiredItem = QuestRewardItem; -/// One entry in a `SMSG_QUESTGIVER_QUEST_LIST` (the greeting panel's quest rows). `icon` is a -/// [`dialog_status`] value (**`u32`** — `Quest.h:270`, the list-entry trap), `level` the quest's -/// display level, `title` the row label. +/// One greeting-panel quest row; `icon` is a [`dialog_status`] value, a `u32` (`Quest.h:270`). #[derive(Debug, Clone, PartialEq, Eq)] pub struct QuestListEntry { pub quest_id: u32, @@ -76,9 +52,8 @@ pub struct QuestGiverList { pub quests: Vec, } -/// `SMSG_QUESTGIVER_QUEST_DETAILS` (vmangos `Quest.cpp:205-273`): the accept panel — full quest -/// text + the rewards on offer. `money` is the reward money (`GetRewOrReqMoney`, `i32`; negative on -/// a money-*sink* quest, which DETAILS shows as a required cost — rare, kept signed). +/// `SMSG_QUESTGIVER_QUEST_DETAILS` (vmangos `Quest.cpp:205-273`): the accept panel. `money` is +/// signed, negative for a quest that costs money (`GetRewOrReqMoney`). #[derive(Debug, Clone, PartialEq, Eq)] pub struct QuestDetails { pub npc: u64, @@ -93,9 +68,7 @@ pub struct QuestDetails { pub reward_spell: u32, } -/// `SMSG_QUESTGIVER_OFFER_REWARD` (vmangos `Quest.cpp:275-339`): the reward panel — the turn-in -/// text + the rewards to grant. Note the emote pairs are `{delay, emote}` (reversed vs DETAILS) and -/// are consumed for alignment only. +/// `SMSG_QUESTGIVER_OFFER_REWARD` (vmangos `Quest.cpp:275-339`): the reward panel. #[derive(Debug, Clone, PartialEq, Eq)] pub struct QuestOfferReward { pub npc: u64, @@ -110,10 +83,8 @@ pub struct QuestOfferReward { pub reward_spell: u32, } -/// `SMSG_QUESTGIVER_REQUEST_ITEMS` (vmangos `Quest.cpp:341-391`): the progress panel — the "bring -/// me these" text + required items/money, and the four completion flags the client ANDs to enable -/// the Continue button. `is_complete` is derived from the second flag word (`0x03` complete / -/// `0x00` not — `Quest.cpp:388`), the only place the completability reaches the client. +/// `SMSG_QUESTGIVER_REQUEST_ITEMS` (vmangos `Quest.cpp:341-391`): the progress panel. The client +/// ANDs four flag words to enable Continue; `is_complete` comes from the second (`Quest.cpp:388`). #[derive(Debug, Clone, PartialEq, Eq)] pub struct QuestRequestItems { pub npc: u64, @@ -127,9 +98,8 @@ pub struct QuestRequestItems { pub is_complete: bool, } -/// `SMSG_QUESTGIVER_QUEST_COMPLETE` (vmangos `Quest.cpp:96-108`): the turn-in result — the XP + -/// money granted and the fixed items handed over. The reward *item* rows here carry only -/// `{itemId, count}` (no display id — the client already knows them from the offer panel). +/// `SMSG_QUESTGIVER_QUEST_COMPLETE` (vmangos `Quest.cpp:96-108`): the XP, money and fixed items +/// granted; the item rows carry no display id. #[derive(Debug, Clone, PartialEq, Eq)] pub struct QuestComplete { pub quest_id: u32, @@ -138,52 +108,46 @@ pub struct QuestComplete { pub items: Vec<(u32, u32)>, } -// ── CMSG encoders (all trivial, readers at vmangos `Quest.cpp:6-50`) ────────────────────────────── +// ── CMSG encoders (vmangos `Quest.cpp:6-50`) ───────────────────────────────────────────────────── -/// Body of `CMSG_QUESTGIVER_STATUS_QUERY` (`Quest.cpp:11-14`): one `u64` NPC guid. Answered by -/// `SMSG_QUESTGIVER_STATUS`. +/// Body of `CMSG_QUESTGIVER_STATUS_QUERY` (`Quest.cpp:11-14`): the `u64` NPC guid. pub fn questgiver_status_query(npc: u64) -> Vec { npc.to_le_bytes().to_vec() } -/// Body of `CMSG_QUESTGIVER_HELLO` (`Quest.cpp:16-19`): one `u64` NPC guid. The same -/// `SendPreparedGossip` path as `CMSG_GOSSIP_HELLO` (vmangos `QuestHandler.cpp:103`), so benilla's -/// interact route can keep sending gossip-hello; this opener exists for completeness/parity. +/// Body of `CMSG_QUESTGIVER_HELLO` (`Quest.cpp:16-19`): the NPC guid. The server answers it like +/// `CMSG_GOSSIP_HELLO`, through `SendPreparedGossip` (`QuestHandler.cpp:103`). pub fn questgiver_hello(npc: u64) -> Vec { npc.to_le_bytes().to_vec() } -/// Body of `CMSG_QUESTGIVER_QUERY_QUEST` (`Quest.cpp:27-31`): `u64 guid, u32 quest`. Answered by -/// `SMSG_QUESTGIVER_QUEST_DETAILS` (vmangos `QuestHandler.cpp:224`, always — this is "look at an -/// available quest"). The click a greeting/gossip quest row makes. +/// Body of `CMSG_QUESTGIVER_QUERY_QUEST` (`Quest.cpp:27-31`): `u64 guid, u32 quest`, sent by a +/// quest row's click and always answered with the details panel (`QuestHandler.cpp:224`). pub fn questgiver_query_quest(npc: u64, quest: u32) -> Vec { guid_quest(npc, quest) } -/// Body of `CMSG_QUESTGIVER_ACCEPT_QUEST` (`Quest.cpp:21-25`): `u64 guid, u32 quest`. Adds the quest -/// to the log, then the server closes the gossip window (`SMSG_GOSSIP_COMPLETE`). +/// Body of `CMSG_QUESTGIVER_ACCEPT_QUEST` (`Quest.cpp:21-25`): `u64 guid, u32 quest`. The server +/// logs the quest, then closes the gossip window (`SMSG_GOSSIP_COMPLETE`). pub fn questgiver_accept_quest(npc: u64, quest: u32) -> Vec { guid_quest(npc, quest) } -/// Body of `CMSG_QUESTGIVER_COMPLETE_QUEST` (`Quest.cpp:46-50`): `u64 guid, u32 quest`. Asks the -/// server to show the turn-in **progress** panel (`SMSG_QUESTGIVER_REQUEST_ITEMS`, vmangos -/// `QuestHandler.cpp:383-397`) — the alternate entry from a quest-log completable quest. +/// Body of `CMSG_QUESTGIVER_COMPLETE_QUEST` (`Quest.cpp:46-50`): `u64 guid, u32 quest`, answered +/// with the progress panel (`QuestHandler.cpp:383-397`). pub fn questgiver_complete_quest(npc: u64, quest: u32) -> Vec { guid_quest(npc, quest) } -/// Body of `CMSG_QUESTGIVER_REQUEST_REWARD` (`Quest.cpp:40-44`): `u64 guid, u32 quest`. The progress -/// panel's Continue button — advances to the **reward** panel (`SMSG_QUESTGIVER_OFFER_REWARD`, -/// vmangos `QuestHandler.cpp:286-312`, which first `CompleteQuest`s if it can). +/// Body of `CMSG_QUESTGIVER_REQUEST_REWARD` (`Quest.cpp:40-44`): `u64 guid, u32 quest`, the +/// progress panel's Continue, answered with the reward panel (`QuestHandler.cpp:286-312`). pub fn questgiver_request_reward(npc: u64, quest: u32) -> Vec { guid_quest(npc, quest) } -/// Body of `CMSG_QUESTGIVER_CHOOSE_REWARD` (`Quest.cpp:33-38`): `u64 guid, u32 quest, u32 reward`. -/// `reward` is the **index** into the quest's choice items (0-based; ignored when the quest has no -/// choice rewards — pass 0). The reward panel's Complete button — grants the reward and answers -/// `SMSG_QUESTGIVER_QUEST_COMPLETE` (vmangos `QuestHandler.cpp:239-284`). +/// Body of `CMSG_QUESTGIVER_CHOOSE_REWARD` (`Quest.cpp:33-38`): `u64 guid, u32 quest, u32 reward`, +/// `reward` a 0-based choice index (0 with no choice). Sent by the reward panel's Complete and +/// answered with `SMSG_QUESTGIVER_QUEST_COMPLETE` (`QuestHandler.cpp:239-284`). pub fn questgiver_choose_reward(npc: u64, quest: u32, reward: u32) -> Vec { let mut body = Vec::with_capacity(16); body.extend_from_slice(&npc.to_le_bytes()); @@ -192,7 +156,6 @@ pub fn questgiver_choose_reward(npc: u64, quest: u32, reward: u32) -> Vec { body } -/// The shared `{u64 guid, u32 quest}` body of the five simple questgiver CMSGs. fn guid_quest(npc: u64, quest: u32) -> Vec { let mut body = Vec::with_capacity(12); body.extend_from_slice(&npc.to_le_bytes()); @@ -200,10 +163,8 @@ fn guid_quest(npc: u64, quest: u32) -> Vec { body } -// ── SMSG parsers ────────────────────────────────────────────────────────────────────────────────── +// ── SMSG parsers ───────────────────────────────────────────────────────────────────────────────── -/// Read a `{itemId u32, count u32, displayId u32}` reward/choice/required triple (the shape shared -/// by DETAILS/OFFER_REWARD/REQUEST_ITEMS). fn read_item_triple(r: &mut &[u8]) -> io::Result { Ok(QuestRewardItem { item_id: read_u32_le(r)?, @@ -212,11 +173,9 @@ fn read_item_triple(r: &mut &[u8]) -> io::Result { }) } -/// Read a count-prefixed run of item triples (`u32 count` then that many triples). fn read_item_block(r: &mut &[u8]) -> io::Result> { let count = read_u32_le(r)?; - // The wider of the two lists this reads: `QUEST_REWARD_CHOICES_COUNT` 6 (vmangos - // `QuestDef.h:39`; the fixed rewards are `QUEST_REWARDS_COUNT` 4). + // `QUEST_REWARD_CHOICES_COUNT` 6, the wider of the two lists (vmangos `QuestDef.h:39`). let mut items = Vec::with_capacity(capacity_hint(count, 6)); for _ in 0..count { items.push(read_item_triple(r)?); @@ -229,18 +188,16 @@ pub(in crate::messages) fn read_questgiver_status(r: &mut &[u8]) -> io::Result<( Ok((read_u64_le(r)?, read_u32_le(r)?)) } -/// Read `SMSG_QUESTGIVER_QUEST_LIST` (vmangos `Quest.cpp:159-203`): `u64 npcGuid, cstr greeting, -/// u32 emoteDelay, u32 emote, u8 count`, then per quest `{u32 questId, u32 icon, u32 level, cstr -/// title}`. Note the entry `icon` is a **`u32`** (the list-entry trap), unlike the gossip option's -/// `u8` icon. +/// Read `SMSG_QUESTGIVER_QUEST_LIST` (vmangos `Quest.cpp:159-203`): `u64 npc`, greeting, +/// `u32 emoteDelay, u32 emote, u8 count`, then per quest `u32 id, u32 icon, u32 level`, title. +/// The icon is a `u32` here, unlike a gossip option's `u8`. pub(in crate::messages) fn read_questgiver_quest_list(r: &mut &[u8]) -> io::Result { let npc = read_u64_le(r)?; let greeting = read_cstring(r)?; let emote_delay = read_u32_le(r)?; let emote = read_u32_le(r)?; let count = read_u8(r)?; - // No server cap (the quest menu is a vector, `GossipDef.h:184`); `GOSSIP_MAX_MENU_ITEMS` 32 - // (`GossipDef.h:32`) is the client's display limit. + // No server cap (`GossipDef.h:184`); `GOSSIP_MAX_MENU_ITEMS` 32 is the display limit. let mut quests = Vec::with_capacity(capacity_hint(count, 32)); for _ in 0..count { quests.push(QuestListEntry { @@ -259,12 +216,10 @@ pub(in crate::messages) fn read_questgiver_quest_list(r: &mut &[u8]) -> io::Resu }) } -/// Read `SMSG_QUESTGIVER_QUEST_DETAILS` (vmangos `Quest.cpp:205-273`): `u64 npcGuid, u32 questId, -/// cstr title, cstr details, cstr objectives, u32 autoFinish`, then the count-prefixed choice block, -/// the count-prefixed reward block, `i32 money`, `u32 rewSpell`, `u32 emoteCount (=4)` and that many -/// `{u32 emote, u32 delay}` pairs (consumed for alignment). The HIDDEN_REWARDS fork writes three -/// `u32` zeros in place of the two blocks + money, which this count-prefixed reader consumes -/// identically to an empty rewards block (the uniform-parse trap). +/// Read `SMSG_QUESTGIVER_QUEST_DETAILS` (vmangos `Quest.cpp:205-273`): npc, id, title, details, +/// objectives, auto-finish, the choice and reward blocks, money, spell, then a count (always 4) +/// of `{u32 emote, u32 delay}` pairs. A hidden-rewards quest writes three zeros for the blocks +/// and money (`Quest.cpp:235-237`), which read as empty blocks. pub(in crate::messages) fn read_questgiver_quest_details( r: &mut &[u8], ) -> io::Result { @@ -297,11 +252,9 @@ pub(in crate::messages) fn read_questgiver_quest_details( }) } -/// Read `SMSG_QUESTGIVER_OFFER_REWARD` (vmangos `Quest.cpp:275-339`): `u64 npcGuid, u32 questId, -/// cstr title, cstr offerText, u32 autoFinish, u32 emoteCount` then that many `{u32 delay, u32 -/// emote}` pairs (**delay first** — reversed vs DETAILS), the choice block, the reward block, `i32 -/// money, u32 questFlags, u32 rewSpell` (the spell is guarded by build > 1.4.2 server-side, which -/// 5875 satisfies — always present). +/// Read `SMSG_QUESTGIVER_OFFER_REWARD` (vmangos `Quest.cpp:275-339`): npc, id, title, text, +/// auto-finish, a counted run of `{u32 delay, u32 emote}` pairs (delay first, unlike DETAILS, +/// `Quest.cpp:308-309`), the choice and reward blocks, money, quest flags, spell. pub(in crate::messages) fn read_questgiver_offer_reward( r: &mut &[u8], ) -> io::Result { @@ -312,7 +265,7 @@ pub(in crate::messages) fn read_questgiver_offer_reward( let auto_finish = read_u32_le(r)?; let emote_count = read_u32_le(r)?; for _ in 0..emote_count { - let _delay = read_u32_le(r)?; // delay FIRST here (reversed vs DETAILS) + let _delay = read_u32_le(r)?; // delay first here, unlike DETAILS let _emote = read_u32_le(r)?; } let choices = read_item_block(r)?; @@ -334,11 +287,9 @@ pub(in crate::messages) fn read_questgiver_offer_reward( }) } -/// Read `SMSG_QUESTGIVER_REQUEST_ITEMS` (vmangos `Quest.cpp:341-391`): `u64 npcGuid, u32 questId, -/// cstr title, cstr requestText, u32 emoteDelay(0), u32 emote, u32 closeOnCancel, u32 requiredMoney, -/// u32 reqCount` then that many `{u32 itemId, u32 count, u32 displayId}` triples, then four `u32` -/// completion flags (`0x02, isComplete?0x03:0x00, 0x04, 0x08`). `is_complete` comes from the second -/// flag word — the only completability signal on this wire. +/// Read `SMSG_QUESTGIVER_REQUEST_ITEMS` (vmangos `Quest.cpp:341-391`): npc, id, title, text, +/// `u32` emote delay, emote, close-on-cancel and money, the required items, then four flag words +/// `0x02, complete ? 0x03 : 0x00, 0x04, 0x08`. pub(in crate::messages) fn read_questgiver_request_items( r: &mut &[u8], ) -> io::Result { @@ -368,9 +319,8 @@ pub(in crate::messages) fn read_questgiver_request_items( }) } -/// Read `SMSG_QUESTGIVER_QUEST_COMPLETE` (vmangos `Quest.cpp:96-108`): `u32 questId, u32 unknown, -/// u32 xp, u32 money, u32 itemCount` then that many `{u32 itemId, u32 count}` pairs (no display id -/// on this wire). +/// Read `SMSG_QUESTGIVER_QUEST_COMPLETE` (vmangos `Quest.cpp:96-108`): `u32` quest, unknown, xp, +/// money and item count, then `{u32 itemId, u32 count}` pairs. pub(in crate::messages) fn read_questgiver_quest_complete( r: &mut &[u8], ) -> io::Result { @@ -406,7 +356,7 @@ pub(in crate::messages) fn read_questgiver_quest_failed(r: &mut &[u8]) -> io::Re mod tests { use super::*; - // ── Small byte-buffer builders for the hand-built parse fixtures ────────────────────────────── + // ── Byte-buffer builders ───────────────────────────────────────────────────────────────────── fn push_u32(b: &mut Vec, v: u32) { b.extend_from_slice(&v.to_le_bytes()); } @@ -430,7 +380,6 @@ mod tests { #[test] fn cmsg_guid_only_bodies() { - // STATUS_QUERY and HELLO are both a lone u64 guid, little-endian. assert_eq!( questgiver_status_query(0x0102_0304_0506_0708), vec![0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01] @@ -440,7 +389,6 @@ mod tests { #[test] fn cmsg_guid_quest_bodies() { - // QUERY/ACCEPT/COMPLETE/REQUEST_REWARD share the {u64 guid, u32 quest} body. let mut want = Vec::new(); push_u64(&mut want, 0x42); push_u32(&mut want, 3); @@ -452,7 +400,6 @@ mod tests { #[test] fn cmsg_choose_reward_body() { - // {u64 guid, u32 quest, u32 reward-index}. let mut want = Vec::new(); push_u64(&mut want, 0x99); push_u32(&mut want, 40); @@ -474,8 +421,7 @@ mod tests { #[test] fn quest_list_reads_u32_icon() { - // The list-entry `icon` is a u32 — exercise it by putting a value that would misalign the - // next field if read as a u8 (0x0000_0005). Two quests. + // Reading the `u32` icon as a `u8` would misalign every field after it. let mut b = Vec::new(); push_u64(&mut b, 0xABCD); push_cstr(&mut b, "Greetings, hero."); @@ -483,7 +429,7 @@ mod tests { push_u32(&mut b, 1); // emote b.push(2); // count (u8) push_u32(&mut b, 100); // questId - push_u32(&mut b, dialog_status::AVAILABLE); // icon (u32!) + push_u32(&mut b, dialog_status::AVAILABLE); // icon, a u32 push_u32(&mut b, 3); // level push_cstr(&mut b, "A Threat Within"); push_u32(&mut b, 200); @@ -504,8 +450,6 @@ mod tests { #[test] fn details_rewards_block_is_uniform() { - // A quest with one choice + one fixed reward + money. The count-prefixed reader is the same - // shape the HIDDEN_REWARDS fork's three zeros parse as (choice 0, reward 0, money 0). let mut b = Vec::new(); push_u64(&mut b, 0x42); push_u32(&mut b, 100); // questId @@ -550,7 +494,6 @@ mod tests { #[test] fn details_hidden_rewards_fork_parses_as_empty() { - // The HIDDEN_REWARDS branch writes exactly three u32 zeros where the blocks + money go. let mut b = Vec::new(); push_u64(&mut b, 0x42); push_u32(&mut b, 100); @@ -576,8 +519,7 @@ mod tests { #[test] fn offer_reward_emotes_are_delay_first() { - // OFFER_REWARD's emote pairs are {delay, emote} — the reverse of DETAILS. Encode a distinctive - // pair and prove the rest of the packet still aligns (choices/rewards/money/flags/spell land). + // Both emote values are discarded; this checks that the fields after them align. let mut b = Vec::new(); push_u64(&mut b, 0x42); push_u32(&mut b, 100); @@ -585,7 +527,7 @@ mod tests { push_cstr(&mut b, "Here is your reward."); push_u32(&mut b, 1); // autoFinish push_u32(&mut b, 2); // emoteCount - push_u32(&mut b, 500); // delay (FIRST) + push_u32(&mut b, 500); // delay, first push_u32(&mut b, 1); // emote push_u32(&mut b, 0); // delay push_u32(&mut b, 2); // emote @@ -671,12 +613,10 @@ mod tests { #[test] fn quest_invalid_and_failed_parse() { - // QUESTGIVER_QUEST_INVALID: one u32 message code. let mut b = Vec::new(); push_u32(&mut b, 7); assert_eq!(read_questgiver_quest_invalid(&mut b.as_slice()).unwrap(), 7); - // QUESTGIVER_QUEST_FAILED: {questId, reason}. let mut b2 = Vec::new(); push_u32(&mut b2, 100); push_u32(&mut b2, 4);