comments: protocol, the trade, trainer and tutorial messages and the update fields
760 comment lines become 262; the code is unchanged. One doc said the opposite of its code: corpse_look is None when either BYTES word is missing, not neither. The ObjectFields doc had drifted onto CorpseLook and is back on its struct. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,93 +1,75 @@
|
||||
//! The player-to-player trade arc's wire layer (decision 0592 phase P0): the request verbs the
|
||||
//! trade window sends and the two status packets the server pushes back. Layout VERIFIED against
|
||||
//! vmangos `Handlers/TradeHandler.cpp`, `Server/Packets/Trade.{h,cpp}`, `Objects/TradeData.{h,cpp}`
|
||||
//! and `SharedDefines.h` (read directly at decision time). Trade renders nothing derived from
|
||||
//! WoW.exe, so vmangos is the whole wire authority: its `ReadFromWorldPacket` pins every CMSG body
|
||||
//! below, its `AppendBodyTo`/`SendUpdateTrade` pin both SMSG parses — no wow-re dispatch is
|
||||
//! load-bearing (decision 0592, "No wow-re dispatch").
|
||||
//! Player-to-player trade wire: the trade window's request verbs and the two status packets the
|
||||
//! server pushes back (vmangos `TradeHandler.cpp`, `Server/Packets/Trade.cpp`).
|
||||
|
||||
use std::io;
|
||||
|
||||
use crate::wire::{read_i32_le, read_u32_le, read_u64_le, read_u8};
|
||||
|
||||
/// `TRADE_SLOT_COUNT` (`TradeData.h`): seven slots per side — six tradeable (0..6) plus the
|
||||
/// seventh **non-traded / enchant** slot ([`TRADE_SLOT_NONTRADED`]), whose item is not exchanged
|
||||
/// but is the target an enchant/lockpick spell is applied to through the window.
|
||||
/// Slots per side (`TradeData.h`): six traded plus the non-traded enchant slot.
|
||||
pub const TRADE_SLOT_COUNT: usize = 7;
|
||||
/// `TRADE_SLOT_TRADED_COUNT` — the six slots (0..6) whose items actually change hands.
|
||||
/// Slots 0..6, whose items change hands.
|
||||
pub const TRADE_SLOT_TRADED_COUNT: usize = 6;
|
||||
/// `TRADE_SLOT_NONTRADED` — the 7th slot (index 6, UI id 7): an item parked here stays with its
|
||||
/// owner; it is the enchant/spell target, not part of the exchange.
|
||||
/// UI slot 7: its item stays with its owner as the target of an enchant or lockpick spell.
|
||||
pub const TRADE_SLOT_NONTRADED: usize = 6;
|
||||
|
||||
/// The trade state machine's status codes (`SharedDefines.h` `enum TradeStatus`, 0..=22). The
|
||||
/// tail-carrying members hold their `SMSG_TRADE_STATUS` payload inline (VERIFIED vmangos
|
||||
/// `WorldPackets::Trade::TradeStatus::AppendBodyTo`): only `BEGIN_TRADE`, `CLOSE_WINDOW` and
|
||||
/// `ONLY_CONJURED` ride a tail; every other status is the bare `u32`. [`TradeStatus::Unknown`]
|
||||
/// keeps an out-of-range code (never emitted by vmangos on 5875) parseable rather than fatal.
|
||||
/// `SMSG_TRADE_STATUS` codes (`SharedDefines.h` `enum TradeStatus`), each with its tail inline.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TradeStatus {
|
||||
/// `0` — the target is already trading / busy (also the initiator's own "already trading").
|
||||
/// The target is busy, or the initiator is already trading.
|
||||
Busy,
|
||||
/// `1` — sent to the *target* of a fresh `CMSG_INITIATE_TRADE`; `partner` is the initiator's
|
||||
/// guid. The client answers `CMSG_BEGIN_TRADE`, which makes the server emit `OpenWindow` to
|
||||
/// both sides.
|
||||
/// To the target of `CMSG_INITIATE_TRADE`; its `CMSG_BEGIN_TRADE` opens both windows.
|
||||
BeginTrade { partner: u64 },
|
||||
/// `2` — open the trade window (fires the client's `TRADE_SHOW`). Sent to both sides once the
|
||||
/// target's `CMSG_BEGIN_TRADE` arrives.
|
||||
/// Opens the window on both sides (the client's `TRADE_SHOW`).
|
||||
OpenWindow,
|
||||
/// `3` — the trade was cancelled; close both windows.
|
||||
/// The trade was cancelled; both windows close.
|
||||
Canceled,
|
||||
/// `4` — the *partner* pressed Trade (drives the accept-glow on their column).
|
||||
/// The partner pressed Trade (the accept highlight on their column).
|
||||
Accept,
|
||||
/// `5` — a second "busy" code, handled as busy.
|
||||
/// A second busy code, handled as `Busy`.
|
||||
Busy2,
|
||||
/// `6` — no such target for `CMSG_INITIATE_TRADE`.
|
||||
/// No such target for `CMSG_INITIATE_TRADE`.
|
||||
NoTarget,
|
||||
/// `7` — a change happened after an accept (or the 200 ms scam-delay bounced an accept):
|
||||
/// drop the accept and go back to editing.
|
||||
/// An offer changed after an accept, or the 200 ms scam delay bounced one: the accept drops.
|
||||
BackToTrade,
|
||||
/// `8` — both sides accepted and the swap completed; close both windows.
|
||||
/// Both sides accepted and the swap completed; both windows close.
|
||||
Complete,
|
||||
/// `9` — the trade was rejected.
|
||||
/// The trade was rejected.
|
||||
Rejected,
|
||||
/// `10` — the partner is out of `TRADE_DISTANCE` (also the flying/no-map initiate refusal).
|
||||
/// Out of `TRADE_DISTANCE`; also the refusal to initiate while flying or off-map.
|
||||
TargetTooFar,
|
||||
/// `11` — cross-faction trade refused (unless the server allows two-side interaction).
|
||||
/// Cross-faction, unless the server allows two-side interaction.
|
||||
WrongFaction,
|
||||
/// `12` — close the window; `result`/`item_limit_category` are the vanilla tail (usually 0;
|
||||
/// the middle `u8` vmangos writes is consumed but carries nothing for player trade).
|
||||
/// The tail is usually zero; the `u8` vmangos writes between the two fields is dropped.
|
||||
CloseWindow {
|
||||
result: u32,
|
||||
item_limit_category: u32,
|
||||
},
|
||||
/// `13` — vmangos comments it is "handled with TRADE_STATUS_TRADE_CANCELED"; kept distinct so
|
||||
/// the wire round-trips.
|
||||
/// Per a vmangos note, handled as `Canceled`; kept distinct so the code round-trips.
|
||||
Unknown13,
|
||||
/// `14` — the target has you on ignore.
|
||||
/// The target has you on ignore.
|
||||
IgnoreYou,
|
||||
/// `15` — you are stunned.
|
||||
/// You are stunned.
|
||||
YouStunned,
|
||||
/// `16` — the target is stunned.
|
||||
/// The target is stunned.
|
||||
TargetStunned,
|
||||
/// `17` — you are dead.
|
||||
/// You are dead.
|
||||
YouDead,
|
||||
/// `18` — the target is dead.
|
||||
/// The target is dead.
|
||||
TargetDead,
|
||||
/// `19` — you are logging out.
|
||||
/// You are logging out.
|
||||
YouLogout,
|
||||
/// `20` — the target is logging out.
|
||||
/// The target is logging out.
|
||||
TargetLogout,
|
||||
/// `21` — a trial account restriction.
|
||||
/// A trial account restriction.
|
||||
TrialAccount,
|
||||
/// `22` — "you can only trade conjured items…"; `slot` names the offending trade slot.
|
||||
/// "You can only trade conjured items"; `slot` is the offending trade slot.
|
||||
OnlyConjured { slot: u8 },
|
||||
/// A code outside 0..=22 (vmangos never sends one on 5875) — kept parseable, no tail read.
|
||||
/// A code outside 0..=22, which vmangos never sends; read with no tail.
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
impl TradeStatus {
|
||||
/// The raw `u32` code this status serializes as (the enum discriminant on the wire).
|
||||
/// The `u32` status code on the wire.
|
||||
pub fn code(self) -> u32 {
|
||||
match self {
|
||||
TradeStatus::Busy => 0,
|
||||
@@ -118,13 +100,10 @@ impl TradeStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// One item shown in a trade slot, as it rides `SMSG_TRADE_STATUS_EXTENDED`'s fixed 60-byte
|
||||
/// (15×u32) per-slot block (VERIFIED vmangos `WorldSession::SendUpdateTrade`). An empty slot is
|
||||
/// all-zero on the wire and folds to `None` rather than a zeroed `Some` (the `entry == 0` signal,
|
||||
/// same convention as [`super::MailAttachment`]).
|
||||
/// One trade slot's 60-byte item block; an all-zero block is an empty slot.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TradeItem {
|
||||
/// `Item.dbc` entry (`OBJECT_FIELD_ENTRY`) — the key to name/icon via `ITEM_QUERY_SINGLE`.
|
||||
/// `Item.dbc` entry (`OBJECT_FIELD_ENTRY`), the key for name and icon via `ITEM_QUERY_SINGLE`.
|
||||
pub entry: u32,
|
||||
pub display_id: u32,
|
||||
pub count: u32,
|
||||
@@ -133,7 +112,7 @@ pub struct TradeItem {
|
||||
pub gift_creator: u64,
|
||||
pub perm_enchant: u32,
|
||||
pub creator: u64,
|
||||
/// Spell charges — signed (a negative count means "N uses left" on some items).
|
||||
/// Signed: a negative count means N uses left on some items.
|
||||
pub charges: i32,
|
||||
pub suffix_factor: u32,
|
||||
pub random_prop_id: u32,
|
||||
@@ -142,62 +121,51 @@ pub struct TradeItem {
|
||||
pub durability: u32,
|
||||
}
|
||||
|
||||
/// A decoded `SMSG_TRADE_STATUS_EXTENDED` — the full item/gold snapshot for **one** window side.
|
||||
/// The server pushes one of these per side whenever that side's offer changes.
|
||||
/// `SMSG_TRADE_STATUS_EXTENDED`: one side's snapshot, sent whenever that side's offer changes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TradeStatusExtended {
|
||||
/// `true` = the *partner's* (recipient / right) column, `false` = our own (left) column
|
||||
/// (vmangos's `trader_state`: `1` for the trader's data, `0` for ours).
|
||||
/// The partner's (right) column when the wire byte is 1, ours (left) when 0.
|
||||
pub their_window: bool,
|
||||
/// Gold offered on this side, in copper.
|
||||
/// In copper.
|
||||
pub gold: u32,
|
||||
/// The spell applied to this side's non-traded slot item (`0` = none) — the enchant slot.
|
||||
/// The spell cast on this side's non-traded slot item, 0 for none.
|
||||
pub enchant_spell_id: u32,
|
||||
/// The seven slots (0..[`TRADE_SLOT_COUNT`]); `None` = empty.
|
||||
pub slots: [Option<TradeItem>; TRADE_SLOT_COUNT],
|
||||
}
|
||||
|
||||
// ── Client → server bodies (what vmangos `ReadFromWorldPacket` consumes) ─────────────────────
|
||||
// ── Client → server bodies ──
|
||||
|
||||
/// Body of `CMSG_INITIATE_TRADE` (vmangos `InitiateTrade::ReadFromWorldPacket`): one full 8-byte
|
||||
/// target-player guid. The server answers the initiator on any refusal (`SMSG_TRADE_STATUS`) and,
|
||||
/// on success, sends the *target* `BeginTrade`.
|
||||
/// `CMSG_INITIATE_TRADE`: a refusal is a status back to us; success sends the target `BeginTrade`.
|
||||
pub fn initiate_trade(target: u64) -> Vec<u8> {
|
||||
target.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Body of `CMSG_ACCEPT_TRADE` (vmangos `AcceptTrade::ReadFromWorldPacket`): one `u32` the server
|
||||
/// **read-skips**. The real client sends `1` once it has seen `OPEN_WINDOW`; we send `1` too (the
|
||||
/// value is discarded either way).
|
||||
/// `CMSG_ACCEPT_TRADE`: a `u32` the server skips; the 1.12 client sends 1.
|
||||
pub fn accept_trade() -> Vec<u8> {
|
||||
1u32.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Body of `CMSG_SET_TRADE_ITEM` (vmangos `SetTradeItem::ReadFromWorldPacket`): `u8 tradeSlot,
|
||||
/// u8 bag, u8 slot` — put the item at inventory (`bag`, `slot`) into trade slot `tradeSlot`.
|
||||
/// `CMSG_SET_TRADE_ITEM`: puts the item at (`bag`, `slot`) into `trade_slot`.
|
||||
pub fn set_trade_item(trade_slot: u8, bag: u8, slot: u8) -> Vec<u8> {
|
||||
vec![trade_slot, bag, slot]
|
||||
}
|
||||
|
||||
/// Body of `CMSG_CLEAR_TRADE_ITEM` (vmangos `ClearTradeItem::ReadFromWorldPacket`): `u8 tradeSlot`.
|
||||
/// `CMSG_CLEAR_TRADE_ITEM` body.
|
||||
pub fn clear_trade_item(trade_slot: u8) -> Vec<u8> {
|
||||
vec![trade_slot]
|
||||
}
|
||||
|
||||
/// Body of `CMSG_SET_TRADE_GOLD` (vmangos `SetTradeGold::ReadFromWorldPacket`): `u32 copper`.
|
||||
/// `CMSG_SET_TRADE_GOLD` body, in copper.
|
||||
pub fn set_trade_gold(copper: u32) -> Vec<u8> {
|
||||
copper.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
// `CMSG_BEGIN_TRADE`, `CMSG_BUSY_TRADE`, `CMSG_IGNORE_TRADE`, `CMSG_UNACCEPT_TRADE` and
|
||||
// `CMSG_CANCEL_TRADE` carry **empty** bodies (vmangos reads them as `NullClientPacket`); the
|
||||
// writer sends `&[]` directly, so they need no builder here.
|
||||
// `CMSG_CANCEL_TRADE` have empty bodies (vmangos `NullClientPacket`), so they need no builder.
|
||||
|
||||
// ── Server → client parses (what vmangos `AppendBodyTo` / `SendUpdateTrade` emit) ─────────────
|
||||
// ── Server → client parses ──
|
||||
|
||||
/// Read `SMSG_TRADE_STATUS` (VERIFIED vmangos `TradeStatus::AppendBodyTo`): `u32 status`, then a
|
||||
/// per-status tail — `BEGIN_TRADE → u64 partnerGuid`; `CLOSE_WINDOW → u32 result, u8 unk, u32
|
||||
/// itemLimitCategory`; `ONLY_CONJURED → u8 slot`; every other status has none.
|
||||
/// `SMSG_TRADE_STATUS` (vmangos `TradeStatus::AppendBodyTo`): a `u32` code, then its tail.
|
||||
pub(super) fn read_trade_status(r: &mut &[u8]) -> io::Result<TradeStatus> {
|
||||
let code = read_u32_le(r)?;
|
||||
Ok(match code {
|
||||
@@ -238,12 +206,8 @@ pub(super) fn read_trade_status(r: &mut &[u8]) -> io::Result<TradeStatus> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Read `SMSG_TRADE_STATUS_EXTENDED` (VERIFIED vmangos `WorldSession::SendUpdateTrade`):
|
||||
/// `u8 which`, `u32 slotCount`, `u32 slotCount` (again — the two counts match), `u32 gold`,
|
||||
/// `u32 enchantSpellId`, then [`TRADE_SLOT_COUNT`] records of `u8 slotIndex` + a fixed 60-byte
|
||||
/// (15×u32) item block (all-zero → the slot is empty). The `slotIndex` byte places the block, so
|
||||
/// a block is stored at `slots[slotIndex]` (defensive against reordering; vmangos writes 0..7 in
|
||||
/// order).
|
||||
/// `SMSG_TRADE_STATUS_EXTENDED` (vmangos `WorldSession::SendUpdateTrade`): `u8 which`, the slot
|
||||
/// count twice, gold, enchant spell, then seven records of `u8 index` and a 60-byte item block.
|
||||
pub(super) fn read_trade_status_extended(r: &mut &[u8]) -> io::Result<TradeStatusExtended> {
|
||||
let which = read_u8(r)?;
|
||||
let _slot_count_a = read_u32_le(r)?;
|
||||
@@ -297,7 +261,7 @@ pub(super) fn read_trade_status_extended(r: &mut &[u8]) -> io::Result<TradeStatu
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── CMSG body goldens (byte-exact against vmangos's ReadFromWorldPacket layout) ──
|
||||
// ── CMSG body goldens ──
|
||||
|
||||
#[test]
|
||||
fn initiate_trade_is_the_target_guid_le() {
|
||||
@@ -314,7 +278,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_trade_item_is_three_u8s() {
|
||||
// Put bag 0 / slot 23 into trade slot 2.
|
||||
assert_eq!(set_trade_item(2, 0, 23), vec![2, 0, 23]);
|
||||
}
|
||||
|
||||
@@ -394,8 +357,7 @@ mod tests {
|
||||
|
||||
// ── SMSG_TRADE_STATUS_EXTENDED parse golden ──
|
||||
|
||||
/// Build a full 444-byte snapshot the way vmangos does: a 17-byte header, then 7 slot records
|
||||
/// (each a `u8 index` + 60-byte block). `items[i] = Some(entry)` fills slot `i`; `None` zeroes it.
|
||||
/// A 444-byte snapshot as vmangos builds it; `Some(entry)` fills a slot, `None` zeroes it.
|
||||
fn extended_wire(
|
||||
which: u8,
|
||||
gold: u32,
|
||||
@@ -434,7 +396,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn extended_snapshot_is_always_444_bytes() {
|
||||
// 17-byte header (u8 which + 4×u32) + 7 slot records of (u8 index + 60-byte block).
|
||||
// A 17-byte header (u8 and four u32), then 7 records of u8 index and a 60-byte block.
|
||||
let wire = extended_wire(0, 0, 0, [None; TRADE_SLOT_COUNT]);
|
||||
assert_eq!(wire.len(), 17 + TRADE_SLOT_COUNT * 61);
|
||||
assert_eq!(wire.len(), 444);
|
||||
|
||||
@@ -1,84 +1,58 @@
|
||||
//! Trainer messages — the class/profession trainer window's wire (opcodes 432-436, vmangos
|
||||
//! `Opcodes_1_12_1.h:433-437`, VERIFIED). Bodies from vmangos `NPCHandler.cpp` (`SendTrainerList` +
|
||||
//! its per-service helper `SendTrainerSpellHelper`) and the hand-serialized `Server/Packets/Npc.cpp`
|
||||
//! (the `TrainerList`/`TrainerBuySpell`/`TrainerBuySucceeded`/`TrainerBuyFailed` structs). One frame
|
||||
//! (`ClassTrainerFrame`) serves both class and tradeskill trainers; the window is reached through the
|
||||
//! gossip trainer option (`GOSSIP_OPTION_TRAINER`, [`super::gossip`]), not a trainer-specific open
|
||||
//! verb — so this arc is the receive side plus the two send verbs (decision 0237).
|
||||
//! Trainer window wire (opcodes 432-436). The window opens off the gossip trainer option
|
||||
//! (`GOSSIP_OPTION_TRAINER`), so there is no open verb, only a list refresh and a buy.
|
||||
|
||||
use std::io;
|
||||
|
||||
use crate::wire::{capacity_hint, read_cstring, read_u32_le, read_u64_le, read_u8};
|
||||
|
||||
/// One trainer service (`SMSG_TRAINER_LIST`, vmangos `SendTrainerSpellHelper`,
|
||||
/// `NPCHandler.cpp:97-139`) — a spell or tradeskill step the trainer can teach. The 38-byte wire
|
||||
/// record; the list is **pre-filtered server-side** to services fitting the player's class/race, so
|
||||
/// what arrives is already the player's own menu.
|
||||
/// One 38-byte `SMSG_TRAINER_LIST` service (`NPCHandler.cpp:97-139`); the server already filters
|
||||
/// the list to the player's class and race.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TrainerSpell {
|
||||
/// The service spell id — what `CMSG_TRAINER_BUY_SPELL` names to purchase it (see
|
||||
/// [`trainer_buy_spell`]).
|
||||
/// The service spell id, which `CMSG_TRAINER_BUY_SPELL` names to buy it.
|
||||
pub spell: u32,
|
||||
/// A [`trainer_spell_state`] byte: the green/red/gray colouring the client renders (it does not
|
||||
/// recompute it). `GREEN_DISABLED` (server-internal, 10) never rides the wire — it is sent as
|
||||
/// `GREEN`.
|
||||
/// A [`trainer_spell_state`]: the client renders the colour as sent, never recomputing it.
|
||||
pub state: u8,
|
||||
/// Cost in copper, already reputation-discounted server-side.
|
||||
/// In copper, already reputation-discounted by the server.
|
||||
pub cost: u32,
|
||||
/// Wire field 4 (`primary_prof_first_rank && can_learn_primary_prof`): the client enables the
|
||||
/// "Learn" button only when this equals [`is_primary_prof_first_rank`] — so a primary-profession
|
||||
/// first rank you *can't* take (already holding two professions) arrives as `false` here with
|
||||
/// `is_primary_prof_first_rank == true`, greying the button.
|
||||
/// Sent as `first_rank && can_learn`; the client enables Learn only when it equals
|
||||
/// `is_primary_prof_first_rank`, which greys a third primary profession.
|
||||
pub can_learn_primary_prof: bool,
|
||||
/// Wire field 5 (`primary_prof_first_rank`): this service is a first-rank primary profession, so
|
||||
/// taking it spends a profession slot (drives the client's confirmation dialog).
|
||||
/// Taking it spends a profession slot, so the client asks for confirmation.
|
||||
pub is_primary_prof_first_rank: bool,
|
||||
/// Character level required to learn (`spellLevel`).
|
||||
pub req_level: u8,
|
||||
/// Required `SkillLine.dbc` id (0 = none) — e.g. the tradeskill this step belongs to.
|
||||
/// A `SkillLine.dbc` id, 0 for none.
|
||||
pub req_skill: u32,
|
||||
/// Required skill points in [`req_skill`] (0 = none).
|
||||
pub req_skill_value: u32,
|
||||
/// Up to three prerequisite spell ids (`SpellChainNode` req/prev + a trailing slot). The trailing
|
||||
/// slot is structurally always 0 on 5875; kept as an array to match the client's own
|
||||
/// `GetTrainerServiceAbilityReq(i)` model.
|
||||
/// The `SpellChainNode` req and prev spells, then a slot vmangos always sends as 0.
|
||||
pub req_spells: [u32; 3],
|
||||
}
|
||||
|
||||
/// `TrainerSpellState` (vmangos `Player.h:119-122`) — the service `state` byte. `GREEN_DISABLED` (10)
|
||||
/// is server-internal and is sent on the wire as `GREEN`, so only these three ever arrive.
|
||||
/// `TrainerSpellState` (`Player.h:119-122`); `GREEN_DISABLED` (10) is sent as `GREEN`.
|
||||
pub mod trainer_spell_state {
|
||||
/// Learnable now.
|
||||
pub const GREEN: u8 = 0;
|
||||
/// Requirements unmet (level / skill / prerequisite).
|
||||
/// Level, skill or prerequisite unmet.
|
||||
pub const RED: u8 = 1;
|
||||
/// Already known.
|
||||
pub const GRAY: u8 = 2;
|
||||
}
|
||||
|
||||
/// The `errorCode` on `SMSG_TRAINER_BUY_FAILED` (vmangos `SharedDefines.h:1120-1122`,
|
||||
/// `TrainerServiceType`-adjacent `TRAIN_FAIL_*`). A `u32` on the wire.
|
||||
/// `SMSG_TRAINER_BUY_FAILED` error codes (vmangos `SharedDefines.h:1120-1122` `TRAIN_FAIL_*`).
|
||||
pub mod train_fail {
|
||||
/// Trainer service unavailable (not a trainer of yours, out of LOS, service not in the list).
|
||||
/// Not your trainer, out of line of sight, or not a listed service.
|
||||
pub const UNAVAILABLE: u32 = 0;
|
||||
/// Not enough money.
|
||||
pub const NOT_ENOUGH_MONEY: u32 = 1;
|
||||
/// Not enough skill points.
|
||||
pub const NOT_ENOUGH_SKILL: u32 = 2;
|
||||
}
|
||||
|
||||
/// Body of `CMSG_TRAINER_LIST` (vmangos `Npc.cpp`, `TrainerList::ReadFromWorldPacket`): one full
|
||||
/// 8-byte trainer guid. Requests (and, after a purchase, re-requests to repaint green→gray) the
|
||||
/// service list — the *refresh* verb, not the open verb: the window first opens off the gossip
|
||||
/// trainer option's `SMSG_TRAINER_LIST` (decision 0237).
|
||||
/// `CMSG_TRAINER_LIST`: re-requests the list, as after a purchase to turn the service gray.
|
||||
pub fn trainer_list(trainer_guid: u64) -> Vec<u8> {
|
||||
trainer_guid.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Body of `CMSG_TRAINER_BUY_SPELL` (vmangos `Npc.cpp`, `TrainerBuySpell::ReadFromWorldPacket`):
|
||||
/// `u64 trainerGuid, u32 spellId` (the [`TrainerSpell::spell`] of the chosen service). The server
|
||||
/// answers `SMSG_TRAINER_BUY_SUCCEEDED` (+ the learned spell via `SMSG_LEARNED_SPELL`) or
|
||||
/// `SMSG_TRAINER_BUY_FAILED`.
|
||||
/// `CMSG_TRAINER_BUY_SPELL`: the server answers `SMSG_TRAINER_BUY_SUCCEEDED` plus
|
||||
/// `SMSG_LEARNED_SPELL`, or `SMSG_TRAINER_BUY_FAILED`.
|
||||
pub fn trainer_buy_spell(trainer_guid: u64, spell_id: u32) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(12);
|
||||
body.extend_from_slice(&trainer_guid.to_le_bytes());
|
||||
@@ -86,21 +60,18 @@ pub fn trainer_buy_spell(trainer_guid: u64, spell_id: u32) -> Vec<u8> {
|
||||
body
|
||||
}
|
||||
|
||||
/// Read `SMSG_TRAINER_LIST` (vmangos `WorldSession::SendTrainerList`, `NPCHandler.cpp:141-241`):
|
||||
/// `u64 trainerGuid, u32 trainerType, u32 count, count ×` [`TrainerSpell`] (38 bytes each)`, cstr
|
||||
/// title`. `trainerType` is the window-framing kind (0 class · 1 mount · 2 tradeskill · 3 pet); the
|
||||
/// title is the greeting line. Returns `(trainer, trainer_type, services, title)`.
|
||||
/// `SMSG_TRAINER_LIST` (`NPCHandler.cpp:141-241`): the type is 0 class, 1 mount, 2 tradeskill or
|
||||
/// 3 pet, and the title is the greeting line.
|
||||
pub(super) fn read_trainer_list(
|
||||
r: &mut &[u8],
|
||||
) -> io::Result<(u64, u32, Vec<TrainerSpell>, String)> {
|
||||
let trainer = read_u64_le(r)?;
|
||||
let trainer_type = read_u32_le(r)?;
|
||||
let count = read_u32_le(r)?;
|
||||
// No protocol bound: the list is every spell the trainer's template carries (vmangos
|
||||
// `NPCHandler.cpp:170` sums its two lists). 1024 is far past any real trainer.
|
||||
// No protocol bound (`NPCHandler.cpp:170` sums two lists); 1024 is past any real trainer.
|
||||
let mut services = Vec::with_capacity(capacity_hint(count, 1024));
|
||||
for _ in 0..count {
|
||||
// Struct-literal fields evaluate top-to-bottom, so this reads in wire order.
|
||||
// Struct-literal fields evaluate top to bottom, so this reads in wire order.
|
||||
services.push(TrainerSpell {
|
||||
spell: read_u32_le(r)?,
|
||||
state: read_u8(r)?,
|
||||
@@ -117,15 +88,13 @@ pub(super) fn read_trainer_list(
|
||||
Ok((trainer, trainer_type, services, title))
|
||||
}
|
||||
|
||||
/// Read `SMSG_TRAINER_BUY_SUCCEEDED` (vmangos `TrainerBuySucceeded::AppendBodyTo`): `u64
|
||||
/// trainerGuid, u32 spellId`. Confirmation + sound only — the learned spell itself lands via
|
||||
/// `SMSG_LEARNED_SPELL`, and the green→gray repaint needs a `CMSG_TRAINER_LIST` re-request.
|
||||
/// `SMSG_TRAINER_BUY_SUCCEEDED` (trainer, spell) is confirmation only: the spell arrives by
|
||||
/// `SMSG_LEARNED_SPELL` and the gray repaint needs a `CMSG_TRAINER_LIST`.
|
||||
pub(super) fn read_trainer_buy_succeeded(r: &mut &[u8]) -> io::Result<(u64, u32)> {
|
||||
Ok((read_u64_le(r)?, read_u32_le(r)?))
|
||||
}
|
||||
|
||||
/// Read `SMSG_TRAINER_BUY_FAILED` (vmangos `TrainerBuyFailed::AppendBodyTo`): `u64 trainerGuid, u32
|
||||
/// serviceId (the spell), u32 errorCode` (a [`train_fail`] code).
|
||||
/// `SMSG_TRAINER_BUY_FAILED`: trainer, spell and a [`train_fail`] code.
|
||||
pub(super) fn read_trainer_buy_failed(r: &mut &[u8]) -> io::Result<(u64, u32, u32)> {
|
||||
Ok((read_u64_le(r)?, read_u32_le(r)?, read_u32_le(r)?))
|
||||
}
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
//! The tutorial system's wire (decision 1976; wow-re `system/ui/scratch/tutorial-flags.md` §7):
|
||||
//! one inbound bank of bits and the three sends the client's tutorial code makes.
|
||||
//!
|
||||
//! The client keeps two bit banks filled from the same `SMSG_TUTORIAL_FLAGS` — the fire-once bank
|
||||
//! its C++ trigger reads and sets, and the acknowledged bank Lua's `FlagTutorial` and six C++
|
||||
//! auto-flag sites write, which is the only path that sends. The handler takes **whatever bytes
|
||||
//! are left in the packet** and sizes both banks as `bytes × 8` bits; vmangos sends 32 bytes.
|
||||
//! Tutorial wire: one inbound bank of bits and the client's three tutorial sends. The 1.12
|
||||
//! client sizes its banks from every remaining byte of the packet; vmangos sends 32 bytes.
|
||||
|
||||
use std::io::{self, Read};
|
||||
|
||||
/// `SMSG_TUTORIAL_FLAGS` (VERIFIED at the bytes, handler `0x4b5700`): the raw bank, every
|
||||
/// remaining byte of the packet — bit `id` is `bytes[id >> 3] & (1 << (id & 7))`, the same
|
||||
/// `word = id >> 5, bit = 1 << (id & 31)` addressing on little-endian dwords.
|
||||
/// `SMSG_TUTORIAL_FLAGS` (handler `0x4b5700`): bit `id` is `bytes[id >> 3] & (1 << (id & 7))`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct TutorialFlags {
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Parse it: the rest of the body, whatever its length.
|
||||
pub(super) fn read_tutorial_flags(r: &mut impl Read) -> io::Result<TutorialFlags> {
|
||||
let mut bytes = Vec::new();
|
||||
r.read_to_end(&mut bytes)?;
|
||||
Ok(TutorialFlags { bytes })
|
||||
}
|
||||
|
||||
/// Body of `CMSG_TUTORIAL_FLAG` (VERIFIED, `0x4b54c0`): one `u32`, the **0-based** id.
|
||||
/// `CMSG_TUTORIAL_FLAG` (`0x4b54c0`): the 0-based id as a `u32`.
|
||||
pub fn tutorial_flag(id: u32) -> Vec<u8> {
|
||||
id.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user