Comments in benilla-protocol: the second read, citations on the pinned revision and restored facts (#375)

The second read of `benilla-protocol`'s comments, the last step #41 asked for: that crate's comments were rewritten to the rule before second readers existed. Four readers compared every file's comments against their state before the rewrite and against the code as it is now.

Fixes #41.

- **Citations.** vmangos line citations are re-pointed to the pinned revision: off-by-a-few lines (`Pet.cpp`, `Guild.cpp`, `Petition.cpp`, `LootHandler.cpp`, `ChatHandler.cpp`), and paths the rewrite dropped where a bare file name is ambiguous (`Server/Packets/Spell.cpp` against `Spells/Spell.cpp`, the two `Item.cpp`, `Group.cpp` and `Chat.cpp`). The gossip-hello citation is `NPCHandler.cpp:347`.
- **Facts restored.** The move-time-skipped chain (`0x603b40` → `0x601560` → `0x61ab90`), the addon-message TAB surviving vmangos's chat sanitizer, the zone-under-attack sender, `SMSG_ADDON_INFO` answering the auth session's addon block, the officer-note permission gate, and `SMSG_ITEM_COOLDOWN` on equipping an item with an on-use spell (the old text said a proc).
- **Deviation labels.** A label now carries its reason, or goes when the behaviour is only unbuilt (the survey window, the group XP bonus, the NPC-text language garble).
- **Two examples.** `park_probe` and `login_refusal_probe` took a hard-coded local account; they now read `WOW_USER` and `WOW_PASS`.
- **Verification.** Every file strips to identical code apart from the two examples; `cargo fmt --check` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-26 01:29:00 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4f8350c9ed
commit 4dc33b4afd
37 changed files with 111 additions and 85 deletions
@@ -1,13 +1,13 @@
//! Diagnostic probe: the server's cooldown window for Charge (spell 100: `recoveryTime 0`,
//! category 44, `categoryRecoveryTime 15000`) against the client's 15 s sweep. It charges a
//! Northshire Kobold Vermin (entry 6), then recasts every 200 ms. vmangos checks the cooldown
//! first in `CheckCast` (`Spell.cpp:5369`), so the first result other than
//! near the top of `CheckCast` (`Spell.cpp:5369`), so the first result other than
//! `SPELL_FAILED_NOT_READY` (60), or a second `SMSG_SPELL_GO`, marks the server's cooldown end
//! relative to the first GO.
//!
//! Run: `cargo run -p benilla-protocol --example cooldown_probe -- probeN <password> [host]` on a
//! probe account with a warrior (a login kicks whoever is on the account); `.learn` needs
//! gmlevel 5 (`SEC_DEVELOPER`).
//! gmlevel 5 (`SEC_DEVELOPER`). It is a combat probe, so it never runs unattended.
use std::time::{Duration, Instant};
@@ -5,7 +5,7 @@
//! and that a valid pick then works on the same socket.
//!
//! A creature guid trips the stateless `!packet.guid.IsPlayer()` guard. Needs the local vmangos
//! and account `two`/`ptwo`.
//! and an account in `WOW_USER`/`WOW_PASS`.
use std::time::Duration;
@@ -25,7 +25,9 @@ fn connect(user: &str, pass: &str) -> anyhow::Result<WorldSession> {
}
fn main() -> anyhow::Result<()> {
let (user, pass) = ("two", "ptwo");
let user = std::env::var("WOW_USER").map_err(|_| anyhow::anyhow!("set WOW_USER"))?;
let pass = std::env::var("WOW_PASS").map_err(|_| anyhow::anyhow!("set WOW_PASS"))?;
let (user, pass) = (user.as_str(), pass.as_str());
let mut s = connect(user, pass)?;
let chars = s.char_enum()?;
println!(
@@ -2,7 +2,7 @@
//! `SMSG_LOGOUT_COMPLETE` and a reconnect re-serves the roster; a socket parked at character
//! select for 130 s still logs in, since vmangos does not kick a quiet authenticated socket.
//!
//! Needs the local vmangos and account `two`/`ptwo`.
//! Needs the local vmangos and an account in `WOW_USER`/`WOW_PASS`.
use std::time::Duration;
@@ -19,7 +19,9 @@ fn connect(user: &str, pass: &str) -> anyhow::Result<WorldSession> {
}
fn main() -> anyhow::Result<()> {
let (user, pass) = ("two", "ptwo");
let user = std::env::var("WOW_USER").map_err(|_| anyhow::anyhow!("set WOW_USER"))?;
let pass = std::env::var("WOW_PASS").map_err(|_| anyhow::anyhow!("set WOW_PASS"))?;
let (user, pass) = (user.as_str(), pass.as_str());
// ── 1 · logout round-trip ──
let mut s = connect(user, pass)?;
@@ -9,8 +9,8 @@
//! ```
//!
//! `--stress N` runs N ordinary fixed-width handshakes and reports the failure rate, waiting out
//! realmd's lockout: 60 s after `WrongPass.MaxAttempts` (default 10) failures, during which it
//! answers `0x08 WOW_FAIL_DB_BUSY`.
//! realmd's lockout: `WrongPass.MaxAttempts` (default 10) failures within 60 s lock the IP out
//! for 60 s, answered with `0x08 WOW_FAIL_DB_BUSY`.
use std::net::TcpStream;
use std::time::Duration;
+2 -1
View File
@@ -165,7 +165,8 @@ const INTEGRITY_HASH_5875_MACOS: [u8; 20] = [
];
/// The integrity digest `H` for `crc_salt`, known only for [`MANGOS_VERSION_CHALLENGE`].
/// Deviation: a stored constant, not a hash of the install, which may lack the five binaries.
/// Deviation: a stored per-OS constant, not the reference's HMAC over its own executables
/// (`0x5b1170`), because every mangos-family realmd issues this one salt.
fn integrity_hash(crc_salt: &[u8; 16]) -> Option<[u8; 20]> {
if *crc_salt != MANGOS_VERSION_CHALLENGE {
return None;
@@ -38,7 +38,7 @@ pub(crate) trait Probe {
// Quest constants shared by several probes.
/// Onto Marshal McBride, the `--quest` turn-in NPC and quest 7's giver and ender.
pub(crate) const QUEST_TURNIN_TP: &str = ".go xyz -8902.59 -162.606 82.0223"; // onto Marshal McBride
pub(crate) const QUEST_TURNIN_TP: &str = ".go xyz -8902.59 -162.606 82.0223";
pub(crate) const QUEST_TURNIN_ENTRY: u32 = 197; // Marshal McBride, who takes 783
/// `PLAYER_QUEST_LOG_1_1`: `UNIT_END` (188) + 0xA, 3 fields per slot for 20 slots
/// (`UpdateFields_1_12_1.h:128`).
+2 -1
View File
@@ -229,7 +229,8 @@ pub enum SessionEvent {
/// 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.
/// reference's `[CMovement+0xac] += lag` (`0x603b40` → `0x601560` → `0x61ab90`), 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.
+1 -1
View File
@@ -5,7 +5,7 @@ use std::io;
use crate::wire::{read_u32_le, read_u64_le};
/// `SMSG_BUY_BANK_SLOT_RESULT`'s reason (`BANK_SLOT_*`, vmangos `Player.h:91-94`).
/// `SMSG_BUY_BANK_SLOT_RESULT`'s reason (`ERR_BANKSLOT_*`, vmangos `Player.h:91-94`).
pub mod bank_slot_result {
pub const FAILED_TOO_MANY: u32 = 0;
pub const INSUFFICIENT_FUNDS: u32 = 1;
@@ -95,7 +95,8 @@ pub struct PvpLogData {
pub rows: Vec<PvpLogRow>,
}
/// Deviation: every row is kept; the reference has 80 row blocks and does not clamp the count.
/// Deviation: every row is kept, because the reference takes the count unclamped into its 80
/// row blocks and a larger one overruns them.
pub(super) fn read_pvp_log_data(r: &mut impl Read) -> io::Result<PvpLogData> {
let ended = read_u8(r)? != 0;
let winner = if ended { Some(read_u8(r)?) } else { None };
@@ -17,7 +17,8 @@ pub(super) fn read_server_message(r: &mut &[u8]) -> io::Result<(u32, String)> {
/// `SMSG_ZONE_UNDER_ATTACK` (`Server/Packets/Misc.cpp:451-454`, handler `0x49dcc0`): the attacked
/// area's `AreaTable.dbc` id, whose own name fills the text. vmangos sends it map-wide to the
/// enemy team when a player kills a guard or PvP-enabling creature, once per area per 10 s.
/// enemy team when a player kills a guard or PvP-enabling creature, once per area per 10 s
/// (`Creature.cpp:2889`).
pub(super) fn read_zone_under_attack(r: &mut &[u8]) -> io::Result<u32> {
read_u32_le(r)
}
@@ -25,7 +26,7 @@ pub(super) fn read_zone_under_attack(r: &mut &[u8]) -> io::Result<u32> {
/// `SMSG_DEFENSE_MESSAGE` (`Maps/Map.cpp:1868-1884`, handler `0x49de30`): `u32` zone id, `u32`
/// length counting the NUL, then the text. The reference skips exactly `length` bytes
/// (`0x419ac0`) and drops the line when that overruns the packet, so an overrun errors here.
/// Deviation: text without a NUL stops at `length`, where the reference reads out of bounds.
/// Deviation: text without a NUL stops at `length`, because the reference reads on out of bounds.
pub(super) fn read_defense_message(r: &mut &[u8]) -> io::Result<(u32, String)> {
let zone_id = read_u32_le(r)?;
let length = read_u32_le(r)? as usize;
+3 -2
View File
@@ -33,8 +33,9 @@ pub const CHAT_MSG_RAID_LEADER: u8 = 0x57;
pub const CHAT_MSG_RAID_WARNING: u8 = 0x58;
pub const CHAT_MSG_RAID_BOSS_WHISPER: u8 = 0x59;
pub const CHAT_MSG_RAID_BOSS_EMOTE: u8 = 0x5A;
/// The server's "your message was filtered" notice, which the chat frame formats as
/// `CHAT_FILTERED`; the client's spam and profanity filters exempt it (`0x49aacc`).
/// The server's "your message was filtered" notice: its text is the addressee's name, which the
/// chat frame formats into `CHAT_FILTERED`; the client's spam and profanity filters exempt it
/// (`0x49aacc`).
pub const CHAT_MSG_FILTERED: u8 = 0x5B;
pub const CHAT_MSG_BATTLEGROUND: u8 = 0x5C;
pub const CHAT_MSG_BATTLEGROUND_LEADER: u8 = 0x5D;
@@ -5,7 +5,7 @@ use std::io;
use crate::wire::{read_cstring, read_f32_le, read_i32_le, read_u32_le, read_u64_le, read_u8};
/// `MSG_CORPSE_QUERY`'s answer (`QueryHandler.cpp:258-304`), a lone `u8(0)` when not found; the
/// request is the same opcode, empty. The server also sends an unprompted not-found when the
/// request is the same opcode, empty. The server also sends an unprompted not-found when a looted
/// corpse turns to bones (`Map.cpp:3624-3629`), which means "drop the marker".
#[derive(Debug, Clone, PartialEq)]
pub struct CorpseLocation {
@@ -48,7 +48,7 @@ pub(super) fn read_corpse_reclaim_delay(r: &mut &[u8]) -> io::Result<u32> {
/// `SMSG_RESURRECT_REQUEST`, a resurrection offer (`Spell.cpp:5024-5044`). `sickness` warns the
/// accept brings resurrection sickness, `has_timer` keeps the reclaim-delay gate, and the stock
/// popup picks its variant by the two.
/// popup picks its variant by the two. `name` is empty for a player caster, named by guid instead.
#[derive(Debug, Clone, PartialEq)]
pub struct ResurrectRequestBody {
pub caster: u64,
@@ -24,7 +24,7 @@ pub struct QuestOption {
}
/// `CMSG_GOSSIP_HELLO` body: the NPC's full guid (`Npc.cpp:3`); it works on any interactable
/// creature, gossip flag or not (`Player.cpp:347`).
/// creature, gossip flag or not (`NPCHandler.cpp:347`).
pub fn gossip_hello(npc_guid: u64) -> Vec<u8> {
npc_guid.to_le_bytes().to_vec()
}
@@ -108,8 +108,8 @@ pub const NPC_TEXT_BLOCKS: usize = 8;
/// Read `SMSG_NPC_TEXT_UPDATE` (vmangos `GossipDef.cpp:298-369`), all eight blocks undecided; the
/// greeting is picked when the frame opens, by [`select_greeting`].
///
/// Deviation: the 1.12 client garbles a block in a non-zero language (`0x49b560`); we drop the
/// language because no 1.12 record sets one.
/// The language is parsed and dropped: the 1.12 client garbles a block in a non-zero language
/// (`0x49b560`), which is not built, and no 1.12 record is known to set one.
pub(super) fn read_npc_text_update(r: &mut &[u8]) -> io::Result<(u32, Vec<NpcTextBlock>)> {
let text_id = read_u32_le(r)?;
let mut blocks = Vec::with_capacity(NPC_TEXT_BLOCKS);
@@ -11,7 +11,7 @@ use crate::wire::{
};
/// The raid-assistant bit of a member's flags byte; bits 0-2 are the subgroup
/// (`Group.cpp:158,166`).
/// (`Group/Group.cpp:1382,1394`).
pub const GROUP_MEMBER_ASSISTANT: u8 = 0x80;
/// vmangos `GroupMemberStatus` (`Group/Group.h:102-111`); bit `0x20` is never set.
@@ -51,7 +51,7 @@ pub mod party_result {
pub struct GroupMemberEntry {
pub name: String,
pub guid: u64,
/// [`member_status`] bits (`GetGroupMemberStatus`, `Group.cpp:45-63`).
/// [`member_status`] bits (`GetGroupMemberStatus`, `Group/Group.cpp:45-63`).
pub status: u8,
/// Subgroup in bits 0-2, plus [`GROUP_MEMBER_ASSISTANT`].
pub flags: u8,
@@ -3,8 +3,9 @@
//!
//! Deviation: string reads are unbounded. The reference `0x4191b0` caps each field, counting the
//! NUL (roster name `0x30`, notes `0x80`, MOTD `0x200`, info `0x7d0`, guild name `0x60`, rank name
//! `0x40`), and on overflow empties it and no-ops every later read in the packet; every cap is far
//! above the server's own length limits, so a legitimate packet reads the same.
//! `0x40`), and on overflow empties it and no-ops every later read in the packet. Unbounded reads
//! cannot poison a packet, and every cap is far above the server's own length limits, so a
//! legitimate packet reads the same.
use std::io::{self, Read};
@@ -550,7 +551,8 @@ pub fn guild_set_public_note(name: &str, note: &str) -> Vec<u8> {
two_cstring_body(name, note)
}
/// Body of `CMSG_GUILD_SET_OFFICER_NOTE` (`Server/Packets/Guild.cpp:67-71`): name, then note.
/// Body of `CMSG_GUILD_SET_OFFICER_NOTE` (`Server/Packets/Guild.cpp:67-71`): name, then note;
/// the server requires [`guild_rank_right::EDIT_OFFICER_NOTE`].
pub fn guild_set_officer_note(name: &str, note: &str) -> Vec<u8> {
two_cstring_body(name, note)
}
@@ -68,7 +68,7 @@ pub enum InstanceResetFailure {
impl InstanceResetFailure {
/// Deviation: a reason of 3 or more is silent, where the reference prints an uninitialized
/// stack buffer; 3 is the server's `INSTANCERESET_FAIL_SILENTLY`.
/// stack buffer, because 3 is the server's `INSTANCERESET_FAIL_SILENTLY`.
pub fn from_wire(reason: u32) -> Option<Self> {
match reason {
0 => Some(Self::General),
+1 -1
View File
@@ -35,7 +35,7 @@ pub mod slot_type {
/// `LootType` (vmangos `LootMgr.h:49-61`). Only these four reach the wire: `SendLoot` remaps
/// skinning and insignia to `PICKPOCKETING`, fishing holes and fails to `FISHING`
/// (`Player.cpp:8117-8131`); `CMSG_LOOT` always gets `CORPSE` (`LootHandler.cpp:340-354`).
/// (`Player.cpp:8120-8136`); `CMSG_LOOT` always gets `CORPSE` (`LootHandler.cpp:380`).
pub mod loot_type {
pub const CORPSE: u8 = 1;
pub const PICKPOCKETING: u8 = 2;
+1 -1
View File
@@ -297,7 +297,7 @@ pub const LANGUAGE_ORCISH: u32 = 0x1;
/// `LANG_ADDON` (vmangos `SharedDefines.h:270`): marks a chat line as addon data, not speech;
/// 1.12 has no addon opcode, and the client routes such a line to `CHAT_MSG_ADDON`. The server
/// skips language, flood and sanitize checks for it, never rewrites it, and allows it only on the
/// group, guild and channel lanes (`ChatHandler.cpp:49,84,176-218`). The 1.12 `SendAddonMessage`
/// group, guild and channel lanes (`ChatHandler.cpp:49,84,172-219`). The 1.12 `SendAddonMessage`
/// (`0x49f920`) sends only PARTY, RAID, GUILD and BATTLEGROUND, and the receive side names any
/// other lane "UNKNOWN" (`0x49aff4`).
pub const LANGUAGE_ADDON: u32 = 0xFFFF_FFFF;
@@ -100,7 +100,7 @@ impl SpeedKind {
}
/// A mode the server grants the controlling client, one `MOVEMENTFLAGS` bit each (vmangos
/// `MovementInfo.h:25-62`, reference setters `0x7c7280`-`0x7c7370`). Root stops translation and
/// `MovementInfo.h:28-62`, reference setters `0x7c7280`-`0x7c7370`). Root stops translation and
/// falling but not turning (`0x618054`); water walk makes liquid walkable (aura 104); feather fall
/// caps the fall at 7 yd/s instead of 60.148 (aura 105, `0x7c5d20`); hover lifts ground contact by
/// 1.0 yd (aura 106, `0x6367b0`). Levitate (spell 1706) grants the last three at once.
+13 -8
View File
@@ -60,9 +60,11 @@ pub const CMSG_SET_WATCHED_FACTION: u16 = 0x0318; // 792
pub const SMSG_AUTH_CHALLENGE: u16 = 0x01EC;
pub const SMSG_AUTH_RESPONSE: u16 = 0x01EE;
/// The Warden anticheat challenge; vmangos kicks a client that leaves it unanswered for 30 s
/// (`Warden::Update`). Deviation: no Warden ([`crate::WardenRequired`]), so the connect refuses.
/// (`Warden::Update`). Deviation: benilla has no Warden, so the connect refuses such a server
/// ([`crate::WardenRequired`]) because a session there would only be kicked.
pub const SMSG_WARDEN_DATA: u16 = 0x02E6;
/// One record per `## Secure:` addon in `CMSG_AUTH_SESSION`, in order, with no count or names.
/// The answer to `CMSG_AUTH_SESSION`'s addon block: one record per `## Secure:` addon, in order,
/// with no count or names.
pub const SMSG_ADDON_INFO: u16 = 0x02EF;
pub const SMSG_COMPRESSED_UPDATE_OBJECT: u16 = 0x01F6;
/// A zlib envelope of whole movement packets. Routine: vmangos switches a session to it after 300
@@ -222,7 +224,8 @@ pub const SMSG_CLEAR_COOLDOWN: u16 = 0x01DE; // 478
pub const SMSG_COOLDOWN_CHEAT: u16 = 0x01E1; // 481
/// A timed item's remaining life in seconds; the display reads this, not `ITEM_FIELD_DURATION`
/// (`Item.cpp:1094`). It shares the reference handler (`0x5e4f69`) with the enchant update below.
/// (`Item.cpp:1094`). Its reference handler is the enchant update's, forking on the opcode
/// (`0x5e4f69`).
pub const SMSG_ITEM_TIME_UPDATE: u16 = 0x01EA; // 490
/// The only source of a temporary enchant's remaining time: the reference tooltip reads a
@@ -607,8 +610,8 @@ pub const CMSG_GMTICKET_SYSTEMSTATUS: u16 = 0x021A; // 538
pub const SMSG_GMTICKET_SYSTEMSTATUS: u16 = 0x021B; // 539
/// A ticket-state push: `u32` 1 updated, 2 closed, 3 survey offered; vmangos never sends it. The
/// reference re-asks for the ticket on 1 (`0x5e7932`). Deviation: 3 opens no survey window,
/// since vmangos can never offer one.
/// reference re-asks for the ticket on 1 (`0x5e7932`); 2 and 3 are recorded, not acted on, and
/// the survey window 3 offers is not built.
pub const SMSG_GM_TICKET_STATUS_UPDATE: u16 = 0x0328; // 808
// The bank: `CMSG_BANKER_ACTIVATE` opens a pure banker; `SMSG_SHOW_BANK` also arrives unasked
@@ -891,9 +894,10 @@ pub const CMSG_GROUP_SWAP_SUB_GROUP: u16 = 0x0280; // 640
pub const CMSG_GROUP_RAID_CONVERT: u16 = 0x028E; // 654
pub const CMSG_GROUP_ASSISTANT_LEADER: u16 = 0x028F; // 655
pub const SMSG_PARTY_MEMBER_STATS_FULL: u16 = 0x02F2; // 754
/// Both ways; the server's side is mode-prefixed (`Group.cpp:77-82` read, `:132-147` write).
/// Both ways; the server's side is mode-prefixed (`Packets/Group.cpp:77-82` read, `:132-147`
/// write).
pub const MSG_RAID_TARGET_UPDATE: u16 = 0x0321; // 801
/// Both ways: an empty body starts a check, a non-empty one answers (`Group.cpp:84-96`).
/// Both ways: an empty body starts a check, a non-empty one answers (`Packets/Group.cpp:84-96`).
pub const MSG_RAID_READY_CHECK: u16 = 0x0322; // 802
/// Ask for our raid lockouts: empty body, sent by `RequestRaidInfo()` on every RaidFrame show.
pub const CMSG_REQUEST_RAID_INFO: u16 = 0x02CD; // 717
@@ -975,7 +979,8 @@ pub const SMSG_UPDATE_WORLD_STATE: u16 = 0x02C3; // 707
// ── Instance and raid lockouts ──
/// "You are now saved to this instance": a `u32` flag, 0 from vmangos (`0x4e7e60`; 1 adds a debug
/// prefix). Deviation: 2 or more prints nothing; the reference prints an uninitialized buffer.
/// prefix). Deviation: 2 or more prints nothing, because the reference prints an uninitialized
/// buffer.
pub const SMSG_INSTANCE_SAVE_CREATED: u16 = 0x02CB; // 715
/// A raid-lockout warning: `u32 type`, `u32 mapId`, `u32 secondsUntilReset` (`0x49e1c0`).
pub const SMSG_RAID_INSTANCE_MESSAGE: u16 = 0x02FA; // 762
@@ -409,7 +409,8 @@ pub enum ServerPacket {
flags: u8,
members: Vec<(u64, u8)>,
},
/// `SMSG_CHAT_PLAYER_NOT_FOUND`: the whisper target is not online (`Chat.cpp:26-29`).
/// `SMSG_CHAT_PLAYER_NOT_FOUND`: the whisper target is not online
/// (`Server/Packets/Chat.cpp:26-29`).
ChatPlayerNotFound {
name: String,
},
@@ -420,7 +421,7 @@ pub enum ServerPacket {
text: String,
},
/// `SMSG_AREA_TRIGGER_MESSAGE`: why a trigger refused us, as `u32 length` then a cstring
/// (`WorldSession.cpp:882-898`).
/// (`WorldSession.cpp:883-898`).
AreaTriggerMessage {
text: String,
},
@@ -751,7 +752,7 @@ pub enum ServerPacket {
vendor: u64,
items: Vec<VendorItem>,
},
/// `SMSG_BUY_ITEM`: the vendor's stock after a purchase (`Item.cpp:190-196`).
/// `SMSG_BUY_ITEM`: the vendor's stock after a purchase (`Server/Packets/Item.cpp:190-196`).
BuyItem {
vendor: u64,
slot: u32,
@@ -230,7 +230,8 @@ fn parse_server_body(
// bytes left, `u32 billingTimeRemaining, u8 billingPlanFlags, u32 billingTimeRested`;
// then, for WAIT_QUEUE, `u32 position`. The 5 guarding a 9-byte group is the client's
// and mis-parses a 6..=9 byte body; matched on purpose. Deviation: a body too short
// for the position gives `None`, because the client redisplays a stale position.
// for the position gives `None` where the client redisplays a stale global, because
// showing a stale queue position as current is a client bug.
let result = read_u8(&mut r)?;
let queued = result == super::AUTH_WAIT_QUEUE;
let mut billing_time_rested = None;
+3 -3
View File
@@ -130,7 +130,7 @@ pub struct PetSpells {
pub state: u32,
/// The ten bar slots, in bar order.
pub bar: [PetActionEntry; PET_ACTION_SLOTS],
/// The pet spellbook, packed like the bar; only a permanent pet has one (`Player.cpp:17549`).
/// The pet spellbook, packed like the bar; only a permanent pet has one (`Player.cpp:17547`).
pub spells: Vec<PetActionEntry>,
pub cooldowns: Vec<PetSpellCooldown>,
}
@@ -231,7 +231,7 @@ pub struct PetMode {
pub state: u32,
}
/// Read `SMSG_PET_MODE`: `u64 petGuid, u32 state` (`0x4bdb10`; vmangos `Packets/Pet.cpp:101-108`).
/// Read `SMSG_PET_MODE`: `u64 petGuid, u32 state` (`0x4bdb10`; vmangos `Packets/Pet.cpp:104-111`).
pub(super) fn read_pet_mode(r: &mut impl Read) -> io::Result<PetMode> {
Ok(PetMode {
pet_guid: read_u64_le(r)?,
@@ -239,7 +239,7 @@ pub(super) fn read_pet_mode(r: &mut impl Read) -> io::Result<PetMode> {
})
}
/// Read `SMSG_PET_ACTION_FEEDBACK` (vmangos `Packets/Pet.cpp:87-90`): one reason byte.
/// Read `SMSG_PET_ACTION_FEEDBACK` (vmangos `Packets/Pet.cpp:90-93`): one reason byte.
pub(super) fn read_pet_action_feedback(r: &mut impl Read) -> io::Result<u8> {
read_u8(r)
}
@@ -13,7 +13,7 @@ use crate::wire::{
/// The guild charter's item entry, vmangos `GUILD_CHARTER` (`PetitionsHandler.cpp:37`).
pub const CHARTER_ITEM_ENTRY: u32 = 5863;
/// The charter's display id, vmangos `CHARTER_DISPLAY_ID` (`PetitionsHandler.cpp:38`).
/// The charter's display id, vmangos `CHARTER_DISPLAY_ID` (`PetitionsHandler.cpp:39`).
pub const CHARTER_DISPLAY_ID: u32 = 16161;
/// The template-flag bit of a signable petition (vmangos `ItemPrototype.h:77`); the reference
@@ -29,7 +29,7 @@ pub const MAX_PETITION_SIGNATURES: usize = 9;
pub const CHARTER_NAME_MAX_LENGTH: usize = 24;
/// 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.
/// (vmangos `PetitionSigns`, `Guild/Guild.h:147-154`); each packet uses its own subset.
pub mod petition_result {
/// Signed, or turned in; both packets.
pub const OK: u32 = 0;
@@ -52,13 +52,13 @@ pub mod petition_result {
/// (`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`).
/// Row index, 1-based (`PetitionsHandler.cpp:496`).
pub index: u32,
/// The item entry sold, [`CHARTER_ITEM_ENTRY`] in practice.
pub charter_entry: u32,
/// Its display id, [`CHARTER_DISPLAY_ID`] in practice.
pub charter_display_id: u32,
/// Price in copper, an `int32` on the wire (vmangos charges 1000, `PetitionsHandler.cpp:39`).
/// Price in copper, an `int32` on the wire (vmangos charges 1000, `PetitionsHandler.cpp:38`).
pub charter_cost: i32,
/// vmangos sends 1; its header says a row "must be `&1` to show it in the UI"
/// (`Petition.h:169`).
@@ -79,7 +79,7 @@ pub struct PetitionShowList {
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`); 8 is generous.
// vmangos sends exactly one charter (`PetitionsHandler.cpp:504`); 8 is generous.
let mut entries = Vec::with_capacity(capacity_hint(count, 8));
for _ in 0..count {
entries.push(PetitionShowListEntry {
@@ -170,9 +170,9 @@ pub struct PetitionQueryResponse {
pub owner: u64,
/// The proposed guild's name, carried by no other packet.
pub name: String,
/// Free text on the charter; empty on vmangos (`PetitionsHandler.cpp:178`).
/// Free text on the charter; empty, as vmangos never sets it (`PetitionsHandler.cpp:177-183`).
pub body_text: String,
/// vmangos sends 1 (`PetitionsHandler.cpp:179`); meaning unknown.
/// vmangos sends 1 (`PetitionsHandler.cpp:181`); meaning unknown.
pub flags: u32,
/// Signatures required, which the reference's Request-Signature button tests; vmangos sends 9
/// even when its `MinPetitionSigns` is lower (`PetitionsHandler.cpp:182`).
@@ -189,7 +189,7 @@ pub struct PetitionQueryResponse {
pub allowed_classes: u32,
/// Race mask restriction, 0 on vmangos.
pub allowed_races: u32,
/// Gender restriction: a `u16`, the packet's one odd width (`Packets/Petition.cpp:94`).
/// Gender restriction: a `u16`, the packet's one odd width (`Packets/Petition.cpp:95`).
pub allowed_gender: u16,
/// Minimum level, 0 on vmangos.
pub allowed_min_level: u32,
@@ -16,8 +16,8 @@ pub struct XpGain {
}
/// 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; vmangos always
/// sends 1.0, meaning none (`Player.cpp:3039`), so 1.12 shows no group-bonus line either.
/// `u32 base, f32 groupBonus`. The group bonus is read and dropped, its chat line not built;
/// vmangos always sends 1.0, meaning none (`Player.cpp:3039`).
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)?;
@@ -116,7 +116,7 @@ pub fn questgiver_status_query(npc: u64) -> Vec<u8> {
}
/// 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`).
/// `CMSG_GOSSIP_HELLO`, through `SendPreparedGossip` (`QuestHandler.cpp:104`).
pub fn questgiver_hello(npc: u64) -> Vec<u8> {
npc.to_le_bytes().to_vec()
}
@@ -41,28 +41,29 @@ pub(super) fn read_initial_spells(r: &mut impl Read) -> io::Result<(Vec<u16>, Ve
Ok((spells, cooldowns))
}
/// Read `SMSG_LEARNED_SPELL` (vmangos `Spell.cpp:175-179`): `u16 spellId`, then an action-bar slot
/// the client does not use. The one message that adds to the book after login.
/// Read `SMSG_LEARNED_SPELL` (vmangos `Server/Packets/Spell.cpp:175-179`): `u16 spellId`, then
/// an action-bar slot the client does not use. The one message that adds to the book after login.
pub(super) fn read_learned_spell(r: &mut impl Read) -> io::Result<u16> {
let spell_id = read_u16_le(r)?;
let _action_bar_slot = read_u16_le(r)?;
Ok(spell_id)
}
/// Read `SMSG_REMOVED_SPELL` (vmangos `Spell.cpp:181`): a bare `u16 spellId`, no action-bar slot.
/// Read `SMSG_REMOVED_SPELL` (vmangos `Server/Packets/Spell.cpp:181`): a bare `u16 spellId`, no
/// action-bar slot.
pub(super) fn read_removed_spell(r: &mut impl Read) -> io::Result<u16> {
read_u16_le(r)
}
/// Read `SMSG_SUPERCEDED_SPELL` (vmangos `Spell.cpp:169-173`): `(oldSpellId, newSpellId)`; a
/// rank-up replaces the old spell in both the book and the action bar.
/// Read `SMSG_SUPERCEDED_SPELL` (vmangos `Server/Packets/Spell.cpp:169-173`): `(oldSpellId,
/// newSpellId)`; a rank-up replaces the old spell in both the book and the action bar.
pub(super) fn read_superceded_spell(r: &mut impl Read) -> io::Result<(u16, u16)> {
Ok((read_u16_le(r)?, read_u16_le(r)?))
}
/// Read `SMSG_SPELL_COOLDOWN` (vmangos `Spell.cpp:142-151`, client `0x6e9460`): a `u64` caster,
/// then `(spell, ms)` pairs to the end, with no flags byte in 1.12. `ms == 0` means the spell's own
/// `Spell.dbc` recovery times. Sent for school lockouts and pet cooldowns.
/// Read `SMSG_SPELL_COOLDOWN` (vmangos `Server/Packets/Spell.cpp:142-150`, client `0x6e9460`): a
/// `u64` caster, then `(spell, ms)` pairs to the end, with no flags byte in 1.12. `ms == 0` means
/// the spell's own `Spell.dbc` recovery times. Sent for school lockouts and pet cooldowns.
pub(super) fn read_spell_cooldown(r: &mut &[u8]) -> io::Result<(u64, Vec<(u32, u32)>)> {
let caster = read_u64_le(r)?;
let mut cooldowns = Vec::new();
@@ -74,15 +75,18 @@ pub(super) fn read_spell_cooldown(r: &mut &[u8]) -> io::Result<(u64, Vec<(u32, u
Ok((caster, cooldowns))
}
/// Read `SMSG_ITEM_COOLDOWN` (vmangos `Item.cpp:229-233`): `(item_guid, spell_id)`. The client
/// (`0x6e95d0`) gives the item a hardcoded 30 s cooldown; no duration is on the wire.
/// Read `SMSG_ITEM_COOLDOWN` (vmangos `Server/Packets/Item.cpp:229-233`): `(item_guid, spell_id)`,
/// sent when an item with an on-use spell is equipped (`Player::ApplyEquipCooldown`,
/// `Player.cpp:19358-19384`). The client (`0x6e95d0`) gives the item a hardcoded 30 s cooldown; no
/// duration is on the wire.
pub(super) fn read_item_cooldown(r: &mut impl Read) -> io::Result<(u64, u32)> {
Ok((read_u64_le(r)?, read_u32_le(r)?))
}
/// Read `SMSG_COOLDOWN_EVENT` or `SMSG_CLEAR_COOLDOWN` (vmangos `Spell.cpp:152-167`, client
/// `0x6e9670`): the `u32` spell id first, then the `u64` caster. EVENT starts the timers of an
/// on-hold (`SPELL_ATTR_COOLDOWN_ON_EVENT`) cooldown; CLEAR removes the cooldown.
/// Read `SMSG_COOLDOWN_EVENT` or `SMSG_CLEAR_COOLDOWN` (vmangos
/// `Server/Packets/Spell.cpp:152-167`, client `0x6e9670`): the `u32` spell id first, then the
/// `u64` caster. EVENT starts the timers of an on-hold (`SPELL_ATTR_COOLDOWN_ON_EVENT`) cooldown;
/// CLEAR removes the cooldown.
pub(super) fn read_cooldown_event(r: &mut impl Read) -> io::Result<(u32, u64)> {
Ok((read_u32_le(r)?, read_u64_le(r)?))
}
@@ -268,8 +268,8 @@ pub(super) fn read_update_aura_duration(r: &mut impl Read) -> io::Result<(u8, u3
Ok((slot, remaining_ms))
}
/// Read `SMSG_PLAY_SPELL_VISUAL` (vmangos `Spell.cpp:54-58`): raw `u64` unit, `u32` kit id. The
/// client (`0x6e98d0`) checks the kit against `SpellVisualKit.dbc` and plays it at stage 0.
/// Read `SMSG_PLAY_SPELL_VISUAL` (vmangos `Packets/Spell.cpp:54-58`): raw `u64` unit, `u32` kit
/// id. The client (`0x6e98d0`) checks the kit against `SpellVisualKit.dbc` and plays it at stage 0.
pub(super) fn read_play_spell_visual(r: &mut impl Read) -> io::Result<(u64, u32)> {
let unit = read_u64_le(r)?;
let kit_id = read_u32_le(r)?;
@@ -99,7 +99,8 @@ impl ObjectFields {
self.unit_pet_number() != 0
}
/// `UNIT_FIELD_PETNUMBER`: `GetUnitName` (`0x609210`) keys the pet-name cache on it when
/// nonzero; a `HIGHGUID_PET` guid's number is not a substitute, no name query answers it.
/// nonzero. A `HIGHGUID_PET` guid's number is no substitute: a companion's is never filed in
/// its charm info, so no pet-name query answers it.
pub fn unit_pet_number(&self) -> u32 {
self.get_u32(FIELD_UNIT_PETNUMBER).unwrap_or(0)
}
@@ -132,9 +132,10 @@ impl WorldWriter {
)
}
/// Send an addon message (`SendAddonMessage`). Its language, [`messages::LANGUAGE_ADDON`], is
/// the only mark of addon data (`0x49f920`); vmangos gates it on `AddonChannel` and skips the
/// language gate, flood control and sanitizing.
/// Send an addon message (`SendAddonMessage`): `text` is the caller's composed `prefix` TAB
/// `message`, on the party, raid, guild or battleground lane. Its language,
/// [`messages::LANGUAGE_ADDON`], is the only mark of addon data (`0x49f920`); vmangos gates it
/// on `AddonChannel` and skips the language gate, flood control and sanitizing.
pub fn send_addon_message(&mut self, chat_type: u32, text: &str) -> Result<()> {
self.send(
opcode::CMSG_MESSAGECHAT,
@@ -1,5 +1,6 @@
//! The gossip sends. `CMSG_GOSSIP_HELLO` opens every NPC service window: vmangos accepts it for any
//! interactable creature (`CanInteractWithNPC` with `UNIT_NPC_FLAG_NONE`, `Player.cpp:347`).
//! interactable creature (`GetNPCIfCanInteractWith` with `UNIT_NPC_FLAG_NONE`,
//! `NPCHandler.cpp:347`).
use anyhow::Result;
+2 -1
View File
@@ -310,7 +310,8 @@ fn messagechat_sendable_types_golden() {
/// `SendAddonMessage` (reference `0x49f920`) sends `CMSG_MESSAGECHAT` on one of four lanes: `u32`
/// type, `u32` language `LANG_ADDON`, then one cstring of prefix, TAB and message (`0x49f9b3`);
/// there is no prefix or target field. The receiver splits on the first TAB (`0x49a8d0`).
/// there is no prefix or target field. The receiver splits on the first TAB (`0x49a8d0`); vmangos
/// relays the TAB intact, as it skips `SanitizeChatMessage` for addon chat (`ChatHandler.cpp:49`).
#[test]
fn addon_message_bodies_golden() {
assert_eq!(
+1 -1
View File
@@ -383,7 +383,7 @@ fn guild_roster_with_no_members_is_just_the_head() {
}
/// A viewer without `GR_RIGHT_VIEWOFFNOTE` gets `""` for every officer note (vmangos
/// `Guild/Guild.cpp:821`, `:844`): the cstring's NUL is still on the wire.
/// `Guild/Guild.cpp:819`, `:846`): the cstring's NUL is still on the wire.
#[test]
fn guild_roster_reads_the_empty_officer_notes_of_an_unprivileged_viewer() {
let mut body = roster_head(2, "motd", "info", &[guild_rank_right::ALL, 0x0000_0003]);
@@ -309,7 +309,7 @@ fn death_arc_family_parses_and_decodes() {
}
}
// The movement-flag bits (vmangos `Objects/MovementInfo.h:25-62`).
// The movement-flag bits (vmangos `Objects/MovementInfo.h:28-62`).
assert_eq!(MoveMode::Root.flag(), 0x0000_1000);
assert_eq!(MoveMode::WaterWalk.flag(), 0x1000_0000);
assert_eq!(MoveMode::FeatherFall.flag(), 0x2000_0000);
+1 -1
View File
@@ -1031,7 +1031,7 @@ fn combat_log_wire_golden() {
}
}
/// Raw caster guid, u32 spellId, u32 count, raw guids (vmangos `Spell.cpp:4970-4997`); in the
/// Raw caster guid, u32 spellId, u32 count, raw guids (vmangos `Spell.cpp:4974-4997`); in the
/// reference (`0x6e9820`) the only writer of the beams' chain-target array (`unit+0xd44`).
#[test]
fn spell_chain_targets_wire() {
+2 -2
View File
@@ -189,7 +189,7 @@ fn trainer_buy_result_wire() {
#[test]
fn learned_and_superceded_spell_wire() {
// SMSG_LEARNED_SPELL: u16 spellId + u16 actionBarSlot, the slot unused by the client and
// dropped (vmangos `Spell.cpp:175-179`).
// dropped (vmangos `Packets/Spell.cpp:175-179`).
match messages::parse_server(messages::opcode::SMSG_LEARNED_SPELL, &hx("cb19aaaa")).unwrap() {
ServerPacket::LearnedSpell { spell_id } => assert_eq!(spell_id, 6603),
other => panic!("learned spell, got {}", other.name()),
@@ -201,7 +201,7 @@ fn learned_and_superceded_spell_wire() {
other => panic!("spell learned event, got {other:?}"),
}
// SMSG_SUPERCEDED_SPELL: u16 oldSpellId + u16 newSpellId (vmangos `Spell.cpp:169-173`).
// SMSG_SUPERCEDED_SPELL: u16 oldSpellId + u16 newSpellId (vmangos `Packets/Spell.cpp:169-173`).
match messages::parse_server(messages::opcode::SMSG_SUPERCEDED_SPELL, &hx("cb19cc19")).unwrap()
{
ServerPacket::SupercededSpell {
+2 -2
View File
@@ -364,7 +364,7 @@
- `WOW_PARTICLE_DEPTHDUMP_BONES` — benilla-world/src/particles/depthdump.rs
- `WOW_PARTICLE_FLAT` — benilla-world/src/particles/render.rs
- `WOW_PARTICLE_NODEPTH` — benilla-world/src/particles/render.rs
- `WOW_PASS` — benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/run_mode.rs
- `WOW_PASS` — benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/run_mode.rs, benilla-protocol/examples/login_refusal_probe.rs, benilla-protocol/examples/park_probe.rs
- `WOW_PERF_HUD` — benilla-app/src/perf/hud.rs
- `WOW_PHASE` — benilla-app/src/capture/phase_probe.rs, benilla-app/src/dev.rs, benilla-world/src/bgwin.rs
- `WOW_PHASE_AT` — benilla-app/src/capture/phase_probe.rs
@@ -493,7 +493,7 @@
- `WOW_UNIT_VISUALS` — benilla-app/src/capture/probes/visual_census.rs, benilla-app/src/dev.rs
- `WOW_UNIT_VISUALS_RADIUS` — benilla-app/src/capture/probes/visual_census.rs
- `WOW_UPLOAD_BUDGET` — benilla-world/src/world_plugins.rs
- `WOW_USER` — benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/run_mode.rs
- `WOW_USER` — benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/run_mode.rs, benilla-protocol/examples/login_refusal_probe.rs, benilla-protocol/examples/park_probe.rs
- `WOW_VIS_DUMP` — benilla-app/src/capture/probes/live_fps.rs
- `WOW_VIS_TRACE` — benilla-world/src/model_render/visibility.rs
- `WOW_VISTA_AT` — benilla-app/src/capture/mod.rs