Files
benilla-octo/crates/benilla-protocol/tests/char_enum.rs
T
SamandClaude Fable 5 cf581aa2ac the auction house opens, the Honor tab lands, and a day of fixes
- the auction house end to end: right-clicking an auctioneer opens Browse/Bids/Auctions on the server's reply — search, the category tree from your own DBCs, the reference's 50-a-page pager, multi-key sorting (directions corrected at the bytes), bid/buyout, the sell slot with deposit and duration, cancel with its fee popup; prices sit on the reference's own money-frame kit in lined-up columns, the twenty auction messages split refusals-red / outcomes-to-chat, and a won or sold auction reads as a proper mail receipt — the window's Lua re-worded in our own terms after the overlap ratchet caught the first cut leaning on the reference's text
- the PvP honor system whole: the Honor tab and the inspect page over a real wire model, with current rank and highest-ever rank correctly two different fields
- a day of reported fixes: WASD survives a /logout re-entry, addon settings save on a window close and Cmd+Q (exactly once — a mac close could fire the tail twice), clams open again, random-enchant items name themselves on every surface, the loot window's rows hold their seats with legible counts, idle players stop sinking through building floors, the shooter re-pulls between Auto Shots, portraits loosen ~19% to the client's own aspect-1.0 matrix, and big creatures thump the camera off the CameraShakes table
- character select clicks like 1.12 — captions whiten on hover, buttons fire on release, Humans face straight ahead off the stage bone's real yaw, the facing is per-character, and hunter/warlock pets stand beside you at their family-ramp size with their own emitters (the imp's flames), the Voidwalker's sequence-hidden shackles culled
- the smaller player-facing set: nameplate on/off rows in Options with the CanAttack category gate (emissaries stop plating), the bonus bar slides up with igBonusBarOpen, the camera's framing height glides on a shapeshift, the open letter evicts the centre seat, the far-band fog hull stops printing a pale horizon stripe at low view distance, the guide arrow is gold with per-source rim art, error text fades through its shadow too, and /convertraid converts the party
- README: the auction house and PvP honor move out of "not built yet"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 19:04:27 +07:00

143 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `SMSG_CHAR_ENUM` (the character-select roster) + the logout round-trip. The enum body is
//! hand-built byte-exact to the vmangos serializer (`Player::BuildEnumData`, `Player.cpp:1629`:
//! guid u64 · name cstring · race/class/gender/skin/face/hairStyle/hairColor/facialHair u8 ·
//! level u8 · zone u32 · map u32 · x/y/z f32 · guild u32 · flags u32 · firstLogin u8 ·
//! petDisplay/petLevel/petFamily u32 · 19×(display u32 + invType u8) equipment · first-bag
//! display u32 + invType u8); two characters in one body exercise the alignment of everything
//! the parser skips.
use benilla_protocol::events::{decode, SessionEvent};
use benilla_protocol::messages;
use benilla_protocol::ServerPacket;
/// One serialized enum entry, vmangos field order. Non-roster fields get distinct junk values so a
/// misaligned parse can't accidentally pass.
#[allow(clippy::too_many_arguments)]
fn enum_entry(
guid: u64,
name: &str,
race: u8,
class: u8,
gender: u8,
level: u8,
map: u32,
pos: [f32; 3],
pet_display: u32,
) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&guid.to_le_bytes());
b.extend_from_slice(name.as_bytes());
b.push(0);
b.extend_from_slice(&[race, class, gender]);
b.extend_from_slice(&[3, 4, 5, 6, 7]); // skin/face/hairStyle/hairColor/facialHair
b.push(level);
b.extend_from_slice(&12u32.to_le_bytes()); // zone
b.extend_from_slice(&map.to_le_bytes());
for c in pos {
b.extend_from_slice(&c.to_le_bytes());
}
b.extend_from_slice(&0u32.to_le_bytes()); // guild id
b.extend_from_slice(&0x0200_0000u32.to_le_bytes()); // character flags (DECLINED — junk)
b.push(1); // first login
b.extend_from_slice(&pet_display.to_le_bytes()); // pet display id
b.extend_from_slice(&7u32.to_le_bytes()); // pet level
b.extend_from_slice(&1u32.to_le_bytes()); // pet family
for slot in 0..19u32 {
b.extend_from_slice(&(1000 + slot).to_le_bytes()); // equipment display id
b.push(slot as u8); // inventory type
}
b.extend_from_slice(&2000u32.to_le_bytes()); // first bag display id
b.push(18); // first bag inventory type
b
}
#[test]
fn char_enum_roster_parses_and_decodes() {
let mut body = vec![2u8]; // count
body.extend(enum_entry(
0x0000_0000_0000_0007,
"Two",
1, // Human
1, // Warrior
0,
1,
0,
[-8949.95, -132.493, 83.5312],
99, // a pet — the select screen stands one beside this character
));
body.extend(enum_entry(
0x0000_0000_0000_0009,
"Twomage",
1, // Human
8, // Mage
1,
60,
1,
[1.5, -2.5, 3.5],
0, // a mage: the server sends the triple zeroed, and no pet renders
));
let p = messages::parse_server(messages::opcode::SMSG_CHAR_ENUM, &body).unwrap();
let ServerPacket::CharEnum { ref characters } = p else {
panic!("char enum")
};
assert_eq!(characters.len(), 2);
let c = &characters[0];
assert_eq!(
(c.guid, c.name.as_str(), c.race, c.class, c.gender),
(7, "Two", 1, 1, 0)
);
// The appearance bytes + zone + flags land verbatim (decision 0465 — the select screen
// renders them; they used to be alignment-skips).
assert_eq!(
(c.skin, c.face, c.hair_style, c.hair_color, c.facial_hair),
(3, 4, 5, 6, 7)
);
assert_eq!((c.zone, c.flags), (12, 0x0200_0000));
// The pet triple, which used to be three alignment-skips: it sits between the first-login byte
// and the equipment array, and reading it *as* the roster's fields is what stands a pet on the
// select screen. The equipment assertions below are the alignment half of this — a triple read
// one field wide would slide every display id.
assert_eq!(
(c.pet_display_id, c.pet_level, c.pet_family),
(99, 7, 1),
"the hunter/warlock pet the select screen renders"
);
assert_eq!((c.level, c.map), (1, 0));
assert_eq!((c.position.x, c.position.y), (-8949.95, -132.493));
// All 19 equipment pairs, in slot order: display 1000+slot, invType = slot.
for (slot, item) in c.equipment.iter().enumerate() {
assert_eq!(
(item.display_id, item.inventory_type),
(1000 + slot as u32, slot as u8)
);
}
let c = &characters[1];
assert_eq!(
(c.guid, c.name.as_str(), c.race, c.class, c.gender),
(9, "Twomage", 1, 8, 1)
);
assert_eq!((c.level, c.map, c.position.z), (60, 1, 3.5));
assert_eq!(c.equipment[18].display_id, 1018);
// No pet: `petDisplayId == 0` is the whole gate — the server zeroes the triple for every
// character but a living hunter's or warlock's, so the client needs no class test of its own.
assert_eq!(c.pet_display_id, 0);
// Decodes to one CharacterList event carrying the roster through (no realm context on the raw
// decode path — the IO thread's own emit is what carries the auth realm entry).
match &decode(p)[..] {
[SessionEvent::CharacterList { characters, realm }] => {
assert_eq!(characters.len(), 2);
assert!(realm.is_none());
}
other => panic!("expected CharacterList, got {other:?}"),
}
}
#[test]
fn logout_complete_decodes_to_logged_out() {
// SMSG_LOGOUT_COMPLETE: empty body.
let p = messages::parse_server(messages::opcode::SMSG_LOGOUT_COMPLETE, &[]).unwrap();
assert!(matches!(decode(p)[..], [SessionEvent::LoggedOut]));
}