Thirteen now assert what they claimed: - every bare trade status, and the 444-byte extended snapshot for empty, full and mixed slots; - the status-1 and status-3 battlefield tails; - every quest-share verdict byte through encode, parse and decode; - the greeting pick itself, and the whole compressed-move batch. Eleven are renamed to what they check, where the claim had nothing to check against: no capture bytes, no production opcode list, no server encoder. No strengthened assertion failed. Probe and assert messages drop their record numbers. The aura probe's refusal message no longer claims the level-6 probe accounts need a grant for a level-4 command. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
335 lines
13 KiB
Rust
335 lines
13 KiB
Rust
//! The petition wire: the guild-charter flow that founds a guild, laid out per vmangos.
|
||
|
||
mod common;
|
||
|
||
use benilla_protocol::events::{decode, SessionEvent};
|
||
use benilla_protocol::messages::{
|
||
self, opcode, petition_result, CHARTER_DISPLAY_ID, CHARTER_ITEM_ENTRY, MAX_PETITION_SIGNATURES,
|
||
};
|
||
use benilla_protocol::ServerPacket;
|
||
use common::hx;
|
||
|
||
#[test]
|
||
fn cmsg_bodies_golden() {
|
||
// CMSG_PETITION_SHOWLIST (vmangos Server/Packets/Petition.cpp:3-6): one u64, the NPC.
|
||
assert_eq!(
|
||
messages::petition_show_list(0x0000_0000_0000_2a1f),
|
||
hx("1f2a000000000000"),
|
||
"CMSG_PETITION_SHOWLIST body"
|
||
);
|
||
|
||
// CMSG_PETITION_SHOW_SIGNATURES (Petition.cpp:8-11): one u64, the charter item.
|
||
assert_eq!(
|
||
messages::petition_show_signatures(0x0000_0000_0001_0203),
|
||
hx("0302010000000000"),
|
||
"CMSG_PETITION_SHOW_SIGNATURES body"
|
||
);
|
||
|
||
// CMSG_TURN_IN_PETITION (Petition.cpp:24-27): one u64, the item.
|
||
assert_eq!(
|
||
messages::turn_in_petition(0x0000_0000_0001_0203),
|
||
hx("0302010000000000"),
|
||
"CMSG_TURN_IN_PETITION body"
|
||
);
|
||
|
||
// MSG_PETITION_DECLINE outbound (Petition.cpp:19-22): one u64, the item, not the player.
|
||
assert_eq!(
|
||
messages::petition_decline(0x0000_0000_0001_0203),
|
||
hx("0302010000000000"),
|
||
"MSG_PETITION_DECLINE body"
|
||
);
|
||
|
||
// CMSG_PETITION_SIGN (Petition.cpp:35-39): u64 item, then an i8 the server skips; the
|
||
// reference defaults it to 1 (`0x4f46d9`).
|
||
assert_eq!(
|
||
messages::petition_sign(0x0000_0000_0001_0203, 1),
|
||
hx("030201000000000001"),
|
||
"CMSG_PETITION_SIGN body, the client's default byte"
|
||
);
|
||
assert_eq!(
|
||
messages::petition_sign(0x0000_0000_0001_0203, -1),
|
||
hx("0302010000000000ff"),
|
||
"…and a negative argument rides as its two's complement"
|
||
);
|
||
|
||
// CMSG_OFFER_PETITION (Petition.cpp:41-45): u64 item, u64 target player.
|
||
assert_eq!(
|
||
messages::offer_petition(0x0000_0000_0001_0203, 0x0000_0000_0000_00aa),
|
||
hx("0302010000000000aa00000000000000"),
|
||
"CMSG_OFFER_PETITION body"
|
||
);
|
||
|
||
// CMSG_PETITION_QUERY (Petition.cpp:13-17): u32 petitionId, u64 itemGuid.
|
||
assert_eq!(
|
||
messages::petition_query(7, 0x0000_0000_0001_0203),
|
||
hx("070000000302010000000000"),
|
||
"CMSG_PETITION_QUERY body"
|
||
);
|
||
|
||
// MSG_PETITION_RENAME outbound (Petition.cpp:29-33): u64 item, cstring newName.
|
||
assert_eq!(
|
||
messages::petition_rename(0x0000_0000_0001_0203, "Legacy"),
|
||
hx("03020100000000004c656761637900"),
|
||
"MSG_PETITION_RENAME body"
|
||
);
|
||
}
|
||
|
||
/// `CMSG_PETITION_BUY` is 72 fixed bytes plus the name, in vmangos's read order
|
||
/// (`Server/Packets/Petition.cpp:47-67`); a short body gets no reply at all, since the server has
|
||
/// no error packet for a malformed buy.
|
||
#[test]
|
||
fn petition_buy_body_is_the_full_seventy_two_byte_frame() {
|
||
let body = messages::petition_buy(0x0000_0000_0000_2a1f, "Legacy");
|
||
assert_eq!(
|
||
body,
|
||
hx(concat!(
|
||
"1f2a000000000000", // u64 npcGuid
|
||
"00000000", // u32 skipped
|
||
"0000000000000000", // u64 skipped
|
||
"4c656761637900", // cstring "Legacy"
|
||
// 10 × u32 skipped
|
||
"00000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||
"0000", // u16 skipped
|
||
"00", // u8 skipped
|
||
"00000000", // u32 index, unused by the server
|
||
"00000000", // u32 skipped
|
||
)),
|
||
"CMSG_PETITION_BUY body"
|
||
);
|
||
assert_eq!(body.len(), 72 + "Legacy".len(), "72 fixed bytes + the name");
|
||
|
||
// The name, the only variable part, starts at offset 20 (8 + 4 + 8).
|
||
assert_eq!(
|
||
&body[20..27],
|
||
b"Legacy\0",
|
||
"the name starts at offset 20, NUL-terminated"
|
||
);
|
||
}
|
||
|
||
/// `SMSG_PETITION_SHOWLIST` opens the guild registrar (`Server/Packets/Petition.cpp:115-127`).
|
||
/// The list is counted: vmangos sends one row, the fixture two, so a one-row reader fails.
|
||
#[test]
|
||
fn petition_show_list_parses_a_counted_list() {
|
||
let body = hx(concat!(
|
||
"1f2a000000000000", // u64 npcGuid
|
||
"02", // u8 count, not vmangos's 1
|
||
// row 1: index 1, entry 5863, display 16161, cost 1000, flags 1
|
||
"01000000",
|
||
"e7160000",
|
||
"213f0000",
|
||
"e8030000",
|
||
"01000000",
|
||
// row 2: index 2, entry 5863, display 16161, cost 2000, flags 0 (the "hidden" case)
|
||
"02000000",
|
||
"e7160000",
|
||
"213f0000",
|
||
"d0070000",
|
||
"00000000",
|
||
));
|
||
let packet = messages::parse_server(opcode::SMSG_PETITION_SHOWLIST, &body).unwrap();
|
||
let ServerPacket::PetitionShowList(list) = &packet else {
|
||
panic!("expected PetitionShowList, got {}", packet.name());
|
||
};
|
||
assert_eq!(list.npc, 0x2a1f);
|
||
assert_eq!(
|
||
list.entries.len(),
|
||
2,
|
||
"both rows parsed, not just the first"
|
||
);
|
||
assert_eq!(list.entries[0].index, 1);
|
||
assert_eq!(list.entries[0].charter_entry, CHARTER_ITEM_ENTRY);
|
||
assert_eq!(list.entries[0].charter_display_id, CHARTER_DISPLAY_ID);
|
||
assert_eq!(list.entries[0].charter_cost, 1000, "10 silver, in copper");
|
||
assert_eq!(list.entries[0].entry_flags, 1);
|
||
assert_eq!(list.entries[1].charter_cost, 2000);
|
||
assert_eq!(
|
||
list.entries[1].entry_flags, 0,
|
||
"the flag is kept raw, not folded into a bool"
|
||
);
|
||
|
||
let events = decode(packet);
|
||
assert!(
|
||
matches!(events.as_slice(), [SessionEvent::PetitionShowList(l)] if l.npc == 0x2a1f),
|
||
"one PetitionShowList event, got {events:?}"
|
||
);
|
||
}
|
||
|
||
/// `SMSG_PETITION_SHOW_SIGNATURES`: `u64` item, `u64` owner, `u32` petitionId, `u8` count, then
|
||
/// 12 bytes per signature, the signer's guid and a zero dword (vmangos
|
||
/// `Handlers/PetitionsHandler.cpp:160-168`); nine signatures make a wrong stride fail plainly.
|
||
#[test]
|
||
fn petition_show_signatures_stride_is_twelve_bytes() {
|
||
let mut body = hx(concat!(
|
||
"0302010000000000", // u64 itemGuid
|
||
"aa00000000000000", // u64 ownerGuid
|
||
"07000000", // u32 petitionId
|
||
"09", // u8 signatureCount
|
||
));
|
||
for signer in 1..=MAX_PETITION_SIGNATURES as u64 {
|
||
body.extend_from_slice(&signer.to_le_bytes());
|
||
body.extend_from_slice(&0u32.to_le_bytes());
|
||
}
|
||
|
||
let packet = messages::parse_server(opcode::SMSG_PETITION_SHOW_SIGNATURES, &body).unwrap();
|
||
let ServerPacket::PetitionShowSignatures(sigs) = &packet else {
|
||
panic!("expected PetitionShowSignatures, got {}", packet.name());
|
||
};
|
||
assert_eq!(sigs.item, 0x0001_0203);
|
||
assert_eq!(sigs.owner, 0xaa);
|
||
assert_eq!(sigs.petition_id, 7);
|
||
assert_eq!(sigs.signatures.len(), MAX_PETITION_SIGNATURES);
|
||
assert_eq!(
|
||
sigs.signatures[MAX_PETITION_SIGNATURES - 1].signer,
|
||
MAX_PETITION_SIGNATURES as u64,
|
||
"the ninth signer, at stride 12"
|
||
);
|
||
assert!(sigs.signatures.iter().all(|s| s.unknown == 0));
|
||
|
||
let events = decode(packet);
|
||
assert!(
|
||
matches!(events.as_slice(), [SessionEvent::PetitionShowSignatures(s)] if s.petition_id == 7),
|
||
"one PetitionShowSignatures event, got {events:?}"
|
||
);
|
||
}
|
||
|
||
/// `SMSG_PETITION_QUERY_RESPONSE`: sixteen fields, of which `allowedGender` alone is a `u16`; the
|
||
/// fields after it get distinct values so a 32-bit read cannot land on a lucky zero.
|
||
#[test]
|
||
fn petition_query_response_gender_is_sixteen_bit() {
|
||
let body = hx(concat!(
|
||
"07000000", // u32 petitionId
|
||
"aa00000000000000", // u64 ownerGuid
|
||
"4c656761637900", // cstring name "Legacy"
|
||
"00", // cstring bodyText ""
|
||
"01000000", // u32 flags
|
||
"09000000", // u32 minSignatures
|
||
"09000000", // u32 maxSignatures
|
||
"11111111", // u32 deadline
|
||
"22222222", // u32 creation
|
||
"33333333", // u32 allowedGuildID
|
||
"44444444", // u32 allowedClasses
|
||
"55555555", // u32 allowedRaces
|
||
"6666", // u16 allowedGender <-- sixteen bits
|
||
"77777777", // u32 allowedMinLevel
|
||
"88888888", // u32 allowedMaxLevel
|
||
"01000000", // u32 choiceCount
|
||
"59657300", // cstring "Yes"
|
||
"99999999", // u32 defaultChoice
|
||
));
|
||
let packet = messages::parse_server(opcode::SMSG_PETITION_QUERY_RESPONSE, &body).unwrap();
|
||
let ServerPacket::PetitionQueryResponse(r) = &packet else {
|
||
panic!("expected PetitionQueryResponse, got {}", packet.name());
|
||
};
|
||
assert_eq!(r.petition_id, 7);
|
||
assert_eq!(r.owner, 0xaa);
|
||
assert_eq!(r.name, "Legacy");
|
||
assert_eq!(r.body_text, "", "vmangos always sends this empty");
|
||
assert_eq!(r.min_signatures, 9);
|
||
assert_eq!(r.max_signatures, 9);
|
||
assert_eq!(r.allowed_gender, 0x6666, "sixteen bits, not thirty-two");
|
||
assert_eq!(r.allowed_min_level, 0x7777_7777);
|
||
assert_eq!(r.allowed_max_level, 0x8888_8888);
|
||
assert_eq!(r.choices, vec!["Yes".to_string()], "the counted tail");
|
||
assert_eq!(r.default_choice, 0x9999_9999);
|
||
|
||
let events = decode(packet);
|
||
assert!(
|
||
matches!(events.as_slice(), [SessionEvent::PetitionQueryResponse(q)] if q.name == "Legacy"),
|
||
"one PetitionQueryResponse event, got {events:?}"
|
||
);
|
||
}
|
||
|
||
/// `SMSG_PETITION_SIGN_RESULTS` (`Petition.cpp:69-74`) carries two guids and a code,
|
||
/// `SMSG_TURN_IN_PETITION_RESULTS` (`:76-79`) only the code, from the same `PetitionSigns` enum.
|
||
#[test]
|
||
fn the_two_results_packets_share_an_enum_but_not_a_layout() {
|
||
let body = hx(concat!(
|
||
"0302010000000000", // u64 itemGuid
|
||
"bb00000000000000", // u64 playerGuid: the signer, in both copies of this packet
|
||
"03000000", // u32 result = CANT_SIGN_OWN
|
||
));
|
||
let packet = messages::parse_server(opcode::SMSG_PETITION_SIGN_RESULTS, &body).unwrap();
|
||
let ServerPacket::PetitionSignResults(r) = &packet else {
|
||
panic!("expected PetitionSignResults, got {}", packet.name());
|
||
};
|
||
assert_eq!(r.item, 0x0001_0203);
|
||
assert_eq!(r.player, 0xbb);
|
||
assert_eq!(r.result, petition_result::CANT_SIGN_OWN);
|
||
|
||
let packet = messages::parse_server(
|
||
opcode::SMSG_TURN_IN_PETITION_RESULTS,
|
||
&hx("04000000"), // NEED_MORE
|
||
)
|
||
.unwrap();
|
||
assert!(
|
||
matches!(
|
||
packet,
|
||
ServerPacket::TurnInPetitionResults { result } if result == petition_result::NEED_MORE
|
||
),
|
||
"expected TurnInPetitionResults, got {}",
|
||
packet.name()
|
||
);
|
||
let events = decode(packet);
|
||
assert!(
|
||
matches!(
|
||
events.as_slice(),
|
||
[SessionEvent::TurnInPetitionResults { result: 4 }]
|
||
),
|
||
"one TurnInPetitionResults event, got {events:?}"
|
||
);
|
||
}
|
||
|
||
/// `MSG_PETITION_DECLINE` sends the charter's item guid but receives the decliner's player guid,
|
||
/// both eight bytes; `MSG_PETITION_RENAME` is `u64` item and cstring name both ways.
|
||
#[test]
|
||
fn msg_petition_decline_differs_by_direction_and_rename_does_not() {
|
||
assert_eq!(
|
||
messages::petition_decline(0x0001_0203),
|
||
hx("0302010000000000"),
|
||
"outbound MSG_PETITION_DECLINE carries the item guid"
|
||
);
|
||
let packet =
|
||
messages::parse_server(opcode::MSG_PETITION_DECLINE, &hx("bb00000000000000")).unwrap();
|
||
assert!(
|
||
matches!(packet, ServerPacket::PetitionDeclined { player } if player == 0xbb),
|
||
"inbound MSG_PETITION_DECLINE carries the player guid, got {}",
|
||
packet.name()
|
||
);
|
||
|
||
// The rename echo arrives only on success.
|
||
let body = hx("03020100000000004c656761637900");
|
||
assert_eq!(
|
||
messages::petition_rename(0x0001_0203, "Legacy"),
|
||
body,
|
||
"outbound MSG_PETITION_RENAME"
|
||
);
|
||
let packet = messages::parse_server(opcode::MSG_PETITION_RENAME, &body).unwrap();
|
||
let ServerPacket::PetitionRenamed(r) = &packet else {
|
||
panic!("expected PetitionRenamed, got {}", packet.name());
|
||
};
|
||
assert_eq!(r.item, 0x0001_0203);
|
||
assert_eq!(r.name, "Legacy");
|
||
|
||
let events = decode(packet);
|
||
assert!(
|
||
matches!(events.as_slice(), [SessionEvent::PetitionRenamed(p)] if p.name == "Legacy"),
|
||
"one PetitionRenamed event, got {events:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn a_freshly_bought_charter_has_no_signatures() {
|
||
let body = hx(concat!(
|
||
"0302010000000000", // item
|
||
"aa00000000000000", // owner
|
||
"07000000", // petitionId
|
||
"00", // no signatures
|
||
));
|
||
let packet = messages::parse_server(opcode::SMSG_PETITION_SHOW_SIGNATURES, &body).unwrap();
|
||
let ServerPacket::PetitionShowSignatures(sigs) = &packet else {
|
||
panic!("expected PetitionShowSignatures, got {}", packet.name());
|
||
};
|
||
assert!(sigs.signatures.is_empty());
|
||
assert_eq!(sigs.owner, 0xaa, "the owner is still named");
|
||
}
|