Compare commits
24 Commits
surf-fix
...
refactor-l
Author | SHA1 | Date | |
---|---|---|---|
2c729adf64 | |||
3467bc77b0 | |||
2cf5ff5059 | |||
b550778a60 | |||
69599b23be | |||
2d9ad990c2 | |||
be9eb30a5c | |||
42310c5d2b | |||
988750a3c6 | |||
2f26662dda | |||
6f739dba8d | |||
2eaddd493d | |||
d5a6f8e1bc | |||
95fb43b908 | |||
3e814cb41a | |||
ea854a548f | |||
5f2cf8f32e | |||
b6b090de78 | |||
affbada62e | |||
8d2ba28700 | |||
3eb4e76ab2 | |||
5d31419370 | |||
44a58044c7 | |||
83ac776b78 |
Cargo.lockCargo.tomlREADME.md
engine
graphics
physics
session
settings
integration-testing
lib
strafe-client
117
Cargo.lock
generated
117
Cargo.lock
generated
@ -493,6 +493,27 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@ -944,6 +965,15 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "integration-testing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"strafesnet_common",
|
||||
"strafesnet_physics",
|
||||
"strafesnet_snf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@ -1588,6 +1618,12 @@ version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.48"
|
||||
@ -1979,6 +2015,13 @@ dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rbxassetid"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@ -1997,6 +2040,17 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@ -2253,19 +2307,17 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
name = "strafe-client"
|
||||
version = "0.11.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"configparser",
|
||||
"ddsfile",
|
||||
"glam",
|
||||
"id",
|
||||
"parking_lot",
|
||||
"pollster",
|
||||
"replace_with",
|
||||
"strafesnet_bsp_loader",
|
||||
"strafesnet_common",
|
||||
"strafesnet_deferred_loader",
|
||||
"strafesnet_graphics",
|
||||
"strafesnet_physics",
|
||||
"strafesnet_rbx_loader",
|
||||
"strafesnet_session",
|
||||
"strafesnet_settings",
|
||||
"strafesnet_snf",
|
||||
"wgpu",
|
||||
"winit",
|
||||
@ -2277,6 +2329,7 @@ version = "0.2.2"
|
||||
dependencies = [
|
||||
"glam",
|
||||
"strafesnet_common",
|
||||
"strafesnet_deferred_loader",
|
||||
"vbsp",
|
||||
"vmdl",
|
||||
]
|
||||
@ -2296,11 +2349,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_deferred_loader"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"strafesnet_common",
|
||||
"url",
|
||||
"vbsp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_graphics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"ddsfile",
|
||||
"glam",
|
||||
"id",
|
||||
"strafesnet_common",
|
||||
"strafesnet_session",
|
||||
"strafesnet_settings",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_physics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"glam",
|
||||
"id",
|
||||
"strafesnet_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2315,8 +2390,32 @@ dependencies = [
|
||||
"rbx_mesh",
|
||||
"rbx_reflection_database",
|
||||
"rbx_xml",
|
||||
"rbxassetid",
|
||||
"roblox_emulator",
|
||||
"strafesnet_common",
|
||||
"strafesnet_deferred_loader",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_session"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"glam",
|
||||
"replace_with",
|
||||
"strafesnet_common",
|
||||
"strafesnet_physics",
|
||||
"strafesnet_settings",
|
||||
"strafesnet_snf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_settings"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"configparser",
|
||||
"directories",
|
||||
"glam",
|
||||
"strafesnet_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -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.
|
||||
|
14
engine/graphics/Cargo.toml
Normal file
14
engine/graphics/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "strafesnet_graphics"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.13.1", features = ["derive"] }
|
||||
ddsfile = "0.5.1"
|
||||
glam = "0.29.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
strafesnet_session = { path = "../session", registry = "strafesnet" }
|
||||
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
|
||||
wgpu = "24.0.0"
|
8
engine/graphics/LICENSE
Normal file
8
engine/graphics/LICENSE
Normal file
@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
@ -1,9 +1,15 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashSet,HashMap};
|
||||
use strafesnet_common::map;
|
||||
use strafesnet_settings::settings;
|
||||
use strafesnet_session::session;
|
||||
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
|
||||
use 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,
|
||||
@ -136,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){
|
||||
@ -448,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(){
|
||||
@ -608,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
|
||||
@ -636,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!(),
|
||||
};
|
||||
|
||||
@ -682,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();
|
||||
|
||||
@ -864,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);
|
||||
@ -875,7 +881,7 @@ impl GraphicsState{
|
||||
view:&wgpu::TextureView,
|
||||
device:&wgpu::Device,
|
||||
queue:&wgpu::Queue,
|
||||
frame_state:crate::session::FrameState,
|
||||
frame_state:session::FrameState,
|
||||
){
|
||||
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input
|
||||
|
2
engine/graphics/src/lib.rs
Normal file
2
engine/graphics/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod model;
|
||||
pub mod graphics;
|
10
engine/physics/Cargo.toml
Normal file
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
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};
|
||||
|
45
engine/physics/src/lib.rs
Normal file
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};
|
||||
@ -718,8 +719,8 @@ impl MinkowskiMesh<'_>{
|
||||
//
|
||||
// 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().map_or(None,|dir|{
|
||||
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;
|
||||
@ -729,23 +730,23 @@ impl MinkowskiMesh<'_>{
|
||||
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_in(&self,relative_body:&Body,start_time:Time,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
self.closest_fev_not_inside(relative_body.clone(),start_time).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
|
||||
fev.crawl(self,relative_body,start_time,time_limit).hit()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_out(&self,relative_body:&Body,start_time:Time,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=-relative_body.clone();
|
||||
self.closest_fev_not_inside(infinity_body,-time_limit).map_or(None,|fev|{
|
||||
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
|
||||
//continue backwards along the body parabola
|
||||
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,start_time:Time,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={
|
||||
@ -1004,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);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
use std::collections::{HashMap,HashSet};
|
||||
use crate::model_physics::{self,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
|
||||
use crate::model::{self as model_physics,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
|
||||
use strafesnet_common::bvh;
|
||||
use strafesnet_common::map;
|
||||
use strafesnet_common::run;
|
||||
@ -14,45 +14,6 @@ use strafesnet_common::integer::{self,vec3,mat3,Planar64,Planar64Vec3,Planar64Ma
|
||||
pub use strafesnet_common::physics::{Time,TimeInner};
|
||||
use gameplay::ModeState;
|
||||
|
||||
// Physics bug fixes can easily desync all bots.
|
||||
//
|
||||
// When replaying a bot, use the exact physics version which it was recorded with.
|
||||
//
|
||||
// When validating a new bot, ignore the version and use the latest version,
|
||||
// and overwrite the version in the file.
|
||||
//
|
||||
// Compatible physics versions should be determined
|
||||
// empirically at development time via leaderboard resimulation.
|
||||
//
|
||||
// Compatible physics versions should result in an identical leaderboard state,
|
||||
// or the only bots which fail are ones exploiting a surgically patched bug.
|
||||
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
|
||||
pub struct PhysicsVersion(u32);
|
||||
pub const VERSION:PhysicsVersion=PhysicsVersion(1);
|
||||
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
|
||||
let compat=[0,1];
|
||||
|
||||
let mut input_version=0;
|
||||
while input_version<compat.len(){
|
||||
// compatible version must be greater that or equal to the input version
|
||||
assert!(input_version as u32<=compat[input_version]);
|
||||
// compatible version must be a version that exists
|
||||
assert!(compat[input_version]<=VERSION.0);
|
||||
input_version+=1;
|
||||
}
|
||||
compat
|
||||
};
|
||||
pub enum PhysicsVersionError{
|
||||
UnknownPhysicsVersion,
|
||||
}
|
||||
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
|
||||
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
|
||||
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
|
||||
}else{
|
||||
Err(PhysicsVersionError::UnknownPhysicsVersion)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Body=crate::body::Body<TimeInner>;
|
||||
type MouseState=strafesnet_common::mouse::MouseState<TimeInner>;
|
||||
|
||||
@ -268,12 +229,6 @@ impl PhysicsModels{
|
||||
.map(|model|&model.transform),
|
||||
}
|
||||
}
|
||||
fn contact_model(&self,model_id:ContactModelId)->&ContactModel{
|
||||
&self.contact_models[&model_id]
|
||||
}
|
||||
fn intersect_model(&self,model_id:IntersectModelId)->&IntersectModel{
|
||||
&self.intersect_models[&model_id]
|
||||
}
|
||||
fn contact_attr(&self,model_id:ContactModelId)->&gameplay_attributes::ContactAttributes{
|
||||
&self.contact_attributes[&self.contact_models[&model_id].attr_id]
|
||||
}
|
||||
@ -308,7 +263,7 @@ impl PhysicsCamera{
|
||||
);
|
||||
self.clamped_mouse_pos=unclamped_mouse_pos;
|
||||
}
|
||||
pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2 {
|
||||
pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2{
|
||||
let a=-self.sensitivity.mul_int((self.clamped_mouse_pos+mouse_delta).as_i64vec2());
|
||||
let ax=Angle32::wrap_from_i64(a.x);
|
||||
let ay=Angle32::clamp_from_i64(a.y)
|
||||
@ -830,13 +785,13 @@ impl TouchingState{
|
||||
crate::push_solve::push_solve(&contacts,acceleration)
|
||||
}
|
||||
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,TimeInner>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
|
||||
// let relative_body=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
|
||||
// let relative_body=body.relative_to(&Body::ZERO);
|
||||
let relative_body=body;
|
||||
for contact in &self.contacts{
|
||||
//detect face slide off
|
||||
let model_mesh=models.contact_mesh(contact);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time,collector.time(),contact.face_id).map(|(_face,time)|{
|
||||
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time..collector.time(),contact.face_id).map(|(_face,time)|{
|
||||
TimedInstruction{
|
||||
time:relative_body.time+time.into(),
|
||||
instruction:InternalInstruction::CollisionEnd(
|
||||
@ -850,7 +805,7 @@ impl TouchingState{
|
||||
//detect model collision in reverse
|
||||
let model_mesh=models.intersect_mesh(intersect);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_out(&relative_body,start_time,collector.time()).map(|(_face,time)|{
|
||||
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time()).map(|(_face,time)|{
|
||||
TimedInstruction{
|
||||
time:relative_body.time+time.into(),
|
||||
instruction:InternalInstruction::CollisionEnd(
|
||||
@ -1179,15 +1134,15 @@ impl PhysicsData{
|
||||
state.body.grow_aabb(&mut aabb,state.time,collector.time());
|
||||
aabb.inflate(data.hitbox_mesh.halfsize);
|
||||
//relative to moving platforms
|
||||
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
|
||||
//let relative_body=state.body.relative_to(&Body::ZERO);
|
||||
let relative_body=&state.body;
|
||||
data.bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
|
||||
//no checks are needed because of the time limits.
|
||||
let model_mesh=data.models.mesh(convex_mesh_id);
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
|
||||
collector.collect(minkowski.predict_collision_in(relative_body,state.time,collector.time())
|
||||
collector.collect(minkowski.predict_collision_in(relative_body,state.time..collector.time())
|
||||
//temp (?) code to avoid collision loops
|
||||
.map_or(None,|(face,dt)|{
|
||||
.and_then(|(face,dt)|{
|
||||
// this must be rounded to avoid the infinite loop when hitting the start zone
|
||||
let time=relative_body.time+dt.into();
|
||||
(state.time<time).then_some((time,face,dt))
|
||||
@ -1242,7 +1197,7 @@ fn recalculate_touching(
|
||||
aabb.grow(body.position);
|
||||
aabb.inflate(hitbox_mesh.halfsize);
|
||||
//relative to moving platforms
|
||||
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
|
||||
//let relative_body=state.body.relative_to(&Body::ZERO);
|
||||
bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
|
||||
//no checks are needed because of the time limits.
|
||||
let model_mesh=models.mesh(convex_mesh_id);
|
||||
@ -1294,7 +1249,7 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
|
||||
let mut culled=false;
|
||||
touching.contacts.retain(|contact|{
|
||||
let n=contact_normal(models,hitbox_mesh,contact);
|
||||
let r=(n.dot(v)>>52).is_positive();
|
||||
let r=n.dot(v).is_positive();
|
||||
if r{
|
||||
culled=true;
|
||||
}
|
||||
@ -1912,8 +1867,6 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use crate::file;
|
||||
use crate::body::VirtualBody;
|
||||
use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
|
||||
use super::*;
|
||||
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
|
||||
@ -1922,7 +1875,7 @@ mod test{
|
||||
let hitbox_mesh=h1.transformed_mesh();
|
||||
let platform_mesh=h0.transformed_mesh();
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO,Time::from_secs(10));
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
|
||||
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
|
||||
}
|
||||
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
|
||||
@ -1940,7 +1893,7 @@ mod test{
|
||||
let hitbox_mesh=h1.transformed_mesh();
|
||||
let platform_mesh=h0.transformed_mesh();
|
||||
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO,Time::from_secs(10));
|
||||
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
|
||||
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
|
||||
}
|
||||
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){
|
||||
@ -1994,111 +1947,111 @@ mod test{
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_west(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(3,3,0),
|
||||
int3(100,-1,0),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_north(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,3,3),
|
||||
int3(0,-1,100),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_east(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-3,3,0),
|
||||
int3(-100,-1,0),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_south(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,3,-3),
|
||||
int3(0,-1,-100),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,-7)>>1,
|
||||
int3(-10,-1,1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_north_from_nw(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,-7)>>1,
|
||||
int3(10,-1,1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(7,6,0)>>1,
|
||||
int3(-1,-1,-10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_east_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(7,6,0)>>1,
|
||||
int3(-1,-1,10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,7)>>1,
|
||||
int3(-10,-1,-1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_south_from_sw(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(0,6,7)>>1,
|
||||
int3(10,-1,-1),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_se(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-7,6,0)>>1,
|
||||
int3(1,-1,-10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_parabola_edge_west_from_ne(){
|
||||
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
|
||||
test_collision(Body::new(
|
||||
int3(-7,6,0)>>1,
|
||||
int3(1,-1,10),
|
||||
int3(0,-1,0),
|
||||
Time::ZERO
|
||||
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
|
||||
}
|
||||
#[test]
|
||||
fn test_collision_oblique(){
|
||||
@ -2127,202 +2080,4 @@ mod test{
|
||||
Time::ZERO
|
||||
),None);
|
||||
}
|
||||
#[test]
|
||||
fn run_replay(){
|
||||
println!("loading map file..");
|
||||
let map=file::load("../tools/bhop_maps/5692113331.snfm");
|
||||
println!("loading bot file..");
|
||||
let bot=file::load("../tools/replays/534s+997497968ns.snfb");
|
||||
if let (Ok(file::LoadFormat::Map(map)),Ok(file::LoadFormat::Bot(bot)))=(map,bot){
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
println!("simulating...");
|
||||
let mut physics=PhysicsState::default();
|
||||
for ins in bot.instructions{
|
||||
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
|
||||
}
|
||||
match physics.get_finish_time(){
|
||||
Some(time)=>println!("finish time:{}",time),
|
||||
None=>println!("simulation did not end in finished state"),
|
||||
}
|
||||
}else{
|
||||
panic!("missing files");
|
||||
}
|
||||
}
|
||||
enum DeterminismResult{
|
||||
Deterministic,
|
||||
NonDeterministic,
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum ReplayError{
|
||||
Load(file::LoadError),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
impl From<file::LoadError> for ReplayError{
|
||||
fn from(value:file::LoadError)->Self{
|
||||
Self::Load(value)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for ReplayError{
|
||||
fn from(value:std::io::Error)->Self{
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
|
||||
// create default physics state
|
||||
let mut physics_deterministic=PhysicsState::default();
|
||||
// create a second physics state
|
||||
let mut physics_filtered=PhysicsState::default();
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
println!("simulating...");
|
||||
|
||||
let mut non_idle_count=0;
|
||||
|
||||
for (i,ins) in bot.instructions.into_iter().enumerate(){
|
||||
let state_deterministic=physics_deterministic.clone();
|
||||
let state_filtered=physics_filtered.clone();
|
||||
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
|
||||
match ins{
|
||||
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
|
||||
other=>{
|
||||
non_idle_count+=1;
|
||||
// run
|
||||
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
|
||||
// check if position matches
|
||||
let b0=physics_deterministic.camera_body();
|
||||
let b1=physics_filtered.camera_body();
|
||||
if b0.position!=b1.position{
|
||||
println!("desync at instruction #{}",i);
|
||||
println!("non idle instructions completed={non_idle_count}");
|
||||
println!("instruction #{i}={:?}",other);
|
||||
println!("deterministic state0:\n{state_deterministic:?}");
|
||||
println!("filtered state0:\n{state_filtered:?}");
|
||||
println!("deterministic state1:\n{:?}",physics_deterministic);
|
||||
println!("filtered state1:\n{:?}",physics_filtered);
|
||||
return DeterminismResult::NonDeterministic;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
match physics_deterministic.get_finish_time(){
|
||||
Some(time)=>println!("[with idle] finish time:{}",time),
|
||||
None=>println!("[with idle] simulation did not end in finished state"),
|
||||
}
|
||||
match physics_filtered.get_finish_time(){
|
||||
Some(time)=>println!("[filtered] finish time:{}",time),
|
||||
None=>println!("[filtered] simulation did not end in finished state"),
|
||||
}
|
||||
DeterminismResult::Deterministic
|
||||
}
|
||||
type ThreadResult=Result<Option<DeterminismResult>,file::LoadError>;
|
||||
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
|
||||
s.spawn(move ||{
|
||||
let result=match file::load(file_path.as_path()){
|
||||
Ok(file::LoadFormat::Bot(bot))=>{
|
||||
println!("Running {:?}",file_path.file_stem());
|
||||
Ok(Some(segment_determinism(bot,physics_data)))
|
||||
},
|
||||
Ok(_)=>{
|
||||
println!("Provided bot file is not a bot file!");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e)=>{
|
||||
println!("Load error");
|
||||
Err(e)
|
||||
},
|
||||
};
|
||||
// send when thread is complete
|
||||
send.send(result).unwrap();
|
||||
});
|
||||
}
|
||||
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
|
||||
Ok(dir_entry.file_type()?.is_file().then_some(
|
||||
dir_entry.path()
|
||||
))
|
||||
}
|
||||
#[test]
|
||||
fn test_determinism()->Result<(),ReplayError>{
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
println!("loading map file..");
|
||||
let file::LoadFormat::Map(map)=file::load("../tools/bhop_maps/5692113331.snfm")? else{
|
||||
panic!("Provided map file is not a map file!");
|
||||
};
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let (send,recv)=std::sync::mpsc::channel();
|
||||
|
||||
let mut read_dir=std::fs::read_dir("../tools/replays")?;
|
||||
|
||||
// promise that &physics_data will outlive the spawned threads
|
||||
let thread_results=std::thread::scope(|s|{
|
||||
let mut thread_results=Vec::new();
|
||||
|
||||
// spawn threads
|
||||
println!("spawning up to {thread_limit} threads...");
|
||||
let mut active_thread_count=0;
|
||||
while active_thread_count<thread_limit{
|
||||
if let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
active_thread_count+=1;
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// spawn another thread every time a message is received from the channel
|
||||
println!("riding parallelism wave...");
|
||||
while let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
// wait for a thread to complete
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}
|
||||
|
||||
// wait for remaining threads to complete
|
||||
println!("waiting for all threads to complete...");
|
||||
for _ in 0..active_thread_count{
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
}
|
||||
|
||||
println!("done.");
|
||||
Ok::<_,ReplayError>(thread_results)
|
||||
})?;
|
||||
|
||||
// tally results
|
||||
#[derive(Default)]
|
||||
struct Totals{
|
||||
deterministic:u32,
|
||||
nondeterministic:u32,
|
||||
invalid:u32,
|
||||
error:u32,
|
||||
}
|
||||
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
|
||||
match result{
|
||||
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
|
||||
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
|
||||
Ok(None)=>totals.invalid+=1,
|
||||
Err(_)=>totals.error+=1,
|
||||
}
|
||||
totals
|
||||
});
|
||||
|
||||
println!("deterministic={deterministic}");
|
||||
println!("nondeterministic={nondeterministic}");
|
||||
println!("invalid={invalid}");
|
||||
println!("error={error}");
|
||||
|
||||
assert!(nondeterministic==0);
|
||||
assert!(invalid==0);
|
||||
assert!(error==0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
12
engine/session/Cargo.toml
Normal file
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
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
|
||||
*******************************************************/
|
2
engine/session/src/lib.rs
Normal file
2
engine/session/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod mouse_interpolator;
|
||||
pub mod session;
|
@ -12,10 +12,11 @@ use strafesnet_common::physics::{
|
||||
};
|
||||
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 crate::physics::{PhysicsContext,PhysicsData};
|
||||
use crate::settings::UserSettings;
|
||||
use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
|
||||
use strafesnet_settings::settings::UserSettings;
|
||||
|
||||
pub enum Instruction<'a>{
|
||||
Input(SessionInputInstruction),
|
||||
@ -57,19 +58,19 @@ pub enum SessionPlaybackInstruction{
|
||||
}
|
||||
|
||||
pub struct FrameState{
|
||||
pub body:crate::physics::Body,
|
||||
pub camera:crate::physics::PhysicsCamera,
|
||||
pub body:physics::Body,
|
||||
pub camera:physics::PhysicsCamera,
|
||||
pub time:PhysicsTime,
|
||||
}
|
||||
|
||||
pub struct Simulation{
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:crate::physics::PhysicsState,
|
||||
physics:physics::PhysicsState,
|
||||
}
|
||||
impl Simulation{
|
||||
pub const fn new(
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:crate::physics::PhysicsState,
|
||||
physics:physics::PhysicsState,
|
||||
)->Self{
|
||||
Self{
|
||||
timer,
|
||||
@ -149,11 +150,12 @@ enum ViewState{
|
||||
}
|
||||
|
||||
pub struct Session{
|
||||
directories:Directories,
|
||||
user_settings:UserSettings,
|
||||
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
|
||||
view_state:ViewState,
|
||||
//gui:GuiState
|
||||
geometry_shared:crate::physics::PhysicsData,
|
||||
geometry_shared:physics::PhysicsData,
|
||||
simulation:Simulation,
|
||||
// below fields not included in lite session
|
||||
recording:Recording,
|
||||
@ -163,10 +165,12 @@ pub struct Session{
|
||||
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,
|
||||
@ -295,11 +299,17 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
let file_name=format!("replays/{}.snfb",ins.time);
|
||||
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").unwrap();
|
||||
let file=std::fs::File::create(file_name).unwrap();
|
||||
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),crate::physics::VERSION.get(),replay.recording.instructions).unwrap();
|
||||
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!");
|
||||
});
|
||||
},
|
10
engine/settings/Cargo.toml
Normal file
10
engine/settings/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "strafesnet_settings"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
configparser = "3.0.2"
|
||||
directories = "6.0.0"
|
||||
glam = "0.29.0"
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
8
engine/settings/LICENSE
Normal file
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
|
||||
*******************************************************/
|
32
engine/settings/src/directories.rs
Normal file
32
engine/settings/src/directories.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::settings::{UserSettings,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"),
|
||||
})
|
||||
}
|
||||
}
|
2
engine/settings/src/lib.rs
Normal file
2
engine/settings/src/lib.rs
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
9
integration-testing/Cargo.toml
Normal file
9
integration-testing/Cargo.toml
Normal file
@ -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" }
|
221
integration-testing/src/main.rs
Normal file
221
integration-testing/src/main.rs
Normal file
@ -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;
|
||||
|
112
lib/bsp_loader/src/loader.rs
Normal file
112
lib/bsp_loader/src/loader.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
89
lib/bsp_loader/src/mesh.rs
Normal file
89
lib/bsp_loader/src/mesh.rs
Normal file
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
@ -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)]
|
||||
|
@ -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 }
|
||||
|
109
lib/deferred_loader/src/deferred_loader.rs
Normal file
109
lib/deferred_loader/src/deferred_loader.rs
Normal file
@ -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;
|
||||
|
8
lib/deferred_loader/src/loader.rs
Normal file
8
lib/deferred_loader/src/loader.rs
Normal file
@ -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>;
|
||||
}
|
17
lib/deferred_loader/src/mesh.rs
Normal file
17
lib/deferred_loader/src/mesh.rs
Normal file
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
102
lib/rbx_loader/src/loader.rs
Normal file
102
lib/rbx_loader/src/loader.rs
Normal file
@ -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()
|
||||
);
|
||||
|
7
lib/rbxassetid/Cargo.toml
Normal file
7
lib/rbxassetid/Cargo.toml
Normal file
@ -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
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)?))
|
||||
}
|
||||
}
|
@ -95,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>,
|
||||
@ -136,11 +121,6 @@ struct ResourceBlockHeader{
|
||||
resource:ResourceType,
|
||||
id:BlockId,
|
||||
}
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
struct ResourceExternalHeader{
|
||||
resource_uuid:ResourceId,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
|
@ -9,25 +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"
|
||||
replace_with = "0.1.7"
|
||||
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 = "24.0.0"
|
||||
winit = "0.30.7"
|
||||
|
68
strafe-client/src/app.rs
Normal file
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")]
|
||||
@ -59,11 +62,19 @@ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
|
||||
}
|
||||
}
|
||||
|
||||
#[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{
|
||||
@ -92,73 +103,47 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
|
||||
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(LoadFormat::Map(map))
|
||||
},
|
||||
#[cfg(feature="source")]
|
||||
ReadFormat::Source(bsp)=>{
|
||||
let mut loader=strafesnet_deferred_loader::source_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_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(LoadFormat::Map(map))
|
||||
},
|
||||
|
@ -1,7 +1,11 @@
|
||||
use strafesnet_graphics::graphics;
|
||||
use strafesnet_session::session;
|
||||
use strafesnet_settings::settings;
|
||||
|
||||
pub enum Instruction{
|
||||
Render(crate::session::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),
|
||||
}
|
||||
|
||||
@ -15,7 +19,7 @@ WorkerDescription{
|
||||
//up to three frames in flight, dropping new frame requests when all three are busy, and dropping output frames when one renders out of order
|
||||
|
||||
pub fn new(
|
||||
mut graphics:crate::graphics::GraphicsState,
|
||||
mut graphics:graphics::GraphicsState,
|
||||
mut config:wgpu::SurfaceConfiguration,
|
||||
surface:wgpu::Surface,
|
||||
device:wgpu::Device,
|
||||
|
@ -1,20 +1,11 @@
|
||||
mod body;
|
||||
mod app;
|
||||
mod file;
|
||||
mod setup;
|
||||
mod window;
|
||||
mod worker;
|
||||
mod physics;
|
||||
mod session;
|
||||
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;
|
||||
mod mouse_interpolator;
|
||||
|
||||
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::graphics_worker::Instruction as GraphicsInstruction;
|
||||
use crate::session::{
|
||||
Session,Simulation,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction,
|
||||
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};
|
||||
@ -20,13 +21,15 @@ pub enum Instruction{
|
||||
|
||||
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::PhysicsState::default();
|
||||
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>|{
|
||||
@ -67,7 +70,7 @@ pub fn new<'a>(
|
||||
},
|
||||
Instruction::ChangeMap(complete_map)=>{
|
||||
run_session_instruction!(ins.time,SessionInstruction::ChangeMap(&complete_map));
|
||||
run_session_instruction!(ins.time,SessionInstruction::Input(SessionInputInstruction::Mode(crate::session::ImplicitModeInstruction::ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId::MAIN,strafesnet_common::gameplay_modes::StageId::FIRST))));
|
||||
run_session_instruction!(ins.time,SessionInstruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(strafesnet_common::gameplay_modes::ModeId::MAIN,strafesnet_common::gameplay_modes::StageId::FIRST))));
|
||||
run_graphics_worker_instruction!(GraphicsInstruction::ChangeMap(complete_map));
|
||||
},
|
||||
Instruction::LoadReplay(bot)=>{
|
||||
|
@ -1,8 +1,3 @@
|
||||
use crate::window::Instruction;
|
||||
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,11 +20,6 @@ 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{
|
||||
@ -113,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,
|
||||
}
|
||||
@ -130,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
|
||||
@ -146,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,
|
||||
@ -155,7 +139,6 @@ impl<'a> SetupContextPartial3<'a>{
|
||||
}
|
||||
}
|
||||
struct SetupContextPartial4<'a>{
|
||||
instance:wgpu::Instance,
|
||||
surface:wgpu::Surface<'a>,
|
||||
adapter:wgpu::Adapter,
|
||||
device:wgpu::Device,
|
||||
@ -172,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,
|
||||
@ -181,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,
|
||||
@ -216,73 +197,16 @@ pub fn setup_and_start(title:&str){
|
||||
);
|
||||
|
||||
for arg in std::env::args().skip(1){
|
||||
let path=std::path::PathBuf::from(arg);
|
||||
window_thread.send(TimedInstruction{
|
||||
time:integer::Time::ZERO,
|
||||
instruction:Instruction::WindowEvent(winit::event::WindowEvent::DroppedFile(path)),
|
||||
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<Instruction,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:Instruction::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:Instruction::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:Instruction::Render}).unwrap();
|
||||
}
|
||||
_=>{
|
||||
window_thread.send(TimedInstruction{time,instruction:Instruction::WindowEvent(event)}).unwrap();
|
||||
}
|
||||
},
|
||||
winit::event::Event::DeviceEvent{
|
||||
event,
|
||||
..
|
||||
} => {
|
||||
window_thread.send(TimedInstruction{time,instruction:Instruction::DeviceEvent(event)}).unwrap();
|
||||
},
|
||||
_=>{}
|
||||
}
|
||||
})
|
||||
let mut app=crate::app::App::new(
|
||||
std::time::Instant::now(),
|
||||
window_thread
|
||||
);
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
}
|
||||
|
@ -3,14 +3,12 @@ use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInn
|
||||
use strafesnet_common::physics::{MiscInstruction,SetControlInstruction};
|
||||
use crate::file::LoadFormat;
|
||||
use crate::physics_worker::Instruction as PhysicsWorkerInstruction;
|
||||
use crate::session::{SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction};
|
||||
use strafesnet_session::session::{self,SessionInputInstruction,SessionControlInstruction,SessionPlaybackInstruction};
|
||||
use strafesnet_settings::directories::Directories;
|
||||
|
||||
pub enum Instruction{
|
||||
Resize(winit::dpi::PhysicalSize<u32>),
|
||||
WindowEvent(winit::event::WindowEvent),
|
||||
DeviceEvent(winit::event::DeviceEvent),
|
||||
RequestRedraw,
|
||||
Render,
|
||||
}
|
||||
|
||||
//holds thread handles to dispatch to
|
||||
@ -150,7 +148,7 @@ impl WindowContext<'_>{
|
||||
"R"|"r"=>s.then(||{
|
||||
//mouse needs to be reset since the position is absolute
|
||||
self.mouse_pos=glam::DVec2::ZERO;
|
||||
SessionInstructionSubset::Input(SessionInputInstruction::Mode(crate::session::ImplicitModeInstruction::ResetAndRestart))
|
||||
SessionInstructionSubset::Input(SessionInputInstruction::Mode(session::ImplicitModeInstruction::ResetAndRestart))
|
||||
}),
|
||||
"F"|"f"=>input_misc!(PracticeFly,s),
|
||||
"B"|"b"=>session_ctrl!(CopyRecordingIntoReplayAndSpectate,s),
|
||||
@ -169,6 +167,23 @@ impl WindowContext<'_>{
|
||||
},
|
||||
}
|
||||
},
|
||||
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();
|
||||
},
|
||||
_=>(),
|
||||
}
|
||||
}
|
||||
@ -210,9 +225,14 @@ pub fn worker<'a>(
|
||||
setup_context:crate::setup::SetupContext<'a>,
|
||||
)->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
|
||||
@ -226,6 +246,7 @@ pub fn worker<'a>(
|
||||
window,
|
||||
physics_thread:crate::physics_worker::new(
|
||||
graphics_thread,
|
||||
directories,
|
||||
user_settings,
|
||||
),
|
||||
};
|
||||
@ -233,31 +254,12 @@ pub fn worker<'a>(
|
||||
//WindowContextSetup::into_worker
|
||||
crate::compat_worker::QNWorker::new(move |ins:TimedInstruction<Instruction,SessionTimeInner>|{
|
||||
match ins.instruction{
|
||||
Instruction::RequestRedraw=>{
|
||||
window_context.window.request_redraw();
|
||||
}
|
||||
Instruction::WindowEvent(window_event)=>{
|
||||
window_context.window_event(ins.time,window_event);
|
||||
},
|
||||
Instruction::DeviceEvent(device_event)=>{
|
||||
window_context.device_event(ins.time,device_event);
|
||||
},
|
||||
Instruction::Resize(size)=>{
|
||||
window_context.physics_thread.send(
|
||||
TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:PhysicsWorkerInstruction::Resize(size)
|
||||
}
|
||||
).unwrap();
|
||||
}
|
||||
Instruction::Render=>{
|
||||
window_context.physics_thread.send(
|
||||
TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:PhysicsWorkerInstruction::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() {
|
||||
|
Reference in New Issue
Block a user