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>
766 lines
28 KiB
Rust
766 lines
28 KiB
Rust
//! The movement wire: the `[packed guid][MovementInfo]` relay of other units' moves, teleport
|
|
//! acks, speed changes, knockbacks, compressed batches and the movement-mode opcodes.
|
|
|
|
mod common;
|
|
|
|
use benilla_protocol::events::{decode, SessionEvent};
|
|
use benilla_protocol::messages::{self, MovementInfo, RelayVerb, TransportPose};
|
|
use benilla_protocol::wire::{write_packed_guid, Vector3d};
|
|
use benilla_protocol::ServerPacket;
|
|
use common::hx;
|
|
|
|
#[test]
|
|
fn movement_relay_decodes_to_unit_move() {
|
|
// Packed guid 0xAA, then the 28-byte FORWARD MovementInfo of `client_bodies_golden`; the
|
|
// server relays another player's move under the mover's own opcode.
|
|
let body = hx("01aa0100000004030201cdd70bc6357e04c3f90fa7420000a03f00000000");
|
|
match messages::parse_server(messages::opcode::MSG_MOVE_START_FORWARD, &body).unwrap() {
|
|
ServerPacket::PlayerMove {
|
|
guid,
|
|
opcode,
|
|
flags,
|
|
position,
|
|
orientation,
|
|
jump,
|
|
..
|
|
} => {
|
|
assert_eq!(guid, 0xAA);
|
|
assert_eq!(opcode, messages::opcode::MSG_MOVE_START_FORWARD);
|
|
assert_eq!(flags, 0x1);
|
|
assert_eq!(
|
|
(position.x, position.y, position.z),
|
|
(-8949.95, -132.493, 83.5312)
|
|
);
|
|
assert_eq!(orientation, 1.25);
|
|
assert_eq!(jump, None, "a non-jumping relay carries no jump tail");
|
|
}
|
|
p => panic!("expected PlayerMove, got {}", p.name()),
|
|
}
|
|
// Every relay opcode shares the body shape.
|
|
for op in [
|
|
messages::opcode::MSG_MOVE_HEARTBEAT,
|
|
messages::opcode::MSG_MOVE_SET_FACING,
|
|
messages::opcode::MSG_MOVE_STOP,
|
|
] {
|
|
let events = decode(messages::parse_server(op, &body).unwrap());
|
|
assert!(
|
|
matches!(
|
|
events.as_slice(),
|
|
[SessionEvent::UnitMove {
|
|
guid: 0xAA,
|
|
orientation,
|
|
flags: 0x1,
|
|
..
|
|
}] if *orientation == 1.25
|
|
),
|
|
"opcode {op:#06x} should decode to one UnitMove, got {events:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn jump_relay_round_trips_the_ballistic_tail() {
|
|
// JUMPING (0x2000) adds the launch tail after the `u32` fall_time (ms): zspeed, cosAngle,
|
|
// sinAngle, xyspeed, cos before sin (vmangos `MovementInfo::Read`).
|
|
let mi = MovementInfo {
|
|
flags: 0x2000 | 0x1,
|
|
timestamp: 0x0102_0304,
|
|
position: Vector3d {
|
|
x: 1.0,
|
|
y: 2.0,
|
|
z: 3.0,
|
|
},
|
|
orientation: 0.5,
|
|
transport: None, // not on a transport: no transport tail
|
|
pitch: 0.0, // not swimming: no pitch tail
|
|
fall_time: 250,
|
|
jump: Some(messages::JumpInfo {
|
|
zspeed: 7.955_547,
|
|
cos_angle: 0.25,
|
|
sin_angle: 0.75,
|
|
xy_speed: 7.0,
|
|
}),
|
|
};
|
|
let mut body = hx("01aa"); // [packed guid 0xAA][MovementInfo], the relay shape.
|
|
body.extend_from_slice(&messages::movement(&mi));
|
|
match messages::parse_server(messages::opcode::MSG_MOVE_JUMP, &body).unwrap() {
|
|
ServerPacket::PlayerMove {
|
|
flags,
|
|
fall_time,
|
|
jump,
|
|
..
|
|
} => {
|
|
assert_eq!(flags, 0x2001);
|
|
assert_eq!(fall_time, 250, "fall_time is a u32 ms count, not an f32");
|
|
let j = jump.expect("a JUMPING relay carries the ballistic tail");
|
|
assert_eq!(j.zspeed, 7.955_547);
|
|
assert_eq!(
|
|
j.cos_angle, 0.25,
|
|
"cosAngle is the 2nd jump float (before sin)"
|
|
);
|
|
assert_eq!(j.sin_angle, 0.75, "sinAngle is the 3rd jump float");
|
|
assert_eq!(j.xy_speed, 7.0);
|
|
}
|
|
p => panic!("expected PlayerMove, got {}", p.name()),
|
|
}
|
|
}
|
|
|
|
/// A rider with ON_TRANSPORT, SWIMMING and JUMPING set: the transport pose, pitch, fall_time and
|
|
/// jump tail share one packet, so a wrong transport-tail length misaligns everything after it.
|
|
#[test]
|
|
fn movement_relay_surfaces_transport_pose_and_keeps_the_rest_aligned() {
|
|
// An elevator guid (HIGH_TRANSPORT 0xF120): entry 900, low guid 4242.
|
|
let transport_guid: u64 = 4242 | (900u64 << 24) | (0xF120u64 << 48);
|
|
|
|
let mut body = Vec::new();
|
|
write_packed_guid(0xAA, &mut body).unwrap(); // mover guid
|
|
let flags: u32 = 0x0200_0000 | 0x20_0000 | 0x2000 | 0x1; // transport, swim, jump, forward
|
|
body.extend_from_slice(&flags.to_le_bytes());
|
|
body.extend_from_slice(&0x1122_3344u32.to_le_bytes()); // timestamp
|
|
body.extend_from_slice(&10.0f32.to_le_bytes()); // position.x
|
|
body.extend_from_slice(&20.0f32.to_le_bytes()); // position.y
|
|
body.extend_from_slice(&30.0f32.to_le_bytes()); // position.z
|
|
body.extend_from_slice(&0.75f32.to_le_bytes()); // orientation
|
|
body.extend_from_slice(&transport_guid.to_le_bytes()); // ON_TRANSPORT tail: full u64 guid
|
|
body.extend_from_slice(&1.0f32.to_le_bytes()); // local x
|
|
body.extend_from_slice(&2.0f32.to_le_bytes()); // local y
|
|
body.extend_from_slice(&3.0f32.to_le_bytes()); // local z
|
|
body.extend_from_slice(&0.5f32.to_le_bytes()); // local o
|
|
body.extend_from_slice(&(-0.3f32).to_le_bytes()); // SWIMMING tail: pitch
|
|
body.extend_from_slice(&999u32.to_le_bytes()); // fall_time (u32, right after pitch)
|
|
body.extend_from_slice(&7.9f32.to_le_bytes()); // JUMPING tail: zspeed
|
|
body.extend_from_slice(&0.6f32.to_le_bytes()); // cos_angle
|
|
body.extend_from_slice(&0.8f32.to_le_bytes()); // sin_angle
|
|
body.extend_from_slice(&5.0f32.to_le_bytes()); // xy_speed
|
|
|
|
let want_transport = TransportPose {
|
|
guid: transport_guid,
|
|
pos: Vector3d {
|
|
x: 1.0,
|
|
y: 2.0,
|
|
z: 3.0,
|
|
},
|
|
orientation: 0.5,
|
|
};
|
|
|
|
match messages::parse_server(messages::opcode::MSG_MOVE_HEARTBEAT, &body).unwrap() {
|
|
ServerPacket::PlayerMove {
|
|
orientation,
|
|
transport,
|
|
fall_time,
|
|
jump,
|
|
..
|
|
} => {
|
|
assert_eq!(
|
|
orientation, 0.75,
|
|
"position/orientation land before the tail, unaffected"
|
|
);
|
|
assert_eq!(
|
|
transport,
|
|
Some(want_transport),
|
|
"the ON_TRANSPORT tail surfaces as the rider's local pose"
|
|
);
|
|
assert_eq!(
|
|
fall_time, 999,
|
|
"fall_time still lands right after the transport + swim-pitch tails"
|
|
);
|
|
let j =
|
|
jump.expect("the jump tail still parses after the transport/pitch/fall_time run");
|
|
assert_eq!(j.zspeed, 7.9);
|
|
assert_eq!(j.cos_angle, 0.6);
|
|
assert_eq!(j.sin_angle, 0.8);
|
|
assert_eq!(j.xy_speed, 5.0);
|
|
}
|
|
p => panic!("expected PlayerMove, got {}", p.name()),
|
|
}
|
|
|
|
let events =
|
|
decode(messages::parse_server(messages::opcode::MSG_MOVE_HEARTBEAT, &body).unwrap());
|
|
match events.as_slice() {
|
|
[SessionEvent::UnitMove {
|
|
transport,
|
|
fall_time,
|
|
jump,
|
|
..
|
|
}] => {
|
|
assert_eq!(transport, &Some(want_transport));
|
|
assert_eq!(*fall_time, 999);
|
|
assert!(jump.is_some());
|
|
}
|
|
other => panic!("expected one UnitMove, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn teleport_ack_parses_pose() {
|
|
// MSG_MOVE_TELEPORT_ACK from the server: [packed guid][u32 counter][MovementInfo].
|
|
let body = hx("01aa070000000100000004030201cdd70bc6357e04c3f90fa7420000a03f00000000");
|
|
match messages::parse_server(messages::opcode::MSG_MOVE_TELEPORT_ACK, &body).unwrap() {
|
|
ServerPacket::Teleport {
|
|
guid,
|
|
counter,
|
|
position,
|
|
orientation,
|
|
} => {
|
|
assert_eq!(guid, 0xAA);
|
|
assert_eq!(counter, 7);
|
|
assert_eq!(
|
|
(position.x, position.y, position.z),
|
|
(-8949.95, -132.493, 83.5312)
|
|
);
|
|
assert_eq!(orientation, 1.25);
|
|
}
|
|
p => panic!("expected Teleport, got {}", p.name()),
|
|
}
|
|
}
|
|
|
|
/// `SMSG_FORCE_*_SPEED_CHANGE` is `[packed guid][u32 counter][f32 speed]` (vmangos
|
|
/// `SendSpeedChangeToController`); the ack is `[u64 guid][u32 counter][MovementInfo][f32 speed]`
|
|
/// with the guid unpacked (`MoveSpeedAck::ReadFromWorldPacket`).
|
|
#[test]
|
|
fn force_speed_change_parses_and_ack_body_golden() {
|
|
use benilla_protocol::messages::SpeedKind;
|
|
|
|
// SMSG_FORCE_RUN_SPEED_CHANGE: packed guid 8 (mask 0x01, one byte), counter 7, speed 14.0.
|
|
let body = hx("01080700000000006041");
|
|
let packet =
|
|
messages::parse_server(messages::opcode::SMSG_FORCE_RUN_SPEED_CHANGE, &body).unwrap();
|
|
match &packet {
|
|
ServerPacket::ForceSpeedChange {
|
|
guid: 8,
|
|
kind: SpeedKind::Run,
|
|
counter: 7,
|
|
speed,
|
|
} => assert_eq!(*speed, 14.0),
|
|
_ => panic!("expected ForceSpeedChange (run, guid 8, counter 7)"),
|
|
}
|
|
match decode(packet).as_slice() {
|
|
[SessionEvent::ForceSpeedChange {
|
|
guid: 8,
|
|
kind: SpeedKind::Run,
|
|
counter: 7,
|
|
speed,
|
|
}] => assert_eq!(*speed, 14.0),
|
|
other => panic!("expected one ForceSpeedChange event, got {other:?}"),
|
|
}
|
|
// Each kind has its own opcode; walk, swim-back and turn rate sit at 730-735, not 226-231.
|
|
for (op, kind) in [
|
|
(
|
|
messages::opcode::SMSG_FORCE_WALK_SPEED_CHANGE,
|
|
SpeedKind::Walk,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_FORCE_SWIM_SPEED_CHANGE,
|
|
SpeedKind::Swim,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_FORCE_TURN_RATE_CHANGE,
|
|
SpeedKind::TurnRate,
|
|
),
|
|
] {
|
|
match messages::parse_server(op, &body).unwrap() {
|
|
ServerPacket::ForceSpeedChange { kind: k, .. } => assert_eq!(k, kind),
|
|
_ => panic!("expected ForceSpeedChange for opcode {op:#06x}"),
|
|
}
|
|
}
|
|
|
|
let info = MovementInfo {
|
|
flags: 0,
|
|
timestamp: 12345,
|
|
position: Vector3d {
|
|
x: 1.0,
|
|
y: 2.0,
|
|
z: 3.0,
|
|
},
|
|
orientation: 0.5,
|
|
transport: None,
|
|
pitch: 0.0,
|
|
fall_time: 42,
|
|
jump: None,
|
|
};
|
|
assert_eq!(
|
|
messages::force_speed_ack(8, 7, &info, 14.0),
|
|
hx("08000000000000000700000000000000393000000000803f00000040000040400000003f2a00000000006041"),
|
|
"CMSG_FORCE_*_SPEED_CHANGE_ACK body"
|
|
);
|
|
assert_eq!(SpeedKind::Run.ack_opcode(), 0x00E3);
|
|
assert_eq!(SpeedKind::Walk.ack_opcode(), 0x02DB);
|
|
assert_eq!(SpeedKind::TurnRate.ack_opcode(), 0x02DF);
|
|
}
|
|
|
|
/// The knockback and its ack: `SMSG_MOVE_KNOCK_BACK` orders the launch vcos, vsin, speedXY,
|
|
/// speedZ; the ack's jump tail orders it zspeed, cos, sin, xyspeed, and vmangos rejects a
|
|
/// mismatched ack. speedZ is down-positive: the reference stores it as-is (`0x7c61f0`) where a
|
|
/// jump stores -7.955547, so -12 throws the body up.
|
|
#[test]
|
|
fn knock_back_parses_and_ack_body_golden() {
|
|
// Packed guid 8, counter 7, then vcos 0.6, vsin 0.8, speedXY 25, speedZ -12.
|
|
let body = hx("0108070000009a99193fcdcc4c3f0000c841000040c1");
|
|
let packet = messages::parse_server(messages::opcode::SMSG_MOVE_KNOCK_BACK, &body).unwrap();
|
|
let launch = match &packet {
|
|
ServerPacket::KnockBack {
|
|
guid: 8,
|
|
counter: 7,
|
|
launch,
|
|
} => *launch,
|
|
other => panic!(
|
|
"expected KnockBack (guid 8, counter 7), got {}",
|
|
other.name()
|
|
),
|
|
};
|
|
assert_eq!(launch.cos_angle, 0.6, "first float on the wire is vcos");
|
|
assert_eq!(launch.sin_angle, 0.8, "second is vsin");
|
|
assert_eq!(launch.xy_speed, 25.0, "third is the horizontal speed");
|
|
assert_eq!(
|
|
launch.zspeed, -12.0,
|
|
"fourth is speedZ, down-positive — negative throws the body up"
|
|
);
|
|
match decode(packet).as_slice() {
|
|
[SessionEvent::KnockBack {
|
|
guid: 8,
|
|
counter: 7,
|
|
launch: got,
|
|
}] => assert_eq!(*got, launch),
|
|
other => panic!("expected one KnockBack event, got {other:?}"),
|
|
}
|
|
|
|
// The ack: the full guid (packed only inbound), the counter, and a MovementInfo with
|
|
// MOVEFLAG_JUMPING (0x2000), without which no jump tail is written for the server to match.
|
|
let info = MovementInfo {
|
|
flags: 0x2000,
|
|
timestamp: 12345,
|
|
position: Vector3d {
|
|
x: 1.0,
|
|
y: 2.0,
|
|
z: 3.0,
|
|
},
|
|
orientation: 0.5,
|
|
transport: None,
|
|
pitch: 0.0,
|
|
fall_time: 0,
|
|
jump: Some(launch),
|
|
};
|
|
assert_eq!(
|
|
messages::knock_back_ack(8, 7, &info),
|
|
hx("08000000000000000700000000200000393000000000803f00000040000040400000003f00000000000040c19a99193fcdcc4c3f0000c841"),
|
|
"CMSG_MOVE_KNOCK_BACK_ACK body"
|
|
);
|
|
assert_eq!(messages::opcode::SMSG_MOVE_KNOCK_BACK, 0x00EF);
|
|
assert_eq!(messages::opcode::CMSG_MOVE_KNOCK_BACK_ACK, 0x00F0);
|
|
}
|
|
|
|
/// `MSG_MOVE_KNOCK_BACK` relays another unit's knockback: `[packed guid][MovementInfo]` and the
|
|
/// launch again. The MovementInfo is built from the victim's ack, whose jump tail already holds
|
|
/// the launch, so the appended quad goes unread.
|
|
#[test]
|
|
fn an_observed_knockback_relays_the_arc() {
|
|
// packed guid 0xAA, a JUMPING MovementInfo whose tail is (zspeed -12, cos 0.6, sin 0.8, xy 25),
|
|
// then the same quad again in the SMSG's own direction-first order.
|
|
let body = hx("01aa00200000393000000000803f00000040000040400000003f00000000000040c19a99193fcdcc4c3f0000c8419a99193fcdcc4c3f0000c841000040c1");
|
|
let packet = messages::parse_server(messages::opcode::MSG_MOVE_KNOCK_BACK, &body).unwrap();
|
|
match &packet {
|
|
ServerPacket::PlayerMove {
|
|
guid: 0xAA,
|
|
flags: 0x2000,
|
|
jump: Some(j),
|
|
..
|
|
} => {
|
|
assert_eq!(j.zspeed, -12.0, "the relayed arc rises, down-positive");
|
|
assert_eq!((j.cos_angle, j.sin_angle, j.xy_speed), (0.6, 0.8, 25.0));
|
|
}
|
|
other => panic!("expected a relayed PlayerMove, got {}", other.name()),
|
|
}
|
|
match decode(packet).as_slice() {
|
|
[SessionEvent::UnitMove { guid: 0xAA, .. }] => {}
|
|
other => panic!("expected one UnitMove event, got {other:?}"),
|
|
}
|
|
assert_eq!(messages::opcode::MSG_MOVE_KNOCK_BACK, 0x00F1);
|
|
}
|
|
|
|
/// An observed unit's speed changes, with no counter or ack: `SMSG_SPLINE_SET_*_SPEED` is
|
|
/// `[packed guid][f32 speed]` (vmangos `SendSpeedChangeToAll`), `MSG_MOVE_SET_*_SPEED` is
|
|
/// `[packed guid][MovementInfo][f32 speed]` (`SendSpeedChangeToObservers`), a pose and a speed.
|
|
#[test]
|
|
fn observer_speed_legs_parse_golden() {
|
|
use benilla_protocol::messages::SpeedKind;
|
|
|
|
// SMSG_SPLINE_SET_RUN_SPEED: packed guid 0xAA (mask 0x01, one byte), speed 14.0.
|
|
let body = hx("01aa00006041");
|
|
let packet =
|
|
messages::parse_server(messages::opcode::SMSG_SPLINE_SET_RUN_SPEED, &body).unwrap();
|
|
match &packet {
|
|
ServerPacket::SplineSpeedChange {
|
|
guid: 0xAA,
|
|
kind: SpeedKind::Run,
|
|
speed,
|
|
} => assert_eq!(*speed, 14.0),
|
|
p => panic!(
|
|
"expected SplineSpeedChange (run, guid 0xAA), got {}",
|
|
p.name()
|
|
),
|
|
}
|
|
match decode(packet).as_slice() {
|
|
[SessionEvent::SpeedChanged {
|
|
guid: 0xAA,
|
|
kind: SpeedKind::Run,
|
|
speed,
|
|
}] => assert_eq!(*speed, 14.0),
|
|
other => panic!("expected one SpeedChanged event, got {other:?}"),
|
|
}
|
|
// Walk (769) and swim-back (770) sit in a different order from the FORCE family's.
|
|
for (op, kind) in [
|
|
(
|
|
messages::opcode::SMSG_SPLINE_SET_WALK_SPEED,
|
|
SpeedKind::Walk,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED,
|
|
SpeedKind::SwimBack,
|
|
),
|
|
] {
|
|
match messages::parse_server(op, &body).unwrap() {
|
|
ServerPacket::SplineSpeedChange { kind: k, .. } => assert_eq!(k, kind),
|
|
p => panic!("expected SplineSpeedChange for {op:#06x}, got {}", p.name()),
|
|
}
|
|
}
|
|
|
|
// MSG_MOVE_SET_RUN_SPEED: packed guid 0xAA, the shared FORWARD MovementInfo, speed 14.0.
|
|
let body = hx("01aa0100000004030201cdd70bc6357e04c3f90fa7420000a03f0000000000006041");
|
|
let packet = messages::parse_server(messages::opcode::MSG_MOVE_SET_RUN_SPEED, &body).unwrap();
|
|
match &packet {
|
|
ServerPacket::MoveSetSpeed {
|
|
guid: 0xAA,
|
|
kind: SpeedKind::Run,
|
|
flags: 0x1,
|
|
orientation,
|
|
speed,
|
|
..
|
|
} => {
|
|
assert_eq!(*orientation, 1.25);
|
|
assert_eq!(*speed, 14.0);
|
|
}
|
|
p => panic!("expected MoveSetSpeed (run, guid 0xAA), got {}", p.name()),
|
|
}
|
|
match decode(packet).as_slice() {
|
|
[SessionEvent::UnitMove {
|
|
guid: 0xAA,
|
|
flags: 0x1,
|
|
position,
|
|
..
|
|
}, SessionEvent::SpeedChanged {
|
|
guid: 0xAA,
|
|
kind: SpeedKind::Run,
|
|
speed,
|
|
}] => {
|
|
assert_eq!(position, &[-8949.95, -132.493, 83.5312]);
|
|
assert_eq!(*speed, 14.0);
|
|
}
|
|
other => panic!("expected UnitMove + SpeedChanged, got {other:?}"),
|
|
}
|
|
|
|
// SMSG_MOUNTRESULT / SMSG_DISMOUNTRESULT: one u32 code (OK = 10 / 3).
|
|
match decode(
|
|
messages::parse_server(messages::opcode::SMSG_MOUNTRESULT, &hx("0a000000")).unwrap(),
|
|
)
|
|
.as_slice()
|
|
{
|
|
[SessionEvent::MountResult {
|
|
mount: true,
|
|
code: 10,
|
|
}] => {}
|
|
other => panic!("expected MountResult(mount, 10), got {other:?}"),
|
|
}
|
|
match decode(
|
|
messages::parse_server(messages::opcode::SMSG_DISMOUNTRESULT, &hx("03000000")).unwrap(),
|
|
)
|
|
.as_slice()
|
|
{
|
|
[SessionEvent::MountResult {
|
|
mount: false,
|
|
code: 3,
|
|
}] => {}
|
|
other => panic!("expected MountResult(dismount, 3), got {other:?}"),
|
|
}
|
|
|
|
// SMSG_MOUNTSPECIAL_ANIM: one unpacked `u64` guid (vmangos `HandleMountSpecialAnimOpcode`).
|
|
match decode(
|
|
messages::parse_server(
|
|
messages::opcode::SMSG_MOUNTSPECIAL_ANIM,
|
|
&hx("0400000000000040"),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.as_slice()
|
|
{
|
|
[SessionEvent::MountSpecial {
|
|
guid: 0x4000_0000_0000_0004,
|
|
}] => {}
|
|
other => panic!("expected MountSpecial(player 4), got {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// `SMSG_COMPRESSED_MOVES`, which vmangos switches a session to after 300 movement packets in ten
|
|
/// seconds: `[u32 size][zlib(records)]`, each record `[u8 size][u16 opcode][body]` with the size
|
|
/// counting the opcode (vmangos `MovementData::BuildPacket`).
|
|
#[test]
|
|
fn compressed_moves_unwraps_to_the_same_events_as_loose_relays() {
|
|
let body = hx("42000000780153d8cac0b88a91818181859989f1ec75ee63a6752c877ff22f77626058600f146650b805540062602a38005600002f2610d9");
|
|
let events =
|
|
decode(messages::parse_server(messages::opcode::SMSG_COMPRESSED_MOVES, &body).unwrap());
|
|
|
|
// Both records, in wire order: the remote-replay queue rebuilds a mover's arc from that order.
|
|
match events.as_slice() {
|
|
[SessionEvent::UnitMove {
|
|
guid: 0xAA,
|
|
flags: 0x1,
|
|
..
|
|
}, SessionEvent::UnitMove {
|
|
guid: 0xAA,
|
|
flags: 0x0,
|
|
..
|
|
}] => {}
|
|
other => panic!(
|
|
"expected the moving then the idle relay for mover 0xAA, in that order, got {other:?}"
|
|
),
|
|
}
|
|
|
|
// The batch's two records, relayed loose under their own opcodes.
|
|
let loose: Vec<SessionEvent> = [
|
|
(
|
|
messages::opcode::MSG_MOVE_START_FORWARD,
|
|
"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.
|
|
#[test]
|
|
fn a_misframed_batch_errors_rather_than_dropping_moves() {
|
|
// A HEARTBEAT record claiming a 40-byte body with only 4 bytes left in the stream.
|
|
let truncated = hx("070000007801d37ac700020006c10119");
|
|
assert!(messages::parse_server(messages::opcode::SMSG_COMPRESSED_MOVES, &truncated).is_err());
|
|
|
|
// A record whose size (1) cannot hold its opcode: misframed, and it must not underflow.
|
|
let undersized = hx("030000007801637cc7000001e200f0");
|
|
assert!(messages::parse_server(messages::opcode::SMSG_COMPRESSED_MOVES, &undersized).is_err());
|
|
}
|
|
|
|
/// The twelve `SMSG_SPLINE_MOVE_*` opcodes (reference dispatch `0x304..=0x31A`, root at `0x31A`):
|
|
/// the body is a bare packed guid with no counter, and `SET_RUN_MODE` clears
|
|
/// `MOVEFLAG_WALK_MODE`, because the reference (`0x617e80`) passes its bool to `SetRunMode`.
|
|
#[test]
|
|
fn spline_move_mode_family_parses_golden() {
|
|
use benilla_protocol::messages::SplineMode;
|
|
|
|
let body = hx("01aa");
|
|
assert_eq!(body.len(), 2, "the whole body is one packed guid");
|
|
|
|
let expected = [
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_ROOT,
|
|
SplineMode::Root,
|
|
true,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_UNROOT,
|
|
SplineMode::Root,
|
|
false,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_WATER_WALK,
|
|
SplineMode::WaterWalk,
|
|
true,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_LAND_WALK,
|
|
SplineMode::WaterWalk,
|
|
false,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_FEATHER_FALL,
|
|
SplineMode::FeatherFall,
|
|
true,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_NORMAL_FALL,
|
|
SplineMode::FeatherFall,
|
|
false,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_SET_HOVER,
|
|
SplineMode::Hover,
|
|
true,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_UNSET_HOVER,
|
|
SplineMode::Hover,
|
|
false,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_START_SWIM,
|
|
SplineMode::Swimming,
|
|
true,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_STOP_SWIM,
|
|
SplineMode::Swimming,
|
|
false,
|
|
),
|
|
// The inversion: the WALK opcode sets the bit, the RUN opcode clears it.
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE,
|
|
SplineMode::WalkMode,
|
|
true,
|
|
),
|
|
(
|
|
messages::opcode::SMSG_SPLINE_MOVE_SET_RUN_MODE,
|
|
SplineMode::WalkMode,
|
|
false,
|
|
),
|
|
];
|
|
|
|
for (op, mode, apply) in expected {
|
|
let packet = messages::parse_server(op, &body)
|
|
.unwrap_or_else(|e| panic!("{op:#06x} must parse from a bare packed guid: {e}"));
|
|
match &packet {
|
|
ServerPacket::SplineMoveMode {
|
|
guid: 0xAA,
|
|
mode: m,
|
|
apply: a,
|
|
} => assert_eq!(
|
|
(*m, *a),
|
|
(mode, apply),
|
|
"{} decoded to the wrong mode/direction",
|
|
packet.name()
|
|
),
|
|
p => panic!("expected SplineMoveMode for {op:#06x}, got {}", p.name()),
|
|
}
|
|
match decode(packet).as_slice() {
|
|
[SessionEvent::SplineMoveMode {
|
|
guid: 0xAA,
|
|
mode: m,
|
|
apply: a,
|
|
}] => assert_eq!((*m, *a), (mode, apply)),
|
|
other => panic!("expected one SplineMoveMode event for {op:#06x}, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
assert_eq!(SplineMode::Root.flag(), messages::MoveMode::Root.flag());
|
|
assert_eq!(
|
|
SplineMode::WaterWalk.flag(),
|
|
messages::MoveMode::WaterWalk.flag()
|
|
);
|
|
assert_eq!(
|
|
SplineMode::FeatherFall.flag(),
|
|
messages::MoveMode::FeatherFall.flag()
|
|
);
|
|
assert_eq!(SplineMode::Hover.flag(), messages::MoveMode::Hover.flag());
|
|
}
|
|
|
|
/// The movement-mode opcodes relayed to observers (root, hover, feather fall, water walk,
|
|
/// teleport) use the plain relay shape, no counter. Only root has a separate off opcode: vmangos
|
|
/// updates the others' flag before broadcasting (`MovementHandler.cpp:626-638`, `:743-744`), so
|
|
/// the flags word carries the state. Only `MSG_MOVE_TELEPORT` is tagged as a teleport.
|
|
#[test]
|
|
fn observer_move_mode_family_parses_golden() {
|
|
// Packed guid 0xAA and a 28-byte MovementInfo with no tails: time 12345, pos (1, 2, 3),
|
|
// orientation 0.5, fall_time 0; 30 bytes, four fewer than `MSG_MOVE_TELEPORT_ACK`'s.
|
|
let info = |flags: u32| {
|
|
let mut b = hx("01aa");
|
|
b.extend_from_slice(&flags.to_le_bytes());
|
|
b.extend_from_slice(&hx("393000000000803f00000040000040400000003f00000000"));
|
|
b
|
|
};
|
|
|
|
const ROOT: u32 = 0x0000_1000;
|
|
const WATER_WALK: u32 = 0x1000_0000;
|
|
const FEATHER_FALL: u32 = 0x2000_0000;
|
|
const HOVER: u32 = 0x4000_0000;
|
|
|
|
// (opcode, the flags word the server writes into it, the opcode's verb)
|
|
let expected = [
|
|
(messages::opcode::MSG_MOVE_ROOT, ROOT, RelayVerb::Root(true)),
|
|
(messages::opcode::MSG_MOVE_UNROOT, 0, RelayVerb::Root(false)),
|
|
(
|
|
messages::opcode::MSG_MOVE_WATER_WALK,
|
|
WATER_WALK,
|
|
RelayVerb::Pose,
|
|
),
|
|
// The same opcode, the other direction: the bit is absent and the verb is still `Pose`.
|
|
(messages::opcode::MSG_MOVE_WATER_WALK, 0, RelayVerb::Pose),
|
|
(
|
|
messages::opcode::MSG_MOVE_FEATHER_FALL,
|
|
FEATHER_FALL,
|
|
RelayVerb::Pose,
|
|
),
|
|
(messages::opcode::MSG_MOVE_HOVER, HOVER, RelayVerb::Pose),
|
|
// Levitate grants all three at once: one word, three bits.
|
|
(
|
|
messages::opcode::MSG_MOVE_HOVER,
|
|
HOVER | FEATHER_FALL | WATER_WALK,
|
|
RelayVerb::Pose,
|
|
),
|
|
(messages::opcode::MSG_MOVE_TELEPORT, 0, RelayVerb::Teleport),
|
|
];
|
|
|
|
for (op, flags, expected_verb) in expected {
|
|
let body = info(flags);
|
|
assert_eq!(body.len(), 30, "the relay shape, with no counter dword");
|
|
let packet = messages::parse_server(op, &body)
|
|
.unwrap_or_else(|e| panic!("{op:#06x} must parse as an ordinary relay: {e}"));
|
|
match &packet {
|
|
ServerPacket::PlayerMove {
|
|
guid: 0xAA,
|
|
opcode,
|
|
flags: f,
|
|
position,
|
|
..
|
|
} => {
|
|
assert_eq!(*opcode, op);
|
|
assert_eq!(*f, flags, "the whole flags word survives for {op:#06x}");
|
|
assert_eq!(
|
|
(position.x, position.y, position.z),
|
|
(1.0, 2.0, 3.0),
|
|
"the pose starts right after the guid for {op:#06x}"
|
|
);
|
|
}
|
|
other => panic!(
|
|
"expected a relayed PlayerMove for {op:#06x}, got {}",
|
|
other.name()
|
|
),
|
|
}
|
|
match decode(packet).as_slice() {
|
|
[SessionEvent::UnitMove {
|
|
guid: 0xAA,
|
|
flags: f,
|
|
verb,
|
|
..
|
|
}] => {
|
|
assert_eq!(*f, flags);
|
|
assert_eq!(
|
|
*verb, expected_verb,
|
|
"the opcode's verb, and only for the three that have one ({op:#06x})"
|
|
);
|
|
}
|
|
other => panic!("expected one UnitMove event for {op:#06x}, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
// Opcode numbers (vmangos `Opcodes_1_12_1.h`).
|
|
assert_eq!(messages::opcode::MSG_MOVE_TELEPORT, 197);
|
|
assert_eq!(messages::opcode::MSG_MOVE_TELEPORT_ACK, 199);
|
|
assert_eq!(messages::opcode::MSG_MOVE_ROOT, 236);
|
|
assert_eq!(messages::opcode::MSG_MOVE_UNROOT, 237);
|
|
assert_eq!(messages::opcode::MSG_MOVE_HOVER, 247);
|
|
assert_eq!(messages::opcode::MSG_MOVE_FEATHER_FALL, 688);
|
|
assert_eq!(messages::opcode::MSG_MOVE_WATER_WALK, 689);
|
|
}
|