Skip to main content

alice_physics/
netcode_prediction.rs

1//! Client-side prediction primitives with server reconciliation.
2//!
3//! Complements [`crate::netcode`] (checksum-based rollback for the
4//! full simulation) with a lighter-weight input-buffer / snapshot-
5//! reconciliation core that individual game systems (character
6//! controllers, projectiles) can wire up without invoking the entire
7//! world rewind.
8//!
9//! # Usage pattern
10//!
11//! ```text
12//! 1. Client records inputs into a `PredictionBuffer` each tick and
13//!    stores a matching `Snapshot` produced by applying that input.
14//! 2. Client sends inputs to the authoritative server.
15//! 3. Server replies with the authoritative `Snapshot` for the tick
16//!    it just simulated.
17//! 4. Client calls `reconcile` with the server snapshot + the list of
18//!    inputs it has retained since that tick. `reconcile` rewinds the
19//!    local snapshot to the authoritative one, replays each stored
20//!    input, and returns the corrected head snapshot.
21//! ```
22//!
23//! The trait interface is generic over the game's input, state, and
24//! integration function so callers can plug in any deterministic
25//! step.
26
27use core::fmt::Debug;
28
29/// A single client-side input tagged with the tick it applies to.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct PredictedInput<A>
32where
33    A: Copy,
34{
35    /// Simulation tick the input applies to.
36    pub tick: u64,
37    /// Player action / control vector.
38    pub action: A,
39}
40
41/// A snapshot of the predicted state at a particular tick.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct Snapshot<S>
44where
45    S: Copy,
46{
47    /// Simulation tick this snapshot represents.
48    pub tick: u64,
49    /// Concrete state (position, velocity, health — game-specific).
50    pub state: S,
51}
52
53/// Rolling buffer of unacknowledged inputs + matching predicted
54/// snapshots, sized so the oldest entry is dropped when the ring is
55/// full.
56#[derive(Debug, Clone)]
57pub struct PredictionBuffer<A, S>
58where
59    A: Copy,
60    S: Copy,
61{
62    inputs: Vec<PredictedInput<A>>,
63    snapshots: Vec<Snapshot<S>>,
64    capacity: usize,
65}
66
67impl<A, S> PredictionBuffer<A, S>
68where
69    A: Copy + Debug,
70    S: Copy + Debug,
71{
72    /// Construct a buffer that retains at most `capacity` entries.
73    ///
74    /// # Panics
75    ///
76    /// Panics if `capacity == 0`.
77    #[must_use]
78    pub fn new(capacity: usize) -> Self {
79        assert!(capacity > 0, "capacity must be > 0");
80        Self {
81            inputs: Vec::with_capacity(capacity),
82            snapshots: Vec::with_capacity(capacity),
83            capacity,
84        }
85    }
86
87    /// Number of buffered entries.
88    #[must_use]
89    pub fn len(&self) -> usize {
90        self.inputs.len()
91    }
92
93    /// True when no inputs are buffered.
94    #[must_use]
95    pub fn is_empty(&self) -> bool {
96        self.inputs.is_empty()
97    }
98
99    /// Push a `(input, snapshot)` pair. Drops the oldest entry when
100    /// the ring is full. The snapshot MUST be the result of applying
101    /// `input` to the previous state — the buffer does not verify
102    /// this.
103    pub fn push(&mut self, input: PredictedInput<A>, snapshot: Snapshot<S>) {
104        debug_assert_eq!(
105            input.tick, snapshot.tick,
106            "input and snapshot ticks must match"
107        );
108        if self.inputs.len() == self.capacity {
109            self.inputs.remove(0);
110            self.snapshots.remove(0);
111        }
112        self.inputs.push(input);
113        self.snapshots.push(snapshot);
114    }
115
116    /// Head (newest) snapshot, if any.
117    #[must_use]
118    pub fn head_snapshot(&self) -> Option<Snapshot<S>> {
119        self.snapshots.last().copied()
120    }
121
122    /// All buffered inputs.
123    #[must_use]
124    pub fn inputs(&self) -> &[PredictedInput<A>] {
125        &self.inputs
126    }
127
128    /// Drop every entry with `tick <= authoritative_tick`. Used after
129    /// reconcile to discard now-obsolete predictions.
130    pub fn drop_acknowledged(&mut self, authoritative_tick: u64) {
131        let cutoff = self
132            .inputs
133            .iter()
134            .position(|i| i.tick > authoritative_tick)
135            .unwrap_or(self.inputs.len());
136        self.inputs.drain(..cutoff);
137        self.snapshots.drain(..cutoff);
138    }
139}
140
141/// Rewind a prediction buffer to `authoritative` and replay every
142/// buffered input with a caller-supplied integration function,
143/// returning the corrected head snapshot.
144///
145/// - `authoritative`: last authoritative snapshot received from the
146///   server. Its tick MUST equal the tick just before the first
147///   buffered input.
148/// - `buffer`: rolling buffer of unacknowledged `(input, snapshot)`
149///   pairs. `reconcile` drops acknowledged entries and replaces the
150///   remaining snapshots with the replayed ones.
151/// - `step`: closure that applies a single input to a state and
152///   returns the updated state.
153///
154/// Returns the head snapshot after replay. If the authoritative
155/// snapshot is newer than every buffered entry, the buffer is
156/// cleared and the authoritative snapshot is returned directly.
157pub fn reconcile<A, S, F>(
158    authoritative: Snapshot<S>,
159    buffer: &mut PredictionBuffer<A, S>,
160    mut step: F,
161) -> Snapshot<S>
162where
163    A: Copy + Debug,
164    S: Copy + Debug,
165    F: FnMut(S, PredictedInput<A>) -> S,
166{
167    buffer.drop_acknowledged(authoritative.tick);
168    if buffer.is_empty() {
169        return authoritative;
170    }
171    let mut state = authoritative.state;
172    let mut new_snapshots = Vec::with_capacity(buffer.len());
173    let inputs_copy: Vec<_> = buffer.inputs.to_vec();
174    for input in &inputs_copy {
175        state = step(state, *input);
176        new_snapshots.push(Snapshot {
177            tick: input.tick,
178            state,
179        });
180    }
181    buffer.snapshots = new_snapshots;
182    buffer
183        .head_snapshot()
184        .expect("buffer non-empty by construction")
185}
186
187/// Why [`reconcile_checked`] could not replay the buffered inputs.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189#[non_exhaustive]
190pub enum ReconcileError {
191    /// The input for tick `needed` is missing: replaying from the authoritative
192    /// snapshot needs every input from `authoritative.tick + 1` on, but the next one
193    /// the buffer holds is for tick `oldest` (the ring dropped the older ones, or the
194    /// buffer skipped a tick).
195    MissingInputs {
196        /// The tick whose input is missing.
197        needed: u64,
198        /// The tick of the next input the buffer does have.
199        oldest: u64,
200    },
201}
202
203/// [`reconcile`] that refuses an incomplete history.
204///
205/// [`reconcile`] replays "every buffered input after the authoritative tick", which
206/// is only the right replay when those inputs are the *consecutive* ticks
207/// `authoritative.tick + 1, + 2, …`. If the server's snapshot is older than the
208/// oldest input the ring still holds (a slow server, a small capacity), or the buffer
209/// skipped a tick, the inputs in between are missing and the replay returns a wrong
210/// state without any sign of it. This returns
211/// [`ReconcileError::MissingInputs`] instead, and leaves the buffer untouched, so the
212/// caller can ask the server for a full state or grow the buffer. With a complete
213/// history it is exactly [`reconcile`].
214///
215/// # Errors
216///
217/// [`ReconcileError::MissingInputs`] when an input between the authoritative tick
218/// and the head is not in the buffer.
219pub fn reconcile_checked<A, S, F>(
220    authoritative: Snapshot<S>,
221    buffer: &mut PredictionBuffer<A, S>,
222    step: F,
223) -> Result<Snapshot<S>, ReconcileError>
224where
225    A: Copy + Debug,
226    S: Copy + Debug,
227    F: FnMut(S, PredictedInput<A>) -> S,
228{
229    let mut needed = authoritative.tick.saturating_add(1);
230    for input in buffer.inputs.iter().filter(|i| i.tick > authoritative.tick) {
231        if input.tick != needed {
232            return Err(ReconcileError::MissingInputs {
233                needed,
234                oldest: input.tick,
235            });
236        }
237        needed = needed.saturating_add(1);
238    }
239    Ok(reconcile(authoritative, buffer, step))
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    // Simple test state: a 1-D scalar position advanced by an integer
247    // delta each tick.
248    fn step(state: i32, input: PredictedInput<i32>) -> i32 {
249        state + input.action
250    }
251
252    #[test]
253    fn buffer_rejects_zero_capacity() {
254        let result = std::panic::catch_unwind(|| PredictionBuffer::<i32, i32>::new(0));
255        assert!(result.is_err());
256    }
257
258    #[test]
259    fn buffer_pushes_up_to_capacity_and_drops_oldest() {
260        let mut buffer: PredictionBuffer<i32, i32> = PredictionBuffer::new(3);
261        for t in 0..5 {
262            buffer.push(
263                PredictedInput { tick: t, action: 1 },
264                Snapshot { tick: t, state: 0 },
265            );
266        }
267        assert_eq!(buffer.len(), 3);
268        // Ticks 0, 1 should have been dropped.
269        assert_eq!(buffer.inputs()[0].tick, 2);
270    }
271
272    #[test]
273    fn drop_acknowledged_removes_stale_entries() {
274        let mut buffer: PredictionBuffer<i32, i32> = PredictionBuffer::new(8);
275        for t in 0..5 {
276            buffer.push(
277                PredictedInput { tick: t, action: 1 },
278                Snapshot { tick: t, state: 0 },
279            );
280        }
281        buffer.drop_acknowledged(2);
282        assert_eq!(buffer.len(), 2);
283        assert_eq!(buffer.inputs()[0].tick, 3);
284    }
285
286    #[test]
287    fn reconcile_with_empty_buffer_returns_authoritative() {
288        let mut buffer: PredictionBuffer<i32, i32> = PredictionBuffer::new(4);
289        let authoritative = Snapshot {
290            tick: 10,
291            state: 99,
292        };
293        let result = reconcile(authoritative, &mut buffer, step);
294        assert_eq!(result, authoritative);
295    }
296
297    #[test]
298    fn reconcile_replays_buffered_inputs() {
299        let mut buffer: PredictionBuffer<i32, i32> = PredictionBuffer::new(8);
300        for t in 5..10 {
301            buffer.push(
302                PredictedInput { tick: t, action: 1 },
303                Snapshot { tick: t, state: 0 },
304            );
305        }
306        let authoritative = Snapshot { tick: 4, state: 0 };
307        let head = reconcile(authoritative, &mut buffer, step);
308        // Authoritative was 0, then five +1 inputs → 5.
309        assert_eq!(head.state, 5);
310        assert_eq!(head.tick, 9);
311    }
312
313    #[test]
314    fn reconcile_discards_acknowledged_before_replay() {
315        let mut buffer: PredictionBuffer<i32, i32> = PredictionBuffer::new(8);
316        for t in 0..10 {
317            buffer.push(
318                PredictedInput { tick: t, action: 1 },
319                Snapshot { tick: t, state: 0 },
320            );
321        }
322        let authoritative = Snapshot {
323            tick: 5,
324            state: 100,
325        };
326        let head = reconcile(authoritative, &mut buffer, step);
327        // Ticks 6, 7, 8, 9 remain; each +1 from base 100 → 104.
328        assert_eq!(head.state, 104);
329        assert_eq!(head.tick, 9);
330        assert_eq!(buffer.len(), 4);
331    }
332
333    #[test]
334    fn buffer_head_snapshot_returns_newest() {
335        let mut buffer: PredictionBuffer<i32, i32> = PredictionBuffer::new(4);
336        buffer.push(
337            PredictedInput { tick: 1, action: 0 },
338            Snapshot { tick: 1, state: 11 },
339        );
340        buffer.push(
341            PredictedInput { tick: 2, action: 0 },
342            Snapshot { tick: 2, state: 22 },
343        );
344        let head = buffer.head_snapshot().unwrap();
345        assert_eq!(head.tick, 2);
346        assert_eq!(head.state, 22);
347    }
348}