Fixes #32. Every vmangos `File.cpp:N` citation in the crates whose comments have been rewritten now reads in one revision, vmangos core `6b34a36`, the one the comments are written against, a few commits from the server benilla is run against. Some citations had been taken from a newer vmangos checkout, where the lines have moved. #32's own targets came from that newer checkout too, so where this PR and #32 differ, this PR follows `6b34a36`. - 53 citations in 31 files are re-pointed: most in benilla-protocol, a few in benilla-ui and benilla-app. Each new line was read and is the construct its comment names. - #32's cases: - The root-ack kick is `MovementHandler.cpp:715-722`; three comments cited the falling deferral after it. - Movement is discarded before `CMSG_SET_ACTIVE_MOVER` lands at `:291-293`, through `Player::GetConfirmedMover`. - The released mover's parting stop is `:955-964`. - The mount-special echo is sent with `self` false at `:972`. Whether the sender gets its own echo depends on the server's broadcaster (`Object.cpp:2273-2280`); that is stated once, where the packet is parsed, and the opcode comment points there. - Ten more citations were a line or two off in every revision. One comment also stated a wrong fact: vmangos's ignoring-you party refusal names the invitee, and the empty name is the raid-convert confirmation's. - Comment text only. Every file strips to identical code, and `cargo fmt --check` is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) 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:620-631`, `:736-737`), 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);
|
|
}
|