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}