From 4c1198098966f4a9c2ad1901bbaee968d2703705 Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Sat, 18 Jan 2025 01:13:45 -0800 Subject: [PATCH] headless replay test --- lib/common/src/run.rs | 6 ++ strafe-client/src/physics.rs | 204 ++++++++++++++++++++++++++++++++++- strafe-client/src/session.rs | 2 +- 3 files changed, 210 insertions(+), 2 deletions(-) diff --git a/lib/common/src/run.rs b/lib/common/src/run.rs index 2b21060f..622ba4e5 100644 --- a/lib/common/src/run.rs +++ b/lib/common/src/run.rs @@ -110,4 +110,10 @@ impl Run{ self.flagged=Some(flag_reason); } } + pub fn get_finish_time(&self)->Option<Time>{ + match &self.state{ + RunState::Finished{timer}=>Some(timer.time()), + _=>None, + } + } } diff --git a/strafe-client/src/physics.rs b/strafe-client/src/physics.rs index b969c570..f2f87089 100644 --- a/strafe-client/src/physics.rs +++ b/strafe-client/src/physics.rs @@ -900,6 +900,9 @@ impl PhysicsState{ pub const fn mode(&self)->gameplay_modes::ModeId{ self.mode_state.get_mode_id() } + pub fn get_finish_time(&self)->Option<run::Time>{ + self.run.get_finish_time() + } pub fn clear(&mut self){ self.touching.clear(); } @@ -1899,8 +1902,9 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI #[cfg(test)] mod test{ - use strafesnet_common::integer::{vec3::{self,int as int3},mat3}; + use crate::file; use crate::body::VirtualBody; + use strafesnet_common::integer::{vec3::{self,int as int3},mat3}; use super::*; fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){ let h0=HitboxMesh::new(PhysicsMesh::unit_cube(),integer::Planar64Affine3::new(mat3::from_diagonal(int3(5,1,5)>>1),vec3::ZERO)); @@ -2113,4 +2117,202 @@ mod test{ Time::ZERO ),None); } + #[test] + fn run_replay(){ + println!("loading map file.."); + let map=file::load("../tools/bhop_maps/5692113331.snfm"); + println!("loading bot file.."); + let bot=file::load("../tools/replays/534s+997497968ns.snfb"); + if let (Ok(file::LoadFormat::Map(map)),Ok(file::LoadFormat::Bot(bot)))=(map,bot){ + // create recording + let mut physics_data=PhysicsData::default(); + println!("generating models.."); + physics_data.generate_models(&map); + println!("simulating..."); + let mut physics=PhysicsState::default(); + for ins in bot.instructions{ + PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins); + } + match physics.get_finish_time(){ + Some(time)=>println!("finish time:{}",time), + None=>println!("simulation did not end in finished state"), + } + }else{ + panic!("missing files"); + } + } + enum DeterminismResult{ + Deterministic, + NonDeterministic, + } + #[allow(unused)] + #[derive(Debug)] + enum ReplayError{ + Load(file::LoadError), + IO(std::io::Error), + } + impl From<file::LoadError> for ReplayError{ + fn from(value:file::LoadError)->Self{ + Self::Load(value) + } + } + impl From<std::io::Error> for ReplayError{ + fn from(value:std::io::Error)->Self{ + Self::IO(value) + } + } + fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{ + // create default physics state + let mut physics_deterministic=PhysicsState::default(); + // create a second physics state + let mut physics_filtered=PhysicsState::default(); + + // invent a new bot id and insert the replay + println!("simulating..."); + + let mut non_idle_count=0; + + for (i,ins) in bot.instructions.into_iter().enumerate(){ + let state_deterministic=physics_deterministic.clone(); + let state_filtered=physics_filtered.clone(); + PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone()); + match ins{ + strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(), + other=>{ + non_idle_count+=1; + // run + PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone()); + // check if position matches + let b0=physics_deterministic.camera_body(); + let b1=physics_filtered.camera_body(); + if b0.position!=b1.position{ + println!("desync at instruction #{}",i); + println!("non idle instructions completed={non_idle_count}"); + println!("instruction #{i}={:?}",other); + println!("deterministic state0:\n{state_deterministic:?}"); + println!("filtered state0:\n{state_filtered:?}"); + println!("deterministic state1:\n{:?}",physics_deterministic); + println!("filtered state1:\n{:?}",physics_filtered); + return DeterminismResult::NonDeterministic; + } + }, + } + } + match physics_deterministic.get_finish_time(){ + Some(time)=>println!("[with idle] finish time:{}",time), + None=>println!("[with idle] simulation did not end in finished state"), + } + match physics_filtered.get_finish_time(){ + Some(time)=>println!("[filtered] finish time:{}",time), + None=>println!("[filtered] simulation did not end in finished state"), + } + DeterminismResult::Deterministic + } + type ThreadResult=Result<Option<DeterminismResult>,file::LoadError>; + fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){ + s.spawn(move ||{ + let result=match file::load(file_path.as_path()){ + Ok(file::LoadFormat::Bot(bot))=>{ + println!("Running {:?}",file_path.file_stem()); + Ok(Some(segment_determinism(bot,physics_data))) + }, + Ok(_)=>{ + println!("Provided bot file is not a bot file!"); + Ok(None) + } + Err(e)=>{ + println!("Load error"); + Err(e) + }, + }; + // send when thread is complete + send.send(result).unwrap(); + }); + } + fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{ + Ok(dir_entry.file_type()?.is_file().then_some( + dir_entry.path() + )) + } + #[test] + fn test_determinism()->Result<(),ReplayError>{ + let thread_limit=std::thread::available_parallelism()?.get(); + println!("loading map file.."); + let file::LoadFormat::Map(map)=file::load("../tools/bhop_maps/5692113331.snfm")? else{ + panic!("Provided map file is not a map file!"); + }; + let mut physics_data=PhysicsData::default(); + println!("generating models.."); + physics_data.generate_models(&map); + let (send,recv)=std::sync::mpsc::channel(); + + let mut read_dir=std::fs::read_dir("../tools/replays")?; + + // promise that &physics_data will outlive the spawned threads + let thread_results=std::thread::scope(|s|{ + let mut thread_results=Vec::new(); + + // spawn threads + println!("spawning up to {thread_limit} threads..."); + let mut active_thread_count=0; + for _ in 0..thread_limit{ + if let Some(dir_entry_result)=read_dir.next(){ + if let Some(file_path)=get_file_path(dir_entry_result?)?{ + active_thread_count+=1; + do_thread(s,file_path,send.clone(),&physics_data); + } + }else{ + break; + } + } + + // spawn another thread every time a message is received from the channel + println!("riding parallelism wave..."); + while let Some(dir_entry_result)=read_dir.next(){ + if let Some(file_path)=get_file_path(dir_entry_result?)?{ + // wait for a thread to complete + thread_results.push(recv.recv().unwrap()); + do_thread(s,file_path,send.clone(),&physics_data); + } + } + + // wait for remaining threads to complete + println!("waiting for all threads to complete..."); + for _ in 0..active_thread_count{ + thread_results.push(recv.recv().unwrap()); + } + + println!("done."); + Ok::<_,ReplayError>(thread_results) + })?; + + // tally results + #[derive(Default)] + struct Totals{ + deterministic:u32, + nondeterministic:u32, + invalid:u32, + error:u32, + } + let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{ + match result{ + Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1, + Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1, + Ok(None)=>totals.invalid+=1, + Err(_)=>totals.error+=1, + } + totals + }); + + println!("deterministic={deterministic}"); + println!("nondeterministic={nondeterministic}"); + println!("invalid={invalid}"); + println!("error={error}"); + + assert!(nondeterministic==0); + assert!(invalid==0); + assert!(error==0); + + Ok(()) + } } diff --git a/strafe-client/src/session.rs b/strafe-client/src/session.rs index 1fa0d89d..e9d10a58 100644 --- a/strafe-client/src/session.rs +++ b/strafe-client/src/session.rs @@ -90,7 +90,7 @@ pub struct Recording{ instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>, } impl Recording{ - fn new( + pub fn new( instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>, )->Self{ Self{instructions}