Skip to main content

proof_engine/networking/
sync.rs

1//! Game-state synchronisation: snapshots, delta encoding, client-side
2//! prediction, lag compensation, and network clock synchronisation.
3//!
4//! ## Design
5//! The server produces a `GameStateSnapshot` every server tick.  Only
6//! changed entities are shipped as `DeltaSnapshot` to save bandwidth.
7//! Clients buffer recent snapshots in a `SnapshotBuffer` and use a
8//! `StateInterpolator` to render smoothly between them.
9//! `ClientPrediction` applies local inputs immediately and reconciles when
10//! the authoritative state arrives.  `LagCompensation` lets the server rewind
11//! its history to the client's perceived point in time for fair hit detection.
12
13use std::collections::VecDeque;
14
15// ─── Vec2 / Vec3 ─────────────────────────────────────────────────────────────
16// Lightweight local types — no glam dependency needed inside this module.
17
18/// 2D vector used for movement directions.
19#[derive(Debug, Clone, Copy, PartialEq, Default)]
20pub struct Vec2 {
21    pub x: f32,
22    pub y: f32,
23}
24
25impl Vec2 {
26    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
27
28    pub fn new(x: f32, y: f32) -> Self { Self { x, y } }
29
30    pub fn length(self) -> f32 {
31        (self.x * self.x + self.y * self.y).sqrt()
32    }
33
34    pub fn normalize(self) -> Self {
35        let len = self.length();
36        if len < f32::EPSILON { Self::ZERO } else { Self { x: self.x / len, y: self.y / len } }
37    }
38
39    pub fn lerp(self, other: Self, t: f32) -> Self {
40        Self {
41            x: self.x + (other.x - self.x) * t,
42            y: self.y + (other.y - self.y) * t,
43        }
44    }
45}
46
47/// 3D vector used for positions, velocities, and rotations.
48#[derive(Debug, Clone, Copy, PartialEq, Default)]
49pub struct Vec3 {
50    pub x: f32,
51    pub y: f32,
52    pub z: f32,
53}
54
55impl Vec3 {
56    pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0 };
57
58    pub fn new(x: f32, y: f32, z: f32) -> Self { Self { x, y, z } }
59
60    pub fn length(self) -> f32 {
61        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
62    }
63
64    pub fn distance(self, other: Self) -> f32 {
65        (self - other).length()
66    }
67
68    pub fn lerp(self, other: Self, t: f32) -> Self {
69        Self {
70            x: self.x + (other.x - self.x) * t,
71            y: self.y + (other.y - self.y) * t,
72            z: self.z + (other.z - self.z) * t,
73        }
74    }
75
76    pub fn add(self, other: Self) -> Self {
77        Self { x: self.x + other.x, y: self.y + other.y, z: self.z + other.z }
78    }
79
80    pub fn scale(self, s: f32) -> Self {
81        Self { x: self.x * s, y: self.y * s, z: self.z * s }
82    }
83}
84
85impl std::ops::Sub for Vec3 {
86    type Output = Self;
87    fn sub(self, rhs: Self) -> Self {
88        Self { x: self.x - rhs.x, y: self.y - rhs.y, z: self.z - rhs.z }
89    }
90}
91
92impl std::ops::Add for Vec3 {
93    type Output = Self;
94    fn add(self, rhs: Self) -> Self {
95        Self { x: self.x + rhs.x, y: self.y + rhs.y, z: self.z + rhs.z }
96    }
97}
98
99impl std::ops::Mul<f32> for Vec3 {
100    type Output = Self;
101    fn mul(self, rhs: f32) -> Self {
102        Self { x: self.x * rhs, y: self.y * rhs, z: self.z * rhs }
103    }
104}
105
106// ─── EntitySnapshot ──────────────────────────────────────────────────────────
107
108/// Complete state of one entity at a specific server tick.
109#[derive(Debug, Clone, PartialEq)]
110pub struct EntitySnapshot {
111    /// Unique entity identifier.
112    pub id:          u64,
113    pub position:    Vec3,
114    pub velocity:    Vec3,
115    /// Euler angles (roll, pitch, yaw) in radians.
116    pub rotation:    Vec3,
117    pub health:      f32,
118    /// Bitfield: alive, grounded, crouching, attacking, etc.
119    pub state_flags: u32,
120    /// Application-specific extra data (e.g. ammo, power-up timer).
121    pub custom:      Vec<u8>,
122}
123
124impl EntitySnapshot {
125    pub fn new(id: u64) -> Self {
126        Self {
127            id, position: Vec3::ZERO, velocity: Vec3::ZERO,
128            rotation: Vec3::ZERO, health: 100.0, state_flags: 0, custom: Vec::new(),
129        }
130    }
131
132    pub fn is_alive(&self) -> bool { self.state_flags & 1 != 0 }
133    pub fn is_grounded(&self) -> bool { self.state_flags & 2 != 0 }
134    pub fn is_crouching(&self) -> bool { self.state_flags & 4 != 0 }
135}
136
137// ─── GameStateSnapshot ───────────────────────────────────────────────────────
138
139/// Authoritative game state for one server tick, ready to be diffed or sent.
140#[derive(Debug, Clone)]
141pub struct GameStateSnapshot {
142    /// Monotonically increasing server tick number.
143    pub tick:      u64,
144    /// Server wall-clock time (seconds since epoch or game start).
145    pub timestamp: f64,
146    pub entities:  Vec<EntitySnapshot>,
147}
148
149impl GameStateSnapshot {
150    pub fn new(tick: u64, timestamp: f64) -> Self {
151        Self { tick, timestamp, entities: Vec::new() }
152    }
153
154    pub fn with_entities(mut self, entities: Vec<EntitySnapshot>) -> Self {
155        self.entities = entities;
156        self
157    }
158
159    /// Find an entity by ID.
160    pub fn entity(&self, id: u64) -> Option<&EntitySnapshot> {
161        self.entities.iter().find(|e| e.id == id)
162    }
163
164    /// Find an entity by ID (mutable).
165    pub fn entity_mut(&mut self, id: u64) -> Option<&mut EntitySnapshot> {
166        self.entities.iter_mut().find(|e| e.id == id)
167    }
168}
169
170// ─── EntityDelta ─────────────────────────────────────────────────────────────
171
172/// Per-field diff for one entity between two consecutive snapshots.
173#[derive(Debug, Clone, PartialEq)]
174pub struct EntityDelta {
175    pub id:               u64,
176    /// `Some(new_position)` if position changed; `None` if unchanged.
177    pub position_delta:   Option<Vec3>,
178    pub velocity_delta:   Option<Vec3>,
179    pub rotation_delta:   Option<Vec3>,
180    pub health_delta:     Option<f32>,
181    pub state_flags:      Option<u32>,
182    pub custom:           Option<Vec<u8>>,
183    /// Whether this entity was newly spawned (not present in base).
184    pub spawned:          bool,
185    /// Whether this entity was destroyed.
186    pub despawned:        bool,
187}
188
189impl EntityDelta {
190    pub fn new(id: u64) -> Self {
191        Self {
192            id, position_delta: None, velocity_delta: None, rotation_delta: None,
193            health_delta: None, state_flags: None, custom: None,
194            spawned: false, despawned: false,
195        }
196    }
197
198    pub fn is_empty(&self) -> bool {
199        self.position_delta.is_none()
200            && self.velocity_delta.is_none()
201            && self.rotation_delta.is_none()
202            && self.health_delta.is_none()
203            && self.state_flags.is_none()
204            && self.custom.is_none()
205            && !self.spawned && !self.despawned
206    }
207}
208
209// ─── DeltaSnapshot ────────────────────────────────────────────────────────────
210
211/// Compact diff between two `GameStateSnapshot` values.
212///
213/// Only entities that changed are included.  The receiver applies the delta
214/// on top of the last ack'd base snapshot to reconstruct the new state.
215#[derive(Debug, Clone)]
216pub struct DeltaSnapshot {
217    /// Tick this delta advances the client to.
218    pub tick:      u64,
219    pub timestamp: f64,
220    /// Which base tick this delta is relative to (last ack'd by client).
221    pub base_tick: u64,
222    pub changed:   Vec<EntityDelta>,
223}
224
225impl DeltaSnapshot {
226    pub fn new(tick: u64, timestamp: f64, base_tick: u64) -> Self {
227        Self { tick, timestamp, base_tick, changed: Vec::new() }
228    }
229
230    /// Build a `DeltaSnapshot` by diffing `base` → `current`.
231    pub fn build(base: &GameStateSnapshot, current: &GameStateSnapshot) -> Self {
232        let mut delta = DeltaSnapshot::new(current.tick, current.timestamp, base.tick);
233
234        // Entities in current
235        for cur in &current.entities {
236            match base.entity(cur.id) {
237                None => {
238                    // Newly spawned
239                    let mut d = EntityDelta::new(cur.id);
240                    d.spawned          = true;
241                    d.position_delta   = Some(cur.position);
242                    d.velocity_delta   = Some(cur.velocity);
243                    d.rotation_delta   = Some(cur.rotation);
244                    d.health_delta     = Some(cur.health);
245                    d.state_flags      = Some(cur.state_flags);
246                    d.custom           = Some(cur.custom.clone());
247                    delta.changed.push(d);
248                }
249                Some(base_ent) => {
250                    let mut d = EntityDelta::new(cur.id);
251                    let thresh = 0.001f32;
252                    if cur.position.distance(base_ent.position) > thresh {
253                        d.position_delta = Some(cur.position - base_ent.position);
254                    }
255                    if cur.velocity.distance(base_ent.velocity) > thresh {
256                        d.velocity_delta = Some(cur.velocity - base_ent.velocity);
257                    }
258                    if cur.rotation.distance(base_ent.rotation) > thresh {
259                        d.rotation_delta = Some(cur.rotation - base_ent.rotation);
260                    }
261                    if (cur.health - base_ent.health).abs() > 0.01 {
262                        d.health_delta = Some(cur.health - base_ent.health);
263                    }
264                    if cur.state_flags != base_ent.state_flags {
265                        d.state_flags = Some(cur.state_flags);
266                    }
267                    if cur.custom != base_ent.custom {
268                        d.custom = Some(cur.custom.clone());
269                    }
270                    if !d.is_empty() {
271                        delta.changed.push(d);
272                    }
273                }
274            }
275        }
276
277        // Despawned entities
278        for base_ent in &base.entities {
279            if current.entity(base_ent.id).is_none() {
280                let mut d = EntityDelta::new(base_ent.id);
281                d.despawned = true;
282                delta.changed.push(d);
283            }
284        }
285
286        delta
287    }
288
289    /// Apply this delta on top of `base`, producing a new full snapshot.
290    pub fn apply(&self, base: &GameStateSnapshot) -> GameStateSnapshot {
291        let mut result = base.clone();
292        result.tick      = self.tick;
293        result.timestamp = self.timestamp;
294
295        for d in &self.changed {
296            if d.despawned {
297                result.entities.retain(|e| e.id != d.id);
298                continue;
299            }
300            if d.spawned {
301                let mut ent = EntitySnapshot::new(d.id);
302                if let Some(p) = d.position_delta { ent.position = p; }
303                if let Some(v) = d.velocity_delta { ent.velocity = v; }
304                if let Some(r) = d.rotation_delta { ent.rotation = r; }
305                if let Some(h) = d.health_delta   { ent.health = h; }
306                if let Some(f) = d.state_flags     { ent.state_flags = f; }
307                if let Some(ref c) = d.custom      { ent.custom = c.clone(); }
308                result.entities.push(ent);
309                continue;
310            }
311            if let Some(ent) = result.entity_mut(d.id) {
312                if let Some(dp) = d.position_delta { ent.position = ent.position + dp; }
313                if let Some(dv) = d.velocity_delta { ent.velocity = ent.velocity + dv; }
314                if let Some(dr) = d.rotation_delta { ent.rotation = ent.rotation + dr; }
315                if let Some(dh) = d.health_delta   { ent.health += dh; }
316                if let Some(f)  = d.state_flags     { ent.state_flags = f; }
317                if let Some(ref c) = d.custom      { ent.custom = c.clone(); }
318            }
319        }
320        result
321    }
322
323    /// Count of entities affected by this delta.
324    pub fn change_count(&self) -> usize { self.changed.len() }
325}
326
327// ─── SnapshotBuffer ───────────────────────────────────────────────────────────
328
329/// Ring buffer of recent game-state snapshots.
330///
331/// Used on both client (for interpolation) and server (for lag compensation).
332pub struct SnapshotBuffer {
333    snapshots: VecDeque<GameStateSnapshot>,
334    max_len:   usize,
335}
336
337impl SnapshotBuffer {
338    pub const DEFAULT_MAX_LEN: usize = 64;
339
340    pub fn new(max_len: usize) -> Self {
341        Self { snapshots: VecDeque::with_capacity(max_len), max_len }
342    }
343
344    pub fn default() -> Self { Self::new(Self::DEFAULT_MAX_LEN) }
345
346    /// Push a new snapshot; evict the oldest if at capacity.
347    pub fn push(&mut self, snap: GameStateSnapshot) {
348        if self.snapshots.len() >= self.max_len {
349            self.snapshots.pop_front();
350        }
351        self.snapshots.push_back(snap);
352    }
353
354    /// Most recent snapshot.
355    pub fn latest(&self) -> Option<&GameStateSnapshot> {
356        self.snapshots.back()
357    }
358
359    /// Oldest stored snapshot.
360    pub fn oldest(&self) -> Option<&GameStateSnapshot> {
361        self.snapshots.front()
362    }
363
364    /// Find the snapshot with the given tick, or the nearest one before it.
365    pub fn at_tick(&self, tick: u64) -> Option<&GameStateSnapshot> {
366        // Snapshots are ordered oldest → newest.
367        let mut best: Option<&GameStateSnapshot> = None;
368        for s in &self.snapshots {
369            if s.tick <= tick {
370                best = Some(s);
371            } else {
372                break;
373            }
374        }
375        best
376    }
377
378    /// Find the two snapshots that bracket `tick` for interpolation.
379    /// Returns `(older, newer)`.
380    pub fn bracket(&self, tick: u64) -> Option<(&GameStateSnapshot, &GameStateSnapshot)> {
381        let snaps: Vec<&GameStateSnapshot> = self.snapshots.iter().collect();
382        for i in 0..snaps.len().saturating_sub(1) {
383            let a = snaps[i];
384            let b = snaps[i + 1];
385            if a.tick <= tick && b.tick >= tick {
386                return Some((a, b));
387            }
388        }
389        None
390    }
391
392    pub fn len(&self) -> usize { self.snapshots.len() }
393    pub fn is_empty(&self) -> bool { self.snapshots.is_empty() }
394    pub fn clear(&mut self) { self.snapshots.clear(); }
395}
396
397// ─── StateInterpolator ────────────────────────────────────────────────────────
398
399/// Smoothly interpolates entity positions between buffered snapshots.
400pub struct StateInterpolator {
401    buffer: SnapshotBuffer,
402    /// Render is behind this many ticks to ensure we always have two samples.
403    interp_delay_ticks: u64,
404}
405
406impl StateInterpolator {
407    pub fn new(interp_delay_ticks: u64) -> Self {
408        Self {
409            buffer: SnapshotBuffer::new(64),
410            interp_delay_ticks,
411        }
412    }
413
414    pub fn push_snapshot(&mut self, snap: GameStateSnapshot) {
415        self.buffer.push(snap);
416    }
417
418    /// Interpolate entity states at `render_tick` (may be fractional via `t`).
419    ///
420    /// `render_tick` is the server tick we want to display.
421    /// Returns a synthetic snapshot with interpolated entity positions.
422    pub fn interpolate(&self, render_tick: u64, t: f32) -> Option<GameStateSnapshot> {
423        let display_tick = render_tick.saturating_sub(self.interp_delay_ticks);
424
425        match self.buffer.bracket(display_tick) {
426            Some((a, b)) => {
427                let tick_range = (b.tick - a.tick) as f32;
428                let local_t = if tick_range > 0.0 {
429                    ((display_tick - a.tick) as f32 + t) / tick_range
430                } else {
431                    0.0
432                };
433                let local_t = local_t.clamp(0.0, 1.0);
434
435                let mut result = GameStateSnapshot::new(display_tick, a.timestamp);
436                for a_ent in &a.entities {
437                    let pos = if let Some(b_ent) = b.entity(a_ent.id) {
438                        a_ent.position.lerp(b_ent.position, local_t)
439                    } else {
440                        a_ent.position
441                    };
442                    let vel = if let Some(b_ent) = b.entity(a_ent.id) {
443                        a_ent.velocity.lerp(b_ent.velocity, local_t)
444                    } else {
445                        a_ent.velocity
446                    };
447                    let rot = if let Some(b_ent) = b.entity(a_ent.id) {
448                        a_ent.rotation.lerp(b_ent.rotation, local_t)
449                    } else {
450                        a_ent.rotation
451                    };
452                    let health = if let Some(b_ent) = b.entity(a_ent.id) {
453                        a_ent.health + (b_ent.health - a_ent.health) * local_t
454                    } else {
455                        a_ent.health
456                    };
457                    let flags = a_ent.state_flags; // discrete — no interpolation
458
459                    result.entities.push(EntitySnapshot {
460                        id: a_ent.id,
461                        position: pos,
462                        velocity: vel,
463                        rotation: rot,
464                        health,
465                        state_flags: flags,
466                        custom: a_ent.custom.clone(),
467                    });
468                }
469                Some(result)
470            }
471            None => {
472                // Dead reckoning: extrapolate from latest snapshot
473                self.dead_reckon(render_tick, t)
474            }
475        }
476    }
477
478    /// Extrapolate entity positions beyond the latest buffered snapshot using
479    /// their velocity.
480    pub fn dead_reckon(&self, render_tick: u64, t: f32) -> Option<GameStateSnapshot> {
481        let latest = self.buffer.latest()?;
482        let dt = ((render_tick.saturating_sub(latest.tick)) as f32 + t) / 60.0; // assume 60 Hz
483
484        let mut result = GameStateSnapshot::new(render_tick, latest.timestamp + dt as f64);
485        for ent in &latest.entities {
486            let predicted_pos = ent.position + ent.velocity * dt;
487            result.entities.push(EntitySnapshot {
488                id: ent.id,
489                position: predicted_pos,
490                velocity: ent.velocity,
491                rotation: ent.rotation,
492                health: ent.health,
493                state_flags: ent.state_flags,
494                custom: ent.custom.clone(),
495            });
496        }
497        Some(result)
498    }
499
500    pub fn buffer(&self) -> &SnapshotBuffer { &self.buffer }
501    pub fn latest_tick(&self) -> Option<u64> { self.buffer.latest().map(|s| s.tick) }
502}
503
504// ─── PlayerInput ─────────────────────────────────────────────────────────────
505
506/// One frame of local player input.
507#[derive(Debug, Clone, PartialEq)]
508pub struct PlayerInput {
509    /// Server tick this input corresponds to.
510    pub tick:     u64,
511    /// Normalised movement direction (-1 to 1 on each axis).
512    pub move_dir: Vec2,
513    pub jump:     bool,
514    /// Bitmask: bit 0=primary fire, 1=secondary fire, 2=reload, 3=interact, etc.
515    pub actions:  u32,
516    /// Yaw angle the player was facing when this input was made.
517    pub facing:   f32,
518}
519
520impl PlayerInput {
521    pub fn new(tick: u64) -> Self {
522        Self { tick, move_dir: Vec2::ZERO, jump: false, actions: 0, facing: 0.0 }
523    }
524
525    pub fn serialize(&self) -> Vec<u8> {
526        let mut out = Vec::with_capacity(32);
527        out.extend_from_slice(&self.tick.to_be_bytes());
528        out.extend_from_slice(&self.move_dir.x.to_bits().to_be_bytes());
529        out.extend_from_slice(&self.move_dir.y.to_bits().to_be_bytes());
530        out.push(self.jump as u8);
531        out.extend_from_slice(&self.actions.to_be_bytes());
532        out.extend_from_slice(&self.facing.to_bits().to_be_bytes());
533        out
534    }
535
536    pub fn deserialize(b: &[u8]) -> Option<Self> {
537        if b.len() < 25 { return None; }
538        let tick     = u64::from_be_bytes(b[0..8].try_into().ok()?);
539        let mx       = f32::from_bits(u32::from_be_bytes(b[8..12].try_into().ok()?));
540        let my       = f32::from_bits(u32::from_be_bytes(b[12..16].try_into().ok()?));
541        let jump     = b[16] != 0;
542        let actions  = u32::from_be_bytes(b[17..21].try_into().ok()?);
543        let facing   = f32::from_bits(u32::from_be_bytes(b[21..25].try_into().ok()?));
544        Some(Self { tick, move_dir: Vec2::new(mx, my), jump, actions, facing })
545    }
546}
547
548// ─── InputBuffer ─────────────────────────────────────────────────────────────
549
550/// Holds un-acknowledged player inputs for prediction reconciliation.
551pub struct InputBuffer {
552    inputs:  VecDeque<PlayerInput>,
553    max_len: usize,
554}
555
556impl InputBuffer {
557    pub const DEFAULT_MAX_LEN: usize = 128;
558
559    pub fn new(max_len: usize) -> Self {
560        Self { inputs: VecDeque::with_capacity(max_len), max_len }
561    }
562
563    pub fn push(&mut self, input: PlayerInput) {
564        if self.inputs.len() >= self.max_len {
565            self.inputs.pop_front();
566        }
567        self.inputs.push_back(input);
568    }
569
570    /// Discard all inputs at or before `acked_tick`.
571    pub fn ack_up_to(&mut self, acked_tick: u64) {
572        while let Some(front) = self.inputs.front() {
573            if front.tick <= acked_tick {
574                self.inputs.pop_front();
575            } else {
576                break;
577            }
578        }
579    }
580
581    /// Inputs not yet acknowledged by the server.
582    pub fn unacked(&self) -> impl Iterator<Item = &PlayerInput> {
583        self.inputs.iter()
584    }
585
586    pub fn len(&self) -> usize { self.inputs.len() }
587    pub fn is_empty(&self) -> bool { self.inputs.is_empty() }
588    pub fn clear(&mut self) { self.inputs.clear(); }
589}
590
591// ─── ClientPrediction ────────────────────────────────────────────────────────
592
593/// Client-side movement prediction with server reconciliation.
594///
595/// The client applies inputs locally before the server confirms them.
596/// When the server snapshot arrives, if our predicted position differs from
597/// the authoritative one, we roll back and replay unacked inputs.
598pub struct ClientPrediction {
599    pub input_buffer:      InputBuffer,
600    /// Predicted entity position (local player).
601    pub predicted_pos:     Vec3,
602    /// Predicted entity velocity.
603    pub predicted_vel:     Vec3,
604    /// Last tick acknowledged by the server.
605    pub last_acked_tick:   u64,
606    /// Correction blend factor per frame (0.1 = smooth over ~10 frames).
607    pub correction_blend:  f32,
608    /// Pending correction offset being smoothly applied.
609    correction_offset:     Vec3,
610    /// Whether we are currently in a correction phase.
611    correcting:            bool,
612    /// Correction threshold below which we snap (metres).
613    snap_threshold:        f32,
614}
615
616impl ClientPrediction {
617    pub fn new() -> Self {
618        Self {
619            input_buffer:    InputBuffer::new(128),
620            predicted_pos:   Vec3::ZERO,
621            predicted_vel:   Vec3::ZERO,
622            last_acked_tick: 0,
623            correction_blend: 0.2,
624            correction_offset: Vec3::ZERO,
625            correcting: false,
626            snap_threshold: 5.0,
627        }
628    }
629
630    /// Apply `input` locally using the provided `simulate` function.
631    /// `simulate(pos, vel, input, dt) -> (new_pos, new_vel)`.
632    pub fn apply_input<F>(&mut self, input: PlayerInput, dt: f32, simulate: F)
633    where F: Fn(Vec3, Vec3, &PlayerInput, f32) -> (Vec3, Vec3) {
634        let (np, nv) = simulate(self.predicted_pos, self.predicted_vel, &input, dt);
635        self.predicted_pos = np;
636        self.predicted_vel = nv;
637        self.input_buffer.push(input);
638    }
639
640    /// Called when an authoritative server state arrives.
641    /// Rolls back to the server position and replays unacked inputs.
642    pub fn reconcile<F>(
643        &mut self,
644        server_pos:  Vec3,
645        server_vel:  Vec3,
646        server_tick: u64,
647        dt:          f32,
648        simulate:    F,
649    ) where F: Fn(Vec3, Vec3, &PlayerInput, f32) -> (Vec3, Vec3) {
650        self.last_acked_tick = server_tick;
651        self.input_buffer.ack_up_to(server_tick);
652
653        // Replay all unacked inputs from the authoritative position
654        let mut pos = server_pos;
655        let mut vel = server_vel;
656        let unacked: Vec<PlayerInput> = self.input_buffer.unacked().cloned().collect();
657        for inp in &unacked {
658            let (np, nv) = simulate(pos, vel, inp, dt);
659            pos = np;
660            vel = nv;
661        }
662
663        // Compute error between our previous prediction and the replayed result
664        let error = pos - self.predicted_pos;
665        let error_dist = error.length();
666
667        if error_dist > self.snap_threshold {
668            // Large error — snap immediately
669            self.predicted_pos = pos;
670            self.predicted_vel = vel;
671            self.correcting    = false;
672            self.correction_offset = Vec3::ZERO;
673        } else if error_dist > 0.001 {
674            // Small error — blend over several frames
675            self.correction_offset = error;
676            self.correcting        = true;
677            self.predicted_pos     = pos;
678            self.predicted_vel     = vel;
679        } else {
680            self.predicted_pos = pos;
681            self.predicted_vel = vel;
682        }
683    }
684
685    /// Advance correction blend each frame.  Returns the visually rendered position.
686    pub fn tick_correction(&mut self) -> Vec3 {
687        if self.correcting {
688            let step = self.correction_offset.scale(self.correction_blend);
689            self.correction_offset = self.correction_offset - step;
690            if self.correction_offset.length() < 0.001 {
691                self.correcting = false;
692                self.correction_offset = Vec3::ZERO;
693            }
694            self.predicted_pos - self.correction_offset
695        } else {
696            self.predicted_pos
697        }
698    }
699
700    pub fn is_correcting(&self) -> bool { self.correcting }
701}
702
703impl Default for ClientPrediction {
704    fn default() -> Self { Self::new() }
705}
706
707// ─── LagCompensation ─────────────────────────────────────────────────────────
708
709/// Server-side lag compensation: rewinds game state to a client's perceived
710/// point in time for accurate hit registration.
711pub struct LagCompensation {
712    history: SnapshotBuffer,
713    /// How many milliseconds of history to keep.
714    max_history_ms: f64,
715    tick_rate_hz: f64,
716}
717
718impl LagCompensation {
719    /// `max_history_ms` should be at least the maximum expected client RTT.
720    pub fn new(max_history_ms: f64, tick_rate_hz: f64) -> Self {
721        // Store enough ticks to cover max_history_ms
722        let max_ticks = ((max_history_ms / 1000.0) * tick_rate_hz).ceil() as usize + 4;
723        Self {
724            history: SnapshotBuffer::new(max_ticks),
725            max_history_ms,
726            tick_rate_hz,
727        }
728    }
729
730    pub fn default_one_second() -> Self {
731        Self::new(1000.0, 60.0)
732    }
733
734    /// Record the authoritative server state each tick.
735    pub fn record(&mut self, snap: GameStateSnapshot) {
736        self.history.push(snap);
737    }
738
739    /// Rewind to the state at `target_tick`.
740    /// Returns `None` if the tick is outside the history window.
741    pub fn rewind_to_tick(&self, target_tick: u64) -> Option<&GameStateSnapshot> {
742        self.history.at_tick(target_tick)
743    }
744
745    /// Rewind to approximate server tick that a client with `rtt_ms` round-trip
746    /// and `client_tick` would have been seeing.
747    pub fn rewind_for_client(&self, client_tick: u64, rtt_ms: f64) -> Option<&GameStateSnapshot> {
748        let ticks_back = (rtt_ms / 1000.0 * self.tick_rate_hz / 2.0).round() as u64;
749        let target_tick = client_tick.saturating_sub(ticks_back);
750        self.rewind_to_tick(target_tick)
751    }
752
753    /// Get the entity state at a particular tick (for hit detection).
754    pub fn entity_at_tick(&self, entity_id: u64, tick: u64) -> Option<&EntitySnapshot> {
755        self.rewind_to_tick(tick)?.entity(entity_id)
756    }
757
758    pub fn oldest_tick(&self) -> Option<u64> {
759        self.history.oldest().map(|s| s.tick)
760    }
761
762    pub fn latest_tick(&self) -> Option<u64> {
763        self.history.latest().map(|s| s.tick)
764    }
765
766    pub fn history_len(&self) -> usize {
767        self.history.len()
768    }
769}
770
771// ─── NetworkClock ─────────────────────────────────────────────────────────────
772
773/// NTP-style network clock: estimates server time from ping round-trips.
774///
775/// Call `record_ping_pong` each time a pong arrives, then query
776/// `server_time(local_time)` to get the best estimate of current server time.
777pub struct NetworkClock {
778    /// Estimated offset: server_time = local_time + offset
779    time_offset: f64,
780    /// Smoothed RTT in seconds.
781    rtt_s:       f64,
782    /// Number of samples taken.
783    samples:     u64,
784    /// EWMA alpha for offset smoothing.
785    alpha:       f64,
786    /// Running correction rate to avoid jumps (seconds per second).
787    correction_rate: f64,
788    correction_remaining: f64,
789}
790
791impl NetworkClock {
792    pub fn new() -> Self {
793        Self {
794            time_offset:           0.0,
795            rtt_s:                 0.05,
796            samples:               0,
797            alpha:                 0.1,
798            correction_rate:       0.001,
799            correction_remaining:  0.0,
800        }
801    }
802
803    /// Call when a pong arrives.
804    ///
805    /// - `send_time_s` — local time (seconds) when we sent the ping.
806    /// - `recv_time_s` — local time (seconds) when we received the pong.
807    /// - `server_send_time_s` — the server's timestamp embedded in the pong.
808    pub fn record_ping_pong(
809        &mut self,
810        send_time_s:        f64,
811        recv_time_s:        f64,
812        server_send_time_s: f64,
813    ) {
814        let rtt = recv_time_s - send_time_s;
815        if rtt <= 0.0 { return; }
816
817        // Update smoothed RTT
818        if self.samples == 0 {
819            self.rtt_s = rtt;
820        } else {
821            self.rtt_s = self.rtt_s * (1.0 - self.alpha) + rtt * self.alpha;
822        }
823
824        // Estimate server time at moment of reception
825        let estimated_server_recv = server_send_time_s + rtt / 2.0;
826        let new_offset = estimated_server_recv - recv_time_s;
827
828        // Smooth the offset
829        if self.samples == 0 {
830            self.time_offset = new_offset;
831        } else {
832            let error = new_offset - self.time_offset;
833            // Queue smooth correction
834            self.correction_remaining += error;
835        }
836        self.samples += 1;
837    }
838
839    /// Advance the clock correction by `dt` seconds.
840    /// Should be called each game frame.
841    pub fn tick(&mut self, dt: f64) {
842        if self.correction_remaining.abs() > f64::EPSILON {
843            let step = self.correction_rate * dt * self.correction_remaining.signum();
844            let step = if step.abs() > self.correction_remaining.abs() {
845                self.correction_remaining
846            } else {
847                step
848            };
849            self.time_offset += step;
850            self.correction_remaining -= step;
851        }
852    }
853
854    /// Best estimate of the current server time given `local_time_s`.
855    pub fn server_time(&self, local_time_s: f64) -> f64 {
856        local_time_s + self.time_offset
857    }
858
859    /// Convert a local time to server tick given tick rate.
860    pub fn to_server_tick(&self, local_time_s: f64, tick_rate_hz: f64) -> u64 {
861        (self.server_time(local_time_s) * tick_rate_hz) as u64
862    }
863
864    pub fn rtt_ms(&self) -> f64 { self.rtt_s * 1000.0 }
865    pub fn offset_s(&self) -> f64 { self.time_offset }
866    pub fn sample_count(&self) -> u64 { self.samples }
867}
868
869impl Default for NetworkClock {
870    fn default() -> Self { Self::new() }
871}
872
873// ─── AuthorityModel ───────────────────────────────────────────────────────────
874
875/// Which peer has authoritative control over a given entity or subsystem.
876#[derive(Debug, Clone, Copy, PartialEq, Eq)]
877pub enum AuthorityModel {
878    /// Server is always right; clients predict but must defer to server.
879    ServerAuthority,
880    /// Client owns the entity; server trusts client (cheating risk).
881    ClientAuthority,
882    /// Negotiated per-property (e.g. physics server-auth, animation client-auth).
883    SharedAuthority,
884}
885
886// ─── Tests ────────────────────────────────────────────────────────────────────
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891
892    fn snap(tick: u64, entities: Vec<EntitySnapshot>) -> GameStateSnapshot {
893        GameStateSnapshot { tick, timestamp: tick as f64 / 60.0, entities }
894    }
895
896    fn ent(id: u64, pos: Vec3) -> EntitySnapshot {
897        EntitySnapshot {
898            id, position: pos, velocity: Vec3::ZERO, rotation: Vec3::ZERO,
899            health: 100.0, state_flags: 1, custom: vec![],
900        }
901    }
902
903    // ── DeltaSnapshot ─────────────────────────────────────────────────────────
904
905    #[test]
906    fn test_delta_snapshot_no_changes() {
907        let e = ent(1, Vec3::new(0.0, 0.0, 0.0));
908        let base    = snap(10, vec![e.clone()]);
909        let current = snap(11, vec![e]);
910        let delta = DeltaSnapshot::build(&base, &current);
911        assert_eq!(delta.change_count(), 0, "no changes expected");
912    }
913
914    #[test]
915    fn test_delta_snapshot_position_change() {
916        let base    = snap(10, vec![ent(1, Vec3::new(0.0, 0.0, 0.0))]);
917        let current = snap(11, vec![ent(1, Vec3::new(1.0, 0.0, 0.0))]);
918        let delta   = DeltaSnapshot::build(&base, &current);
919        assert_eq!(delta.change_count(), 1);
920        let d = &delta.changed[0];
921        assert!(d.position_delta.is_some());
922    }
923
924    #[test]
925    fn test_delta_snapshot_spawn_despawn() {
926        let base    = snap(10, vec![ent(1, Vec3::ZERO)]);
927        let current = snap(11, vec![ent(1, Vec3::ZERO), ent(2, Vec3::new(5.0, 0.0, 0.0))]);
928        let delta   = DeltaSnapshot::build(&base, &current);
929        assert!(delta.changed.iter().any(|d| d.id == 2 && d.spawned));
930
931        // Now entity 1 despawns
932        let base2    = snap(11, vec![ent(1, Vec3::ZERO), ent(2, Vec3::ZERO)]);
933        let current2 = snap(12, vec![ent(2, Vec3::ZERO)]);
934        let delta2 = DeltaSnapshot::build(&base2, &current2);
935        assert!(delta2.changed.iter().any(|d| d.id == 1 && d.despawned));
936    }
937
938    #[test]
939    fn test_delta_apply_roundtrip() {
940        let base    = snap(10, vec![ent(1, Vec3::new(0.0, 0.0, 0.0))]);
941        let target  = snap(11, vec![ent(1, Vec3::new(3.0, 1.0, 2.0))]);
942        let delta   = DeltaSnapshot::build(&base, &target);
943        let applied = delta.apply(&base);
944        let ent_r   = applied.entity(1).unwrap();
945        assert!((ent_r.position.x - 3.0).abs() < 0.001);
946        assert!((ent_r.position.y - 1.0).abs() < 0.001);
947        assert!((ent_r.position.z - 2.0).abs() < 0.001);
948    }
949
950    // ── SnapshotBuffer ────────────────────────────────────────────────────────
951
952    #[test]
953    fn test_snapshot_buffer_capacity() {
954        let mut buf = SnapshotBuffer::new(4);
955        for i in 0..6u64 {
956            buf.push(snap(i, vec![]));
957        }
958        assert_eq!(buf.len(), 4);
959        assert_eq!(buf.oldest().unwrap().tick, 2);
960        assert_eq!(buf.latest().unwrap().tick, 5);
961    }
962
963    #[test]
964    fn test_snapshot_buffer_at_tick() {
965        let mut buf = SnapshotBuffer::new(64);
966        for i in [10u64, 20, 30, 40] {
967            buf.push(snap(i, vec![]));
968        }
969        assert_eq!(buf.at_tick(25).unwrap().tick, 20);
970        assert_eq!(buf.at_tick(40).unwrap().tick, 40);
971        assert!(buf.at_tick(5).is_none());
972    }
973
974    // ── StateInterpolator ─────────────────────────────────────────────────────
975
976    #[test]
977    fn test_interpolator_between_snapshots() {
978        let mut interp = StateInterpolator::new(0);
979        interp.push_snapshot(snap(10, vec![ent(1, Vec3::new(0.0, 0.0, 0.0))]));
980        interp.push_snapshot(snap(20, vec![ent(1, Vec3::new(10.0, 0.0, 0.0))]));
981
982        let result = interp.interpolate(15, 0.0).unwrap();
983        let e      = result.entity(1).unwrap();
984        assert!((e.position.x - 5.0).abs() < 0.1, "expected ~5.0, got {}", e.position.x);
985    }
986
987    // ── PlayerInput serialization ─────────────────────────────────────────────
988
989    #[test]
990    fn test_player_input_roundtrip() {
991        let inp = PlayerInput {
992            tick:     42,
993            move_dir: Vec2::new(0.5, -0.5),
994            jump:     true,
995            actions:  0b1010,
996            facing:   1.57,
997        };
998        let bytes   = inp.serialize();
999        let decoded = PlayerInput::deserialize(&bytes).unwrap();
1000        assert_eq!(decoded.tick, inp.tick);
1001        assert!((decoded.move_dir.x - inp.move_dir.x).abs() < 0.0001);
1002        assert_eq!(decoded.jump, inp.jump);
1003        assert_eq!(decoded.actions, inp.actions);
1004    }
1005
1006    // ── ClientPrediction ──────────────────────────────────────────────────────
1007
1008    #[test]
1009    fn test_client_prediction_reconcile_snaps_large_error() {
1010        let mut pred = ClientPrediction::new();
1011        pred.predicted_pos = Vec3::new(100.0, 0.0, 0.0);
1012        pred.predicted_vel = Vec3::ZERO;
1013
1014        let sim = |_pos: Vec3, vel: Vec3, _inp: &PlayerInput, _dt: f32| (Vec3::new(1.0, 0.0, 0.0), vel);
1015        pred.reconcile(Vec3::new(0.0, 0.0, 0.0), Vec3::ZERO, 5, 0.016, sim);
1016
1017        // Error > snap_threshold → should have snapped
1018        assert!(!pred.is_correcting());
1019    }
1020
1021    #[test]
1022    fn test_client_prediction_reconcile_blends_small_error() {
1023        let mut pred = ClientPrediction::new();
1024        pred.predicted_pos = Vec3::new(1.0, 0.0, 0.0);
1025        let sim = |_pos: Vec3, vel: Vec3, _inp: &PlayerInput, _dt: f32| (Vec3::new(1.0, 0.0, 0.0), vel);
1026        // Server says 1.05 — small error
1027        pred.reconcile(Vec3::new(1.05, 0.0, 0.0), Vec3::ZERO, 0, 0.016, sim);
1028        // Error < snap_threshold but > 0.001 → blending
1029        assert!(pred.is_correcting());
1030    }
1031
1032    // ── LagCompensation ───────────────────────────────────────────────────────
1033
1034    #[test]
1035    fn test_lag_compensation_rewind() {
1036        let mut lc = LagCompensation::default_one_second();
1037        for tick in 0..65u64 {
1038            lc.record(snap(tick, vec![ent(1, Vec3::new(tick as f32, 0.0, 0.0))]));
1039        }
1040        let rewound = lc.rewind_to_tick(10).unwrap();
1041        assert!(rewound.tick <= 10);
1042
1043        let ent_at_10 = lc.entity_at_tick(1, 10).unwrap();
1044        assert!((ent_at_10.position.x - 10.0).abs() < 0.001);
1045    }
1046
1047    // ── NetworkClock ──────────────────────────────────────────────────────────
1048
1049    #[test]
1050    fn test_network_clock_basic_sync() {
1051        let mut clock = NetworkClock::new();
1052        // Simulate: local sends at t=1.0, server is 5s ahead, pong received at t=1.1
1053        let send_t  = 1.0f64;
1054        let recv_t  = 1.1f64;
1055        let srv_t   = 6.05f64; // server time at midpoint of RTT
1056        clock.record_ping_pong(send_t, recv_t, srv_t);
1057        // After one sample, offset should be close to 5.0
1058        let est = clock.server_time(recv_t);
1059        assert!((est - 6.1).abs() < 0.2, "est={est}");
1060    }
1061
1062    #[test]
1063    fn test_network_clock_tick_applies_correction() {
1064        let mut clock = NetworkClock::new();
1065        clock.record_ping_pong(0.0, 0.1, 5.05);
1066        clock.record_ping_pong(1.0, 1.1, 6.05); // second ping, same offset
1067        // Correction should converge; just verify it doesn't panic
1068        for _ in 0..100 {
1069            clock.tick(0.016);
1070        }
1071        assert!(clock.sample_count() >= 2);
1072    }
1073}