1use crate::math::{Fix128, Vec3Fix};
47use crate::solver::{PhysicsConfig, PhysicsWorld, RigidBody};
48
49#[cfg(not(feature = "std"))]
50use alloc::{collections::VecDeque, vec, vec::Vec};
51#[cfg(feature = "std")]
52use std::collections::VecDeque;
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub struct FrameInput {
64 pub player_id: u8,
66 pub movement: Vec3Fix,
68 pub actions: u32,
70 pub aim_direction: Vec3Fix,
72}
73
74impl FrameInput {
75 #[inline]
77 #[must_use]
78 pub const fn new(player_id: u8) -> Self {
79 Self {
80 player_id,
81 movement: Vec3Fix::ZERO,
82 actions: 0,
83 aim_direction: Vec3Fix::ZERO,
84 }
85 }
86
87 #[inline]
89 #[must_use]
90 pub const fn with_movement(mut self, movement: Vec3Fix) -> Self {
91 self.movement = movement;
92 self
93 }
94
95 #[inline]
97 #[must_use]
98 pub const fn with_actions(mut self, actions: u32) -> Self {
99 self.actions = actions;
100 self
101 }
102
103 #[inline]
105 #[must_use]
106 pub const fn with_aim(mut self, aim: Vec3Fix) -> Self {
107 self.aim_direction = aim;
108 self
109 }
110
111 #[inline]
113 #[must_use]
114 pub const fn has_action(&self, bit: u32) -> bool {
115 self.actions & bit != 0
116 }
117
118 #[must_use]
120 pub fn to_bytes(&self) -> [u8; 20] {
121 let mut buf = [0u8; 20];
122 buf[0] = self.player_id;
123 debug_assert!(i16::try_from(self.movement.x.hi).is_ok());
125 debug_assert!(i16::try_from(self.movement.y.hi).is_ok());
126 debug_assert!(i16::try_from(self.movement.z.hi).is_ok());
127 let mx = self.movement.x.hi as i16;
128 let my = self.movement.y.hi as i16;
129 let mz = self.movement.z.hi as i16;
130 buf[1..3].copy_from_slice(&mx.to_le_bytes());
131 buf[3..5].copy_from_slice(&my.to_le_bytes());
132 buf[5..7].copy_from_slice(&mz.to_le_bytes());
133 buf[7..11].copy_from_slice(&self.actions.to_le_bytes());
135 let ax = self.aim_direction.x.hi as i16;
137 let ay = self.aim_direction.y.hi as i16;
138 let az = self.aim_direction.z.hi as i16;
139 buf[11..13].copy_from_slice(&ax.to_le_bytes());
140 buf[13..15].copy_from_slice(&ay.to_le_bytes());
141 buf[15..17].copy_from_slice(&az.to_le_bytes());
142 buf[17] = 0;
144 buf[18] = 0;
145 buf[19] = 0;
146 buf
147 }
148
149 #[must_use]
151 pub const fn from_bytes(data: &[u8; 20]) -> Self {
152 let player_id = data[0];
153 let mx = i16::from_le_bytes([data[1], data[2]]);
154 let my = i16::from_le_bytes([data[3], data[4]]);
155 let mz = i16::from_le_bytes([data[5], data[6]]);
156 let actions = u32::from_le_bytes([data[7], data[8], data[9], data[10]]);
157 let ax = i16::from_le_bytes([data[11], data[12]]);
158 let ay = i16::from_le_bytes([data[13], data[14]]);
159 let az = i16::from_le_bytes([data[15], data[16]]);
160
161 Self {
162 player_id,
163 movement: Vec3Fix::from_int(mx as i64, my as i64, mz as i64),
164 actions,
165 aim_direction: Vec3Fix::from_int(ax as i64, ay as i64, az as i64),
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct SimulationChecksum(pub u64);
180
181impl SimulationChecksum {
182 #[must_use]
202 pub fn from_world(world: &PhysicsWorld) -> Self {
203 let blob = world.serialize_state();
204 let mut hash: u64 = 0;
205 for (i, chunk) in blob.chunks(8).enumerate() {
206 let mut word = [0u8; 8];
207 word[..chunk.len()].copy_from_slice(chunk);
208 let mut h = u64::from_le_bytes(word) ^ (i as u64).rotate_left(32);
210 h = (h ^ (h >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
212 h = (h ^ (h >> 27)).wrapping_mul(0x94d049bb133111eb);
213 h ^= h >> 31;
214 hash ^= h;
215 }
216 Self(hash)
217 }
218}
219
220#[derive(Clone, Debug)]
229pub struct SimulationSnapshot {
230 pub frame: u64,
232 pub state: Vec<u8>,
234 pub checksum: SimulationChecksum,
236}
237
238#[derive(Clone, Debug)]
244pub struct NetcodeConfig {
245 pub physics: PhysicsConfig,
247 pub fixed_dt: Fix128,
249 pub max_snapshots: usize,
251 pub checksum_history_len: usize,
253 pub player_count: u8,
255}
256
257impl Default for NetcodeConfig {
258 fn default() -> Self {
259 Self {
260 physics: PhysicsConfig::default(),
261 fixed_dt: Fix128::from_ratio(1, 60),
262 max_snapshots: 10,
263 checksum_history_len: 120,
264 player_count: 2,
265 }
266 }
267}
268
269pub trait InputApplicator {
295 fn apply(
297 &self,
298 world: &mut PhysicsWorld,
299 player_id: u8,
300 body_index: usize,
301 input: &FrameInput,
302 dt: Fix128,
303 );
304}
305
306pub struct DefaultInputApplicator {
308 pub move_speed: Fix128,
310 pub jump_impulse: Fix128,
312}
313
314impl Default for DefaultInputApplicator {
315 fn default() -> Self {
316 Self {
317 move_speed: Fix128::from_int(5),
318 jump_impulse: Fix128::from_int(8),
319 }
320 }
321}
322
323impl InputApplicator for DefaultInputApplicator {
324 fn apply(
325 &self,
326 world: &mut PhysicsWorld,
327 _player_id: u8,
328 body_index: usize,
329 input: &FrameInput,
330 _dt: Fix128,
331 ) {
332 if body_index >= world.bodies.len() {
333 return;
334 }
335 let body = &mut world.bodies[body_index];
336
337 body.velocity.x = input.movement.x * self.move_speed;
339 body.velocity.z = input.movement.z * self.move_speed;
340
341 if input.has_action(0x1) && body.velocity.y.is_zero() {
343 body.velocity.y = self.jump_impulse;
344 }
345 }
346}
347
348pub struct DeterministicSimulation {
366 pub world: PhysicsWorld,
368 frame: u64,
370 dt: Fix128,
372 player_bodies: Vec<Option<usize>>,
374 snapshots: VecDeque<SimulationSnapshot>,
376 max_snapshots: usize,
378 checksum_history: VecDeque<(u64, SimulationChecksum)>,
380 checksum_history_len: usize,
382}
383
384impl DeterministicSimulation {
385 #[must_use]
387 #[allow(clippy::needless_pass_by_value)] pub fn new(config: NetcodeConfig) -> Self {
389 let world = PhysicsWorld::new(config.physics);
390 let player_count = config.player_count as usize;
391
392 Self {
393 world,
394 frame: 0,
395 dt: config.fixed_dt,
396 player_bodies: vec![None; player_count],
397 snapshots: VecDeque::with_capacity(config.max_snapshots + 1),
398 max_snapshots: config.max_snapshots,
399 checksum_history: VecDeque::with_capacity(config.checksum_history_len + 1),
400 checksum_history_len: config.checksum_history_len,
401 }
402 }
403
404 pub fn add_body(&mut self, body: RigidBody) -> usize {
407 self.world.add_body(body)
408 }
409
410 pub fn assign_player_body(&mut self, player_id: u8, body_index: usize) {
412 let pid = player_id as usize;
413 if pid >= self.player_bodies.len() {
414 self.player_bodies.resize(pid + 1, None);
415 }
416 self.player_bodies[pid] = Some(body_index);
417 }
418
419 pub fn advance_frame(&mut self, inputs: &[FrameInput]) -> SimulationChecksum {
427 self.advance_frame_with_applicator(inputs, &DefaultInputApplicator::default())
428 }
429
430 pub fn advance_frame_with_applicator(
432 &mut self,
433 inputs: &[FrameInput],
434 applicator: &dyn InputApplicator,
435 ) -> SimulationChecksum {
436 self.frame += 1;
437
438 for input in inputs {
440 let pid = input.player_id as usize;
441 if let Some(&Some(body_idx)) = self.player_bodies.get(pid) {
442 applicator.apply(&mut self.world, input.player_id, body_idx, input, self.dt);
443 }
444 }
445
446 self.world.step(self.dt);
448
449 let checksum = SimulationChecksum::from_world(&self.world);
451
452 self.checksum_history.push_back((self.frame, checksum));
454 if self.checksum_history.len() > self.checksum_history_len {
455 self.checksum_history.pop_front();
456 }
457
458 checksum
459 }
460
461 pub fn save_snapshot(&mut self) -> &SimulationSnapshot {
467 let state = self.world.serialize_state();
468 let checksum = SimulationChecksum::from_world(&self.world);
469 let snapshot = SimulationSnapshot {
470 frame: self.frame,
471 state,
472 checksum,
473 };
474 self.snapshots.push_back(snapshot);
475 if self.snapshots.len() > self.max_snapshots {
476 self.snapshots.pop_front();
477 }
478 self.snapshots
479 .back()
480 .expect("snapshots should not be empty after push")
481 }
482
483 pub fn load_snapshot(&mut self, frame: u64) -> bool {
489 if let Some(snap) = self.snapshots.iter().find(|s| s.frame == frame) {
490 let state = snap.state.clone();
491 let snap_frame = snap.frame;
492 if self.world.deserialize_state(&state) {
493 self.frame = snap_frame;
494 while self.checksum_history.back().is_some_and(|c| c.0 > frame) {
496 self.checksum_history.pop_back();
497 }
498 return true;
499 }
500 }
501 false
502 }
503
504 #[must_use]
506 pub fn get_snapshot(&self, frame: u64) -> Option<&SimulationSnapshot> {
507 self.snapshots.iter().find(|s| s.frame == frame)
508 }
509
510 #[must_use]
512 pub fn checksum(&self) -> SimulationChecksum {
513 SimulationChecksum::from_world(&self.world)
514 }
515
516 #[inline]
518 #[must_use]
519 pub const fn frame(&self) -> u64 {
520 self.frame
521 }
522
523 #[must_use]
525 pub fn checksum_at(&self, frame: u64) -> Option<SimulationChecksum> {
526 self.checksum_history
527 .iter()
528 .find(|&&(f, _)| f == frame)
529 .map(|&(_, c)| c)
530 }
531
532 #[must_use]
539 pub fn verify_checksum(&self, frame: u64, remote: SimulationChecksum) -> Option<bool> {
540 self.checksum_at(frame).map(|local| local == remote)
541 }
542
543 #[inline]
545 #[must_use]
546 pub const fn dt(&self) -> Fix128 {
547 self.dt
548 }
549
550 #[inline]
552 #[must_use]
553 pub fn snapshot_count(&self) -> usize {
554 self.snapshots.len()
555 }
556}
557
558#[cfg(test)]
563mod tests {
564 use super::*;
565
566 fn make_sim(player_count: u8) -> DeterministicSimulation {
567 let config = NetcodeConfig {
568 player_count,
569 ..Default::default()
570 };
571 let mut sim = DeterministicSimulation::new(config);
572
573 for i in 0..player_count {
575 let body = RigidBody::new_dynamic(Vec3Fix::from_int(i as i64 * 5, 10, 0), Fix128::ONE);
576 let idx = sim.add_body(body);
577 sim.assign_player_body(i, idx);
578 }
579
580 sim.add_body(RigidBody::new_static(Vec3Fix::ZERO));
582
583 sim
584 }
585
586 #[test]
587 fn test_deterministic_two_sims() {
588 let mut sim_a = make_sim(2);
590 let mut sim_b = make_sim(2);
591
592 let inputs = [
593 FrameInput::new(0).with_movement(Vec3Fix::from_int(1, 0, 0)),
594 FrameInput::new(1).with_movement(Vec3Fix::from_int(0, 0, -1)),
595 ];
596
597 for _ in 0..60 {
598 let cs_a = sim_a.advance_frame(&inputs);
599 let cs_b = sim_b.advance_frame(&inputs);
600 assert_eq!(cs_a, cs_b, "Desync at frame {}", sim_a.frame());
601 }
602
603 for i in 0..sim_a.world.bodies.len() {
605 let a = &sim_a.world.bodies[i];
606 let b = &sim_b.world.bodies[i];
607 assert_eq!(a.position.x.hi, b.position.x.hi);
608 assert_eq!(a.position.x.lo, b.position.x.lo);
609 assert_eq!(a.position.y.hi, b.position.y.hi);
610 assert_eq!(a.position.y.lo, b.position.y.lo);
611 assert_eq!(a.position.z.hi, b.position.z.hi);
612 assert_eq!(a.position.z.lo, b.position.z.lo);
613 }
614 }
615
616 #[test]
617 fn test_snapshot_rollback() {
618 let mut sim = make_sim(1);
619 let input = [FrameInput::new(0).with_movement(Vec3Fix::from_int(1, 0, 0))];
620
621 for _ in 0..30 {
623 sim.advance_frame(&input);
624 }
625 sim.save_snapshot();
626 let checksum_at_30 = sim.checksum();
627
628 for _ in 0..30 {
630 sim.advance_frame(&input);
631 }
632 let pos_at_60 = sim.world.bodies[0].position;
633
634 assert!(sim.load_snapshot(30));
636 assert_eq!(sim.frame(), 30);
637 assert_eq!(sim.checksum(), checksum_at_30);
638
639 for _ in 0..30 {
641 sim.advance_frame(&input);
642 }
643 let pos_replayed = sim.world.bodies[0].position;
644
645 assert_eq!(pos_at_60.x.hi, pos_replayed.x.hi);
646 assert_eq!(pos_at_60.y.hi, pos_replayed.y.hi);
647 assert_eq!(pos_at_60.z.hi, pos_replayed.z.hi);
648 }
649
650 #[test]
651 fn test_checksum_verify() {
652 let mut sim = make_sim(1);
653 let input = [FrameInput::new(0)];
654
655 sim.advance_frame(&input);
656 let cs1 = sim.checksum();
657
658 assert_eq!(sim.verify_checksum(1, cs1), Some(true));
660
661 assert_eq!(
663 sim.verify_checksum(1, SimulationChecksum(0xDEADBEEF)),
664 Some(false),
665 );
666
667 assert_eq!(sim.verify_checksum(999, cs1), None);
669 }
670
671 #[test]
672 fn test_frame_input_serialization() {
673 let input = FrameInput::new(3)
674 .with_movement(Vec3Fix::from_int(1, 0, -1))
675 .with_actions(0x05)
676 .with_aim(Vec3Fix::from_int(0, 1, 0));
677
678 let bytes = input.to_bytes();
679 let restored = FrameInput::from_bytes(&bytes);
680
681 assert_eq!(restored.player_id, 3);
682 assert_eq!(restored.actions, 0x05);
683 assert_eq!(restored.movement.x.hi, 1);
684 assert_eq!(restored.movement.z.hi, -1);
685 assert_eq!(restored.aim_direction.y.hi, 1);
686 }
687
688 #[test]
689 fn test_different_inputs_different_checksums() {
690 let mut sim_a = make_sim(1);
691 let mut sim_b = make_sim(1);
692
693 let input_a = [FrameInput::new(0).with_movement(Vec3Fix::from_int(1, 0, 0))];
695 let input_b = [FrameInput::new(0).with_movement(Vec3Fix::from_int(-1, 0, 0))];
696
697 for _ in 0..10 {
698 sim_a.advance_frame(&input_a);
699 sim_b.advance_frame(&input_b);
700 }
701
702 assert_ne!(sim_a.checksum(), sim_b.checksum());
703 }
704
705 #[test]
706 fn test_empty_inputs() {
707 let mut sim = make_sim(2);
708
709 let cs1 = sim.advance_frame(&[]);
711 let cs2 = sim.advance_frame(&[]);
712
713 assert_ne!(cs1, cs2);
715 }
716
717 #[test]
718 fn test_multiple_snapshots() {
719 let mut sim = make_sim(1);
720 let input = [FrameInput::new(0)];
721
722 for f in 0..30 {
724 sim.advance_frame(&input);
725 if (f + 1) % 10 == 0 {
726 sim.save_snapshot();
727 }
728 }
729
730 assert_eq!(sim.snapshot_count(), 3);
731 assert!(sim.get_snapshot(10).is_some());
732 assert!(sim.get_snapshot(20).is_some());
733 assert!(sim.get_snapshot(30).is_some());
734 assert!(sim.get_snapshot(15).is_none());
735
736 assert!(sim.load_snapshot(20));
738 assert_eq!(sim.frame(), 20);
739 }
740}