steel_protocol/packets/game/c_move_entity.rs
1//! Packets for entity movement updates.
2//!
3//! These packets use fixed-point encoding for position deltas. The client maintains
4//! a `VecDeltaCodec` for each entity which tracks the "base" position. Deltas are
5//! computed as `encode(current) - encode(base)` where encode multiplies by 4096
6//! and rounds.
7//!
8//! The server must track what the client's base position is (`PositionCodec`) to
9//! compute correct deltas and know when the delta would overflow i16 bounds.
10
11use std::io::{self, Write};
12
13use steel_macros::{ClientPacket, WriteTo};
14use steel_math::DEGREE_360;
15use steel_registry::packets::play::{C_MOVE_ENTITY_POS, C_MOVE_ENTITY_POS_ROT, C_MOVE_ENTITY_ROT};
16use steel_utils::codec::VarInt;
17
18/// Fixed-point encoding multiplier (1/4096 block precision).
19const TRUNCATION_STEPS: f64 = 4096.0;
20
21/// Maximum delta value that fits in i16.
22const MAX_DELTA: i64 = i16::MAX as i64;
23
24/// Minimum delta value that fits in i16.
25const MIN_DELTA: i64 = i16::MIN as i64;
26
27/// One leg of a stepped movement delta.
28///
29/// The client replays each step in order, `ticks` apart, to interpolate sub-tick
30/// movement instead of sliding straight to the end position.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct DeltaStep {
33 /// Delta X from the previous step.
34 pub dx: PackedEntityDelta,
35 /// Delta Y from the previous step.
36 pub dy: PackedEntityDelta,
37 /// Delta Z from the previous step.
38 pub dz: PackedEntityDelta,
39 /// Ticks elapsed since the previous step.
40 pub ticks: i32,
41}
42
43/// A movement delta relative to the position the client last acknowledged.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum VecDelta {
46 /// A single delta the client moves straight along.
47 Linear {
48 /// Delta X.
49 dx: PackedEntityDelta,
50 /// Delta Y.
51 dy: PackedEntityDelta,
52 /// Delta Z.
53 dz: PackedEntityDelta,
54 },
55 /// Intermediate deltas the client replays in order.
56 Stepped(Vec<DeltaStep>),
57}
58
59impl VecDelta {
60 /// A delta that leaves the entity where the client already has it.
61 pub const ZERO: Self = Self::Linear {
62 dx: PackedEntityDelta::from_raw(0),
63 dy: PackedEntityDelta::from_raw(0),
64 dz: PackedEntityDelta::from_raw(0),
65 };
66
67 /// Returns the number of steps, which the reader needs to tell the two forms apart.
68 #[must_use]
69 pub const fn step_count(&self) -> i32 {
70 match self {
71 Self::Linear { .. } => 0,
72 Self::Stepped(steps) => steps.len() as i32,
73 }
74 }
75
76 /// Packs the on-ground flag and the step count into the `properties` field.
77 fn pack_properties(&self, on_ground: bool) -> i32 {
78 i32::from(on_ground) | (self.step_count() << 1)
79 }
80}
81
82impl steel_utils::serial::WriteTo for VecDelta {
83 fn write(&self, writer: &mut impl Write) -> io::Result<()> {
84 match self {
85 Self::Linear { dx, dy, dz } => {
86 dx.write(writer)?;
87 dy.write(writer)?;
88 dz.write(writer)
89 }
90 Self::Stepped(steps) => {
91 for step in steps {
92 VarInt(step.ticks).write(writer)?;
93 step.dx.write(writer)?;
94 step.dy.write(writer)?;
95 step.dz.write(writer)?;
96 }
97 Ok(())
98 }
99 }
100 }
101}
102
103/// Updates an entity's position with a delta from the position the client last acknowledged.
104///
105/// The wire form is `entityId`, a `properties` `VarInt` packing the on-ground flag into
106/// bit 0 and the delta's step count into the upper bits, then the delta itself.
107#[derive(ClientPacket, Clone, Debug)]
108#[packet_id(Play = C_MOVE_ENTITY_POS)]
109pub struct CMoveEntityPos {
110 /// The entity being moved.
111 pub entity_id: i32,
112 /// Movement since the client's last acknowledged position.
113 pub delta: VecDelta,
114 /// Whether the entity is on the ground.
115 pub on_ground: bool,
116}
117
118impl steel_utils::serial::WriteTo for CMoveEntityPos {
119 fn write(&self, writer: &mut impl Write) -> io::Result<()> {
120 VarInt(self.entity_id).write(writer)?;
121 VarInt(self.delta.pack_properties(self.on_ground)).write(writer)?;
122 self.delta.write(writer)
123 }
124}
125
126/// Updates an entity's position and rotation.
127///
128/// Carries the same `entityId`/`properties`/delta prefix as [`CMoveEntityPos`], followed by
129/// the rotation as angle bytes.
130#[derive(ClientPacket, Clone, Debug)]
131#[packet_id(Play = C_MOVE_ENTITY_POS_ROT)]
132pub struct CMoveEntityPosRot {
133 /// The entity being moved.
134 pub entity_id: i32,
135 /// Movement since the client's last acknowledged position.
136 pub delta: VecDelta,
137 /// Yaw as an angle byte.
138 pub y_rot: i8,
139 /// Pitch as an angle byte.
140 pub x_rot: i8,
141 /// Whether the entity is on the ground.
142 pub on_ground: bool,
143}
144
145impl steel_utils::serial::WriteTo for CMoveEntityPosRot {
146 fn write(&self, writer: &mut impl Write) -> io::Result<()> {
147 VarInt(self.entity_id).write(writer)?;
148 VarInt(self.delta.pack_properties(self.on_ground)).write(writer)?;
149 self.delta.write(writer)?;
150 self.y_rot.write(writer)?;
151 self.x_rot.write(writer)
152 }
153}
154
155/// A fixed-point entity movement delta encoded as a protocol `i16`.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub struct PackedEntityDelta(i16);
158
159impl PackedEntityDelta {
160 /// Creates a packed entity delta from its raw protocol representation.
161 #[must_use]
162 pub const fn from_raw(raw: i16) -> Self {
163 Self(raw)
164 }
165
166 /// Returns the raw protocol representation.
167 #[must_use]
168 pub const fn as_i16(self) -> i16 {
169 self.0
170 }
171
172 /// Precision lost when an absolute coordinate is quantized for relative movement
173 #[must_use]
174 pub fn encoding_precision_loss(position: f64) -> f64 {
175 encode_position(position) as f64 / TRUNCATION_STEPS - position
176 }
177
178 /// Calculates a packed movement delta between two absolute coordinates.
179 ///
180 /// Returns `None` if the delta doesn't fit in the protocol's `i16` range.
181 #[must_use]
182 pub fn between(current: f64, previous: f64) -> Option<Self> {
183 let delta = encode_position(current) - encode_position(previous);
184 if (MIN_DELTA..=MAX_DELTA).contains(&delta) {
185 Some(Self(delta as i16))
186 } else {
187 None
188 }
189 }
190}
191
192impl steel_utils::serial::WriteTo for PackedEntityDelta {
193 fn write(&self, writer: &mut impl Write) -> io::Result<()> {
194 steel_utils::serial::WriteTo::write(&self.0, writer)
195 }
196}
197
198/// Updates an entity's rotation only.
199///
200/// This packet carries no delta, so `on_ground` is a plain boolean written ahead of the
201/// angle bytes rather than a packed `properties` `VarInt`.
202#[derive(ClientPacket, WriteTo, Clone, Debug)]
203#[packet_id(Play = C_MOVE_ENTITY_ROT)]
204pub struct CMoveEntityRot {
205 #[write(as = VarInt)]
206 pub entity_id: i32,
207 pub on_ground: bool,
208 /// Yaw as angle byte
209 pub y_rot: i8,
210 /// Pitch as angle byte
211 pub x_rot: i8,
212}
213
214/// Converts degrees to a protocol angle byte (0-255 representing 0-360 degrees).
215///
216/// Mirrors vanilla's `Mth.packDegrees()`: `(byte)floor(angle * 256.0F / 360.0F)`
217#[inline]
218#[must_use]
219pub const fn to_angle_byte(degrees: f32) -> i8 {
220 // Vanilla: (byte)floor(angle * 256.0F / 360.0F)
221 // Cast to i32 first (safe for all angle values), then truncate to i8.
222 // This matches Java's (byte) cast which truncates the low 8 bits.
223 (degrees * 256.0 / DEGREE_360).floor() as i32 as i8
224}
225
226/// Encodes a position component to the protocol's fixed-point format.
227///
228/// Mirrors vanilla's `VecDeltaCodec.encode()` which uses `Math.round()`.
229/// Java's `Math.round()` rounds half towards positive infinity (half-up),
230/// which differs from Rust's `round()` that rounds half away from zero.
231#[inline]
232#[must_use]
233pub const fn encode_position(value: f64) -> i64 {
234 // Java Math.round() rounds half towards positive infinity:
235 // Math.round(0.5) = 1, Math.round(-0.5) = 0
236 // Rust round() rounds half away from zero:
237 // (0.5).round() = 1, (-0.5).round() = -1
238 // To match Java, use floor(x + 0.5) which always rounds half-up.
239 (value * TRUNCATION_STEPS + 0.5).floor() as i64
240}
241
242/// Calculates the delta for entity movement.
243///
244/// Returns `None` if the delta doesn't fit in i16 (requires full sync).
245#[inline]
246#[must_use]
247pub fn calc_delta(current: f64, previous: f64) -> Option<PackedEntityDelta> {
248 PackedEntityDelta::between(current, previous)
249}
250
251#[cfg(test)]
252mod tests {
253 use steel_utils::serial::WriteTo as _;
254
255 use super::*;
256
257 // `entityId` VarInt, `properties` VarInt (on-ground flag in bit 0, step count in the
258 // upper bits), then the delta: three big-endian shorts for a linear delta.
259 #[test]
260 fn pos_packet_matches_vanilla_wire_format() {
261 let packet = CMoveEntityPos {
262 entity_id: 5,
263 delta: VecDelta::Linear {
264 dx: PackedEntityDelta::from_raw(1),
265 dy: PackedEntityDelta::from_raw(-1),
266 dz: PackedEntityDelta::from_raw(300),
267 },
268 on_ground: true,
269 };
270 let mut buf = Vec::new();
271 packet.write(&mut buf).unwrap();
272
273 assert_eq!(
274 buf,
275 vec![
276 5, // entity_id VarInt
277 1, // properties VarInt: on_ground=1, step_count=0
278 0, 1, // dx = 1
279 0xFF, 0xFF, // dy = -1
280 1, 44, // dz = 300
281 ]
282 );
283 }
284
285 // A stepped delta carries its length in `properties`, not as its own prefix, and each
286 // step is a `ticks` VarInt followed by the three shorts.
287 #[test]
288 fn pos_packet_writes_stepped_delta_with_packed_step_count() {
289 let packet = CMoveEntityPos {
290 entity_id: 5,
291 delta: VecDelta::Stepped(vec![
292 DeltaStep {
293 dx: PackedEntityDelta::from_raw(1),
294 dy: PackedEntityDelta::from_raw(0),
295 dz: PackedEntityDelta::from_raw(0),
296 ticks: 2,
297 },
298 DeltaStep {
299 dx: PackedEntityDelta::from_raw(0),
300 dy: PackedEntityDelta::from_raw(-1),
301 dz: PackedEntityDelta::from_raw(0),
302 ticks: 3,
303 },
304 ]),
305 on_ground: true,
306 };
307 let mut buf = Vec::new();
308 packet.write(&mut buf).unwrap();
309
310 assert_eq!(
311 buf,
312 vec![
313 5, // entity_id VarInt
314 5, // properties VarInt: on_ground=1, step_count=2
315 2, // step 0 ticks
316 0, 1, 0, 0, 0, 0, // step 0 dx/dy/dz
317 3, // step 1 ticks
318 0, 0, 0xFF, 0xFF, 0, 0, // step 1 dx/dy/dz
319 ]
320 );
321 }
322
323 #[test]
324 fn pos_rot_packet_matches_vanilla_wire_format() {
325 let packet = CMoveEntityPosRot {
326 entity_id: 5,
327 delta: VecDelta::ZERO,
328 y_rot: 64,
329 x_rot: -64,
330 on_ground: false,
331 };
332 let mut buf = Vec::new();
333 packet.write(&mut buf).unwrap();
334
335 assert_eq!(
336 buf,
337 vec![
338 5, // entity_id VarInt
339 0, // properties VarInt: on_ground=0, step_count=0
340 0, 0, 0, 0, 0, 0, // dx, dy, dz = 0
341 64, // y_rot
342 192, // x_rot (-64 as u8)
343 ]
344 );
345 }
346
347 // `CMoveEntityRot` has no delta: `entityId` VarInt, `on_ground` as a plain boolean, then
348 // the angle bytes.
349 #[test]
350 fn rot_packet_matches_vanilla_wire_format() {
351 let packet = CMoveEntityRot {
352 entity_id: 5,
353 on_ground: true,
354 y_rot: 64,
355 x_rot: -64,
356 };
357 let mut buf = Vec::new();
358 packet.write(&mut buf).unwrap();
359
360 assert_eq!(buf, vec![5, 1, 64, 192]);
361 }
362
363 #[test]
364 fn test_encode_matches_java_rounding() {
365 // Java Math.round() rounds half towards positive infinity
366 assert_eq!(encode_position(0.5 / 4096.0), 1); // 0.5 -> 1
367 assert_eq!(encode_position(-0.5 / 4096.0), 0); // -0.5 -> 0 (not -1!)
368 assert_eq!(encode_position(1.5 / 4096.0), 2);
369 assert_eq!(encode_position(-1.5 / 4096.0), -1); // -1.5 -> -1 (not -2!)
370 }
371
372 #[test]
373 fn test_calc_delta() {
374 // Small movement should produce valid delta
375 let delta = calc_delta(100.001, 100.0);
376 assert!(delta.is_some());
377 assert!(delta.unwrap().as_i16().abs() < 100);
378
379 // Movement larger than i16 max (32767/4096 ≈ 8 blocks) should fail
380 let delta = calc_delta(10.0, 0.0); // 10 blocks = 40960 units > i16::MAX
381 assert!(delta.is_none());
382 }
383
384 #[test]
385 fn test_angle_byte() {
386 assert_eq!(to_angle_byte(0.0), 0);
387 assert_eq!(to_angle_byte(90.0), 64);
388 // 180 * 256 / 360 = 128, which wraps to -128 as signed byte
389 assert_eq!(to_angle_byte(180.0), -128);
390 assert_eq!(to_angle_byte(-90.0), -64);
391 assert_eq!(to_angle_byte(360.0), 0); // Full rotation wraps
392 }
393}