Skip to main content

elura_netcode/
input.rs

1use std::collections::{BTreeSet, VecDeque};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{NetcodeError, NetcodeResult};
6
7/// Bounds for client-side unacknowledged input history.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(default, deny_unknown_fields)]
10#[non_exhaustive]
11pub struct InputSenderConfig {
12    /// Maximum inputs retained until acknowledged by the server.
13    pub history_capacity: usize,
14    /// Most recent unacknowledged inputs included in each packet.
15    pub redundancy: usize,
16}
17
18impl Default for InputSenderConfig {
19    fn default() -> Self {
20        Self {
21            history_capacity: 64,
22            redundancy: 3,
23        }
24    }
25}
26
27impl InputSenderConfig {
28    /// Validates sender memory and packet bounds.
29    pub fn validate(&self) -> NetcodeResult<()> {
30        if self.history_capacity == 0 {
31            return Err(NetcodeError::InvalidConfig(
32                "input history capacity must be positive",
33            ));
34        }
35        if self.redundancy == 0 || self.redundancy > self.history_capacity {
36            return Err(NetcodeError::InvalidConfig(
37                "input redundancy must be within history capacity",
38            ));
39        }
40        Ok(())
41    }
42}
43
44/// Bounds for server-side input validation and reordering.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(default, deny_unknown_fields)]
47#[non_exhaustive]
48pub struct InputReceiverConfig {
49    /// Maximum input frames accepted in one packet.
50    pub max_inputs_per_packet: usize,
51    /// Maximum sequence distance retained above the cumulative acknowledgement.
52    pub reorder_window: u64,
53    /// Maximum number of past ticks accepted for application-owned rollback.
54    pub max_past_ticks: u64,
55    /// Maximum number of future ticks accepted for scheduling.
56    pub max_future_ticks: u64,
57}
58
59impl Default for InputReceiverConfig {
60    fn default() -> Self {
61        Self {
62            max_inputs_per_packet: 16,
63            reorder_window: 256,
64            max_past_ticks: 12,
65            max_future_ticks: 120,
66        }
67    }
68}
69
70impl InputReceiverConfig {
71    /// Validates packet, sequence, and Tick bounds.
72    pub fn validate(&self) -> NetcodeResult<()> {
73        if self.max_inputs_per_packet == 0 {
74            return Err(NetcodeError::InvalidConfig(
75                "maximum inputs per packet must be positive",
76            ));
77        }
78        if self.reorder_window == 0 {
79            return Err(NetcodeError::InvalidConfig(
80                "input reorder window must be positive",
81            ));
82        }
83        if self.max_future_ticks == 0 {
84            return Err(NetcodeError::InvalidConfig(
85                "maximum future ticks must be positive",
86            ));
87        }
88        Ok(())
89    }
90}
91
92/// One game-specific input assigned to an authoritative simulation Tick.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct InputFrame<T> {
95    /// Monotonically increasing client input sequence.
96    pub sequence: u64,
97    /// Authoritative server Tick on which this input should be applied.
98    pub target_tick: u64,
99    /// Application-owned input value.
100    pub input: T,
101}
102
103/// Transport-neutral client input packet.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct InputPacket<T> {
106    /// Client simulation Tick when the packet was constructed.
107    pub client_tick: u64,
108    /// Newest authoritative server Tick whose state the client has consumed.
109    pub acknowledged_server_tick: u64,
110    /// Recent unacknowledged inputs, ordered by sequence.
111    pub inputs: Vec<InputFrame<T>>,
112}
113
114/// Cumulative server acknowledgement returned to one input sender.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116pub struct InputAck {
117    /// Authoritative server Tick when the packet was processed.
118    pub server_tick: u64,
119    /// Highest contiguous client input sequence received by the server.
120    pub acknowledged_sequence: u64,
121}
122
123/// Client-side bounded input history and redundant packet builder.
124#[derive(Debug, Clone)]
125pub struct InputSender<T> {
126    config: InputSenderConfig,
127    next_sequence: u64,
128    acknowledged_sequence: u64,
129    acknowledged_server_tick: u64,
130    history: VecDeque<InputFrame<T>>,
131}
132
133impl<T> InputSender<T> {
134    /// Creates an input sender whose first generated sequence is one.
135    pub fn new(config: InputSenderConfig) -> NetcodeResult<Self> {
136        Self::with_next_sequence(config, 1)
137    }
138
139    /// Creates an input sender with an application-restored next sequence.
140    pub fn with_next_sequence(
141        config: InputSenderConfig,
142        next_sequence: u64,
143    ) -> NetcodeResult<Self> {
144        config.validate()?;
145        if next_sequence == 0 {
146            return Err(NetcodeError::InvalidConfig(
147                "next input sequence must be positive",
148            ));
149        }
150        Ok(Self {
151            config,
152            next_sequence,
153            acknowledged_sequence: next_sequence - 1,
154            acknowledged_server_tick: 0,
155            history: VecDeque::with_capacity(config.history_capacity),
156        })
157    }
158
159    /// Records one input and returns its assigned sequence.
160    pub fn record(&mut self, target_tick: u64, input: T) -> NetcodeResult<u64> {
161        if target_tick == 0 {
162            return Err(NetcodeError::InvalidInput(
163                "input target Tick must be positive",
164            ));
165        }
166        if self.history.len() >= self.config.history_capacity {
167            return Err(NetcodeError::InputHistoryFull);
168        }
169        let sequence = self.next_sequence;
170        self.next_sequence = self
171            .next_sequence
172            .checked_add(1)
173            .ok_or(NetcodeError::SequenceExhausted)?;
174        self.history.push_back(InputFrame {
175            sequence,
176            target_tick,
177            input,
178        });
179        Ok(sequence)
180    }
181
182    /// Applies a cumulative server acknowledgement and returns the number of released inputs.
183    pub fn acknowledge(&mut self, acknowledgement: InputAck) -> NetcodeResult<usize> {
184        let last_issued = self.next_sequence - 1;
185        if acknowledgement.acknowledged_sequence > last_issued {
186            return Err(NetcodeError::InvalidAcknowledgement);
187        }
188        self.acknowledged_server_tick = self
189            .acknowledged_server_tick
190            .max(acknowledgement.server_tick);
191        if acknowledgement.acknowledged_sequence <= self.acknowledged_sequence {
192            return Ok(0);
193        }
194        self.acknowledged_sequence = acknowledgement.acknowledged_sequence;
195        let before = self.history.len();
196        while self
197            .history
198            .front()
199            .is_some_and(|frame| frame.sequence <= self.acknowledged_sequence)
200        {
201            self.history.pop_front();
202        }
203        Ok(before - self.history.len())
204    }
205
206    /// Returns the highest input sequence cumulatively acknowledged by the server.
207    pub fn acknowledged_sequence(&self) -> u64 {
208        self.acknowledged_sequence
209    }
210
211    /// Returns the highest authoritative server Tick observed in an acknowledgement.
212    pub fn acknowledged_server_tick(&self) -> u64 {
213        self.acknowledged_server_tick
214    }
215
216    /// Returns the number of unacknowledged inputs retained locally.
217    pub fn pending_len(&self) -> usize {
218        self.history.len()
219    }
220
221    /// Returns whether no unacknowledged inputs remain.
222    pub fn is_empty(&self) -> bool {
223        self.history.is_empty()
224    }
225}
226
227impl<T: Clone> InputSender<T> {
228    /// Builds a packet containing the newest configured number of unacknowledged inputs.
229    pub fn packet(&self, client_tick: u64) -> InputPacket<T> {
230        let skip = self.history.len().saturating_sub(self.config.redundancy);
231        InputPacket {
232            client_tick,
233            acknowledged_server_tick: self.acknowledged_server_tick,
234            inputs: self.history.iter().skip(skip).cloned().collect(),
235        }
236    }
237}
238
239/// Result of observing one sequence in a [`SequenceWindow`].
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum SequenceDisposition {
242    /// The sequence had not previously been received.
243    Accepted,
244    /// The sequence was already cumulatively acknowledged or buffered.
245    Duplicate,
246}
247
248/// Bounded, cumulative acknowledgement window safe for out-of-order delivery.
249#[derive(Debug, Clone)]
250pub struct SequenceWindow {
251    reorder_window: u64,
252    acknowledged: u64,
253    pending: BTreeSet<u64>,
254}
255
256impl SequenceWindow {
257    /// Creates a sequence window beginning before sequence one.
258    pub fn new(reorder_window: u64) -> NetcodeResult<Self> {
259        Self::with_acknowledged(reorder_window, 0)
260    }
261
262    /// Creates a sequence window restored from a cumulative acknowledgement.
263    pub fn with_acknowledged(reorder_window: u64, acknowledged: u64) -> NetcodeResult<Self> {
264        if reorder_window == 0 {
265            return Err(NetcodeError::InvalidConfig(
266                "input reorder window must be positive",
267            ));
268        }
269        Ok(Self {
270            reorder_window,
271            acknowledged,
272            pending: BTreeSet::new(),
273        })
274    }
275
276    /// Observes one sequence and advances the cumulative acknowledgement when gaps close.
277    pub fn observe(&mut self, sequence: u64) -> NetcodeResult<SequenceDisposition> {
278        if sequence == 0 {
279            return Err(NetcodeError::InvalidInput(
280                "input sequence must be positive",
281            ));
282        }
283        if sequence <= self.acknowledged || self.pending.contains(&sequence) {
284            return Ok(SequenceDisposition::Duplicate);
285        }
286        if sequence > self.acknowledged.saturating_add(self.reorder_window) {
287            return Err(NetcodeError::InvalidInput(
288                "input sequence exceeds the reorder window",
289            ));
290        }
291        self.pending.insert(sequence);
292        while let Some(next) = self.acknowledged.checked_add(1) {
293            if !self.pending.remove(&next) {
294                break;
295            }
296            self.acknowledged = next;
297        }
298        Ok(SequenceDisposition::Accepted)
299    }
300
301    /// Returns the highest contiguous received sequence.
302    pub fn acknowledged(&self) -> u64 {
303        self.acknowledged
304    }
305
306    /// Returns the number of received sequences waiting above a gap.
307    pub fn pending_len(&self) -> usize {
308        self.pending.len()
309    }
310}
311
312/// Server-side result after validating and de-duplicating one input packet.
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct InputReceiveReport<T> {
315    /// Newly accepted frames, ordered by input sequence.
316    pub accepted: Vec<InputFrame<T>>,
317    /// Number of already received frames ignored from this packet.
318    pub duplicates: usize,
319    /// Client simulation Tick carried by the packet.
320    pub client_tick: u64,
321    /// Newest server Tick acknowledged by the client.
322    pub acknowledged_server_tick: u64,
323    /// Cumulative input acknowledgement to return to the client.
324    pub acknowledgement: InputAck,
325}
326
327/// Server-side packet validator and out-of-order de-duplicator for one client stream.
328#[derive(Debug, Clone)]
329pub struct InputReceiver {
330    config: InputReceiverConfig,
331    sequences: SequenceWindow,
332}
333
334impl InputReceiver {
335    /// Creates a receiver expecting sequence one next.
336    pub fn new(config: InputReceiverConfig) -> NetcodeResult<Self> {
337        Self::with_acknowledged(config, 0)
338    }
339
340    /// Restores a receiver from a previously persisted cumulative acknowledgement.
341    pub fn with_acknowledged(
342        config: InputReceiverConfig,
343        acknowledged: u64,
344    ) -> NetcodeResult<Self> {
345        config.validate()?;
346        Ok(Self {
347            sequences: SequenceWindow::with_acknowledged(config.reorder_window, acknowledged)?,
348            config,
349        })
350    }
351
352    /// Validates a packet at the current server Tick and returns only newly accepted inputs.
353    ///
354    /// Packet processing is transactional: one invalid new frame leaves the receive window
355    /// unchanged. Already received redundant frames are ignored even after their target Tick ages
356    /// out of the configured rollback window.
357    pub fn receive<T>(
358        &mut self,
359        current_server_tick: u64,
360        mut packet: InputPacket<T>,
361    ) -> NetcodeResult<InputReceiveReport<T>> {
362        if packet.inputs.len() > self.config.max_inputs_per_packet {
363            return Err(NetcodeError::InvalidInput(
364                "packet contains too many input frames",
365            ));
366        }
367        if packet.acknowledged_server_tick > current_server_tick {
368            return Err(NetcodeError::InvalidInput(
369                "client acknowledged a future server Tick",
370            ));
371        }
372        packet.inputs.sort_by_key(|frame| frame.sequence);
373        let mut next_sequences = self.sequences.clone();
374        let mut accepted = Vec::with_capacity(packet.inputs.len());
375        let mut duplicates = 0;
376        let earliest = current_server_tick.saturating_sub(self.config.max_past_ticks);
377        let latest = current_server_tick.saturating_add(self.config.max_future_ticks);
378
379        for frame in packet.inputs {
380            match next_sequences.observe(frame.sequence)? {
381                SequenceDisposition::Duplicate => duplicates += 1,
382                SequenceDisposition::Accepted => {
383                    if frame.target_tick == 0
384                        || frame.target_tick < earliest
385                        || frame.target_tick > latest
386                    {
387                        return Err(NetcodeError::InvalidInput(
388                            "input target Tick is outside the receive window",
389                        ));
390                    }
391                    accepted.push(frame);
392                }
393            }
394        }
395
396        self.sequences = next_sequences;
397        Ok(InputReceiveReport {
398            accepted,
399            duplicates,
400            client_tick: packet.client_tick,
401            acknowledged_server_tick: packet.acknowledged_server_tick,
402            acknowledgement: InputAck {
403                server_tick: current_server_tick,
404                acknowledged_sequence: self.sequences.acknowledged(),
405            },
406        })
407    }
408
409    /// Returns the highest contiguous received input sequence.
410    pub fn acknowledged_sequence(&self) -> u64 {
411        self.sequences.acknowledged()
412    }
413
414    /// Returns the number of out-of-order sequences waiting for a gap.
415    pub fn pending_sequences(&self) -> usize {
416        self.sequences.pending_len()
417    }
418}