Skip to main content

plaza_client_utils/
rollback.rs

1//! Rollback netcode: the deterministic-lockstep building blocks.
2//!
3//! The rest of this crate serves *server-authoritative* play, where the server
4//! decides and the client predicts its own entity. Rollback is the other family:
5//! peers run the *same deterministic simulation*, exchange only inputs, and stay
6//! identical frame for frame. The catch is latency, a peer cannot wait for a
7//! remote input and still feel responsive, so it **predicts** the missing input
8//! (usually: repeat the last one), simulates ahead, and when the real input
9//! arrives and disagrees, **rolls back** to the frame it went wrong and
10//! re-simulates to the present with the correction. Determinism is what makes the
11//! re-simulation land on the same state the remote peer already has.
12//!
13//! Three pieces, smallest first:
14//!
15//! - [`StateHistory`]: a frame-indexed ring of whole-world snapshots. Rollback
16//!   restores one of these and re-simulates forward. Pure save/restore by frame,
17//!   no interpolation (unlike a server's [`crate::interpolation::SnapshotBuffer`],
18//!   which blends between times).
19//! - [`InputTimeline`]: the inputs known for one source, with the missing frames
20//!   predicted by repeating the last confirmed one.
21//! - [`RollbackSession`]: the bundle that wires them together and runs the loop,
22//!   analogous to [`crate::PredictedPlayer`] for the authoritative model. The
23//!   deterministic step is a plain `fn` pointer you supply, so the simulation
24//!   rules stay entirely yours.
25//!
26//! Everything here is pure logic: no clock, no transport, no float assumptions.
27//! You drive it a frame at a time and feed it inputs as they arrive.
28
29use std::collections::VecDeque;
30use std::fmt::Debug;
31
32/// A logical simulation frame. Rollback counts in fixed frames, not wall time:
33/// two peers agree on "frame 900", never on a millisecond.
34pub type Frame = u64;
35
36/// A frame-indexed ring of whole-world state snapshots.
37///
38/// Rollback saves the state at the start of every frame and, on a misprediction,
39/// restores the one at the frame that went wrong. Frames are contiguous: you save
40/// `f`, then `f + 1`, and so on; re-simulation saves the same frames again, which
41/// overwrites in place. Only the most recent `capacity` frames are kept, which is
42/// the maximum distance you can ever roll back.
43#[derive(Debug, Clone)]
44pub struct StateHistory<State: Clone> {
45  ring: VecDeque<State>,
46  base_frame: Frame,
47  capacity: usize,
48  resets: u64,
49}
50
51impl<State: Clone> StateHistory<State> {
52  /// Keeps at most `capacity` frames of history.
53  ///
54  /// # Panics
55  /// Panics if `capacity` is 0.
56  pub fn new(capacity: usize) -> Self {
57    if capacity == 0 {
58      panic!("StateHistory capacity must be greater than 0");
59    }
60    Self {
61      ring: VecDeque::with_capacity(capacity),
62      base_frame: 0,
63      capacity,
64      resets: 0,
65    }
66  }
67
68  /// Records the state at `frame`.
69  ///
70  /// The intended use is contiguous: append `frame == latest + 1`, or overwrite a
71  /// frame already inside the window (re-simulation does this). A save that skips
72  /// ahead of the window resets it, so the buffer never holds a gap.
73  pub fn save(&mut self, frame: Frame, state: State) {
74    if self.ring.is_empty() {
75      self.base_frame = frame;
76      self.ring.push_back(state);
77      return;
78    }
79
80    let end = self.base_frame + self.ring.len() as u64; // one past the newest
81    if frame == end {
82      self.ring.push_back(state);
83      while self.ring.len() > self.capacity {
84        self.ring.pop_front();
85        self.base_frame += 1;
86      }
87    } else if frame >= self.base_frame && frame < end {
88      let idx = (frame - self.base_frame) as usize;
89      self.ring[idx] = state;
90    } else {
91      self.resets += 1;
92      tracing::warn!(?frame, base = self.base_frame, len = self.ring.len(), "StateHistory: non-contiguous save, resetting the window");
93      self.ring.clear();
94      self.base_frame = frame;
95      self.ring.push_back(state);
96    }
97  }
98
99  /// The state saved at `frame`, if still retained. `None` if it was evicted or
100  /// never saved.
101  pub fn restore(&self, frame: Frame) -> Option<State> {
102    if self.ring.is_empty() || frame < self.base_frame {
103      return None;
104    }
105    let idx = (frame - self.base_frame) as usize;
106    self.ring.get(idx).cloned()
107  }
108
109  /// The oldest frame still retained, or `None` if empty.
110  pub fn oldest_frame(&self) -> Option<Frame> {
111    (!self.ring.is_empty()).then_some(self.base_frame)
112  }
113
114  /// The newest frame saved, or `None` if empty.
115  pub fn latest_frame(&self) -> Option<Frame> {
116    (!self.ring.is_empty()).then(|| self.base_frame + self.ring.len() as u64 - 1)
117  }
118
119  pub fn len(&self) -> usize {
120    self.ring.len()
121  }
122
123  pub fn is_empty(&self) -> bool {
124    self.ring.is_empty()
125  }
126
127  pub fn clear(&mut self) {
128    self.ring.clear();
129    self.base_frame = 0;
130  }
131
132  /// How many saves fell outside the window and reset it.
133  ///
134  /// Rollback assumes frames are saved contiguously, so this should stay zero for
135  /// the whole life of a session. Non-zero means the window was thrown away and
136  /// rebuilt from one frame, which silently shortens how far back the session can
137  /// roll: a correction that arrives next frame finds nothing to restore.
138  pub fn resets(&self) -> u64 {
139    self.resets
140  }
141}
142
143/// The inputs known for one input source (one player), by frame, with the gaps
144/// predicted.
145///
146/// A confirmed input is one the source actually produced; an unconfirmed frame is
147/// **predicted** by repeating the last confirmed input at or before it. That is
148/// the standard rollback guess, and it is right whenever a player holds a
149/// direction, which is most of the time. [`RollbackSession`] compares a later
150/// confirmation against what it predicted to decide whether to roll back.
151#[derive(Debug, Clone)]
152pub struct InputTimeline<Input: Clone + Debug> {
153  slots: VecDeque<Option<Input>>,
154  base_frame: Frame,
155  last_confirmed: Option<Frame>,
156  capacity: usize,
157}
158
159impl<Input: Clone + Debug> InputTimeline<Input> {
160  /// Retains inputs across at most `capacity` frames.
161  ///
162  /// # Panics
163  /// Panics if `capacity` is 0.
164  pub fn new(capacity: usize) -> Self {
165    if capacity == 0 {
166      panic!("InputTimeline capacity must be greater than 0");
167    }
168    Self {
169      slots: VecDeque::with_capacity(capacity),
170      base_frame: 0,
171      last_confirmed: None,
172      capacity,
173    }
174  }
175
176  /// Records the real input the source produced for `frame`.
177  ///
178  /// Frames may arrive out of order (a resent input can fill a gap left by a lost
179  /// packet); any missing frames between the window and `frame` are held as gaps
180  /// until they too are confirmed. A frame older than the retained window is
181  /// dropped, it is already past the rollback horizon.
182  pub fn confirm(&mut self, frame: Frame, input: Input) {
183    if self.slots.is_empty() {
184      self.base_frame = frame;
185      self.slots.push_back(Some(input));
186    } else if frame < self.base_frame {
187      return; // older than anything retained: beyond the horizon
188    } else {
189      let end = self.base_frame + self.slots.len() as u64;
190      if frame < end {
191        self.slots[(frame - self.base_frame) as usize] = Some(input);
192      } else {
193        while self.base_frame + (self.slots.len() as u64) < frame {
194          self.slots.push_back(None);
195        }
196        self.slots.push_back(Some(input));
197      }
198      while self.slots.len() > self.capacity {
199        self.slots.pop_front();
200        self.base_frame += 1;
201      }
202    }
203    self.last_confirmed = Some(self.last_confirmed.map_or(frame, |c| c.max(frame)));
204  }
205
206  /// The confirmed input at `frame`, or `None` if that frame is unconfirmed
207  /// (predicted) or outside the window.
208  pub fn confirmed_at(&self, frame: Frame) -> Option<&Input> {
209    if self.slots.is_empty() || frame < self.base_frame {
210      return None;
211    }
212    let idx = (frame - self.base_frame) as usize;
213    self.slots.get(idx).and_then(|s| s.as_ref())
214  }
215
216  /// The most recent confirmed input at or before `frame`: the basis for
217  /// predicting `frame` when it is not itself confirmed.
218  pub fn last_confirmed_at_or_before(&self, frame: Frame) -> Option<&Input> {
219    if self.slots.is_empty() || frame < self.base_frame {
220      return None;
221    }
222    let end = self.base_frame + self.slots.len() as u64;
223    let mut f = frame.min(end - 1);
224    loop {
225      let idx = (f - self.base_frame) as usize;
226      if let Some(input) = self.slots[idx].as_ref() {
227        return Some(input);
228      }
229      if f == self.base_frame {
230        return None;
231      }
232      f -= 1;
233    }
234  }
235
236  /// The newest frame ever confirmed for this source, or `None` if none has been.
237  pub fn last_confirmed_frame(&self) -> Option<Frame> {
238    self.last_confirmed
239  }
240}
241
242/// How a [`RollbackSession`] is set up.
243#[derive(Debug, Clone, Copy)]
244pub struct RollbackConfig {
245  /// The furthest back the session can roll, in frames. It bounds the state and
246  /// input history retained, so it must comfortably exceed the worst prediction
247  /// horizon (round-trip latency in frames). Default 240 (four seconds at 60 fps).
248  pub max_rollback_frames: usize,
249}
250
251impl Default for RollbackConfig {
252  fn default() -> Self {
253    Self { max_rollback_frames: 240 }
254  }
255}
256
257/// The default input predictor: repeat the last confirmed input unchanged.
258///
259/// This is right whenever a player holds their input steady, which dominates most
260/// games, and it is what a session uses unless [`RollbackSession::with_predictor`]
261/// supplies another rule.
262pub fn repeat_last_input<Input: Clone>(last: &Input, _frame: Frame) -> Input {
263  last.clone()
264}
265
266/// The whole rollback loop for one peer, wired.
267///
268/// It owns a [`StateHistory`], an [`InputTimeline`] per player, and the current
269/// frame, and drives the predict / detect / rollback / re-simulate cycle against
270/// a deterministic step you supply. The primitives stay public for anyone who
271/// wants to wire the loop differently; this is the ready-made path, the rollback
272/// counterpart to [`crate::PredictedPlayer`].
273///
274/// Each peer runs its own session and calls its local player index the "local"
275/// one; the two are otherwise identical: both re-simulate to the same state
276/// from the same inputs.
277///
278/// ```ignore
279/// // Two players; the deterministic step advances the shared world one frame.
280/// let mut session = RollbackSession::new(initial_world, vec![NEUTRAL, NEUTRAL],
281///                                        RollbackConfig::default(), step);
282///
283/// // Each frame: feed the local input, fold in any remote inputs that arrived,
284/// // then advance (which rolls back first if a remote input disproved a guess).
285/// session.queue_local_input(LOCAL, my_input);
286/// for (frame, input) in inbox.drain() { session.confirm_remote_input(REMOTE, frame, input); }
287/// session.advance_frame();
288/// draw(session.state());
289/// ```
290pub struct RollbackSession<State: Clone + Debug, Input: Clone + Debug + PartialEq> {
291  state_history: StateHistory<State>,
292  /// What input was actually fed to each player for each simulated frame, so a
293  /// later confirmation can be checked against the guess that was used.
294  used: Vec<StateHistory<Input>>,
295  timelines: Vec<InputTimeline<Input>>,
296  /// The neutral input assumed for a player before it has confirmed anything.
297  neutral: Vec<Input>,
298
299  current_state: State,
300  head_frame: Frame,
301
302  advance: fn(&State, &[Input]) -> State,
303  predictor: fn(&Input, Frame) -> Input,
304
305  rollback_enabled: bool,
306  earliest_incorrect: Option<Frame>,
307  last_rollback_len: usize,
308  max_rollback_len: usize,
309  rollback_count: u64,
310}
311
312impl<State: Clone + Debug, Input: Clone + Debug + PartialEq> RollbackSession<State, Input> {
313  /// Creates a session over `neutral_inputs.len()` players, starting from
314  /// `initial_state`. `neutral_inputs[p]` is the input assumed for player `p`
315  /// before any of its inputs are known (typically "no input"). `advance` is the
316  /// deterministic step: same state and inputs in, same state out, every time and
317  /// on every peer, which is what rollback rests on.
318  pub fn new(initial_state: State, neutral_inputs: Vec<Input>, config: RollbackConfig, advance: fn(&State, &[Input]) -> State) -> Self {
319    let cap = config.max_rollback_frames.max(1);
320    let players = neutral_inputs.len();
321    Self {
322      state_history: StateHistory::new(cap),
323      used: (0..players).map(|_| StateHistory::new(cap)).collect(),
324      timelines: (0..players).map(|_| InputTimeline::new(cap)).collect(),
325      neutral: neutral_inputs,
326      current_state: initial_state,
327      head_frame: 0,
328      advance,
329      predictor: repeat_last_input,
330      rollback_enabled: true,
331      earliest_incorrect: None,
332      last_rollback_len: 0,
333      max_rollback_len: 0,
334      rollback_count: 0,
335    }
336  }
337
338  /// Replaces the input predictor (default: [`repeat_last_input`]). A predictor
339  /// takes the last confirmed input and the frame being predicted and returns the
340  /// guess for that frame.
341  pub fn with_predictor(mut self, predictor: fn(&Input, Frame) -> Input) -> Self {
342    self.predictor = predictor;
343    self
344  }
345
346  pub fn num_players(&self) -> usize {
347    self.timelines.len()
348  }
349
350  /// The next frame to be simulated. [`state`](Self::state) is the world at the
351  /// start of this frame, i.e. after every frame before it.
352  pub fn current_frame(&self) -> Frame {
353    self.head_frame
354  }
355
356  /// The world as it stands now: the present the peer renders. Includes the effect
357  /// of every predicted input still awaiting confirmation.
358  pub fn state(&self) -> &State {
359    &self.current_state
360  }
361
362  /// The world at the start of `frame`, if still retained. This is the *saved*
363  /// state, so for a fully-confirmed frame it is identical on every peer.
364  /// Comparing two peers here shows whether they are in sync. Returns the
365  /// present for the current frame.
366  pub fn state_at(&self, frame: Frame) -> Option<State> {
367    if frame == self.head_frame {
368      return Some(self.current_state.clone());
369    }
370    self.state_history.restore(frame)
371  }
372
373  /// Turns rollback on or off (on by default). With it off the session still
374  /// predicts and advances, but never restores or re-simulates: it trusts every
375  /// guess permanently. Do not ship with it off, because predictions that are
376  /// never corrected drift a peer out of sync. It is useful for measuring what
377  /// rollback contributes. A delay-based front end also turns rollback off with
378  /// it when it waits for inputs instead of predicting them.
379  pub fn set_rollback_enabled(&mut self, enabled: bool) {
380    self.rollback_enabled = enabled;
381  }
382
383  /// Supplies the local player's input for the current frame. Local inputs are
384  /// known before their frame runs, so they are never mispredicted. Call once per
385  /// frame before [`advance_frame`](Self::advance_frame).
386  pub fn queue_local_input(&mut self, player: usize, input: Input) {
387    self.timelines[player].confirm(self.head_frame, input);
388  }
389
390  /// Folds in a remote input that has arrived for a past or current `frame`. If it
391  /// contradicts the guess already used for an *already-simulated* frame, the
392  /// session marks that frame for rollback on the next
393  /// [`advance_frame`](Self::advance_frame).
394  pub fn confirm_remote_input(&mut self, player: usize, frame: Frame, input: Input) {
395    self.timelines[player].confirm(frame, input.clone());
396
397    if frame < self.head_frame
398      && let Some(used) = self.used[player].restore(frame)
399      && used != input
400    {
401      self.earliest_incorrect = Some(self.earliest_incorrect.map_or(frame, |cur| cur.min(frame)));
402    }
403  }
404
405  /// Advances the simulation by one frame: first rolls back and re-simulates if a
406  /// confirmation disproved a guess, then simulates the current frame, predicting
407  /// any input not yet known.
408  pub fn advance_frame(&mut self) {
409    self.resolve_rollback();
410    let f = self.head_frame;
411    self.simulate_frame(f);
412    self.head_frame += 1;
413  }
414
415  /// Whether every player's input for `frame` is confirmed (none predicted). A
416  /// delay-based peer waits for this before advancing; a rollback peer ignores it
417  /// and predicts. Which to do is the app's policy, so this only reports.
418  pub fn is_frame_confirmed(&self, frame: Frame) -> bool {
419    self.timelines.iter().all(|t| t.confirmed_at(frame).is_some())
420  }
421
422  /// The newest frame this player's input is confirmed through, or `None`.
423  pub fn confirmed_frame(&self, player: usize) -> Option<Frame> {
424    self.timelines[player].last_confirmed_frame()
425  }
426
427  /// How many frames the present is running ahead of the least-confirmed player:
428  /// the depth of prediction currently exposed to a rollback. Zero when every
429  /// input is known up to the last simulated frame.
430  pub fn prediction_horizon(&self) -> usize {
431    if self.head_frame == 0 {
432      return 0;
433    }
434    let last_simulated = self.head_frame - 1;
435    (0..self.timelines.len())
436      .map(|p| match self.timelines[p].last_confirmed_frame() {
437        Some(c) if c >= last_simulated => 0,
438        Some(c) => (last_simulated - c) as usize,
439        None => self.head_frame as usize,
440      })
441      .max()
442      .unwrap_or(0)
443  }
444
445  /// Frames re-simulated by the most recent [`advance_frame`](Self::advance_frame)
446  /// (0 if it did not roll back).
447  pub fn last_rollback_frames(&self) -> usize {
448    self.last_rollback_len
449  }
450
451  /// The deepest rollback seen so far, in frames.
452  pub fn max_rollback_frames(&self) -> usize {
453    self.max_rollback_len
454  }
455
456  /// How many times the session has rolled back.
457  pub fn rollback_count(&self) -> u64 {
458    self.rollback_count
459  }
460
461  /// Restores the earliest mispredicted frame and re-simulates to the present.
462  fn resolve_rollback(&mut self) {
463    self.last_rollback_len = 0;
464    let Some(mut earliest) = self.earliest_incorrect.take() else {
465      return;
466    };
467    if !self.rollback_enabled {
468      return; // guess kept, never corrected: the "why rollback" comparison
469    }
470    if earliest >= self.head_frame {
471      return; // nothing simulated yet went wrong
472    }
473    // Clamp to what is still retained: a correction older than the history can no
474    // longer be applied exactly. Re-simulating from the oldest kept frame is the
475    // closest the session can get, and keeps it from diverging further.
476    match self.state_history.oldest_frame() {
477      Some(oldest) if earliest < oldest => earliest = oldest,
478      None => return,
479      _ => {}
480    }
481    let Some(restored) = self.state_history.restore(earliest) else {
482      return;
483    };
484
485    let target = self.head_frame; // re-simulate [earliest, target)
486    self.current_state = restored;
487    let mut f = earliest;
488    while f < target {
489      self.simulate_frame(f);
490      f += 1;
491    }
492
493    let len = (target - earliest) as usize;
494    self.last_rollback_len = len;
495    self.max_rollback_len = self.max_rollback_len.max(len);
496    self.rollback_count += 1;
497  }
498
499  /// Simulates one frame from `current_state`: saves the pre-state, gathers each
500  /// player's input (confirmed or predicted), records what it used, and steps.
501  fn simulate_frame(&mut self, frame: Frame) {
502    self.state_history.save(frame, self.current_state.clone());
503
504    let mut inputs = Vec::with_capacity(self.timelines.len());
505    for p in 0..self.timelines.len() {
506      let input = self.input_for(p, frame);
507      self.used[p].save(frame, input.clone());
508      inputs.push(input);
509    }
510    self.current_state = (self.advance)(&self.current_state, &inputs);
511  }
512
513  /// Player `p`'s input for `frame`: the confirmed value if known, otherwise the
514  /// predictor applied to the last confirmed input (or the neutral input if the
515  /// player has confirmed nothing yet).
516  fn input_for(&self, p: usize, frame: Frame) -> Input {
517    if let Some(confirmed) = self.timelines[p].confirmed_at(frame) {
518      return confirmed.clone();
519    }
520    let basis = self.timelines[p].last_confirmed_at_or_before(frame).cloned().unwrap_or_else(|| self.neutral[p].clone());
521    (self.predictor)(&basis, frame)
522  }
523}
524
525#[cfg(test)]
526mod tests {
527  use super::*;
528
529  // A tiny deterministic world: each player's position is an integer, moved by an
530  // integer input. Integers keep re-simulation exactly comparable, no float drift.
531  #[derive(Clone, Copy, Debug, PartialEq)]
532  struct World {
533    pos: [i64; 2],
534  }
535
536  #[derive(Clone, Copy, Debug, PartialEq)]
537  struct In(i64);
538
539  const NEUTRAL: In = In(0);
540
541  fn step(state: &World, inputs: &[In]) -> World {
542    let mut next = *state;
543    next.pos[0] += inputs[0].0;
544    next.pos[1] += inputs[1].0;
545    next
546  }
547
548  fn session() -> RollbackSession<World, In> {
549    RollbackSession::new(World { pos: [0, 0] }, vec![NEUTRAL, NEUTRAL], RollbackConfig { max_rollback_frames: 64 }, step)
550  }
551
552  /// The predictor swap and the readouts.
553  ///
554  /// A rollback session's cost is mostly how often it has to re-simulate,
555  /// which depends on how good the guess was. The default repeats the last
556  /// input, which is right for a held direction and wrong for anything that
557  /// alternates, so it needs to be replaceable.
558  mod the_predictor {
559    use super::*;
560
561    /// Always guesses neutral, which is wrong whenever the peer is holding a
562    /// direction: the pessimistic predictor, for measuring against.
563    fn always_neutral(_last: &In, _frame: Frame) -> In {
564      NEUTRAL
565    }
566
567    /// Runs a session where the peer holds a steady input that arrives late,
568    /// and reports how many rollbacks it cost.
569    fn rollbacks_with(predictor: Option<fn(&In, Frame) -> In>) -> u64 {
570      let mut s = session();
571      if let Some(predictor) = predictor {
572        s = s.with_predictor(predictor);
573      }
574      for frame in 0..20u64 {
575        s.queue_local_input(0, In(1));
576        // The peer has been holding the same direction throughout, and each
577        // confirmation lands two frames after the fact.
578        if frame >= 2 {
579          s.confirm_remote_input(1, frame - 2, In(1));
580        }
581        s.advance_frame();
582      }
583      s.rollback_count()
584    }
585
586    #[test]
587    fn a_predictor_that_guesses_right_costs_fewer_rollbacks() {
588      // Repeating the last input is exactly right for a peer holding a
589      // direction and guessing neutral is wrong every frame.
590      let repeating = rollbacks_with(None);
591      let neutral = rollbacks_with(Some(always_neutral));
592      assert!(
593        neutral > repeating,
594        "the wrong guess re-simulates more: {neutral} against {repeating}"
595      );
596    }
597
598    #[test]
599    fn the_frame_counters_agree_with_what_was_driven() {
600      // `current_frame`, `confirmed_frame` and `num_players` are what an
601      // application shows and branches on.
602      let mut s = session();
603      assert_eq!(s.num_players(), 2);
604      assert_eq!(s.current_frame(), 0);
605      assert_eq!(s.confirmed_frame(1), None, "nothing confirmed yet");
606
607      for frame in 0..5u64 {
608        s.queue_local_input(0, In(1));
609        s.confirm_remote_input(1, frame, In(0));
610        s.advance_frame();
611      }
612
613      assert_eq!(s.current_frame(), 5, "five frames driven, five frames on");
614      assert_eq!(s.confirmed_frame(1), Some(4), "and the peer is confirmed through the last");
615    }
616  }
617
618  // A ground-truth simulation with every input known up front, for comparison.
619  fn ground_truth(inputs0: &[In], inputs1: &[In]) -> World {
620    let mut w = World { pos: [0, 0] };
621    for f in 0..inputs0.len() {
622      w = step(&w, &[inputs0[f], inputs1[f]]);
623    }
624    w
625  }
626
627  #[test]
628  fn state_history_saves_restores_and_evicts_by_frame() {
629    let mut h = StateHistory::new(3);
630    h.save(0, 10);
631    h.save(1, 11);
632    h.save(2, 12);
633    assert_eq!(h.restore(1), Some(11));
634    assert_eq!(h.oldest_frame(), Some(0));
635    assert_eq!(h.latest_frame(), Some(2));
636
637    h.save(3, 13); // evicts frame 0
638    assert_eq!(h.restore(0), None);
639    assert_eq!(h.oldest_frame(), Some(1));
640    assert_eq!(h.restore(3), Some(13));
641  }
642
643  #[test]
644  fn state_history_overwrites_a_frame_in_place() {
645    let mut h = StateHistory::new(4);
646    h.save(0, 1);
647    h.save(1, 2);
648    h.save(1, 99); // re-simulation re-saves the same frame
649    assert_eq!(h.restore(1), Some(99));
650    assert_eq!(h.latest_frame(), Some(1), "overwriting does not extend the window");
651  }
652
653  #[test]
654  fn a_non_contiguous_save_is_counted_because_it_shortens_the_reach() {
655    // Rollback assumes contiguous saves, so this should stay zero for a whole
656    // session. Non-zero means the window was thrown away and rebuilt from one
657    // frame, and a correction arriving next frame finds nothing to restore.
658    let mut h = StateHistory::new(4);
659    h.save(0, 1);
660    h.save(1, 2);
661    assert_eq!(h.resets(), 0);
662    h.save(50, 3);
663    assert_eq!(h.resets(), 1);
664    assert_eq!(h.oldest_frame(), Some(50), "and the reach is now one frame");
665    assert_eq!(h.restore(1), None);
666  }
667
668  #[test]
669  fn input_timeline_predicts_the_last_confirmed() {
670    let mut t = InputTimeline::new(8);
671    t.confirm(0, In(5));
672    t.confirm(1, In(5));
673    assert_eq!(t.confirmed_at(1), Some(&In(5)));
674    assert_eq!(t.confirmed_at(2), None, "frame 2 is unconfirmed");
675    // The basis for predicting frame 5 is the last confirmed at or before it.
676    assert_eq!(t.last_confirmed_at_or_before(5), Some(&In(5)));
677    assert_eq!(t.last_confirmed_frame(), Some(1));
678  }
679
680  #[test]
681  fn a_correct_prediction_never_rolls_back() {
682    // The remote holds In(2) the whole time. Once repeat-last has a basis to
683    // repeat, it predicts the held input exactly, so lagging confirmations never
684    // contradict a guess. The basis is the first confirmed input, so frame 0 is
685    // known before it runs (both peers start from a known first input).
686    let mut s = session();
687    let inputs = [In(2); 10];
688    s.confirm_remote_input(1, 0, inputs[0]);
689    for f in 0..10u64 {
690      s.queue_local_input(0, In(1));
691      // Confirm the rest three frames late.
692      if f >= 3 {
693        s.confirm_remote_input(1, f - 3, inputs[(f - 3) as usize]);
694      }
695      s.advance_frame();
696    }
697    assert_eq!(s.rollback_count(), 0, "a held remote input is predicted exactly");
698    assert_eq!(s.last_rollback_frames(), 0);
699  }
700
701  #[test]
702  fn a_wrong_prediction_rolls_back_and_lands_on_the_truth() {
703    // The remote changes direction at frame 4, which repeat-last cannot foresee.
704    let remote: Vec<In> = (0..10).map(|f| if f < 4 { In(1) } else { In(-3) }).collect();
705    let local = vec![In(1); 10];
706
707    let mut s = session();
708    for f in 0..10u64 {
709      s.queue_local_input(0, local[f as usize]);
710      // The remote input for frame f arrives two frames late.
711      if f >= 2 {
712        let past = f - 2;
713        s.confirm_remote_input(1, past, remote[past as usize]);
714      }
715      s.advance_frame();
716    }
717    // Drain the last in-flight confirmations and let it settle.
718    for f in 8..10u64 {
719      s.confirm_remote_input(1, f, remote[f as usize]);
720    }
721    s.resolve_rollback();
722
723    assert!(s.rollback_count() > 0, "the direction change forced at least one rollback");
724    // After every input is known, the re-simulated present equals the world that
725    // had every input from the start.
726    let truth = ground_truth(&local, &remote);
727    assert_eq!(*s.state(), truth, "rollback converged on the ground truth");
728  }
729
730  #[test]
731  fn two_peers_exchanging_inputs_converge_to_the_same_world() {
732    // Two independent sessions, each local to one player, predict the other
733    // player and roll back. With every input eventually delivered they must
734    // agree with each other and with ground truth.
735    let p0: Vec<In> = (0..40).map(|f| In(((f * 7) % 5) as i64 - 2)).collect();
736    let p1: Vec<In> = (0..40).map(|f| In(((f * 3) % 4) as i64 - 1)).collect();
737
738    let mut a = session(); // peer A: local player 0
739    let mut b = session(); // peer B: local player 1
740
741    // A two-frame delay each way, delivered in order.
742    let delay = 2u64;
743    for f in 0..40u64 {
744      a.queue_local_input(0, p0[f as usize]);
745      b.queue_local_input(1, p1[f as usize]);
746
747      if f >= delay {
748        let past = f - delay;
749        a.confirm_remote_input(1, past, p1[past as usize]); // B's input reaches A
750        b.confirm_remote_input(0, past, p0[past as usize]); // A's input reaches B
751      }
752      a.advance_frame();
753      b.advance_frame();
754    }
755    // Flush the inputs still in flight when the loop ended.
756    for f in 38..40u64 {
757      a.confirm_remote_input(1, f, p1[f as usize]);
758      b.confirm_remote_input(0, f, p0[f as usize]);
759    }
760    a.resolve_rollback();
761    b.resolve_rollback();
762
763    let truth = ground_truth(&p0, &p1);
764    assert_eq!(*a.state(), truth, "peer A converged");
765    assert_eq!(*b.state(), truth, "peer B converged");
766    assert_eq!(a.state(), b.state(), "the two peers agree frame for frame");
767  }
768
769  #[test]
770  fn delay_based_policy_can_tell_when_a_frame_is_fully_known() {
771    let mut s = session();
772    s.queue_local_input(0, In(1));
773    assert!(!s.is_frame_confirmed(0), "the remote input for frame 0 is not in yet");
774    s.confirm_remote_input(1, 0, In(1));
775    assert!(s.is_frame_confirmed(0), "now every player's frame-0 input is known");
776  }
777
778  #[test]
779  fn a_correction_older_than_the_history_does_not_panic() {
780    // Roll the window forward well past a frame, then confirm a contradicting
781    // input for that long-evicted frame. It cannot roll back that far; it must
782    // clamp to the oldest retained frame rather than panic or corrupt state.
783    let mut s = RollbackSession::new(World { pos: [0, 0] }, vec![NEUTRAL, NEUTRAL], RollbackConfig { max_rollback_frames: 4 }, step);
784    for f in 0..20u64 {
785      s.queue_local_input(0, In(1));
786      s.confirm_remote_input(1, f, In(1)); // predicted correctly along the way
787      s.advance_frame();
788    }
789    // A late, contradicting confirmation for frame 0, long since evicted.
790    s.confirm_remote_input(1, 0, In(99));
791    s.advance_frame(); // must not panic
792    assert!(s.state().pos[0].is_positive() || s.state().pos[0] == 0);
793    let _ = s.state();
794  }
795
796  #[test]
797  fn rollback_disabled_keeps_a_wrong_guess_and_diverges_from_the_truth() {
798    // The same direction change as the rollback test, but with rollback off: the
799    // misprediction is detected and then ignored, so the present never lands on
800    // the ground truth.
801    let remote: Vec<In> = (0..10).map(|f| if f < 4 { In(1) } else { In(-3) }).collect();
802    let local = vec![In(1); 10];
803
804    let mut s = session();
805    s.set_rollback_enabled(false);
806    for f in 0..10u64 {
807      s.queue_local_input(0, local[f as usize]);
808      if f >= 2 {
809        let past = f - 2;
810        s.confirm_remote_input(1, past, remote[past as usize]);
811      }
812      s.advance_frame();
813    }
814    for f in 8..10u64 {
815      s.confirm_remote_input(1, f, remote[f as usize]);
816    }
817    s.resolve_rollback();
818
819    assert_eq!(s.rollback_count(), 0, "rollback disabled never re-simulates");
820    let truth = ground_truth(&local, &remote);
821    assert_ne!(*s.state(), truth, "without rollback the trusted guess never converges");
822  }
823
824  #[test]
825  fn state_at_a_confirmed_frame_matches_across_two_peers() {
826    // Two peers, predictions and rollbacks along the way. At a frame both have
827    // fully confirmed, their saved states are identical.
828    let p0: Vec<In> = (0..30).map(|f| In(((f * 5) % 3) as i64 - 1)).collect();
829    let p1: Vec<In> = (0..30).map(|f| In(((f * 2) % 3) as i64 - 1)).collect();
830
831    let mut a = session();
832    let mut b = session();
833    for f in 0..30u64 {
834      a.queue_local_input(0, p0[f as usize]);
835      b.queue_local_input(1, p1[f as usize]);
836      if f >= 3 {
837        a.confirm_remote_input(1, f - 3, p1[(f - 3) as usize]);
838        b.confirm_remote_input(0, f - 3, p0[(f - 3) as usize]);
839      }
840      a.advance_frame();
841      b.advance_frame();
842    }
843    // A frame both peers have every input for (confirmed through f-1 at f=30 loop
844    // end, minus the 3-frame delay still in flight): pick a safely-confirmed one.
845    let cf = 20;
846    let sa = a.state_at(cf).expect("A retains the frame");
847    let sb = b.state_at(cf).expect("B retains the frame");
848    assert_eq!(sa, sb, "a fully-confirmed frame is identical on both peers");
849  }
850
851  #[test]
852  fn the_prediction_horizon_reflects_how_far_ahead_of_confirmation_it_is() {
853    let mut s = session();
854    for _ in 0..6u64 {
855      s.queue_local_input(0, In(1));
856      s.advance_frame();
857    }
858    // Local player is always confirmed; the remote has confirmed nothing, so the
859    // horizon spans every simulated frame.
860    assert_eq!(s.prediction_horizon(), 6);
861    s.confirm_remote_input(1, 4, In(0));
862    assert_eq!(s.prediction_horizon(), 1, "confirmed through frame 4, one frame ahead");
863  }
864}