Compare commits
12 Commits
modular
...
directorie
| Author | SHA1 | Date | |
|---|---|---|---|
| 58f892c414 | |||
| 324367a24a | |||
| 3e814cb41a | |||
| ea854a548f | |||
| 5f2cf8f32e | |||
| b6b090de78 | |||
| affbada62e | |||
| 8d2ba28700 | |||
| 3eb4e76ab2 | |||
| 5d31419370 | |||
| 44a58044c7 | |||
| 83ac776b78 |
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -493,6 +493,27 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@@ -1588,6 +1609,12 @@ version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.48"
|
||||
@@ -1997,6 +2024,17 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@@ -2358,6 +2396,7 @@ name = "strafesnet_settings"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"configparser",
|
||||
"directories",
|
||||
"glam",
|
||||
"strafesnet_common",
|
||||
]
|
||||
|
||||
@@ -13,4 +13,4 @@ See [releases](https://git.itzana.me/StrafesNET/strafe-project/releases) for dow
|
||||
4. `cargo run --release --bin strafe-client`
|
||||
|
||||
## Licenses
|
||||
Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client has a sole proprietor license.
|
||||
Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client and engine crates have a sole proprietor license.
|
||||
|
||||
@@ -31,6 +31,14 @@ impl<T> Body<T>
|
||||
time,
|
||||
}
|
||||
}
|
||||
pub const fn relative_to<'a>(&'a self,body0:&'a Body<T>)->VirtualBody<'a,T>{
|
||||
//(p0,v0,a0,t0)
|
||||
//(p1,v1,a1,t1)
|
||||
VirtualBody{
|
||||
body0,
|
||||
body1:self,
|
||||
}
|
||||
}
|
||||
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
|
||||
let dt=time-self.time;
|
||||
self.position
|
||||
@@ -137,14 +145,6 @@ pub struct VirtualBody<'a,T>{
|
||||
impl<T> VirtualBody<'_,T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub const fn relative<'a>(body0:&'a Body<T>,body1:&'a Body<T>)->VirtualBody<'a,T>{
|
||||
//(p0,v0,a0,t0)
|
||||
//(p1,v1,a1,t1)
|
||||
VirtualBody{
|
||||
body0,
|
||||
body1,
|
||||
}
|
||||
}
|
||||
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
|
||||
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
|
||||
}
|
||||
|
||||
@@ -4,3 +4,42 @@ mod face_crawler;
|
||||
mod model;
|
||||
|
||||
pub mod physics;
|
||||
|
||||
// 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, ignore the version and use the latest version,
|
||||
// and overwrite the version in the file.
|
||||
//
|
||||
// Compatible physics versions should be determined
|
||||
// empirically at development time 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,45 +14,6 @@ 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, ignore the version and use the latest version,
|
||||
// and overwrite the version in the file.
|
||||
//
|
||||
// Compatible physics versions should be determined
|
||||
// empirically at development time 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(2);
|
||||
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
|
||||
let compat=[0,1,2];
|
||||
|
||||
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>;
|
||||
|
||||
@@ -830,7 +791,7 @@ impl TouchingState{
|
||||
crate::push_solve::push_solve(&contacts,acceleration)
|
||||
}
|
||||
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,TimeInner>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
|
||||
// let relative_body=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
|
||||
// let relative_body=body.relative_to(&Body::ZERO);
|
||||
let relative_body=body;
|
||||
for contact in &self.contacts{
|
||||
//detect face slide off
|
||||
@@ -1179,7 +1140,7 @@ impl PhysicsData{
|
||||
state.body.grow_aabb(&mut aabb,state.time,collector.time());
|
||||
aabb.inflate(data.hitbox_mesh.halfsize);
|
||||
//relative to moving platforms
|
||||
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
|
||||
//let relative_body=state.body.relative_to(&Body::ZERO);
|
||||
let relative_body=&state.body;
|
||||
data.bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
|
||||
//no checks are needed because of the time limits.
|
||||
@@ -1242,7 +1203,7 @@ fn recalculate_touching(
|
||||
aabb.grow(body.position);
|
||||
aabb.inflate(hitbox_mesh.halfsize);
|
||||
//relative to moving platforms
|
||||
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
|
||||
//let relative_body=state.body.relative_to(&Body::ZERO);
|
||||
bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
|
||||
//no checks are needed because of the time limits.
|
||||
let model_mesh=models.mesh(convex_mesh_id);
|
||||
@@ -1912,7 +1873,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::*;
|
||||
@@ -1994,111 +1954,111 @@ mod test{
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_west(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(3,3,0),
|
||||
int3(100,-1,0),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_north(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,3,3),
|
||||
int3(0,-1,100),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_east(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-3,3,0),
|
||||
int3(-100,-1,0),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_south(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,3,-3),
|
||||
int3(0,-1,-100),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,-7)>>1,
|
||||
int3(-10,-1,1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_nw(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,-7)>>1,
|
||||
int3(10,-1,1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(7,6,0)>>1,
|
||||
int3(-1,-1,-10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(7,6,0)>>1,
|
||||
int3(-1,-1,10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,7)>>1,
|
||||
int3(-10,-1,-1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_sw(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,7)>>1,
|
||||
int3(10,-1,-1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-7,6,0)>>1,
|
||||
int3(1,-1,-10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-7,6,0)>>1,
|
||||
int3(1,-1,10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_oblique(){
|
||||
@@ -2127,202 +2087,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<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;
|
||||
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 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),physics::VERSION.get(),replay.recording.instructions).unwrap();
|
||||
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),strafesnet_physics::VERSION.get(),replay.recording.instructions).unwrap();
|
||||
println!("Finished writing bot file!");
|
||||
});
|
||||
},
|
||||
|
||||
@@ -5,5 +5,6 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
configparser = "3.0.2"
|
||||
directories = "6.0.0"
|
||||
glam = "0.29.0"
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
|
||||
32
engine/settings/src/directories.rs
Normal file
32
engine/settings/src/directories.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::settings::UserSettings;
|
||||
|
||||
pub struct Directories{
|
||||
pub settings:PathBuf,
|
||||
pub maps:PathBuf,
|
||||
pub replays:PathBuf,
|
||||
}
|
||||
impl Directories{
|
||||
pub fn user_settings(&self)->UserSettings{
|
||||
crate::settings::read_user_settings(&self.settings)
|
||||
}
|
||||
pub fn installed()->Option<Self>{
|
||||
let dirs=directories::ProjectDirs::from("net.strafes","StrafesNET","Strafe Client")?;
|
||||
Some(Self{
|
||||
settings:dirs.config_dir().join("settings.conf"),
|
||||
maps:dirs.cache_dir().join("maps"),
|
||||
// separate directory for remote downloaded replays (cache)
|
||||
// bots:dirs.cache_dir().join("bots"),
|
||||
replays:dirs.data_local_dir().join("replays"),
|
||||
})
|
||||
}
|
||||
pub fn portable()->Result<Self,std::io::Error>{
|
||||
let current_dir=std::env::current_dir()?;
|
||||
Ok(Self{
|
||||
settings:current_dir.join("settings.conf"),
|
||||
maps:current_dir.join("maps"),
|
||||
replays:current_dir.join("replays"),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod settings;
|
||||
pub mod directories;
|
||||
|
||||
@@ -74,9 +74,9 @@ sensitivity_y_from_x_ratio=1
|
||||
Sensitivity::DeriveY{x:0.0.001,y:DerivedSensitivity{ratio:1.0}}
|
||||
*/
|
||||
|
||||
pub fn read_user_settings()->UserSettings{
|
||||
pub fn read_user_settings(path:&std::path::Path)->UserSettings{
|
||||
let mut cfg=configparser::ini::Ini::new();
|
||||
if let Ok(_)=cfg.load("settings.conf"){
|
||||
if let Ok(_)=cfg.load(path){
|
||||
let (cfg_fov_x,cfg_fov_y)=(cfg.getfloat("camera","fov_x"),cfg.getfloat("camera","fov_y"));
|
||||
let fov=match(cfg_fov_x,cfg_fov_y){
|
||||
(Ok(Some(fov_x)),Ok(Some(fov_y)))=>Fov::Exactly {
|
||||
@@ -136,4 +136,4 @@ pub fn read_user_settings()->UserSettings{
|
||||
}else{
|
||||
UserSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(){
|
||||
|
||||
1
strafe-client/src/tests/mod.rs
Normal file
1
strafe-client/src/tests/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod replay;
|
||||
203
strafe-client/src/tests/replay.rs
Normal file
203
strafe-client/src/tests/replay.rs
Normal file
@@ -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<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;
|
||||
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 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(())
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use strafesnet_common::physics::{MiscInstruction,SetControlInstruction};
|
||||
use crate::file::LoadFormat;
|
||||
use crate::physics_worker::Instruction as PhysicsWorkerInstruction;
|
||||
use strafesnet_session::session::{self,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction};
|
||||
use strafesnet_settings::settings;
|
||||
use strafesnet_settings::directories;
|
||||
|
||||
pub enum Instruction{
|
||||
Resize(winit::dpi::PhysicalSize<u32>),
|
||||
@@ -211,7 +211,7 @@ pub fn worker<'a>(
|
||||
setup_context:crate::setup::SetupContext<'a>,
|
||||
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{
|
||||
// WindowContextSetup::new
|
||||
let user_settings=settings::read_user_settings();
|
||||
let user_settings=directories::Directories::portable().unwrap().user_settings();
|
||||
|
||||
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config);
|
||||
graphics.load_user_settings(&user_settings);
|
||||
|
||||
Reference in New Issue
Block a user