Files
benilla-octo/crates/benilla-protocol/tests/pvp.rs
T
SamandClaude Fable 5 6aa78bc468 the shapeshift cloud both ways, and the Honor tab
- druids get the shift cloud back in both directions, by the reference's own mechanism: every shapeshift aura add *or* remove arms a one-slot pending-morph latch that the model-swap rebuild drains and replays — which is why shifting out (a client-side cancel the server sends no visual for) puffs at all, and why a GM morph or a revive correctly shows nothing; the first fix that merely let one-shot effects survive the swap's teardown produced the right pixels for the wrong reason and is superseded
- the honor system lands whole: the Honor tab on the character window, the honor page on inspect, and thirteen missing Lua bindings over a real wire model — session/yesterday/this-week/last-week kills, lifetime HKs and DKs, rank progress and badge art
- the load-bearing find: your current rank and your highest-ever rank are two separately-streamed fields, and the one we already had was the wrong one for anyone who ever de-ranked; a binary cross-check corrected five display details after the build, down to the rank title never being gendered

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

132 lines
5.7 KiB
Rust

//! The honor-system wire route end to end (decision 1512), mirroring `src/messages/pvp.rs`: raw
//! body → [`messages::parse_server`] → `ServerPacket` → [`decode`] → `SessionEvent`. The
//! byte-exact field goldens live inline in `pvp.rs`; what this file adds is the part those cannot
//! reach — that the opcode actually dispatches to the right reader, that the packet names itself
//! correctly in the log, and that the event fan-out carries the payload through unchanged. See
//! `tests/common` for the shared fixtures and methodology note.
mod common;
use benilla_protocol::events::{decode, SessionEvent};
use benilla_protocol::messages;
use benilla_protocol::ServerPacket;
use common::hx;
/// The 50-byte `MSG_INSPECT_HONOR_STATS` reply, in `InspectHonorStatsResponse::AppendBodyTo`
/// order: guid `0x0000000100002AB3`, highestRank 11, sessionKills 17|2<<16, yesterdayHK 41,
/// unknownOld1 0, lastWeekHK 420, unknownOld2 0, thisWeekHK 123, unknownOld3 0, lifetimeHK 3907,
/// lifetimeDHK 12, yesterdayHonor 640, lastWeekHonor 8431, thisWeekHonor 1250, lastWeekRank 57,
/// rankBar 191.
fn inspect_honor_body() -> Vec<u8> {
hx(concat!(
"b32a000001000000", // u64 playerGuid
"0b", // u8 highestRank
"11000200", // u32 sessionKills (17 HK | 2 DK)
"2900", // u16 yesterdayHK
"0000", // u16 unknownOld1
"a401", // u16 lastWeekHK
"0000", // u16 unknownOld2
"7b00", // u16 thisWeekHK
"0000", // u16 unknownOld3
"430f0000", // u32 lifetimeHK
"0c000000", // u32 lifetimeDHK
"80020000", // u32 yesterdayHonor
"ef200000", // u32 lastWeekHonor
"e2040000", // u32 thisWeekHonor
"39000000", // u32 lastWeekRank (the standing)
"bf", // u8 rankBar
))
}
/// The reply dispatches off opcode 0x2D6 and reaches the UI as one event with every field intact.
/// The opcode is an `MSG_` — the same number carries our request — but `parse_server` only ever
/// sees inbound bodies, so there is nothing to disambiguate here.
#[test]
fn inspect_honor_stats_wire() {
let body = inspect_honor_body();
assert_eq!(body.len(), 50, "the 1.12.1 reply body is exactly 50 bytes");
let packet = messages::parse_server(messages::opcode::MSG_INSPECT_HONOR_STATS, &body).unwrap();
assert_eq!(packet.name(), "MSG_INSPECT_HONOR_STATS");
let ServerPacket::InspectHonorStats(stats) = packet else {
panic!("inspect honor stats");
};
assert_eq!(stats.player_guid, 0x0000_0001_0000_2AB3);
assert_eq!(stats.highest_rank, 11);
assert_eq!(stats.session_kills(), (17, 2));
assert_eq!(
(stats.yesterday_hk, stats.last_week_hk, stats.this_week_hk),
(41, 420, 123)
);
assert_eq!(
(stats.unknown_old1, stats.unknown_old2, stats.unknown_old3),
(0, 0, 0),
"vmangos writes all three deprecated slots as 0"
);
assert_eq!((stats.lifetime_hk, stats.lifetime_dhk), (3_907, 12));
assert_eq!(
(
stats.yesterday_honor,
stats.last_week_honor,
stats.this_week_honor
),
(640, 8_431, 1_250)
);
assert_eq!(stats.last_week_rank, 57, "the STANDING, not a rank");
assert_eq!(stats.rank_bar, 191);
match decode(ServerPacket::InspectHonorStats(stats)).as_slice() {
[SessionEvent::InspectHonorStats(ev)] => assert_eq!(*ev, stats),
other => panic!("one inspect-honor event, got {other:?}"),
}
// Our own request on the same opcode is the bare guid — a different shape entirely, which is
// exactly why direction and not content has to tell the two apart.
assert_eq!(
messages::inspect_honor_stats(0x0000_0001_0000_2AB3),
hx("b32a000001000000")
);
}
/// `SMSG_PVP_CREDIT` dispatches off 0x28C and carries honor / victim / rank through to the event.
/// The victim rank is the **internal** rank (`SendPVPCredit` sends `GetRank().rank`, floored at 5
/// for a player victim) — a direct `PVP_RANK_<rank>_<team>` key, not the visual badge number.
#[test]
fn pvp_credit_wire() {
// i32 honor = 143, u64 victimGuid = 0x0000000100002AB3, i32 victimRank = 11.
let body = hx("8f000000b32a0000010000000b000000");
assert_eq!(body.len(), 16);
let packet = messages::parse_server(messages::opcode::SMSG_PVP_CREDIT, &body).unwrap();
assert_eq!(packet.name(), "SMSG_PVP_CREDIT");
let ServerPacket::PvpCredit(credit) = packet else {
panic!("pvp credit");
};
assert_eq!(credit.honor, 143);
assert_eq!(credit.victim_guid, 0x0000_0001_0000_2AB3);
assert_eq!(credit.victim_rank, 11);
match decode(ServerPacket::PvpCredit(credit)).as_slice() {
[SessionEvent::PvpCredit(ev)] => assert_eq!(*ev, credit),
other => panic!("one pvp-credit event, got {other:?}"),
}
// A dishonorable kill rides the same packet with negative honor (`HonorMgr.cpp:807`).
let dk = hx("fbffffffb32a0000010000000b000000");
let ServerPacket::PvpCredit(credit) =
messages::parse_server(messages::opcode::SMSG_PVP_CREDIT, &dk).unwrap()
else {
panic!("pvp credit");
};
assert_eq!(credit.honor, -5);
}
/// A truncated body must surface as a parse error, not a panic — both opcodes, at the wire edge
/// rather than only at the reader.
#[test]
fn truncated_honor_bodies_error_at_the_dispatch() {
let full = inspect_honor_body();
assert!(
messages::parse_server(messages::opcode::MSG_INSPECT_HONOR_STATS, &full[..49]).is_err()
);
assert!(messages::parse_server(messages::opcode::SMSG_PVP_CREDIT, &[0u8; 15]).is_err());
}