comments: protocol, the pet, petition, pose, progression, honor and quest-giver messages
Each item says what it is and the wire or server fact behind it, with the vmangos or client citation; the two kept deviations say why. History, record numbers and emphasis go. With comments stripped the six files are identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<u32> 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<PetActionEntry>,
|
||||
/// The pet's running cooldowns.
|
||||
pub cooldowns: Vec<PetSpellCooldown>,
|
||||
}
|
||||
|
||||
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
|
||||
/// <the cooldown block> 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<PetSpells> {
|
||||
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<PetSpells> {
|
||||
}
|
||||
|
||||
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<PetSpells> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<Vec<PetSpellCooldown>> {
|
||||
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<Vec<PetSpellCooldown>> {
|
||||
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<Vec<PetSpellCooldown>> {
|
||||
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<PetMode> {
|
||||
Ok(PetMode {
|
||||
pet_guid: read_u64_le(r)?,
|
||||
@@ -372,32 +239,18 @@ pub(super) fn read_pet_mode(r: &mut impl Read) -> io::Result<PetMode> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_PET_SET_ACTION` (opcode 372): `u64 petGuid` then one or two
|
||||
/// `(u32 position, u32 packedData)` pairs. The server distinguishes the forms **by body size
|
||||
/// alone** — 24 bytes ⇒ two entries, anything else ⇒ one (`PetSetAction::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/Pet.cpp:52-62`) — so a one-entry body must not be padded.
|
||||
///
|
||||
/// This one opcode carries **both** of the bar's write verbs, which is the thing to know about it:
|
||||
///
|
||||
/// - **the autocast toggle** — one pair, the pressed slot and its word with bit 30 flipped. That
|
||||
/// is `TogglePetAutocast`'s whole send (wow-re §10.2, `0x4bcc1e push 0x174`), and the server
|
||||
/// reads the flip straight out of the type byte: `ACT_ENABLED` ⇒ `ToggleAutocast(true)`,
|
||||
/// `ACT_DISABLED` ⇒ `false` (`HandlePetSetAction`, `PetHandler.cpp`). `CMSG_PET_SPELL_AUTOCAST`
|
||||
/// (0x2F3) is **not** this verb — it belongs to `ToggleSpellAutocast`, the pet *spellbook*
|
||||
/// frame's binding, and indexes the spellbook rather than the bar (wow-re §10.2/§11).
|
||||
/// - **the drag** — one or two pairs, from `0x4bc9a0` (wow-re §10.4): the optional first pair
|
||||
/// relocates whatever occupied the target slot, the mandatory second writes the drop. vmangos
|
||||
/// requires the two-pair form whenever a command/reaction token moves.
|
||||
/// Body of `CMSG_PET_SET_ACTION` (opcode 372): `u64` pet guid, then one or two
|
||||
/// `(u32 position, u32 packed)` pairs, counted by body size alone (24 bytes is two, vmangos
|
||||
/// `Packets/Pet.cpp:52-62`), so one pair must not be padded. It carries the bar's autocast toggle
|
||||
/// (one pair, bit 30 flipped, `0x4bcc1e`) and its drag (`0x4bc9a0`), whose optional first pair
|
||||
/// relocates the target slot's occupant; vmangos requires it when a command or reaction moves.
|
||||
pub fn pet_set_action(pet_guid: u64, entries: &[(u32, u32)]) -> Vec<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
trainer.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
@@ -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<PetitionShowListEntry>,
|
||||
}
|
||||
|
||||
/// 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<PetitionShowList> {
|
||||
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<PetitionS
|
||||
Ok(PetitionShowList { npc, entries })
|
||||
}
|
||||
|
||||
/// One signature on a charter — a signer's guid, and the dword that always follows it.
|
||||
/// One signature on a charter: the signer's guid and a trailing dword.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PetitionSignature {
|
||||
/// The signing character's guid. Turning it into a *name* is the client's job: the packet
|
||||
/// carries no names at all.
|
||||
/// The signer's guid; the packet carries no names.
|
||||
pub signer: u64,
|
||||
/// The trailing dword, which vmangos writes as a literal `0`
|
||||
/// (`Guild/GuildMgr.cpp:358-366`, `data << 0`). Kept rather than skipped so the field is
|
||||
/// visible in a dump and a non-zero value from another server would be *noticed* rather than
|
||||
/// silently consumed.
|
||||
/// Always 0 from vmangos (`GuildMgr.cpp:358-366`); decoded, not skipped.
|
||||
pub unknown: u32,
|
||||
}
|
||||
|
||||
/// `SMSG_PETITION_SHOW_SIGNATURES` — who has signed a charter. **The answer to two different
|
||||
/// questions** (module doc): our own `CMSG_PETITION_SHOW_SIGNATURES`, and someone else's
|
||||
/// `CMSG_OFFER_PETITION` aimed at us.
|
||||
///
|
||||
/// Note what is *not* here: the guild name, the body text, and the signature requirement. Those
|
||||
/// live only in `SMSG_PETITION_QUERY_RESPONSE`, keyed by [`Self::petition_id`] — the same
|
||||
/// two-caches shape [`super::guild`] has, where the roster carries no guild name.
|
||||
/// `SMSG_PETITION_SHOW_SIGNATURES`: a charter's signers, in answer to our request or when
|
||||
/// someone offers us theirs. Its name and requirement come from `SMSG_PETITION_QUERY_RESPONSE`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PetitionShowSignatures {
|
||||
/// The charter **item's** guid — the handle every later verb takes (sign, offer, rename, turn
|
||||
/// in, decline). Not the petition id.
|
||||
/// The charter item's guid, which every later verb takes; not the petition id.
|
||||
pub item: u64,
|
||||
/// The charter owner's character guid. Whether this is us is what makes the window the
|
||||
/// leader's view or a signer's view.
|
||||
/// The charter's owner; whether it is us decides the leader's view or a signer's.
|
||||
pub owner: u64,
|
||||
/// The petition id — the key `CMSG_PETITION_QUERY` takes, and the same value the charter
|
||||
/// item's enchantment slot 0 carries (module doc).
|
||||
/// The key `CMSG_PETITION_QUERY` takes, also carried in the charter's enchantment slot 0.
|
||||
pub petition_id: u32,
|
||||
/// The signatures so far, in wire order.
|
||||
pub signatures: Vec<PetitionSignature>,
|
||||
}
|
||||
|
||||
/// 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<PetitionShowSignatures> {
|
||||
@@ -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<PetitionSignResults> {
|
||||
Ok(PetitionSignResults {
|
||||
item: read_u64_le(r)?,
|
||||
@@ -255,64 +161,48 @@ pub(super) fn read_petition_sign_results(r: &mut impl Read) -> io::Result<Petiti
|
||||
})
|
||||
}
|
||||
|
||||
/// `SMSG_PETITION_QUERY_RESPONSE` — a petition's own record, keyed by petition id.
|
||||
///
|
||||
/// Sixteen fields, of which vmangos fills three with anything but a constant, and whose own header
|
||||
/// says *"all those fields below don't really change anything in the UI"* (`Petition.h:126`). They
|
||||
/// are parsed in full regardless: the trailing `choice` list is length-prefixed, so skipping the
|
||||
/// middle would make the tail unreadable, and a field that is constant on this server is not
|
||||
/// thereby constant on the wire.
|
||||
/// `SMSG_PETITION_QUERY_RESPONSE`: a petition's record, keyed by petition id.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PetitionQueryResponse {
|
||||
/// The petition id we asked about.
|
||||
pub petition_id: u32,
|
||||
/// The charter owner's character guid.
|
||||
pub owner: u64,
|
||||
/// The proposed guild's name. This is the only place it exists on the wire.
|
||||
/// The proposed guild's name, carried by no other packet.
|
||||
pub name: String,
|
||||
/// Free text on the charter — always empty on vmangos (`PetitionsHandler.cpp:178`).
|
||||
/// Free text on the charter; empty on vmangos (`PetitionsHandler.cpp:178`).
|
||||
pub body_text: String,
|
||||
/// vmangos sends `1` (`:179`). Meaning unknown; kept because it is a real wire field.
|
||||
/// vmangos sends 1 (`PetitionsHandler.cpp:179`); meaning unknown.
|
||||
pub flags: u32,
|
||||
/// How many signatures are **required**. vmangos hardcodes 9 here even when its
|
||||
/// `MinPetitionSigns` config is lower (module doc), so this is the number the reference's
|
||||
/// Request-Signature button disables against, and it need not equal
|
||||
/// [`MAX_PETITION_SIGNATURES`].
|
||||
/// Signatures required, which the reference's Request-Signature button tests; vmangos sends 9
|
||||
/// even when its `MinPetitionSigns` is lower (`PetitionsHandler.cpp:182`).
|
||||
pub min_signatures: u32,
|
||||
/// How many signatures fit. vmangos hardcodes 9 (`:183`).
|
||||
/// Signatures that fit; 9 on vmangos (`PetitionsHandler.cpp:183`).
|
||||
pub max_signatures: u32,
|
||||
/// Deadline timestamp — `0` on vmangos.
|
||||
/// Deadline timestamp, 0 on vmangos.
|
||||
pub deadline: u32,
|
||||
/// Creation timestamp — `0` on vmangos.
|
||||
/// Creation timestamp, 0 on vmangos.
|
||||
pub creation: u32,
|
||||
/// Restricting guild id — `0` on vmangos.
|
||||
/// Restricting guild id, 0 on vmangos.
|
||||
pub allowed_guild_id: u32,
|
||||
/// Class mask restriction — `0` on vmangos.
|
||||
/// Class mask restriction, 0 on vmangos.
|
||||
pub allowed_classes: u32,
|
||||
/// Race mask restriction — `0` on vmangos.
|
||||
/// Race mask restriction, 0 on vmangos.
|
||||
pub allowed_races: u32,
|
||||
/// Gender restriction — **`u16` on the wire, not `u32`** (`Petition.cpp:94`). The one odd
|
||||
/// width in the packet, and misreading it as a dword shifts every field after it by two bytes.
|
||||
/// Gender restriction: a `u16`, the packet's one odd width (`Packets/Petition.cpp:94`).
|
||||
pub allowed_gender: u16,
|
||||
/// Minimum level — `0` on vmangos.
|
||||
/// Minimum level, 0 on vmangos.
|
||||
pub allowed_min_level: u32,
|
||||
/// Maximum level — `0` on vmangos.
|
||||
/// Maximum level, 0 on vmangos.
|
||||
pub allowed_max_level: u32,
|
||||
/// The multiple-choice options this petition offers — always empty on vmangos, and the reason
|
||||
/// the tail cannot be skipped: the count is what says where `default_choice` starts.
|
||||
/// The petition's multiple-choice options; empty on vmangos.
|
||||
pub choices: Vec<String>,
|
||||
/// 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<PetitionQueryResponse> {
|
||||
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<Peti
|
||||
})
|
||||
}
|
||||
|
||||
/// `MSG_PETITION_RENAME` — the server's echo of a successful rename.
|
||||
///
|
||||
/// Sent **only on success** (`PetitionsHandler.cpp:209-215`); a rejected name comes back as an
|
||||
/// `SMSG_GUILD_COMMAND_RESULT` instead. Note the handler does **no ownership check** — anyone
|
||||
/// holding the item may rename it.
|
||||
/// `MSG_PETITION_RENAME`: the server's echo of a successful rename (`PetitionsHandler.cpp:209`);
|
||||
/// a rejected name gets `SMSG_GUILD_COMMAND_RESULT`. Any holder of the item may rename it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PetitionRename {
|
||||
/// The charter item's guid.
|
||||
pub item: u64,
|
||||
/// Its new name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Read `MSG_PETITION_RENAME` (VERIFIED vmangos `Server/Packets/Petition.cpp:104-108`
|
||||
/// `PetitionRename::AppendBodyTo`): `u64 itemGuid`, cstring newName. The same two fields the
|
||||
/// client sends, in the same order — which is what makes it an `MSG_` rather than a pair.
|
||||
/// Read `MSG_PETITION_RENAME` (vmangos `Packets/Petition.cpp:104-108`): `u64` item, cstring name.
|
||||
pub(super) fn read_petition_rename(r: &mut impl Read) -> io::Result<PetitionRename> {
|
||||
Ok(PetitionRename {
|
||||
item: read_u64_le(r)?,
|
||||
@@ -378,56 +262,29 @@ pub(super) fn read_petition_rename(r: &mut impl Read) -> io::Result<PetitionRena
|
||||
})
|
||||
}
|
||||
|
||||
/// Read `SMSG_TURN_IN_PETITION_RESULTS` (VERIFIED vmangos `Server/Packets/Petition.cpp:76-79`
|
||||
/// `TurnInPetitionResults::AppendBodyTo`): one `u32`, a [`petition_result`] code — and **only** the
|
||||
/// code. Which charter it refers to is implicit; there can be only one in flight.
|
||||
///
|
||||
/// The three codes vmangos actually sends here are [`petition_result::OK`] (`:473`),
|
||||
/// [`petition_result::ALREADY_IN_GUILD`] (`:427`) and [`petition_result::NEED_MORE`] (`:438`). A
|
||||
/// name collision is reported as `SMSG_GUILD_COMMAND_RESULT` instead and produces no results
|
||||
/// packet at all (`:445`), so "no answer" is a real outcome of a turn-in.
|
||||
/// Read `SMSG_TURN_IN_PETITION_RESULTS` (vmangos `Packets/Petition.cpp:76-79`): a lone
|
||||
/// [`petition_result`] code. A name collision sends `SMSG_GUILD_COMMAND_RESULT` and no result
|
||||
/// packet at all (`PetitionsHandler.cpp:445`).
|
||||
pub(super) fn read_turn_in_petition_results(r: &mut impl Read) -> io::Result<u32> {
|
||||
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<u64> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
item.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Append a NUL-terminated string to `body`.
|
||||
fn push_cstring(body: &mut Vec<u8>, s: &str) {
|
||||
body.extend_from_slice(s.as_bytes());
|
||||
body.push(0);
|
||||
@@ -540,9 +364,7 @@ fn push_cstring(body: &mut Vec<u8>, 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);
|
||||
|
||||
@@ -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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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()),
|
||||
|
||||
@@ -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<XpGain> {
|
||||
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<XpGain> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<ExplorationXp> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<TalentWipeConfirm> {
|
||||
Ok(TalentWipeConfirm {
|
||||
trainer: read_u64_le(r)?,
|
||||
@@ -104,35 +80,28 @@ pub(super) fn read_talent_wipe_confirm(r: &mut impl Read) -> io::Result<TalentWi
|
||||
})
|
||||
}
|
||||
|
||||
/// Body of the **outbound** `MSG_TALENT_WIPE_CONFIRM` — the `CONFIRM_TALENT_WIPE` dialog's Accept
|
||||
/// (vmangos `WorldPackets::Skill::TalentWipeConfirm`, `Skill.cpp:14-17`: one guid, no cost). The
|
||||
/// guid is the one the question arrived with; the real client sends its own latched copy
|
||||
/// (`0xc4d7a0`, wow-re `talent-api.md`), which is the same number. Decision 1580.
|
||||
/// Body of the outbound `MSG_TALENT_WIPE_CONFIRM`, the `CONFIRM_TALENT_WIPE` Accept: the trainer
|
||||
/// guid alone (vmangos `Skill.cpp:14-17`), as the reference latches it at `0xc4d7a0`.
|
||||
pub fn talent_wipe_confirm(trainer_guid: u64) -> Vec<u8> {
|
||||
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<LevelUpInfo> {
|
||||
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<LevelUpInfo> {
|
||||
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();
|
||||
|
||||
@@ -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<InspectHonorStats> {
|
||||
Ok(InspectHonorStats {
|
||||
player_guid: read_u64_le(r)?,
|
||||
@@ -122,66 +78,30 @@ pub(super) fn read_inspect_honor_stats(r: &mut &[u8]) -> io::Result<InspectHonor
|
||||
})
|
||||
}
|
||||
|
||||
/// Body of the `MSG_INSPECT_HONOR_STATS` **request**: the inspected player's full 8-byte GUID and
|
||||
/// nothing else (VERIFIED vmangos `WorldPackets::Misc::InspectHonorStats::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/Misc.cpp:93-96` — `recv_data >> 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<u8> {
|
||||
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<int32>(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_<rank>_<team>` 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_<rank>_<team>` 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<PvpCredit> {
|
||||
Ok(PvpCredit {
|
||||
honor: read_i32_le(r)?,
|
||||
@@ -194,9 +114,6 @@ pub(super) fn read_pvp_credit(r: &mut &[u8]) -> io::Result<PvpCredit> {
|
||||
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_<team> key)
|
||||
// [12..16) i32 victimRank = 11 (internal rank, the PVP_RANK_11_<team> 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];
|
||||
|
||||
@@ -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<QuestListEntry>,
|
||||
}
|
||||
|
||||
/// `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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
body
|
||||
}
|
||||
|
||||
/// The shared `{u64 guid, u32 quest}` body of the five simple questgiver CMSGs.
|
||||
fn guid_quest(npc: u64, quest: u32) -> Vec<u8> {
|
||||
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<u8> {
|
||||
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<QuestRewardItem> {
|
||||
Ok(QuestRewardItem {
|
||||
item_id: read_u32_le(r)?,
|
||||
@@ -212,11 +173,9 @@ fn read_item_triple(r: &mut &[u8]) -> io::Result<QuestRewardItem> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a count-prefixed run of item triples (`u32 count` then that many triples).
|
||||
fn read_item_block(r: &mut &[u8]) -> io::Result<Vec<QuestRewardItem>> {
|
||||
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<QuestGiverList> {
|
||||
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<QuestDetails> {
|
||||
@@ -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<QuestOfferReward> {
|
||||
@@ -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<QuestRequestItems> {
|
||||
@@ -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<QuestComplete> {
|
||||
@@ -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<u8>, 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);
|
||||
|
||||
Reference in New Issue
Block a user