Files
benilla-octo/crates/benilla-protocol/src/messages/trainer.rs
T
SamandClaude Opus 5.5 2a66832215 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>
2026-09-23 20:02:35 +07:00

101 lines
4.1 KiB
Rust

//! 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 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, which `CMSG_TRAINER_BUY_SPELL` names to buy it.
pub spell: u32,
/// A [`trainer_spell_state`]: the client renders the colour as sent, never recomputing it.
pub state: u8,
/// In copper, already reputation-discounted by the server.
pub cost: u32,
/// 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,
/// Taking it spends a profession slot, so the client asks for confirmation.
pub is_primary_prof_first_rank: bool,
pub req_level: u8,
/// A `SkillLine.dbc` id, 0 for none.
pub req_skill: u32,
pub req_skill_value: u32,
/// The `SpellChainNode` req and prev spells, then a slot vmangos always sends as 0.
pub req_spells: [u32; 3],
}
/// `TrainerSpellState` (`Player.h:119-122`); `GREEN_DISABLED` (10) is sent as `GREEN`.
pub mod trainer_spell_state {
/// Learnable now.
pub const GREEN: u8 = 0;
/// Level, skill or prerequisite unmet.
pub const RED: u8 = 1;
/// Already known.
pub const GRAY: u8 = 2;
}
/// `SMSG_TRAINER_BUY_FAILED` error codes (vmangos `SharedDefines.h:1120-1122` `TRAIN_FAIL_*`).
pub mod train_fail {
/// Not your trainer, out of line of sight, or not a listed service.
pub const UNAVAILABLE: u32 = 0;
pub const NOT_ENOUGH_MONEY: u32 = 1;
pub const NOT_ENOUGH_SKILL: u32 = 2;
}
/// `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()
}
/// `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());
body.extend_from_slice(&spell_id.to_le_bytes());
body
}
/// `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 (`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.
services.push(TrainerSpell {
spell: read_u32_le(r)?,
state: read_u8(r)?,
cost: read_u32_le(r)?,
can_learn_primary_prof: read_u32_le(r)? != 0,
is_primary_prof_first_rank: read_u32_le(r)? != 0,
req_level: read_u8(r)?,
req_skill: read_u32_le(r)?,
req_skill_value: read_u32_le(r)?,
req_spells: [read_u32_le(r)?, read_u32_le(r)?, read_u32_le(r)?],
});
}
let title = read_cstring(r)?;
Ok((trainer, trainer_type, services, title))
}
/// `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)?))
}
/// `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)?))
}