the dressing room opens, and the pet gets its paper doll
- ctrl-click an item to try it on: the dressing room dresses your double, and shift-click posts the item link to chat - the pet tab lands in the character window: paper doll stats, family and diet, the right-click menu, and dropping food on the pet feeds it - water-only spells refuse on dry land: aquatic form gates to swimming, client-side like the real one - the macro icon picker's white squares get their art back, and option rows explain themselves on hover Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -347,6 +347,10 @@ pub enum SessionEvent {
|
||||
/// The template's `CreatureType.dbc` id (Beast, Humanoid, Critter, …) — the TAB-target
|
||||
/// critter/totem filter's input. `None` on a server miss.
|
||||
creature_type: Option<u32>,
|
||||
/// The template's `CreatureFamily.dbc` id (Wolf, Cat, Imp, …) — `UnitCreatureFamily`'s
|
||||
/// word and, through that row's pet-food mask, the diet tooltip (decision 1062). `0` for
|
||||
/// everything that is neither a tameable beast nor a warlock minion, and `0` on a miss.
|
||||
pet_family: u32,
|
||||
/// Elite rank 0..4 (the unit tooltip's rank word, decision 0276). `0` on a miss.
|
||||
rank: u32,
|
||||
/// The template type flags — bit `0x10` hides the tooltip's faction-name line. `0` on a miss.
|
||||
|
||||
@@ -608,28 +608,42 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
vec![SessionEvent::PetName { pet_number, name }]
|
||||
}
|
||||
ServerPacket::CreatureQueryResponse { entry, info } => {
|
||||
let (name, subname, creature_type, rank, type_flags, civilian, racial_leader) =
|
||||
match info {
|
||||
Some(i) => (
|
||||
Some(i.name),
|
||||
// An EMPTY wire subname is NO subname (vmangos sends "" for creatures
|
||||
// without one; the client renders no line for it — the nameplate
|
||||
// builder's verified shape, 0x608f50). Mapped here, once, so no consumer
|
||||
// ever sees a present-but-empty tooltip/nameplate line.
|
||||
Some(i.subname).filter(|s| !s.is_empty()),
|
||||
Some(i.creature_type),
|
||||
i.rank,
|
||||
i.type_flags,
|
||||
i.civilian,
|
||||
i.racial_leader,
|
||||
),
|
||||
None => (None, None, None, 0, 0, false, false),
|
||||
};
|
||||
let (
|
||||
name,
|
||||
subname,
|
||||
creature_type,
|
||||
pet_family,
|
||||
rank,
|
||||
type_flags,
|
||||
civilian,
|
||||
racial_leader,
|
||||
) = match info {
|
||||
Some(i) => (
|
||||
Some(i.name),
|
||||
// An EMPTY wire subname is NO subname (vmangos sends "" for creatures
|
||||
// without one; the client renders no line for it — the nameplate
|
||||
// builder's verified shape, 0x608f50). Mapped here, once, so no consumer
|
||||
// ever sees a present-but-empty tooltip/nameplate line.
|
||||
Some(i.subname).filter(|s| !s.is_empty()),
|
||||
Some(i.creature_type),
|
||||
// NOT wrapped in an `Option` like the type beside it: family `0` already
|
||||
// means "this template has no family", which is exactly what a miss means
|
||||
// too, so both answer the same `0` rather than two shapes for one state
|
||||
// (`CreatureFamily.dbc` has no row 0 — decision 1062).
|
||||
i.pet_family,
|
||||
i.rank,
|
||||
i.type_flags,
|
||||
i.civilian,
|
||||
i.racial_leader,
|
||||
),
|
||||
None => (None, None, None, 0, 0, 0, false, false),
|
||||
};
|
||||
vec![SessionEvent::CreatureName {
|
||||
entry,
|
||||
name,
|
||||
subname,
|
||||
creature_type,
|
||||
pet_family,
|
||||
rank,
|
||||
type_flags,
|
||||
civilian,
|
||||
|
||||
@@ -130,12 +130,13 @@ pub use opcode_names::opcode_name;
|
||||
pub use packet::{CreatureQueryInfo, MonsterMoveFacing, ServerPacket};
|
||||
pub use parse::parse_server;
|
||||
pub use pet::{
|
||||
pet_action, pet_cancel_aura, pet_set_action, pet_spell_autocast, pet_stop_attack,
|
||||
PetActionEntry, PetMode, PetSpellCooldown, PetSpells, PET_ACTION_SLOTS, PET_ACT_COMMAND,
|
||||
PET_ACT_DISABLED, PET_ACT_ENABLED, PET_ACT_PASSIVE, PET_ACT_REACTION, PET_AUTOCAST_ALLOWED,
|
||||
PET_AUTOCAST_ON, PET_COMMAND_ATTACK, PET_COMMAND_DISMISS, PET_COMMAND_FOLLOW, PET_COMMAND_STAY,
|
||||
PET_COOLDOWN_PERMANENT, PET_REACT_AGGRESSIVE, PET_REACT_DEFENSIVE, PET_REACT_PASSIVE,
|
||||
PET_STATE_BAR_DISABLED, PET_TYPE_SPELL_FIRST, PET_TYPE_SPELL_LAST, PET_UNUSABLE_UNIT_FLAGS,
|
||||
pet_abandon, pet_action, pet_cancel_aura, pet_rename, pet_set_action, pet_spell_autocast,
|
||||
pet_stop_attack, PetActionEntry, PetMode, PetSpellCooldown, PetSpells, PET_ACTION_SLOTS,
|
||||
PET_ACT_COMMAND, PET_ACT_DISABLED, PET_ACT_ENABLED, PET_ACT_PASSIVE, PET_ACT_REACTION,
|
||||
PET_AUTOCAST_ALLOWED, PET_AUTOCAST_ON, PET_COMMAND_ATTACK, PET_COMMAND_DISMISS,
|
||||
PET_COMMAND_FOLLOW, PET_COMMAND_STAY, PET_COOLDOWN_PERMANENT, PET_REACT_AGGRESSIVE,
|
||||
PET_REACT_DEFENSIVE, PET_REACT_PASSIVE, PET_STATE_BAR_DISABLED, PET_TYPE_SPELL_FIRST,
|
||||
PET_TYPE_SPELL_LAST, PET_UNUSABLE_UNIT_FLAGS,
|
||||
};
|
||||
pub use pose::{set_sheathed, stand_state_change};
|
||||
pub use progression::{learn_talent, ExplorationXp, LevelUpInfo, XpGain};
|
||||
|
||||
@@ -256,6 +256,29 @@ pub const CMSG_PET_STOP_ATTACK: u16 = 0x02EA; // 746
|
||||
/// whose aura to drop.
|
||||
pub const CMSG_PET_CANCEL_AURA: u16 = 0x026B; // 619
|
||||
|
||||
// The pet right-click menu's two outbound verbs (decision 1066; VERIFIED vmangos
|
||||
// `Opcodes_1_12_1.h`: 374/375). Bodies in [`super::pet`].
|
||||
/// **Give the pet up permanently** — the right-click menu's Abandon row, and `PetAbandon
|
||||
/// 0x4be4c0`'s only packet (wow-re `ui/scratch/pet-action-bar-api.md` §11c, `push 0x176`
|
||||
/// @`0x4bd765`). `HandlePetAbandon` deletes a hunter pet (`PET_SAVE_AS_DELETED`) and merely
|
||||
/// unsummons anything else (`PET_SAVE_NOT_IN_SLOT`, `PetHandler.cpp:347-374`).
|
||||
///
|
||||
/// The menu's **Dismiss** row is NOT this opcode, which is worth stating because the two would be
|
||||
/// indistinguishable in play: `PetDismiss 0x4be4d0` opens no packet at all and hands the packed
|
||||
/// word `0x07000003` to the pet bar's own dispatcher, so it leaves as [`CMSG_PET_ACTION`].
|
||||
pub const CMSG_PET_ABANDON: u16 = 0x0176; // 374
|
||||
/// Rename the pet — `PetRename 0x4be4e0` → `0x4bd840` (`push 0x177` @`0x4bd8fe`). Gated server-side
|
||||
/// on `UNIT_FLAG_PET_RENAME` and **one-shot**: the handler clears that bit on success, so the row
|
||||
/// disappears after the first rename (`PetHandler.cpp:302-345`). Nothing client-side clears it —
|
||||
/// wow-re's census found no writer of that byte anywhere in `.text`.
|
||||
///
|
||||
/// A refused name answers with `SMSG_PET_NAME_INVALID` (0x178), which is **not modelled** and does
|
||||
/// not need to be: vmangos sends it with an empty body — `SendPetNameInvalid` drops both the reason
|
||||
/// code and the name with the comment "not read by vanilla client" (`PetHandler.cpp:542-548`) — and
|
||||
/// the shipped 1.12 `FrameXML` has no event and no string for it. It lands in `ServerPacket::Other`
|
||||
/// under its own name, which is the whole of what there is to do with it.
|
||||
pub const CMSG_PET_RENAME: u16 = 0x0177; // 375
|
||||
|
||||
/// VERIFIED vmangos `Opcodes_1_12_1.h`: 94 (decision 0236). Body in
|
||||
/// [`super::gameobject::gameobject_query`] — the ask-once GO template lookup, identical shape to
|
||||
/// `CMSG_CREATURE_QUERY`.
|
||||
|
||||
@@ -44,6 +44,11 @@ pub struct CreatureQueryInfo {
|
||||
pub subname: String,
|
||||
/// `CreatureType.dbc` id (Beast/Humanoid/… — the level line's type word + the TAB filter).
|
||||
pub creature_type: u32,
|
||||
/// `CreatureFamily.dbc` id (Wolf/Cat/Imp/… — `UnitCreatureFamily`'s row, and through that row's
|
||||
/// pet-food mask, `GetPetFoodTypes`'s diet list; decision 1062). `0` for anything that is not a
|
||||
/// tameable beast or a warlock minion — which is most of the table, and the reason both
|
||||
/// consumers treat a missing row as nil rather than as an error.
|
||||
pub pet_family: u32,
|
||||
/// Elite rank: 0 normal, 1 elite, 2 rare-elite, 3 world boss, 4 rare — the unit tooltip's
|
||||
/// rank word `{"", Elite, Elite, Boss, ""}` (decision 0276's byte-verified table).
|
||||
pub rank: u32,
|
||||
@@ -226,9 +231,9 @@ pub enum ServerPacket {
|
||||
},
|
||||
/// `SMSG_CREATURE_QUERY_RESPONSE` — a creature template's display data, answering
|
||||
/// `CMSG_CREATURE_QUERY`. We surface what the UI consumes ([`CreatureQueryInfo`]: name,
|
||||
/// subname, `CreatureType.dbc` id, the elite **rank**, the civilian flag); the rest of the
|
||||
/// tail (type flags/family/pet spells/display id/racial leader) is parsed for alignment and
|
||||
/// dropped until a consumer exists. A **miss** (unknown entry) is a lone `u32` of
|
||||
/// subname, `CreatureType.dbc` id, the `CreatureFamily.dbc` id, the elite **rank**, the type
|
||||
/// flags, and the civilian/racial-leader pair); only `unk`, the pet spell-list id and the
|
||||
/// display id are still parsed for alignment and dropped. A **miss** (unknown entry) is a lone `u32` of
|
||||
/// `entry | 0x8000_0000` → `None` (VERIFIED vmangos `HandleCreatureQueryOpcode`, both
|
||||
/// branches).
|
||||
CreatureQueryResponse {
|
||||
|
||||
@@ -765,13 +765,15 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
// The tail (VERIFIED vmangos `HandleCreatureQueryOpcode`, 5875 = every build
|
||||
// guard included): type_flags, type, pet_family, RANK, unk, pet_spell_list_id,
|
||||
// display_id (u32 ×7), then civilian + racial_leader (u8 ×2). We keep `type`
|
||||
// (the `CreatureType.dbc` id — the TAB-target filter's input), `rank` (the unit
|
||||
// tooltip's Elite/Boss word, decision 0276's level-line law), `type_flags` (bit
|
||||
// 0x10 hides its faction line), and the `civilian`/`racial_leader` pair (its
|
||||
// green CIVILIAN / white LEADER lines); the rest stays alignment-only.
|
||||
// (the `CreatureType.dbc` id — the TAB-target filter's input), `pet_family` (the
|
||||
// `CreatureFamily.dbc` id behind `UnitCreatureFamily` and the diet tooltip,
|
||||
// decision 1062), `rank` (the unit tooltip's Elite/Boss word, decision 0276's
|
||||
// level-line law), `type_flags` (bit 0x10 hides its faction line), and the
|
||||
// `civilian`/`racial_leader` pair (its green CIVILIAN / white LEADER lines); the
|
||||
// rest stays alignment-only.
|
||||
let type_flags = read_u32_le(&mut r)?;
|
||||
let creature_type = read_u32_le(&mut r)?;
|
||||
let _pet_family = read_u32_le(&mut r)?;
|
||||
let pet_family = read_u32_le(&mut r)?;
|
||||
let rank = read_u32_le(&mut r)?;
|
||||
let _unk = read_u32_le(&mut r)?;
|
||||
let _pet_spell_list = read_u32_le(&mut r)?;
|
||||
@@ -784,6 +786,7 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
name,
|
||||
subname,
|
||||
creature_type,
|
||||
pet_family,
|
||||
rank,
|
||||
type_flags,
|
||||
civilian,
|
||||
|
||||
@@ -484,6 +484,32 @@ 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.
|
||||
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.
|
||||
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());
|
||||
body.extend_from_slice(name.as_bytes());
|
||||
body.push(0);
|
||||
body
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -671,6 +697,22 @@ mod tests {
|
||||
// form is 16 bytes and the two-entry form is 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.
|
||||
assert_eq!(
|
||||
pet_rename(0x2A, "Bruce"),
|
||||
[&0x2Au64.to_le_bytes()[..], b"Bruce\0"].concat()
|
||||
);
|
||||
assert_eq!(
|
||||
pet_rename(0x2A, "").len(),
|
||||
9,
|
||||
"an empty name is still framed"
|
||||
);
|
||||
}
|
||||
|
||||
/// The autocast flip touches **bit 30 and nothing else** — the client's own in-place edit, and
|
||||
|
||||
@@ -112,6 +112,14 @@ const FIELD_UNIT_MOUNTDISPLAYID: u16 = 133;
|
||||
/// client decides a creature has **no** classification (rank forced to 0), so no elite dragon, no
|
||||
/// ELITE/BOSS tooltip word and no world-boss skull on an enslaved mob.
|
||||
const FIELD_UNIT_PETNUMBER: u16 = 139;
|
||||
/// `UNIT_FIELD_PET_NAME_TIMESTAMP` (`OBJECT_END(6) + 0x86` = 140, PUBLIC — vmangos
|
||||
/// `UpdateFields_1_12_1.cpp:80`) — the unix second at which this pet's name was last set.
|
||||
///
|
||||
/// It matters because a pet's name does **not** ride its descriptor: it is answered once by
|
||||
/// `CMSG_PET_NAME_QUERY` and cached by pet NUMBER, so a rename would otherwise stay invisible until
|
||||
/// relog. The server bumps this on every `SetName` (`Pet.cpp:285`, and `HandlePetRename`'s last
|
||||
/// line `PetHandler.cpp:344`), which makes a **change** here the cache's one invalidation signal.
|
||||
const FIELD_UNIT_PET_NAME_TIMESTAMP: u16 = 140;
|
||||
/// `UNIT_FIELD_PETEXPERIENCE` (idx 141) / `UNIT_FIELD_PETNEXTLEVELEXP` (142) — a hunter pet's XP
|
||||
/// bar, the `(currXP, nextXP)` pair `GetPetExperience 0x4be840` returns in that order.
|
||||
/// Byte-verified client-side at `[unit+0x110]+0x21c` / `+0x220` (wow-re
|
||||
@@ -127,6 +135,15 @@ const FIELD_UNIT_DYNAMIC_FLAGS: u16 = 143;
|
||||
/// `UNIT_CHANNEL_SPELL` (idx 144, `OBJECT_END(6) + 0x8A`, PUBLIC — VERIFIED vmangos
|
||||
/// `UpdateFields_1_12_1.h:89`) — the spell id a unit is channeling (`0` = not channeling).
|
||||
const FIELD_UNIT_CHANNEL_SPELL: u16 = 144;
|
||||
/// `UNIT_CREATED_BY_SPELL` (idx 146, `OBJECT_END(6) + 0x8C`, PUBLIC — vmangos
|
||||
/// `UpdateFields_1_12_1.h`) — the spell that summoned this unit (`0` = not summoned by one). The
|
||||
/// **first** of the three gates on the reference's feed-pet path `0x6ea1e0`: a unit with no
|
||||
/// creating spell is not a pet you can feed, whatever else it is.
|
||||
///
|
||||
/// The index is fixed by construction rather than asserted from a header we don't ship: this table
|
||||
/// already pins both ends of a contiguous run — `PETEXPERIENCE` 141, `PETNEXTLEVELEXP` 142,
|
||||
/// `DYNAMIC_FLAGS` 143, `CHANNEL_SPELL` 144, `MOD_CAST_SPEED` 145, then this, then `NPC_FLAGS` 147.
|
||||
const FIELD_UNIT_CREATED_BY_SPELL: u16 = 146;
|
||||
const FIELD_UNIT_NPC_FLAGS: u16 = 147;
|
||||
/// `UNIT_NPC_EMOTESTATE` (`OBJECT_END + 0x8E`; PUBLIC — VERIFIED vmangos `UpdateFields_1_12_1.h:93`)
|
||||
/// — the unit's looping **state emote** as an `Emotes.dbc` id (dance, NPC cooking/working flavor).
|
||||
|
||||
@@ -6,7 +6,15 @@ use super::*;
|
||||
|
||||
impl ObjectFields {
|
||||
/// `OBJECT_FIELD_ENTRY` (3) — the object's template entry (an item object's item id, a
|
||||
/// GameObject's template). The one field that names WHAT a streamed object is.
|
||||
/// GameObject's template, a creature's `creature_template` row). The one field that names WHAT
|
||||
/// a streamed object is.
|
||||
///
|
||||
/// **For a PET this is the only route to its template**, and that is load-bearing: a
|
||||
/// `HIGHGUID_PET` guid carries a *pet number* in the slot a creature's entry occupies
|
||||
/// ([`crate::guid::pet_number`]), so [`crate::guid::entry`] refuses it — but vmangos still
|
||||
/// writes the real `cinfo->entry` here (`Creature::InitEntry`, `Creature.cpp:376`,
|
||||
/// `SetEntry(entry) // normal entry always`). Decision 1062's creature-family lookup goes
|
||||
/// through this field for exactly that reason.
|
||||
pub fn object_entry(&self) -> Option<u32> {
|
||||
self.get_u32(3)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,13 @@ impl ObjectFields {
|
||||
pub fn unit_created_by(&self) -> Option<u64> {
|
||||
self.get_guid(FIELD_UNIT_CREATEDBY).filter(|&g| g != 0)
|
||||
}
|
||||
/// `UNIT_CREATED_BY_SPELL` — the spell that summoned this unit, `None` when zero (nothing
|
||||
/// summoned it). The reference's feed-pet validator `0x6ea1e0` requires it non-zero before it
|
||||
/// will accept a unit as a feedable pet — the first of that function's three gates.
|
||||
pub fn unit_created_by_spell(&self) -> Option<u32> {
|
||||
self.get_u32(FIELD_UNIT_CREATED_BY_SPELL)
|
||||
.filter(|&s| s != 0)
|
||||
}
|
||||
/// `UNIT_FIELD_PETNUMBER` — non-zero iff this unit is somebody's **pet or charm**.
|
||||
///
|
||||
/// Not a count and not an index anyone displays: the client reads it as a pure boolean, as the
|
||||
@@ -125,6 +132,15 @@ impl ObjectFields {
|
||||
pub fn unit_is_pet_or_charm(&self) -> bool {
|
||||
self.get_u32(FIELD_UNIT_PETNUMBER).unwrap_or(0) != 0
|
||||
}
|
||||
/// `UNIT_FIELD_PET_NAME_TIMESTAMP` — when this pet's name was last set. `None` when the field
|
||||
/// has never been sent (a create omits zeros), which reads the same as "never renamed".
|
||||
///
|
||||
/// The **only** thing to do with the value is compare it with the last one seen: a pet's name
|
||||
/// lives in a query cache keyed by pet number, and this changing is what says that cache has
|
||||
/// gone stale (see the constant).
|
||||
pub fn unit_pet_name_timestamp(&self) -> Option<u32> {
|
||||
self.get_u32(FIELD_UNIT_PET_NAME_TIMESTAMP)
|
||||
}
|
||||
/// `UNIT_FIELD_HEALTH` — current hit points (0 ⇒ dead).
|
||||
pub fn unit_health(&self) -> Option<u32> {
|
||||
self.get_u32(FIELD_UNIT_HEALTH)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The pet bar's `WorldWriter` sends — mirroring [`crate::messages::pet`] (decisions 0982, 0988).
|
||||
//! The pet's `WorldWriter` sends — mirroring [`crate::messages::pet`] (decisions 0982, 0988, 1066).
|
||||
//!
|
||||
//! Four verbs, and the thing to know about them is that **no reply packet answers any of them**.
|
||||
//! The first three are intents the client has already applied to its own state before sending
|
||||
//! Six verbs, and the thing to know about the bar's four is that **no reply packet answers any of
|
||||
//! them**. The first three are intents the client has already applied to its own state before sending
|
||||
//! (wow-re §10.1/§10.2: the reaction, command and autocast paths all write `[0xb71468]` or the
|
||||
//! slot word and fire `PET_BAR_UPDATE` *before* the packet leaves). The bar's contents change only
|
||||
//! when a fresh `SMSG_PET_SPELLS` arrives — a summon, a swap, a learned spell — never as a reply
|
||||
@@ -12,6 +12,11 @@
|
||||
//! descriptor. The aura leaves in a `UNIT_FIELD_AURA` delta, and the slot's icon follows from
|
||||
//! that — so the round trip is visible, just not as an answer.
|
||||
//!
|
||||
//! The menu's two ([`WorldWriter::pet_abandon`], [`WorldWriter::pet_rename`]) invert that: neither
|
||||
//! is pre-applied, because what each asks for is not a UI state the server ignores but a change to
|
||||
//! the world that it may refuse — and the answer arrives as the pet leaving, or as a bumped name
|
||||
//! timestamp in its descriptor.
|
||||
//!
|
||||
//! Every one carries the pet's guid: the server re-checks ownership on each
|
||||
//! (`PetHandler.cpp`'s `GetCharmerOrOwnerGuid` gate) and silently drops anything naming a unit we
|
||||
//! don't control.
|
||||
@@ -90,4 +95,29 @@ impl WorldWriter {
|
||||
&messages::pet_spell_autocast(pet_guid, spell_id, enabled),
|
||||
)
|
||||
}
|
||||
|
||||
/// Give the pet up (`CMSG_PET_ABANDON`, layout in [`messages::pet_abandon`]) — the right-click
|
||||
/// menu's Abandon **and** its Dismiss, which are one message (decision 1066).
|
||||
///
|
||||
/// Unlike everything above it this one is not pre-applied and does not need to be: what answers
|
||||
/// it is the pet leaving — `SMSG_PET_SPELLS` with a zero guid tears the bar down, and the
|
||||
/// object's own removal takes the frame with it. Optimism would only race that.
|
||||
pub fn pet_abandon(&mut self, pet_guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_PET_ABANDON, &messages::pet_abandon(pet_guid))
|
||||
}
|
||||
|
||||
/// Rename the pet (`CMSG_PET_RENAME`, layout in [`messages::pet_rename`]) — the `RENAME_PET` →
|
||||
/// `PETRENAMECONFIRM` popup chain's payload.
|
||||
///
|
||||
/// Also not pre-applied, and here that is load-bearing: the server validates the name
|
||||
/// (`ObjectMgr::CheckPetName`, the reserved-name list) and may refuse it outright, so a client
|
||||
/// that wrote the new name locally would show a name the world does not agree with. What
|
||||
/// arrives on success is a bumped `UNIT_FIELD_PET_NAME_TIMESTAMP` in the pet's descriptor,
|
||||
/// which is what makes the name cache re-ask.
|
||||
pub fn pet_rename(&mut self, pet_guid: u64, name: &str) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PET_RENAME,
|
||||
&messages::pet_rename(pet_guid, name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user