Skip to main content

ghast_client/
movement.rs

1//! The per-tick movement packet (`ClientPlayerEntity.sendMovementPackets`, 1.8 and 1.16) and the stand-in mover used until `ghast-physics` is
2//! integrated.
3
4use crate::common::{Controls, EntityAction, MovePacket, Pose, Vec3};
5
6/// Squared distance above which the position is resent (both 1.8 and 1.16).
7const POSITION_EPSILON_SQ: f64 = 9.0e-4;
8/// The position is resent at least every this many ticks.
9const POSITION_REFRESH_TICKS: u32 = 20;
10
11/// Chooses the tick's packets the way vanilla does: sprint change, sneak change, then exactly
12/// one of `flying` / `position` / `look` / `position_look`.
13#[derive(Debug, Clone, Copy, Default, PartialEq)]
14pub struct MovementSender {
15    last_pos: Vec3,
16    last_yaw: f32,
17    last_pitch: f32,
18    ticks_since_pos: u32,
19    server_sprint: bool,
20    server_sneak: bool,
21    /// 1.9+: `ticksSinceLastPositionPacketSent` is incremented *before* the check (a position at least
22    /// every 20 ticks); 1.8 increments after it (every 21st tick).
23    increment_first: bool,
24}
25
26/// The packets of one movement step, in send order.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct MoveStep {
29    /// Sprint change (`entity_action`), sent first.
30    pub sprint: Option<EntityAction>,
31    /// Sneak change (`entity_action`), sent second.
32    pub sneak: Option<EntityAction>,
33    /// The movement packet.
34    pub packet: MovePacket,
35}
36
37impl MovementSender {
38    /// A fresh 1.8 client player: `lastReported*` are all zero, nothing sprinting or
39    /// sneaking; the position is refreshed after 20 ticks without one (every 21st tick).
40    #[must_use]
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// A fresh 1.9+ client player: like [`MovementSender::new`], but `ticksSinceLastPositionPacketSent`
46    /// is incremented before the check, so a position goes out at least every 20 ticks
47    /// (Grim's BadPacketsE flags a 21-tick gap on these versions).
48    #[must_use]
49    pub fn new_1_9() -> Self {
50        MovementSender {
51            increment_first: true,
52            ..Self::default()
53        }
54    }
55
56    /// Ticks since the position was last sent.
57    #[must_use]
58    pub fn ticks_since_pos(&self) -> u32 {
59        self.ticks_since_pos
60    }
61
62    /// One tick's packets for the pose after the physics step.
63    pub fn update(&mut self, pose: &Pose) -> MoveStep {
64        let sprint = (pose.sprinting != self.server_sprint).then(|| {
65            self.server_sprint = pose.sprinting;
66            if pose.sprinting {
67                EntityAction::StartSprinting
68            } else {
69                EntityAction::StopSprinting
70            }
71        });
72        let sneak = (pose.sneaking != self.server_sneak).then(|| {
73            self.server_sneak = pose.sneaking;
74            if pose.sneaking {
75                EntityAction::StartSneaking
76            } else {
77                EntityAction::StopSneaking
78            }
79        });
80        let dx = pose.pos.x - self.last_pos.x;
81        let dy = pose.pos.y - self.last_pos.y;
82        let dz = pose.pos.z - self.last_pos.z;
83        // `(double)(yaw - lastYaw) != 0`: an f32 difference.
84        let turned = pose.yaw - self.last_yaw != 0.0 || pose.pitch - self.last_pitch != 0.0;
85        if self.increment_first {
86            self.ticks_since_pos += 1;
87        }
88        let moved = dx * dx + dy * dy + dz * dz > POSITION_EPSILON_SQ
89            || self.ticks_since_pos >= POSITION_REFRESH_TICKS;
90        let on_ground = pose.on_ground;
91        let packet = match (moved, turned) {
92            (true, true) => MovePacket::PositionLook {
93                pos: pose.pos,
94                yaw: pose.yaw,
95                pitch: pose.pitch,
96                on_ground,
97            },
98            (true, false) => MovePacket::Position {
99                pos: pose.pos,
100                on_ground,
101            },
102            (false, true) => MovePacket::Look {
103                yaw: pose.yaw,
104                pitch: pose.pitch,
105                on_ground,
106            },
107            (false, false) => MovePacket::Flying { on_ground },
108        };
109        if !self.increment_first {
110            self.ticks_since_pos += 1;
111        }
112        if moved {
113            self.last_pos = pose.pos;
114            self.ticks_since_pos = 0;
115        }
116        if turned {
117            self.last_yaw = pose.yaw;
118            self.last_pitch = pose.pitch;
119        }
120        MoveStep {
121            sprint,
122            sneak,
123            packet,
124        }
125    }
126}
127
128/// The stand-in mover used until `ghast-physics` is integrated: the player never moves.
129///
130/// It reproduces what the vanilla client computes for a player standing still on a floor
131/// right after a (non-relative) teleport: the first tick moves by velocity y = 0 and is not on
132/// ground; gravity then makes the next move collide with the floor, so every later tick is on
133/// ground. It assumes the server put the player on a floor; without world data it cannot
134/// know. Movement keys are ignored (only the sneak key is honoured, it does not move).
135#[derive(Debug, Clone, Copy, Default, PartialEq)]
136pub struct StandStill {
137    ticks_since_teleport: u32,
138}
139
140impl StandStill {
141    /// The server teleported the player.
142    pub fn teleported(&mut self) {
143        self.ticks_since_teleport = 0;
144    }
145
146    /// One tick: updates `on_ground`, `sneaking` and `sprinting`; position and look are kept.
147    pub fn step(&mut self, pose: &mut Pose, controls: &Controls) {
148        pose.on_ground = self.ticks_since_teleport > 0;
149        self.ticks_since_teleport = self.ticks_since_teleport.saturating_add(1);
150        pose.motion = Vec3::default();
151        pose.sneaking = controls.sneak && !pose.flying;
152        pose.sprinting = false;
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    fn pose(x: f64, y: f64, z: f64, yaw: f32, pitch: f32) -> Pose {
161        Pose {
162            pos: Vec3::new(x, y, z),
163            yaw,
164            pitch,
165            on_ground: true,
166            ..Pose::default()
167        }
168    }
169
170    #[test]
171    fn packet_choice() {
172        let mut s = MovementSender::new();
173        // Fresh player: last reported is the origin, so the first packet carries everything.
174        let p = pose(10.5, 64.0, -3.5, 90.0, 10.0);
175        assert!(matches!(
176            s.update(&p).packet,
177            MovePacket::PositionLook { .. }
178        ));
179        // Standing still: 20 flying packets, then a position refresh (vanilla checks
180        // `ticksSinceLastPositionPacketSent >= 20` before incrementing it: a 21-tick period).
181        for _ in 0..20 {
182            assert!(matches!(
183                s.update(&p).packet,
184                MovePacket::Flying { on_ground: true }
185            ));
186        }
187        assert!(matches!(s.update(&p).packet, MovePacket::Position { .. }));
188        // A tiny move below the threshold is not sent; a look change is.
189        let p2 = pose(10.51, 64.0, -3.5, 91.0, 10.0);
190        assert!(matches!(s.update(&p2).packet, MovePacket::Look { .. }));
191        let p3 = pose(10.6, 64.0, -3.5, 91.0, 10.0);
192        assert!(matches!(s.update(&p3).packet, MovePacket::Position { .. }));
193    }
194
195    #[test]
196    fn position_refresh_1_9() {
197        let mut s = MovementSender::new_1_9();
198        let p = pose(10.5, 64.0, -3.5, 90.0, 10.0);
199        assert!(matches!(
200            s.update(&p).packet,
201            MovePacket::PositionLook { .. }
202        ));
203        for _ in 0..19 {
204            assert!(matches!(s.update(&p).packet, MovePacket::Flying { .. }));
205        }
206        assert!(matches!(s.update(&p).packet, MovePacket::Position { .. }));
207    }
208
209    #[test]
210    fn sneak_and_sprint_order() {
211        let mut s = MovementSender::new();
212        let mut p = pose(0.0, 0.0, 0.0, 0.0, 0.0);
213        p.sneaking = true;
214        p.sprinting = true;
215        let st = s.update(&p);
216        assert_eq!(st.sprint, Some(EntityAction::StartSprinting));
217        assert_eq!(st.sneak, Some(EntityAction::StartSneaking));
218        let st = s.update(&p);
219        assert_eq!((st.sprint, st.sneak), (None, None));
220        p.sneaking = false;
221        assert_eq!(s.update(&p).sneak, Some(EntityAction::StopSneaking));
222    }
223
224    #[test]
225    fn stand_still_ground() {
226        let mut m = StandStill::default();
227        let mut p = Pose::default();
228        m.teleported();
229        m.step(&mut p, &Controls::default());
230        assert!(!p.on_ground);
231        m.step(&mut p, &Controls::default());
232        assert!(p.on_ground);
233    }
234}