- 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>
132 lines
5.7 KiB
Rust
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());
|
|
}
|