diff --git a/engine/physics/src/physics.rs b/engine/physics/src/physics.rs index 709efb7..19ecc8f 100644 --- a/engine/physics/src/physics.rs +++ b/engine/physics/src/physics.rs @@ -1912,7 +1912,6 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI #[cfg(test)] mod test{ - use crate::file; use crate::body::VirtualBody; use strafesnet_common::integer::{vec3::{self,int as int3},mat3}; use super::*; @@ -2127,202 +2126,4 @@ 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 for ReplayError{ - fn from(value:file::LoadError)->Self{ - Self::Load(value) - } - } - impl From 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,file::LoadError>; - fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender,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,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; - while active_thread_count(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/main.rs b/strafe-client/src/main.rs index 65beb0f..efd75d8 100644 --- a/strafe-client/src/main.rs +++ b/strafe-client/src/main.rs @@ -6,6 +6,9 @@ mod compat_worker; mod physics_worker; mod graphics_worker; +#[cfg(test)] +mod tests; + const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION")); fn main(){ diff --git a/strafe-client/src/tests/mod.rs b/strafe-client/src/tests/mod.rs new file mode 100644 index 0000000..6160572 --- /dev/null +++ b/strafe-client/src/tests/mod.rs @@ -0,0 +1 @@ +mod replay; diff --git a/strafe-client/src/tests/replay.rs b/strafe-client/src/tests/replay.rs new file mode 100644 index 0000000..9d25d3b --- /dev/null +++ b/strafe-client/src/tests/replay.rs @@ -0,0 +1,203 @@ + +use crate::file; + +use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext}; + +#[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 for ReplayError{ + fn from(value:file::LoadError)->Self{ + Self::Load(value) + } +} +impl From 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,file::LoadError>; +fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender,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,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; + while active_thread_count(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(()) +}