Skip to main content

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}