diff --git a/crates/benilla-protocol/examples/addon_chat_probe.rs b/crates/benilla-protocol/examples/addon_chat_probe.rs index e1e9bfa6..f221ec21 100644 --- a/crates/benilla-protocol/examples/addon_chat_probe.rs +++ b/crates/benilla-protocol/examples/addon_chat_probe.rs @@ -39,7 +39,7 @@ fn probe_char_name(user: &str) -> Option { } fn login(host: &str, user: &str, pass: &str) -> Result<(WorldSession, String)> { - let name = probe_char_name(user).context("account must be a probeN (decision 0530)")?; + let name = probe_char_name(user).context("account must be a probeN")?; let logon = benilla_protocol::logon(host, user, pass)?; let addr = logon .realms diff --git a/crates/benilla-protocol/src/bin/benilla-world/probes/aura.rs b/crates/benilla-protocol/src/bin/benilla-world/probes/aura.rs index aabf612d..8d88869c 100644 --- a/crates/benilla-protocol/src/bin/benilla-world/probes/aura.rs +++ b/crates/benilla-protocol/src/bin/benilla-world/probes/aura.rs @@ -115,10 +115,8 @@ impl Probe for Aura { .context( "--aura: spell 1126 never appeared in UNIT_FIELD_AURA. Either the descriptor field \ index is wrong, or the GM `.aura` command was refused — it needs gmlevel >= 4 \ - (VERIFIED vmangos `Chat/Chat.cpp:1229`: SEC_BASIC_ADMIN, which is 4 in \ - `shared/Common.h:142`), and the probe accounts are gmlevel 6, so this \ - probe cannot run as `probeN` without a temporary grant (docs/METHOD.md, decision 0450's \ - precedent for --worldstate).", + (vmangos `Chat/Chat.cpp:1229`: SEC_BASIC_ADMIN, which is 4 in \ + `shared/Common.h:142`).", )?; let debuff = auras .iter() @@ -192,7 +190,7 @@ impl Probe for Aura { ensure!( d < v, "SMSG_UPDATE_AURA_DURATION arrived AFTER the descriptor delta (seq {d} vs {v}) — \ - decision 0255's slot-keyed buffering is built on the opposite order" + the app's slot-keyed buffering is built on the opposite order" ); println!("✅ duration packet precedes the descriptor delta (event {d} before {v})"); @@ -203,7 +201,7 @@ impl Probe for Aura { { bail!( "--aura: slot {slot} reported a duration without being (re)applied — durations are \ - not the apply/refresh edges decision 0255's client-side countdown assumes" + not the apply/refresh edges the client-side countdown assumes" ); } println!( diff --git a/crates/benilla-protocol/src/bin/benilla-world/probes/mount_tele.rs b/crates/benilla-protocol/src/bin/benilla-world/probes/mount_tele.rs index dcad224d..eab01b8d 100644 --- a/crates/benilla-protocol/src/bin/benilla-world/probes/mount_tele.rs +++ b/crates/benilla-protocol/src/bin/benilla-world/probes/mount_tele.rs @@ -162,8 +162,8 @@ impl Probe for MountTele { ensure!( (create_run - mounted).abs() < 0.01, "--mount-tele: the destination's create block should still carry the MOUNTED run \ - speed ({mounted}), got {create_run} — the strip would then precede the create and \ - B213's ordering would not be this one" + speed ({mounted}), got {create_run} — the strip would then precede the create, \ + the opposite of the order this probe expects" ); ensure!( (strip_run - BASE_RUN).abs() < 0.01, @@ -174,8 +174,7 @@ impl Probe for MountTele { "\n--mount-tele PASS: arrived on map {map}; create block carried the mount's \ {create_run} yd/s, the dismount's SMSG_FORCE_RUN_SPEED_CHANGE followed {} µs later \ at {strip_run} yd/s.\n Both packets are written by one HandleMoveWorldportAckOpcode \ - call, so a client that drains its socket once a frame sees them in ONE drain — which \ - is what B213 (decision 1478) turns on.", + call, so a client that drains its socket once a frame sees them in ONE drain.", gap.as_micros() ); Ok(()) diff --git a/crates/benilla-protocol/src/bin/benilla-world/probes/quest_item.rs b/crates/benilla-protocol/src/bin/benilla-world/probes/quest_item.rs index b41aa7eb..324c6683 100644 --- a/crates/benilla-protocol/src/bin/benilla-world/probes/quest_item.rs +++ b/crates/benilla-protocol/src/bin/benilla-world/probes/quest_item.rs @@ -158,7 +158,7 @@ impl Probe for QuestItem { ) .context( "--quest-item: the re-query got no DETAILS — this server DOES gate the query by \ - quest status, so the panel would not re-open (decision 0669 expects vmangos' \ + quest status, so the panel would not re-open (the probe expects vmangos's \ ungated handler)", )?; println!("✅ the panel legitimately re-opens: DETAILS {reopened}"); diff --git a/crates/benilla-protocol/src/bin/benilla-world/probes/questlog.rs b/crates/benilla-protocol/src/bin/benilla-world/probes/questlog.rs index 6f274848..1168fb64 100644 --- a/crates/benilla-protocol/src/bin/benilla-world/probes/questlog.rs +++ b/crates/benilla-protocol/src/bin/benilla-world/probes/questlog.rs @@ -248,17 +248,17 @@ impl Probe for QuestLog { bail!( "--questlog: slot {slot}'s PLAYER_QUEST_LOG id field never cleared to 0 within \ 8s after CMSG_QUESTLOG_REMOVE_QUEST (no ack SMSG exists on this wire — the field \ - clear IS the confirmation, decision 0109)" + clear IS the confirmation)" ); } println!( "✅ abandon: slot {slot}'s PLAYER_QUEST_LOG id field cleared to 0 (no ack SMSG on \ - this wire — the field update is the confirmation, decision 0109)." + this wire — the field update is the confirmation)." ); println!( "\n✅ --questlog PASS: template parsed, slot {slot} tracked → COMPLETE → abandoned \ - (id field cleared) — the quest-log wire verified end to end (decision 0109)." + (id field cleared) — the quest-log wire verified end to end." ); Ok(()) } diff --git a/crates/benilla-protocol/src/messages/addons.rs b/crates/benilla-protocol/src/messages/addons.rs index 7d19f77a..97e8d64f 100644 --- a/crates/benilla-protocol/src/messages/addons.rs +++ b/crates/benilla-protocol/src/messages/addons.rs @@ -98,8 +98,9 @@ pub fn addon_tail(addons: &[SecureAddon]) -> Vec { mod tests { use super::*; + /// The block has retail's 342-byte length and parses as twelve sorted, stock-signed records. #[test] - fn stock_block_matches_the_retail_capture() { + fn stock_block_has_the_retail_size_and_twelve_sorted_stock_records() { let plain = addon_block(&STOCK_SECURE_ADDONS); assert_eq!(plain.len(), 342, "retail sent 342 uncompressed bytes"); diff --git a/crates/benilla-protocol/src/messages/battlefield.rs b/crates/benilla-protocol/src/messages/battlefield.rs index aceafd77..d4270c03 100644 --- a/crates/benilla-protocol/src/messages/battlefield.rs +++ b/crates/benilla-protocol/src/messages/battlefield.rs @@ -249,6 +249,7 @@ pub fn battlefield_port(map_id: u32, accept: bool) -> Vec { mod tests { use super::*; + /// Status 2, 3 and 1 each read their own tail and no further; a zero map ends the packet. #[test] fn status_reads_the_conditional_tails() { let mut body = vec![ @@ -260,6 +261,34 @@ mod tests { (1, 30, 5, 7, 2) ); assert_eq!(s.time_ms, Some(100)); + assert_eq!((s.in_progress, s.queued), (None, None)); + + // The same slot, map, bracket and instance under status 3, then status 1. + let header = body[..13].to_vec(); + let mut b = header.clone(); + for v in [3u32, 120_000, 45_000] { + b.extend_from_slice(&v.to_le_bytes()); + } + b.push(0xEE); // a byte past the tail + let mut r = b.as_slice(); + let s = read_battlefield_status(&mut r).unwrap(); + assert_eq!(s.status, 3); + assert_eq!(s.in_progress, Some((120_000, 45_000))); + assert_eq!((s.time_ms, s.queued), (None, None)); + assert_eq!(r, [0xEE], "the status-3 tail is two u32s"); + + let mut b = header; + for v in [1u32, 30_000, 5_000] { + b.extend_from_slice(&v.to_le_bytes()); + } + b.push(0xEE); + let mut r = b.as_slice(); + let s = read_battlefield_status(&mut r).unwrap(); + assert_eq!(s.status, 1); + assert_eq!(s.queued, Some((30_000, 5_000))); + assert_eq!((s.time_ms, s.in_progress), (None, None)); + assert_eq!(r, [0xEE], "the status-1 tail is two u32s"); + body = vec![0u8, 0, 0, 0, 0, 0, 0, 0]; let s = read_battlefield_status(&mut body.as_slice()).unwrap(); assert_eq!( diff --git a/crates/benilla-protocol/src/messages/death.rs b/crates/benilla-protocol/src/messages/death.rs index 99c1295e..169edfe4 100644 --- a/crates/benilla-protocol/src/messages/death.rs +++ b/crates/benilla-protocol/src/messages/death.rs @@ -186,6 +186,7 @@ mod tests { ); } + /// A spirit healer's offer as `Spell::SendResurrectRequest` builds it: named, with sickness. #[test] fn resurrect_request_npc_caster_golden() { let name = b"Spirit Healer"; @@ -195,12 +196,19 @@ mod tests { body.extend_from_slice(name); body.push(0); body.push(1); // sickness = true - body.push(1); + body.push(1); // hasResTimer = true let mut r = body.as_slice(); let req = read_resurrect_request(&mut r).unwrap(); assert!(r.is_empty()); - assert_eq!(req.name, "Spirit Healer"); - assert!(req.sickness); + assert_eq!( + req, + ResurrectRequestBody { + caster: 0xF130_0FBE_0000_2AB3, + name: "Spirit Healer".into(), + sickness: true, + has_timer: true, + } + ); } /// `SMSG_SPIRIT_HEALER_CONFIRM` is one full guid (`SpellEffects.cpp:825-829`). @@ -231,6 +239,10 @@ mod tests { let mut expect = 0x2Au64.to_le_bytes().to_vec(); expect.push(0); assert_eq!(resurrect_response(0x2A, false), expect); + assert_eq!( + area_spirit_healer(0xF130_0033_3C00_0010), + 0xF130_0033_3C00_0010u64.to_le_bytes().to_vec() + ); } } diff --git a/crates/benilla-protocol/src/messages/items.rs b/crates/benilla-protocol/src/messages/items.rs index 7d4649d2..d7e3f057 100644 --- a/crates/benilla-protocol/src/messages/items.rs +++ b/crates/benilla-protocol/src/messages/items.rs @@ -657,6 +657,8 @@ mod tests { fn wrap_item_body_paper_first() { // 467: giftbag, giftslot, itembag, itemslot (Item.cpp:121-127). assert_eq!(wrap_item(255, 23, 255, 24), vec![255, 23, 255, 24]); + // Paper in the first bag's slot 3, the target in backpack slot 24: four distinct bytes. + assert_eq!(wrap_item(19, 3, 255, 24), vec![19, 3, 255, 24]); } #[test] diff --git a/crates/benilla-protocol/src/messages/mirror_timer.rs b/crates/benilla-protocol/src/messages/mirror_timer.rs index 68b72e40..86763dc9 100644 --- a/crates/benilla-protocol/src/messages/mirror_timer.rs +++ b/crates/benilla-protocol/src/messages/mirror_timer.rs @@ -122,6 +122,12 @@ mod tests { ); assert!(start.paused); assert_eq!(start.spell_id, 5697); + + // The draining rate, -1 on the wire, reads back negative. + body[12..16].copy_from_slice(&(-1i32).to_le_bytes()); + let start = read_start_mirror_timer(&mut &body[..]).unwrap(); + assert_eq!(start.scale, -1, "draining"); + assert_eq!(start.spell_id, 5697); } #[test] diff --git a/crates/benilla-protocol/src/messages/page_text.rs b/crates/benilla-protocol/src/messages/page_text.rs index c6094c4d..ba669811 100644 --- a/crates/benilla-protocol/src/messages/page_text.rs +++ b/crates/benilla-protocol/src/messages/page_text.rs @@ -60,8 +60,9 @@ mod tests { } } + /// A last page's zero `nextPageId` reads as 0, the value that ends the chain. #[test] - fn last_page_terminates_the_chain() { + fn the_last_page_reads_a_zero_next_page_id() { let mut body = 9u32.to_le_bytes().to_vec(); body.extend_from_slice(b"The end.\0"); body.extend_from_slice(&0u32.to_le_bytes()); diff --git a/crates/benilla-protocol/src/messages/parse.rs b/crates/benilla-protocol/src/messages/parse.rs index 407e9c50..8e589874 100644 --- a/crates/benilla-protocol/src/messages/parse.rs +++ b/crates/benilla-protocol/src/messages/parse.rs @@ -1636,8 +1636,9 @@ mod tests { assert_eq!(tail, 0); } + /// A count past the body ends in `UnexpectedEof` at the first missing row, in both opcodes. #[test] - fn a_lying_faction_count_is_a_short_read_not_an_allocation() { + fn a_lying_faction_count_is_a_short_read() { let mut body = 0xFFFF_FFFFu32.to_le_bytes().to_vec(); body.push(0x01); // flags body.extend_from_slice(&3000i32.to_le_bytes()); // standing diff --git a/crates/benilla-protocol/src/messages/quest/giver.rs b/crates/benilla-protocol/src/messages/quest/giver.rs index 823f30d9..56edfa86 100644 --- a/crates/benilla-protocol/src/messages/quest/giver.rs +++ b/crates/benilla-protocol/src/messages/quest/giver.rs @@ -517,9 +517,9 @@ mod tests { assert_eq!(d.reward_spell, 42); } + /// The pairs are read and discarded, so the check is that the fields after the run align. #[test] - fn offer_reward_emotes_are_delay_first() { - // Both emote values are discarded; this checks that the fields after them align. + fn offer_reward_skips_its_counted_emote_pairs() { let mut b = Vec::new(); push_u64(&mut b, 0x42); push_u32(&mut b, 100); diff --git a/crates/benilla-protocol/src/messages/social.rs b/crates/benilla-protocol/src/messages/social.rs index 61177b42..390eb591 100644 --- a/crates/benilla-protocol/src/messages/social.rs +++ b/crates/benilla-protocol/src/messages/social.rs @@ -475,8 +475,9 @@ mod tests { assert_eq!(who(&request), want); } + /// A bare `/who` as bytes: levels 0 to 100, both names empty, every race and class, no lists. #[test] - fn the_default_who_query_is_the_clients_unset_shape() { + fn the_default_who_query_encodes_levels_0_to_100_with_every_filter_open() { let body = who(&WhoRequest::default()); assert_eq!(&body[0..4], &0u32.to_le_bytes(), "levelMin"); assert_eq!(&body[4..8], &100u32.to_le_bytes(), "levelMax — not 255"); diff --git a/crates/benilla-protocol/src/messages/trade.rs b/crates/benilla-protocol/src/messages/trade.rs index 661c2d87..b1810f3f 100644 --- a/crates/benilla-protocol/src/messages/trade.rs +++ b/crates/benilla-protocol/src/messages/trade.rs @@ -293,12 +293,42 @@ mod tests { // ── SMSG_TRADE_STATUS parse goldens ── + /// Every code without a tail reads as its variant and leaves the byte after the code unread. #[test] fn trade_status_bare_code_has_no_tail() { - let buf = 2u32.to_le_bytes(); // OPEN_WINDOW - let mut r = &buf[..]; - assert_eq!(read_trade_status(&mut r).unwrap(), TradeStatus::OpenWindow); - assert!(r.is_empty(), "no tail should be consumed for a bare status"); + for (code, want) in [ + (0, TradeStatus::Busy), + (2, TradeStatus::OpenWindow), + (3, TradeStatus::Canceled), + (4, TradeStatus::Accept), + (5, TradeStatus::Busy2), + (6, TradeStatus::NoTarget), + (7, TradeStatus::BackToTrade), + (8, TradeStatus::Complete), + (9, TradeStatus::Rejected), + (10, TradeStatus::TargetTooFar), + (11, TradeStatus::WrongFaction), + (13, TradeStatus::Unknown13), + (14, TradeStatus::IgnoreYou), + (15, TradeStatus::YouStunned), + (16, TradeStatus::TargetStunned), + (17, TradeStatus::YouDead), + (18, TradeStatus::TargetDead), + (19, TradeStatus::YouLogout), + (20, TradeStatus::TargetLogout), + (21, TradeStatus::TrialAccount), + ] { + let mut buf = u32::to_le_bytes(code).to_vec(); + buf.push(0xEE); // the next byte, which a bare status leaves unread + let mut r = &buf[..]; + assert_eq!(read_trade_status(&mut r).unwrap(), want, "code {code}"); + assert_eq!( + r, + [0xEE], + "no tail should be consumed for bare status {code}" + ); + assert_eq!(want.code(), code); + } } #[test] @@ -394,12 +424,30 @@ mod tests { b } + /// The parser reads exactly 444 bytes, whether the slots are empty, full or mixed. #[test] fn extended_snapshot_is_always_444_bytes() { // A 17-byte header (u8 and four u32), then 7 records of u8 index and a 60-byte block. - let wire = extended_wire(0, 0, 0, [None; TRADE_SLOT_COUNT]); - assert_eq!(wire.len(), 17 + TRADE_SLOT_COUNT * 61); - assert_eq!(wire.len(), 444); + let mut mixed = [None; TRADE_SLOT_COUNT]; + mixed[0] = Some(0xABCD); + mixed[TRADE_SLOT_NONTRADED] = Some(0x1111); + for entries in [ + [None; TRADE_SLOT_COUNT], + [Some(0xABCD); TRADE_SLOT_COUNT], + mixed, + ] { + let mut wire = extended_wire(0, 0, 0, entries); + assert_eq!(wire.len(), 17 + TRADE_SLOT_COUNT * 61); + assert_eq!(wire.len(), 444); + assert!( + read_trade_status_extended(&mut &wire[..443]).is_err(), + "443 bytes is a short read" + ); + wire.push(0xEE); // a byte past the snapshot + let mut r = &wire[..]; + read_trade_status_extended(&mut r).unwrap(); + assert_eq!(r, [0xEE], "the parser stops at byte 444"); + } } #[test] diff --git a/crates/benilla-protocol/src/wire.rs b/crates/benilla-protocol/src/wire.rs index 52fe6df9..f4aef1a1 100644 --- a/crates/benilla-protocol/src/wire.rs +++ b/crates/benilla-protocol/src/wire.rs @@ -173,7 +173,7 @@ mod tests { /// Every `with_capacity` under `messages/` whose argument is a bare variable goes through /// [`capacity_hint`], carries a `.min(`, or is listed in [`EXEMPT`]. #[test] - fn wire_count_capacity_hints_are_capped() { + fn bare_variable_capacity_hints_under_messages_are_capped() { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/messages"); let mut offenders = Vec::new(); for file in rust_files(&root) { @@ -195,7 +195,7 @@ mod tests { process instead of skipping the packet. Bound each with \ `crate::wire::capacity_hint(count, CAP)` (CAP = the protocol's own bound, cited at \ the site, or a generous sane one), or add it to `EXEMPT` in `wire.rs` with the reason \ - it is not a wire count (decision 2265 §B1):\n {}", + it is not a wire count:\n {}", offenders.join("\n ") ); } diff --git a/crates/benilla-protocol/src/world/movement.rs b/crates/benilla-protocol/src/world/movement.rs index a16d4a3a..967434a9 100644 --- a/crates/benilla-protocol/src/world/movement.rs +++ b/crates/benilla-protocol/src/world/movement.rs @@ -36,12 +36,13 @@ pub(super) fn movement_info(pos: [f32; 3], orientation: f32, flags: u32) -> Move mod tests { use super::*; + /// Never 0, and a stamp taken right after is not smaller; it wraps at `u32::MAX` ms. #[test] - fn client_uptime_ms_is_nonzero_and_monotonic() { + fn client_uptime_ms_is_nonzero_and_back_to_back_calls_do_not_decrease() { let a = client_uptime_ms(); let b = client_uptime_ms(); assert!(a >= 1, "stamp is non-zero: {a}"); - assert!(b >= a, "stamp is monotonic non-decreasing: {a} -> {b}"); + assert!(b >= a, "the next stamp is not smaller: {a} -> {b}"); } #[test] diff --git a/crates/benilla-protocol/tests/client.rs b/crates/benilla-protocol/tests/client.rs index 40f44622..e49efe7f 100644 --- a/crates/benilla-protocol/tests/client.rs +++ b/crates/benilla-protocol/tests/client.rs @@ -529,6 +529,13 @@ fn name_and_creature_query_roundtrip() { "CMSG_CREATURE_QUERY body" ); + // CMSG_NAME_QUERY: the full guid (vmangos `QueryPlayerName`), built by `full_guid`. + assert_eq!( + messages::full_guid(7), + hx("0700000000000000"), + "CMSG_NAME_QUERY body" + ); + // SMSG_NAME_QUERY_RESPONSE (vmangos `NameQueryResponse::AppendBodyTo`): guid, name, an empty // realm cstring, then race, gender and class as `u32`. let body = hx("070000000000000042656e696c6c610000010000000000000001000000"); diff --git a/crates/benilla-protocol/tests/gossip_vendor.rs b/crates/benilla-protocol/tests/gossip_vendor.rs index a17c3297..397eb890 100644 --- a/crates/benilla-protocol/tests/gossip_vendor.rs +++ b/crates/benilla-protocol/tests/gossip_vendor.rs @@ -249,6 +249,16 @@ fn npc_text_update_greeting_extraction() { SessionEvent::NpcGreeting { text_id, blocks } => { assert_eq!(text_id, 321); assert_eq!(blocks.len(), messages::NPC_TEXT_BLOCKS); + + // A near-zero threshold draws the first block with text in the gender's column, the + // full sum the last; the male column skips block 6, the female column block 0. + const NEAR: f32 = 1.999_999_9; + const FAR: f32 = 1.0; + let pick = |gender, roll| messages::select_greeting(&blocks, gender, roll); + assert_eq!(pick(0, NEAR), Some("Low probability greeting")); + assert_eq!(pick(0, FAR), Some("Welcome, $N!")); + assert_eq!(pick(1, NEAR), Some("Welcome, traveler!")); + assert_eq!(pick(1, FAR), Some("Female-only greeting")); } other => panic!("npc greeting event, got {other:?}"), } diff --git a/crates/benilla-protocol/tests/group.rs b/crates/benilla-protocol/tests/group.rs index e4b2ac3e..8dbdf442 100644 --- a/crates/benilla-protocol/tests/group.rs +++ b/crates/benilla-protocol/tests/group.rs @@ -337,6 +337,12 @@ fn group_list_raid_with_assistant_flag() { fn group_list_empty_you_left_shape_is_14_bytes() { let body = vec![0u8; 14]; assert_eq!(body.len(), 14); + let (_, tail) = messages::parse_server_with_tail(opcode::SMSG_GROUP_LIST, &body).unwrap(); + assert_eq!(tail, 0, "the parser reads all 14 bytes"); + assert!( + messages::parse_server(opcode::SMSG_GROUP_LIST, &body[..13]).is_err(), + "13 bytes is a short read" + ); let p = messages::parse_server(opcode::SMSG_GROUP_LIST, &body).unwrap(); match &p { diff --git a/crates/benilla-protocol/tests/movement.rs b/crates/benilla-protocol/tests/movement.rs index 0b0d4465..2ddfbcda 100644 --- a/crates/benilla-protocol/tests/movement.rs +++ b/crates/benilla-protocol/tests/movement.rs @@ -524,14 +524,21 @@ fn compressed_moves_unwraps_to_the_same_events_as_loose_relays() { ), } - let loose = decode( - messages::parse_server( + // The batch's two records, relayed loose under their own opcodes. + let loose: Vec = [ + ( messages::opcode::MSG_MOVE_START_FORWARD, - &hx("01aa0100000004030201cdd70bc6357e04c3f90fa7420000a03f00000000"), - ) - .unwrap(), - ); - assert_eq!(format!("{:?}", events[0]), format!("{:?}", loose[0])); + "01aa0100000004030201cdd70bc6357e04c3f90fa7420000a03f00000000", + ), + ( + messages::opcode::MSG_MOVE_SET_FACING, + "01aa0000000004030201cdd70bc6357e04c3f90fa7420000c03f00000000", + ), + ] + .into_iter() + .flat_map(|(opcode, body)| decode(messages::parse_server(opcode, &hx(body)).unwrap())) + .collect(); + assert_eq!(format!("{events:?}"), format!("{loose:?}")); } /// A misframed batch is a parse error, never a silent drop of the moves it carries. diff --git a/crates/benilla-protocol/tests/petition.rs b/crates/benilla-protocol/tests/petition.rs index 44aebe48..eb6c64bc 100644 --- a/crates/benilla-protocol/tests/petition.rs +++ b/crates/benilla-protocol/tests/petition.rs @@ -282,7 +282,7 @@ fn the_two_results_packets_share_an_enum_but_not_a_layout() { /// `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_opcodes_read_a_different_body_than_they_write() { +fn msg_petition_decline_differs_by_direction_and_rename_does_not() { assert_eq!( messages::petition_decline(0x0001_0203), hx("0302010000000000"), diff --git a/crates/benilla-protocol/tests/quest_share.rs b/crates/benilla-protocol/tests/quest_share.rs index 667aa351..3007384e 100644 --- a/crates/benilla-protocol/tests/quest_share.rs +++ b/crates/benilla-protocol/tests/quest_share.rs @@ -55,19 +55,31 @@ fn push_result_decodes_member_and_verdict() { } } -/// An unmapped verdict byte reaches the app as data rather than failing the parse. +/// Every verdict byte survives encode, parse and decode; an unmapped one is data, not an error. #[test] fn every_verdict_byte_round_trips() { - for raw in [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xFF] { + for raw in 0..=u8::MAX { let mut body = 1u64.to_le_bytes().to_vec(); body.push(raw); - match messages::parse_server(opcode::MSG_QUEST_PUSH_RESULT, &body).unwrap() { + assert_eq!( + messages::quest_push_result(1, QuestShareMsg(raw)), + body, + "verdict {raw} encodes" + ); + let p = messages::parse_server(opcode::MSG_QUEST_PUSH_RESULT, &body).unwrap(); + match &p { ServerPacket::QuestPushResult(r) => { assert_eq!(r.msg, QuestShareMsg(raw), "verdict {raw}"); assert_eq!(r.member, 1); } other => panic!("verdict {raw} parsed as {}", other.name()), } + match decode(p).as_slice() { + [SessionEvent::QuestPushResult { member: 1, msg }] => { + assert_eq!(*msg, QuestShareMsg(raw), "verdict {raw} decodes"); + } + other => panic!("verdict {raw} decoded to {other:?}"), + } } } diff --git a/crates/benilla-protocol/tests/simple_packets.rs b/crates/benilla-protocol/tests/simple_packets.rs index 1a7811c5..5ac6e1af 100644 --- a/crates/benilla-protocol/tests/simple_packets.rs +++ b/crates/benilla-protocol/tests/simple_packets.rs @@ -49,7 +49,7 @@ fn simple_server_bodies_parse() { /// The response is one u32 of unix-epoch seconds (vmangos `Handlers/QueryHandler.cpp:418-423`). #[test] -fn query_time_round_trips_the_server_wall_clock() { +fn query_time_asks_empty_and_decodes_the_server_wall_clock() { assert!( messages::query_time().is_empty(), "CMSG_QUERY_TIME is a NullClientPacket" @@ -77,7 +77,8 @@ fn query_time_round_trips_the_server_wall_clock() { ); } -/// `SMSG_TRIGGER_CINEMATIC` is one u32 `CinematicSequences.dbc` id; `SMSG_CHAR_DELETE` one byte. +/// `SMSG_TRIGGER_CINEMATIC` is one u32 `CinematicSequences.dbc` id; `SMSG_CHAR_DELETE` one byte, +/// which the handshake consumes, so it decodes to no event. #[test] fn cinematic_and_char_delete_parse_and_decode() { // 41 = the dwarf intro. @@ -95,6 +96,7 @@ fn cinematic_and_char_delete_parse_and_decode() { // 0x39 = CHAR_DELETE_SUCCESS. let p = messages::parse_server(messages::opcode::SMSG_CHAR_DELETE, &[0x39]).unwrap(); assert!(matches!(p, ServerPacket::CharDelete { result: 0x39 })); + assert!(decode(p).is_empty()); } #[test] diff --git a/crates/benilla-protocol/tests/spells.rs b/crates/benilla-protocol/tests/spells.rs index 92407eda..ec00c833 100644 --- a/crates/benilla-protocol/tests/spells.rs +++ b/crates/benilla-protocol/tests/spells.rs @@ -131,7 +131,7 @@ fn spell_and_action_wire() { assert_eq!( messages::set_actionbar_toggles(0x0f), hx("0f"), - "all four — the largest value the reference binding can ever accumulate (§2)" + "all four — the largest value the reference binding can ever accumulate" ); assert_eq!( @@ -1103,9 +1103,9 @@ fn spell_chain_targets_wire() { ); } -/// Eight more combat-log bodies, each checked against the vmangos writer cited at it. +/// Eight combat-log opcodes, each parsed against the vmangos writer cited at it. #[test] -fn combat_log_completeness_wire() { +fn kill_outcome_dispel_enchantment_and_execute_logs_parse() { use benilla_protocol::messages::{ DispelFailed, EnchantmentLog, ExecuteLog, PartyKillLog, SpellDispelLog, SpellInstaKillLog, SpellLogExecute, SpellOutcomeLog, diff --git a/crates/benilla-protocol/tests/stable.rs b/crates/benilla-protocol/tests/stable.rs index e9b771e5..1d8e19eb 100644 --- a/crates/benilla-protocol/tests/stable.rs +++ b/crates/benilla-protocol/tests/stable.rs @@ -149,9 +149,9 @@ fn an_empty_list_still_carries_the_purchased_slot_count() { assert!(pets.is_empty()); } -/// `SUCCESS_UNSTABLE` answers both an unstable and a swap, so the reply never names the verb. +/// Each result byte parses as itself, and the codes are vmangos's `StableResultCode` values. #[test] -fn stable_result_codes_round_trip() { +fn stable_result_codes_parse_as_their_vmangos_bytes() { for code in [ stable_result::ERR_MONEY, stable_result::ERR_STABLE,