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
52 changed files with 1213 additions and 270 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,14 +14,15 @@ 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),
Control(SessionControlInstruction), Control(SessionControlInstruction),
Playback(SessionPlaybackInstruction), Playback(SessionPlaybackInstruction),
ChangeMap(&'a strafesnet_common::map::CompleteMap), ChangeMap(&'a strafesnet_common::map::CompleteMap),
LoadReplay(strafesnet_snf::bot::Segment),
Idle, Idle,
} }
@@ -44,6 +45,8 @@ pub enum SessionControlInstruction{
// copy the current session simulation recording into a replay and view it // copy the current session simulation recording into a replay and view it
CopyRecordingIntoReplayAndSpectate, CopyRecordingIntoReplayAndSpectate,
StopSpectate, StopSpectate,
SaveReplay,
LoadIntoReplayState,
} }
pub enum SessionPlaybackInstruction{ pub enum SessionPlaybackInstruction{
SkipForward, SkipForward,
@@ -54,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,
@@ -87,12 +90,17 @@ pub struct Recording{
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>, instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
} }
impl Recording{ impl Recording{
pub fn new(
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
)->Self{
Self{instructions}
}
fn clear(&mut self){ fn clear(&mut self){
self.instructions.clear(); self.instructions.clear();
} }
} }
pub struct Replay{ pub struct Replay{
last_instruction_id:usize, next_instruction_id:usize,
recording:Recording, recording:Recording,
simulation:Simulation, simulation:Simulation,
} }
@@ -102,7 +110,7 @@ impl Replay{
simulation:Simulation, simulation:Simulation,
)->Self{ )->Self{
Self{ Self{
last_instruction_id:0, next_instruction_id:0,
recording, recording,
simulation, simulation,
} }
@@ -110,16 +118,16 @@ impl Replay{
pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){ pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){
let mut time=self.simulation.timer.time(time_limit); let mut time=self.simulation.timer.time(time_limit);
loop{ loop{
if let Some(ins)=self.recording.instructions.get(self.last_instruction_id){ if let Some(ins)=self.recording.instructions.get(self.next_instruction_id){
if ins.time<time{ if ins.time<time{
PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone()); PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone());
self.last_instruction_id+=1; self.next_instruction_id+=1;
}else{ }else{
break; break;
} }
}else{ }else{
// loop playback // loop playback
self.last_instruction_id=0; self.next_instruction_id=0;
// No need to reset physics because the very first instruction is 'Reset' // No need to reset physics because the very first instruction is 'Reset'
let new_time=self.recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time); let new_time=self.recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
self.simulation.timer.set_time(time_limit,new_time); self.simulation.timer.set_time(time_limit,new_time);
@@ -145,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,
@@ -243,7 +251,7 @@ impl InstructionConsumer<Instruction<'_>> for Session{
// what if they pause for 5ms lmao // what if they pause for 5ms lmao
_=self.simulation.timer.set_paused(ins.time,paused); _=self.simulation.timer.set_paused(ins.time,paused);
}, },
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=>{ Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
// Bind: B // Bind: B
// pause simulation // pause simulation
@@ -281,6 +289,35 @@ impl InstructionConsumer<Instruction<'_>> for Session{
} }
_=self.simulation.timer.set_paused(ins.time,false); _=self.simulation.timer.set_paused(ins.time,false);
}, },
Instruction::Control(SessionControlInstruction::SaveReplay)=>{
// Bind: N
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
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);
},
Instruction::Control(SessionControlInstruction::LoadIntoReplayState)=>{
// Bind: J
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
self.recording.instructions=replay.recording.instructions.into_iter().take(replay.next_instruction_id).collect();
self.simulation=replay.simulation;
},
}
// don't unpause -- use the replay timer state whether it is pasued or unpaused
},
Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{ Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{
match &self.view_state{ match &self.view_state{
ViewState::Play=>{ ViewState::Play=>{
@@ -323,7 +360,7 @@ impl InstructionConsumer<Instruction<'_>> for Session{
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5)); let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
replay.simulation.timer.set_time(ins.time,time); replay.simulation.timer.set_time(ins.time,time);
// resimulate the entire playback lol // resimulate the entire playback lol
replay.last_instruction_id=0; replay.next_instruction_id=0;
}, },
} }
}, },
@@ -339,6 +376,30 @@ impl InstructionConsumer<Instruction<'_>> for Session{
self.clear_recording(); self.clear_recording();
self.change_map(complete_map); self.change_map(complete_map);
}, },
Instruction::LoadReplay(bot)=>{
// pause simulation
_=self.simulation.timer.set_paused(ins.time,true);
// create recording
let recording=Recording::new(bot.instructions);
// create timer starting at first instruction (or zero if the list is empty)
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
let timer=Timer::unpaused(ins.time,new_time);
// create default physics state
let simulation=Simulation::new(timer,Default::default());
// invent a new bot id and insert the replay
let bot_id=BotId(self.replays.len() as u32);
self.replays.insert(bot_id,Replay::new(
recording,
simulation,
));
// begin spectate
self.view_state=ViewState::Replay(bot_id);
},
Instruction::Idle=>{ Instruction::Idle=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle); run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle);
// this just refreshes the replays // this just refreshes the replays

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

@@ -1,98 +1,347 @@
use binrw::{BinReaderExt, binrw}; use binrw::{binrw,BinReaderExt,BinWrite,BinWriterExt};
use crate::newtypes;
use crate::file::BlockId;
use strafesnet_common::physics::Time;
const VERSION:u32=0;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
InvalidHeader, InvalidHeader(binrw::Error),
InvalidSegment(binrw::Error), InvalidSegment(binrw::Error),
SegmentConvert(newtypes::integer::RatioError),
InstructionConvert(newtypes::physics::InstructionConvert),
InstructionWrite(binrw::Error),
InvalidSegmentId(SegmentId), InvalidSegmentId(SegmentId),
InvalidData(binrw::Error),
IO(std::io::Error),
File(crate::file::Error), File(crate::file::Error),
} }
// Bot files are simply the sequence of instructions that the physics received during the run.
// The instructions are partitioned into timestamped blocks for ease of streaming.
//
// Keyframe information required for efficient seeking
// is part of a different file, and is generated from this file.
/* block types /* block types
BLOCK_BOT_HEADER: BLOCK_BOT_HEADER:
u128 map_resource_uuid //which map is this bot running // Segments are laid out in chronological order,
//don't include style info in bot header because it's in the simulation state // but block_id is not necessarily in ascending order.
//blocks are laid out in chronological order, but indices may jump around. //
u64 num_segments // 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
// 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
for _ in 0..num_segments{ for _ in 0..num_segments{
i64 time //simulation_state timestamp i64 time
u64 block_id u32 instruction_count
u32 block_id
} }
BLOCK_BOT_SEGMENT: BLOCK_BOT_SEGMENT:
//format version indicates what version of these structures to use // segments can potentially be losslessly compressed!
SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about. for _ in 0..instruction_count{
//to read, greedily decode instructions until eof // TODO: delta encode as much as possible (time,mousepos)
loop{ i64 time
//delta encode as much as possible (time,mousepos) physics::Instruction instruction
//strafe ticks are implied
//physics can be implied in an input-only bot file
TimedInstruction<SimulationInstruction> instruction
} }
*/ */
//error hiding mock code #[binrw]
mod simulation{ #[brw(little)]
#[super::binrw] struct SegmentHeader{
#[brw(little)] time:i64,
pub struct State{} instruction_count:u32,
#[super::binrw] block_id:BlockId,
#[brw(little)] }
pub struct Instruction{} #[binrw]
#[brw(little)]
struct Header{
physics_version:u32,
num_segments:u32,
#[br(count=num_segments)]
segments:Vec<SegmentHeader>,
} }
// mod instruction{
// #[super::binrw]
// #[brw(little)]
// pub struct TimedInstruction<Instruction:binrw::BinRead+binrw::BinWrite>{
// time:u64,
// instruction:Instruction
// }
// }
// mod timeline{
// #[super::binrw]
// #[brw(little)]
// pub struct Timeline<Instruction:binrw::BinRead+binrw::BinWrite>{
// #[bw(try_calc(u32::try_from(instructions.len())))]
// instruction_count:u32,
// #[br(count=instruction_count)]
// instructions:Vec<super::instruction::TimedInstruction<Instruction>>
// }
// }
//serious code
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]
#[derive(Clone,Copy,Debug,id::Id)] #[derive(Clone,Copy,Debug,id::Id)]
pub struct SegmentId(u32); pub struct SegmentId(u32);
#[binrw]
#[brw(little)]
pub struct Segment{ pub struct Segment{
state:simulation::State, pub instructions:Vec<TimedPhysicsInstruction>
//#[bw(try_calc(u32::try_from(instructions.len())))] }
//instruction_count:u32,
//#[br(count=instruction_count)]
//instructions:Vec<instruction::TimedInstruction<simulation::Instruction>>
//please remember that strafe ticks are implicit! 33% smaller bot files #[derive(Clone,Copy,Debug)]
pub struct SegmentInfo{
/// time of the first instruction in this segment.
time:Time,
instruction_count:u32,
/// How many total instructions in segments up to and including this segment
/// Alternatively, the id of the first instruction be in the _next_ segment
instructions_subtotal:u64,
block_id:BlockId,
} }
pub struct StreamableBot<R:BinReaderExt>{ pub struct StreamableBot<R:BinReaderExt>{
file:crate::file::File<R>, file:crate::file::File<R>,
//timeline:timeline::Timeline<SegmentId>, segment_map:Vec<SegmentInfo>,
segment_id_to_block_id:Vec<crate::file::BlockId>,
} }
impl<R:BinReaderExt> StreamableBot<R>{ impl<R:BinReaderExt> StreamableBot<R>{
pub(crate) fn new(file:crate::file::File<R>)->Result<Self,Error>{ pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{
Err(Error::InvalidHeader) //assume the file seek is in the right place to start reading header
let header:Header=file.data_mut().read_le().map_err(Error::InvalidHeader)?;
let mut instructions_subtotal=0;
let segment_map=header.segments.into_iter().map(|SegmentHeader{time,instruction_count,block_id}|{
instructions_subtotal+=instruction_count as u64;
SegmentInfo{
time:Time::raw(time),
instruction_count,
instructions_subtotal,
block_id,
}
}).collect();
Ok(Self{
file,
segment_map,
})
} }
pub fn load_segment(&mut self,segment_id:SegmentId)->Result<Segment,Error>{ fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
let block_id=*self.segment_id_to_block_id.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?; Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
let mut block=self.file.block_reader(block_id).map_err(Error::File)?; }
let segment=block.read_le().map_err(Error::InvalidSegment)?; pub fn find_segments_instruction_range(&self,start_instruction:u64,end_instruction:u64)->&[SegmentInfo]{
let start=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<start_instruction);
let end=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<end_instruction);
&self.segment_map[start..=end]
}
// pub fn find_segments_time_range(&self,start_time:Time,end_time:Time)->&[SegmentInfo]{
// // TODO: This is off by one, both should be one less
// let start=self.segment_map.partition_point(|segment_info|segment_info.time<start_time);
// let end=self.segment_map.partition_point(|segment_info|segment_info.time<end_time);
// &self.segment_map[start..=end]
// }
fn append_to_segment(&mut self,segment_info:SegmentInfo,segment:&mut Segment)->Result<(),Error>{
let mut block=self.file.block_reader(segment_info.block_id).map_err(Error::File)?;
for _ in 0..segment_info.instruction_count{
let instruction:newtypes::physics::TimedInstruction=block.read_le().map_err(Error::InvalidSegment)?;
segment.instructions.push(instruction.try_into().map_err(Error::SegmentConvert)?);
}
Ok(())
}
pub fn load_segment(&mut self,segment_info:SegmentInfo)->Result<Segment,Error>{
let mut segment=Segment{
instructions:Vec::with_capacity(segment_info.instruction_count as usize),
};
self.append_to_segment(segment_info,&mut segment)?;
Ok(segment)
}
pub fn read_all(&mut self)->Result<Segment,Error>{
let mut segment=Segment{
instructions:Vec::new(),
};
for i in 0..self.segment_map.len(){
let segment_info=self.segment_map[i];
self.append_to_segment(segment_info,&mut segment)?;
}
Ok(segment) Ok(segment)
} }
} }
const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
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
// write segment 1 to block 1
// write segment N to block 2
// write rest of segments
// 1 2 3 4 5
// becomes
// [1 5] 2 3 4
struct SegmentHeaderInfo{
time:Time,
instruction_count:u32,
range:core::ops::Range<usize>
}
let mut segment_header_infos=Vec::new();
let mut raw_segments=std::io::Cursor::new(Vec::new());
// block info
let mut start_time=Time::ZERO;
let mut start_position=raw_segments.position() as usize;
let mut instruction_count=0;
let mut last_position=start_position;
let mut iter=instructions.into_iter();
macro_rules! collect_instruction{
($instruction:expr)=>{
let time=$instruction.time;
let instruction_writable:newtypes::physics::TimedInstruction=$instruction.try_into().map_err(Error::InstructionConvert)?;
instruction_writable.write_le(&mut raw_segments).map_err(Error::InstructionWrite)?;
instruction_count+=1;
let position=raw_segments.position() as usize;
// exceeds max block size
if MAX_BLOCK_SIZE<position-last_position{
segment_header_infos.push(SegmentHeaderInfo{
time:start_time,
instruction_count,
range:start_position..last_position,
});
start_position=last_position;
instruction_count=0;
start_time=time;
}
last_position=position;
}
}
// unroll one loop iteration to grab the starting time
if let Some(instruction)=iter.next(){
start_time=instruction.time;
collect_instruction!(instruction);
}
for instruction in iter{
collect_instruction!(instruction);
}
//last block, whatever size it happens to be
{
let final_position=raw_segments.position() as usize;
segment_header_infos.push(SegmentHeaderInfo{
time:start_time,
instruction_count,
range:start_position..final_position,
});
}
// drop cursor
let raw_segments=raw_segments.into_inner();
let num_segments=segment_header_infos.len();
// segments list is in chronological order
let make_segment_header=|block_id,&SegmentHeaderInfo{time,instruction_count,range:ref _range}|SegmentHeader{
time:time.get(),
instruction_count,
block_id,
};
let segments=if 2<num_segments{
let mut segments=Vec::with_capacity(num_segments);
// segment 1 is second block
if let Some(seg)=segment_header_infos.first(){
segments.push(make_segment_header(BlockId::new(1),seg));
}
// rest of segments start at fourth block
for (i,seg) in segment_header_infos[1..num_segments-1].iter().enumerate(){
make_segment_header(BlockId::new(3+i as u32),seg);
}
// last segment is third block
if let Some(seg)=segment_header_infos.last(){
segments.push(make_segment_header(BlockId::new(2),seg));
}
segments
}else{
// all segments in order
segment_header_infos.iter().enumerate().map(|(i,seg)|
make_segment_header(BlockId::new(1+i as u32),seg)
).collect()
};
let header=Header{
physics_version,
num_segments:num_segments as u32,
segments,
};
// map header is +1
let block_count=1+num_segments as u32;
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
// block_location is one longer than block_count
let mut block_location=Vec::with_capacity(1+block_count as usize);
//probe header length
let mut bot_header_data=Vec::new();
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
// the first block location is the map header
block_location.push(offset);
offset+=bot_header_data.len() as u64;
block_location.push(offset);
// priming includes file header + first 3 blocks [bot header, first segment, last segment]
let priming=if 2<num_segments{
// segment 1 is block 2
if let Some(seg)=segment_header_infos.first(){
offset+=seg.range.len() as u64;
block_location.push(offset);
}
// last segment is block 3
if let Some(seg)=segment_header_infos.last(){
offset+=seg.range.len() as u64;
block_location.push(offset);
}
let priming=offset;
// rest of segments
for seg in &segment_header_infos[1..num_segments-1]{
offset+=seg.range.len() as u64;
block_location.push(offset);
}
priming
}else{
// all segments in order
for seg in &segment_header_infos{
offset+=seg.range.len() as u64;
block_location.push(offset);
}
offset
};
let file_header=crate::file::Header{
fourcc:crate::file::FourCC::Bot,
version:VERSION,
priming,
resource:0,
block_count,
block_location,
};
// write file header
writer.write_le(&file_header).map_err(Error::InvalidData)?;
// write bot header
writer.write(&bot_header_data).map_err(Error::IO)?;
// write blocks
if 2<num_segments{
// segment 1 is block 2
if let Some(seg)=segment_header_infos.first(){
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
// last segment is block 3
if let Some(seg)=segment_header_infos.last(){
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
// rest of segments
for seg in &segment_header_infos[1..num_segments-1]{
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
}else{
// all segments in order
for seg in segment_header_infos{
writer.write(&raw_segments[seg.range]).map_err(Error::IO)?;
}
}
Ok(())
}

View File

@@ -1,3 +1,9 @@
pub const fn flag(b:bool,mask:u8)->u8{ pub const fn flag(b:bool,mask:u8)->u8{
(-(b as i8) as u8)&mask (-(b as i8) as u8)&mask
} }
pub fn bool_from_u8(value:u8)->bool{
value!=0
}
pub fn bool_into_u8(value:&bool)->u8{
*value as u8
}

View File

@@ -38,6 +38,23 @@ pub struct Ratio64Vec2{
pub x:Ratio64, pub x:Ratio64,
pub y:Ratio64, pub y:Ratio64,
} }
impl TryInto<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
type Error=RatioError;
fn try_into(self)->Result<strafesnet_common::integer::Ratio64Vec2,Self::Error>{
Ok(strafesnet_common::integer::Ratio64Vec2{
x:self.x.try_into()?,
y:self.y.try_into()?,
})
}
}
impl From<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
fn from(value:strafesnet_common::integer::Ratio64Vec2)->Self{
Self{
x:value.x.into(),
y:value.y.into(),
}
}
}
pub type Angle32=i32; pub type Angle32=i32;
pub type Planar64=i64; pub type Planar64=i64;

View File

@@ -1,7 +1,9 @@
mod common; mod common;
pub mod aabb; pub mod aabb;
pub mod model; pub mod model;
pub mod mouse;
pub mod integer; pub mod integer;
pub mod physics;
pub mod gameplay_modes; pub mod gameplay_modes;
pub mod gameplay_style; pub mod gameplay_style;
pub mod gameplay_attributes; pub mod gameplay_attributes;

View File

@@ -0,0 +1,25 @@
use super::integer::Time;
#[binrw::binrw]
#[brw(little)]
pub struct MouseState{
pub pos:[i32;2],
pub time:Time,
}
impl<T> Into<strafesnet_common::mouse::MouseState<T>> for MouseState{
fn into(self)->strafesnet_common::mouse::MouseState<T>{
strafesnet_common::mouse::MouseState{
pos:self.pos.into(),
time:strafesnet_common::integer::Time::raw(self.time),
}
}
}
impl<T> From<strafesnet_common::mouse::MouseState<T>> for MouseState{
fn from(value:strafesnet_common::mouse::MouseState<T>)->Self{
Self{
pos:value.pos.to_array(),
time:value.time.get(),
}
}
}

View File

@@ -0,0 +1,156 @@
use super::integer::Time;
use super::common::{bool_from_u8,bool_into_u8};
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
#[binrw::binrw]
#[brw(little)]
pub struct TimedInstruction{
pub time:Time,
pub instruction:Instruction,
}
impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::integer::RatioError;
fn try_into(self)->Result<TimedPhysicsInstruction,Self::Error>{
Ok(strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::raw(self.time),
instruction:self.instruction.try_into()?,
})
}
}
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::physics::InstructionConvert;
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
Ok(Self{
time:value.time.get(),
instruction:value.instruction.try_into()?,
})
}
}
#[binrw::binrw]
#[brw(little)]
pub enum Instruction{
#[brw(magic=0u8)]
ReplaceMouse{
m0:super::mouse::MouseState,
m1:super::mouse::MouseState
},
#[brw(magic=1u8)]
SetNextMouse(super::mouse::MouseState),
#[brw(magic=2u8)]
SetMoveRight(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=3u8)]
SetMoveUp(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=4u8)]
SetMoveBack(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=5u8)]
SetMoveLeft(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=6u8)]
SetMoveDown(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=7u8)]
SetMoveForward(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=8u8)]
SetJump(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=9u8)]
SetZoom(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=10u8)]
Reset,
#[brw(magic=11u8)]
Restart(super::gameplay_modes::ModeId),
#[brw(magic=12u8)]
Spawn(super::gameplay_modes::ModeId,super::gameplay_modes::StageId),
#[brw(magic=13u8)]
PracticeFly,
#[brw(magic=14u8)]
SetSensitivity(super::integer::Ratio64Vec2),
#[brw(magic=255u8)]
Idle,
}
#[derive(Debug)]
pub enum InstructionConvert{
/// This is an instruction that can be dropped when serializing
DropInstruction,
}
impl std::fmt::Display for InstructionConvert{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for InstructionConvert{}
impl TryInto<strafesnet_common::physics::Instruction> for Instruction{
type Error=super::integer::RatioError;
fn try_into(self)->Result<strafesnet_common::physics::Instruction,Self::Error>{
Ok(match self{
Instruction::ReplaceMouse{m0,m1}=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
Instruction::SetNextMouse(m)=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m.into())),
Instruction::SetMoveRight(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state.into())),
Instruction::SetMoveUp(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state.into())),
Instruction::SetMoveBack(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state.into())),
Instruction::SetMoveLeft(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state.into())),
Instruction::SetMoveDown(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state.into())),
Instruction::SetMoveForward(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state.into())),
Instruction::SetJump(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state.into())),
Instruction::SetZoom(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state.into())),
Instruction::Reset=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset),
Instruction::Restart(mode_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::new(mode_id))),
Instruction::Spawn(mode_id,stage_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(
strafesnet_common::gameplay_modes::ModeId::new(mode_id),
strafesnet_common::gameplay_modes::StageId::new(stage_id),
)),
Instruction::PracticeFly=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly),
Instruction::SetSensitivity(sensitivity)=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity.try_into()?)),
Instruction::Idle=>strafesnet_common::physics::Instruction::Idle,
})
}
}
impl TryFrom<strafesnet_common::physics::Instruction> for Instruction{
type Error=InstructionConvert;
fn try_from(value:strafesnet_common::physics::Instruction)->Result<Self,Self::Error>{
match value{
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0,m1})=>Ok(Instruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m))=>Ok(Instruction::SetNextMouse(m.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state))=>Ok(Instruction::SetMoveRight(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state))=>Ok(Instruction::SetMoveUp(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state))=>Ok(Instruction::SetMoveBack(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state))=>Ok(Instruction::SetMoveLeft(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state))=>Ok(Instruction::SetMoveDown(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state))=>Ok(Instruction::SetMoveForward(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state))=>Ok(Instruction::SetJump(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state))=>Ok(Instruction::SetZoom(state.into())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset)=>Ok(Instruction::Reset),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(mode_id))=>Ok(Instruction::Restart(mode_id.get())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(mode_id,stage_id))=>Ok(Instruction::Spawn(
mode_id.get(),
stage_id.get(),
)),
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly)=>Ok(Instruction::PracticeFly),
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity))=>Ok(Instruction::SetSensitivity(sensitivity.into())),
strafesnet_common::physics::Instruction::Idle=>Ok(Instruction::Idle),
}
}
}

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

@@ -10,6 +10,8 @@ pub enum ReadError{
StrafesNET(strafesnet_snf::Error), StrafesNET(strafesnet_snf::Error),
#[cfg(feature="snf")] #[cfg(feature="snf")]
StrafesNETMap(strafesnet_snf::map::Error), StrafesNETMap(strafesnet_snf::map::Error),
#[cfg(feature="snf")]
StrafesNETBot(strafesnet_snf::bot::Error),
Io(std::io::Error), Io(std::io::Error),
UnknownFileFormat, UnknownFileFormat,
} }
@@ -20,28 +22,39 @@ impl std::fmt::Display for ReadError{
} }
impl std::error::Error for ReadError{} impl std::error::Error for ReadError{}
pub enum DataStructure{ 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")]
Source(strafesnet_bsp_loader::Bsp), Source(strafesnet_bsp_loader::Bsp),
#[cfg(feature="snf")] #[cfg(feature="snf")]
StrafesNET(strafesnet_common::map::CompleteMap), SNFM(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")]
SNFB(strafesnet_snf::bot::Segment),
} }
pub fn read<R:Read+std::io::Seek>(input:R)->Result<DataStructure,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(DataStructure::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(DataStructure::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(DataStructure::StrafesNET( 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")]
b"SNFB"=>Ok(ReadFormat::SNFB(
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
.read_all().map_err(ReadError::StrafesNETBot)?
)),
_=>Err(ReadError::UnknownFileFormat), _=>Err(ReadError::UnknownFileFormat),
} }
} }
@@ -59,14 +72,23 @@ impl std::fmt::Display for LoadError{
} }
impl std::error::Error for LoadError{} impl std::error::Error for LoadError{}
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::CompleteMap,LoadError>{ pub enum LoadFormat{
#[cfg(feature="snf")]
Map(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")]
Bot(strafesnet_snf::bot::Segment),
}
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")]
DataStructure::StrafesNET(map)=>Ok(map), ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
#[cfg(feature="snf")]
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
DataStructure::Roblox(model)=>{ ReadFormat::Roblox(model)=>{
let mut place=model.into_place(); let mut place=model.into_place();
place.run_scripts(); place.run_scripts();
@@ -99,10 +121,10 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::Co
) )
); );
Ok(map) Ok(LoadFormat::Map(map))
}, },
#[cfg(feature="source")] #[cfg(feature="source")]
DataStructure::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();
@@ -138,7 +160,7 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::Co
), ),
); );
Ok(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};
@@ -15,13 +16,14 @@ pub enum Instruction{
Render, Render,
Resize(winit::dpi::PhysicalSize<u32>), Resize(winit::dpi::PhysicalSize<u32>),
ChangeMap(strafesnet_common::map::CompleteMap), ChangeMap(strafesnet_common::map::CompleteMap),
LoadReplay(strafesnet_snf::bot::Segment),
} }
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(
@@ -66,9 +68,12 @@ 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)=>{
run_session_instruction!(ins.time,SessionInstruction::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
@@ -215,7 +212,7 @@ pub fn setup_and_start(title:&str){
setup_context, setup_context,
); );
if let Some(arg)=std::env::args().nth(1){ for arg in std::env::args().skip(1){
let path=std::path::PathBuf::from(arg); let path=std::path::PathBuf::from(arg);
window_thread.send(TimedInstruction{ window_thread.send(TimedInstruction{
time:integer::Time::ZERO, time:integer::Time::ZERO,

View File

@@ -1,8 +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::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>),
@@ -29,8 +31,9 @@ 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(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(),
Err(e)=>println!("Failed to load map: {e}"), Ok(LoadFormat::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}).unwrap(),
Err(e)=>println!("Failed to load file: {e}"),
} }
}, },
winit::event::WindowEvent::Focused(state)=>{ winit::event::WindowEvent::Focused(state)=>{
@@ -148,11 +151,13 @@ 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),
"X"|"x"=>session_ctrl!(StopSpectate,s), "X"|"x"=>session_ctrl!(StopSpectate,s),
"N"|"n"=>session_ctrl!(SaveReplay,s),
"J"|"j"=>session_ctrl!(LoadIntoReplayState,s),
_=>None, _=>None,
}, },
_=>None, _=>None,
@@ -206,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 "$@"