Skip to main content

steel_core/player/movement/
state.rs

1//! Movement tracking state for position validation and anti-cheat rate limiting.
2
3use glam::DVec3;
4
5use crate::physics::ClientAuthoredMovementState;
6use crate::player::PlayerInput;
7
8/// Internal movement tracking state, stored behind a single `SyncMutex` on `Player`.
9pub struct MovementState {
10    /// Vanilla validation state for client-authored body movement.
11    client_movement: ClientAuthoredMovementState,
12    /// Vanilla validation state for the controlled root vehicle.
13    client_vehicle_movement: ClientAuthoredMovementState,
14    /// Whether vanilla accepted player-authored movement during the current client tick.
15    received_movement_this_tick: bool,
16    /// Whether a valid positional packet arrived during the current client tick
17    received_position_this_tick: bool,
18    /// Entity id of the controlled root vehicle tracked this tick.
19    client_vehicle_id: Option<i32>,
20    /// Server tick of the last correction for a non controlling passenger
21    pub(in crate::player) vehicle_position_last_reset_at: i32,
22    /// Latest vanilla client input snapshot sent by the player.
23    last_client_input: PlayerInput,
24}
25
26impl MovementState {
27    #[must_use]
28    pub const fn new() -> Self {
29        Self {
30            client_movement: ClientAuthoredMovementState::new(),
31            client_vehicle_movement: ClientAuthoredMovementState::new(),
32            received_movement_this_tick: false,
33            received_position_this_tick: false,
34            client_vehicle_id: None,
35            vehicle_position_last_reset_at: 0,
36            last_client_input: PlayerInput::EMPTY,
37        }
38    }
39
40    /// Stores the latest client input snapshot.
41    pub(in crate::player) const fn set_last_client_input(&mut self, input: PlayerInput) {
42        self.last_client_input = input;
43    }
44
45    /// Returns the latest client input snapshot.
46    #[must_use]
47    pub(in crate::player) const fn last_client_input(&self) -> PlayerInput {
48        self.last_client_input
49    }
50
51    /// Resets per-tick vanilla movement validation bases.
52    pub(in crate::player) const fn reset_for_tick(&mut self, position: DVec3) {
53        self.client_movement.reset_for_tick(position);
54    }
55
56    /// Resets per-tick vanilla controlled-vehicle movement validation bases.
57    pub(in crate::player) const fn reset_vehicle_for_tick(
58        &mut self,
59        vehicle_id: i32,
60        position: DVec3,
61    ) {
62        self.client_vehicle_id = Some(vehicle_id);
63        self.client_vehicle_movement.reset_for_tick(position);
64    }
65
66    /// Clears the active controlled-vehicle validation state for this tick.
67    pub(in crate::player) const fn clear_vehicle_for_tick(&mut self) {
68        self.client_vehicle_id = None;
69        self.client_vehicle_movement.clear_client_floating();
70    }
71
72    /// Returns the current vanilla controlled-vehicle validation positions.
73    #[must_use]
74    pub(in crate::player) const fn vehicle_good_positions(
75        &self,
76        vehicle_id: i32,
77    ) -> Option<(DVec3, DVec3)> {
78        if !matches!(self.client_vehicle_id, Some(active_id) if active_id == vehicle_id) {
79            return None;
80        }
81
82        Some(self.client_vehicle_movement.good_positions())
83    }
84
85    /// Resets movement validation bases after a server position sync.
86    pub(in crate::player) const fn reset_for_position_sync(&mut self, position: DVec3) {
87        self.client_movement.reset_for_position_sync(position);
88        self.received_movement_this_tick = false;
89    }
90
91    /// Returns the current vanilla first-good and last-good validation positions.
92    #[must_use]
93    pub(in crate::player) const fn good_positions(&self) -> (DVec3, DVec3) {
94        self.client_movement.good_positions()
95    }
96
97    /// Records a received movement packet and returns packets since the last tick.
98    pub(in crate::player) const fn record_move_packet_delta(&mut self) -> i32 {
99        self.client_movement.record_move_packet_delta()
100    }
101
102    /// Claims the positional allowance including packets ignored by movement gates
103    pub(in crate::player) const fn record_position_packet(&mut self) -> bool {
104        if self.received_position_this_tick {
105            return false;
106        }
107        self.received_position_this_tick = true;
108        true
109    }
110
111    /// Marks a movement target as the latest accepted vanilla last-good position.
112    pub(in crate::player) const fn mark_last_good_position(&mut self, position: DVec3) {
113        self.client_movement.mark_last_good_position(position);
114    }
115
116    /// Marks a controlled-vehicle target as the latest accepted vanilla last-good position.
117    pub(in crate::player) const fn mark_vehicle_last_good_position(
118        &mut self,
119        vehicle_id: i32,
120        position: DVec3,
121    ) {
122        if matches!(self.client_vehicle_id, Some(active_id) if active_id == vehicle_id) {
123            self.client_vehicle_movement
124                .mark_last_good_position(position);
125        }
126    }
127
128    /// Sets the last accepted client movement vector.
129    pub(in crate::player) const fn set_last_known_client_movement(&mut self, movement: DVec3) {
130        self.client_movement
131            .set_last_known_client_movement(movement);
132        self.received_movement_this_tick = true;
133    }
134
135    /// Clears the last accepted client movement vector.
136    pub(in crate::player) const fn reset_last_known_client_movement(&mut self) {
137        self.client_movement.reset_last_known_client_movement();
138        self.received_movement_this_tick = false;
139    }
140
141    /// Applies vanilla client-tick-end known-movement handling.
142    pub(in crate::player) const fn finish_client_tick(&mut self) {
143        if !self.received_movement_this_tick {
144            self.client_movement.reset_last_known_client_movement();
145        }
146        self.received_movement_this_tick = false;
147        self.received_position_this_tick = false;
148    }
149
150    /// Returns the last accepted client movement vector.
151    #[must_use]
152    pub(in crate::player) const fn last_known_client_movement(&self) -> DVec3 {
153        self.client_movement.last_known_client_movement()
154    }
155
156    /// Records whether the latest accepted movement made the client appear to float.
157    pub(in crate::player) const fn record_client_floating(&mut self, client_is_floating: bool) {
158        self.client_movement
159            .record_client_floating(client_is_floating);
160    }
161
162    /// Records whether the controlled vehicle appeared to float after accepted movement.
163    pub(in crate::player) const fn record_vehicle_client_floating(
164        &mut self,
165        vehicle_id: i32,
166        client_is_floating: bool,
167    ) {
168        if matches!(self.client_vehicle_id, Some(active_id) if active_id == vehicle_id) {
169            self.client_vehicle_movement
170                .record_client_floating(client_is_floating);
171        }
172    }
173
174    /// Resets the vanilla floating violation counter.
175    pub(in crate::player) const fn reset_flying_ticks(&mut self) {
176        self.client_movement.reset_flying_ticks();
177        self.client_vehicle_movement.reset_flying_ticks();
178    }
179
180    /// Advances the vanilla floating violation tracker.
181    ///
182    /// Returns true once the client has exceeded the configured maximum flying ticks.
183    pub(in crate::player) const fn tick_client_floating(
184        &mut self,
185        should_count: bool,
186        maximum_flying_ticks: i32,
187    ) -> bool {
188        self.client_movement
189            .tick_client_floating(should_count, maximum_flying_ticks)
190    }
191
192    /// Advances the vanilla controlled-vehicle floating violation tracker.
193    ///
194    /// Returns true once the client has exceeded the configured maximum flying ticks.
195    pub(in crate::player) const fn tick_vehicle_client_floating(
196        &mut self,
197        vehicle_id: i32,
198        maximum_flying_ticks: i32,
199    ) -> bool {
200        if !matches!(self.client_vehicle_id, Some(active_id) if active_id == vehicle_id) {
201            self.client_vehicle_movement.clear_client_floating();
202            return false;
203        }
204
205        self.client_vehicle_movement
206            .tick_client_floating(true, maximum_flying_ticks)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use glam::DVec3;
213
214    use crate::player::PlayerInput;
215
216    use super::MovementState;
217
218    #[test]
219    fn movement_state_starts_with_zero_known_client_movement() {
220        let state = MovementState::new();
221        assert_eq!(state.last_known_client_movement(), DVec3::ZERO);
222    }
223
224    #[test]
225    fn client_tick_end_clears_known_movement_without_accepted_movement() {
226        let mut state = MovementState::new();
227        state.set_last_known_client_movement(DVec3::new(0.1, 0.0, 0.0));
228        state.finish_client_tick();
229
230        state.finish_client_tick();
231
232        assert_eq!(state.last_known_client_movement(), DVec3::ZERO);
233    }
234
235    #[test]
236    fn client_tick_end_keeps_known_movement_after_accepted_movement() {
237        let mut state = MovementState::new();
238        let movement = DVec3::new(0.1, 0.0, 0.0);
239
240        state.set_last_known_client_movement(movement);
241        state.finish_client_tick();
242
243        assert_eq!(state.last_known_client_movement(), movement);
244    }
245
246    #[test]
247    fn movement_state_tracks_last_client_input() {
248        let mut state = MovementState::new();
249        let input = PlayerInput::from_flags(0x01 | 0x08 | 0x10 | 0x40);
250
251        assert_eq!(state.last_client_input(), PlayerInput::EMPTY);
252        state.set_last_client_input(input);
253
254        assert_eq!(state.last_client_input(), input);
255        assert_eq!(
256            state.last_client_input().movement_input(),
257            DVec3::new(-1.0, 0.0, 1.0)
258        );
259    }
260
261    #[test]
262    fn tick_reset_updates_both_good_positions_and_packet_count() {
263        let mut state = MovementState::new();
264        state.mark_last_good_position(DVec3::new(1.0, 2.0, 3.0));
265        state.record_move_packet_delta();
266        state.record_move_packet_delta();
267
268        state.reset_for_tick(DVec3::new(4.0, 5.0, 6.0));
269
270        assert_eq!(
271            state.good_positions(),
272            (DVec3::new(4.0, 5.0, 6.0), DVec3::new(4.0, 5.0, 6.0))
273        );
274        assert_eq!(state.record_move_packet_delta(), 1);
275    }
276
277    #[test]
278    fn position_sync_reset_clears_packet_counts_and_known_movement() {
279        let mut state = MovementState::new();
280        state.record_move_packet_delta();
281        state.set_last_known_client_movement(DVec3::new(0.1, 0.0, 0.0));
282
283        state.reset_for_position_sync(DVec3::new(2.0, 3.0, 4.0));
284
285        assert_eq!(state.good_positions().0, DVec3::new(2.0, 3.0, 4.0));
286        assert_eq!(state.good_positions().1, DVec3::new(2.0, 3.0, 4.0));
287        assert_eq!(state.last_known_client_movement(), DVec3::ZERO);
288        assert_eq!(state.record_move_packet_delta(), 1);
289    }
290
291    #[test]
292    fn floating_tracker_counts_only_while_client_is_floating() {
293        let mut state = MovementState::new();
294        state.record_client_floating(true);
295
296        assert!(!state.tick_client_floating(true, 2));
297        assert!(!state.tick_client_floating(true, 2));
298        assert!(state.tick_client_floating(true, 2));
299
300        state.record_client_floating(false);
301        assert!(!state.tick_client_floating(true, 2));
302
303        state.record_client_floating(true);
304        assert!(!state.tick_client_floating(true, 2));
305    }
306
307    #[test]
308    fn floating_tracker_resets_when_tick_conditions_do_not_count() {
309        let mut state = MovementState::new();
310        state.record_client_floating(true);
311
312        assert!(!state.tick_client_floating(true, 1));
313        assert!(!state.tick_client_floating(false, 1));
314
315        state.record_client_floating(true);
316        assert!(!state.tick_client_floating(true, 1));
317    }
318
319    #[test]
320    fn vehicle_tick_reset_tracks_active_vehicle_id() {
321        let mut state = MovementState::new();
322
323        state.reset_vehicle_for_tick(42, DVec3::new(1.0, 2.0, 3.0));
324
325        assert_eq!(
326            state.vehicle_good_positions(42),
327            Some((DVec3::new(1.0, 2.0, 3.0), DVec3::new(1.0, 2.0, 3.0)))
328        );
329        assert_eq!(state.vehicle_good_positions(41), None);
330        assert!(!state.tick_vehicle_client_floating(41, 0));
331        assert!(!state.tick_vehicle_client_floating(42, 0));
332    }
333
334    #[test]
335    fn vehicle_last_good_update_is_guarded_by_active_vehicle_id() {
336        let mut state = MovementState::new();
337        state.reset_vehicle_for_tick(42, DVec3::new(1.0, 2.0, 3.0));
338
339        state.mark_vehicle_last_good_position(41, DVec3::new(9.0, 9.0, 9.0));
340        assert_eq!(
341            state.vehicle_good_positions(42),
342            Some((DVec3::new(1.0, 2.0, 3.0), DVec3::new(1.0, 2.0, 3.0)))
343        );
344
345        state.mark_vehicle_last_good_position(42, DVec3::new(4.0, 5.0, 6.0));
346
347        assert_eq!(
348            state.vehicle_good_positions(42),
349            Some((DVec3::new(1.0, 2.0, 3.0), DVec3::new(4.0, 5.0, 6.0)))
350        );
351    }
352
353    #[test]
354    fn vehicle_floating_update_is_guarded_by_active_vehicle_id() {
355        let mut state = MovementState::new();
356        state.reset_vehicle_for_tick(42, DVec3::new(1.0, 2.0, 3.0));
357
358        state.record_vehicle_client_floating(41, true);
359        assert!(!state.tick_vehicle_client_floating(42, 0));
360
361        state.record_vehicle_client_floating(42, true);
362        assert!(state.tick_vehicle_client_floating(42, 0));
363    }
364
365    #[test]
366    fn vehicle_state_clears_when_no_controlled_vehicle_is_active() {
367        let mut state = MovementState::new();
368        state.reset_vehicle_for_tick(42, DVec3::new(1.0, 2.0, 3.0));
369
370        state.clear_vehicle_for_tick();
371
372        assert!(!state.tick_vehicle_client_floating(42, 0));
373    }
374
375    #[test]
376    fn reset_flying_ticks_applies_to_vehicle_counter() {
377        let mut state = MovementState::new();
378        state.reset_vehicle_for_tick(42, DVec3::new(1.0, 2.0, 3.0));
379        state.client_vehicle_movement.record_client_floating(true);
380
381        assert!(!state.tick_vehicle_client_floating(42, 1));
382        state.reset_flying_ticks();
383        assert!(!state.tick_vehicle_client_floating(42, 1));
384        assert!(state.tick_vehicle_client_floating(42, 1));
385    }
386}