Compare commits

..

54 Commits
bot ... modular

Author SHA1 Message Date
1a328c2a0f fix paths 2025-01-22 05:30:34 -08:00
de5b4e4214 move required limits function to graphics 2025-01-22 05:30:23 -08:00
e8e81eede7 add licenses to modules 2025-01-22 05:23:35 -08:00
f7bc750f02 use session and settings in graphics 2025-01-22 05:23:21 -08:00
49d8a3863c create session+settings modules 2025-01-22 05:23:07 -08:00
febb31e0ac fix paths 2025-01-22 04:55:33 -08:00
1ee0a3cc63 manage module dependencies 2025-01-22 04:55:33 -08:00
6a6b72d0e2 create graphic module 2025-01-22 04:18:35 -08:00
db7be2987f create physics module 2025-01-22 04:18:23 -08:00
267c91d005 replace .map_or(None, with .and_then( 2025-01-21 12:59:34 -08:00
32b361b122 use Range to express time range 2025-01-21 12:50:00 -08:00
f5c3209c7c readme: try it out 2025-01-21 12:20:08 -08:00
2eb74f2788 Revert "bodge surfs"
This reverts commit 65b49d2726.
2025-01-21 12:20:08 -08:00
871aadb78a physics version 2025-01-21 09:43:29 -08:00
65b49d2726 bodge surfs 2025-01-21 09:42:07 -08:00
ed70b7c484 tests: fix thread limit 2025-01-21 09:00:12 -08:00
c7575f2e21 deref bool 2025-01-21 08:49:44 -08:00
c2f78eab48 CrawlResult impls 2025-01-21 08:30:11 -08:00
195014400f apply setvelocity before teleport and jump 2025-01-21 07:47:39 -08:00
ee9585a4c2 accept slice in push_solve 2025-01-21 07:20:58 -08:00
77012d6caa remove unnecessary reference 2025-01-21 07:20:34 -08:00
f9509353dd change constrain_{velocity|acceleration} function signature 2025-01-21 07:20:22 -08:00
5bce4a84cf tweak ReachWalkTargetVelocity 2025-01-21 06:05:47 -08:00
4bbccd68ed tweak version plan 2025-01-21 05:47:39 -08:00
8be3be4002 write comment about handling identical timestamps implicitly 2025-01-21 05:39:12 -08:00
76bafa4d0a fix divide by zero crashes when mouse has not moved 2025-01-21 05:34:45 -08:00
a612d1f864 quiet down physics 2025-01-21 05:18:30 -08:00
ad9ef141bf print when bot file finishes writing 2025-01-21 05:18:30 -08:00
4c11980989 headless replay test 2025-01-21 05:18:30 -08:00
091da88b5c roblox_emulator: name macro variable 2025-01-20 15:46:26 -08:00
045e540020 roblox_emulator: use match guard 2025-01-20 15:46:13 -08:00
c14f8975fc snf: bot: fix lint SegmentId 2025-01-20 10:37:21 -08:00
8e228033e0 snf: bot: version const 2025-01-20 10:37:21 -08:00
eaecbc5b73 physics versioning plan 2025-01-20 10:36:40 -08:00
91a1b3d65e read entire file 2025-01-20 09:14:01 -08:00
e1d51ff2da rename stupidly named file enums 2025-01-20 09:11:09 -08:00
c5f2395b3a tools: copy all bash args in map scripts 2025-01-20 07:57:56 -08:00
77aa83d6ac physics: explicit start_time 2025-01-20 07:51:39 -08:00
184b12a9cc tools: copy all bash args in run script 2025-01-20 05:56:26 -08:00
9ba3484e25 create replays folder + write replay using spawned thread :) 2025-01-20 05:40:11 -08:00
adcd7db4f1 common: timer: the time of a paused timer does not depend on the parent time 2025-01-20 05:25:13 -08:00
710670d78e don't use mold
It makes compilation slightly more difficult for non-experts.  It can still be enabled the same way in the system-wide config located at ~/.cargo/config.toml
2025-01-18 22:43:12 -08:00
70e1514ef9 v0.11.0 replays 2025-01-18 09:24:34 -08:00
45e9b3d0ce use existing replay timer 2025-01-18 09:24:34 -08:00
1eac734c35 don't make a new replay if you are spectating 2025-01-18 09:24:34 -08:00
25e1312fe4 document another bind 2025-01-18 09:24:34 -08:00
cc8f6b059d SessionControlInstruction::LoadIntoReplayState (J) 2025-01-18 09:24:34 -08:00
4cd287dbb8 unpause sim on bot file export 2025-01-18 09:24:34 -08:00
40ea0e6c7d rename last_instruction_id to next_instruction_id 2025-01-18 09:24:34 -08:00
b45b92c627 allow writing idle instructions 2025-01-18 09:24:34 -08:00
e90f53a111 integrate replay save/load 2025-01-18 09:24:34 -08:00
6beb6c5f9a implement bot file 2025-01-18 09:24:21 -08:00
a1507d4f26 implement newtypes for bot files 2025-01-18 09:24:05 -08:00
77db5a7a6b tweak instruction collector 2025-01-18 05:22:02 -08:00
47 changed files with 627 additions and 213 deletions

View File

@@ -1,6 +1,2 @@
[registries.strafesnet] [registries.strafesnet]
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]

57
Cargo.lock generated
View File

@@ -2251,21 +2251,19 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strafe-client" name = "strafe-client"
version = "0.10.5" 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 +2301,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 +2341,27 @@ 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",
"glam",
"strafesnet_common",
]
[[package]] [[package]]
name = "strafesnet_snf" name = "strafesnet_snf"
version = "0.2.0" version = "0.2.0"

View File

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

View File

@@ -3,6 +3,9 @@
# Strafe Project # Strafe Project
Monorepo for working on projects related to strafe client. Monorepo for working on projects related to strafe client.
## Try it out
See [releases](https://git.itzana.me/StrafesNET/strafe-project/releases) for downloads.
## How to build and run ## How to build and run
1. Have rust and git installed 1. Have rust and git installed
2. `git clone https://git.itzana.me/StrafesNET/strafe-project` 2. `git clone https://git.itzana.me/StrafesNET/strafe-project`

View File

@@ -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
View 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
*******************************************************/

View File

@@ -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

View File

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

10
engine/physics/Cargo.toml Normal file
View 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
View 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
*******************************************************/

View File

@@ -82,7 +82,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();

View File

@@ -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};
@@ -12,6 +12,20 @@ pub enum CrawlResult<M:MeshQuery>{
Miss(FEV<M>), Miss(FEV<M>),
Hit(M::Face,GigaTime), Hit(M::Face,GigaTime),
} }
impl<M:MeshQuery> CrawlResult<M>{
pub fn hit(self)->Option<(M::Face,GigaTime)>{
match self{
CrawlResult::Miss(_)=>None,
CrawlResult::Hit(face,time)=>Some((face,time)),
}
}
pub fn miss(self)->Option<FEV<M>>{
match self{
CrawlResult::Miss(fev)=>Some(fev),
CrawlResult::Hit(_,_)=>None,
}
}
}
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M> impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
where where

View File

@@ -0,0 +1,6 @@
mod body;
mod push_solve;
mod face_crawler;
mod model;
pub mod physics;

View File

@@ -1,5 +1,6 @@
use std::borrow::{Borrow,Cow}; use std::borrow::{Borrow,Cow};
use std::collections::{HashSet,HashMap}; use std::collections::{HashSet,HashMap};
use core::ops::Range;
use strafesnet_common::integer::vec3::Vector3; use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter}; use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio}; use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@@ -718,48 +719,40 @@ impl MinkowskiMesh<'_>{
// //
// Most of the calculation time is just calculating the starting point // Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}). // for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body)->Option<FEV<MinkowskiMesh>>{ fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().map_or(None,|dir|{ infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position); let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola //a line is simpler to solve than a parabola
infinity_body.velocity=dir; infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO; infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev //crawl in from negative infinity along a tangent line to get the closest fev
// TODO: change crawl_fev args to delta time? Optional values? // TODO: change crawl_fev args to delta time? Optional values?
match infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time){ infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
crate::face_crawler::CrawlResult::Miss(fev)=>Some(fev),
crate::face_crawler::CrawlResult::Hit(_,_)=>None,
}
}) })
} }
pub fn predict_collision_in(&self,relative_body:&Body,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{ pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(relative_body.clone()).map_or(None,|fev|{ self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{
//continue forwards along the body parabola //continue forwards along the body parabola
match fev.crawl(self,relative_body,relative_body.time,time_limit){ fev.crawl(self,relative_body,start_time,time_limit).hit()
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
}) })
} }
pub fn predict_collision_out(&self,relative_body:&Body,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{ pub fn predict_collision_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
//create an extrapolated body at time_limit //create an extrapolated body at time_limit
let infinity_body=Body::new( let infinity_body=-relative_body.clone();
relative_body.extrapolated_position(time_limit), self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
-relative_body.extrapolated_velocity(time_limit),
relative_body.acceleration,
-time_limit,
);
self.closest_fev_not_inside(infinity_body).map_or(None,|fev|{
//continue backwards along the body parabola //continue backwards along the body parabola
match fev.crawl(self,&-relative_body.clone(),-time_limit,-relative_body.time){ fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
crate::face_crawler::CrawlResult::Miss(_)=>None, //no need to test -time<time_limit because of the first step
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,-time)),//no need to test -time<time_limit because of the first step .map(|(face,time)|(face,-time))
}
}) })
} }
pub fn predict_collision_face_out(&self,relative_body:&Body,time_limit:Time,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{ pub fn predict_collision_face_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
//no algorithm needed, there is only one state and two cases (Edge,None) //no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case) //determine when it passes an edge ("sliding off" case)
let start_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num,r.den)
};
let mut best_time={ let mut best_time={
let r=(time_limit-relative_body.time).to_ratio(); let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4()) Ratio::new(r.num.fix_4(),r.den.fix_4())
@@ -775,7 +768,7 @@ impl MinkowskiMesh<'_>{
//WARNING! d outside of *2 //WARNING! d outside of *2
//WARNING: truncated precision //WARNING: truncated precision
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){ for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){
if Ratio::new(Planar64::ZERO,Planar64::EPSILON).le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt; best_time=dt;
best_edge=Some(directed_edge_id); best_edge=Some(directed_edge_id);
break; break;
@@ -786,10 +779,7 @@ impl MinkowskiMesh<'_>{
} }
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{ fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position); let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
match infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time){ infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
} }
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{ pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO); let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
@@ -995,7 +985,7 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
for k in 0..len{ for k in 0..len{
if k!=i&&k!=j{ if k!=i&&k!=j{
let d=n.dot(normals[k]).is_negative(); let d=n.dot(normals[k]).is_negative();
if let Some(comp)=&d_comp{ if let &Some(comp)=&d_comp{
// This is testing if d_comp*d < 0 // This is testing if d_comp*d < 0
if comp^d{ if comp^d{
return true; return true;

View File

@@ -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,6 +14,45 @@ 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>;
@@ -32,7 +71,7 @@ pub enum InternalInstruction{
// Water, // Water,
} }
#[derive(Clone,Debug,Default)] #[derive(Clone,Debug)]
pub struct InputState{ pub struct InputState{
mouse:MouseState, mouse:MouseState,
next_mouse:MouseState, next_mouse:MouseState,
@@ -40,10 +79,15 @@ pub struct InputState{
} }
impl InputState{ impl InputState{
fn set_next_mouse(&mut self,next_mouse:MouseState){ fn set_next_mouse(&mut self,next_mouse:MouseState){
// would this be correct?
// if self.next_mouse.time==next_mouse.time{
// self.next_mouse=next_mouse;
// }else{
//I like your functions magic language //I like your functions magic language
self.mouse=std::mem::replace(&mut self.next_mouse,next_mouse); self.mouse=std::mem::replace(&mut self.next_mouse,next_mouse);
//equivalently: //equivalently:
//(self.next_mouse,self.mouse)=(next_mouse,self.next_mouse.clone()); //(self.next_mouse,self.mouse)=(next_mouse,self.next_mouse.clone());
// }
} }
fn replace_mouse(&mut self,mouse:MouseState,next_mouse:MouseState){ fn replace_mouse(&mut self,mouse:MouseState,next_mouse:MouseState){
(self.next_mouse,self.mouse)=(next_mouse,mouse); (self.next_mouse,self.mouse)=(next_mouse,mouse);
@@ -65,6 +109,15 @@ impl InputState{
((dm*t)/dt).as_ivec2() ((dm*t)/dt).as_ivec2()
} }
} }
impl Default for InputState{
fn default()->Self{
Self{
mouse:MouseState{pos:Default::default(),time:Time::ZERO-Time::EPSILON*2},
next_mouse:MouseState{pos:Default::default(),time:Time::ZERO-Time::EPSILON},
controls:Default::default(),
}
}
}
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
enum JumpDirection{ enum JumpDirection{
Exactly(Planar64Vec3), Exactly(Planar64Vec3),
@@ -147,17 +200,17 @@ fn ground_things(walk_settings:&gameplay_style::WalkSettings,contact:&ContactCol
let normal=contact_normal(models,hitbox_mesh,contact); let normal=contact_normal(models,hitbox_mesh,contact);
let gravity=touching.base_acceleration(models,style,camera,input_state); let gravity=touching.base_acceleration(models,style,camera,input_state);
let control_dir=style.get_y_control_dir(camera,input_state.controls); let control_dir=style.get_y_control_dir(camera,input_state.controls);
let mut target_velocity=walk_settings.get_walk_target_velocity(control_dir,normal); let target_velocity=walk_settings.get_walk_target_velocity(control_dir,normal);
touching.constrain_velocity(models,hitbox_mesh,&mut target_velocity); let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity);
(gravity,target_velocity) (gravity,target_velocity_clipped)
} }
fn ladder_things(ladder_settings:&gameplay_style::LadderSettings,contact:&ContactCollision,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState)->(Planar64Vec3,Planar64Vec3){ fn ladder_things(ladder_settings:&gameplay_style::LadderSettings,contact:&ContactCollision,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState)->(Planar64Vec3,Planar64Vec3){
let normal=contact_normal(models,hitbox_mesh,contact); let normal=contact_normal(models,hitbox_mesh,contact);
let gravity=touching.base_acceleration(models,style,camera,input_state); let gravity=touching.base_acceleration(models,style,camera,input_state);
let control_dir=style.get_y_control_dir(camera,input_state.controls); let control_dir=style.get_y_control_dir(camera,input_state.controls);
let mut target_velocity=ladder_settings.get_ladder_target_velocity(control_dir,normal); let target_velocity=ladder_settings.get_ladder_target_velocity(control_dir,normal);
touching.constrain_velocity(models,hitbox_mesh,&mut target_velocity); let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity);
(gravity,target_velocity) (gravity,target_velocity_clipped)
} }
#[derive(Default)] #[derive(Default)]
@@ -754,8 +807,8 @@ impl TouchingState{
//TODO: add water //TODO: add water
a a
} }
fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:&mut Planar64Vec3){ fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:Planar64Vec3)->Planar64Vec3{
let contacts=self.contacts.iter().map(|contact|{ let contacts:Vec<_>=self.contacts.iter().map(|contact|{
let n=contact_normal(models,hitbox_mesh,contact); let n=contact_normal(models,hitbox_mesh,contact);
crate::push_solve::Contact{ crate::push_solve::Contact{
position:vec3::ZERO, position:vec3::ZERO,
@@ -763,10 +816,10 @@ impl TouchingState{
normal:n, normal:n,
} }
}).collect(); }).collect();
*velocity=crate::push_solve::push_solve(&contacts,*velocity); crate::push_solve::push_solve(&contacts,velocity)
} }
fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:&mut Planar64Vec3){ fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:Planar64Vec3)->Planar64Vec3{
let contacts=self.contacts.iter().map(|contact|{ let contacts:Vec<_>=self.contacts.iter().map(|contact|{
let n=contact_normal(models,hitbox_mesh,contact); let n=contact_normal(models,hitbox_mesh,contact);
crate::push_solve::Contact{ crate::push_solve::Contact{
position:vec3::ZERO, position:vec3::ZERO,
@@ -774,15 +827,16 @@ impl TouchingState{
normal:n, normal:n,
} }
}).collect(); }).collect();
*acceleration=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,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=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
let relative_body=body;
for contact in &self.contacts{ for contact in &self.contacts{
//detect face slide off //detect face slide off
let model_mesh=models.contact_mesh(contact); let model_mesh=models.contact_mesh(contact);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(_face,time)|{ collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time..collector.time(),contact.face_id).map(|(_face,time)|{
TimedInstruction{ TimedInstruction{
time:relative_body.time+time.into(), time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd( instruction:InternalInstruction::CollisionEnd(
@@ -796,7 +850,7 @@ impl TouchingState{
//detect model collision in reverse //detect model collision in reverse
let model_mesh=models.intersect_mesh(intersect); let model_mesh=models.intersect_mesh(intersect);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(_face,time)|{ collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time()).map(|(_face,time)|{
TimedInstruction{ TimedInstruction{
time:relative_body.time+time.into(), time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd( instruction:InternalInstruction::CollisionEnd(
@@ -860,6 +914,9 @@ impl PhysicsState{
pub const fn mode(&self)->gameplay_modes::ModeId{ pub const fn mode(&self)->gameplay_modes::ModeId{
self.mode_state.get_mode_id() self.mode_state.get_mode_id()
} }
pub fn get_finish_time(&self)->Option<run::Time>{
self.run.get_finish_time()
}
pub fn clear(&mut self){ pub fn clear(&mut self){
self.touching.clear(); self.touching.clear();
} }
@@ -1111,7 +1168,7 @@ impl PhysicsData{
//this is the one who asks //this is the one who asks
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{ fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
//JUST POLLING!!! NO MUTATION //JUST POLLING!!! NO MUTATION
let mut collector = instruction::InstructionCollector::new(time_limit); let mut collector=instruction::InstructionCollector::new(time_limit);
collector.collect(state.next_move_instruction()); collector.collect(state.next_move_instruction());
@@ -1128,9 +1185,9 @@ impl PhysicsData{
//no checks are needed because of the time limits. //no checks are needed because of the time limits.
let model_mesh=data.models.mesh(convex_mesh_id); let model_mesh=data.models.mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_in(relative_body,collector.time()) collector.collect(minkowski.predict_collision_in(relative_body,state.time..collector.time())
//temp (?) code to avoid collision loops //temp (?) code to avoid collision loops
.map_or(None,|(face,dt)|{ .and_then(|(face,dt)|{
// this must be rounded to avoid the infinite loop when hitting the start zone // this must be rounded to avoid the infinite loop when hitting the start zone
let time=relative_body.time+dt.into(); let time=relative_body.time+dt.into();
(state.time<time).then_some((time,face,dt)) (state.time<time).then_some((time,face,dt))
@@ -1145,7 +1202,7 @@ impl PhysicsData{
) )
); );
}); });
collector.instruction() collector.take()
} }
@@ -1240,16 +1297,14 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
let r=n.dot(v).is_positive(); let r=n.dot(v).is_positive();
if r{ if r{
culled=true; culled=true;
println!("set_velocity_cull contact={:?}",contact);
} }
!r !r
}); });
set_velocity(body,touching,models,hitbox_mesh,v); set_velocity(body,touching,models,hitbox_mesh,v);
culled culled
} }
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,mut v:Planar64Vec3){ fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3){
touching.constrain_velocity(models,hitbox_mesh,&mut v); body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);;
body.velocity=v;
} }
fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3)->bool{ fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3)->bool{
//This is not correct but is better than what I have //This is not correct but is better than what I have
@@ -1259,16 +1314,14 @@ fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&Phys
let r=n.dot(a).is_positive(); let r=n.dot(a).is_positive();
if r{ if r{
culled=true; culled=true;
println!("set_acceleration_cull contact={:?}",contact);
} }
!r !r
}); });
set_acceleration(body,touching,models,hitbox_mesh,a); set_acceleration(body,touching,models,hitbox_mesh,a);
culled culled
} }
fn set_acceleration(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,mut a:Planar64Vec3){ fn set_acceleration(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3){
touching.constrain_acceleration(models,hitbox_mesh,&mut a); body.acceleration=touching.constrain_acceleration(models,hitbox_mesh,a);
body.acceleration=a;
} }
fn teleport( fn teleport(
@@ -1477,7 +1530,7 @@ fn collision_start_contact(
let model_id=contact.model_id.into(); let model_id=contact.model_id.into();
let mut allow_run_teleport_behaviour=not_spawn_at(mode,model_id); let mut allow_run_teleport_behaviour=not_spawn_at(mode,model_id);
match &attr.contacting.contact_behaviour{ match &attr.contacting.contact_behaviour{
Some(gameplay_attributes::ContactingBehaviour::Surf)=>println!("I'm surfing!"), Some(gameplay_attributes::ContactingBehaviour::Surf)=>(),
Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"), Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
&Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{ &Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{
let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).fix_1(); let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).fix_1();
@@ -1507,6 +1560,21 @@ fn collision_start_contact(
} }
}, },
} }
match &attr.general.trajectory{
Some(trajectory)=>{
match trajectory{
gameplay_attributes::SetTrajectory::AirTime(_)=>todo!(),
gameplay_attributes::SetTrajectory::Height(_)=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointTime{..}=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointSpeed{..}=>todo!(),
&gameplay_attributes::SetTrajectory::Velocity(velocity)=>{
move_state.cull_velocity(velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
},
gameplay_attributes::SetTrajectory::DotVelocity{..}=>todo!(),
}
},
None=>(),
}
//I love making functions with 10 arguments to dodge the borrow checker //I love making functions with 10 arguments to dodge the borrow checker
if allow_run_teleport_behaviour{ if allow_run_teleport_behaviour{
run_teleport_behaviour(model_id,attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time); run_teleport_behaviour(model_id,attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time);
@@ -1534,21 +1602,6 @@ fn collision_start_contact(
} }
} }
} }
match &attr.general.trajectory{
Some(trajectory)=>{
match trajectory{
gameplay_attributes::SetTrajectory::AirTime(_)=>todo!(),
gameplay_attributes::SetTrajectory::Height(_)=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointTime { target_point: _, time: _ }=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointSpeed { target_point: _, speed: _, trajectory_choice: _ }=>todo!(),
&gameplay_attributes::SetTrajectory::Velocity(velocity)=>{
move_state.cull_velocity(velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
},
gameplay_attributes::SetTrajectory::DotVelocity { direction: _, dot: _ }=>todo!(),
}
},
None=>(),
}
//doing enum to set the acceleration when surfing //doing enum to set the acceleration when surfing
//doing input_and_body to refresh the walk state if you hit a wall while accelerating //doing input_and_body to refresh the walk state if you hit a wall while accelerating
move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state); move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
@@ -1719,19 +1772,19 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
|MoveState::Fly |MoveState::Fly
=>println!("ReachWalkTargetVelocity fired for non-walking MoveState"), =>println!("ReachWalkTargetVelocity fired for non-walking MoveState"),
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>{ MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>{
match &walk_state.target{ //velocity is already handled by advance_time
//we know that the acceleration is precisely zero because the walk target is known to be reachable
//which means that gravity can be fully cancelled
//ignore moving platforms for now
let target=core::mem::replace(&mut walk_state.target,TransientAcceleration::Reached);
set_acceleration(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
// check what the target was to see if it was invalid
match target{
//you are not supposed to reach a walk target which is already reached! //you are not supposed to reach a walk target which is already reached!
TransientAcceleration::Reached=>unreachable!(), TransientAcceleration::Reached=>println!("Invalid walk target: Reached"),
TransientAcceleration::Reachable{acceleration:_,time:_}=>{ TransientAcceleration::Reachable{..}=>(),
//velocity is already handled by advance_time
//we know that the acceleration is precisely zero because the walk target is known to be reachable
//which means that gravity can be fully cancelled
//ignore moving platforms for now
set_acceleration(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
walk_state.target=TransientAcceleration::Reached;
},
//you are not supposed to reach an unreachable walk target! //you are not supposed to reach an unreachable walk target!
TransientAcceleration::Unreachable{acceleration:_}=>unreachable!(), TransientAcceleration::Unreachable{..}=>println!("Invalid walk target: Unreachable"),
} }
} }
} }
@@ -1790,7 +1843,7 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
let jump_dir=walk_state.jump_direction.direction(&data.models,&data.hitbox_mesh,&walk_state.contact); let jump_dir=walk_state.jump_direction.direction(&data.models,&data.hitbox_mesh,&walk_state.contact);
let booster_option=data.models.contact_attr(walk_state.contact.model_id).general.booster.as_ref(); let booster_option=data.models.contact_attr(walk_state.contact.model_id).general.booster.as_ref();
let jumped_velocity=jump_settings.jumped_velocity(&state.style,jump_dir,state.body.velocity,booster_option); let jumped_velocity=jump_settings.jumped_velocity(&state.style,jump_dir,state.body.velocity,booster_option);
state.cull_velocity(&data,jumped_velocity); state.cull_velocity(data,jumped_velocity);
} }
} }
b_refresh_walk_target=false; b_refresh_walk_target=false;
@@ -1859,8 +1912,9 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
#[cfg(test)] #[cfg(test)]
mod test{ mod test{
use strafesnet_common::integer::{vec3::{self,int as int3},mat3}; use crate::file;
use crate::body::VirtualBody; use crate::body::VirtualBody;
use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
use super::*; use super::*;
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){ fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
let h0=HitboxMesh::new(PhysicsMesh::unit_cube(),integer::Planar64Affine3::new(mat3::from_diagonal(int3(5,1,5)>>1),vec3::ZERO)); let h0=HitboxMesh::new(PhysicsMesh::unit_cube(),integer::Planar64Affine3::new(mat3::from_diagonal(int3(5,1,5)>>1),vec3::ZERO));
@@ -1868,7 +1922,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh(); let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh(); let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::from_secs(10)); let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision"); assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
} }
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){ fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
@@ -1886,7 +1940,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh(); let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh(); let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::from_secs(10)); let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision"); assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
} }
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){ fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){
@@ -2073,4 +2127,202 @@ 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(())
}
} }

View File

@@ -289,7 +289,7 @@ fn get_best_push_ray_and_conts<'a>(
} }
} }
fn get_first_touch<'a>(contacts:&'a Vec<Contact>,ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{ fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
contacts.iter() contacts.iter()
.filter(|&contact| .filter(|&contact|
!conts.iter().any(|&c|std::ptr::eq(c,contact)) !conts.iter().any(|&c|std::ptr::eq(c,contact))
@@ -299,7 +299,7 @@ fn get_first_touch<'a>(contacts:&'a Vec<Contact>,ray:&Ray,conts:&Conts)->Option<
.min_by_key(|&(t,_)|t) .min_by_key(|&(t,_)|t)
} }
pub fn push_solve(contacts:&Vec<Contact>,point:Planar64Vec3)->Planar64Vec3{ pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point); let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point);
loop{ loop{
let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){ let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){

12
engine/session/Cargo.toml Normal file
View 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
View 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
*******************************************************/

View File

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

View File

@@ -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,
@@ -90,7 +90,7 @@ pub struct Recording{
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>, instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
} }
impl Recording{ impl Recording{
fn new( pub fn new(
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>, instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
)->Self{ )->Self{
Self{instructions} Self{instructions}
@@ -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,
@@ -292,11 +292,16 @@ impl InstructionConsumer<Instruction<'_>> for Session{
Instruction::Control(SessionControlInstruction::SaveReplay)=>{ Instruction::Control(SessionControlInstruction::SaveReplay)=>{
// Bind: N // Bind: N
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play); let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
let file=std::fs::File::create(format!("{}.snfb",ins.time)).unwrap();
match view_state{ match view_state{
ViewState::Play=>(), ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){ ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),replay.recording.instructions).unwrap(); let file_name=format!("replays/{}.snfb",ins.time);
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();
println!("Finished writing bot file!");
});
}, },
} }
_=self.simulation.timer.set_paused(ins.time,false); _=self.simulation.timer.set_paused(ins.time,false);

View File

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

8
engine/settings/LICENSE Normal file
View 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
*******************************************************/

View File

@@ -0,0 +1 @@
pub mod settings;

View File

@@ -62,6 +62,7 @@ impl<I,T> InstructionCollector<I,T>
pub const fn time(&self)->Time<T>{ pub const fn time(&self)->Time<T>{
self.time self.time
} }
#[inline]
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){ pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
if let Some(ins)=instruction{ if let Some(ins)=instruction{
if ins.time<self.time{ if ins.time<self.time{
@@ -70,7 +71,8 @@ impl<I,T> InstructionCollector<I,T>
} }
} }
} }
pub fn instruction(self)->Option<TimedInstruction<I,T>>{ #[inline]
pub fn take(self)->Option<TimedInstruction<I,T>>{
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR //STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
self.instruction.map(|instruction|TimedInstruction{ self.instruction.map(|instruction|TimedInstruction{
time:self.time, time:self.time,

View File

@@ -14,6 +14,7 @@ impl<T> Time<T>{
pub const MIN:Self=Self::raw(i64::MIN); pub const MIN:Self=Self::raw(i64::MIN);
pub const MAX:Self=Self::raw(i64::MAX); pub const MAX:Self=Self::raw(i64::MAX);
pub const ZERO:Self=Self::raw(0); pub const ZERO:Self=Self::raw(0);
pub const EPSILON:Self=Self::raw(1);
pub const ONE_SECOND:Self=Self::raw(1_000_000_000); pub const ONE_SECOND:Self=Self::raw(1_000_000_000);
pub const ONE_MILLISECOND:Self=Self::raw(1_000_000); pub const ONE_MILLISECOND:Self=Self::raw(1_000_000);
pub const ONE_MICROSECOND:Self=Self::raw(1_000); pub const ONE_MICROSECOND:Self=Self::raw(1_000);

View File

@@ -76,7 +76,7 @@ impl Run{
match &self.state{ match &self.state{
RunState::Created=>Time::ZERO, RunState::Created=>Time::ZERO,
RunState::Started{timer}=>timer.time(time), RunState::Started{timer}=>timer.time(time),
RunState::Finished{timer}=>timer.time(time), RunState::Finished{timer}=>timer.time(),
} }
} }
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{ pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
@@ -110,4 +110,10 @@ impl Run{
self.flagged=Some(flag_reason); self.flagged=Some(flag_reason);
} }
} }
pub fn get_finish_time(&self)->Option<Time>{
match &self.state{
RunState::Finished{timer}=>Some(timer.time()),
_=>None,
}
}
} }

View File

@@ -157,7 +157,7 @@ impl<T:TimerState> TimerFixed<T,Paused>
where Time<T::In>:Copy, where Time<T::In>:Copy,
{ {
pub fn into_unpaused(self,time:Time<T::In>)->TimerFixed<T,Unpaused>{ pub fn into_unpaused(self,time:Time<T::In>)->TimerFixed<T,Unpaused>{
let new_time=self.time(time); let new_time=self.time();
let mut timer=TimerFixed{ let mut timer=TimerFixed{
state:self.state, state:self.state,
_paused:Unpaused, _paused:Unpaused,
@@ -165,6 +165,9 @@ impl<T:TimerState> TimerFixed<T,Paused>
timer.set_time(time,new_time); timer.set_time(time,new_time);
timer timer
} }
pub fn time(&self)->Time<T::Out>{
self.state.get_offset().coerce()
}
} }
impl<T:TimerState> TimerFixed<T,Unpaused> impl<T:TimerState> TimerFixed<T,Unpaused>
where Time<T::In>:Copy, where Time<T::In>:Copy,
@@ -178,6 +181,9 @@ impl<T:TimerState> TimerFixed<T,Unpaused>
timer.set_time(time,new_time); timer.set_time(time,new_time);
timer timer
} }
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
self.state.get_time(time)
}
} }
//the new constructor and time queries are generic across both //the new constructor and time queries are generic across both
@@ -199,12 +205,6 @@ impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
pub fn into_state(self)->T{ pub fn into_state(self)->T{
self.state self.state
} }
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
match P::IS_PAUSED{
true=>self.state.get_offset().coerce(),
false=>self.state.get_time(time),
}
}
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){ pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
match P::IS_PAUSED{ match P::IS_PAUSED{
true=>self.state.set_offset(new_time.coerce()), true=>self.state.set_offset(new_time.coerce()),
@@ -256,7 +256,7 @@ impl<T:TimerState> Timer<T>
} }
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{ pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
match self{ match self{
Self::Paused(timer)=>timer.time(time), Self::Paused(timer)=>timer.time(),
Self::Unpaused(timer)=>timer.time(time), Self::Unpaused(timer)=>timer.time(time),
} }
} }
@@ -329,7 +329,7 @@ mod test{
//create a paused timer that reads 0s //create a paused timer that reads 0s
let timer=TimerFixed::<Scaled<Parent,Calculated>,Paused>::from_state(Scaled::new(0.5f32.try_into().unwrap(),sec!(0))); let timer=TimerFixed::<Scaled<Parent,Calculated>,Paused>::from_state(Scaled::new(0.5f32.try_into().unwrap(),sec!(0)));
//the paused timer at 1 second should read 0s //the paused timer at 1 second should read 0s
assert_eq!(timer.time(sec!(1)),sec!(0)); assert_eq!(timer.time(),sec!(0));
//unpause it after one second //unpause it after one second
let timer=timer.into_unpaused(sec!(1)); let timer=timer.into_unpaused(sec!(1));
@@ -339,7 +339,7 @@ mod test{
//pause the timer after 11 seconds //pause the timer after 11 seconds
let timer=timer.into_paused(sec!(11)); let timer=timer.into_paused(sec!(11));
//the paused timer at 20 seconds should read 5s //the paused timer at 20 seconds should read 5s
assert_eq!(timer.time(sec!(20)),sec!(5)); assert_eq!(timer.time(),sec!(5));
} }
#[test] #[test]
fn test_timer()->Result<(),Error>{ fn test_timer()->Result<(),Error>{

View File

@@ -1,22 +1,22 @@
macro_rules! type_from_lua_userdata{ macro_rules! type_from_lua_userdata{
($asd:ident)=>{ ($ty:ident)=>{
impl mlua::FromLua for $asd{ impl mlua::FromLua for $ty{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{ fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{ match value{
mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?), mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))), other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
} }
} }
} }
}; };
} }
macro_rules! type_from_lua_userdata_lua_lifetime{ macro_rules! type_from_lua_userdata_lua_lifetime{
($asd:ident)=>{ ($ty:ident)=>{
impl mlua::FromLua for $asd<'static>{ impl mlua::FromLua for $ty<'static>{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{ fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{ match value{
mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?), mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))), other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
} }
} }
} }

View File

@@ -59,8 +59,8 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
match delay.classify(){ match delay.classify(){
std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?, std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?,
// cases where the number is too large to schedule // cases where the number is too large to schedule
std::num::FpCategory::Infinite=>return Ok(()), std::num::FpCategory::Infinite
std::num::FpCategory::Normal=>if (u64::MAX as f64)<delay{ |std::num::FpCategory::Normal if (u64::MAX as f64)<delay=>{
return Ok(()); return Ok(());
}, },
_=>(), _=>(),

View File

@@ -4,6 +4,8 @@ use crate::newtypes;
use crate::file::BlockId; use crate::file::BlockId;
use strafesnet_common::physics::Time; use strafesnet_common::physics::Time;
const VERSION:u32=0;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>; type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
#[derive(Debug)] #[derive(Debug)]
@@ -28,12 +30,16 @@ pub enum Error{
/* block types /* block types
BLOCK_BOT_HEADER: BLOCK_BOT_HEADER:
// Tegments are laid out in chronological order, // Segments are laid out in chronological order,
// but block_id is not necessarily in ascending order. // but block_id is not necessarily in ascending order.
// //
// This is to place the final segment close to the start of the file, // This is to place the final segment close to the start of the file,
// which allows the duration of the bot to be conveniently calculated // which allows the duration of the bot to be conveniently calculated
// from the first and last instruction timestamps. // from the first and last instruction timestamps.
//
// Use exact physics version for replay playback
// Use highest compatible physics version for verification
u32 physics_version
u32 num_segments u32 num_segments
for _ in 0..num_segments{ for _ in 0..num_segments{
i64 time i64 time
@@ -61,6 +67,7 @@ struct SegmentHeader{
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]
struct Header{ struct Header{
physics_version:u32,
num_segments:u32, num_segments:u32,
#[br(count=num_segments)] #[br(count=num_segments)]
segments:Vec<SegmentHeader>, segments:Vec<SegmentHeader>,
@@ -69,7 +76,7 @@ struct Header{
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]
#[derive(Clone,Copy,Debug,id::Id)] #[derive(Clone,Copy,Debug,id::Id)]
struct SegmentId(u32); pub struct SegmentId(u32);
pub struct Segment{ pub struct Segment{
pub instructions:Vec<TimedPhysicsInstruction> pub instructions:Vec<TimedPhysicsInstruction>
@@ -151,7 +158,7 @@ impl<R:BinReaderExt> StreamableBot<R>{
} }
const MAX_BLOCK_SIZE:usize=64*1024;//64 kB const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{ pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
// decide which instructions to put in which segment // decide which instructions to put in which segment
// write segment 1 to block 1 // write segment 1 to block 1
// write segment N to block 2 // write segment N to block 2
@@ -251,6 +258,7 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Ite
}; };
let header=Header{ let header=Header{
physics_version,
num_segments:num_segments as u32, num_segments:num_segments as u32,
segments, segments,
}; };
@@ -303,7 +311,7 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,instructions:impl IntoIterator<Ite
let file_header=crate::file::Header{ let file_header=crate::file::Header{
fourcc:crate::file::FourCC::Bot, fourcc:crate::file::FourCC::Bot,
version:0, version:VERSION,
priming, priming,
resource:0, resource:0,
block_count, block_count,

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "strafe-client" name = "strafe-client"
version = "0.10.5" version = "0.11.0"
edition = "2021" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "Custom" license = "Custom"
@@ -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"

View File

@@ -22,7 +22,7 @@ impl std::fmt::Display for ReadError{
} }
impl std::error::Error for ReadError{} impl std::error::Error for ReadError{}
enum Format{ pub enum ReadFormat{
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
Roblox(strafesnet_rbx_loader::Model), Roblox(strafesnet_rbx_loader::Model),
#[cfg(feature="source")] #[cfg(feature="source")]
@@ -33,22 +33,26 @@ enum Format{
SNFB(strafesnet_snf::bot::Segment), SNFB(strafesnet_snf::bot::Segment),
} }
pub fn read<R:Read+std::io::Seek>(input:R)->Result<Format,ReadError>{ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
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)?; let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned();
match &peek[0..4]{ // reading the entire file is way faster than round tripping the disk constantly
let mut entire_file=Vec::new();
buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?;
let cursor=std::io::Cursor::new(entire_file);
match peek.as_slice(){
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
b"<rob"=>Ok(Format::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)), b"<rob"=>Ok(ReadFormat::Roblox(strafesnet_rbx_loader::read(cursor).map_err(ReadError::Roblox)?)),
#[cfg(feature="source")] #[cfg(feature="source")]
b"VBSP"=>Ok(Format::Source(strafesnet_bsp_loader::read(buf).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"=>Ok(Format::SNFM( b"SNFM"=>Ok(ReadFormat::SNFM(
strafesnet_snf::read_map(buf).map_err(ReadError::StrafesNET)? strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
.into_complete_map().map_err(ReadError::StrafesNETMap)? .into_complete_map().map_err(ReadError::StrafesNETMap)?
)), )),
#[cfg(feature="snf")] #[cfg(feature="snf")]
b"SNFB"=>Ok(Format::SNFB( b"SNFB"=>Ok(ReadFormat::SNFB(
strafesnet_snf::read_bot(buf).map_err(ReadError::StrafesNET)? strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
.read_all().map_err(ReadError::StrafesNETBot)? .read_all().map_err(ReadError::StrafesNETBot)?
)), )),
_=>Err(ReadError::UnknownFileFormat), _=>Err(ReadError::UnknownFileFormat),
@@ -68,23 +72,23 @@ impl std::fmt::Display for LoadError{
} }
impl std::error::Error for LoadError{} impl std::error::Error for LoadError{}
pub enum Format2{ pub enum LoadFormat{
#[cfg(feature="snf")] #[cfg(feature="snf")]
Map(strafesnet_common::map::CompleteMap), Map(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")] #[cfg(feature="snf")]
Bot(strafesnet_snf::bot::Segment), Bot(strafesnet_snf::bot::Segment),
} }
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<Format2,LoadError>{ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
//blocking because it's simpler... //blocking because it's simpler...
let file=std::fs::File::open(path).map_err(LoadError::File)?; let file=std::fs::File::open(path).map_err(LoadError::File)?;
match read(file).map_err(LoadError::ReadError)?{ match read(file).map_err(LoadError::ReadError)?{
#[cfg(feature="snf")] #[cfg(feature="snf")]
Format::SNFB(bot)=>Ok(Format2::Bot(bot)), ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
#[cfg(feature="snf")] #[cfg(feature="snf")]
Format::SNFM(map)=>Ok(Format2::Map(map)), ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
Format::Roblox(model)=>{ ReadFormat::Roblox(model)=>{
let mut place=model.into_place(); let mut place=model.into_place();
place.run_scripts(); place.run_scripts();
@@ -117,10 +121,10 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<Format2,LoadError>{
) )
); );
Ok(Format2::Map(map)) Ok(LoadFormat::Map(map))
}, },
#[cfg(feature="source")] #[cfg(feature="source")]
Format::Source(bsp)=>{ ReadFormat::Source(bsp)=>{
let mut loader=strafesnet_deferred_loader::source_legacy(); let mut loader=strafesnet_deferred_loader::source_legacy();
let (texture_loader,mesh_loader)=loader.get_inner_mut(); let (texture_loader,mesh_loader)=loader.get_inner_mut();
@@ -156,7 +160,7 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<Format2,LoadError>{
), ),
); );
Ok(Format2::Map(map)) Ok(LoadFormat::Map(map))
}, },
} }
} }

View File

@@ -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,

View File

@@ -1,20 +1,10 @@
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;
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION")); const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));

View File

@@ -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)=>{

View File

@@ -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

View File

@@ -1,9 +1,10 @@
use strafesnet_common::instruction::TimedInstruction; use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner}; use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::physics::{MiscInstruction,SetControlInstruction}; use strafesnet_common::physics::{MiscInstruction,SetControlInstruction};
use crate::file::Format2; 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::settings;
pub enum Instruction{ pub enum Instruction{
Resize(winit::dpi::PhysicalSize<u32>), Resize(winit::dpi::PhysicalSize<u32>),
@@ -30,8 +31,8 @@ impl WindowContext<'_>{
match event{ match event{
winit::event::WindowEvent::DroppedFile(path)=>{ winit::event::WindowEvent::DroppedFile(path)=>{
match crate::file::load(path.as_path()){ match crate::file::load(path.as_path()){
Ok(Format2::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(), Ok(LoadFormat::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(),
Ok(Format2::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}).unwrap(), Ok(LoadFormat::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}).unwrap(),
Err(e)=>println!("Failed to load file: {e}"), Err(e)=>println!("Failed to load file: {e}"),
} }
}, },
@@ -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=settings::read_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

View File

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

View File

@@ -1 +1 @@
mangohud ../target/release/strafe-client bhop_maps/5692113331.snfm mangohud ../target/release/strafe-client bhop_maps/5692113331.snfm "$@"

View File

@@ -1 +1 @@
mangohud ../target/release/strafe-client bhop_maps/5692124338.snfm mangohud ../target/release/strafe-client bhop_maps/5692124338.snfm "$@"

1
tools/replays Symbolic link
View File

@@ -0,0 +1 @@
/run/media/quat/Files/Documents/map-files/verify-scripts/replays

View File

@@ -1 +1 @@
mangohud ../target/release/strafe-client "$1" mangohud ../target/release/strafe-client "$@"

View File

@@ -1 +1 @@
mangohud ../target/release/strafe-client bhop_maps/5692152916.snfm mangohud ../target/release/strafe-client bhop_maps/5692152916.snfm "$@"

View File

@@ -1 +1 @@
mangohud ../target/release/strafe-client surf_maps/5692145408.snfm mangohud ../target/release/strafe-client surf_maps/5692145408.snfm "$@"