Files
benilla-octo/crates/benilla-protocol/src/events.rs
T
SamandClaude Opus 5.5 4c2f3cd196 comments: protocol, the session events and their decoder
1,081 comment lines become 459; the code is unchanged. Corrections:
- MonsterMove's description sat atop MoveTimeSkipped's doc and
  documented the wrong variant; it is back on MonsterMove.
- GossipMenu's tuple doc gains its missing level.
- Four false claims are gone: "only primitives" on SessionEvent, a
  code field LoginFailed does not have, a broken object_scale link,
  and "filled from nothing else" on SpellChainTargets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 20:11:09 +07:00

1125 lines
56 KiB
Rust

//! Decoded session events; coordinates stay raw WoW, which the app maps to `bevy = (-y, z, -x)`.
use crate::messages::{
ActionButton, AttackSwingError, AttackerState, AuctionBidderNotification, AuctionCommandTail,
AuctionListEntry, AuctionOwnerNotification, ChannelNoticeTail, Character, CreateSpline,
DamageShield, DispelFailed, EnchantmentLog, EnvironmentalDamageLog, ExplorationXp, FriendEntry,
FriendStatusUpdate, GmTicket, GossipOption, GroupLootInfo, GroupMemberEntry,
GuildCommandResult, GuildEventNotice, GuildInfo, GuildQueryResponse, GuildRoster,
InspectHonorStats, ItemInfo, ItemPushResult, JumpInfo, LevelUpInfo, LootAllPassed, LootItem,
LootRoll, LootRollWon, LootStartRoll, MailListEntry, MirrorTimerStart, MonsterMoveFacing,
MoverState, ObjectFields, PartyKillLog, PartyMemberStatsInfo, PeriodicAuraLog, PetMode,
PetSpells, PetitionQueryResponse, PetitionRename, PetitionShowList, PetitionShowSignatures,
PetitionSignResults, PvpCredit, QuestComplete, QuestConfirmAccept, QuestDetails,
QuestGiverList, QuestOfferReward, QuestRequestItems, QuestShareMsg, QuestTemplate,
SpellDamageLog, SpellDispelLog, SpellEnergizeLog, SpellHealLog, SpellInstaKillLog,
SpellLogExecute, SpellLogMiss, SpellOutcomeLog, StabledPet, TaxiMask, TradeStatus,
TradeStatusExtended, TrainerSpell, TransportPose, VendorItem, WhoResults, XpGain,
};
/// Coarse entity classification, free of wire types, carried on [`SessionEvent::ObjectCreate`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EntityKind {
Player,
Unit,
GameObject,
/// `TYPEID_DYNAMICOBJECT` (6): an area effect's anchor, drawn via `DYNAMICOBJECT_SPELLID`.
DynamicObject,
/// `TYPEID_CORPSE` (7): a dead player's body or bone pile, drawn as the player from its
/// `CORPSE_FIELD_BYTES_*` and `CORPSE_FIELD_ITEM`.
Corpse,
Other,
}
/// Which character verb a [`SessionEvent::CharActionResult`] answers, and so which code family.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CharAction {
Create,
Delete,
}
/// A unit's six movement speeds from its `LIVING` block: yd/s, except `turn_rate` in rad/s.
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct MoveSpeeds {
pub walk: f32,
pub run: f32,
pub run_back: f32,
pub swim: f32,
pub swim_back: f32,
pub turn_rate: f32,
}
impl MoveSpeeds {
fn from_wire(s: [f32; 6]) -> Self {
Self {
walk: s[0],
run: s[1],
run_back: s[2],
swim: s[3],
swim_back: s[4],
turn_rate: s[5],
}
}
}
/// A login attempt's pre-roster stage; the connecting dialog shows its `LOGIN_STATE_*` string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoginStage {
/// Dialing the auth server (`LOGIN_STATE_CONNECTING`).
Connecting,
/// SRP6 challenge/proof in flight (`LOGIN_STATE_AUTHENTICATING`).
Authenticating,
/// Realm picked; the world-socket handshake and char enum (`LOGIN_STATE_HANDSHAKING`).
Handshaking,
}
/// How a world session ended. On a loss the reference fires `DISCONNECTED_FROM_SERVER` and
/// `GlueParent.lua` returns to the login screen; a logout returns to character select.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionEnd {
/// A logout the server confirmed (`SMSG_LOGOUT_COMPLETE`); a fresh roster follows.
LoggedOut,
/// The stream died: a socket error, an EOF or a post-roster handshake failure. A displacement
/// kick is this too: vmangos `WorldSession::KickPlayer` only closes the socket, no packet.
Lost,
}
/// Which server refused the login, and its result byte. The two enums overlap: 0x0C is
/// `AUTH_LOGON_FAILED_SUSPENDED` to realmd but `AUTH_OK` to the world server.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoginRefusal {
/// realmd's logon-proof `AuthLogonResult` (`crate::AuthReject`).
Logon(u8),
/// The world server's `SMSG_AUTH_RESPONSE` code (`crate::WorldAuthReject`).
World(u8),
}
impl LoginRefusal {
/// The raw byte, for logging only: without its server it is ambiguous.
pub fn byte(self) -> u8 {
match self {
LoginRefusal::Logon(b) | LoginRefusal::World(b) => b,
}
}
}
/// One decoded world-stream event; its fieldless twin [`SessionEventKind`] keys the handler table.
#[derive(Debug, Clone, strum::EnumDiscriminants)]
#[strum_discriminants(
name(SessionEventKind),
derive(Hash, PartialOrd, Ord, strum::EnumIter, strum::IntoStaticStr)
)]
pub enum SessionEvent {
/// A login attempt reached `stage`; emitted by the IO thread, never wire-decoded.
LoginStage { stage: LoginStage },
/// The account's realms (`CMD_REALM_LIST`); the IO thread blocks for the app's pick. Re-sent on
/// each refresh (the reference re-requests every 5 s) and on a realm change.
RealmList { realms: Vec<crate::RealmInfo> },
/// Queued for a full realm (`AUTH_WAIT_QUEUE`), per queue packet: a wait, not an outcome.
LoginQueued {
position: Option<u32>,
realm: Option<String>,
},
/// A login failed before the roster; `refusal` is `None` for a transport failure. A `terminal`
/// one ([`crate::WardenRequired`]) no retry can fix: show `reason` and stop.
LoginFailed {
refusal: Option<LoginRefusal>,
reason: String,
terminal: bool,
/// `Some` only when the socket never opened: which dial failure, and the address.
dial: Option<crate::DialFailure>,
},
/// The roster (`SMSG_CHAR_ENUM`); the IO thread then blocks for the app's pick before
/// `CMSG_PLAYER_LOGIN`. `realm` is `None` on the raw [`crate::decode`] path, which has no auth.
CharacterList {
characters: Vec<Character>,
realm: Option<crate::RealmInfo>,
},
/// `SMSG_CHAR_CREATE`/`_DELETE`'s raw `WorldResult`; on success a fresh roster comes first.
CharActionResult { action: CharAction, code: u8 },
/// We are in the world; the IO thread emits this before any object update.
Connected {
self_guid: u64,
name: String,
/// Rested billing minutes for `GetBillingTimeRested()`, sent once, at auth.
billing_time_rested: u32,
/// `SMSG_TUTORIAL_FLAGS` if it came during the handshake; `None` if it comes in-world.
tutorial_flags: Option<Vec<u8>>,
/// The addons `SMSG_ADDON_INFO` hid. `None` is no reply at all, where the reference's
/// `GetNumAddOns()` answers 0; an empty list is a reply that hid nothing.
addon_info: Option<Vec<String>>,
},
/// Our pick was refused (`SMSG_CHARACTER_LOGIN_FAILED`) after [`Self::Connected`] was sent;
/// `result` is a 1-based index into the reference's six `CHAR_LOGIN_*` strings.
CharacterLoginFailed { result: u8 },
/// Logout confirmed (`SMSG_LOGOUT_COMPLETE`); a fresh [`Self::CharacterList`] follows.
LoggedOut,
/// `SMSG_LOGOUT_RESPONSE`. A non-zero `reason` refuses: 1 in combat, 2 GM-frozen, 3 falling
/// (vmangos). Zero starts the 20 s camp timer, or with `instant` (resting, taxi, GM) logs out.
LogoutResponse { reason: u32, instant: bool },
/// The server dropped a pending logout at our `CMSG_LOGOUT_CANCEL` (`SMSG_LOGOUT_CANCEL_ACK`).
LogoutCancelled,
/// The session ended; `reason` is human-readable.
Disconnected { reason: String, end: SessionEnd },
/// A placed object entered view, its spawn identity decoded per object type.
ObjectCreate {
guid: u64,
kind: EntityKind,
/// The type's own display-id field; `None` when absent or zero.
display_id: Option<u32>,
position: [f32; 3],
orientation: f32,
/// `OBJECT_FIELD_SCALE_X`, the complete render scale; `1.0` when absent.
scale: f32,
/// From the `LIVING` block, so not in `fields`; `None` for a GameObject.
speeds: Option<MoveSpeeds>,
/// Flags and swim pitch the unit streams in with, applied by the reference before its model
/// exists: the flags via the relay's `0x75a07dff` merge, the pitch via `0x7c6420`.
mover: Option<MoverState>,
/// `UPDATE_FLAG_TRANSPORT`'s path progress in ms, only on a type-11/15 transport create;
/// progress is this plus elapsed ms; position is `timetable(progress % period)`.
transport_progress: Option<u32>,
/// Its rider pose, local to the transport, when created with `MOVEFLAG_ON_TRANSPORT`.
transport: Option<TransportPose>,
/// The path it was already walking (`MOVEFLAG_SPLINE_ENABLED`), with the ms already ridden.
spline: Option<CreateSpline>,
/// The full descriptor from the create mask; later [`Self::ObjectValues`] merge into it.
fields: ObjectFields,
},
/// An item or container: a pose-less descriptor (vmangos sends `UPDATEFLAG_ALL` and a
/// constant); its slot is in the owner's `PLAYER_FIELD_*_SLOT` or `CONTAINER_FIELD_SLOT`.
ItemCreate {
guid: u64,
container: bool,
/// The full descriptor; `OBJECT_FIELD_ENTRY` is the template for `CMSG_ITEM_QUERY_SINGLE`.
fields: ObjectFields,
},
/// An update-object movement block: a one-off authoritative pose that supersedes any path.
ObjectMove {
guid: u64,
position: [f32; 3],
orientation: f32,
},
/// A relayed player move (`MSG_MOVE_*`, about 2 Hz): `fall_time` is ms airborne, `jump` rides
/// with `JUMPING`, `transport` is deck-local with `MOVEFLAG_ON_TRANSPORT`.
UnitMove {
guid: u64,
position: [f32; 3],
orientation: f32,
flags: u32,
/// Swim pitch in radians, up positive, `0.0` unless `MOVEFLAG_SWIMMING`; the reference's
/// swim velocity basis includes it (`0x7c5880`).
pitch: f32,
/// vmangos's ms clock stamped at receipt (`MovementInfo::Read`), one clock for all movers;
/// the reference paces a remote's apply by the deltas between consecutive stamps.
time: u32,
/// What the opcode adds to the pose: a heartbeat, a teleport, a root, or nothing.
verb: crate::messages::RelayVerb,
fall_time: u32,
jump: Option<JumpInfo>,
transport: Option<TransportPose>,
},
/// A descriptor `Values` delta, only the changed fields; never empty (the codec drops those).
ObjectValues { guid: u64, fields: ObjectFields },
/// Left view range (`OutOfRange`); the reference keeps them in a staging table for re-create.
ObjectsRemoved(Vec<u64>),
/// `SMSG_DESTROY_OBJECT`: the reference frees it at once, no fade; a respawn is a fresh create.
ObjectDestroyed(u64),
/// `SMSG_TRIGGER_CINEMATIC`. Until `CMSG_COMPLETE_CINEMATIC` vmangos anchors visibility to the
/// cinematic camera (`Player::UpdateCinematic`), despawning the world around the body.
CinematicTriggered { cinematic_id: u32 },
/// `MSG_MOVE_TIME_SKIPPED`: not a pose. The mover's relay stamp advances by `lag_ms`, as the
/// reference's `[CMovement+0xac] += lag` (`0x603b40`), or its next packet is scheduled late.
MoveTimeSkipped { guid: u64, lag_ms: u32 },
/// A server path: `path` is the travel-order polyline from `start`, walked at constant speed
/// over `duration_ms`; a stop, a zero duration or fewer than two points leaves it empty.
MonsterMove {
guid: u64,
/// `SMSG_MONSTER_MOVE_TRANSPORT`: `start`/`path` are deck-local, and the unit now rides it.
transport: Option<u64>,
start: [f32; 3],
/// Echoed in `CMSG_MOVE_SPLINE_DONE` when the spline drives our own player.
spline_id: u32,
path: Vec<[f32; 3]>,
/// The final facing (`moveType` 2/3/4), snapped; how a creature turns without walking.
facing: MonsterMoveFacing,
stop: bool,
duration_ms: u32,
/// Ground walks take Z from the terrain; flights and `transport` paths keep the wire Z.
flying: bool,
/// `SPLINEFLAG_RUNMODE`; its absence forces `MOVEFLAG_WALK_MODE` on the moved unit.
run_mode: bool,
},
/// Same-map teleport (`MSG_MOVE_TELEPORT_ACK`): the app snaps our player and echoes the ack.
Teleport {
guid: u64,
counter: u32,
position: [f32; 3],
orientation: f32,
},
/// `SMSG_NEW_WORLD` (acked) or the login `SMSG_LOGIN_VERIFY_WORLD`. After a transfer that
/// named a transport the pose is boat-local (vmangos `SendNewWorld` sends `GetTransportPos()`).
Worldport {
map_id: u32,
position: [f32; 3],
orientation: f32,
needs_ack: bool,
},
/// `SMSG_TRANSFER_PENDING`: a worldport follows, boat-local when `transport_entry` is set.
TransferPending {
map_id: u32,
transport_entry: Option<u32>,
},
/// `SMSG_TRANSFER_ABORTED`: the announced transfer is off (map full, no instance).
TransferAborted { reason: u8 },
/// The server in-game clock (`SMSG_LOGIN_SETTIMESPEED`): drives time-of-day lighting.
TimeSpeed {
hours: u8,
minutes: u8,
/// `year·372 + month·31 + day` of the packed server date; the moon phase's input.
day_serial: u32,
timescale: f32,
},
/// The server's wall clock in unix seconds (`SMSG_QUERY_TIME_RESPONSE`), the base of absolute
/// descriptor stamps: a timed quest's deadline is `time(nullptr) + limitTime`.
ServerUnixTime { unix_time: u32 },
/// The hearthstone bind (`SMSG_BINDPOINTUPDATE`): the AreaTable id the `$z` token names.
BindPoint { area: u32 },
/// `SMSG_GMTICKET_GETTICKET`, `None` for no ticket. The Help window re-polls every 10 minutes
/// and fires `UPDATE_TICKET` on each answer, changed or not: count these, never diff them.
GmTicket { ticket: Option<Box<GmTicket>> },
/// `SMSG_GMTICKET_CREATE`: 2 created, 3 refused, 1 already exists (vmangos never sends 1, and
/// sometimes sends nothing at all).
GmTicketCreated { response: u32 },
/// The answer to editing a ticket (`SMSG_GMTICKET_UPDATETEXT`): 4 = saved, 5 = refused.
GmTicketUpdated { response: u32 },
/// `SMSG_GMTICKET_DELETETICKET`: 9 = deleted; also unasked on a GM's `.ticket delete`.
GmTicketDeleted { response: u32 },
/// `SMSG_GMTICKETSYSTEMSTATUS`: 1 = the queue takes tickets; drives `UPDATE_GM_STATUS`.
GmTicketSystemStatus { status: i32 },
/// `SMSG_GM_TICKET_STATUS_UPDATE`: 1 updated, 2 closed, 3 survey; vmangos never sends it.
GmTicketStatusUpdate { status: u32 },
/// `SMSG_BINDER_CONFIRM`, the `CONFIRM_BINDER` dialog; echo `binder` in `CMSG_BINDER_ACTIVATE`.
BinderConfirm { binder: u64 },
/// `SMSG_SUMMON_REQUEST`: `zone` is the summoner's AreaTable id, `delay_ms` the auto-decline
/// hold. Accepting sends `CMSG_SUMMON_RESPONSE`; there is no decline opcode.
SummonRequest {
summoner: u64,
zone: u32,
delay_ms: u32,
},
/// `MSG_TALENT_WIPE_CONFIRM`, `cost` in copper; accepting echoes it, declining sends nothing.
TalentWipeConfirm { trainer: u64, cost: u32 },
/// `SMSG_PET_UNLEARN_CONFIRM`: accept with `CMSG_PET_UNLEARN`; declining sends nothing.
PetUnlearnConfirm { trainer: u64, cost: u32 },
/// The instance-boot clock (`SMSG_RAID_GROUP_ONLY`): a positive delay arms it
/// (`INSTANCE_BOOT_START`), zero clears it (`INSTANCE_BOOT_STOP`) and shows `reason` 1 or 2.
RaidGroupOnly { delay_ms: u32, reason: u32 },
/// A spirit healer's next resurrection wave (`SMSG_AREA_SPIRIT_HEALER_TIME`): arms
/// `GetAreaSpiritHealerTime` and fires `AREA_SPIRIT_HEALER_IN_RANGE`.
AreaSpiritHealerTime { healer: u64, ms: u32 },
/// One of the client's three battleground queue slots (`SMSG_BATTLEFIELD_STATUS`).
BattlefieldStatus(crate::messages::BattlefieldStatus),
/// The battleground scoreboard (`MSG_PVP_LOG_DATA`), rows in wire order, unsorted.
PvpLogData(crate::messages::PvpLogData),
/// The battleground instance list (`SMSG_BATTLEFIELD_LIST`); fires `BATTLEFIELDS_SHOW`.
BattlefieldList(crate::messages::BattlefieldList),
/// Teammate and flag-carrier positions (`MSG_BATTLEGROUND_PLAYER_POSITIONS`), raw WoW.
BattlefieldPositions(crate::messages::BattlefieldPositions),
/// `MSG_TABARDVENDOR_ACTIVATE`: the vendor whose tabard designer opens.
TabardVendorActivate(u64),
/// `MSG_SAVE_GUILD_EMBLEM`: the save's result row.
SaveGuildEmblemResult(u32),
/// `SMSG_GROUP_JOINED_BATTLEGROUND`: `0xFFFFFFFE` deserters, a map id joined, else the
/// generic failure; a message line each, no state.
GroupJoinedBattleground { result: u32 },
/// `SMSG_BATTLEGROUND_PLAYER_JOINED`/`_LEFT`, printed once the name cache has the guid.
BattlegroundPlayer { guid: u64, joined: bool },
/// The meeting-stone queue state (`SMSG 0x295`): the area queued for and a status byte.
MeetingStoneSetQueue { area: u32, status: u8 },
/// One of the meeting stone's four chat-line-only replies (`0x297/0x298/0x299/0x2BB`).
MeetingStoneNotice(crate::messages::MeetingStoneNotice),
/// The tutorial bank bytes (`SMSG_TUTORIAL_FLAGS`); the client copies both its banks from them.
TutorialFlags(Vec<u8>),
/// The bind took (`SMSG_PLAYERBOUND`); `area` matches the [`Self::BindPoint`] sent beside it.
PlayerBound { binder: u64, area: u32 },
/// One item class's subclass mask (`SMSG_SET_PROFICIENCY`), the reference's `0xc4d4a0[class]`.
Proficiency { item_class: u32, subclass_mask: u32 },
/// `SMSG_INITIALIZE_FACTIONS` at login: `(flags, standing)` per `Faction.dbc`
/// `reputationIndex`. The standing excludes the DBC race/class base; add it before ranking.
Reputations { standings: Vec<(u8, i32)> },
/// `SMSG_SET_FACTION_STANDING`: `(reputationListId, standing)` per slot, base excluded.
ReputationDelta { standings: Vec<(u32, i32)> },
/// `SMSG_SET_FACTION_VISIBLE`: sets the slot's visible flag, which decides if it is listed.
ReputationVisible { list_id: u32 },
/// `SMSG_NAME_QUERY_RESPONSE`: names are never descriptor fields in 1.12, only query answers.
/// An unknown guid answers with an empty `name`.
PlayerName {
guid: u64,
name: String,
race: u32,
gender: u32,
class: u32,
},
/// `SMSG_PET_NAME_QUERY_RESPONSE`, keyed by the pet number its guid carries in place of an
/// entry ([`crate::guid::pet_number`]). There is no miss reply: the server stays silent.
PetName { pet_number: u32, name: String },
/// `SMSG_CREATURE_QUERY_RESPONSE`, keyed by template `entry`. An unknown entry has a `None`
/// `name`, and every other field then reads `None`, `0` or `false`.
CreatureName {
entry: u32,
name: Option<String>,
subname: Option<String>,
/// `CreatureType.dbc` id; the TAB-target critter/totem filter reads it.
creature_type: Option<u32>,
/// `CreatureFamily.dbc` id for `UnitCreatureFamily` and the diet tooltip; `0` is none.
pet_family: u32,
/// Elite rank 0..4, the unit tooltip's rank word.
rank: u32,
/// Bit `0x10` hides the tooltip's faction-name line.
type_flags: u32,
/// `CreatureDisplayInfo.dbc` id, to draw a creature with no world object (a stabled pet).
display_id: u32,
/// The tooltip's green CIVILIAN line.
civilian: bool,
/// The tooltip's white LEADER line.
racial_leader: bool,
},
/// `SMSG_GAMEOBJECT_QUERY_RESPONSE`, keyed by template `entry`; `data` is the type-specific
/// raw tail. An unknown entry still fires, every field zero or empty.
GameObjectInfo {
entry: u32,
type_id: u32,
display_id: u32,
name: String,
data: [i32; 24],
},
/// `SMSG_GAMEOBJECT_CUSTOM_ANIM`: the reference arms substate `8 + anim_id`, AnimationData
/// 153..156 (Custom0..3), and rejects `anim_id >= 4`. A fishing bobber's bite sends 0.
GameObjectCustomAnim { guid: u64, anim_id: u32 },
/// `SMSG_GAMEOBJECT_DESPAWN_ANIM`: the reference arms substate 12 (AnimationData 157 Despawn)
/// and keeps the object alive through the same-tick `SMSG_DESTROY_OBJECT` while it plays.
GameObjectDespawnAnim { guid: u64 },
/// `SMSG_OPEN_CONTAINER`: the reference fires `BAG_OPEN` with 0 for our own guid (the
/// backpack), 1..10 for a bag-cache slot, and nothing for a miss.
OpenContainer { item: u64 },
/// Our own stand state (`SMSG_STANDSTATE_UPDATE`), applied ungated through the setter a
/// volunteered change uses (reference: `0x603e50` → `0x6127b0`).
StandStateUpdate { state: u8 },
/// Nothing hooked (`SMSG_FISH_NOT_HOOKED`, empty body): the red `ERR_FISH_NOT_HOOKED` line.
FishNotHooked,
/// The fish got away on the bobber click (`SMSG_FISH_ESCAPED`, empty body): `ERR_FISH_ESCAPED`.
FishEscaped,
/// A server-pushed 2D sound kit (`SMSG_PLAY_SOUND`): BG events, quest/zone scripts.
PlaySound { sound_id: u32 },
/// A server-pushed music kit for the music channel (`SMSG_PLAY_MUSIC`).
PlayMusic { music_id: u32 },
/// A server-pushed 3D sound kit at object `guid` (`SMSG_PLAY_OBJECT_SOUND`).
PlayObjectSound { sound_id: u32, guid: u64 },
/// The zone's weather (`SMSG_WEATHER`); `sound_id` is a SoundEntries loop, 8533..8558, 0 clear.
Weather {
weather_type: u32,
grade: f32,
sound_id: u32,
instant: bool,
},
/// A chat emote (`SMSG_TEXT_EMOTE`, `EmotesText.dbc`); empty `target_name` means untargeted.
TextEmote {
guid: u64,
text_emote: u32,
target_name: String,
},
/// A unit's anim emote (`SMSG_EMOTE`): an `Emotes.dbc` id, its anim and its `EventSoundID`.
Emote { guid: u64, emote_id: u32 },
/// The spell book at login (`SMSG_INITIAL_SPELLS`), ids widened from `u16`, and live cooldowns.
SpellBook {
spell_ids: Vec<u32>,
cooldowns: Vec<crate::messages::SpellCooldown>,
},
/// The saved action bar at login (`SMSG_ACTION_BUTTONS`): slots 0..119, 0..11 the main bar.
ActionButtons { buttons: Vec<ActionButton> },
/// A spell learned after login (`SMSG_LEARNED_SPELL`), widened from the wire `u16`.
SpellLearned { spell_id: u32 },
/// A spell unlearned (`SMSG_REMOVED_SPELL`), widened from `u16`; a respec sends one per rank.
SpellRemoved { spell_id: u32 },
/// A rank-up (`SMSG_SUPERCEDED_SPELL`) in book and bar; both ids widened from `u16`.
SpellSuperceded {
old_spell_id: u32,
new_spell_id: u32,
},
/// Our cast's verdict (`SMSG_CAST_RESULT`); `arg` fills its `%s`: a `SpellFocusObject.dbc` id
/// for `REQUIRES_SPELL_FOCUS`, an `AreaTable.dbc` id for `REQUIRES_AREA`.
CastResult {
spell_id: u32,
success: bool,
reason: Option<u8>,
arg: Option<u32>,
},
/// The whole pet bar (`SMSG_PET_SPELLS`), never a delta; a zero `pet_guid` tears it down.
PetSpells(Box<PetSpells>),
/// The pet's react/command state alone (`SMSG_PET_MODE`), with no bar edit.
PetMode(PetMode),
/// A refused pet order (`SMSG_PET_ACTION_FEEDBACK`): one reason code for the red error line.
PetActionFeedback { reason: u8 },
/// The pet's cast refusal (`SMSG_PET_CAST_FAILED`); it never touches our own cast state.
PetCastFailed { spell_id: u32, reason: Option<u8> },
/// A refused tame, Call Pet or Revive Pet (`SMSG_PET_TAME_FAILURE`): the reason's
/// `PETTAME_*` string fills `ERR_TAME_FAILED`.
PetTameFailure { reason: u8 },
/// A refused pet rename (`SMSG_PET_NAME_INVALID`, empty body): `ERR_INVALID_PETNAME`.
PetNameInvalid,
/// The pet ran away at zero loyalty (`SMSG_PET_BROKEN`, empty body): `ERR_PET_BROKEN`.
PetBroken,
/// The pet's voice (`SMSG_PET_ACTION_SOUND`): a talk selector into its `CreatureSoundData` row.
PetActionSound { pet_guid: u64, talk: u32 },
/// A dismissed pet's sound (`SMSG_PET_DISMISS_SOUND`): the `CreatureModelData` column-29 kit.
PetDismissSound { model_id: u32, position: [f32; 3] },
/// `SMSG_ITEM_QUERY_SINGLE_RESPONSE`, by entry; `None` is unknown. Boxed, being wide.
ItemTemplate {
entry: u32,
info: Option<Box<ItemInfo>>,
},
/// One chat line (`SMSG_MESSAGECHAT`); system lines (type `0x0A`) carry GM command feedback.
Chat(crate::messages::ChatMessage),
/// A channel notice (`SMSG_CHANNEL_NOTIFY`); `notice` selects the `tail` payload.
ChannelNotify {
notice: u8,
channel: String,
tail: ChannelNoticeTail,
},
/// A channel roster (`SMSG_CHANNEL_LIST`), member flags per vmangos `Chat/Channel.h:119-130`.
ChannelList {
channel: String,
flags: u8,
members: Vec<(u64, u8)>,
},
/// A whisper target wasn't found online (`SMSG_CHAT_PLAYER_NOT_FOUND`).
ChatPlayerNotFound { name: String },
/// A cross-faction whisper was refused (`SMSG_CHAT_WRONG_FACTION`); empty body.
ChatWrongFaction,
/// A server notice (`SMSG_NOTIFICATION`) the reference flashes in UIErrorsFrame.
Notification { text: String },
/// Why an area trigger refused (`SMSG_AREA_TRIGGER_MESSAGE`), shown as a notification.
AreaTriggerMessage { text: String },
/// A shutdown countdown or a broadcast (`SMSG_SERVER_MESSAGE`): a `ServerMessages.dbc` row
/// and its `%s`, shown as `CHAT_MSG_SYSTEM`.
ServerMessage { message_type: u32, text: String },
/// `SMSG_ZONE_UNDER_ATTACK`: an `AreaTable.dbc` id, shown on the joined defense channels.
ZoneUnderAttack { area_id: u32 },
/// A defense broadcast (`SMSG_DEFENSE_MESSAGE`), shown where [`Self::ZoneUnderAttack`] is.
DefenseMessage { zone_id: u32, text: String },
/// A trial account's whisper cap (`SMSG_CHAT_RESTRICTED`, empty body): `DisplayError(0x1c3)`.
ChatRestricted,
/// `/played` (`SMSG_PLAYED_TIME`): total and this-level played time, in seconds.
PlayedTime { total: u32, level: u32 },
/// A `/random` broadcast (`MSG_RANDOM_ROLL`).
RandomRoll {
min: u32,
max: u32,
roll: u32,
guid: u64,
},
/// A refused inventory operation (`SMSG_INVENTORY_CHANGE_FAILURE`), never `EQUIP_ERR_OK`.
InventoryFailure {
reason: u8,
required_level: Option<u32>,
item_guid: u64,
/// The destination bag's absolute player slot, reason 16's `%s`; 255 is the player's own.
bag_slot: u8,
},
/// A unit began melee auto-attack (`SMSG_ATTACKSTART`), our own included.
AttackStart { attacker: u64, victim: u64 },
/// A unit stopped melee auto-attack (`SMSG_ATTACKSTOP`).
AttackStop { attacker: u64, victim: u64 },
/// One melee swing (`SMSG_ATTACKERSTATEUPDATE`): one packet is one swing, no client timer.
AttackerState(AttackerState),
/// Our melee swing was refused (`SMSG_ATTACKSWING_*`); sent to the swinger alone.
AttackSwingError(AttackSwingError),
/// The server stopped our attack (`SMSG_CANCEL_COMBAT`): the reference stops, no message.
CancelCombat,
/// Feign Death resisted (`SMSG_FEIGN_DEATH_RESISTED`): one red line, no state.
FeignDeathResisted,
/// `SMSG_AI_REACTION`: 2 hostile (each creature melee start), 0 alert (stealth detection),
/// anything else a no-op; the reference only plays the vocal.
AiReaction { unit: u64, reaction: u32 },
/// A non-triggered cast began (`SMSG_SPELL_START`), instants included with `cast_time_ms` 0.
SpellStart {
caster: u64,
spell_id: u32,
cast_flags: u16,
cast_time_ms: u32,
target: Option<u64>,
/// The `CAST_FLAG_AMMO` tail: nocked ammo for any caster (`0x60ba30` from `0x6e78b6`).
ammo_display_id: Option<u32>,
},
/// The cast launched (`SMSG_SPELL_GO`); the server times impact off `Spell.dbc` Speed itself.
SpellGo {
caster: u64,
spell_id: u32,
cast_flags: u16,
hits: Vec<u64>,
misses: Vec<(u64, u8)>,
target: Option<u64>,
/// The GameObject an open-lock cast targets (`TARGET_FLAG_GAMEOBJECT`), a chest or door.
go_target: Option<u64>,
/// The ground point (`TARGET_FLAG_DEST_LOCATION`, raw WoW); the lasting effect is a
/// DynamicObject.
dest: Option<[f32; 3]>,
ammo_display_id: Option<u32>,
/// The cast item when the first guid is not the caster; the item cooldown keys on it.
item_caster: Option<u64>,
},
/// A channeled beam's hops (`SMSG_SPELL_UPDATE_CHAIN_TARGETS`), consumed once by the next chain
/// `CharProc`; the reference fills that `unit+0xd44` array from spell-go hits too (`0x6e800d`).
SpellChainTargets {
caster: u64,
spell_id: u32,
targets: Vec<u64>,
},
/// An observed cast was interrupted or cancelled (`SMSG_SPELL_FAILED_OTHER`).
SpellFailedOther { caster: u64, spell_id: u32 },
/// Our cast was pushed back by damage (`SMSG_SPELL_DELAYED`); its end moves out by `delay_ms`.
SpellDelayed { caster: u64, delay_ms: u32 },
/// Stop our own ranged auto-repeat (`SMSG_CANCEL_AUTO_REPEAT`, self-only).
CancelAutoRepeat,
/// `SMSG_SPELL_COOLDOWN` for the player or pet: `(spell_id, cooldown_ms)`, 0 ms meaning the
/// spell's own `Spell.dbc` recovery; a normal cast's cooldown is client-tracked.
SpellCooldowns {
caster: u64,
cooldowns: Vec<(u32, u32)>,
},
/// Put an item instance on the client's fixed 30 s use cooldown (`SMSG_ITEM_COOLDOWN`).
ItemCooldown { item_guid: u64, spell_id: u32 },
/// `SMSG_ITEM_TIME_UPDATE`: seconds left on a timed item, 0 when expired. The client shows
/// this, not `ITEM_FIELD_DURATION` (vmangos `Item::SendTimeUpdate`, `Objects/Item.cpp:1094`).
ItemTime { item_guid: u64, seconds: u32 },
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE`: seconds left on a temporary enchant, 0 when expired; the
/// tooltip countdown's only feed, never the `ITEM_FIELD_ENCHANTMENT` duration.
ItemEnchantTime {
item_guid: u64,
slot: u32,
seconds: u32,
},
/// A talent spell-modifier cell (`SMSG_SET_FLAT_SPELL_MODIFIER`/`_PCT_`), absolute: row
/// `mask_bit`, a `SpellFamilyFlags` bit, column `op`, neither bounded on the wire. Deviation:
/// `benilla::spell_mods` refuses an out-of-range pair, because the reference would overrun it.
SpellModifier {
flat: bool,
mask_bit: u8,
op: u8,
value: i32,
},
/// Start an on-hold (`SPELL_ATTR_COOLDOWN_ON_EVENT`) cooldown now (`SMSG_COOLDOWN_EVENT`).
CooldownEvent { spell_id: u32, caster: u64 },
/// Remove one spell's cooldown record (`SMSG_CLEAR_COOLDOWN`).
ClearCooldown { spell_id: u32, caster: u64 },
/// Wipe every cooldown for `caster` (`SMSG_COOLDOWN_CHEAT`, the GM reset).
CooldownCheat { caster: u64 },
/// Our own channel opened (`MSG_CHANNEL_START`, self-only, no guid on the wire).
ChannelStart { spell_id: u32, duration_ms: u32 },
/// A channel's time left (`MSG_CHANNEL_UPDATE`, self-only); 0 is over, natural or interrupted.
ChannelUpdate { remaining_ms: u32 },
/// Time left on one of our auras by `UNIT_FIELD_AURA` slot (`SMSG_UPDATE_AURA_DURATION`),
/// never for a permanent one. It arrives before the delta naming the slot's spell.
AuraDuration { slot: u8, remaining_ms: u32 },
/// A spell-visual kit outside the cast sequence (`SMSG_PLAY_SPELL_VISUAL`), such as eat/drink.
PlaySpellVisual { unit: u64, kit_id: u32 },
/// Spell damage dealt (`SMSG_SPELLNONMELEEDAMAGELOG`).
SpellDamageLog(SpellDamageLog),
/// Periodic aura ticks: DoT, HoT, regen (`SMSG_PERIODICAURALOG`).
PeriodicAuraLog(PeriodicAuraLog),
/// A direct heal (`SMSG_SPELLHEALLOG`).
SpellHealLog(SpellHealLog),
/// An instant power gain (`SMSG_SPELLENERGIZELOG`).
SpellEnergizeLog(SpellEnergizeLog),
/// A damage-shield (Thorns-style) return hit (`SMSG_SPELLDAMAGESHIELD`).
DamageShield(DamageShield),
/// Environmental damage (`SMSG_ENVIRONMENTALDAMAGELOG`); kit from `EnvironmentalDamage.dbc`.
EnvironmentalDamageLog(EnvironmentalDamageLog),
/// A cast's per-target miss list (`SMSG_SPELLLOGMISS`).
SpellLogMiss(SpellLogMiss),
/// The killing blow (`SMSG_PARTYKILLLOG`), the only source of "You have slain %s!".
PartyKillLog(PartyKillLog),
/// An instant kill (`SMSG_SPELLINSTAKILLLOG`).
SpellInstaKillLog(SpellInstaKillLog),
/// A proc the target resisted (`SMSG_PROCRESIST`).
ProcResist(SpellOutcomeLog),
/// A target immune to the spell (`SMSG_SPELLORDAMAGE_IMMUNE`).
SpellOrDamageImmune(SpellOutcomeLog),
/// The auras a dispel removed (`SMSG_SPELLDISPELLOG`).
SpellDispelLog(SpellDispelLog),
/// The auras a dispel failed to remove (`SMSG_DISPEL_FAILED`).
DispelFailed(DispelFailed),
/// An enchant landing on or fading from an item (`SMSG_ENCHANTMENTLOG`).
EnchantmentLog(EnchantmentLog),
/// What a cast's effects did (`SMSG_SPELLLOGEXECUTE`).
SpellLogExecute(SpellLogExecute),
/// An XP award, kill or non-kill (`SMSG_LOG_XPGAIN`).
XpGain(XpGain),
/// A first visit to an area and its XP (`SMSG_EXPLORATION_EXPERIENCE`), the "Discovered" line.
ExplorationXp(ExplorationXp),
/// Our own level-up (`SMSG_LEVELUP_INFO`, self-only).
LevelUp(LevelUpInfo),
/// A gossip menu (`SMSG_GOSSIP_MESSAGE`): `text_id` is the greeting for
/// [`crate::messages::npc_text_query`]; `quests` rows are `(quest_id, icon, level, title)`.
GossipMenu {
npc: u64,
text_id: u32,
options: Vec<GossipOption>,
quests: Vec<(u32, u32, u32, String)>,
},
/// An NPC's `!`/`?` marker (`SMSG_QUESTGIVER_STATUS`), a [`crate::messages::dialog_status`].
QuestGiverStatus { npc: u64, status: u32 },
/// The greeting panel: an NPC's offered/active quest rows (`SMSG_QUESTGIVER_QUEST_LIST`).
QuestGreeting(QuestGiverList),
/// The accept panel: quest text and rewards on offer (`SMSG_QUESTGIVER_QUEST_DETAILS`).
QuestDetail(QuestDetails),
/// The progress panel: what to bring (`SMSG_QUESTGIVER_REQUEST_ITEMS`).
QuestProgress(QuestRequestItems),
/// The reward panel: turn-in text and rewards to grant (`SMSG_QUESTGIVER_OFFER_REWARD`).
QuestOffer(QuestOfferReward),
/// The turn-in result: XP, money and fixed items granted (`SMSG_QUESTGIVER_QUEST_COMPLETE`).
QuestComplete(QuestComplete),
/// The full quest template (`SMSG_QUEST_QUERY_RESPONSE`), asked once and cached by id.
QuestTemplate(Box<QuestTemplate>),
/// A kill objective ticked (`SMSG_QUESTUPDATE_ADD_KILL`), the announce line only; a GO
/// `entry` is `(-id)|0x80000000`, and the durable count is in the `PLAYER_QUEST_LOG` slot.
QuestObjectiveKill {
quest_id: u32,
entry: u32,
count: u32,
required: u32,
},
/// An item-collection objective toast (`SMSG_QUESTUPDATE_ADD_ITEM`).
QuestObjectiveItem { item_id: u32, count: u32 },
/// All objectives done (`SMSG_QUESTUPDATE_COMPLETE`), a line only; the slot state is durable.
QuestObjectivesComplete { quest_id: u32 },
/// The quest failed (`SMSG_QUESTUPDATE_FAILED`, or `_FAILEDTIMER` when `timed`).
QuestFailed { quest_id: u32, timed: bool },
/// The log refused a new quest: no free slot (`SMSG_QUESTLOG_FULL`).
QuestLogFull,
/// A member's verdict on a quest we shared (`MSG_QUEST_PUSH_RESULT`). `member` is that
/// member, never the sharer ([`crate::messages::QuestPushResult`]); it fills the `%s`.
QuestPushResult { member: u64, msg: QuestShareMsg },
/// A member's escort quest offered to us too (`SMSG_QUEST_CONFIRM_ACCEPT`).
QuestConfirmAccept(QuestConfirmAccept),
/// The giver won't offer the quest (`SMSG_QUESTGIVER_QUEST_INVALID`): a reason, no quest id
/// (vmangos `SendCanTakeQuestResponse` on a failed `CanTakeQuest`).
QuestGiverInvalid { reason: u32 },
/// An offered quest failed on accept (`SMSG_QUESTGIVER_QUEST_FAILED`); the reference reads it
/// with a different handler and message table from [`Self::QuestGiverInvalid`].
QuestGiverFailed { quest_id: u32, reason: u32 },
/// The gossip window closes (`SMSG_GOSSIP_COMPLETE`); no menu is open server-side.
GossipComplete,
/// A guard's directions (`SMSG_GOSSIP_POI`): a map marker sent for an `action_poi_id` option.
GossipPoi(crate::messages::GossipPoi),
/// All 8 greeting blocks, undrawn (`SMSG_NPC_TEXT_UPDATE`): the pick depends on the NPC's
/// gender and a roll as the frame opens; `$N` and its kin are still unsubstituted.
NpcGreeting {
text_id: u32,
blocks: Vec<crate::messages::NpcTextBlock>,
},
/// A vendor's stock (`SMSG_LIST_INVENTORY`); `current_count == 0xFFFF_FFFF` is unlimited.
VendorInventory { vendor: u64, items: Vec<VendorItem> },
/// Trainer list (`SMSG_TRAINER_LIST`); `trainer_type` 0 class, 1 mount, 2 tradeskill, 3 pet.
TrainerList {
trainer: u64,
trainer_type: u32,
services: Vec<TrainerSpell>,
greeting: String,
},
/// Forget a player's cached name (`SMSG_INVALIDATE_PLAYER`), the cache's only eviction.
InvalidatePlayer { guid: u64 },
/// The stable list (`MSG_LIST_STABLED_PETS`), unasked on the gossip option and on refresh;
/// slots are rebased, 0 the current pet. Read by slot: a petless hunter has no slot-0 row.
ListStabledPets {
npc: u64,
num_stable_slots: u8,
pets: Vec<StabledPet>,
},
/// Every stable verb's answer (`SMSG_STABLE_RESULT`), a [`crate::messages::stable_result`];
/// no success carries a list, so repaint with a fresh `MSG_LIST_STABLED_PETS`.
StableResult { result: u8 },
/// A purchase took (`SMSG_TRAINER_BUY_SUCCEEDED`); the server never resends the list itself.
TrainerBuySucceeded { trainer: u64, spell_id: u32 },
/// A refused purchase (`SMSG_TRAINER_BUY_FAILED`), `error` a [`crate::messages::train_fail`].
TrainerBuyFailed {
trainer: u64,
spell_id: u32,
error: u32,
},
/// A vendor's stock after a purchase (`SMSG_BUY_ITEM`); the item comes by the normal create.
VendorBuyResult {
vendor: u64,
slot: u32,
new_count: u32,
purchase_count: u32,
},
/// A refused sale (`SMSG_SELL_ITEM`, a [`crate::messages::sell_result`]); a success is silent.
VendorSellFailed {
vendor: u64,
item_guid: u64,
reason: u8,
},
/// A refused purchase (`SMSG_BUY_FAILED`), `reason` a [`crate::messages::buy_result`].
VendorBuyFailed {
vendor: u64,
item_entry: u32,
reason: u8,
},
/// The bank opens (`SMSG_SHOW_BANK`), also unasked via gossip; contents are in the descriptor.
ShowBank { banker: u64 },
/// A refused bank-slot purchase (`SMSG_BUY_BANK_SLOT_RESULT`); a success sends nothing, only
/// the `PLAYER_BYTES_2` bag count and the coinage change.
BuyBankSlotResult { result: u32 },
/// A loot window opened (`SMSG_LOOT_RESPONSE`); quest rows follow the normal ones. A row under
/// a group roll is `ROLL_ONGOING`; under master loot, vmangos marks a member's rows `MASTER`.
LootResponse {
guid: u64,
loot_type: u8,
gold: u32,
items: Vec<LootItem>,
},
/// A refused loot window (`SMSG_LOOT_RESPONSE`'s error shape), a `loot::loot_error` code.
LootError { guid: u64, error: u8 },
/// A loot row was taken, by anyone (`SMSG_LOOT_REMOVED`).
LootRemoved { slot: u8 },
/// Our share of the coin (`SMSG_LOOT_MONEY_NOTIFY`), answering `CMSG_LOOT_MONEY`.
LootMoneyNotify { amount: u32 },
/// The coin line disappears for every current looter (`SMSG_LOOT_CLEAR_MONEY`).
LootClearMoney,
/// The loot window closes (`SMSG_LOOT_RELEASE_RESPONSE`), answering our `CMSG_LOOT_RELEASE`.
LootReleaseResponse { guid: u64 },
/// A group roll opened on one drop (`SMSG_LOOT_START_ROLL`); its `rollID` is client-allocated.
LootStartRoll(LootStartRoll),
/// A vote or dice (`SMSG_LOOT_ROLL`); `LootRoll::is_dice`/`vote` split the overloaded pair.
LootRoll(LootRoll),
/// A group roll resolved (`SMSG_LOOT_ROLL_WON`), closing its frame.
LootRollWon(LootRollWon),
/// Everyone passed (`SMSG_LOOT_ALL_PASSED`); the item returns to ordinary looting.
LootAllPassed(LootAllPassed),
/// Master-loot candidates (`SMSG_LOOT_MASTER_LIST`), sent by `SendLoot` before its response.
LootMasterList { candidates: Vec<u64> },
/// An item landed in our bags (`SMSG_ITEM_PUSH_RESULT`): the "You receive loot" line.
ItemPushResult(ItemPushResult),
/// The keepalive echo (`SMSG_PONG`) of a 30 s `CMSG_PING`, matched by `sequence`.
Pong { sequence: u32 },
/// Our speed changed (`SMSG_FORCE_*_SPEED_CHANGE`): ack `counter`, the exact `speed` and a live
/// `MovementInfo`, or vmangos flags anticheat in about 4 s (`CheckPendingMovementChanges`).
ForceSpeedChange {
guid: u64,
kind: crate::messages::SpeedKind,
counter: u32,
speed: f32,
},
/// Another unit's speed (`SMSG_SPLINE_SET_*_SPEED`/`MSG_MOVE_SET_*_SPEED`), no ack.
SpeedChanged {
guid: u64,
kind: crate::messages::SpeedKind,
speed: f32,
},
/// `SMSG_MOUNTRESULT` (or `_DISMOUNTRESULT` if not `mount`): OK is 10 mounting, 3 dismounting.
MountResult { mount: bool, code: u32 },
/// A rider's flourish (`SMSG_MOUNTSPECIAL_ANIM`): MountSpecial (94) on its mount. Whether our
/// own echo arrives depends on vmangos's broadcaster, so the app plays ours at send instead.
MountSpecial { guid: u64 },
/// Control of a unit granted or taken (`SMSG_CLIENT_CONTROL_UPDATE`). Taking it names us with
/// `allow_move = false`, so "about me" and "may move" are separate questions. A grant needs
/// `CMSG_SET_ACTIVE_MOVER`: vmangos drops every `MSG_MOVE_*` for an unconfirmed mover.
ClientControl { mover: u64, allow_move: bool },
/// A dropped packet: `unparseable` is false when no parse arm exists
/// (`ServerPacket::Other`), true when its parser errored and the body was skipped.
PacketDropped { opcode: u16, unparseable: bool },
/// Where our corpse is (`MSG_CORPSE_QUERY`); `found == false` also comes unasked when it turns
/// to bones. `display_map`/`position` are entrance-adjusted, `corpse_map` the real map.
CorpseQuery {
found: bool,
display_map: i32,
position: [f32; 3],
corpse_map: u32,
},
/// Corpse reclaim delay (`SMSG_CORPSE_RECLAIM_DELAY`): 30 s, 60 or 120 s on repeated deaths.
CorpseReclaimDelay { delay_ms: u32 },
/// The 10% death durability loss (`SMSG_DURABILITY_DAMAGE_DEATH`, empty body): the red line.
DurabilityDamageDeath,
/// A resurrect offer (`SMSG_RESURRECT_REQUEST`), answered by `CMSG_RESURRECT_RESPONSE`.
/// `name` is empty for a player caster; `sickness`/`has_timer` pick the popup variant.
ResurrectRequest {
caster: u64,
name: String,
sickness: bool,
has_timer: bool,
},
/// The spirit healer's `CONFIRM_XP_LOSS` popup (`SMSG_SPIRIT_HEALER_CONFIRM`); accepting sends
/// `CMSG_SPIRIT_HEALER_ACTIVATE` with `npc`.
SpiritHealerConfirm { npc: u64 },
/// Our root, water-walk, feather-fall or hover changed. Ack with `counter` and our
/// `MovementInfo`, or the server never applies it and observers never see it.
MoveMode {
guid: u64,
counter: u32,
mode: crate::messages::MoveMode,
apply: bool,
},
/// Any unit's movement mode (the twelve `SMSG_SPLINE_MOVE_*`), no ack; on vmangos, a unit the
/// server drives. `apply` sets the mode's `MOVEMENTFLAGS` bit, and the reference then re-runs
/// the unit's gait selector (`0x6014ec`).
SplineMoveMode {
guid: u64,
mode: crate::messages::SplineMode,
apply: bool,
},
/// A knockback on our mover (`SMSG_MOVE_KNOCK_BACK`): a launch we fly ourselves, `zspeed`
/// down-positive. Ack with `counter` and exactly this jump tail, or vmangos logs a cheat and
/// observers never see it.
KnockBack {
guid: u64,
counter: u32,
launch: crate::messages::JumpInfo,
},
/// Someone invited us to their group (`SMSG_GROUP_INVITE`): the invite popup.
GroupInvite { inviter: String },
/// An invite we sent was declined (`SMSG_GROUP_DECLINE`).
GroupDecline { name: String },
/// We were removed from our group (`SMSG_GROUP_UNINVITE`, empty body), kicked or left.
GroupUninvited,
/// The group's leader changed (`SMSG_GROUP_SET_LEADER`).
GroupLeaderChanged { name: String },
/// The group disbanded outright (`SMSG_GROUP_DESTROYED`, empty body).
GroupDestroyed,
/// The full roster (`SMSG_GROUP_LIST`), without our own row; `loot` is `None` when alone.
GroupList {
group_type: u8,
own_flags: u8,
members: Vec<GroupMemberEntry>,
leader: u64,
loot: Option<GroupLootInfo>,
},
/// A group command's verdict (`SMSG_PARTY_COMMAND_RESULT`): a
/// [`crate::messages::party_operation`] and a [`crate::messages::party_result`] code.
PartyCommandResult {
operation: u32,
member: String,
result: u32,
},
/// A member's stats (`SMSG_PARTY_MEMBER_STATS`, or the asked-for `_FULL` form when `full`).
PartyMemberStats {
guid: u64,
full: bool,
info: Box<PartyMemberStatsInfo>,
},
/// Someone pinged the minimap (`MSG_MINIMAP_PING`): the group ping marker.
MinimapPing { guid: u64, x: f32, y: f32 },
/// One raid-target icon changed (`MSG_RAID_TARGET_UPDATE`, delta shape); `guid == 0` clears it.
RaidTargetSet { icon: u8, guid: u64 },
/// The set raid-target icons (`MSG_RAID_TARGET_UPDATE`, full-list shape).
RaidTargetList { entries: Vec<(u8, u64)> },
/// The raid leader started a ready check (`MSG_RAID_READY_CHECK`, empty body).
ReadyCheckRequest,
/// A member's ready-check answer, to the leader only (`MSG_RAID_READY_CHECK`, with a body).
ReadyCheckAnswer { guid: u64, ready: u8 },
/// Our saved raid lockouts (`SMSG_RAID_INSTANCE_INFO`), replacing the list; empty is normal.
RaidInstanceInfo {
entries: Vec<crate::messages::RaidInstanceEntry>,
},
// ── Instance lockouts ─────────────────────────────────────────────────────────────────
// Four become client-composed `CHAT_MSG_SYSTEM` lines and two feed `CanShowResetInstances()`;
// the map name is never on the wire, only a `Map.dbc` id.
/// A raid lockout's welcome/countdown line (`SMSG_RAID_INSTANCE_MESSAGE`).
RaidInstanceMessage {
message: crate::messages::RaidInstanceMessage,
},
/// We are now saved to this instance (`SMSG_INSTANCE_SAVE_CREATED`); vmangos always sends
/// `flag` 0, and 1 is the reference's debug wrapper.
InstanceSaveCreated { flag: u32 },
/// An instance reset took (`SMSG_INSTANCE_RESET`); `map` is a `Map.dbc` id.
InstanceReset { map: u32 },
/// An instance reset was refused (`SMSG_INSTANCE_RESET_FAILED`).
InstanceResetFailed {
failure: crate::messages::InstanceResetFailed,
},
/// The `Map.dbc` id of the dungeon we were last inside (`SMSG_UPDATE_LAST_INSTANCE`).
UpdateLastInstance { map: u32 },
/// Whether we hold any permanent bind (`SMSG_UPDATE_INSTANCE_OWNERSHIP`).
UpdateInstanceOwnership { owns: bool },
/// A duel challenge (`SMSG_DUEL_REQUESTED`), sent to both sides; `arbiter` is the flag object
/// echoed on accept or cancel, and `challenger` is us when we asked.
DuelRequested { arbiter: u64, challenger: u64 },
/// We left the duel-flag bubble (`SMSG_DUEL_OUTOFBOUNDS`): the 10 s forfeit timer runs.
DuelOutOfBounds,
/// We are back inside the bubble (`SMSG_DUEL_INBOUNDS`); the forfeit timer is cleared.
DuelInBounds,
/// The duel ended (`SMSG_DUEL_COMPLETE`). `started` is false only when it never began.
DuelComplete { started: bool },
/// The duel's outcome line (`SMSG_DUEL_WINNER`), to all nearby; `fled` picks the retreat one.
DuelWinner {
fled: bool,
winner: String,
loser: String,
},
/// Start the duel countdown (`SMSG_DUEL_COUNTDOWN`), already in whole seconds.
DuelCountdown { seconds: u32 },
/// Another player's honor stats (`MSG_INSPECT_HONOR_STATS`); a refused ask gets no reply.
InspectHonorStats(InspectHonorStats),
/// A kill paid out honor (`SMSG_PVP_CREDIT`); a dishonorable kill's `honor` is negative.
PvpCredit(PvpCredit),
/// A breath or fatigue bar started or restated (`SMSG_START_MIRROR_TIMER`): with no update
/// opcode the server resends it on any change, so treat it as idempotent.
MirrorTimerStart(MirrorTimerStart),
/// Freeze or unfreeze a mirror timer (`SMSG_PAUSE_MIRROR_TIMER`). vmangos never sends it: the
/// stock `MirrorTimer.lua` errors on one.
MirrorTimerPause { kind: u32, paused: bool },
/// A mirror timer is over (`SMSG_STOP_MIRROR_TIMER`): refilled, or its condition ended.
MirrorTimerStop { kind: u32 },
/// The whole friend list (`SMSG_FRIEND_LIST`), replacing what is held; no names on the wire.
FriendList { friends: Vec<FriendEntry> },
/// The whole ignore list (`SMSG_IGNORE_LIST`), replacing what is held; guids only.
IgnoreList { guids: Vec<u64> },
/// A friend/ignore result (`SMSG_FRIEND_STATUS`): an add/remove ack or a friend's login or
/// logout broadcast; the [`friend_result`](crate::messages::friend_result) code says which.
FriendStatus(FriendStatusUpdate),
/// A `/who` answer (`SMSG_WHO`): at most 49 rows, plus the true match total.
WhoResults(WhoResults),
/// A guild's name, ten rank names and tabard (`SMSG_GUILD_QUERY_RESPONSE`), cached by id.
GuildQueryResponse(GuildQueryResponse),
/// The whole guild (`SMSG_GUILD_ROSTER`), a complete snapshot that replaces what is held.
GuildRoster(GuildRoster),
/// A guild event (`SMSG_GUILD_EVENT`): a [`guild_event`](crate::messages::guild_event) id and
/// its display-text arguments; a guid rides only on sign-on/off.
GuildEvent(GuildEventNotice),
/// A guild verb's verdict (`SMSG_GUILD_COMMAND_RESULT`); its command tag tells result `0x08`'s
/// two meanings apart ([`guild_command_error`](crate::messages::guild_command_error)).
GuildCommandResult(GuildCommandResult),
/// A guild invite (`SMSG_GUILD_INVITE`); `CMSG_GUILD_ACCEPT`/`_DECLINE` do not name it.
GuildInvite { inviter: String, guild: String },
/// The player we invited declined (`SMSG_GUILD_DECLINE`), sent to the inviter only.
GuildDecline { name: String },
/// The guild's founding date, member and account counts (`SMSG_GUILD_INFO`).
GuildInfo(GuildInfo),
/// A registrar's charter list (`SMSG_PETITION_SHOWLIST`), opening its window; one row in 1.12.
PetitionShowList(PetitionShowList),
/// A charter's signers (`SMSG_PETITION_SHOW_SIGNATURES`): our own ask or an offer to sign, told
/// apart only by whether `owner` is us; name and requirement are in the petition query.
PetitionShowSignatures(PetitionShowSignatures),
/// A signature's verdict (`SMSG_PETITION_SIGN_RESULTS`); on success signer and owner each get
/// a copy naming the signer, the same bytes.
PetitionSignResults(PetitionSignResults),
/// A petition's guild name and signature requirement (`SMSG_PETITION_QUERY_RESPONSE`).
PetitionQueryResponse(PetitionQueryResponse),
/// A turn-in's bare code (`SMSG_TURN_IN_PETITION_RESULTS`); a name collision sends only a
/// [`Self::GuildCommandResult`].
TurnInPetitionResults { result: u32 },
/// Somebody declined our charter (`MSG_PETITION_DECLINE`), sent to the owner only.
PetitionDeclined { player: u64 },
/// A charter rename took (`MSG_PETITION_RENAME`); a rejected one is a guild command result.
PetitionRenamed(PetitionRename),
/// The taxi map (`SMSG_SHOWTAXINODES`); `nearest_node` is the flight master's own node.
TaxiNodesShown {
flightmaster: u64,
nearest_node: u32,
known_mask: TaxiMask,
},
/// Whether the flight master's nearest node is known (`SMSG_TAXINODE_STATUS`); also sent with
/// [`Self::NewTaxiPath`] on a first visit (vmangos `SendLearnNewTaxiNode`).
TaxiNodeStatus { guid: u64, known: bool },
/// A flight request's verdict (`SMSG_ACTIVATETAXIREPLY`), a [`crate::messages::taxi_reply`]:
/// 0 is OK; a refusal starts no flight and sends no mount or spline.
ActivateTaxiReply { code: u32 },
/// `SMSG_NEW_TAXI_PATH`, empty, sent with [`Self::TaxiNodeStatus`] on a first visit.
NewTaxiPath,
/// The inbox (`SMSG_MAIL_LIST_RESULT`), answering `CMSG_GET_MAIL_LIST`.
MailList { mails: Vec<MailListEntry> },
/// A mail action's verdict (`SMSG_SEND_MAIL_RESULT`); `equip_error`/`item` are exclusive tails.
SendMailResult {
mail_id: u32,
action: u32,
error: u32,
equip_error: Option<u32>,
item: Option<(u32, u32)>,
},
/// One book page (`SMSG_PAGE_TEXT_QUERY_RESPONSE`), for items and `GAMEOBJECT_TYPE_TEXT`;
/// `next_page_id == 0` ends it, and vmangos pushes the whole chain on the first query.
PageText {
page_id: u32,
text: String,
next_page_id: u32,
},
/// A letter's body (`SMSG_ITEM_TEXT_QUERY_RESPONSE`), for a mail's non-zero `item_text_id`.
MailItemText { text_id: u32, text: String },
/// A mail arrived (`SMSG_RECEIVED_MAIL`); `seconds` of delay, always `0.0` from vmangos.
ReceivedMail { seconds: f32 },
/// `MSG_QUERY_NEXT_MAIL_TIME`'s reply: `0.0` unread mail, `-86400.0` none; for `HasNewMail()`.
NextMailTime { seconds: f32 },
/// `MSG_AUCTION_HELLO`'s reply, which opens the auction window: the auctioneer and its
/// `AuctionHouse.dbc` row (1..7, the deposit and cut rates).
AuctionHello { auctioneer: u64, house_id: u32 },
/// A sell, cancel or bid verdict (`SMSG_AUCTION_COMMAND_RESULT`); `error` selects `tail`,
/// `auction_id` is 0 on most failures, and several refusals send nothing.
AuctionCommandResult {
auction_id: u32,
action: u32,
error: u32,
tail: AuctionCommandTail,
},
/// A Browse page (`SMSG_AUCTION_LIST_RESULT`), at most 50 rows; `total_count` is pre-cap.
AuctionListResult {
auctions: Vec<AuctionListEntry>,
total_count: u32,
},
/// The Auctions tab page, our own listings (`SMSG_AUCTION_OWNER_LIST_RESULT`).
AuctionOwnerListResult {
auctions: Vec<AuctionListEntry>,
total_count: u32,
},
/// The Bids tab page (`SMSG_AUCTION_BIDDER_LIST_RESULT`): refreshed ids first, then live bids,
/// so a row can appear twice.
AuctionBidderListResult {
auctions: Vec<AuctionListEntry>,
total_count: u32,
},
/// We won or were outbid (`SMSG_AUCTION_BIDDER_NOTIFICATION`); `bid_or_zero == 0` means won.
AuctionBidderNotification(AuctionBidderNotification),
/// Our auction sold or took a bid (`SMSG_AUCTION_OWNER_NOTIFICATION`); `bidder_guid` 0 is sold.
AuctionOwnerNotification(AuctionOwnerNotification),
/// The seller cancelled an auction we bid on (`SMSG_AUCTION_REMOVED_NOTIFICATION`).
AuctionRemovedNotification {
auction_id: u32,
item_entry: u32,
random_property_id: i32,
},
/// One step of the trade state machine (`SMSG_TRADE_STATUS`), refusals included.
TradeStatus { status: TradeStatus },
/// One trade side's items and gold (`SMSG_TRADE_STATUS_EXTENDED`), boxed at about 460 bytes.
TradeStatusExtended { state: Box<TradeStatusExtended> },
/// World states: `SMSG_INIT_WORLD_STATES` with its `(map, zone)` scope and the wire run
/// verbatim, terminator included, or `SMSG_UPDATE_WORLD_STATE` as one entry with no scope.
/// The reference funnels both into one setter too.
WorldStates {
scope: Option<(u32, u32)>,
states: Vec<(u32, u32)>,
},
}
impl SessionEventKind {
/// Every kind, in declaration order.
pub fn all() -> impl Iterator<Item = Self> {
<Self as strum::IntoEnumIterator>::iter()
}
}
/// The result of polling the reader for the next packet's events.
pub enum Poll {
/// A decoded packet; `events` is empty for an opcode parsed but not modelled. `tail` counts
/// body bytes left unread, which shows a decoder shorter than the server's layout.
Events {
opcode: u16,
events: Vec<SessionEvent>,
tail: usize,
},
/// An unparseable packet, skipped to keep the stream aligned; `reason` has a hex preview.
Skipped { opcode: u16, reason: String },
}
mod decode;
pub use decode::decode;