use std::io::Cursor; use std::path::Path; use std::time::Instant; use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext}; fn main(){ test_determinism().unwrap(); } #[allow(unused)] #[derive(Debug)] enum ReplayError{ IO(std::io::Error), SNF(strafesnet_snf::Error), SNFM(strafesnet_snf::map::Error), SNFB(strafesnet_snf::bot::Error), } impl From<std::io::Error> for ReplayError{ fn from(value:std::io::Error)->Self{ Self::IO(value) } } impl From<strafesnet_snf::Error> for ReplayError{ fn from(value:strafesnet_snf::Error)->Self{ Self::SNF(value) } } impl From<strafesnet_snf::map::Error> for ReplayError{ fn from(value:strafesnet_snf::map::Error)->Self{ Self::SNFM(value) } } impl From<strafesnet_snf::bot::Error> for ReplayError{ fn from(value:strafesnet_snf::bot::Error)->Self{ Self::SNFB(value) } } fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{ let data=std::fs::read(path)?; Ok(Cursor::new(data)) } fn run_replay()->Result<(),ReplayError>{ println!("loading map file.."); let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?; let map=strafesnet_snf::read_map(data)?.into_complete_map()?; println!("loading bot file.."); let data=read_entire_file("../tools/replays/535s+159764769ns.snfb")?; let bot=strafesnet_snf::read_bot(data)?.read_all()?; // 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"), } Ok(()) } enum DeterminismResult{ Deterministic, NonDeterministic, } struct SegmentResult{ determinism:DeterminismResult, ticks:u64, nanoseconds:u64, } fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->SegmentResult{ // 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; let instruction_count=bot.instructions.len(); let start=Instant::now(); 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{ let nanoseconds=start.elapsed().as_nanos() as u64; 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 SegmentResult{ determinism:DeterminismResult::NonDeterministic, ticks:1+i as u64+non_idle_count, nanoseconds, }; } }, } } let nanoseconds=start.elapsed().as_nanos() as u64; 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"), } SegmentResult{ determinism:DeterminismResult::Deterministic, ticks:instruction_count as u64+non_idle_count, nanoseconds, } } type ThreadResult=Result<Option<SegmentResult>,ReplayError>; fn read_and_run(file_path:std::path::PathBuf,physics_data:&PhysicsData)->ThreadResult{ let data=read_entire_file(file_path.as_path())?; let bot=strafesnet_snf::read_bot(data)?.read_all()?; println!("Running {:?}",file_path.file_stem()); Ok(Some(segment_determinism(bot,physics_data))) } 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=read_and_run(file_path,physics_data); // 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() )) } fn test_determinism()->Result<(),ReplayError>{ let thread_limit=std::thread::available_parallelism()?.get(); println!("loading map file.."); let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?; let map=strafesnet_snf::read_map(data)?.into_complete_map()?; 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_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 mut nanoseconds=0; let mut ticks=0; let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{ match result{ Ok(Some(segment_result))=>{ ticks+=segment_result.ticks; nanoseconds+=segment_result.nanoseconds; match segment_result.determinism{ DeterminismResult::Deterministic=>totals.deterministic+=1, 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}"); println!("average ticks/s per core: {}",ticks*1_000_000_000/nanoseconds); assert!(nondeterministic==0); assert!(invalid==0); assert!(error==0); Ok(()) }