Compare commits

..

12 Commits

36 changed files with 556 additions and 357 deletions

94
Cargo.lock generated

@ -493,6 +493,27 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "dispatch" name = "dispatch"
version = "0.2.0" version = "0.2.0"
@ -1588,6 +1609,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "orbclient" name = "orbclient"
version = "0.3.48" version = "0.3.48"
@ -1997,6 +2024,17 @@ dependencies = [
"bitflags 2.8.0", "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]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -2253,19 +2291,17 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
name = "strafe-client" name = "strafe-client"
version = "0.11.0" version = "0.11.0"
dependencies = [ dependencies = [
"arrayvec",
"bytemuck",
"configparser",
"ddsfile",
"glam", "glam",
"id",
"parking_lot", "parking_lot",
"pollster", "pollster",
"replace_with",
"strafesnet_bsp_loader", "strafesnet_bsp_loader",
"strafesnet_common", "strafesnet_common",
"strafesnet_deferred_loader", "strafesnet_deferred_loader",
"strafesnet_graphics",
"strafesnet_physics",
"strafesnet_rbx_loader", "strafesnet_rbx_loader",
"strafesnet_session",
"strafesnet_settings",
"strafesnet_snf", "strafesnet_snf",
"wgpu", "wgpu",
"winit", "winit",
@ -2303,6 +2339,30 @@ dependencies = [
"vbsp", "vbsp",
] ]
[[package]]
name = "strafesnet_graphics"
version = "0.1.0"
dependencies = [
"bytemuck",
"ddsfile",
"glam",
"id",
"strafesnet_common",
"strafesnet_session",
"strafesnet_settings",
"wgpu",
]
[[package]]
name = "strafesnet_physics"
version = "0.1.0"
dependencies = [
"arrayvec",
"glam",
"id",
"strafesnet_common",
]
[[package]] [[package]]
name = "strafesnet_rbx_loader" name = "strafesnet_rbx_loader"
version = "0.5.2" version = "0.5.2"
@ -2319,6 +2379,28 @@ dependencies = [
"strafesnet_common", "strafesnet_common",
] ]
[[package]]
name = "strafesnet_session"
version = "0.1.0"
dependencies = [
"glam",
"replace_with",
"strafesnet_common",
"strafesnet_physics",
"strafesnet_settings",
"strafesnet_snf",
]
[[package]]
name = "strafesnet_settings"
version = "0.1.0"
dependencies = [
"configparser",
"directories",
"glam",
"strafesnet_common",
]
[[package]] [[package]]
name = "strafesnet_snf" name = "strafesnet_snf"
version = "0.2.0" version = "0.2.0"

@ -1,5 +1,9 @@
[workspace] [workspace]
members = [ members = [
"engine/graphics",
"engine/physics",
"engine/session",
"engine/settings",
"lib/bsp_loader", "lib/bsp_loader",
"lib/common", "lib/common",
"lib/deferred_loader", "lib/deferred_loader",

@ -13,4 +13,4 @@ See [releases](https://git.itzana.me/StrafesNET/strafe-project/releases) for dow
4. `cargo run --release --bin strafe-client` 4. `cargo run --release --bin strafe-client`
## Licenses ## 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.

@ -0,0 +1,14 @@
[package]
name = "strafesnet_graphics"
version = "0.1.0"
edition = "2021"
[dependencies]
bytemuck = { version = "1.13.1", features = ["derive"] }
ddsfile = "0.5.1"
glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_session = { path = "../session", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
wgpu = "24.0.0"

8
engine/graphics/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -1,9 +1,15 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::{HashSet,HashMap}; use std::collections::{HashSet,HashMap};
use strafesnet_common::map; use strafesnet_common::map;
use strafesnet_settings::settings;
use strafesnet_session::session;
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId}; use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel}; use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
use crate::model_graphics::{self,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex}; use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
struct Indices{ struct Indices{
count:u32, count:u32,
@ -136,7 +142,7 @@ impl GraphicsState{
pub fn clear(&mut self){ pub fn clear(&mut self){
self.models.clear(); self.models.clear();
} }
pub fn load_user_settings(&mut self,user_settings:&crate::settings::UserSettings){ pub fn load_user_settings(&mut self,user_settings:&settings::UserSettings){
self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2(); self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2();
} }
pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){ pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
@ -448,7 +454,7 @@ impl GraphicsState{
//.into_iter() the modeldata vec so entities can be /moved/ to models.entities //.into_iter() the modeldata vec so entities can be /moved/ to models.entities
let mut model_count=0; let mut model_count=0;
let mut instance_count=0; let mut instance_count=0;
let uniform_buffer_binding_size=crate::setup::required_limits().max_uniform_buffer_binding_size as usize; let uniform_buffer_binding_size=required_limits().max_uniform_buffer_binding_size as usize;
let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES; let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES;
self.models.reserve(models.len()); self.models.reserve(models.len());
for model in models.into_iter(){ for model in models.into_iter(){
@ -608,7 +614,7 @@ impl GraphicsState{
// Create the render pipeline // Create the render pipeline
let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{ let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{
label:None, label:None,
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../../../strafe-client/src/shader.wgsl"))),
}); });
//load textures //load textures
@ -636,10 +642,10 @@ impl GraphicsState{
wgpu::TextureFormat::Astc{ wgpu::TextureFormat::Astc{
block:AstcBlock::B4x4, block:AstcBlock::B4x4,
channel:AstcChannel::UnormSrgb, channel:AstcChannel::UnormSrgb,
}=>&include_bytes!("../images/astc.dds")[..], }=>&include_bytes!("../../../strafe-client/images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../images/etc2.dds")[..], wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../../../strafe-client/images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../images/bc1.dds")[..], wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../../../strafe-client/images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../images/bgra.dds")[..], wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../../../strafe-client/images/bgra.dds")[..],
_=>unreachable!(), _=>unreachable!(),
}; };
@ -682,7 +688,7 @@ impl GraphicsState{
//squid //squid
let squid_texture_view={ let squid_texture_view={
let bytes=include_bytes!("../images/squid.dds"); let bytes=include_bytes!("../../../strafe-client/images/squid.dds");
let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap(); let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
@ -864,7 +870,7 @@ impl GraphicsState{
&mut self, &mut self,
device:&wgpu::Device, device:&wgpu::Device,
config:&wgpu::SurfaceConfiguration, config:&wgpu::SurfaceConfiguration,
user_settings:&crate::settings::UserSettings, user_settings:&settings::UserSettings,
){ ){
self.depth_view=Self::create_depth_texture(config,device); self.depth_view=Self::create_depth_texture(config,device);
self.camera.screen_size=glam::uvec2(config.width,config.height); self.camera.screen_size=glam::uvec2(config.width,config.height);
@ -875,7 +881,7 @@ impl GraphicsState{
view:&wgpu::TextureView, view:&wgpu::TextureView,
device:&wgpu::Device, device:&wgpu::Device,
queue:&wgpu::Queue, queue:&wgpu::Queue,
frame_state:crate::session::FrameState, frame_state:session::FrameState,
){ ){
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input //TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input

@ -0,0 +1,2 @@
pub mod model;
pub mod graphics;

10
engine/physics/Cargo.toml Normal file

@ -0,0 +1,10 @@
[package]
name = "strafesnet_physics"
version = "0.1.0"
edition = "2021"
[dependencies]
arrayvec = "0.7.6"
glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }

8
engine/physics/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -31,6 +31,14 @@ impl<T> Body<T>
time, 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{ pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time; let dt=time-self.time;
self.position self.position
@ -82,7 +90,7 @@ impl<T> Body<T>
// a*dt + v // a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
} }
pub fn advance_time_ratio_dt(&mut self,dt:crate::model_physics::GigaTime){ pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
self.position=self.extrapolated_position_ratio_dt(dt); self.position=self.extrapolated_position_ratio_dt(dt);
self.velocity=self.extrapolated_velocity_ratio_dt(dt); self.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into(); self.time+=dt.into();
@ -137,14 +145,6 @@ pub struct VirtualBody<'a,T>{
impl<T> VirtualBody<'_,T> impl<T> VirtualBody<'_,T>
where Time<T>:Copy, 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{ pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time) self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
} }

@ -1,4 +1,4 @@
use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge}; use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3}; use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body}; use crate::physics::{Time,Body};

45
engine/physics/src/lib.rs Normal file

@ -0,0 +1,45 @@
mod body;
mod push_solve;
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)
}
}

@ -1,5 +1,5 @@
use std::collections::{HashMap,HashSet}; use std::collections::{HashMap,HashSet};
use crate::model_physics::{self,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId}; use crate::model::{self as model_physics,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
use strafesnet_common::bvh; use strafesnet_common::bvh;
use strafesnet_common::map; use strafesnet_common::map;
use strafesnet_common::run; use strafesnet_common::run;
@ -14,45 +14,6 @@ use strafesnet_common::integer::{self,vec3,mat3,Planar64,Planar64Vec3,Planar64Ma
pub use strafesnet_common::physics::{Time,TimeInner}; pub use strafesnet_common::physics::{Time,TimeInner};
use gameplay::ModeState; 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>; pub type Body=crate::body::Body<TimeInner>;
type MouseState=strafesnet_common::mouse::MouseState<TimeInner>; type MouseState=strafesnet_common::mouse::MouseState<TimeInner>;
@ -830,7 +791,7 @@ impl TouchingState{
crate::push_solve::push_solve(&contacts,acceleration) 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){ 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; let relative_body=body;
for contact in &self.contacts{ for contact in &self.contacts{
//detect face slide off //detect face slide off
@ -1179,7 +1140,7 @@ impl PhysicsData{
state.body.grow_aabb(&mut aabb,state.time,collector.time()); state.body.grow_aabb(&mut aabb,state.time,collector.time());
aabb.inflate(data.hitbox_mesh.halfsize); aabb.inflate(data.hitbox_mesh.halfsize);
//relative to moving platforms //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; let relative_body=&state.body;
data.bvh.the_tester(&aabb,&mut |&convex_mesh_id|{ data.bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
//no checks are needed because of the time limits. //no checks are needed because of the time limits.
@ -1242,7 +1203,7 @@ fn recalculate_touching(
aabb.grow(body.position); aabb.grow(body.position);
aabb.inflate(hitbox_mesh.halfsize); aabb.inflate(hitbox_mesh.halfsize);
//relative to moving platforms //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|{ bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
//no checks are needed because of the time limits. //no checks are needed because of the time limits.
let model_mesh=models.mesh(convex_mesh_id); 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)] #[cfg(test)]
mod test{ mod test{
use crate::file;
use crate::body::VirtualBody; use crate::body::VirtualBody;
use strafesnet_common::integer::{vec3::{self,int as int3},mat3}; use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
use super::*; use super::*;
@ -1994,111 +1954,111 @@ mod test{
} }
#[test] #[test]
fn test_collision_parabola_edge_east_from_west(){ 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(3,3,0),
int3(100,-1,0), int3(100,-1,0),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_south_from_north(){ 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,3,3),
int3(0,-1,100), int3(0,-1,100),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_west_from_east(){ 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(-3,3,0),
int3(-100,-1,0), int3(-100,-1,0),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_north_from_south(){ 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,3,-3),
int3(0,-1,-100), int3(0,-1,-100),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_north_from_ne(){ 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(0,6,-7)>>1,
int3(-10,-1,1), int3(-10,-1,1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_north_from_nw(){ 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(0,6,-7)>>1,
int3(10,-1,1), int3(10,-1,1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_east_from_se(){ 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(7,6,0)>>1,
int3(-1,-1,-10), int3(-1,-1,-10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_east_from_ne(){ 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(7,6,0)>>1,
int3(-1,-1,10), int3(-1,-1,10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_south_from_se(){ 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(0,6,7)>>1,
int3(-10,-1,-1), int3(-10,-1,-1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_south_from_sw(){ 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(0,6,7)>>1,
int3(10,-1,-1), int3(10,-1,-1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_west_from_se(){ 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(-7,6,0)>>1,
int3(1,-1,-10), int3(1,-1,-10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_parabola_edge_west_from_ne(){ 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(-7,6,0)>>1,
int3(1,-1,10), int3(1,-1,10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO 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] #[test]
fn test_collision_oblique(){ fn test_collision_oblique(){
@ -2127,202 +2087,4 @@ mod test{
Time::ZERO Time::ZERO
),None); ),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(())
}
} }

12
engine/session/Cargo.toml Normal file

@ -0,0 +1,12 @@
[package]
name = "strafesnet_session"
version = "0.1.0"
edition = "2021"
[dependencies]
glam = "0.29.0"
replace_with = "0.1.7"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../physics", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" }

8
engine/session/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -0,0 +1,2 @@
mod mouse_interpolator;
pub mod session;

@ -14,8 +14,8 @@ use strafesnet_common::timer::{Scaled,Timer};
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime}; use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction}; use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
use crate::physics::{PhysicsContext,PhysicsData}; use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
use crate::settings::UserSettings; use strafesnet_settings::settings::UserSettings;
pub enum Instruction<'a>{ pub enum Instruction<'a>{
Input(SessionInputInstruction), Input(SessionInputInstruction),
@ -57,19 +57,19 @@ pub enum SessionPlaybackInstruction{
} }
pub struct FrameState{ pub struct FrameState{
pub body:crate::physics::Body, pub body:physics::Body,
pub camera:crate::physics::PhysicsCamera, pub camera:physics::PhysicsCamera,
pub time:PhysicsTime, pub time:PhysicsTime,
} }
pub struct Simulation{ pub struct Simulation{
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>, timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:crate::physics::PhysicsState, physics:physics::PhysicsState,
} }
impl Simulation{ impl Simulation{
pub const fn new( pub const fn new(
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>, timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:crate::physics::PhysicsState, physics:physics::PhysicsState,
)->Self{ )->Self{
Self{ Self{
timer, timer,
@ -153,7 +153,7 @@ pub struct Session{
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator, mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
view_state:ViewState, view_state:ViewState,
//gui:GuiState //gui:GuiState
geometry_shared:crate::physics::PhysicsData, geometry_shared:physics::PhysicsData,
simulation:Simulation, simulation:Simulation,
// below fields not included in lite session // below fields not included in lite session
recording:Recording, recording:Recording,
@ -299,7 +299,7 @@ impl InstructionConsumer<Instruction<'_>> for Session{
std::thread::spawn(move ||{ std::thread::spawn(move ||{
std::fs::create_dir_all("replays").unwrap(); std::fs::create_dir_all("replays").unwrap();
let file=std::fs::File::create(file_name).unwrap(); let file=std::fs::File::create(file_name).unwrap();
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),crate::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!"); println!("Finished writing bot file!");
}); });
}, },

@ -0,0 +1,10 @@
[package]
name = "strafesnet_settings"
version = "0.1.0"
edition = "2021"
[dependencies]
configparser = "3.0.2"
directories = "6.0.0"
glam = "0.29.0"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }

8
engine/settings/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -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"),
})
}
}

@ -0,0 +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}} 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(); 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 (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){ let fov=match(cfg_fov_x,cfg_fov_y){
(Ok(Some(fov_x)),Ok(Some(fov_y)))=>Fov::Exactly { (Ok(Some(fov_x)),Ok(Some(fov_y)))=>Fov::Exactly {
@ -136,4 +136,4 @@ pub fn read_user_settings()->UserSettings{
}else{ }else{
UserSettings::default() UserSettings::default()
} }
} }

@ -15,19 +15,17 @@ source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"] roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
[dependencies] [dependencies]
arrayvec = "0.7.6"
bytemuck = { version = "1.13.1", features = ["derive"] }
configparser = "3.0.2"
ddsfile = "0.5.1"
glam = "0.29.0" glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }
parking_lot = "0.12.1" parking_lot = "0.12.1"
pollster = "0.4.0" pollster = "0.4.0"
replace_with = "0.1.7"
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true } strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
strafesnet_common = { path = "../lib/common", registry = "strafesnet" } strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_deferred_loader = { path = "../lib/deferred_loader", features = ["legacy"], registry = "strafesnet", optional = true } strafesnet_deferred_loader = { path = "../lib/deferred_loader", features = ["legacy"], registry = "strafesnet", optional = true }
strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true } strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true } strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "24.0.0" wgpu = "24.0.0"
winit = "0.30.7" winit = "0.30.7"

@ -34,20 +34,11 @@ pub enum ReadFormat{
} }
pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
let t=std::time::Instant::now();
println!("reading fourcc...");
let mut buf=std::io::BufReader::new(input); let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned(); let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned();
let dt=t.elapsed();
println!("{:?} elapsed={:?}",core::str::from_utf8(&peek),dt);
let t=std::time::Instant::now();
println!("reading entire file...");
// reading the entire file is way faster than round tripping the disk constantly // reading the entire file is way faster than round tripping the disk constantly
let mut entire_file=Vec::new(); let mut entire_file=Vec::new();
buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?; buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?;
println!("elapsed={:?}",t.elapsed());
let cursor=std::io::Cursor::new(entire_file); let cursor=std::io::Cursor::new(entire_file);
match peek.as_slice(){ match peek.as_slice(){
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
@ -55,15 +46,10 @@ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
#[cfg(feature="source")] #[cfg(feature="source")]
b"VBSP"=>Ok(ReadFormat::Source(strafesnet_bsp_loader::read(cursor).map_err(ReadError::Source)?)), b"VBSP"=>Ok(ReadFormat::Source(strafesnet_bsp_loader::read(cursor).map_err(ReadError::Source)?)),
#[cfg(feature="snf")] #[cfg(feature="snf")]
b"SNFM"=>{ b"SNFM"=>Ok(ReadFormat::SNFM(
let t=std::time::Instant::now(); strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
println!("decoding map..."); .into_complete_map().map_err(ReadError::StrafesNETMap)?
let map= )),
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
.into_complete_map().map_err(ReadError::StrafesNETMap)?;
println!("elapsed={:?}",t.elapsed());
Ok(ReadFormat::SNFM(map))
},
#[cfg(feature="snf")] #[cfg(feature="snf")]
b"SNFB"=>Ok(ReadFormat::SNFB( b"SNFB"=>Ok(ReadFormat::SNFB(
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)? strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?

@ -1,7 +1,11 @@
use strafesnet_graphics::graphics;
use strafesnet_session::session;
use strafesnet_settings::settings;
pub enum Instruction{ pub enum Instruction{
Render(crate::session::FrameState), Render(session::FrameState),
//UpdateModel(crate::graphics::GraphicsModelUpdate), //UpdateModel(graphics::GraphicsModelUpdate),
Resize(winit::dpi::PhysicalSize<u32>,crate::settings::UserSettings), Resize(winit::dpi::PhysicalSize<u32>,settings::UserSettings),
ChangeMap(strafesnet_common::map::CompleteMap), ChangeMap(strafesnet_common::map::CompleteMap),
} }
@ -15,7 +19,7 @@ WorkerDescription{
//up to three frames in flight, dropping new frame requests when all three are busy, and dropping output frames when one renders out of order //up to three frames in flight, dropping new frame requests when all three are busy, and dropping output frames when one renders out of order
pub fn new( pub fn new(
mut graphics:crate::graphics::GraphicsState, mut graphics:graphics::GraphicsState,
mut config:wgpu::SurfaceConfiguration, mut config:wgpu::SurfaceConfiguration,
surface:wgpu::Surface, surface:wgpu::Surface,
device:wgpu::Device, device:wgpu::Device,

@ -1,20 +1,13 @@
mod body;
mod file; mod file;
mod setup; mod setup;
mod window; mod window;
mod worker; mod worker;
mod physics;
mod session;
mod graphics;
mod settings;
mod push_solve;
mod face_crawler;
mod compat_worker; mod compat_worker;
mod model_physics;
mod model_graphics;
mod physics_worker; mod physics_worker;
mod graphics_worker; mod graphics_worker;
mod mouse_interpolator;
#[cfg(test)]
mod tests;
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION")); const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));

@ -1,6 +1,7 @@
use crate::graphics_worker::Instruction as GraphicsInstruction; use crate::graphics_worker::Instruction as GraphicsInstruction;
use crate::session::{ use strafesnet_settings::settings;
Session,Simulation,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction, use strafesnet_session::session::{
Session,Simulation,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction,ImplicitModeInstruction,
Instruction as SessionInstruction, Instruction as SessionInstruction,
}; };
use strafesnet_common::instruction::{TimedInstruction,InstructionConsumer}; use strafesnet_common::instruction::{TimedInstruction,InstructionConsumer};
@ -20,9 +21,9 @@ pub enum Instruction{
pub fn new<'a>( pub fn new<'a>(
mut graphics_worker:crate::compat_worker::INWorker<'a,crate::graphics_worker::Instruction>, mut graphics_worker:crate::compat_worker::INWorker<'a,crate::graphics_worker::Instruction>,
user_settings:crate::settings::UserSettings, user_settings:settings::UserSettings,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{ )->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{
let physics=crate::physics::PhysicsState::default(); let physics=strafesnet_physics::physics::PhysicsState::default();
let timer=Timer::unpaused(SessionTime::ZERO,PhysicsTime::ZERO); let timer=Timer::unpaused(SessionTime::ZERO,PhysicsTime::ZERO);
let simulation=Simulation::new(timer,physics); let simulation=Simulation::new(timer,physics);
let mut session=Session::new( let mut session=Session::new(
@ -67,7 +68,7 @@ pub fn new<'a>(
}, },
Instruction::ChangeMap(complete_map)=>{ Instruction::ChangeMap(complete_map)=>{
run_session_instruction!(ins.time,SessionInstruction::ChangeMap(&complete_map)); run_session_instruction!(ins.time,SessionInstruction::ChangeMap(&complete_map));
run_session_instruction!(ins.time,SessionInstruction::Input(SessionInputInstruction::Mode(crate::session::ImplicitModeInstruction::ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId::MAIN,strafesnet_common::gameplay_modes::StageId::FIRST)))); run_session_instruction!(ins.time,SessionInstruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId::MAIN,strafesnet_common::gameplay_modes::StageId::FIRST))));
run_graphics_worker_instruction!(GraphicsInstruction::ChangeMap(complete_map)); run_graphics_worker_instruction!(GraphicsInstruction::ChangeMap(complete_map));
}, },
Instruction::LoadReplay(bot)=>{ Instruction::LoadReplay(bot)=>{

@ -17,9 +17,6 @@ fn required_downlevel_capabilities()->wgpu::DownlevelCapabilities{
..wgpu::DownlevelCapabilities::default() ..wgpu::DownlevelCapabilities::default()
} }
} }
pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
struct SetupContextPartial1{ struct SetupContextPartial1{
backends:wgpu::Backends, backends:wgpu::Backends,
@ -130,7 +127,7 @@ impl<'a> SetupContextPartial3<'a>{
let required_features=required_features(); let required_features=required_features();
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface. // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
let needed_limits=required_limits().using_resolution(self.adapter.limits()); let needed_limits=strafesnet_graphics::graphics::required_limits().using_resolution(self.adapter.limits());
let trace_dir=std::env::var("WGPU_TRACE"); let trace_dir=std::env::var("WGPU_TRACE");
let (device, queue)=pollster::block_on(self.adapter let (device, queue)=pollster::block_on(self.adapter

@ -0,0 +1 @@
mod replay;

@ -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(())
}

@ -3,7 +3,8 @@ use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInn
use strafesnet_common::physics::{MiscInstruction,SetControlInstruction}; use strafesnet_common::physics::{MiscInstruction,SetControlInstruction};
use crate::file::LoadFormat; use crate::file::LoadFormat;
use crate::physics_worker::Instruction as PhysicsWorkerInstruction; use crate::physics_worker::Instruction as PhysicsWorkerInstruction;
use crate::session::{SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction}; use strafesnet_session::session::{self,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction};
use strafesnet_settings::directories;
pub enum Instruction{ pub enum Instruction{
Resize(winit::dpi::PhysicalSize<u32>), Resize(winit::dpi::PhysicalSize<u32>),
@ -150,7 +151,7 @@ impl WindowContext<'_>{
"R"|"r"=>s.then(||{ "R"|"r"=>s.then(||{
//mouse needs to be reset since the position is absolute //mouse needs to be reset since the position is absolute
self.mouse_pos=glam::DVec2::ZERO; self.mouse_pos=glam::DVec2::ZERO;
SessionInstructionSubset::Input(SessionInputInstruction::Mode(crate::session::ImplicitModeInstruction::ResetAndRestart)) SessionInstructionSubset::Input(SessionInputInstruction::Mode(session::ImplicitModeInstruction::ResetAndRestart))
}), }),
"F"|"f"=>input_misc!(PracticeFly,s), "F"|"f"=>input_misc!(PracticeFly,s),
"B"|"b"=>session_ctrl!(CopyRecordingIntoReplayAndSpectate,s), "B"|"b"=>session_ctrl!(CopyRecordingIntoReplayAndSpectate,s),
@ -210,9 +211,9 @@ pub fn worker<'a>(
setup_context:crate::setup::SetupContext<'a>, setup_context:crate::setup::SetupContext<'a>,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{ )->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{
// WindowContextSetup::new // WindowContextSetup::new
let user_settings=crate::settings::read_user_settings(); let user_settings=directories::Directories::portable().unwrap().user_settings();
let mut graphics=crate::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config); let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config);
graphics.load_user_settings(&user_settings); graphics.load_user_settings(&user_settings);
//WindowContextSetup::into_context //WindowContextSetup::into_context

@ -176,7 +176,7 @@ impl<'a,Task:Send+'a> INWorker<'a,Task>{
#[cfg(test)] #[cfg(test)]
mod test{ mod test{
use super::{thread,QRWorker}; use super::{thread,QRWorker};
type Body=crate::physics::Body; type Body=strafesnet_physics::physics::Body;
use strafesnet_common::{integer,instruction}; use strafesnet_common::{integer,instruction};
#[test]//How to run this test with printing: cargo test --release -- --nocapture #[test]//How to run this test with printing: cargo test --release -- --nocapture
fn test_worker() { fn test_worker() {