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_registry::packets::play::{C_MOVE_ENTITY_POS, C_MOVE_ENTITY_POS_ROT, C_MOVE_ENTITY_ROT};
15
16/// Fixed-point encoding multiplier (1/4096 block precision).
17const TRUNCATION_STEPS: f64 = 4096.0;
18
19/// Maximum delta value that fits in i16.
20const MAX_DELTA: i64 = i16::MAX as i64;
21
22/// Minimum delta value that fits in i16.
23const MIN_DELTA: i64 = i16::MIN as i64;
24
25/// Updates an entity's position with a delta from its current position.
26#[derive(ClientPacket, WriteTo, Clone, Debug)]
27#[packet_id(Play = C_MOVE_ENTITY_POS)]
28pub struct CMoveEntityPos {
29    #[write(as = VarInt)]
30    pub entity_id: i32,
31    /// Delta X (current X * 4096 - previous X * 4096)
32    pub dx: PackedEntityDelta,
33    /// Delta Y
34    pub dy: PackedEntityDelta,
35    /// Delta Z
36    pub dz: PackedEntityDelta,
37    pub on_ground: bool,
38}
39
40/// Updates an entity's position and rotation.
41#[derive(ClientPacket, WriteTo, Clone, Debug)]
42#[packet_id(Play = C_MOVE_ENTITY_POS_ROT)]
43pub struct CMoveEntityPosRot {
44    #[write(as = VarInt)]
45    pub entity_id: i32,
46    /// Delta X (current X * 4096 - previous X * 4096)
47    pub dx: PackedEntityDelta,
48    /// Delta Y
49    pub dy: PackedEntityDelta,
50    /// Delta Z
51    pub dz: PackedEntityDelta,
52    /// Yaw as angle byte
53    pub y_rot: i8,
54    /// Pitch as angle byte
55    pub x_rot: i8,
56    pub on_ground: bool,
57}
58
59/// A fixed-point entity movement delta encoded as a protocol `i16`.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct PackedEntityDelta(i16);
62
63impl PackedEntityDelta {
64    /// Creates a packed entity delta from its raw protocol representation.
65    #[must_use]
66    pub const fn from_raw(raw: i16) -> Self {
67        Self(raw)
68    }
69
70    /// Returns the raw protocol representation.
71    #[must_use]
72    pub const fn as_i16(self) -> i16 {
73        self.0
74    }
75
76    /// Calculates a packed movement delta between two absolute coordinates.
77    ///
78    /// Returns `None` if the delta doesn't fit in the protocol's `i16` range.
79    #[must_use]
80    pub fn between(current: f64, previous: f64) -> Option<Self> {
81        let delta = encode_position(current) - encode_position(previous);
82        if (MIN_DELTA..=MAX_DELTA).contains(&delta) {
83            Some(Self(delta as i16))
84        } else {
85            None
86        }
87    }
88}
89
90impl steel_utils::serial::WriteTo for PackedEntityDelta {
91    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
92        steel_utils::serial::WriteTo::write(&self.0, writer)
93    }
94}
95
96/// Updates an entity's rotation only.
97#[derive(ClientPacket, WriteTo, Clone, Debug)]
98#[packet_id(Play = C_MOVE_ENTITY_ROT)]
99pub struct CMoveEntityRot {
100    #[write(as = VarInt)]
101    pub entity_id: i32,
102    /// Yaw as angle byte
103    pub y_rot: i8,
104    /// Pitch as angle byte
105    pub x_rot: i8,
106    pub on_ground: bool,
107}
108
109/// Converts degrees to a protocol angle byte (0-255 representing 0-360 degrees).
110///
111/// Mirrors vanilla's `Mth.packDegrees()`: `(byte)floor(angle * 256.0F / 360.0F)`
112#[inline]
113#[must_use]
114pub const fn to_angle_byte(degrees: f32) -> i8 {
115    // Vanilla: (byte)floor(angle * 256.0F / 360.0F)
116    // Cast to i32 first (safe for all angle values), then truncate to i8.
117    // This matches Java's (byte) cast which truncates the low 8 bits.
118    (degrees * 256.0 / 360.0).floor() as i32 as i8
119}
120
121/// Encodes a position component to the protocol's fixed-point format.
122///
123/// Mirrors vanilla's `VecDeltaCodec.encode()` which uses `Math.round()`.
124/// Java's `Math.round()` rounds half towards positive infinity (half-up),
125/// which differs from Rust's `round()` that rounds half away from zero.
126#[inline]
127#[must_use]
128pub const fn encode_position(value: f64) -> i64 {
129    // Java Math.round() rounds half towards positive infinity:
130    //   Math.round(0.5) = 1, Math.round(-0.5) = 0
131    // Rust round() rounds half away from zero:
132    //   (0.5).round() = 1, (-0.5).round() = -1
133    // To match Java, use floor(x + 0.5) which always rounds half-up.
134    (value * TRUNCATION_STEPS + 0.5).floor() as i64
135}
136
137/// Calculates the delta for entity movement.
138///
139/// Returns `None` if the delta doesn't fit in i16 (requires full sync).
140#[inline]
141#[must_use]
142pub fn calc_delta(current: f64, previous: f64) -> Option<PackedEntityDelta> {
143    PackedEntityDelta::between(current, previous)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_encode_matches_java_rounding() {
152        // Java Math.round() rounds half towards positive infinity
153        assert_eq!(encode_position(0.5 / 4096.0), 1); // 0.5 -> 1
154        assert_eq!(encode_position(-0.5 / 4096.0), 0); // -0.5 -> 0 (not -1!)
155        assert_eq!(encode_position(1.5 / 4096.0), 2);
156        assert_eq!(encode_position(-1.5 / 4096.0), -1); // -1.5 -> -1 (not -2!)
157    }
158
159    #[test]
160    fn test_calc_delta() {
161        // Small movement should produce valid delta
162        let delta = calc_delta(100.001, 100.0);
163        assert!(delta.is_some());
164        assert!(delta.unwrap().as_i16().abs() < 100);
165
166        // Movement larger than i16 max (32767/4096 ≈ 8 blocks) should fail
167        let delta = calc_delta(10.0, 0.0); // 10 blocks = 40960 units > i16::MAX
168        assert!(delta.is_none());
169    }
170
171    #[test]
172    fn test_angle_byte() {
173        assert_eq!(to_angle_byte(0.0), 0);
174        assert_eq!(to_angle_byte(90.0), 64);
175        // 180 * 256 / 360 = 128, which wraps to -128 as signed byte
176        assert_eq!(to_angle_byte(180.0), -128);
177        assert_eq!(to_angle_byte(-90.0), -64);
178        assert_eq!(to_angle_byte(360.0), 0); // Full rotation wraps
179    }
180}