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