Skip to main content

elura_netcode/
prediction.rs

1use std::collections::VecDeque;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{NetcodeError, NetcodeResult};
6
7/// Bounds for client prediction history and authoritative replay work.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(default, deny_unknown_fields)]
10#[non_exhaustive]
11pub struct PredictionConfig {
12    /// Maximum unconfirmed input frames retained locally.
13    pub history_capacity: usize,
14    /// Maximum pending inputs replayed by one reconciliation.
15    pub max_replay_steps: usize,
16}
17
18impl Default for PredictionConfig {
19    fn default() -> Self {
20        Self {
21            history_capacity: 256,
22            max_replay_steps: 64,
23        }
24    }
25}
26
27impl PredictionConfig {
28    /// Validates prediction memory and replay bounds.
29    pub fn validate(&self) -> NetcodeResult<()> {
30        if self.history_capacity == 0 {
31            return Err(NetcodeError::InvalidConfig(
32                "prediction history capacity must be positive",
33            ));
34        }
35        if self.max_replay_steps == 0 || self.max_replay_steps > self.history_capacity {
36            return Err(NetcodeError::InvalidConfig(
37                "prediction replay limit must be within history capacity",
38            ));
39        }
40        Ok(())
41    }
42}
43
44/// One locally simulated input and the predicted state after it committed.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct PredictionFrame<I, S> {
47    /// Client simulation Tick assigned to this input.
48    pub tick: u64,
49    /// Application-owned local input.
50    pub input: I,
51    /// Predicted application state after this input and Tick.
52    pub predicted_state: S,
53}
54
55/// Result of restoring an authoritative state and replaying unconfirmed inputs.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ReconciliationReport<S> {
58    /// Server Tick represented by the authoritative state.
59    pub authoritative_tick: u64,
60    /// Local predicted state previously recorded at the authoritative Tick, when retained.
61    pub predicted_state_at_authoritative_tick: Option<S>,
62    /// Corrected present state after replaying every pending input.
63    pub corrected_state: S,
64    /// Number of unconfirmed inputs replayed.
65    pub replayed_inputs: usize,
66    /// Newest local predicted Tick after reconciliation.
67    pub current_tick: u64,
68}
69
70/// Bounded client input/state history for server reconciliation.
71#[derive(Debug, Clone)]
72pub struct PredictionBuffer<I, S> {
73    config: PredictionConfig,
74    frames: VecDeque<PredictionFrame<I, S>>,
75    confirmed_tick: u64,
76}
77
78impl<I, S> PredictionBuffer<I, S> {
79    /// Creates an empty prediction history.
80    pub fn new(config: PredictionConfig) -> NetcodeResult<Self> {
81        config.validate()?;
82        Ok(Self {
83            config,
84            frames: VecDeque::with_capacity(config.history_capacity),
85            confirmed_tick: 0,
86        })
87    }
88
89    /// Records one successfully simulated local input and resulting state.
90    pub fn record(&mut self, tick: u64, input: I, predicted_state: S) -> NetcodeResult<()> {
91        let previous = self
92            .frames
93            .back()
94            .map_or(self.confirmed_tick, |frame| frame.tick);
95        if tick == 0 || tick <= previous {
96            return Err(NetcodeError::InvalidInput(
97                "prediction Tick must increase and be positive",
98            ));
99        }
100        if self.frames.len() >= self.config.history_capacity {
101            return Err(NetcodeError::PredictionHistoryFull);
102        }
103        self.frames.push_back(PredictionFrame {
104            tick,
105            input,
106            predicted_state,
107        });
108        Ok(())
109    }
110
111    /// Returns the highest authoritative Tick already reconciled.
112    pub fn confirmed_tick(&self) -> u64 {
113        self.confirmed_tick
114    }
115
116    /// Returns the number of inputs awaiting authoritative confirmation.
117    pub fn pending_len(&self) -> usize {
118        self.frames.len()
119    }
120
121    /// Returns whether no unconfirmed input remains.
122    pub fn is_empty(&self) -> bool {
123        self.frames.is_empty()
124    }
125
126    /// Iterates retained prediction frames in Tick order.
127    pub fn frames(&self) -> impl Iterator<Item = &PredictionFrame<I, S>> {
128        self.frames.iter()
129    }
130}
131
132impl<I, S> PredictionBuffer<I, S>
133where
134    S: Clone,
135{
136    /// Restores an authoritative state and deterministically replays later local inputs.
137    ///
138    /// `simulate` owns game rules and must advance `state` exactly once for the supplied Tick and
139    /// input. A replay-limit or ordering error leaves the buffer unchanged.
140    pub fn reconcile<Simulate>(
141        &mut self,
142        authoritative_tick: u64,
143        authoritative_state: S,
144        mut simulate: Simulate,
145    ) -> NetcodeResult<ReconciliationReport<S>>
146    where
147        Simulate: FnMut(&mut S, u64, &I),
148    {
149        if authoritative_tick < self.confirmed_tick {
150            return Err(NetcodeError::InvalidInput(
151                "authoritative prediction Tick moved backwards",
152            ));
153        }
154        let confirmed_frames = self
155            .frames
156            .iter()
157            .position(|frame| frame.tick > authoritative_tick)
158            .unwrap_or(self.frames.len());
159        let replayed_inputs = self.frames.len() - confirmed_frames;
160        if replayed_inputs > self.config.max_replay_steps {
161            return Err(NetcodeError::ReplayLimitExceeded);
162        }
163
164        let predicted_state_at_authoritative_tick = confirmed_frames
165            .checked_sub(1)
166            .and_then(|index| self.frames.get(index))
167            .filter(|frame| frame.tick == authoritative_tick)
168            .map(|frame| frame.predicted_state.clone());
169        let mut corrected_state = authoritative_state;
170        self.frames.drain(..confirmed_frames);
171        for frame in &mut self.frames {
172            simulate(&mut corrected_state, frame.tick, &frame.input);
173            frame.predicted_state = corrected_state.clone();
174        }
175        let current_tick = self
176            .frames
177            .back()
178            .map_or(authoritative_tick, |frame| frame.tick);
179
180        self.confirmed_tick = authoritative_tick;
181        Ok(ReconciliationReport {
182            authoritative_tick,
183            predicted_state_at_authoritative_tick,
184            corrected_state,
185            replayed_inputs,
186            current_tick,
187        })
188    }
189
190    /// Clears all prediction history and installs a new confirmed Tick.
191    pub fn reset(&mut self, confirmed_tick: u64) {
192        self.frames.clear();
193        self.confirmed_tick = confirmed_tick;
194    }
195}