Compare commits

..

104 Commits

Author SHA1 Message Date
2c729adf64 implement new bsp_loader 2025-01-29 12:20:24 -08:00
3467bc77b0 implement new rbx_loader 2025-01-29 12:20:24 -08:00
2cf5ff5059 impl deferred_loader::Loader for bsp_loader 2025-01-29 12:20:24 -08:00
b550778a60 impl deferred_loader::Loader for rbx_loader 2025-01-29 12:18:55 -08:00
69599b23be move rbxassetid into lib 2025-01-29 11:17:12 -08:00
2d9ad990c2 refactor deferred_loader 2025-01-29 11:17:12 -08:00
be9eb30a5c minimize lints 2025-01-24 05:20:22 -08:00
42310c5d2b convert to app 2025-01-23 06:07:46 -08:00
988750a3c6 physics: typo 2025-01-22 14:52:47 -08:00
2f26662dda it: drop strafe-client::file dependency 2025-01-22 09:37:16 -08:00
6f739dba8d add integration testing 2025-01-22 09:36:35 -08:00
2eaddd493d session: use replay directory 2025-01-22 08:57:15 -08:00
d5a6f8e1bc compute UserSettings from directories 2025-01-22 08:54:05 -08:00
95fb43b908 settings: implement directories 2025-01-22 08:52:27 -08:00
3e814cb41a settings: add directories dep 2025-01-22 07:47:41 -08:00
ea854a548f Update README.md 2025-01-22 07:10:04 -08:00
5f2cf8f32e physics: change body api 2025-01-22 07:00:42 -08:00
b6b090de78 physics: place version code into lib.rs 2025-01-22 06:45:56 -08:00
affbada62e move replay tests into test module 2025-01-22 05:48:08 -08:00
8d2ba28700 move engine components into modules 2025-01-22 05:48:08 -08:00
3eb4e76ab2 replace .map_or(None, with .and_then( 2025-01-22 05:47:32 -08:00
5d31419370 use Range to express time range 2025-01-22 05:47:32 -08:00
44a58044c7 readme: try it out 2025-01-22 05:47:32 -08:00
83ac776b78 Revert "bodge surfs"
This reverts commit 65b49d2726.
2025-01-22 05:47:32 -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
75c8bc2bbb fix timescale 2025-01-17 21:47:02 -08:00
d49a6b2f0a file::Header::calculate_size 2025-01-17 21:29:04 -08:00
19778ac7aa update wgpu 2025-01-17 11:47:23 -08:00
5b62052222 snf: move newtypes.rs into newtypes/mod.rs 2025-01-16 21:29:46 -08:00
5a8bc141d3 tweak ModeInstruction and document meaning 2025-01-16 20:54:02 -08:00
83a067320b fix usage of Cow in model_physics 2025-01-16 19:31:49 -08:00
2faa61225f refactor physics to use shared context for multiple simulations 2025-01-16 18:51:22 -08:00
28499800cb comment about bad algorithm 2025-01-16 07:40:08 -08:00
57cc49dc1a explicitly reset and spawn instead of implicitly 2025-01-16 06:35:36 -08:00
d517b78a8c replay controls 2025-01-16 05:10:39 -08:00
e6d1d69241 delete replay on StopSpectate 2025-01-16 05:10:39 -08:00
b28fa25279 comment about determinism 2025-01-16 05:10:39 -08:00
713b235816 maintain replay state according to real time 2025-01-16 05:10:39 -08:00
d2002383cb idle is special 2025-01-16 00:17:39 -08:00
52f7de809d rename variable 2025-01-16 00:17:39 -08:00
4efe1209b8 implement copy instruction into replay 2025-01-15 23:44:42 -08:00
15a9136fc4 session instruction changes for control and playback 2025-01-15 22:59:59 -08:00
035736e7af record 2025-01-15 21:41:44 -08:00
7f9a16a56d refactor physics enums so Mouse-NonMouse distinction is private to mouse_interpolator 2025-01-15 21:09:08 -08:00
814e573d91 rename physics instructions 2025-01-15 20:19:20 -08:00
6fa0f1c83e model_physics: use entry or_insert_with pattern 2025-01-15 08:46:26 -08:00
870cb56dac do not use ResourceType 2025-01-15 08:46:15 -08:00
1aac2a9303 model_physics: remove pointless unsafe usage from the before times 2025-01-15 08:46:06 -08:00
38661b3a68 fixed_wide: another derivable trait 2025-01-15 04:15:41 -08:00
7ad76740d4 common: tidy instruction module 2025-01-15 03:29:48 -08:00
c2d6af8bda TimedInstruction::set_time 2025-01-15 03:29:48 -08:00
fbacef83b9 fix non-determinism bug 2025-01-15 03:16:00 -08:00
2a9e848541 explain non-determinism bug 2025-01-15 03:03:34 -08:00
3413ec8740 mouse_interpolator tweaks 2025-01-15 02:59:34 -08:00
168d6708d1 fix some physics lints 2025-01-15 01:34:10 -08:00
13cae4c7c5 rewrite mouse_interpolator, introduce session 2025-01-15 01:01:24 -08:00
0dc462a3b1 comment infinite loop avoidance 2025-01-09 20:38:32 -08:00
ca003edbc3 reintroduce generics to Instruction traits 2025-01-09 20:11:00 -08:00
16abe23e97 push solve tweaks 2025-01-09 06:27:50 -08:00
67f8569178 push_solve infallible type signature 2025-01-09 05:56:11 -08:00
121c9c5258 resize immediately 2025-01-09 05:36:55 -08:00
411b997b87 use mold linker because it's faster 2025-01-09 05:36:55 -08:00
4d587b6c0b fixed_wide: clippy angry about derivable traits 2025-01-08 23:33:39 -08:00
6ff74f08ab roblox_emulator: deref returns correct type 2025-01-08 23:31:56 -08:00
88 changed files with 3475 additions and 1936 deletions

@ -1,2 +1,2 @@
[registries.strafesnet]
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"

518
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,5 +1,10 @@
[workspace]
members = [
"engine/graphics",
"engine/physics",
"engine/session",
"engine/settings",
"integration-testing",
"lib/bsp_loader",
"lib/common",
"lib/deferred_loader",
@ -7,6 +12,7 @@ members = [
"lib/linear_ops",
"lib/ratio_ops",
"lib/rbx_loader",
"lib/rbxassetid",
"lib/roblox_emulator",
"lib/snf",
"strafe-client",

@ -3,6 +3,9 @@
# Strafe Project
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
1. Have rust and git installed
2. `git clone https://git.itzana.me/StrafesNET/strafe-project`
@ -10,4 +13,4 @@ Monorepo for working on projects related to strafe client.
4. `cargo run --release --bin strafe-client`
## Licenses
Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client has a sole proprietor license.
Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client and engine crates have a sole proprietor license.

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

8
engine/graphics/LICENSE Normal file

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

@ -1,10 +1,15 @@
use std::borrow::Cow;
use std::collections::{HashSet,HashMap};
use strafesnet_common::map;
use strafesnet_common::integer;
use strafesnet_settings::settings;
use strafesnet_session::session;
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
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{
count:u32,
@ -137,7 +142,7 @@ impl GraphicsState{
pub fn clear(&mut self){
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();
}
pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
@ -449,7 +454,7 @@ impl GraphicsState{
//.into_iter() the modeldata vec so entities can be /moved/ to models.entities
let mut model_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;
self.models.reserve(models.len());
for model in models.into_iter(){
@ -609,7 +614,7 @@ impl GraphicsState{
// Create the render pipeline
let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{
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
@ -637,10 +642,10 @@ impl GraphicsState{
wgpu::TextureFormat::Astc{
block:AstcBlock::B4x4,
channel:AstcChannel::UnormSrgb,
}=>&include_bytes!("../images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../images/bgra.dds")[..],
}=>&include_bytes!("../../../strafe-client/images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../../../strafe-client/images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../../../strafe-client/images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../../../strafe-client/images/bgra.dds")[..],
_=>unreachable!(),
};
@ -683,7 +688,7 @@ impl GraphicsState{
//squid
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();
@ -865,7 +870,7 @@ impl GraphicsState{
&mut self,
device:&wgpu::Device,
config:&wgpu::SurfaceConfiguration,
user_settings:&crate::settings::UserSettings,
user_settings:&settings::UserSettings,
){
self.depth_view=Self::create_depth_texture(config,device);
self.camera.screen_size=glam::uvec2(config.width,config.height);
@ -876,7 +881,7 @@ impl GraphicsState{
view:&wgpu::TextureView,
device:&wgpu::Device,
queue:&wgpu::Queue,
frame_state:crate::physics_worker::FrameState,
frame_state:session::FrameState,
){
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input

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

10
engine/physics/Cargo.toml Normal file

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

8
engine/physics/LICENSE Normal file

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

@ -31,6 +31,14 @@ impl<T> Body<T>
time,
}
}
pub const fn relative_to<'a>(&'a self,body0:&'a Body<T>)->VirtualBody<'a,T>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1:self,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.position
@ -82,7 +90,7 @@ impl<T> Body<T>
// a*dt + v
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.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into();
@ -137,14 +145,6 @@ pub struct VirtualBody<'a,T>{
impl<T> VirtualBody<'_,T>
where Time<T>:Copy,
{
pub const fn relative<'a>(body0:&'a Body<T>,body1:&'a Body<T>)->VirtualBody<'a,T>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
}

@ -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 crate::physics::{Time,Body};
@ -12,6 +12,20 @@ pub enum CrawlResult<M:MeshQuery>{
Miss(FEV<M>),
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>
where

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

@ -0,0 +1,45 @@
mod body;
mod push_solve;
mod face_crawler;
mod model;
pub mod physics;
// Physics bug fixes can easily desync all bots.
//
// When replaying a bot, use the exact physics version which it was recorded with.
//
// When validating a new bot, ignore the version and use the latest version,
// and overwrite the version in the file.
//
// Compatible physics versions should be determined
// empirically at development time via leaderboard resimulation.
//
// Compatible physics versions should result in an identical leaderboard state,
// or the only bots which fail are ones exploiting a surgically patched bug.
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
pub struct PhysicsVersion(u32);
pub const VERSION:PhysicsVersion=PhysicsVersion(0);
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
let compat=[0];
let mut input_version=0;
while input_version<compat.len(){
// compatible version must be greater than or equal to the input version
assert!(input_version as u32<=compat[input_version]);
// compatible version must be a version that exists
assert!(compat[input_version]<=VERSION.0);
input_version+=1;
}
compat
};
pub enum PhysicsVersionError{
UnknownPhysicsVersion,
}
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
}else{
Err(PhysicsVersionError::UnknownPhysicsVersion)
}
}

@ -1,5 +1,6 @@
use std::borrow::{Borrow,Cow};
use std::collections::{HashSet,HashMap};
use core::ops::Range;
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@ -84,11 +85,11 @@ pub trait MeshQuery{
}
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:Self::Face)->Cow<Vec<Self::Edge>>;
fn face_edges(&self,face_id:Self::Face)->Cow<[Self::Edge]>;
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Face;2]>;
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Vert;2]>;
fn vert_edges(&self,vert_id:Self::Vert)->Cow<Vec<Self::Edge>>;
fn vert_faces(&self,vert_id:Self::Vert)->Cow<Vec<Self::Face>>;
fn vert_edges(&self,vert_id:Self::Vert)->Cow<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->Cow<[Self::Face]>;
}
struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>,
@ -279,15 +280,12 @@ struct EdgePool{
}
impl EdgePool{
fn push(&mut self,edge_ref_verts:EdgeRefVerts)->(&mut EdgeRefFaces,SubmeshEdgeId){
let edge_id=if let Some(&edge_id)=self.edge_id_from_guy.get(&edge_ref_verts){
edge_id
}else{
let edge_id=*self.edge_id_from_guy.entry(edge_ref_verts.clone()).or_insert_with(||{
let edge_id=SubmeshEdgeId::new(self.edge_guys.len() as u32);
self.edge_guys.push((edge_ref_verts.clone(),EdgeRefFaces::new()));
self.edge_id_from_guy.insert(edge_ref_verts,edge_id);
self.edge_guys.push((edge_ref_verts,EdgeRefFaces::new()));
edge_id
};
(&mut unsafe{self.edge_guys.get_unchecked_mut(edge_id.get() as usize)}.1,edge_id)
});
(&mut self.edge_guys[edge_id.get() as usize].1,edge_id)
}
}
@ -322,16 +320,11 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
let mut submesh_verts=Vec::new();
let mut submesh_vert_id_from_mesh_vert_id=HashMap::<MeshVertId,SubmeshVertId>::new();
//lazy closure
let mut get_submesh_vert_id=|vert_id:MeshVertId|{
if let Some(&submesh_vert_id)=submesh_vert_id_from_mesh_vert_id.get(&vert_id){
submesh_vert_id
}else{
let submesh_vert_id=SubmeshVertId::new(submesh_verts.len() as u32);
submesh_verts.push(vert_id);
submesh_vert_id_from_mesh_vert_id.insert(vert_id,submesh_vert_id);
submesh_vert_id
}
};
let mut get_submesh_vert_id=|vert_id:MeshVertId|*submesh_vert_id_from_mesh_vert_id.entry(vert_id).or_insert_with(||{
let submesh_vert_id=SubmeshVertId::new(submesh_verts.len() as u32);
submesh_verts.push(vert_id);
submesh_vert_id
});
let mut edge_pool=EdgePool::default();
let mut vert_ref_guys=vec![VertRefGuy::default();mesh.unique_pos.len()];
let mut face_ref_guys=Vec::new();
@ -364,10 +357,10 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
edge_ref_faces.push(!is_sorted as usize,submesh_face_id);
//index edges & face into vertices
{
let vert_ref_guy=unsafe{vert_ref_guys.get_unchecked_mut(submesh_vert0_id.get() as usize)};
let vert_ref_guy=&mut vert_ref_guys[submesh_vert0_id.get() as usize];
vert_ref_guy.edges.insert(edge_id.as_directed(is_sorted));
vert_ref_guy.faces.insert(submesh_face_id);
unsafe{vert_ref_guys.get_unchecked_mut(submesh_vert1_id.get() as usize)}.edges.insert(edge_id.as_directed(!is_sorted));
vert_ref_guys[submesh_vert1_id.get() as usize].edges.insert(edge_id.as_directed(!is_sorted));
}
//return directed_edge_id
edge_id.as_directed(is_sorted)
@ -442,7 +435,7 @@ impl MeshQuery for PhysicsMeshView<'_>{
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
self.data.verts[vert_idx].0
}
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<Vec<SubmeshDirectedEdgeId>>{
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.face_topology[face_id.get() as usize].edges)
}
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
@ -451,10 +444,10 @@ impl MeshQuery for PhysicsMeshView<'_>{
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].verts)
}
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshDirectedEdgeId>>{
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].edges)
}
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshFaceId>>{
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].faces)
}
}
@ -520,7 +513,7 @@ impl MeshQuery for TransformedMesh<'_>{
self.transform.vertex.transform_point3(self.view.vert(vert_id)).fix_1()
}
#[inline]
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<Vec<SubmeshDirectedEdgeId>>{
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
self.view.face_edges(face_id)
}
#[inline]
@ -532,11 +525,11 @@ impl MeshQuery for TransformedMesh<'_>{
self.view.edge_verts(edge_id)
}
#[inline]
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshDirectedEdgeId>>{
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
self.view.vert_edges(vert_id)
}
#[inline]
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshFaceId>>{
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
self.view.vert_faces(vert_id)
}
}
@ -712,48 +705,54 @@ impl MinkowskiMesh<'_>{
},
}
}
fn closest_fev_not_inside(&self,mut infinity_body:Body)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().map_or(None,|dir|{
// TODO: fundamentally improve this algorithm.
// All it needs to do is find the closest point on the mesh
// and return the FEV which the point resides on.
//
// What it actually does is use the above functions to trace a ray in from infinity,
// crawling the closest point along the mesh surface until the ray reaches
// the starting point to discover the final FEV.
//
// The actual collision prediction probably does a single test
// and then immediately returns with 0 FEV transitions on average,
// because of the strict time_limit constraint.
//
// Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola
infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev
// TODO: change crawl_fev args to delta time? Optional values?
match infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time){
crate::face_crawler::CrawlResult::Miss(fev)=>Some(fev),
crate::face_crawler::CrawlResult::Hit(_,_)=>None,
}
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
})
}
pub fn predict_collision_in(&self,relative_body:&Body,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(relative_body.clone()).map_or(None,|fev|{
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(),start_time).and_then(|fev|{
//continue forwards along the body parabola
match fev.crawl(self,relative_body,relative_body.time,time_limit){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
fev.crawl(self,relative_body,start_time,time_limit).hit()
})
}
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
let infinity_body=Body::new(
relative_body.extrapolated_position(time_limit),
-relative_body.extrapolated_velocity(time_limit),
relative_body.acceleration,
-time_limit,
);
self.closest_fev_not_inside(infinity_body).map_or(None,|fev|{
let infinity_body=-relative_body.clone();
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
//continue backwards along the body parabola
match fev.crawl(self,&-relative_body.clone(),-time_limit,-relative_body.time){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,-time)),//no need to test -time<time_limit because of the first step
}
fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
//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)
//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 r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
@ -769,7 +768,7 @@ impl MinkowskiMesh<'_>{
//WARNING! d outside of *2
//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()){
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_edge=Some(directed_edge_id);
break;
@ -780,10 +779,7 @@ impl MinkowskiMesh<'_>{
}
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
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){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
}
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
@ -832,7 +828,7 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn face_edges(&self,face_id:MinkowskiFace)->Cow<Vec<MinkowskiDirectedEdge>>{
fn face_edges(&self,face_id:MinkowskiFace)->Cow<[MinkowskiDirectedEdge]>{
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
Cow::Owned(self.mesh1.face_edges(f1).iter().map(|&edge_id1|{
@ -933,7 +929,7 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<Vec<MinkowskiDirectedEdge>>{
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<[MinkowskiDirectedEdge]>{
match vert_id{
MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new();
@ -975,7 +971,7 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<Vec<MinkowskiFace>>{
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<[MinkowskiFace]>{
unimplemented!()
}
}
@ -989,7 +985,7 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
for k in 0..len{
if k!=i&&k!=j{
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
if comp^d{
return true;
@ -1009,9 +1005,3 @@ fn test_is_empty_volume(){
assert!(!is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3()].to_vec()));
assert!(is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3(),vec3::NEG_X.fix_3()].to_vec()));
}
#[test]
fn build_me_a_cube(){
let mesh=PhysicsMesh::unit_cube();
//println!("mesh={:?}",mesh);
}

File diff suppressed because it is too large Load Diff

@ -164,8 +164,8 @@ fn is_space_enclosed_4(
||is_space_enclosed_3(b,c,d)
}
const fn get_push_ray_0(point:Planar64Vec3)->Option<Ray>{
Some(Ray{origin:point,direction:vec3::ZERO})
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
Ray{origin:point,direction:vec3::ZERO}
}
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
let direction=solve1(c0)?.divide().fix_1();
@ -204,11 +204,8 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
Some(Ray{origin,direction})
}
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->Option<(Ray,Conts<'a>)>{
match get_push_ray_0(point){
Some(ray)=>Some((ray,Conts::new_const())),
None=>None,
}
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
(get_push_ray_0(point),Conts::new_const())
}
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts)>{
get_push_ray_1(point,c0)
@ -280,19 +277,19 @@ fn get_best_push_ray_and_conts_4<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Co
fn get_best_push_ray_and_conts<'a>(
point:Planar64Vec3,
conts:Conts<'a>,
conts:&[&'a Contact],
)->Option<(Ray,Conts<'a>)>{
match conts.as_slice(){
match conts{
&[c0,c1,c2,c3]=>get_best_push_ray_and_conts_4(point,c0,c1,c2,c3),
&[c0,c1,c2]=>get_best_push_ray_and_conts_3(point,c0,c1,c2),
&[c0,c1]=>get_best_push_ray_and_conts_2(point,c0,c1),
&[c0]=>get_best_push_ray_and_conts_1(point,c0),
&[]=>get_best_push_ray_and_conts_0(point),
&[]=>Some(get_best_push_ray_and_conts_0(point)),
_=>unreachable!(),
}
}
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()
.filter(|&contact|
!conts.iter().any(|&c|std::ptr::eq(c,contact))
@ -302,17 +299,16 @@ fn get_first_touch<'a>(contacts:&'a Vec<Contact>,ray:&Ray,conts:&Conts)->Option<
.min_by_key(|&(t,_)|t)
}
pub fn push_solve(contacts:&Vec<Contact>,point:Planar64Vec3)->Option<Planar64Vec3>{
const ZERO:Ratio<Fixed<1,32>,Fixed<1,32>>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point)?;
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point);
loop{
let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){
Some((t,conts))=>(t,conts),
None=>return Some(ray.origin),
Some((t,cont))=>(t,cont),
None=>return ray.origin,
};
if ZERO.le_ratio(next_t){
return Some(ray.origin);
if RATIO_ZERO.le_ratio(next_t){
return ray.origin;
}
//push_front
@ -326,9 +322,9 @@ pub fn push_solve(contacts:&Vec<Contact>,point:Planar64Vec3)->Option<Planar64Vec
}
let meet_point=ray.extrapolate(next_t);
match get_best_push_ray_and_conts(meet_point,conts){
match get_best_push_ray_and_conts(meet_point,conts.as_slice()){
Some((new_ray,new_conts))=>(ray,conts)=(new_ray,new_conts),
None=>return Some(meet_point),
None=>return meet_point,
}
}
}
@ -346,7 +342,7 @@ mod tests{
}
];
assert_eq!(
Some(vec3::ZERO),
vec3::ZERO,
push_solve(&contacts,vec3::NEG_Y)
);
}

12
engine/session/Cargo.toml Normal file

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

8
engine/session/LICENSE Normal file

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

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

@ -0,0 +1,281 @@
use strafesnet_common::mouse::MouseState;
use strafesnet_common::physics::{
MouseInstruction,SetControlInstruction,ModeInstruction,MiscInstruction,
Instruction as PhysicsInstruction,
TimeInner as PhysicsTimeInner,
Time as PhysicsTime,
};
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTimeInner>;
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTimeInner>;
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
/// To be fed into MouseInterpolator
#[derive(Clone,Debug)]
pub(crate) enum Instruction{
MoveMouse(glam::IVec2),
SetControl(SetControlInstruction),
Mode(ModeInstruction),
Misc(MiscInstruction),
Idle,
}
#[derive(Clone,Debug)]
enum UnbufferedInstruction{
MoveMouse(glam::IVec2),
NonMouse(NonMouseInstruction),
}
#[derive(Clone,Debug)]
enum BufferedInstruction{
Mouse(MouseInstruction),
NonMouse(NonMouseInstruction),
}
#[derive(Clone,Debug)]
pub(crate) enum NonMouseInstruction{
SetControl(SetControlInstruction),
Mode(ModeInstruction),
Misc(MiscInstruction),
Idle,
}
impl From<Instruction> for UnbufferedInstruction{
#[inline]
fn from(value:Instruction)->Self{
match value{
Instruction::MoveMouse(mouse_instruction)=>UnbufferedInstruction::MoveMouse(mouse_instruction),
Instruction::SetControl(set_control_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::SetControl(set_control_instruction)),
Instruction::Mode(mode_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Mode(mode_instruction)),
Instruction::Misc(misc_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Misc(misc_instruction)),
Instruction::Idle=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Idle),
}
}
}
impl From<BufferedInstruction> for PhysicsInstruction{
#[inline]
fn from(value:BufferedInstruction)->Self{
match value{
BufferedInstruction::Mouse(mouse_instruction)=>PhysicsInstruction::Mouse(mouse_instruction),
BufferedInstruction::NonMouse(non_mouse_instruction)=>match non_mouse_instruction{
NonMouseInstruction::SetControl(set_control_instruction)=>PhysicsInstruction::SetControl(set_control_instruction),
NonMouseInstruction::Mode(mode_instruction)=>PhysicsInstruction::Mode(mode_instruction),
NonMouseInstruction::Misc(misc_instruction)=>PhysicsInstruction::Misc(misc_instruction),
NonMouseInstruction::Idle=>PhysicsInstruction::Idle,
},
}
}
}
pub(crate) enum StepInstruction{
Pop,
Timeout,
}
#[derive(Clone,Debug)]
enum BufferState{
Unbuffered,
Initializing(SessionTime,MouseState<PhysicsTimeInner>),
Buffered(SessionTime,MouseState<PhysicsTimeInner>),
}
pub struct MouseInterpolator{
buffer_state:BufferState,
// double timestamped timeline?
buffer:std::collections::VecDeque<TimedPhysicsInstruction>,
output:std::collections::VecDeque<TimedPhysicsInstruction>,
}
// Maybe MouseInterpolator manipulation is better expressed using impls
// and called from Instruction trait impls in session
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:DoubleTimedSelfInstruction){
self.push_unbuffered_input(ins.time,ins.instruction.time,ins.instruction.instruction.into())
}
}
impl InstructionEmitter<StepInstruction> for MouseInterpolator{
type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
self.buffered_instruction_with_timeout(time_limit)
}
}
impl MouseInterpolator{
pub fn new()->MouseInterpolator{
MouseInterpolator{
buffer_state:BufferState::Unbuffered,
buffer:std::collections::VecDeque::new(),
output:std::collections::VecDeque::new(),
}
}
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
self.buffer.push_front(TimedInstruction{
time:ins.time,
instruction:BufferedInstruction::Mouse(ins.instruction).into(),
});
// flush buffer to output
if self.output.len()==0{
// swap buffers
core::mem::swap(&mut self.buffer,&mut self.output);
}else{
// append buffer contents to output
self.output.append(&mut self.buffer);
}
}
fn get_mouse_timedout_at(&self,time_limit:SessionTime)->Option<SessionTime>{
match &self.buffer_state{
BufferState::Unbuffered=>None,
BufferState::Initializing(time,_mouse_state)
|BufferState::Buffered(time,_mouse_state)=>{
let timeout=*time+MOUSE_TIMEOUT;
(timeout<time_limit).then_some(timeout)
}
}
}
fn timeout_mouse(&mut self,timeout_time:PhysicsTime){
// the state always changes to unbuffered
let buffer_state=core::mem::replace(&mut self.buffer_state,BufferState::Unbuffered);
match buffer_state{
BufferState::Unbuffered=>(),
BufferState::Initializing(_time,mouse_state)=>{
// only a single mouse move was sent in 10ms, this is very much an edge case!
self.push_mouse_and_flush_buffer(TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::ReplaceMouse{
m1:MouseState{pos:mouse_state.pos,time:timeout_time},
m0:mouse_state,
},
});
}
BufferState::Buffered(_time,mouse_state)=>{
// duplicate the currently buffered mouse state but at a later (future, from the physics perspective) time
self.push_mouse_and_flush_buffer(TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::SetNextMouse(MouseState{pos:mouse_state.pos,time:timeout_time}),
});
},
}
}
fn push_unbuffered_input(&mut self,session_time:SessionTime,physics_time:PhysicsTime,ins:UnbufferedInstruction){
// new input
// if there is zero instruction buffered, it means the mouse is not moving
// case 1: unbuffered
// no mouse event is buffered
// - ins is mouse event? change to buffered
// - ins other -> write to timeline
// case 2: buffered
// a mouse event is buffered, and exists within the last 10ms
// case 3: stop
// a mouse event is buffered, but no mouse events have transpired within 10ms
// replace_with allows the enum variant to safely be replaced
// from behind a mutable reference, but a panic in the closure means that
// the entire program terminates rather than completing an unwind.
let (ins_mouse,ins_other)=replace_with::replace_with_or_abort_and_return(&mut self.buffer_state,|buffer_state|{
match ins{
UnbufferedInstruction::MoveMouse(pos)=>{
let next_mouse_state=MouseState{pos,time:physics_time};
match buffer_state{
BufferState::Unbuffered=>{
((None,None),BufferState::Initializing(session_time,next_mouse_state))
},
BufferState::Initializing(_time,mouse_state)=>{
let ins_mouse=TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::ReplaceMouse{
m0:mouse_state,
m1:next_mouse_state.clone(),
},
};
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
},
BufferState::Buffered(_time,mouse_state)=>{
let ins_mouse=TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::SetNextMouse(next_mouse_state.clone()),
};
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
},
}
},
UnbufferedInstruction::NonMouse(other_instruction)=>((None,Some(TimedInstruction{
time:physics_time,
instruction:other_instruction,
})),buffer_state),
}
});
if let Some(ins)=ins_mouse{
self.push_mouse_and_flush_buffer(ins);
}
if let Some(ins)=ins_other{
let instruction=TimedInstruction{
time:ins.time,
instruction:BufferedInstruction::NonMouse(ins.instruction).into(),
};
if matches!(self.buffer_state,BufferState::Unbuffered){
self.output.push_back(instruction);
}else{
self.buffer.push_back(instruction);
}
}
}
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
match self.get_mouse_timedout_at(time_limit){
Some(timeout)=>Some(TimedInstruction{
time:timeout,
instruction:StepInstruction::Timeout,
}),
None=>(self.output.len()!=0).then_some(TimedInstruction{
// this timestamp should not matter
time:time_limit,
instruction:StepInstruction::Pop,
}),
}
}
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedPhysicsInstruction>{
match ins.instruction{
StepInstruction::Pop=>(),
StepInstruction::Timeout=>self.timeout_mouse(ins.time),
}
self.output.pop_front()
}
}
#[cfg(test)]
mod test{
use super::*;
#[test]
fn test(){
let mut interpolator=MouseInterpolator::new();
let timer=strafesnet_common::timer::Timer::<strafesnet_common::timer::Scaled<SessionTimeInner,PhysicsTimeInner>>::unpaused(SessionTime::ZERO,PhysicsTime::from_secs(1000));
macro_rules! push{
($time:expr,$ins:expr)=>{
println!("in={:?}",$ins);
interpolator.push_unbuffered_input(
$time,
timer.time($time),
$ins,
);
while let Some(ins)=interpolator.buffered_instruction_with_timeout($time){
let ins_retimed=TimedInstruction{
time:timer.time(ins.time),
instruction:ins.instruction,
};
let out=interpolator.pop_buffered_instruction(ins_retimed);
println!("out={out:?}");
}
};
}
// test each buffer_state transition
let mut t=SessionTime::ZERO;
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(5);
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(5);
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(1);
}
}

@ -0,0 +1,443 @@
use std::collections::HashMap;
use strafesnet_common::gameplay_modes::{ModeId,StageId};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
// session represents the non-hardware state of the client.
// Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
use strafesnet_common::physics::{
ModeInstruction,MiscInstruction,
Instruction as PhysicsInputInstruction,
TimeInner as PhysicsTimeInner,
Time as PhysicsTime
};
use strafesnet_common::timer::{Scaled,Timer};
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
use strafesnet_settings::directories::Directories;
use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
use strafesnet_settings::settings::UserSettings;
pub enum Instruction<'a>{
Input(SessionInputInstruction),
Control(SessionControlInstruction),
Playback(SessionPlaybackInstruction),
ChangeMap(&'a strafesnet_common::map::CompleteMap),
LoadReplay(strafesnet_snf::bot::Segment),
Idle,
}
pub enum SessionInputInstruction{
Mouse(glam::IVec2),
SetControl(strafesnet_common::physics::SetControlInstruction),
Mode(ImplicitModeInstruction),
Misc(strafesnet_common::physics::MiscInstruction),
}
/// Implicit mode instruction are fed separately to session.
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
#[derive(Clone,Debug)]
pub enum ImplicitModeInstruction{
ResetAndRestart,
ResetAndSpawn(ModeId,StageId),
}
pub enum SessionControlInstruction{
SetPaused(bool),
// copy the current session simulation recording into a replay and view it
CopyRecordingIntoReplayAndSpectate,
StopSpectate,
SaveReplay,
LoadIntoReplayState,
}
pub enum SessionPlaybackInstruction{
SkipForward,
SkipBack,
TogglePaused,
DecreaseTimescale,
IncreaseTimescale,
}
pub struct FrameState{
pub body:physics::Body,
pub camera:physics::PhysicsCamera,
pub time:PhysicsTime,
}
pub struct Simulation{
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:physics::PhysicsState,
}
impl Simulation{
pub const fn new(
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:physics::PhysicsState,
)->Self{
Self{
timer,
physics,
}
}
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
FrameState{
body:self.physics.camera_body(),
camera:self.physics.camera(),
time:self.timer.time(time),
}
}
}
#[derive(Default)]
pub struct Recording{
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
}
impl Recording{
pub fn new(
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
)->Self{
Self{instructions}
}
fn clear(&mut self){
self.instructions.clear();
}
}
pub struct Replay{
next_instruction_id:usize,
recording:Recording,
simulation:Simulation,
}
impl Replay{
pub const fn new(
recording:Recording,
simulation:Simulation,
)->Self{
Self{
next_instruction_id:0,
recording,
simulation,
}
}
pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){
let mut time=self.simulation.timer.time(time_limit);
loop{
if let Some(ins)=self.recording.instructions.get(self.next_instruction_id){
if ins.time<time{
PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone());
self.next_instruction_id+=1;
}else{
break;
}
}else{
// loop playback
self.next_instruction_id=0;
// 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);
self.simulation.timer.set_time(time_limit,new_time);
time=new_time;
}
}
}
}
#[derive(Clone,Copy,Hash,PartialEq,Eq)]
struct BotId(u32);
//#[derive(Clone,Copy,Hash,PartialEq,Eq)]
//struct PlayerId(u32);
enum ViewState{
Play,
//Spectate(PlayerId),
Replay(BotId),
}
pub struct Session{
directories:Directories,
user_settings:UserSettings,
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
view_state:ViewState,
//gui:GuiState
geometry_shared:physics::PhysicsData,
simulation:Simulation,
// below fields not included in lite session
recording:Recording,
//players:HashMap<PlayerId,Simulation>,
replays:HashMap<BotId,Replay>,
}
impl Session{
pub fn new(
user_settings:UserSettings,
directories:Directories,
simulation:Simulation,
)->Self{
Self{
user_settings,
directories,
mouse_interpolator:MouseInterpolator::new(),
geometry_shared:Default::default(),
simulation,
view_state:ViewState::Play,
recording:Default::default(),
replays:HashMap::new(),
}
}
fn clear_recording(&mut self){
self.recording.clear();
}
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
self.simulation.physics.clear();
self.geometry_shared.generate_models(map);
}
pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
match &self.view_state{
ViewState::Play=>Some(self.simulation.get_frame_state(time)),
ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
replay.simulation.get_frame_state(time)
),
}
}
pub fn user_settings(&self)->&UserSettings{
&self.user_settings
}
}
// mouseinterpolator consumes RawInputInstruction
// mouseinterpolator emits PhysicsInputInstruction
// mouseinterpolator consumes DoStep to move on to the next emitted instruction
// Session comsumes SessionInstruction -> forwards RawInputInstruction to mouseinterpolator
// Session consumes DoStep -> forwards DoStep to mouseinterpolator
// Session emits DoStep
impl InstructionConsumer<Instruction<'_>> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::TimeInner>){
// repetitive procedure macro
macro_rules! run_mouse_interpolator_instruction{
($instruction:expr)=>{
self.mouse_interpolator.process_instruction(TimedInstruction{
time:ins.time,
instruction:TimedInstruction{
time:self.simulation.timer.time(ins.time),
instruction:$instruction,
},
});
};
}
// process any timeouts that occured since the last instruction
self.process_exhaustive(ins.time);
match ins.instruction{
// send it down to MouseInterpolator with two timestamps, SessionTime and PhysicsTime
Instruction::Input(SessionInputInstruction::Mouse(pos))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::MoveMouse(pos));
},
Instruction::Input(SessionInputInstruction::SetControl(set_control_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::SetControl(set_control_instruction));
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndRestart))=>{
self.clear_recording();
let mode_id=self.simulation.physics.mode();
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
self.clear_recording();
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
},
Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));
},
Instruction::Control(SessionControlInstruction::SetPaused(paused))=>{
// don't flush the buffered instructions in the mouse interpolator
// until the mouse is confirmed to be not moving at a later time
// what if they pause for 5ms lmao
_=self.simulation.timer.set_paused(ins.time,paused);
},
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
// Bind: B
// pause simulation
_=self.simulation.timer.set_paused(ins.time,true);
// create recording
let mut recording=Recording::default();
recording.instructions.extend(self.recording.instructions.iter().cloned());
// 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::Control(SessionControlInstruction::StopSpectate)=>{
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
// delete the bot, otherwise it's inaccessible and wastes CPU
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>{
self.replays.remove(&bot_id);
},
}
_=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 mut replays_path=self.directories.replays.clone();
let file_name=format!("{}.snfb",ins.time);
std::thread::spawn(move ||{
std::fs::create_dir_all(replays_path.as_path()).unwrap();
replays_path.push(file_name);
let file=std::fs::File::create(replays_path).unwrap();
strafesnet_snf::bot::write_bot(
std::io::BufWriter::new(file),
strafesnet_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)=>{
match &self.view_state{
ViewState::Play=>{
// allow simulation timescale for fun
let scale=self.simulation.timer.get_scale();
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
},
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let scale=replay.simulation.timer.get_scale();
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
},
}
},
Instruction::Playback(SessionPlaybackInstruction::DecreaseTimescale)=>{
match &self.view_state{
ViewState::Play=>{
// allow simulation timescale for fun
let scale=self.simulation.timer.get_scale();
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
},
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let scale=replay.simulation.timer.get_scale();
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
},
}
},
Instruction::Playback(SessionPlaybackInstruction::SkipForward)=>{
match &self.view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
replay.simulation.timer.set_time(ins.time,time);
},
}
},
Instruction::Playback(SessionPlaybackInstruction::SkipBack)=>{
match &self.view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
replay.simulation.timer.set_time(ins.time,time);
// resimulate the entire playback lol
replay.next_instruction_id=0;
},
}
},
Instruction::Playback(SessionPlaybackInstruction::TogglePaused)=>{
match &self.view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
_=replay.simulation.timer.set_paused(ins.time,!replay.simulation.timer.is_paused());
},
}
}
Instruction::ChangeMap(complete_map)=>{
self.clear_recording();
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=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle);
// this just refreshes the replays
for replay in self.replays.values_mut(){
// TODO: filter idles from recording, inject new idles in real time
replay.advance(&self.geometry_shared,ins.time);
}
}
};
// process all emitted output instructions
self.process_exhaustive(ins.time);
}
}
impl InstructionConsumer<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::TimeInner>){
let time=self.simulation.timer.time(ins.time);
if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins.set_time(time)){
//record
self.recording.instructions.push(instruction.clone());
PhysicsContext::run_input_instruction(&mut self.simulation.physics,&self.geometry_shared,instruction);
}
}
}
impl InstructionEmitter<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
self.mouse_interpolator.next_instruction(time_limit)
}
}

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

8
engine/settings/LICENSE Normal file

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

@ -0,0 +1,32 @@
use std::path::PathBuf;
use crate::settings::{UserSettings,load_user_settings};
pub struct Directories{
pub settings:PathBuf,
pub maps:PathBuf,
pub replays:PathBuf,
}
impl Directories{
pub fn settings(&self)->UserSettings{
load_user_settings(&self.settings)
}
pub fn user()->Option<Self>{
let dirs=directories::ProjectDirs::from("net.strafes","StrafesNET","Strafe Client")?;
Some(Self{
settings:dirs.config_dir().join("settings.conf"),
maps:dirs.cache_dir().join("maps"),
// separate directory for remote downloaded replays (cache)
// bots:dirs.cache_dir().join("bots"),
replays:dirs.data_local_dir().join("replays"),
})
}
pub fn portable()->Result<Self,std::io::Error>{
let current_dir=std::env::current_dir()?;
Ok(Self{
settings:current_dir.join("settings.conf"),
maps:current_dir.join("maps"),
replays:current_dir.join("replays"),
})
}
}

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

@ -74,9 +74,9 @@ sensitivity_y_from_x_ratio=1
Sensitivity::DeriveY{x:0.0.001,y:DerivedSensitivity{ratio:1.0}}
*/
pub fn read_user_settings()->UserSettings{
pub fn load_user_settings(path:&std::path::Path)->UserSettings{
let mut cfg=configparser::ini::Ini::new();
if let Ok(_)=cfg.load("settings.conf"){
if let Ok(_)=cfg.load(path){
let (cfg_fov_x,cfg_fov_y)=(cfg.getfloat("camera","fov_x"),cfg.getfloat("camera","fov_y"));
let fov=match(cfg_fov_x,cfg_fov_y){
(Ok(Some(fov_x)),Ok(Some(fov_y)))=>Fov::Exactly {
@ -136,4 +136,4 @@ pub fn read_user_settings()->UserSettings{
}else{
UserSettings::default()
}
}
}

@ -0,0 +1,9 @@
[package]
name = "integration-testing"
version = "0.1.0"
edition = "2021"
[dependencies]
strafesnet_common = { version = "0.5.2", path = "../lib/common", registry = "strafesnet" }
strafesnet_physics = { version = "0.1.0", path = "../engine/physics", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }

@ -0,0 +1,221 @@
use std::{io::{Cursor,Read},path::Path};
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){
test_determinism().unwrap();
}
#[allow(unused)]
#[derive(Debug)]
enum ReplayError{
IO(std::io::Error),
SNF(strafesnet_snf::Error),
SNFM(strafesnet_snf::map::Error),
SNFB(strafesnet_snf::bot::Error),
}
impl From<std::io::Error> for ReplayError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
}
}
impl From<strafesnet_snf::Error> for ReplayError{
fn from(value:strafesnet_snf::Error)->Self{
Self::SNF(value)
}
}
impl From<strafesnet_snf::map::Error> for ReplayError{
fn from(value:strafesnet_snf::map::Error)->Self{
Self::SNFM(value)
}
}
impl From<strafesnet_snf::bot::Error> for ReplayError{
fn from(value:strafesnet_snf::bot::Error)->Self{
Self::SNFB(value)
}
}
fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let mut file=std::fs::File::open(path)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Cursor::new(data))
}
fn run_replay()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
println!("loading bot file..");
let data=read_entire_file("../tools/replays/535s+159764769ns.snfb")?;
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
let mut physics=PhysicsState::default();
for ins in bot.instructions{
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
}
match physics.get_finish_time(){
Some(time)=>println!("finish time:{}",time),
None=>println!("simulation did not end in finished state"),
}
Ok(())
}
enum DeterminismResult{
Deterministic,
NonDeterministic,
}
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>,ReplayError>;
fn read_and_run(file_path:std::path::PathBuf,physics_data:&PhysicsData)->ThreadResult{
let data=read_entire_file(file_path.as_path())?;
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
println!("Running {:?}",file_path.file_stem());
Ok(Some(segment_determinism(bot,physics_data)))
}
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
s.spawn(move ||{
let result=read_and_run(file_path,physics_data);
// send when thread is complete
send.send(result).unwrap();
});
}
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
Ok(dir_entry.file_type()?.is_file().then_some(
dir_entry.path()
))
}
fn test_determinism()->Result<(),ReplayError>{
let thread_limit=std::thread::available_parallelism()?.get();
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let (send,recv)=std::sync::mpsc::channel();
let mut read_dir=std::fs::read_dir("../tools/replays")?;
// promise that &physics_data will outlive the spawned threads
let thread_results=std::thread::scope(|s|{
let mut thread_results=Vec::new();
// spawn threads
println!("spawning up to {thread_limit} threads...");
let mut active_thread_count=0;
while active_thread_count<thread_limit{
if let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
active_thread_count+=1;
do_thread(s,file_path,send.clone(),&physics_data);
}
}else{
break;
}
}
// spawn another thread every time a message is received from the channel
println!("riding parallelism wave...");
while let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
// wait for a thread to complete
thread_results.push(recv.recv().unwrap());
do_thread(s,file_path,send.clone(),&physics_data);
}
}
// wait for remaining threads to complete
println!("waiting for all threads to complete...");
for _ in 0..active_thread_count{
thread_results.push(recv.recv().unwrap());
}
println!("done.");
Ok::<_,ReplayError>(thread_results)
})?;
// tally results
#[derive(Default)]
struct Totals{
deterministic:u32,
nondeterministic:u32,
invalid:u32,
error:u32,
}
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
match result{
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
Ok(None)=>totals.invalid+=1,
Err(_)=>totals.error+=1,
}
totals
});
println!("deterministic={deterministic}");
println!("nondeterministic={nondeterministic}");
println!("invalid={invalid}");
println!("error={error}");
assert!(nondeterministic==0);
assert!(invalid==0);
assert!(error==0);
Ok(())
}

@ -12,5 +12,6 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
glam = "0.29.0"
strafesnet_common = { path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader" }
vbsp = "0.6.0"
vmdl = "0.2.0"

@ -1,18 +1,18 @@
use strafesnet_common::{map,model,integer,gameplay_attributes};
use std::borrow::Cow;
const VALVE_SCALE:f32=1.0/16.0;
fn valve_transform([x,y,z]:[f32;3])->integer::Planar64Vec3{
integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
}
pub fn convert_bsp<AcquireRenderConfigId,AcquireMeshId>(
bsp:&vbsp::Bsp,
mut acquire_render_config_id:AcquireRenderConfigId,
mut acquire_mesh_id:AcquireMeshId
)->PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
AcquireMeshId:FnMut(&str)->model::MeshId,
{
use strafesnet_common::{map,model,integer,gameplay_attributes};
use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfigDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
use crate::valve_transform;
pub fn convert<'a>(
bsp:&'a crate::Bsp,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>,
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
)->PartialMap1{
let bsp=bsp.as_ref();
//figure out real attributes later
let mut unique_attributes=Vec::new();
unique_attributes.push(gameplay_attributes::CollisionAttributes::Decoration);
@ -22,7 +22,7 @@ where
//declare all prop models to Loader
let prop_models=bsp.static_props().map(|prop|{
//get or create mesh_id
let mesh_id=acquire_mesh_id(prop.model());
let mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model());
//not the most failsafe code but this is just for the map tool lmao
if prop_mesh_count==mesh_id.get(){
prop_mesh_count+=1;
@ -65,7 +65,7 @@ where
//this automatically figures out what the texture is trying to do and creates
//a render config for it, and then returns the id to that render config
let render_id=acquire_render_config_id(Some(face_texture_data.name()));
let render_id=render_config_deferred_loader.acquire_render_config_id(Some(face_texture_data.name().into()));
//normal
let normal=face.normal();
@ -176,26 +176,13 @@ pub struct PartialMap1{
modes:strafesnet_common::gameplay_modes::Modes,
}
impl PartialMap1{
pub fn add_prop_meshes<AcquireRenderConfigId>(
pub fn add_prop_meshes<'a>(
self,
prop_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::ModelData)>,
mut acquire_render_config_id:AcquireRenderConfigId,
)->PartialMap2
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
prop_meshes:Meshes,
)->PartialMap2{
PartialMap2{
attributes:self.attributes,
prop_meshes:prop_meshes.into_iter().filter_map(|(mesh_id,model_data)|
//this will generate new render ids and texture ids
match convert_mesh(model_data,&mut acquire_render_config_id){
Ok(mesh)=>Some((mesh_id,mesh)),
Err(e)=>{
println!("error converting mesh: {e}");
None
}
}
).collect(),
prop_meshes:prop_meshes.consume().collect(),
prop_models:self.prop_models,
world_meshes:self.world_meshes,
world_models:self.world_models,
@ -214,8 +201,7 @@ pub struct PartialMap2{
impl PartialMap2{
pub fn add_render_configs_and_textures(
mut self,
render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
render_configs:RenderConfigs,
)->map::CompleteMap{
//merge mesh and model lists, flatten and remap all ids
let mesh_id_offset=self.world_meshes.len();
@ -234,13 +220,14 @@ impl PartialMap2{
})
));
//let mut models=Vec::new();
let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,std::collections::HashMap<model::TextureId,model::TextureId>)
=textures.into_iter()
//.filter_map(f) cull unused textures
.enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
.enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{
(texture,(old_texture_id,model::TextureId::new(new_texture_id as u32)))
}).unzip();
let render_configs=render_configs.into_iter().map(|(render_config_id,mut render_config)|{
let render_configs=render_configs.into_iter().map(|(_render_config_id,mut render_config)|{
//this may generate duplicate no-texture render configs but idc
render_config.texture=render_config.texture.and_then(|texture_id|
texture_id_map.get(&texture_id).copied()
@ -257,77 +244,3 @@ impl PartialMap2{
}
}
}
fn convert_mesh<AcquireRenderConfigId>(
model_data:crate::data::ModelData,
acquire_render_config_id:&mut AcquireRenderConfigId,
)->Result<model::Mesh,vmdl::ModelError>
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
let model=model_data.read_model()?;
let texture_paths=model.texture_directories();
if texture_paths.len()!=1{
println!("WARNING: multiple texture paths");
}
let skin=model.skin_tables().nth(0).unwrap();
let mut spam_pos=Vec::with_capacity(model.vertices().len());
let mut spam_normal=Vec::with_capacity(model.vertices().len());
let mut spam_tex=Vec::with_capacity(model.vertices().len());
let mut spam_vertices=Vec::with_capacity(model.vertices().len());
for (i,vertex) in model.vertices().iter().enumerate(){
spam_pos.push(valve_transform(vertex.position.into()));
spam_normal.push(valve_transform(vertex.normal.into()));
spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates));
spam_vertices.push(model::IndexedVertex{
pos:model::PositionId::new(i as u32),
tex:model::TextureCoordinateId::new(i as u32),
normal:model::NormalId::new(i as u32),
color:model::ColorId::new(0),
});
}
let mut graphics_groups=Vec::new();
let mut physics_groups=Vec::new();
let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
let mut path=std::path::PathBuf::from(texture_path.as_str());
path.push(texture_name);
acquire_render_config_id(path.as_os_str().to_str())
}else{
acquire_render_config_id(None)
};
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![polygon_group_id],
});
physics_groups.push(model::IndexedPhysicsGroup{
groups:vec![polygon_group_id],
});
model::PolygonGroup::PolygonList(model::PolygonList::new(
//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
mesh.vertex_strip_indices().flat_map(|mut strip|
std::iter::from_fn(move||{
match (strip.next(),strip.next(),strip.next()){
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None,
}
})
).collect()
))
}).collect();
Ok(model::Mesh{
unique_pos:spam_pos,
unique_normal:spam_normal,
unique_tex:spam_tex,
unique_color:vec![glam::Vec4::ONE],
unique_vertices:spam_vertices,
polygon_groups,
graphics_groups,
physics_groups,
})
}

@ -1,60 +0,0 @@
pub struct Bsp(vbsp::Bsp);
impl Bsp{
pub const fn new(value:vbsp::Bsp)->Self{
Self(value)
}
}
impl AsRef<vbsp::Bsp> for Bsp{
fn as_ref(&self)->&vbsp::Bsp{
&self.0
}
}
pub struct MdlData(Vec<u8>);
impl MdlData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for MdlData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct VtxData(Vec<u8>);
impl VtxData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for VtxData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct VvdData(Vec<u8>);
impl VvdData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for VvdData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct ModelData{
pub mdl:MdlData,
pub vtx:VtxData,
pub vvd:VvdData,
}
impl ModelData{
pub fn read_model(&self)->Result<vmdl::Model,vmdl::ModelError>{
Ok(vmdl::Model::from_parts(
vmdl::mdl::Mdl::read(self.mdl.as_ref())?,
vmdl::vtx::Vtx::read(self.vtx.as_ref())?,
vmdl::vvd::Vvd::read(self.vvd.as_ref())?,
))
}
}

@ -1,7 +1,23 @@
mod bsp;
pub mod data;
mod mesh;
pub mod loader;
pub use data::Bsp;
pub struct Bsp(vbsp::Bsp);
impl Bsp{
pub const fn new(value:vbsp::Bsp)->Self{
Self(value)
}
}
impl AsRef<vbsp::Bsp> for Bsp{
fn as_ref(&self)->&vbsp::Bsp{
&self.0
}
}
const VALVE_SCALE:f32=1.0/16.0;
pub(crate) fn valve_transform([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
strafesnet_common::integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
}
#[derive(Debug)]
pub enum ReadError{
@ -24,14 +40,4 @@ pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{
vbsp::Bsp::read(s.as_slice()).map(Bsp::new).map_err(ReadError::Bsp)
}
pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
bsp:&Bsp,
acquire_render_config_id:AcquireRenderConfigId,
acquire_mesh_id:AcquireMeshId
)->bsp::PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId,
AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId,
{
bsp::convert_bsp(bsp.as_ref(),acquire_render_config_id,acquire_mesh_id)
}
pub use bsp::convert;

@ -0,0 +1,112 @@
use std::{borrow::Cow, io::Read};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::{mesh::ModelData, Bsp};
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
}
impl std::fmt::Display for TextureError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for TextureError{}
impl From<std::io::Error> for TextureError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError;
type Index=Cow<'a,str>;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Texture::ImageDDS(data))
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
VMDL(vmdl::ModelError),
VBSP(vbsp::BspError),
MissingMdl,
MissingVtx,
MissingVvd,
}
impl std::fmt::Display for MeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for MeshError{}
impl From<std::io::Error> for MeshError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<vmdl::ModelError> for MeshError{
fn from(value:vmdl::ModelError)->Self{
Self::VMDL(value)
}
}
impl From<vbsp::BspError> for MeshError{
fn from(value:vbsp::BspError)->Self{
Self::VBSP(value)
}
}
pub struct MeshLoader<'a,'b>{
bsp:&'a Bsp,
deferred_loader:&'b mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
}
impl MeshLoader<'_,'_>{
pub fn new<'a,'b>(
bsp:&'a Bsp,
deferred_loader:&'b mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
)->MeshLoader<'a,'b>{
MeshLoader{
bsp,
deferred_loader,
}
}
}
impl<'a> Loader for MeshLoader<'a,'_>{
type Error=MeshError;
type Index=&'a str;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let mdl_path_lower=index.to_lowercase();
//.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mdl_path_lower.as_str());
let mut vvd_path=path.clone();
let mut vtx_path=path;
vvd_path.set_extension("vvd");
vtx_path.set_extension("dx90.vtx");
// TODO: search more packs, possibly using an index of multiple packs
let bsp=self.bsp.as_ref();
let mdl=bsp.pack.get(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl)?;
let vtx=bsp.pack.get(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?;
let vvd=bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?;
let model=ModelData{mdl,vtx,vvd};
let mesh=model.convert_mesh(&mut self.deferred_loader)?;
Ok(mesh)
}
}

@ -0,0 +1,89 @@
use std::borrow::Cow;
use strafesnet_common::model;
use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader;
use crate::valve_transform;
pub struct ModelData{
pub mdl:Vec<u8>,
pub vtx:Vec<u8>,
pub vvd:Vec<u8>,
}
impl ModelData{
fn read_model(&self)->Result<vmdl::Model,vmdl::ModelError>{
Ok(vmdl::Model::from_parts(
vmdl::mdl::Mdl::read(self.mdl.as_ref())?,
vmdl::vtx::Vtx::read(self.vtx.as_ref())?,
vmdl::vvd::Vvd::read(self.vvd.as_ref())?,
))
}
pub fn convert_mesh<'a>(self,deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>)->Result<model::Mesh,vmdl::ModelError>{
let model=self.read_model()?;
let texture_paths=model.texture_directories();
if texture_paths.len()!=1{
println!("WARNING: multiple texture paths");
}
let skin=model.skin_tables().nth(0).unwrap();
let mut spam_pos=Vec::with_capacity(model.vertices().len());
let mut spam_normal=Vec::with_capacity(model.vertices().len());
let mut spam_tex=Vec::with_capacity(model.vertices().len());
let mut spam_vertices=Vec::with_capacity(model.vertices().len());
for (i,vertex) in model.vertices().iter().enumerate(){
spam_pos.push(valve_transform(vertex.position.into()));
spam_normal.push(valve_transform(vertex.normal.into()));
spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates));
spam_vertices.push(model::IndexedVertex{
pos:model::PositionId::new(i as u32),
tex:model::TextureCoordinateId::new(i as u32),
normal:model::NormalId::new(i as u32),
color:model::ColorId::new(0),
});
}
let mut graphics_groups=Vec::new();
let mut physics_groups=Vec::new();
let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
let mut path=std::path::PathBuf::from(texture_path.as_str());
path.push(texture_name);
let index=path.as_os_str().to_str().map(|s|Cow::Owned(s.to_owned()));
deferred_loader.acquire_render_config_id(index)
}else{
deferred_loader.acquire_render_config_id(None)
};
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![polygon_group_id],
});
physics_groups.push(model::IndexedPhysicsGroup{
groups:vec![polygon_group_id],
});
model::PolygonGroup::PolygonList(model::PolygonList::new(
//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
mesh.vertex_strip_indices().flat_map(|mut strip|
std::iter::from_fn(move||{
match (strip.next(),strip.next(),strip.next()){
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None,
}
})
).collect()
))
}).collect();
Ok(model::Mesh{
unique_pos:spam_pos,
unique_normal:spam_normal,
unique_tex:spam_tex,
unique_color:vec![glam::Vec4::ONE],
unique_vertices:spam_vertices,
polygon_groups,
graphics_groups,
physics_groups,
})
}
}

@ -1,21 +1,47 @@
use crate::integer::Time;
#[derive(Debug)]
#[derive(Clone,Debug)]
pub struct TimedInstruction<I,T>{
pub time:Time<T>,
pub instruction:I,
}
impl<I,T> TimedInstruction<I,T>{
#[inline]
pub fn set_time<TimeInner>(self,new_time:Time<TimeInner>)->TimedInstruction<I,TimeInner>{
TimedInstruction{
time:new_time,
instruction:self.instruction,
}
}
}
pub trait InstructionEmitter{
type Instruction;
/// Ensure all emitted instructions are processed before consuming external instructions
pub trait InstructionEmitter<I>{
type TimeInner;
fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<Self::Instruction,Self::TimeInner>>;
fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<I,Self::TimeInner>>;
}
pub trait InstructionConsumer{
type Instruction;
/// Apply an atomic state update
pub trait InstructionConsumer<I>{
type TimeInner;
fn process_instruction(&mut self, instruction:TimedInstruction<Self::Instruction,Self::TimeInner>);
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::TimeInner>);
}
/// If the object produces its own instructions, allow exhaustively feeding them back in
pub trait InstructionFeedback<I,T>:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>
where
Time<T>:Copy,
{
#[inline]
fn process_exhaustive(&mut self,time_limit:Time<T>){
while let Some(instruction)=self.next_instruction(time_limit){
self.process_instruction(instruction);
}
}
}
impl<I,T,X> InstructionFeedback<I,T> for X
where
Time<T>:Copy,
X:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>,
{}
//PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I,T>{
@ -25,6 +51,7 @@ pub struct InstructionCollector<I,T>{
impl<I,T> InstructionCollector<I,T>
where Time<T>:Copy+PartialOrd,
{
#[inline]
pub const fn new(time:Time<T>)->Self{
Self{
time,
@ -35,25 +62,21 @@ impl<I,T> InstructionCollector<I,T>
pub const fn time(&self)->Time<T>{
self.time
}
#[inline]
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
match instruction{
Some(unwrap_instruction)=>{
if unwrap_instruction.time<self.time {
self.time=unwrap_instruction.time;
self.instruction=Some(unwrap_instruction.instruction);
}
},
None=>(),
if let Some(ins)=instruction{
if ins.time<self.time{
self.time=ins.time;
self.instruction=Some(ins.instruction);
}
}
}
pub fn instruction(self)->Option<TimedInstruction<I,T>>{
#[inline]
pub fn take(self)->Option<TimedInstruction<I,T>>{
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
match self.instruction{
Some(instruction)=>Some(TimedInstruction{
time:self.time,
instruction
}),
None=>None,
}
self.instruction.map(|instruction|TimedInstruction{
time:self.time,
instruction
})
}
}

@ -14,6 +14,7 @@ impl<T> Time<T>{
pub const MIN:Self=Self::raw(i64::MIN);
pub const MAX:Self=Self::raw(i64::MAX);
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_MILLISECOND:Self=Self::raw(1_000_000);
pub const ONE_MICROSECOND:Self=Self::raw(1_000);

@ -1,11 +1,33 @@
use crate::mouse::MouseState;
use crate::gameplay_modes::{ModeId,StageId};
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;
#[derive(Clone,Debug)]
pub enum Instruction{
ReplaceMouse(crate::mouse::MouseState<TimeInner>,crate::mouse::MouseState<TimeInner>),
SetNextMouse(crate::mouse::MouseState<TimeInner>),
Mouse(MouseInstruction),
SetControl(SetControlInstruction),
Mode(ModeInstruction),
Misc(MiscInstruction),
/// Idle: there were no input events, but the simulation is safe to advance to this timestep
Idle,
}
impl Instruction{
pub const IDLE:Self=Self::Idle;
}
#[derive(Clone,Debug)]
pub enum MouseInstruction{
/// Replace the entire interpolation state to avoid dividing by zero when replacing twice
ReplaceMouse{
m0:MouseState<TimeInner>,
m1:MouseState<TimeInner>,
},
SetNextMouse(MouseState<TimeInner>),
}
#[derive(Clone,Debug)]
pub enum SetControlInstruction{
SetMoveRight(bool),
SetMoveUp(bool),
SetMoveBack(bool),
@ -14,18 +36,21 @@ pub enum Instruction{
SetMoveForward(bool),
SetJump(bool),
SetZoom(bool),
}
#[derive(Clone,Debug)]
pub enum ModeInstruction{
/// Reset: fully replace the physics state.
/// This forgets all inputs and settings which need to be reapplied.
Reset,
/// Restart: Teleport to the start zone.
Restart,
/// This runs when you press R or teleport to a bonus
Restart(ModeId),
/// Spawn: Teleport to a specific mode's spawn
/// Sets current mode & spawn
Spawn(crate::gameplay_modes::ModeId,crate::gameplay_modes::StageId),
Idle,
//Idle: there were no input events, but the simulation is safe to advance to this timestep
//for interpolation / networking / playback reasons, most playback heads will always want
//to be 1 instruction ahead to generate the next state for interpolation.
/// This runs when the map loads to put you at the map lobby
Spawn(ModeId,StageId),
}
#[derive(Clone,Debug)]
pub enum MiscInstruction{
PracticeFly,
SetSensitivity(crate::integer::Ratio64Vec2),
}

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

@ -23,7 +23,7 @@ impl PauseState for Unpaused{
}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
enum Inner{}
pub enum Inner{}
type InnerTime=Time<Inner>;
#[derive(Clone,Copy,Debug)]
@ -157,7 +157,7 @@ impl<T:TimerState> TimerFixed<T,Paused>
where Time<T::In>:Copy,
{
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{
state:self.state,
_paused:Unpaused,
@ -165,6 +165,9 @@ impl<T:TimerState> TimerFixed<T,Paused>
timer.set_time(time,new_time);
timer
}
pub fn time(&self)->Time<T::Out>{
self.state.get_offset().coerce()
}
}
impl<T:TimerState> TimerFixed<T,Unpaused>
where Time<T::In>:Copy,
@ -178,6 +181,9 @@ impl<T:TimerState> TimerFixed<T,Unpaused>
timer.set_time(time,new_time);
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
@ -199,12 +205,6 @@ impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
pub fn into_state(self)->T{
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>){
match P::IS_PAUSED{
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>{
match self{
Self::Paused(timer)=>timer.time(time),
Self::Paused(timer)=>timer.time(),
Self::Unpaused(timer)=>timer.time(time),
}
}
@ -329,7 +329,7 @@ mod test{
//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)));
//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
let timer=timer.into_unpaused(sec!(1));
@ -339,7 +339,7 @@ mod test{
//pause the timer after 11 seconds
let timer=timer.into_paused(sec!(11));
//the paused timer at 20 seconds should read 5s
assert_eq!(timer.time(sec!(20)),sec!(5));
assert_eq!(timer.time(),sec!(5));
}
#[test]
fn test_timer()->Result<(),Error>{

@ -1,3 +1,5 @@
// This whole thing should be a drive macro
pub trait Updatable<Updater>{
fn update(&mut self,update:Updater);
}
@ -53,4 +55,3 @@ impl Updatable<OuterUpdate> for Outer{
}
}
}
//*/

@ -1,6 +1,6 @@
[package]
name = "strafesnet_deferred_loader"
version = "0.4.1"
version = "0.5.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@ -9,13 +9,5 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["legacy"]
legacy = ["dep:url","dep:vbsp"]
#roblox = ["dep:lazy-regex"]
#source = ["dep:vbsp"]
[dependencies]
strafesnet_common = { path = "../common", registry = "strafesnet" }
url = { version = "2.5.2", optional = true }
vbsp = { version = "0.6.0", optional = true }

@ -0,0 +1,109 @@
use std::collections::HashMap;
use crate::loader::Loader;
use crate::mesh::Meshes;
use crate::texture::{RenderConfigs,Texture};
use strafesnet_common::model::{Mesh,MeshId,RenderConfig,RenderConfigId,TextureId};
pub enum LoadFailureMode{
DefaultToNone,
Fatal,
}
pub struct RenderConfigDeferredLoader<H>{
texture_count:u32,
render_configs:Vec<RenderConfig>,
render_config_id_from_asset_id:HashMap<Option<H>,RenderConfigId>,
}
impl<H> RenderConfigDeferredLoader<H>{
pub fn new()->Self{
Self{
texture_count:0,
render_configs:Vec::new(),
render_config_id_from_asset_id:HashMap::new(),
}
}
}
impl<H:core::hash::Hash+Eq> RenderConfigDeferredLoader<H>{
pub fn acquire_render_config_id(&mut self,index:Option<H>)->RenderConfigId{
let some_texture=index.is_some();
*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
//create the render config.
let render_config=if some_texture{
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
let render_id=RenderConfigId::new(self.render_configs.len() as u32);
self.render_configs.push(render_config);
render_id
})
}
pub fn into_render_configs<L:Loader<Index=H,Resource=Texture>>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{
let mut sorted_textures=vec![None;self.texture_count as usize];
for (index_option,render_config_id) in self.render_config_id_from_asset_id{
let render_config=&mut self.render_configs[render_config_id.get() as usize];
if let (Some(index),Some(texture_id))=(index_option,render_config.texture){
let resource_result=loader.load(index);
let texture=match failure_mode{
// if texture fails to load, use no texture
LoadFailureMode::DefaultToNone=>match resource_result{
Ok(texture)=>Some(texture),
Err(e)=>{
render_config.texture=None;
println!("Error loading resource: {e}");
None
},
},
// loading failure is fatal
LoadFailureMode::Fatal=>Some(resource_result?)
};
sorted_textures[texture_id.get() as usize]=texture;
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_configs,
))
}
}
pub struct MeshDeferredLoader<H>{
mesh_id_from_asset_id:HashMap<H,MeshId>,
}
impl<H> MeshDeferredLoader<H>{
pub fn new()->Self{
Self{
mesh_id_from_asset_id:HashMap::new(),
}
}
}
impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{
pub fn acquire_mesh_id(&mut self,index:H)->MeshId{
let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
}
pub fn into_meshes<L:Loader<Index=H,Resource=Mesh>>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{
let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()];
for (index,mesh_id) in self.mesh_id_from_asset_id{
let resource_result=loader.load(index);
let mesh=match failure_mode{
// if mesh fails to load, use no mesh
LoadFailureMode::DefaultToNone=>match resource_result{
Ok(mesh)=>Some(mesh),
Err(e)=>{
println!("Error loading resource: {e}");
None
},
},
// loading failure is fatal
LoadFailureMode::Fatal=>Some(resource_result?)
};
mesh_list[mesh_id.get() as usize]=mesh;
}
Ok(Meshes::new(mesh_list))
}
}

@ -1,34 +1,5 @@
#[cfg(feature="legacy")]
mod roblox_legacy;
#[cfg(feature="legacy")]
mod source_legacy;
#[cfg(feature="roblox")]
mod roblox;
#[cfg(feature="source")]
mod source;
#[cfg(any(feature="roblox",feature="legacy"))]
pub mod rbxassetid;
pub mod mesh;
pub mod loader;
pub mod texture;
#[cfg(any(feature="source",feature="legacy"))]
pub mod valve_mesh;
#[cfg(any(feature="roblox",feature="legacy"))]
pub mod roblox_mesh;
#[cfg(feature="legacy")]
pub fn roblox_legacy()->roblox_legacy::Loader{
roblox_legacy::Loader::new()
}
#[cfg(feature="legacy")]
pub fn source_legacy()->source_legacy::Loader{
source_legacy::Loader::new()
}
#[cfg(feature="roblox")]
pub fn roblox()->roblox::Loader{
roblox::Loader::new()
}
#[cfg(feature="source")]
pub fn source()->source::Loader{
source::Loader::new()
}
pub mod deferred_loader;

@ -0,0 +1,8 @@
use std::error::Error;
pub trait Loader{
type Error:Error;
type Index;
type Resource;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>;
}

@ -0,0 +1,17 @@
use strafesnet_common::model::{Mesh,MeshId};
pub struct Meshes{
meshes:Vec<Option<Mesh>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<Mesh>>)->Self{
Self{
meshes,
}
}
pub fn consume(self)->impl Iterator<Item=(MeshId,Mesh)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)
}
}

@ -1,48 +0,0 @@
#[derive(Hash,Eq,PartialEq)]
pub struct RobloxAssetId(pub u64);
#[derive(Debug)]
#[allow(dead_code)]
pub struct StringWithError{
string:String,
error:RobloxAssetIdParseErr,
}
impl std::fmt::Display for StringWithError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for StringWithError{}
impl StringWithError{
const fn new(
string:String,
error:RobloxAssetIdParseErr,
)->Self{
Self{string,error}
}
}
#[derive(Debug)]
pub enum RobloxAssetIdParseErr{
Url(url::ParseError),
UnknownScheme,
ParseInt(std::num::ParseIntError),
MissingAssetId,
}
impl std::str::FromStr for RobloxAssetId{
type Err=StringWithError;
fn from_str(s:&str)->Result<Self,Self::Err>{
let url=url::Url::parse(s).map_err(|e|StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::Url(e)))?;
let parsed_asset_id=match url.scheme(){
"rbxassetid"=>url.domain().ok_or_else(||StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::MissingAssetId))?.parse(),
"http"|"https"=>{
let (_,asset_id)=url.query_pairs()
.find(|(id,_)|match id.as_ref(){
"ID"|"id"|"Id"|"iD"=>true,
_=>false,
}).ok_or_else(||StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::MissingAssetId))?;
asset_id.parse()
},
_=>Err(StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::UnknownScheme))?,
};
Ok(Self(parsed_asset_id.map_err(|e|StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::ParseInt(e)))?))
}
}

@ -1,112 +0,0 @@
use std::io::Read;
use std::collections::HashMap;
use crate::roblox_mesh;
use crate::texture::{RenderConfigs,Texture};
use strafesnet_common::model::{MeshId,RenderConfig,RenderConfigId,TextureId};
use crate::rbxassetid::RobloxAssetId;
#[derive(Default)]
pub struct RenderConfigLoader{
texture_count:u32,
render_configs:Vec<RenderConfig>,
render_config_id_from_asset_id:HashMap<Option<RobloxAssetId>,RenderConfigId>,
}
impl RenderConfigLoader{
pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
let render_id=RenderConfigId::new(self.render_config_id_from_asset_id.len() as u32);
let index=name.and_then(|name|{
match name.parse::<RobloxAssetId>(){
Ok(asset_id)=>Some(asset_id),
Err(e)=>{
println!("Failed to parse AssetId: {e}");
None
},
}
});
*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
//create the render config.
let render_config=if name.is_some(){
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
self.render_configs.push(render_config);
render_id
})
}
}
#[derive(Default)]
pub struct MeshLoader{
mesh_id_from_asset_id:HashMap<Option<RobloxAssetId>,MeshId>,
}
impl MeshLoader{
pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
let index=match name.parse::<RobloxAssetId>(){
Ok(asset_id)=>Some(asset_id),
Err(e)=>{
println!("Failed to parse AssetId: {e}");
None
},
};
*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
}
pub fn load_meshes(&mut self)->Result<roblox_mesh::Meshes,std::io::Error>{
let mut mesh_data=vec![None;self.mesh_id_from_asset_id.len()];
for (asset_id_option,mesh_id) in &self.mesh_id_from_asset_id{
if let Some(asset_id)=asset_id_option{
if let Ok(mut file)=std::fs::File::open(format!("meshes/{}",asset_id.0)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
mesh_data[mesh_id.get() as usize]=Some(roblox_mesh::RobloxMeshData::new(data));
}else{
println!("[roblox_legacy] no mesh name={}",asset_id.0);
}
}
}
Ok(roblox_mesh::Meshes::new(mesh_data))
}
}
pub struct Loader{
render_config_loader:RenderConfigLoader,
mesh_loader:MeshLoader,
}
impl Loader{
pub fn new()->Self{
Self{
render_config_loader:RenderConfigLoader::default(),
mesh_loader:MeshLoader::default(),
}
}
pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
(&mut self.render_config_loader,&mut self.mesh_loader)
}
pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
for (asset_id_option,render_config_id) in self.render_config_loader.render_config_id_from_asset_id{
let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
if let (Some(asset_id),Some(texture_id))=(asset_id_option,render_config.texture){
if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",asset_id.0)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
}else{
//texture failed to load
render_config.texture=None;
}
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_config_loader.render_configs,
))
}
}

@ -1,30 +0,0 @@
use strafesnet_common::model::MeshId;
#[derive(Clone)]
pub struct RobloxMeshData(Vec<u8>);
impl RobloxMeshData{
pub(crate) fn new(data:Vec<u8>)->Self{
Self(data)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
pub struct Meshes{
meshes:Vec<Option<RobloxMeshData>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<RobloxMeshData>>)->Self{
Self{
meshes,
}
}
pub fn get_texture(&self,texture_id:MeshId)->Option<&RobloxMeshData>{
self.meshes.get(texture_id.get() as usize)?.as_ref()
}
pub fn into_iter(self)->impl Iterator<Item=(MeshId,RobloxMeshData)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)
}
}

@ -1,102 +0,0 @@
use std::io::Read;
use std::collections::HashMap;
use crate::valve_mesh;
use crate::texture::{Texture,RenderConfigs};
use strafesnet_common::model::{MeshId,TextureId,RenderConfig,RenderConfigId};
pub struct RenderConfigLoader{
texture_count:u32,
render_configs:Vec<RenderConfig>,
texture_paths:HashMap<Option<Box<str>>,RenderConfigId>,
}
impl RenderConfigLoader{
pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
let render_id=RenderConfigId::new(self.texture_paths.len() as u32);
*self.texture_paths.entry(name.map(Into::into)).or_insert_with(||{
//create the render config.
let render_config=if name.is_some(){
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
self.render_configs.push(render_config);
render_id
})
}
}
pub struct MeshLoader{
mesh_paths:HashMap<Box<str>,MeshId>,
}
impl MeshLoader{
pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
let mesh_id=MeshId::new(self.mesh_paths.len() as u32);
*self.mesh_paths.entry(name.into()).or_insert(mesh_id)
}
//load_meshes should look like load_textures
pub fn load_meshes(&mut self,bsp:&vbsp::Bsp)->valve_mesh::Meshes{
let mut mesh_data=vec![None;self.mesh_paths.len()];
for (mesh_path,mesh_id) in &self.mesh_paths{
let mesh_path_lower=mesh_path.to_lowercase();
//.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mesh_path_lower.as_str());
let mut vvd_path=path.clone();
let mut vtx_path=path.clone();
vvd_path.set_extension("vvd");
vtx_path.set_extension("dx90.vtx");
match (bsp.pack.get(mesh_path_lower.as_str()),bsp.pack.get(vvd_path.as_os_str().to_str().unwrap()),bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())){
(Ok(Some(mdl_file)),Ok(Some(vvd_file)),Ok(Some(vtx_file)))=>{
mesh_data[mesh_id.get() as usize]=Some(valve_mesh::ModelData{
mdl:valve_mesh::MdlData::new(mdl_file),
vtx:valve_mesh::VtxData::new(vtx_file),
vvd:valve_mesh::VvdData::new(vvd_file),
});
},
_=>println!("no model name={}",mesh_path),
}
}
valve_mesh::Meshes::new(mesh_data)
}
}
pub struct Loader{
render_config_loader:RenderConfigLoader,
mesh_loader:MeshLoader,
}
impl Loader{
pub fn new()->Self{
Self{
render_config_loader:RenderConfigLoader{
texture_count:0,
texture_paths:HashMap::new(),
render_configs:Vec::new(),
},
mesh_loader:MeshLoader{mesh_paths:HashMap::new()},
}
}
pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
(&mut self.render_config_loader,&mut self.mesh_loader)
}
pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
for (texture_path,render_config_id) in self.render_config_loader.texture_paths{
let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
if let (Some(texture_path),Some(texture_id))=(texture_path,render_config.texture){
if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",texture_path)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
}else{
//texture failed to load
render_config.texture=None;
}
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_config_loader.render_configs,
))
}
}

@ -1,60 +0,0 @@
use strafesnet_common::model::MeshId;
//duplicate this code for now
#[derive(Clone)]
pub struct MdlData(Vec<u8>);
impl MdlData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct VtxData(Vec<u8>);
impl VtxData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct VvdData(Vec<u8>);
impl VvdData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct ModelData{
pub mdl:MdlData,
pub vtx:VtxData,
pub vvd:VvdData,
}
//meshes is more prone to failure
pub struct Meshes{
meshes:Vec<Option<ModelData>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<ModelData>>)->Self{
Self{
meshes,
}
}
pub fn get_texture(&self,texture_id:MeshId)->Option<&ModelData>{
self.meshes.get(texture_id.get() as usize)?.as_ref()
}
pub fn into_iter(self)->impl Iterator<Item=(MeshId,ModelData)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)
}
}

@ -1,6 +1,6 @@
use bnum::{BInt,cast::As};
#[derive(Clone,Copy,Debug,Default,Hash)]
#[derive(Clone,Copy,Debug,Default,Hash,PartialEq,Eq,PartialOrd,Ord)]
/// A Fixed point number for which multiply operations widen the bits in the output. (when the wide-mul feature is enabled)
/// N is the number of u64s to use
/// F is the number of fractional bits (always N*32 lol)
@ -87,12 +87,6 @@ impl_from!(
i8,i16,i32,i64,i128,isize
);
impl<const N:usize,const F:usize> PartialEq for Fixed<N,F>{
#[inline]
fn eq(&self,other:&Self)->bool{
self.bits.eq(&other.bits)
}
}
impl<const N:usize,const F:usize,T> PartialEq<T> for Fixed<N,F>
where
T:Copy,
@ -103,14 +97,7 @@ where
self.bits.eq(&other.into())
}
}
impl<const N:usize,const F:usize> Eq for Fixed<N,F>{}
impl<const N:usize,const F:usize> PartialOrd for Fixed<N,F>{
#[inline]
fn partial_cmp(&self,other:&Self)->Option<std::cmp::Ordering>{
self.bits.partial_cmp(&other.bits)
}
}
impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
where
T:Copy,
@ -121,12 +108,6 @@ impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
self.bits.partial_cmp(&other.into())
}
}
impl<const N:usize,const F:usize> Ord for Fixed<N,F>{
#[inline]
fn cmp(&self,other:&Self)->std::cmp::Ordering{
self.bits.cmp(&other.bits)
}
}
impl<const N:usize,const F:usize> std::ops::Neg for Fixed<N,F>{
type Output=Self;

@ -57,7 +57,7 @@ fn from_f32(){
assert_eq!(b,Ok(a));
//I32F32::MIN hits a special case since it's not representable as a positive signed integer
//TODO: don't return an overflow because this is technically possible
let a=I32F32::MIN;
let _a=I32F32::MIN;
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
//16 is within the 24 bits of float precision

@ -18,5 +18,7 @@ rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_mesh = "0.1.2"
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid" }
roblox_emulator = { path = "../roblox_emulator", registry = "strafesnet" }
strafesnet_common = { path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader" }

@ -3,6 +3,7 @@ use rbx_dom_weak::WeakDom;
mod rbx;
mod mesh;
pub mod loader;
mod primitives;
pub mod data{
@ -42,7 +43,7 @@ pub struct Place{
services:roblox_emulator::context::Services,
}
impl Place{
fn new(dom:WeakDom)->Option<Self>{
pub fn new(dom:WeakDom)->Option<Self>{
let context=roblox_emulator::context::Context::from_ref(&dom);
Some(Self{
services:context.find_services()?,
@ -94,14 +95,4 @@ pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
//ConvertError
pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
dom:impl AsRef<WeakDom>,
acquire_render_config_id:AcquireRenderConfigId,
acquire_mesh_id:AcquireMeshId
)->rbx::PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId,
AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId,
{
rbx::convert(&dom.as_ref(),acquire_render_config_id,acquire_mesh_id)
}
pub use rbx::convert;

@ -0,0 +1,102 @@
use std::io::Read;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes;
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
RobloxAssetIdParse(RobloxAssetIdParseErr),
}
impl std::fmt::Display for TextureError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for TextureError{}
impl From<std::io::Error> for TextureError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<RobloxAssetIdParseErr> for TextureError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError;
type Index=&'a str;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id);
let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Texture::ImageDDS(data))
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
RobloxAssetIdParse(RobloxAssetIdParseErr),
Mesh(crate::mesh::Error)
}
impl std::fmt::Display for MeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for MeshError{}
impl From<std::io::Error> for MeshError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<RobloxAssetIdParseErr> for MeshError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
impl From<crate::mesh::Error> for MeshError{
fn from(value:crate::mesh::Error)->Self{
Self::Mesh(value)
}
}
pub struct MeshLoader<'a>(std::marker::PhantomData<&'a ()>);
impl MeshLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for MeshLoader<'a>{
type Error=MeshError;
type Index=&'a str;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("meshes/{}",asset_id);
let mut file=std::fs::File::open(file_name)?;
// reading the entire file is way faster than
// round tripping to disk every read from the parser
let mut data=Vec::new();
file.read_to_end(&mut data)?;
let mesh=crate::mesh::convert(RobloxMeshBytes::new(data))?;
Ok(mesh)
}
}

@ -3,6 +3,7 @@ use std::collections::HashMap;
use rbx_mesh::mesh::{Vertex2, Vertex2Truncated};
use strafesnet_common::{integer::vec3,model::{self, ColorId, IndexedVertex, NormalId, PolygonGroup, PolygonList, PositionId, TextureCoordinateId, VertexId}};
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),

@ -1,4 +1,4 @@
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,IndexedVertexList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::integer::{vec3,Planar64Vec3};
#[derive(Debug)]
@ -126,9 +126,6 @@ const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 0,-1, 0),//CornerWedge::Bottom
vec3::int( 0, 0,-1),//CornerWedge::Front
];
pub fn unit_sphere(render:RenderConfigId)->Mesh{
unit_cube(render)
}
#[derive(Default)]
pub struct CubeFaceDescription([Option<FaceDescription>;6]);
impl CubeFaceDescription{
@ -149,10 +146,6 @@ pub fn unit_cube(render:RenderConfigId)->Mesh{
t.insert(CubeFace::Front,FaceDescription::new_with_render_id(render));
generate_partial_unit_cube(t)
}
pub fn unit_cylinder(render:RenderConfigId)->Mesh{
//lmao
unit_cube(render)
}
#[derive(Default)]
pub struct WedgeFaceDescription([Option<FaceDescription>;5]);
impl WedgeFaceDescription{
@ -163,15 +156,15 @@ impl WedgeFaceDescription{
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
}
}
pub fn unit_wedge(render:RenderConfigId)->Mesh{
let mut t=WedgeFaceDescription::default();
t.insert(WedgeFace::Right,FaceDescription::new_with_render_id(render));
t.insert(WedgeFace::TopFront,FaceDescription::new_with_render_id(render));
t.insert(WedgeFace::Back,FaceDescription::new_with_render_id(render));
t.insert(WedgeFace::Left,FaceDescription::new_with_render_id(render));
t.insert(WedgeFace::Bottom,FaceDescription::new_with_render_id(render));
generate_partial_unit_wedge(t)
}
// pub fn unit_wedge(render:RenderConfigId)->Mesh{
// let mut t=WedgeFaceDescription::default();
// t.insert(WedgeFace::Right,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::TopFront,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Back,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Left,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Bottom,FaceDescription::new_with_render_id(render));
// generate_partial_unit_wedge(t)
// }
#[derive(Default)]
pub struct CornerWedgeFaceDescription([Option<FaceDescription>;5]);
impl CornerWedgeFaceDescription{
@ -182,15 +175,15 @@ impl CornerWedgeFaceDescription{
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
}
}
pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{
let mut t=CornerWedgeFaceDescription::default();
t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render));
t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render));
t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render));
t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render));
t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render));
generate_partial_unit_cornerwedge(t)
}
// pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{
// let mut t=CornerWedgeFaceDescription::default();
// t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render));
// generate_partial_unit_cornerwedge(t)
// }
#[derive(Clone)]
pub struct FaceDescription{

@ -8,6 +8,9 @@ use strafesnet_common::gameplay_attributes as attr;
use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
use strafesnet_common::model::RenderConfigId;
use strafesnet_common::updatable::Updatable;
use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
fn class_is_a(class: &str, superclass: &str) -> bool {
if class==superclass {
@ -130,9 +133,9 @@ impl ModesBuilder{
fn push_mode_update(&mut self,mode_id:gameplay_modes::ModeId,mode_update:gameplay_modes::ModeUpdate){
self.mode_updates.push((mode_id,mode_update));
}
fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){
self.stage_updates.push((mode_id,stage_id,stage_update));
}
// fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){
// self.stage_updates.push((mode_id,stage_id,stage_update));
// }
}
fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->attr::CollisionAttributes{
let mut general=attr::GeneralAttributes::default();
@ -432,23 +435,18 @@ struct GetAttributesArgs{
can_collide:bool,
velocity:Planar64Vec3,
}
pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
dom:&rbx_dom_weak::WeakDom,
mut acquire_render_config_id:AcquireRenderConfigId,
mut acquire_mesh_id:AcquireMeshId,
)->PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
AcquireMeshId:FnMut(&str)->model::MeshId,
{
pub fn convert<'a>(
dom:&'a rbx_dom_weak::WeakDom,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
)->PartialMap1{
let mut deferred_models_deferred_attributes=Vec::new();
let mut primitive_models_deferred_attributes=Vec::new();
let mut primitive_meshes=Vec::new();
let mut mesh_id_from_description=HashMap::new();
//just going to leave it like this for now instead of reworking the data structures for this whole thing
let textureless_render_group=acquire_render_config_id(None);
let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
let mut object_refs=Vec::new();
let mut temp_objects=Vec::new();
@ -485,9 +483,6 @@ where
continue;
}
//at this point a new model is going to be generated for sure.
let model_id=model::ModelId::new(primitive_models_deferred_attributes.len() as u32);
//TODO: also detect "CylinderMesh" etc here
let shape=match object.class.as_str(){
"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get("Shape"){
@ -532,7 +527,7 @@ where
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
let render_id=acquire_render_config_id(Some(content.as_ref()));
let render_id=render_config_deferred_loader.acquire_render_config_id(Some(content.as_ref()));
let normal_id=normalid.to_u32();
if normal_id<6{
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
@ -694,8 +689,8 @@ where
object.properties.get("TextureID"),
){
(
MeshAvailability::Deferred(acquire_render_config_id(Some(texture_asset_id.as_ref()))),
acquire_mesh_id(mesh_asset_id.as_ref()),
MeshAvailability::Deferred(render_config_deferred_loader.acquire_render_config_id(Some(texture_asset_id.as_ref()))),
mesh_deferred_loader.acquire_mesh_id(mesh_asset_id.as_ref()),
)
}else{
panic!("Mesh has no Mesh or Texture");
@ -739,7 +734,7 @@ pub struct PartialMap1{
impl PartialMap1{
pub fn add_meshpart_meshes_and_calculate_attributes(
mut self,
meshpart_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::RobloxMeshBytes)>,
meshpart_meshes:Meshes,
)->PartialMap2{
//calculate attributes
let mut modes_builder=ModesBuilder::default();
@ -752,24 +747,16 @@ impl PartialMap1{
//decode roblox meshes
//generate mesh_id_map based on meshes that failed to load
let loaded_meshes:HashMap<model::MeshId,MeshWithAabb>=
meshpart_meshes.into_iter().flat_map(|(old_mesh_id,roblox_mesh_bytes)|
match crate::mesh::convert(roblox_mesh_bytes){
Ok(mesh)=>{
let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
Some((old_mesh_id,MeshWithAabb{
mesh,
aabb,
}))
},
Err(e)=>{
println!("Error converting mesh: {e:?}");
None
},
meshpart_meshes.consume().map(|(old_mesh_id,mesh)|{
let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
).collect();
(old_mesh_id,MeshWithAabb{
mesh,
aabb,
})
}).collect();
let mut mesh_id_from_render_config_id=HashMap::new();
//ignore meshes that fail to load completely for now
@ -882,15 +869,21 @@ pub struct PartialMap2{
impl PartialMap2{
pub fn add_render_configs_and_textures(
self,
render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
render_configs:RenderConfigs,
)->map::CompleteMap{
let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>)
=textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
=textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{
(texture,(old_texture_id,model::TextureId::new(new_texture_id as u32)))
}).unzip();
let render_configs=render_configs.into_iter().map(|(render_config_id,mut render_config)|{
//this may generate duplicate no-texture render configs but idc
let render_configs=render_configs.into_iter().map(|(_render_config_id,mut render_config)|{
// This may generate duplicate no-texture render configs but idc
//
// This is because some textures may not exist, so the render config
// that it points to is unique but is texture.
//
// I don't think this needs to be fixed because missing textures
// should be a conversion error anyways.
render_config.texture=render_config.texture.and_then(|texture_id|
texture_id_map.get(&texture_id).copied()
);

@ -0,0 +1,7 @@
[package]
name = "rbxassetid"
version = "0.1.0"
edition = "2021"
[dependencies]
url = "2.5.4"

35
lib/rbxassetid/src/lib.rs Normal file

@ -0,0 +1,35 @@
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub struct RobloxAssetId(pub u64);
#[derive(Debug)]
pub enum RobloxAssetIdParseErr{
Url(url::ParseError),
UnknownScheme,
ParseInt(std::num::ParseIntError),
MissingAssetId,
MissingIDQueryParam,
}
impl std::fmt::Display for RobloxAssetIdParseErr{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for RobloxAssetIdParseErr{}
impl std::str::FromStr for RobloxAssetId{
type Err=RobloxAssetIdParseErr;
fn from_str(s:&str)->Result<Self,Self::Err>{
let url=url::Url::parse(s).map_err(RobloxAssetIdParseErr::Url)?;
let parsed_asset_id=match url.scheme(){
"rbxassetid"=>url.domain().ok_or(RobloxAssetIdParseErr::MissingAssetId)?.parse(),
"http"|"https"=>{
let (_,asset_id)=url.query_pairs()
.find(|(id,_)|match id.as_ref(){
"ID"|"id"|"Id"|"iD"=>true,
_=>false,
}).ok_or(RobloxAssetIdParseErr::MissingIDQueryParam)?;
asset_id.parse()
},
_=>Err(RobloxAssetIdParseErr::UnknownScheme)?,
};
Ok(Self(parsed_asset_id.map_err(RobloxAssetIdParseErr::ParseInt)?))
}
}

@ -33,7 +33,7 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
// LMAO look at this function!
pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{
let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
f(&mut *dom)
f(*dom)
}
fn coerce_float32(value:&mlua::Value)->Option<f32>{

@ -1,22 +1,22 @@
macro_rules! type_from_lua_userdata{
($asd:ident)=>{
impl mlua::FromLua for $asd{
($ty:ident)=>{
impl mlua::FromLua for $ty{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
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{
($asd:ident)=>{
impl mlua::FromLua for $asd<'static>{
($ty:ident)=>{
impl mlua::FromLua for $ty<'static>{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
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))),
}
}
}

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

@ -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)]
pub enum Error{
InvalidHeader,
InvalidHeader(binrw::Error),
InvalidSegment(binrw::Error),
SegmentConvert(newtypes::integer::RatioError),
InstructionConvert(newtypes::physics::InstructionConvert),
InstructionWrite(binrw::Error),
InvalidSegmentId(SegmentId),
InvalidData(binrw::Error),
IO(std::io::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_BOT_HEADER:
u128 map_resource_uuid //which map is this bot running
//don't include style info in bot header because it's in the simulation state
//blocks are laid out in chronological order, but indices may jump around.
u64 num_segments
// Segments are laid out in chronological order,
// but block_id is not necessarily in ascending order.
//
// 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{
i64 time //simulation_state timestamp
u64 block_id
i64 time
u32 instruction_count
u32 block_id
}
BLOCK_BOT_SEGMENT:
//format version indicates what version of these structures to use
SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about.
//to read, greedily decode instructions until eof
loop{
//delta encode as much as possible (time,mousepos)
//strafe ticks are implied
//physics can be implied in an input-only bot file
TimedInstruction<SimulationInstruction> instruction
// segments can potentially be losslessly compressed!
for _ in 0..instruction_count{
// TODO: delta encode as much as possible (time,mousepos)
i64 time
physics::Instruction instruction
}
*/
//error hiding mock code
mod simulation{
#[super::binrw]
#[brw(little)]
pub struct State{}
#[super::binrw]
#[brw(little)]
pub struct Instruction{}
#[binrw]
#[brw(little)]
struct SegmentHeader{
time:i64,
instruction_count:u32,
block_id:BlockId,
}
#[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]
#[brw(little)]
#[derive(Clone,Copy,Debug,id::Id)]
pub struct SegmentId(u32);
#[binrw]
#[brw(little)]
pub struct Segment{
state:simulation::State,
//#[bw(try_calc(u32::try_from(instructions.len())))]
//instruction_count:u32,
//#[br(count=instruction_count)]
//instructions:Vec<instruction::TimedInstruction<simulation::Instruction>>
pub instructions:Vec<TimedPhysicsInstruction>
}
//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>{
file:crate::file::File<R>,
//timeline:timeline::Timeline<SegmentId>,
segment_id_to_block_id:Vec<crate::file::BlockId>,
segment_map:Vec<SegmentInfo>,
}
impl<R:BinReaderExt> StreamableBot<R>{
pub(crate) fn new(file:crate::file::File<R>)->Result<Self,Error>{
Err(Error::InvalidHeader)
pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{
//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>{
let block_id=*self.segment_id_to_block_id.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)?;
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
}
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)
}
}
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(())
}

@ -73,6 +73,16 @@ pub struct Header{
#[br(count=block_count+1)]
pub block_location:Vec<u64>,
}
impl Header{
pub const fn calculate_size(block_count:u32)->usize{
4 // fourcc
+4 // version
+8 // priming
+16 // resource
+4 // block_count
+(block_count as usize+1)*8 // block_location
}
}
#[binrw]
#[brw(little)]

@ -86,6 +86,7 @@ for model_id in 0..num_models{
//if you hash the resource itself and set the first 8 bits to this, that's the resource uuid
#[binrw]
#[brw(little,repr=u8)]
#[repr(u8)]
enum ResourceType{
Mesh,
Texture,
@ -94,21 +95,6 @@ enum ResourceType{
//Video,
//Animation,
}
const RESOURCE_TYPE_VARIANT_COUNT:u8=2;
#[binrw]
#[brw(little)]
struct ResourceId(u128);
impl ResourceId{
fn resource_type(&self)->Option<ResourceType>{
let discriminant=self.0 as u8;
//TODO: use this when it is stabilized https://github.com/rust-lang/rust/issues/73662
//if (discriminant as usize)<std::mem::variant_count::<ResourceType>(){
match discriminant<RESOURCE_TYPE_VARIANT_COUNT{
true=>Some(unsafe{std::mem::transmute::<u8,ResourceType>(discriminant)}),
false=>None,
}
}
}
struct ResourceMap<T>{
meshes:HashMap<strafesnet_common::model::MeshId,T>,
@ -135,11 +121,6 @@ struct ResourceBlockHeader{
resource:ResourceType,
id:BlockId,
}
#[binrw]
#[brw(little)]
struct ResourceExternalHeader{
resource_uuid:ResourceId,
}
#[binrw]
#[brw(little)]
@ -411,28 +392,26 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
attributes:map.attributes.into_iter().map(Into::into).collect(),
render_configs:map.render_configs.into_iter().map(Into::into).collect(),
};
let mut file_header=crate::file::Header{
//probe header length
let mut map_header_data=Vec::new();
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
// calculate final file header
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
offset+=map_header_data.len() as u64;
// priming includes map header
let priming=offset;
for position in &mut block_location{
*position+=offset;
}
let file_header=crate::file::Header{
fourcc:crate::file::FourCC::Map,
version:0,
priming:0,
priming,
resource:0,
block_count,
block_location,
};
//probe header length
let mut file_header_data=Vec::new();
binrw::BinWrite::write_le(&file_header,&mut std::io::Cursor::new(&mut file_header_data)).map_err(Error::InvalidData)?;
let mut map_header_data=Vec::new();
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
//update file header according to probe data
let mut offset=file_header_data.len() as u64;
file_header.priming=offset;
file_header.block_location[0]=offset;
offset+=map_header_data.len() as u64;
for position in &mut file_header.block_location[1..]{
*position+=offset;
}
//write (updated) file header
writer.write_le(&file_header).map_err(Error::InvalidData)?;

@ -1,3 +1,9 @@
pub const fn flag(b:bool,mask:u8)->u8{
(-(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
}

@ -38,6 +38,23 @@ pub struct Ratio64Vec2{
pub x: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 Planar64=i64;

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

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

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

@ -1,6 +1,6 @@
[package]
name = "strafe-client"
version = "0.10.5"
version = "0.11.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "Custom"
@ -9,24 +9,24 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
user-install=[] # as opposed to portable install
default = ["snf"]
snf = ["dep:strafesnet_snf"]
source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
[dependencies]
arrayvec = "0.7.6"
bytemuck = { version = "1.13.1", features = ["derive"] }
configparser = "3.0.2"
ddsfile = "0.5.1"
glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }
parking_lot = "0.12.1"
pollster = "0.4.0"
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
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", 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_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "23.0.1"
wgpu = "24.0.0"
winit = "0.30.7"

68
strafe-client/src/app.rs Normal file

@ -0,0 +1,68 @@
use crate::window::Instruction;
use strafesnet_common::integer;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::TimeInner as SessionTimeInner;
pub struct App<'a>{
root_time:std::time::Instant,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>,
}
impl<'a> App<'a>{
pub fn new(
root_time:std::time::Instant,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>,
)->App<'a>{
Self{
root_time,
window_thread,
}
}
fn send_timed_instruction(&mut self,instruction:Instruction){
let time=integer::Time::from_nanos(self.root_time.elapsed().as_nanos() as i64);
self.window_thread.send(TimedInstruction{time,instruction}).unwrap();
}
}
impl winit::application::ApplicationHandler for App<'_>{
fn resumed(&mut self,_event_loop:&winit::event_loop::ActiveEventLoop){
//
}
fn window_event(
&mut self,
event_loop:&winit::event_loop::ActiveEventLoop,
_window_id:winit::window::WindowId,
event:winit::event::WindowEvent,
){
match event{
winit::event::WindowEvent::KeyboardInput{
event:winit::event::KeyEvent{
logical_key:winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),
state:winit::event::ElementState::Pressed,
..
},
..
}
|winit::event::WindowEvent::CloseRequested=>{
event_loop.exit();
},
_=>(),
}
self.send_timed_instruction(Instruction::WindowEvent(event));
}
fn device_event(
&mut self,
_event_loop:&winit::event_loop::ActiveEventLoop,
_device_id:winit::event::DeviceId,
event:winit::event::DeviceEvent,
){
self.send_timed_instruction(Instruction::DeviceEvent(event));
}
fn about_to_wait(
&mut self,
_event_loop:&winit::event_loop::ActiveEventLoop
){
self.send_timed_instruction(Instruction::WindowEvent(winit::event::WindowEvent::RedrawRequested));
}
}

@ -1,5 +1,8 @@
use std::io::Read;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
#[allow(dead_code)]
#[derive(Debug)]
pub enum ReadError{
#[cfg(feature="roblox")]
@ -10,6 +13,8 @@ pub enum ReadError{
StrafesNET(strafesnet_snf::Error),
#[cfg(feature="snf")]
StrafesNETMap(strafesnet_snf::map::Error),
#[cfg(feature="snf")]
StrafesNETBot(strafesnet_snf::bot::Error),
Io(std::io::Error),
UnknownFileFormat,
}
@ -20,37 +25,56 @@ impl std::fmt::Display for ReadError{
}
impl std::error::Error for ReadError{}
pub enum DataStructure{
pub enum ReadFormat{
#[cfg(feature="roblox")]
Roblox(strafesnet_rbx_loader::Model),
#[cfg(feature="source")]
Source(strafesnet_bsp_loader::Bsp),
#[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 peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
match &peek[0..4]{
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned();
// 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")]
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")]
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")]
b"SNFM"=>Ok(DataStructure::StrafesNET(
strafesnet_snf::read_map(buf).map_err(ReadError::StrafesNET)?
b"SNFM"=>Ok(ReadFormat::SNFM(
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
.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),
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum LoadError{
ReadError(ReadError),
File(std::io::Error),
Io(std::io::Error),
#[cfg(feature="roblox")]
LoadRobloxMesh(strafesnet_rbx_loader::loader::MeshError),
#[cfg(feature="roblox")]
LoadRobloxTexture(strafesnet_rbx_loader::loader::TextureError),
#[cfg(feature="source")]
LoadSourceMesh(strafesnet_bsp_loader::loader::MeshError),
#[cfg(feature="source")]
LoadSourceTexture(strafesnet_bsp_loader::loader::TextureError),
}
impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@ -59,86 +83,69 @@ impl std::fmt::Display 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...
let file=std::fs::File::open(path).map_err(LoadError::File)?;
match read(file).map_err(LoadError::ReadError)?{
#[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")]
DataStructure::Roblox(model)=>{
ReadFormat::Roblox(model)=>{
let mut place=model.into_place();
place.run_scripts();
let mut loader=strafesnet_deferred_loader::roblox_legacy();
let (texture_loader,mesh_loader)=loader.get_inner_mut();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=strafesnet_rbx_loader::convert(
&place,
|name|texture_loader.acquire_render_config_id(name),
|name|mesh_loader.acquire_mesh_id(name),
place.as_ref(),
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let meshpart_meshes=mesh_loader.load_meshes().map_err(LoadError::Io)?;
let mut mesh_loader=strafesnet_rbx_loader::loader::MeshLoader::new();
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRobloxMesh)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(
meshpart_meshes.into_iter().map(|(mesh_id,loader_model)|
(mesh_id,strafesnet_rbx_loader::data::RobloxMeshBytes::new(loader_model.get()))
)
);
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
let (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
let mut texture_loader=strafesnet_rbx_loader::loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRobloxTexture)?;
let map=map_step2.add_render_configs_and_textures(
render_configs.into_iter(),
textures.into_iter().map(|(texture_id,texture)|
(texture_id,match texture{
strafesnet_deferred_loader::texture::Texture::ImageDDS(data)=>data,
})
)
);
let map=map_step2.add_render_configs_and_textures(render_configs);
Ok(map)
Ok(LoadFormat::Map(map))
},
#[cfg(feature="source")]
DataStructure::Source(bsp)=>{
let mut loader=strafesnet_deferred_loader::source_legacy();
let (texture_loader,mesh_loader)=loader.get_inner_mut();
ReadFormat::Source(bsp)=>{
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=strafesnet_bsp_loader::convert(
&bsp,
|name|texture_loader.acquire_render_config_id(name),
|name|mesh_loader.acquire_mesh_id(name),
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let prop_meshes=mesh_loader.load_meshes(bsp.as_ref());
let mut mesh_loader=strafesnet_bsp_loader::loader::MeshLoader::new(&bsp,&mut texture_deferred_loader);
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,LoadFailureMode::DefaultToNone).map_err(LoadError::LoadSourceMesh)?;
let map_step2=map_step1.add_prop_meshes(
//the type conflagulator 9000
prop_meshes.into_iter().map(|(mesh_id,loader_model)|
(mesh_id,strafesnet_bsp_loader::data::ModelData{
mdl:strafesnet_bsp_loader::data::MdlData::new(loader_model.mdl.get()),
vtx:strafesnet_bsp_loader::data::VtxData::new(loader_model.vtx.get()),
vvd:strafesnet_bsp_loader::data::VvdData::new(loader_model.vvd.get()),
})
),
|name|texture_loader.acquire_render_config_id(name),
);
let map_step2=map_step1.add_prop_meshes(prop_meshes);
let (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
let mut texture_loader=strafesnet_bsp_loader::loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,LoadFailureMode::DefaultToNone).map_err(LoadError::LoadSourceTexture)?;
let map=map_step2.add_render_configs_and_textures(
render_configs.into_iter(),
textures.into_iter().map(|(texture_id,texture)|
(texture_id,match texture{
strafesnet_deferred_loader::texture::Texture::ImageDDS(data)=>data,
})
),
);
let map=map_step2.add_render_configs_and_textures(render_configs);
Ok(map)
Ok(LoadFormat::Map(map))
},
}
}

@ -1,7 +1,11 @@
use strafesnet_graphics::graphics;
use strafesnet_session::session;
use strafesnet_settings::settings;
pub enum Instruction{
Render(crate::physics_worker::FrameState),
//UpdateModel(crate::graphics::GraphicsModelUpdate),
Resize(winit::dpi::PhysicalSize<u32>,crate::settings::UserSettings),
Render(session::FrameState),
//UpdateModel(graphics::GraphicsModelUpdate),
Resize(winit::dpi::PhysicalSize<u32>,settings::UserSettings),
ChangeMap(strafesnet_common::map::CompleteMap),
}
@ -14,14 +18,13 @@ 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
pub fn new<'a>(
mut graphics:crate::graphics::GraphicsState,
pub fn new(
mut graphics:graphics::GraphicsState,
mut config:wgpu::SurfaceConfiguration,
surface:wgpu::Surface<'a>,
surface:wgpu::Surface,
device:wgpu::Device,
queue:wgpu::Queue,
)->crate::compat_worker::INWorker<'a,Instruction>{
let mut resize=None;
)->crate::compat_worker::INWorker<'_,Instruction>{
crate::compat_worker::INWorker::new(move |ins:Instruction|{
match ins{
Instruction::ChangeMap(map)=>{
@ -29,18 +32,15 @@ pub fn new<'a>(
graphics.generate_models(&device,&queue,&map);
},
Instruction::Resize(size,user_settings)=>{
resize=Some((size,user_settings));
println!("Resizing to {:?}",size);
let t0=std::time::Instant::now();
config.width=size.width.max(1);
config.height=size.height.max(1);
surface.configure(&device,&config);
graphics.resize(&device,&config,&user_settings);
println!("Resize took {:?}",t0.elapsed());
}
Instruction::Render(frame_state)=>{
if let Some((size,user_settings))=resize.take(){
println!("Resizing to {:?}",size);
let t0=std::time::Instant::now();
config.width=size.width.max(1);
config.height=size.height.max(1);
surface.configure(&device,&config);
graphics.resize(&device,&config,&user_settings);
println!("Resize took {:?}",t0.elapsed());
}
//this has to go deeper somehow
let frame=match surface.get_current_texture(){
Ok(frame)=>frame,

@ -1,16 +1,9 @@
mod body;
mod app;
mod file;
mod setup;
mod window;
mod worker;
mod physics;
mod graphics;
mod settings;
mod push_solve;
mod face_crawler;
mod compat_worker;
mod model_physics;
mod model_graphics;
mod physics_worker;
mod graphics_worker;

@ -1,249 +1,81 @@
use strafesnet_common::mouse::MouseState;
use crate::graphics_worker::Instruction as GraphicsInstruction;
use strafesnet_settings::{directories::Directories,settings};
use strafesnet_session::session::{
Session,Simulation,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction,ImplicitModeInstruction,
Instruction as SessionInstruction,
};
use strafesnet_common::instruction::{TimedInstruction,InstructionConsumer};
use strafesnet_common::physics::Time as PhysicsTime;
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::physics::{Time as PhysicsTime,TimeInner as PhysicsTimeInner,Instruction as PhysicsInputInstruction};
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::timer::{Scaled,Timer,TimerState};
use mouse_interpolator::MouseInterpolator;
use strafesnet_common::timer::Timer;
pub struct FrameState{
pub body:crate::physics::Body,
pub camera:crate::physics::PhysicsCamera,
pub time:PhysicsTime,
}
#[derive(Debug)]
pub enum InputInstruction{
MoveMouse(glam::IVec2),
MoveRight(bool),
MoveUp(bool),
MoveBack(bool),
MoveLeft(bool),
MoveDown(bool),
MoveForward(bool),
Jump(bool),
Zoom(bool),
ResetAndRestart,
ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId,strafesnet_common::gameplay_modes::StageId),
PracticeFly,
}
pub enum Instruction{
Input(InputInstruction),
SessionInput(SessionInputInstruction),
SessionControl(SessionControlInstruction),
SessionPlayback(SessionPlaybackInstruction),
Render,
Resize(winit::dpi::PhysicalSize<u32>),
ChangeMap(strafesnet_common::map::CompleteMap),
//SetPaused is not an InputInstruction: the physics doesn't know that it's paused.
SetPaused(bool),
//Graphics(crate::graphics_worker::Instruction),
}
mod mouse_interpolator{
use super::*;
//TODO: move this or tab
pub struct MouseInterpolator{
//"PlayerController"
user_settings:crate::settings::UserSettings,
//"MouseInterpolator"
timeline:std::collections::VecDeque<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
last_mouse_time:PhysicsTime,
mouse_blocking:bool,
//"Simulation"
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:crate::physics::PhysicsContext,
}
impl MouseInterpolator{
pub fn new(
physics:crate::physics::PhysicsContext,
user_settings:crate::settings::UserSettings,
)->MouseInterpolator{
MouseInterpolator{
mouse_blocking:true,
last_mouse_time:physics.get_next_mouse().time,
timeline:std::collections::VecDeque::new(),
timer:Timer::from_state(Scaled::identity(),false),
physics,
user_settings,
}
}
fn push_mouse_instruction(&mut self,ins:&TimedInstruction<Instruction,SessionTimeInner>,m:glam::IVec2){
if self.mouse_blocking{
//tell the game state which is living in the past about its future
self.timeline.push_front(TimedInstruction{
time:self.last_mouse_time,
instruction:PhysicsInputInstruction::SetNextMouse(MouseState{time:self.timer.time(ins.time),pos:m}),
});
}else{
//mouse has just started moving again after being still for longer than 10ms.
//replace the entire mouse interpolation state to avoid an intermediate state with identical m0.t m1.t timestamps which will divide by zero
self.timeline.push_front(TimedInstruction{
time:self.last_mouse_time,
instruction:PhysicsInputInstruction::ReplaceMouse(
MouseState{time:self.last_mouse_time,pos:self.physics.get_next_mouse().pos},
MouseState{time:self.timer.time(ins.time),pos:m}
),
});
//delay physics execution until we have an interpolation target
self.mouse_blocking=true;
}
self.last_mouse_time=self.timer.time(ins.time);
}
fn push(&mut self,time:SessionTime,phys_input:PhysicsInputInstruction){
//This is always a non-mouse event
self.timeline.push_back(TimedInstruction{
time:self.timer.time(time),
instruction:phys_input,
});
}
/// returns should_empty_queue
/// may or may not mutate internal state XD!
fn map_instruction(&mut self,ins:&TimedInstruction<Instruction,SessionTimeInner>)->bool{
let mut update_mouse_blocking=true;
match &ins.instruction{
Instruction::Input(input_instruction)=>match input_instruction{
&InputInstruction::MoveMouse(m)=>{
if !self.timer.is_paused(){
self.push_mouse_instruction(ins,m);
}
update_mouse_blocking=false;
},
&InputInstruction::MoveForward(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveForward(s)),
&InputInstruction::MoveLeft(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveLeft(s)),
&InputInstruction::MoveBack(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveBack(s)),
&InputInstruction::MoveRight(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveRight(s)),
&InputInstruction::MoveUp(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveUp(s)),
&InputInstruction::MoveDown(s)=>self.push(ins.time,PhysicsInputInstruction::SetMoveDown(s)),
&InputInstruction::Jump(s)=>self.push(ins.time,PhysicsInputInstruction::SetJump(s)),
&InputInstruction::Zoom(s)=>self.push(ins.time,PhysicsInputInstruction::SetZoom(s)),
&InputInstruction::ResetAndSpawn(mode_id,stage_id)=>{
self.push(ins.time,PhysicsInputInstruction::Reset);
self.push(ins.time,PhysicsInputInstruction::SetSensitivity(self.user_settings.calculate_sensitivity()));
self.push(ins.time,PhysicsInputInstruction::Spawn(mode_id,stage_id));
},
InputInstruction::ResetAndRestart=>{
self.push(ins.time,PhysicsInputInstruction::Reset);
self.push(ins.time,PhysicsInputInstruction::SetSensitivity(self.user_settings.calculate_sensitivity()));
self.push(ins.time,PhysicsInputInstruction::Restart);
},
InputInstruction::PracticeFly=>self.push(ins.time,PhysicsInputInstruction::PracticeFly),
},
//do these really need to idle the physics?
//sending None dumps the instruction queue
Instruction::ChangeMap(_)=>self.push(ins.time,PhysicsInputInstruction::Idle),
Instruction::Resize(_)=>self.push(ins.time,PhysicsInputInstruction::Idle),
Instruction::Render=>self.push(ins.time,PhysicsInputInstruction::Idle),
&Instruction::SetPaused(paused)=>{
if let Err(e)=self.timer.set_paused(ins.time,paused){
println!("Cannot SetPaused: {e}");
}
self.push(ins.time,PhysicsInputInstruction::Idle);
},
}
if update_mouse_blocking{
//this returns the bool for us
self.update_mouse_blocking(ins.time)
}else{
//do flush that queue
true
}
}
/// must check if self.mouse_blocking==true before calling!
fn unblock_mouse(&mut self,time:SessionTime){
//push an event to extrapolate no movement from
self.timeline.push_front(TimedInstruction{
time:self.last_mouse_time,
instruction:PhysicsInputInstruction::SetNextMouse(MouseState{time:self.timer.time(time),pos:self.physics.get_next_mouse().pos}),
});
self.last_mouse_time=self.timer.time(time);
//stop blocking. the mouse is not moving so the physics does not need to live in the past and wait for interpolation targets.
self.mouse_blocking=false;
}
fn update_mouse_blocking(&mut self,time:SessionTime)->bool{
if self.mouse_blocking{
//assume the mouse has stopped moving after 10ms.
//shitty mice are 125Hz which is 8ms so this should cover that.
//setting this to 100us still doesn't print even though it's 10x lower than the polling rate,
//so mouse events are probably not handled separately from drawing and fire right before it :(
if PhysicsTime::from_millis(10)<self.timer.time(time)-self.physics.get_next_mouse().time{
self.unblock_mouse(time);
true
}else{
false
}
}else{
//keep this up to date so that it can be used as a known-timestamp
//that the mouse was not moving when the mouse starts moving again
self.last_mouse_time=self.timer.time(time);
true
}
}
fn empty_queue(&mut self){
while let Some(instruction)=self.timeline.pop_front(){
self.physics.run_input_instruction(instruction);
}
}
pub fn handle_instruction(&mut self,ins:&TimedInstruction<Instruction,SessionTimeInner>){
let should_empty_queue=self.map_instruction(ins);
if should_empty_queue{
self.empty_queue();
}
}
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
FrameState{
body:self.physics.camera_body(),
camera:self.physics.camera(),
time:self.timer.time(time),
}
}
pub fn change_map(&mut self,time:SessionTime,map:&strafesnet_common::map::CompleteMap){
//dump any pending interpolation state
if self.mouse_blocking{
self.unblock_mouse(time);
}
self.empty_queue();
//doing it like this to avoid doing PhysicsInstruction::ChangeMap(Rc<CompleteMap>)
self.physics.generate_models(&map);
//use the standard input interface so the instructions are written out to bots
self.handle_instruction(&TimedInstruction{
time,
instruction:Instruction::Input(InputInstruction::ResetAndSpawn(
strafesnet_common::gameplay_modes::ModeId::MAIN,
strafesnet_common::gameplay_modes::StageId::FIRST,
)),
});
}
pub const fn user_settings(&self)->&crate::settings::UserSettings{
&self.user_settings
}
}
LoadReplay(strafesnet_snf::bot::Segment),
}
pub fn new<'a>(
mut graphics_worker:crate::compat_worker::INWorker<'a,crate::graphics_worker::Instruction>,
user_settings:crate::settings::UserSettings,
directories:Directories,
user_settings:settings::UserSettings,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{
let physics=crate::physics::PhysicsContext::default();
let mut interpolator=MouseInterpolator::new(
physics,
user_settings
let physics=strafesnet_physics::physics::PhysicsState::default();
let timer=Timer::unpaused(SessionTime::ZERO,PhysicsTime::ZERO);
let simulation=Simulation::new(timer,physics);
let mut session=Session::new(
user_settings,
directories,
simulation,
);
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction,SessionTimeInner>|{
interpolator.handle_instruction(&ins);
// excruciating pain
macro_rules! run_session_instruction{
($time:expr,$instruction:expr)=>{
session.process_instruction(TimedInstruction{
time:$time,
instruction:$instruction,
});
};
}
macro_rules! run_graphics_worker_instruction{
($instruction:expr)=>{
graphics_worker.send($instruction).unwrap();
};
}
match ins.instruction{
Instruction::SessionInput(unbuffered_instruction)=>{
run_session_instruction!(ins.time,SessionInstruction::Input(unbuffered_instruction));
},
Instruction::SessionControl(unbuffered_instruction)=>{
run_session_instruction!(ins.time,SessionInstruction::Control(unbuffered_instruction));
},
Instruction::SessionPlayback(unbuffered_instruction)=>{
run_session_instruction!(ins.time,SessionInstruction::Playback(unbuffered_instruction));
},
Instruction::Render=>{
let frame_state=interpolator.get_frame_state(ins.time);
graphics_worker.send(crate::graphics_worker::Instruction::Render(frame_state)).unwrap();
run_session_instruction!(ins.time,SessionInstruction::Idle);
if let Some(frame_state)=session.get_frame_state(ins.time){
run_graphics_worker_instruction!(GraphicsInstruction::Render(frame_state));
}
},
Instruction::Resize(size)=>{
graphics_worker.send(crate::graphics_worker::Instruction::Resize(size,interpolator.user_settings().clone())).unwrap();
Instruction::Resize(physical_size)=>{
run_session_instruction!(ins.time,SessionInstruction::Idle);
let user_settings=session.user_settings().clone();
run_graphics_worker_instruction!(GraphicsInstruction::Resize(physical_size,user_settings));
},
Instruction::ChangeMap(map)=>{
interpolator.change_map(ins.time,&map);
graphics_worker.send(crate::graphics_worker::Instruction::ChangeMap(map)).unwrap();
Instruction::ChangeMap(complete_map)=>{
run_session_instruction!(ins.time,SessionInstruction::ChangeMap(&complete_map));
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));
},
Instruction::Input(_)=>(),
Instruction::SetPaused(_)=>(),
Instruction::LoadReplay(bot)=>{
run_session_instruction!(ins.time,SessionInstruction::LoadReplay(bot));
}
}
})
}

@ -1,8 +1,3 @@
use crate::window::WindowInstruction;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::integer;
use strafesnet_common::session::TimeInner as SessionTimeInner;
fn optional_features()->wgpu::Features{
wgpu::Features::TEXTURE_COMPRESSION_ASTC
|wgpu::Features::TEXTURE_COMPRESSION_ETC2
@ -17,9 +12,6 @@ fn required_downlevel_capabilities()->wgpu::DownlevelCapabilities{
..wgpu::DownlevelCapabilities::default()
}
}
pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
struct SetupContextPartial1{
backends:wgpu::Backends,
@ -28,23 +20,13 @@ struct SetupContextPartial1{
fn create_window(title:&str,event_loop:&winit::event_loop::EventLoop<()>)->Result<winit::window::Window,winit::error::OsError>{
let mut attr=winit::window::WindowAttributes::default();
attr=attr.with_title(title);
#[cfg(windows_OFF)] // TODO
{
use winit::platform::windows::WindowBuilderExtWindows;
builder=builder.with_no_redirection_bitmap(true);
}
event_loop.create_window(attr)
}
fn create_instance()->SetupContextPartial1{
let backends=wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all);
let dx12_shader_compiler=wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default();
let backends=wgpu::Backends::from_env().unwrap_or_default();
SetupContextPartial1{
backends,
instance:wgpu::Instance::new(wgpu::InstanceDescriptor{
backends,
dx12_shader_compiler,
..Default::default()
}),
instance:Default::default(),
}
}
impl SetupContextPartial1{
@ -118,14 +100,12 @@ impl<'a> SetupContextPartial2<'a>{
required_downlevel_capabilities.flags - downlevel_capabilities.flags
);
SetupContextPartial3{
instance:self.instance,
surface:self.surface,
adapter,
}
}
}
struct SetupContextPartial3<'a>{
instance:wgpu::Instance,
surface:wgpu::Surface<'a>,
adapter:wgpu::Adapter,
}
@ -135,7 +115,7 @@ impl<'a> SetupContextPartial3<'a>{
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.
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 (device, queue)=pollster::block_on(self.adapter
@ -151,7 +131,6 @@ impl<'a> SetupContextPartial3<'a>{
.expect("Unable to find a suitable GPU adapter!");
SetupContextPartial4{
instance:self.instance,
surface:self.surface,
adapter:self.adapter,
device,
@ -160,7 +139,6 @@ impl<'a> SetupContextPartial3<'a>{
}
}
struct SetupContextPartial4<'a>{
instance:wgpu::Instance,
surface:wgpu::Surface<'a>,
adapter:wgpu::Adapter,
device:wgpu::Device,
@ -177,7 +155,6 @@ impl<'a> SetupContextPartial4<'a>{
self.surface.configure(&self.device, &config);
SetupContext{
instance:self.instance,
surface:self.surface,
device:self.device,
queue:self.queue,
@ -186,7 +163,6 @@ impl<'a> SetupContextPartial4<'a>{
}
}
pub struct SetupContext<'a>{
pub instance:wgpu::Instance,
pub surface:wgpu::Surface<'a>,
pub device:wgpu::Device,
pub queue:wgpu::Queue,
@ -220,74 +196,17 @@ pub fn setup_and_start(title:&str){
setup_context,
);
if let Some(arg)=std::env::args().nth(1){
let path=std::path::PathBuf::from(arg);
window_thread.send(TimedInstruction{
time:integer::Time::ZERO,
instruction:WindowInstruction::WindowEvent(winit::event::WindowEvent::DroppedFile(path)),
for arg in std::env::args().skip(1){
window_thread.send(strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::ZERO,
instruction:crate::window::Instruction::WindowEvent(winit::event::WindowEvent::DroppedFile(arg.into())),
}).unwrap();
};
println!("Entering event loop...");
let root_time=std::time::Instant::now();
run_event_loop(event_loop,window_thread,root_time).unwrap();
}
fn run_event_loop(
event_loop:winit::event_loop::EventLoop<()>,
mut window_thread:crate::compat_worker::QNWorker<TimedInstruction<WindowInstruction,SessionTimeInner>>,
root_time:std::time::Instant
)->Result<(),winit::error::EventLoopError>{
event_loop.run(move |event,elwt|{
let time=integer::Time::from_nanos(root_time.elapsed().as_nanos() as i64);
// *control_flow=if cfg!(feature="metal-auto-capture"){
// winit::event_loop::ControlFlow::Exit
// }else{
// winit::event_loop::ControlFlow::Poll
// };
match event{
winit::event::Event::AboutToWait=>{
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::RequestRedraw}).unwrap();
}
winit::event::Event::WindowEvent {
event:
// WindowEvent::Resized(size)
// | WindowEvent::ScaleFactorChanged {
// new_inner_size: &mut size,
// ..
// },
winit::event::WindowEvent::Resized(size),//ignoring scale factor changed for now because mutex bruh
window_id:_,
} => {
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::Resize(size)}).unwrap();
}
winit::event::Event::WindowEvent{event,..}=>match event{
winit::event::WindowEvent::KeyboardInput{
event:
winit::event::KeyEvent {
logical_key: winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),
state: winit::event::ElementState::Pressed,
..
},
..
}
|winit::event::WindowEvent::CloseRequested=>{
elwt.exit();
}
winit::event::WindowEvent::RedrawRequested=>{
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::Render}).unwrap();
}
_=>{
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::WindowEvent(event)}).unwrap();
}
},
winit::event::Event::DeviceEvent{
event,
..
} => {
window_thread.send(TimedInstruction{time,instruction:WindowInstruction::DeviceEvent(event)}).unwrap();
},
_=>{}
}
})
let mut app=crate::app::App::new(
std::time::Instant::now(),
window_thread
);
event_loop.run_app(&mut app).unwrap();
}

@ -1,23 +1,23 @@
use crate::physics_worker::InputInstruction;
use strafesnet_common::integer;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::physics::{MiscInstruction,SetControlInstruction};
use crate::file::LoadFormat;
use crate::physics_worker::Instruction as PhysicsWorkerInstruction;
use strafesnet_session::session::{self,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction};
use strafesnet_settings::directories::Directories;
pub enum WindowInstruction{
Resize(winit::dpi::PhysicalSize<u32>),
pub enum Instruction{
WindowEvent(winit::event::WindowEvent),
DeviceEvent(winit::event::DeviceEvent),
RequestRedraw,
Render,
}
//holds thread handles to dispatch to
struct WindowContext<'a>{
manual_mouse_lock:bool,
mouse:strafesnet_common::mouse::MouseState<SessionTimeInner>,//std::sync::Arc<std::sync::Mutex<>>
mouse_pos:glam::DVec2,
screen_size:glam::UVec2,
window:&'a winit::window::Window,
physics_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<crate::physics_worker::Instruction,SessionTimeInner>>,
physics_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<PhysicsWorkerInstruction,SessionTimeInner>>,
}
impl WindowContext<'_>{
@ -28,15 +28,16 @@ impl WindowContext<'_>{
match event{
winit::event::WindowEvent::DroppedFile(path)=>{
match crate::file::load(path.as_path()){
Ok(map)=>self.physics_thread.send(TimedInstruction{time,instruction:crate::physics_worker::Instruction::ChangeMap(map)}).unwrap(),
Err(e)=>println!("Failed to load map: {e}"),
Ok(LoadFormat::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}).unwrap(),
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)=>{
//pause unpause
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::SetPaused(!state),
instruction:PhysicsWorkerInstruction::SessionControl(SessionControlInstruction::SetPaused(!state)),
}).unwrap();
//recalculate pressed keys on focus
},
@ -91,34 +92,98 @@ impl WindowContext<'_>{
},
(keycode,state)=>{
let s=state.is_pressed();
if let Some(input_instruction)=match keycode{
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space)=>Some(InputInstruction::Jump(s)),
// internal variants for this scope
enum SessionInstructionSubset{
Input(SessionInputInstruction),
Control(SessionControlInstruction),
Playback(SessionPlaybackInstruction),
}
macro_rules! input_ctrl{
($variant:ident,$state:expr)=>{
Some(SessionInstructionSubset::Input(SessionInputInstruction::SetControl(SetControlInstruction::$variant($state))))
};
}
macro_rules! input_misc{
($variant:ident,$state:expr)=>{
s.then_some(SessionInstructionSubset::Input(SessionInputInstruction::Misc(MiscInstruction::$variant)))
};
}
macro_rules! session_ctrl{
($variant:ident,$state:expr)=>{
s.then_some(SessionInstructionSubset::Control(SessionControlInstruction::$variant))
};
}
macro_rules! session_playback{
($variant:ident,$state:expr)=>{
s.then_some(SessionInstructionSubset::Playback(SessionPlaybackInstruction::$variant))
};
}
impl From<SessionInstructionSubset> for PhysicsWorkerInstruction{
fn from(value:SessionInstructionSubset)->Self{
match value{
SessionInstructionSubset::Input(session_input_instruction)=>PhysicsWorkerInstruction::SessionInput(session_input_instruction),
SessionInstructionSubset::Control(session_control_instruction)=>PhysicsWorkerInstruction::SessionControl(session_control_instruction),
SessionInstructionSubset::Playback(session_playback_instruction)=>PhysicsWorkerInstruction::SessionPlayback(session_playback_instruction),
}
}
}
if let Some(session_instruction)=match keycode{
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space)=>input_ctrl!(SetJump,s),
// TODO: bind system so playback pausing can use spacebar
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Enter)=>session_playback!(TogglePaused,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowUp)=>session_playback!(IncreaseTimescale,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowDown)=>session_playback!(DecreaseTimescale,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowLeft)=>session_playback!(SkipBack,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowRight)=>session_playback!(SkipForward,s),
winit::keyboard::Key::Character(key)=>match key.as_str(){
"W"|"w"=>Some(InputInstruction::MoveForward(s)),
"A"|"a"=>Some(InputInstruction::MoveLeft(s)),
"S"|"s"=>Some(InputInstruction::MoveBack(s)),
"D"|"d"=>Some(InputInstruction::MoveRight(s)),
"E"|"e"=>Some(InputInstruction::MoveUp(s)),
"Q"|"q"=>Some(InputInstruction::MoveDown(s)),
"Z"|"z"=>Some(InputInstruction::Zoom(s)),
"R"|"r"=>if s{
"W"|"w"=>input_ctrl!(SetMoveForward,s),
"A"|"a"=>input_ctrl!(SetMoveLeft,s),
"S"|"s"=>input_ctrl!(SetMoveBack,s),
"D"|"d"=>input_ctrl!(SetMoveRight,s),
"E"|"e"=>input_ctrl!(SetMoveUp,s),
"Q"|"q"=>input_ctrl!(SetMoveDown,s),
"Z"|"z"=>input_ctrl!(SetZoom,s),
"R"|"r"=>s.then(||{
//mouse needs to be reset since the position is absolute
self.mouse=strafesnet_common::mouse::MouseState::default();
Some(InputInstruction::ResetAndRestart)
}else{None},
"F"|"f"=>if s{Some(InputInstruction::PracticeFly)}else{None},
self.mouse_pos=glam::DVec2::ZERO;
SessionInstructionSubset::Input(SessionInputInstruction::Mode(session::ImplicitModeInstruction::ResetAndRestart))
}),
"F"|"f"=>input_misc!(PracticeFly,s),
"B"|"b"=>session_ctrl!(CopyRecordingIntoReplayAndSpectate,s),
"X"|"x"=>session_ctrl!(StopSpectate,s),
"N"|"n"=>session_ctrl!(SaveReplay,s),
"J"|"j"=>session_ctrl!(LoadIntoReplayState,s),
_=>None,
},
_=>None,
}{
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Input(input_instruction),
instruction:session_instruction.into(),
}).unwrap();
}
},
}
},
winit::event::WindowEvent::Resized(size)=>{
self.physics_thread.send(
TimedInstruction{
time,
instruction:PhysicsWorkerInstruction::Resize(size)
}
).unwrap();
},
winit::event::WindowEvent::RedrawRequested=>{
self.window.request_redraw();
self.physics_thread.send(
TimedInstruction{
time,
instruction:PhysicsWorkerInstruction::Render
}
).unwrap();
},
_=>(),
}
}
@ -126,7 +191,7 @@ impl WindowContext<'_>{
fn device_event(&mut self,time:SessionTime,event: winit::event::DeviceEvent){
match event{
winit::event::DeviceEvent::MouseMotion{
delta,//these (f64,f64) are integers on my machine
delta,
}=>{
if self.manual_mouse_lock{
match self.window.set_cursor_position(self.get_middle_of_screen()){
@ -134,14 +199,10 @@ impl WindowContext<'_>{
Err(e)=>println!("Could not set cursor position: {:?}",e),
}
}
//do not step the physics because the mouse polling rate is higher than the physics can run.
//essentially the previous input will be overwritten until a true step runs
//which is fine because they run all the time.
let delta=glam::ivec2(delta.0 as i32,delta.1 as i32);
self.mouse.pos+=delta;
self.mouse_pos+=glam::dvec2(delta.0,delta.1);
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Input(InputInstruction::MoveMouse(self.mouse.pos)),
instruction:PhysicsWorkerInstruction::SessionInput(SessionInputInstruction::Mouse(self.mouse_pos.as_ivec2())),
}).unwrap();
},
winit::event::DeviceEvent::MouseWheel {
@ -151,7 +212,7 @@ impl WindowContext<'_>{
if false{//self.physics.style.use_scroll{
self.physics_thread.send(TimedInstruction{
time,
instruction:crate::physics_worker::Instruction::Input(InputInstruction::Jump(true)),//activates the immediate jump path, but the style modifier prevents controls&CONTROL_JUMP bit from being set to auto jump
instruction:PhysicsWorkerInstruction::SessionInput(SessionInputInstruction::SetControl(SetControlInstruction::SetJump(true))),//activates the immediate jump path, but the style modifier prevents controls&CONTROL_JUMP bit from being set to auto jump
}).unwrap();
}
},
@ -162,11 +223,16 @@ impl WindowContext<'_>{
pub fn worker<'a>(
window:&'a winit::window::Window,
setup_context:crate::setup::SetupContext<'a>,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<WindowInstruction,SessionTimeInner>>{
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>{
// WindowContextSetup::new
let user_settings=crate::settings::read_user_settings();
#[cfg(feature="user-install")]
let directories=Directories::user().unwrap();
#[cfg(not(feature="user-install"))]
let directories=Directories::portable().unwrap();
let mut graphics=crate::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config);
let user_settings=directories.settings();
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config);
graphics.load_user_settings(&user_settings);
//WindowContextSetup::into_context
@ -174,44 +240,26 @@ pub fn worker<'a>(
let graphics_thread=crate::graphics_worker::new(graphics,setup_context.config,setup_context.surface,setup_context.device,setup_context.queue);
let mut window_context=WindowContext{
manual_mouse_lock:false,
mouse:strafesnet_common::mouse::MouseState::default(),
mouse_pos:glam::DVec2::ZERO,
//make sure to update this!!!!!
screen_size,
window,
physics_thread:crate::physics_worker::new(
graphics_thread,
directories,
user_settings,
),
};
//WindowContextSetup::into_worker
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<WindowInstruction,SessionTimeInner>|{
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction,SessionTimeInner>|{
match ins.instruction{
WindowInstruction::RequestRedraw=>{
window_context.window.request_redraw();
}
WindowInstruction::WindowEvent(window_event)=>{
Instruction::WindowEvent(window_event)=>{
window_context.window_event(ins.time,window_event);
},
WindowInstruction::DeviceEvent(device_event)=>{
Instruction::DeviceEvent(device_event)=>{
window_context.device_event(ins.time,device_event);
},
WindowInstruction::Resize(size)=>{
window_context.physics_thread.send(
TimedInstruction{
time:ins.time,
instruction:crate::physics_worker::Instruction::Resize(size)
}
).unwrap();
}
WindowInstruction::Render=>{
window_context.physics_thread.send(
TimedInstruction{
time:ins.time,
instruction:crate::physics_worker::Instruction::Render
}
).unwrap();
}
}
})
}

@ -176,7 +176,7 @@ impl<'a,Task:Send+'a> INWorker<'a,Task>{
#[cfg(test)]
mod test{
use super::{thread,QRWorker};
type Body=crate::physics::Body;
type Body=strafesnet_physics::physics::Body;
use strafesnet_common::{integer,instruction};
#[test]//How to run this test with printing: cargo test --release -- --nocapture
fn test_worker() {
@ -190,7 +190,7 @@ mod test{
for _ in 0..5 {
let task = instruction::TimedInstruction{
time:strafesnet_common::physics::Time::ZERO,
instruction:strafesnet_common::physics::Instruction::Idle,
instruction:strafesnet_common::physics::Instruction::IDLE,
};
worker.send(task).unwrap();
}
@ -204,7 +204,7 @@ mod test{
// Send a new task
let task = instruction::TimedInstruction{
time:integer::Time::ZERO,
instruction:strafesnet_common::physics::Instruction::Idle,
instruction:strafesnet_common::physics::Instruction::IDLE,
};
worker.send(task).unwrap();

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

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

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

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

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

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