From eaecbc5b735d9d060858d360bcc0b9c866d4c3ea Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Mon, 20 Jan 2025 10:36:28 -0800
Subject: [PATCH] physics versioning plan

---
 lib/snf/src/bot.rs           | 10 +++++++--
 strafe-client/src/physics.rs | 39 ++++++++++++++++++++++++++++++++++++
 strafe-client/src/session.rs |  2 +-
 3 files changed, 48 insertions(+), 3 deletions(-)

diff --git a/lib/snf/src/bot.rs b/lib/snf/src/bot.rs
index 3712341b..77936aa3 100644
--- a/lib/snf/src/bot.rs
+++ b/lib/snf/src/bot.rs
@@ -28,12 +28,16 @@ pub enum Error{
 /* block types
 
 BLOCK_BOT_HEADER:
-// Tegments are laid out in chronological order,
+// Segments are laid out in chronological order,
 // but block_id is not necessarily in ascending order.
 //
 // This is to place the final segment close to the start of the file,
 // which allows the duration of the bot to be conveniently calculated
 // from the first and last instruction timestamps.
+//
+// Use exact physics version for replay playback
+// Use highest compatible physics version for verification
+u32 physics_version
 u32 num_segments
 for _ in 0..num_segments{
 	i64 time
@@ -61,6 +65,7 @@ struct SegmentHeader{
 #[binrw]
 #[brw(little)]
 struct Header{
+	physics_version:u32,
 	num_segments:u32,
 	#[br(count=num_segments)]
 	segments:Vec<SegmentHeader>,
@@ -151,7 +156,7 @@ impl<R:BinReaderExt> StreamableBot<R>{
 }
 
 const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
-pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
+pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
 	// decide which instructions to put in which segment
 	// write segment 1 to block 1
 	// write segment N to block 2
@@ -251,6 +256,7 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Ite
 	};
 
 	let header=Header{
+		physics_version,
 		num_segments:num_segments as u32,
 		segments,
 	};
diff --git a/strafe-client/src/physics.rs b/strafe-client/src/physics.rs
index b0388764..b969c570 100644
--- a/strafe-client/src/physics.rs
+++ b/strafe-client/src/physics.rs
@@ -14,6 +14,45 @@ use strafesnet_common::integer::{self,vec3,mat3,Planar64,Planar64Vec3,Planar64Ma
 pub use strafesnet_common::physics::{Time,TimeInner};
 use gameplay::ModeState;
 
+// Physics bug fixes can easily desync all bots.
+//
+// When replaying a bot, use the exact physics version which it was recorded with.
+//
+// When validating a new bot, use the latest compatible physics version
+// from the compatiblity matrix, since it may include bugfixes
+// for things like clipping through walls with surgical precision
+// i.e. without breaking bots which don't exploit the bug.
+//
+// Compatible physics versions should be determined empirically via leaderboard resimulation.
+// Compatible physics versions should result in an identical leaderboard state,
+// or the only bots which fail are ones exploiting a surgically patched bug.
+#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
+pub struct PhysicsVersion(u32);
+pub const VERSION:PhysicsVersion=PhysicsVersion(0);
+const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
+	let compat=[0];
+
+	let mut input_version=0;
+	while input_version<compat.len(){
+		// compatible version must be greater that or equal to the input version
+		assert!(input_version as u32<=compat[input_version]);
+		// compatible version must be a version that exists
+		assert!(compat[input_version]<=VERSION.0);
+		input_version+=1;
+	}
+	compat
+};
+pub enum PhysicsVersionError{
+	UnknownPhysicsVersion,
+}
+pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
+	if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
+		Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
+	}else{
+		Err(PhysicsVersionError::UnknownPhysicsVersion)
+	}
+}
+
 pub type Body=crate::body::Body<TimeInner>;
 type MouseState=strafesnet_common::mouse::MouseState<TimeInner>;
 
diff --git a/strafe-client/src/session.rs b/strafe-client/src/session.rs
index d6518e65..1fa0d89d 100644
--- a/strafe-client/src/session.rs
+++ b/strafe-client/src/session.rs
@@ -299,7 +299,7 @@ impl InstructionConsumer<Instruction<'_>> for Session{
 						std::thread::spawn(move ||{
 							std::fs::create_dir_all("replays").unwrap();
 							let file=std::fs::File::create(file_name).unwrap();
-							strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),replay.recording.instructions).unwrap();
+							strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),crate::physics::VERSION.get(),replay.recording.instructions).unwrap();
 						});
 					},
 				}