forked from StrafesNET/strafe-project
Compare commits
282 Commits
roblox-bot
...
master
Author | SHA1 | Date | |
---|---|---|---|
4805d237e2 | |||
8e91fbb08e | |||
a2a69c48d6 | |||
e74535d91f | |||
dbca48f051 | |||
ad6584d82a | |||
ceb5fff333 | |||
c6f31a3eb5 | |||
6c81fc43bb | |||
5317cd0609 | |||
09d151ee1c | |||
441b9a8559 | |||
707ff98d27 | |||
2c1910bc24 | |||
614535ce8c | |||
78c29657d6 | |||
9d9695f363 | |||
ce3d8fde11 | |||
1ddc013fd0 | |||
d0d2e6eca3 | |||
1ba7b0591b | |||
205db9a0db | |||
ca50bf35c2 | |||
6522c255cd | |||
6b4122c59c | |||
eb0365217d | |||
c128046d88 | |||
ff57825232 | |||
8e867449f0 | |||
f028dc6c85 | |||
85480cb13e | |||
a594f77453 | |||
c356a9d654 | |||
23ee771c22 | |||
a5079f21d7 | |||
349cd9c233 | |||
d455cf4dc9 | |||
3227a6486a | |||
1ce51dd4da | |||
1ad9723905 | |||
be9eb30a5c | |||
42310c5d2b | |||
988750a3c6 | |||
2f26662dda | |||
6f739dba8d | |||
2eaddd493d | |||
d5a6f8e1bc | |||
95fb43b908 | |||
3e814cb41a | |||
ea854a548f | |||
5f2cf8f32e | |||
b6b090de78 | |||
affbada62e | |||
8d2ba28700 | |||
3eb4e76ab2 | |||
5d31419370 | |||
44a58044c7 | |||
83ac776b78 | |||
871aadb78a | |||
65b49d2726 | |||
ed70b7c484 | |||
c7575f2e21 | |||
c2f78eab48 | |||
195014400f | |||
ee9585a4c2 | |||
77012d6caa | |||
f9509353dd | |||
5bce4a84cf | |||
4bbccd68ed | |||
8be3be4002 | |||
76bafa4d0a | |||
a612d1f864 | |||
ad9ef141bf | |||
4c11980989 | |||
091da88b5c | |||
045e540020 | |||
c14f8975fc | |||
8e228033e0 | |||
eaecbc5b73 | |||
91a1b3d65e | |||
e1d51ff2da | |||
c5f2395b3a | |||
77aa83d6ac | |||
184b12a9cc | |||
9ba3484e25 | |||
adcd7db4f1 | |||
710670d78e | |||
70e1514ef9 | |||
45e9b3d0ce | |||
1eac734c35 | |||
25e1312fe4 | |||
cc8f6b059d | |||
4cd287dbb8 | |||
40ea0e6c7d | |||
b45b92c627 | |||
e90f53a111 | |||
6beb6c5f9a | |||
a1507d4f26 | |||
77db5a7a6b | |||
75c8bc2bbb | |||
d49a6b2f0a | |||
19778ac7aa | |||
5b62052222 | |||
5a8bc141d3 | |||
83a067320b | |||
2faa61225f | |||
28499800cb | |||
57cc49dc1a | |||
d517b78a8c | |||
e6d1d69241 | |||
b28fa25279 | |||
713b235816 | |||
d2002383cb | |||
52f7de809d | |||
4efe1209b8 | |||
15a9136fc4 | |||
035736e7af | |||
7f9a16a56d | |||
814e573d91 | |||
6fa0f1c83e | |||
870cb56dac | |||
1aac2a9303 | |||
38661b3a68 | |||
7ad76740d4 | |||
c2d6af8bda | |||
fbacef83b9 | |||
2a9e848541 | |||
3413ec8740 | |||
168d6708d1 | |||
13cae4c7c5 | |||
0dc462a3b1 | |||
ca003edbc3 | |||
16abe23e97 | |||
67f8569178 | |||
121c9c5258 | |||
411b997b87 | |||
4d587b6c0b | |||
6ff74f08ab | |||
08f419f931 | |||
6066e82fd2 | |||
ca8035cdfc | |||
ff5d954cfb | |||
a967f31004 | |||
8ad5d28e51 | |||
ab05893813 | |||
2f7597146e | |||
004e0d3776 | |||
120d8197b7 | |||
36ba73a892 | |||
86cf7e74b1 | |||
24787fede5 | |||
3797408bc8 | |||
47c9b77b00 | |||
479e657251 | |||
63fbc94287 | |||
1318ae20ca | |||
851d9c935d | |||
d0a190861c | |||
4dca7fc369 | |||
62dfe23539 | |||
3991cb5064 | |||
1dc2556d85 | |||
4f21985290 | |||
ccce54c1a3 | |||
02bb2d797c | |||
41b28fa7d2 | |||
a2ab23097b | |||
602061b44c | |||
1989369956 | |||
a18aea828c | |||
b7000ee9af | |||
2b77ea5712 | |||
cf98f8e7bb | |||
a56c114d08 | |||
b6a5324ae7 | |||
6f5a3c5176 | |||
6bab31f3b3 | |||
9cdeed160f | |||
d0c59b51a4 | |||
451f3ccecb | |||
ed9701981d | |||
60e0197344 | |||
4d97a490c1 | |||
52ba44c6be | |||
95b6272b18 | |||
0172675b04 | |||
982b4aecac | |||
c1ddcdb0c5 | |||
c2d0a4487c | |||
dc9fd2c442 | |||
4199d41d3f | |||
7fbcb206ff | |||
a17901d473 | |||
b88c6b899a | |||
835d4bbecd | |||
b756dc979c | |||
1e888ebb01 | |||
b9dccb1af5 | |||
c6d293cc6b | |||
a386f90f51 | |||
43115cbac6 | |||
35b5aff9a7 | |||
36419af870 | |||
a7518bef46 | |||
6df1f41599 | |||
422d0a160d | |||
1727f9213c | |||
afa9e7447d | |||
ff85efa54f | |||
fa69c53cfc | |||
a57c228580 | |||
5dc69db885 | |||
e54400a436 | |||
e2a5edf8df | |||
d6dd1b8abd | |||
a2b793fcd3 | |||
9cb34f14c8 | |||
bd2e3aa2d3 | |||
07f6053839 | |||
0d5b918ea1 | |||
20a568220a | |||
d670d4129e | |||
de7b0bd5cc | |||
01524146c7 | |||
45e8e415d0 | |||
4417bafc5c | |||
8553625738 | |||
3a3749eaeb | |||
53539f290b | |||
479dd37f53 | |||
34b6a869f0 | |||
19a455ee5e | |||
9904b7a044 | |||
6efa811eb6 | |||
81e4a201bd | |||
8fd5618af2 | |||
54c26d6e1e | |||
110ec94a08 | |||
980da5a6a7 | |||
1cd77984d4 | |||
b0fe231388 | |||
5a4a39ab75 | |||
|
1b2324deeb | ||
4c485e76e4 | |||
7bbb9ca24f | |||
eff55af1b4 | |||
0d05cc9996 | |||
2a55ef90df | |||
1a6202ae66 | |||
|
742f7b4ec0 | ||
2cb346f49a | |||
e5cca9ed04 | |||
52d911a25a | |||
7ab20f36a7 | |||
a7554da1c5 | |||
37f0dad7a1 | |||
e309f15cb8 | |||
29374e4ff5 | |||
b7d04d1f40 | |||
432ec11ea6 | |||
01449b1850 | |||
327d0a4992 | |||
420dbaa022 | |||
cad29af4bb | |||
e0e8744bfd | |||
b434dce0f6 | |||
6ef8fd2f69 | |||
7234065bd8 | |||
41d8e700c5 | |||
4ca3d56f0f | |||
593b6902fd | |||
7523c4313a | |||
694440bd29 | |||
755e1d4d5b | |||
4334a6f330 | |||
553ad2cca5 | |||
3f15d2f5a8 | |||
206bb01c1a | |||
df24bc9945 | |||
4c1632c0b6 | |||
9e482891ba | |||
38d1ff62ab |
2493
Cargo.lock
generated
2493
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,10 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"engine/graphics",
|
||||
"engine/physics",
|
||||
"engine/session",
|
||||
"engine/settings",
|
||||
"integration-testing",
|
||||
"lib/bsp_loader",
|
||||
"lib/common",
|
||||
"lib/deferred_loader",
|
||||
@ -7,8 +12,10 @@ members = [
|
||||
"lib/linear_ops",
|
||||
"lib/ratio_ops",
|
||||
"lib/rbx_loader",
|
||||
"lib/rbxassetid",
|
||||
"lib/roblox_emulator",
|
||||
"lib/snf",
|
||||
"map-tool",
|
||||
"strafe-client",
|
||||
]
|
||||
resolver = "2"
|
||||
|
@ -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,10 +1,15 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashSet,HashMap};
|
||||
use strafesnet_common::map;
|
||||
use strafesnet_common::integer;
|
||||
use strafesnet_settings::settings;
|
||||
use strafesnet_session::session;
|
||||
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
|
||||
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
|
||||
use crate::model_graphics::{self,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
|
||||
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
|
||||
|
||||
pub fn required_limits()->wgpu::Limits{
|
||||
wgpu::Limits::default()
|
||||
}
|
||||
|
||||
struct Indices{
|
||||
count:u32,
|
||||
@ -98,12 +103,6 @@ impl std::default::Default for GraphicsCamera{
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameState{
|
||||
pub body:crate::physics::Body,
|
||||
pub camera:crate::physics::PhysicsCamera,
|
||||
pub time:integer::Time,
|
||||
}
|
||||
|
||||
pub struct GraphicsState{
|
||||
pipelines:GraphicsPipelines,
|
||||
bind_groups:GraphicsBindGroups,
|
||||
@ -143,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){
|
||||
@ -455,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(){
|
||||
@ -615,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
|
||||
@ -643,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!(),
|
||||
};
|
||||
|
||||
@ -689,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();
|
||||
|
||||
@ -871,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);
|
||||
@ -882,7 +881,7 @@ impl GraphicsState{
|
||||
view:&wgpu::TextureView,
|
||||
device:&wgpu::Device,
|
||||
queue:&wgpu::Queue,
|
||||
frame_state: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
|
||||
*******************************************************/
|
160
engine/physics/src/body.rs
Normal file
160
engine/physics/src/body.rs
Normal file
@ -0,0 +1,160 @@
|
||||
use strafesnet_common::aabb;
|
||||
use strafesnet_common::integer::{self,vec3,Time,Planar64,Planar64Vec3};
|
||||
#[derive(Clone,Copy,Debug,Hash)]
|
||||
pub struct Body<T>{
|
||||
pub position:Planar64Vec3,//I64 where 2^32 = 1 u
|
||||
pub velocity:Planar64Vec3,//I64 where 2^32 = 1 u/s
|
||||
pub acceleration:Planar64Vec3,//I64 where 2^32 = 1 u/s/s
|
||||
pub time:Time<T>,//nanoseconds x xxxxD!
|
||||
}
|
||||
impl<T> std::ops::Neg for Body<T>{
|
||||
type Output=Self;
|
||||
fn neg(self)->Self::Output{
|
||||
Self{
|
||||
position:self.position,
|
||||
velocity:-self.velocity,
|
||||
acceleration:self.acceleration,
|
||||
time:-self.time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Body<T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
|
||||
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
|
||||
Self{
|
||||
position,
|
||||
velocity,
|
||||
acceleration,
|
||||
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
|
||||
+(self.velocity*dt).map(|elem|elem.divide().fix_1())
|
||||
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().fix_1())
|
||||
}
|
||||
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
|
||||
let dt=time-self.time;
|
||||
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().fix_1())
|
||||
}
|
||||
pub fn advance_time(&mut self,time:Time<T>){
|
||||
self.position=self.extrapolated_position(time);
|
||||
self.velocity=self.extrapolated_velocity(time);
|
||||
self.time=time;
|
||||
}
|
||||
pub fn extrapolated_position_ratio_dt<Num,Den,N1,D1,N2,N3,D2,N4,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
|
||||
where
|
||||
// Why?
|
||||
// All of this can be removed with const generics because the type can be specified as
|
||||
// Ratio<Fixed<N,NF>,Fixed<D,DF>>
|
||||
// which is known to implement all the necessary traits
|
||||
Num:Copy,
|
||||
Den:Copy+core::ops::Mul<i64,Output=D1>,
|
||||
D1:Copy,
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
Planar64:core::ops::Mul<D1,Output=N2>,
|
||||
N1:core::ops::Add<N2,Output=N3>,
|
||||
Num:core::ops::Mul<N3,Output=N4>,
|
||||
Den:core::ops::Mul<D1,Output=D2>,
|
||||
D2:Copy,
|
||||
Planar64:core::ops::Mul<D2,Output=N4>,
|
||||
N4:integer::Divide<D2,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
{
|
||||
// a*dt^2/2 + v*dt + p
|
||||
// (a*dt/2+v)*dt+p
|
||||
(self.acceleration.map(|elem|dt*elem/2)+self.velocity).map(|elem|dt.mul_ratio(elem))
|
||||
.map(|elem|elem.divide().fix())+self.position
|
||||
}
|
||||
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
|
||||
where
|
||||
Num:Copy,
|
||||
Den:Copy,
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
Planar64:core::ops::Mul<Den,Output=N1>,
|
||||
N1:integer::Divide<Den,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
{
|
||||
// a*dt + v
|
||||
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
|
||||
}
|
||||
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();
|
||||
}
|
||||
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
|
||||
if self.velocity==vec3::ZERO{
|
||||
if self.acceleration==vec3::ZERO{
|
||||
None
|
||||
}else{
|
||||
Some(self.acceleration)
|
||||
}
|
||||
}else{
|
||||
Some(self.velocity)
|
||||
}
|
||||
}
|
||||
pub fn grow_aabb(&self,aabb:&mut aabb::Aabb,t0:Time<T>,t1:Time<T>){
|
||||
aabb.grow(self.extrapolated_position(t0));
|
||||
aabb.grow(self.extrapolated_position(t1));
|
||||
//v+a*t==0
|
||||
//goober code
|
||||
if !self.acceleration.x.is_zero(){
|
||||
let t=-self.velocity.x/self.acceleration.x;
|
||||
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
|
||||
aabb.grow(self.extrapolated_position_ratio_dt(t));
|
||||
}
|
||||
}
|
||||
if !self.acceleration.y.is_zero(){
|
||||
let t=-self.velocity.y/self.acceleration.y;
|
||||
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
|
||||
aabb.grow(self.extrapolated_position_ratio_dt(t));
|
||||
}
|
||||
}
|
||||
if !self.acceleration.z.is_zero(){
|
||||
let t=-self.velocity.z/self.acceleration.z;
|
||||
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
|
||||
aabb.grow(self.extrapolated_position_ratio_dt(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
impl<T> std::fmt::Display for Body<T>{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"p({}) v({}) a({}) t({})",self.position,self.velocity,self.acceleration,self.time)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualBody<'a,T>{
|
||||
body0:&'a Body<T>,
|
||||
body1:&'a Body<T>,
|
||||
}
|
||||
impl<T> VirtualBody<'_,T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
|
||||
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
|
||||
}
|
||||
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
|
||||
self.body1.extrapolated_velocity(time)-self.body0.extrapolated_velocity(time)
|
||||
}
|
||||
pub fn acceleration(&self)->Planar64Vec3{
|
||||
self.body1.acceleration-self.body0.acceleration
|
||||
}
|
||||
pub fn body(&self,time:Time<T>)->Body<T>{
|
||||
Body::new(self.extrapolated_position(time),self.extrapolated_velocity(time),self.acceleration(),time)
|
||||
}
|
||||
}
|
@ -1,23 +1,48 @@
|
||||
use crate::physics::Body;
|
||||
use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge,MinkowskiMesh,MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert};
|
||||
use strafesnet_common::integer::{Time,Fixed,Ratio};
|
||||
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
|
||||
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
|
||||
use crate::physics::{Time,Body};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Transition<F,E:DirectedEdge,V>{
|
||||
enum Transition<M:MeshQuery>{
|
||||
Miss,
|
||||
Next(FEV<F,E,V>,GigaTime),
|
||||
Hit(F,GigaTime),
|
||||
Next(FEV<M>,GigaTime),
|
||||
Hit(M::Face,GigaTime),
|
||||
}
|
||||
|
||||
type MinkowskiFEV=FEV<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
|
||||
type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
|
||||
pub enum CrawlResult<M:MeshQuery>{
|
||||
Miss(FEV<M>),
|
||||
Hit(M::Face,GigaTime),
|
||||
}
|
||||
impl<M:MeshQuery> CrawlResult<M>{
|
||||
pub fn hit(self)->Option<(M::Face,GigaTime)>{
|
||||
match self{
|
||||
CrawlResult::Miss(_)=>None,
|
||||
CrawlResult::Hit(face,time)=>Some((face,time)),
|
||||
}
|
||||
}
|
||||
pub fn miss(self)->Option<FEV<M>>{
|
||||
match self{
|
||||
CrawlResult::Miss(fev)=>Some(fev),
|
||||
CrawlResult::Hit(_,_)=>None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_transition(fev:&MinkowskiFEV,body_time:GigaTime,mesh:&MinkowskiMesh,body:&Body,mut best_time:GigaTime)->MinkowskiTransition{
|
||||
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
where
|
||||
// This is hardcoded for MinkowskiMesh lol
|
||||
M::Face:Copy,
|
||||
M::Edge:Copy,
|
||||
M::Vert:Copy,
|
||||
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
|
||||
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
|
||||
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
|
||||
{
|
||||
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
|
||||
//conflicting derivative means it crosses in the wrong direction.
|
||||
//if the transition time is equal to an already tested transition, do not replace the current best.
|
||||
let mut best_transition=MinkowskiTransition::Miss;
|
||||
match fev{
|
||||
&MinkowskiFEV::Face(face_id)=>{
|
||||
let mut best_transition=Transition::Miss;
|
||||
match self{
|
||||
&FEV::Face(face_id)=>{
|
||||
//test own face collision time, ignoring roots with zero or conflicting derivative
|
||||
//n=face.normal d=face.dot
|
||||
//n.a t^2+n.v t+n.p-d==0
|
||||
@ -27,7 +52,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
|
||||
for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_transition=MinkowskiTransition::Hit(face_id,dt);
|
||||
best_transition=Transition::Hit(face_id,dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -41,14 +66,14 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
|
||||
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(verts[0])+mesh.vert(verts[1]))).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//if none:
|
||||
},
|
||||
&MinkowskiFEV::Edge(edge_id)=>{
|
||||
&FEV::Edge(edge_id)=>{
|
||||
//test each face collision time, ignoring roots with zero or conflicting derivative
|
||||
let edge_n=mesh.edge_n(edge_id);
|
||||
let edge_verts=mesh.edge_verts(edge_id);
|
||||
@ -61,7 +86,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
|
||||
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Face(edge_face_id),dt);
|
||||
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -74,14 +99,14 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
|
||||
best_time=dt;
|
||||
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Vert(vert_id),dt);
|
||||
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//if none:
|
||||
},
|
||||
&MinkowskiFEV::Vert(vert_id)=>{
|
||||
&FEV::Vert(vert_id)=>{
|
||||
//test each edge collision time, ignoring roots with zero or conflicting derivative
|
||||
for &directed_edge_id in mesh.vert_edges(vert_id).iter(){
|
||||
//edge is directed away from vertex, but we want the dot product to turn out negative
|
||||
@ -90,7 +115,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
|
||||
best_time=dt;
|
||||
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -100,12 +125,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
|
||||
}
|
||||
best_transition
|
||||
}
|
||||
pub enum CrawlResult<F,E:DirectedEdge,V>{
|
||||
Miss(FEV<F,E,V>),
|
||||
Hit(F,GigaTime),
|
||||
}
|
||||
type MinkowskiCrawlResult=CrawlResult<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
|
||||
pub fn crawl_fev(mut fev:MinkowskiFEV,mesh:&MinkowskiMesh,relative_body:&Body,start_time:Time,time_limit:Time)->MinkowskiCrawlResult{
|
||||
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
|
||||
let mut body_time={
|
||||
let r=(start_time-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
@ -115,13 +135,14 @@ pub fn crawl_fev(mut fev:MinkowskiFEV,mesh:&MinkowskiMesh,relative_body:&Body,st
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
};
|
||||
for _ in 0..20{
|
||||
match next_transition(&fev,body_time,mesh,relative_body,time_limit){
|
||||
Transition::Miss=>return CrawlResult::Miss(fev),
|
||||
Transition::Next(next_fev,next_time)=>(fev,body_time)=(next_fev,next_time),
|
||||
match self.next_transition(body_time,mesh,relative_body,time_limit){
|
||||
Transition::Miss=>return CrawlResult::Miss(self),
|
||||
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
|
||||
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
|
||||
}
|
||||
}
|
||||
//TODO: fix all bugs
|
||||
//println!("Too many iterations! Using default behaviour instead of crashing...");
|
||||
CrawlResult::Miss(fev)
|
||||
CrawlResult::Miss(self)
|
||||
}
|
||||
}
|
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,8 +1,12 @@
|
||||
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};
|
||||
use strafesnet_common::physics::Time;
|
||||
|
||||
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
|
||||
|
||||
pub trait UndirectedEdge{
|
||||
type DirectedEdge:Copy+DirectedEdge;
|
||||
@ -51,10 +55,10 @@ impl DirectedEdge for SubmeshDirectedEdgeId{
|
||||
|
||||
//Vertex <-> Edge <-> Face -> Collide
|
||||
#[derive(Debug)]
|
||||
pub enum FEV<F,E:DirectedEdge,V>{
|
||||
Face(F),
|
||||
Edge(E::UndirectedEdge),
|
||||
Vert(V),
|
||||
pub enum FEV<M:MeshQuery>{
|
||||
Face(M::Face),
|
||||
Edge(<M::Edge as DirectedEdge>::UndirectedEdge),
|
||||
Vert(M::Vert),
|
||||
}
|
||||
|
||||
//use Unit32 #[repr(C)] for map files
|
||||
@ -64,25 +68,28 @@ struct Face{
|
||||
dot:Planar64,
|
||||
}
|
||||
struct Vert(Planar64Vec3);
|
||||
pub trait MeshQuery<FACE:Clone,EDGE:Clone+DirectedEdge,VERT:Clone>{
|
||||
pub trait MeshQuery{
|
||||
type Face:Clone;
|
||||
type Edge:Clone+DirectedEdge;
|
||||
type Vert:Clone;
|
||||
// Vertex must be Planar64Vec3 because it represents an actual position
|
||||
type Normal;
|
||||
type Offset;
|
||||
fn edge_n(&self,edge_id:EDGE::UndirectedEdge)->Planar64Vec3{
|
||||
fn edge_n(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Planar64Vec3{
|
||||
let verts=self.edge_verts(edge_id);
|
||||
self.vert(verts[1].clone())-self.vert(verts[0].clone())
|
||||
}
|
||||
fn directed_edge_n(&self,directed_edge_id:EDGE)->Planar64Vec3{
|
||||
fn directed_edge_n(&self,directed_edge_id:Self::Edge)->Planar64Vec3{
|
||||
let verts=self.edge_verts(directed_edge_id.as_undirected());
|
||||
(self.vert(verts[1].clone())-self.vert(verts[0].clone()))*((directed_edge_id.parity() as i64)*2-1)
|
||||
}
|
||||
fn vert(&self,vert_id:VERT)->Planar64Vec3;
|
||||
fn face_nd(&self,face_id:FACE)->(Self::Normal,Self::Offset);
|
||||
fn face_edges(&self,face_id:FACE)->Cow<Vec<EDGE>>;
|
||||
fn edge_faces(&self,edge_id:EDGE::UndirectedEdge)->Cow<[FACE;2]>;
|
||||
fn edge_verts(&self,edge_id:EDGE::UndirectedEdge)->Cow<[VERT;2]>;
|
||||
fn vert_edges(&self,vert_id:VERT)->Cow<Vec<EDGE>>;
|
||||
fn vert_faces(&self,vert_id:VERT)->Cow<Vec<FACE>>;
|
||||
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
|
||||
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
|
||||
fn face_edges(&self,face_id:Self::Face)->Cow<[Self::Edge]>;
|
||||
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Face;2]>;
|
||||
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Vert;2]>;
|
||||
fn vert_edges(&self,vert_id:Self::Vert)->Cow<[Self::Edge]>;
|
||||
fn vert_faces(&self,vert_id:Self::Vert)->Cow<[Self::Face]>;
|
||||
}
|
||||
struct FaceRefs{
|
||||
edges:Vec<SubmeshDirectedEdgeId>,
|
||||
@ -273,15 +280,12 @@ struct EdgePool{
|
||||
}
|
||||
impl EdgePool{
|
||||
fn push(&mut self,edge_ref_verts:EdgeRefVerts)->(&mut EdgeRefFaces,SubmeshEdgeId){
|
||||
let edge_id=if let Some(&edge_id)=self.edge_id_from_guy.get(&edge_ref_verts){
|
||||
edge_id
|
||||
}else{
|
||||
let edge_id=*self.edge_id_from_guy.entry(edge_ref_verts.clone()).or_insert_with(||{
|
||||
let edge_id=SubmeshEdgeId::new(self.edge_guys.len() as u32);
|
||||
self.edge_guys.push((edge_ref_verts.clone(),EdgeRefFaces::new()));
|
||||
self.edge_id_from_guy.insert(edge_ref_verts,edge_id);
|
||||
self.edge_guys.push((edge_ref_verts,EdgeRefFaces::new()));
|
||||
edge_id
|
||||
};
|
||||
(&mut unsafe{self.edge_guys.get_unchecked_mut(edge_id.get() as usize)}.1,edge_id)
|
||||
});
|
||||
(&mut self.edge_guys[edge_id.get() as usize].1,edge_id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,16 +320,11 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
|
||||
let mut submesh_verts=Vec::new();
|
||||
let mut submesh_vert_id_from_mesh_vert_id=HashMap::<MeshVertId,SubmeshVertId>::new();
|
||||
//lazy closure
|
||||
let mut get_submesh_vert_id=|vert_id:MeshVertId|{
|
||||
if let Some(&submesh_vert_id)=submesh_vert_id_from_mesh_vert_id.get(&vert_id){
|
||||
submesh_vert_id
|
||||
}else{
|
||||
let mut get_submesh_vert_id=|vert_id:MeshVertId|*submesh_vert_id_from_mesh_vert_id.entry(vert_id).or_insert_with(||{
|
||||
let submesh_vert_id=SubmeshVertId::new(submesh_verts.len() as u32);
|
||||
submesh_verts.push(vert_id);
|
||||
submesh_vert_id_from_mesh_vert_id.insert(vert_id,submesh_vert_id);
|
||||
submesh_vert_id
|
||||
}
|
||||
};
|
||||
});
|
||||
let mut edge_pool=EdgePool::default();
|
||||
let mut vert_ref_guys=vec![VertRefGuy::default();mesh.unique_pos.len()];
|
||||
let mut face_ref_guys=Vec::new();
|
||||
@ -358,10 +357,10 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
|
||||
edge_ref_faces.push(!is_sorted as usize,submesh_face_id);
|
||||
//index edges & face into vertices
|
||||
{
|
||||
let vert_ref_guy=unsafe{vert_ref_guys.get_unchecked_mut(submesh_vert0_id.get() as usize)};
|
||||
let vert_ref_guy=&mut vert_ref_guys[submesh_vert0_id.get() as usize];
|
||||
vert_ref_guy.edges.insert(edge_id.as_directed(is_sorted));
|
||||
vert_ref_guy.faces.insert(submesh_face_id);
|
||||
unsafe{vert_ref_guys.get_unchecked_mut(submesh_vert1_id.get() as usize)}.edges.insert(edge_id.as_directed(!is_sorted));
|
||||
vert_ref_guys[submesh_vert1_id.get() as usize].edges.insert(edge_id.as_directed(!is_sorted));
|
||||
}
|
||||
//return directed_edge_id
|
||||
edge_id.as_directed(is_sorted)
|
||||
@ -421,7 +420,10 @@ pub struct PhysicsMeshView<'a>{
|
||||
data:&'a PhysicsMeshData,
|
||||
topology:&'a PhysicsMeshTopology,
|
||||
}
|
||||
impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for PhysicsMeshView<'_>{
|
||||
impl MeshQuery for PhysicsMeshView<'_>{
|
||||
type Face=SubmeshFaceId;
|
||||
type Edge=SubmeshDirectedEdgeId;
|
||||
type Vert=SubmeshVertId;
|
||||
type Normal=Planar64Vec3;
|
||||
type Offset=Planar64;
|
||||
fn face_nd(&self,face_id:SubmeshFaceId)->(Planar64Vec3,Planar64){
|
||||
@ -433,7 +435,7 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for PhysicsMes
|
||||
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
|
||||
self.data.verts[vert_idx].0
|
||||
}
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<Vec<SubmeshDirectedEdgeId>>{
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
Cow::Borrowed(&self.topology.face_topology[face_id.get() as usize].edges)
|
||||
}
|
||||
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
|
||||
@ -442,10 +444,10 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for PhysicsMes
|
||||
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
|
||||
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].verts)
|
||||
}
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshDirectedEdgeId>>{
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].edges)
|
||||
}
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshFaceId>>{
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
|
||||
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].faces)
|
||||
}
|
||||
}
|
||||
@ -495,7 +497,10 @@ impl TransformedMesh<'_>{
|
||||
)
|
||||
}
|
||||
}
|
||||
impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for TransformedMesh<'_>{
|
||||
impl MeshQuery for TransformedMesh<'_>{
|
||||
type Face=SubmeshFaceId;
|
||||
type Edge=SubmeshDirectedEdgeId;
|
||||
type Vert=SubmeshVertId;
|
||||
type Normal=Vector3<Fixed<3,96>>;
|
||||
type Offset=Fixed<4,128>;
|
||||
fn face_nd(&self,face_id:SubmeshFaceId)->(Self::Normal,Self::Offset){
|
||||
@ -508,7 +513,7 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for Transforme
|
||||
self.transform.vertex.transform_point3(self.view.vert(vert_id)).fix_1()
|
||||
}
|
||||
#[inline]
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<Vec<SubmeshDirectedEdgeId>>{
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
self.view.face_edges(face_id)
|
||||
}
|
||||
#[inline]
|
||||
@ -520,11 +525,11 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for Transforme
|
||||
self.view.edge_verts(edge_id)
|
||||
}
|
||||
#[inline]
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshDirectedEdgeId>>{
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
|
||||
self.view.vert_edges(vert_id)
|
||||
}
|
||||
#[inline]
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshFaceId>>{
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
|
||||
self.view.vert_faces(vert_id)
|
||||
}
|
||||
}
|
||||
@ -669,13 +674,13 @@ impl MinkowskiMesh<'_>{
|
||||
}
|
||||
}
|
||||
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
|
||||
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>{
|
||||
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh>{
|
||||
//start on any vertex
|
||||
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
|
||||
//cross edge-face boundary if it's uncrossable
|
||||
match self.crawl_boundaries(self.farthest_vert(infinity_dir),infinity_dir,point){
|
||||
//if a vert is returned, it is the closest point to the infinity point
|
||||
EV::Vert(vert_id)=>FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Vert(vert_id),
|
||||
EV::Vert(vert_id)=>FEV::Vert(vert_id),
|
||||
EV::Edge(edge_id)=>{
|
||||
//cross to face if the boundary is not crossable and we are on the wrong side
|
||||
let edge_n=self.edge_n(edge_id);
|
||||
@ -693,55 +698,61 @@ impl MinkowskiMesh<'_>{
|
||||
//infinity_dir can always be treated as a velocity
|
||||
if !boundary_d.is_positive()&&boundary_n.dot(infinity_dir).is_zero(){
|
||||
//both faces cannot pass this condition, return early if one does.
|
||||
return FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Face(face_id);
|
||||
return FEV::Face(face_id);
|
||||
}
|
||||
}
|
||||
FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Edge(edge_id)
|
||||
FEV::Edge(edge_id)
|
||||
},
|
||||
}
|
||||
}
|
||||
fn closest_fev_not_inside(&self,mut infinity_body:crate::physics::Body)->Option<FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>>{
|
||||
infinity_body.infinity_dir().map_or(None,|dir|{
|
||||
// TODO: fundamentally improve this algorithm.
|
||||
// All it needs to do is find the closest point on the mesh
|
||||
// and return the FEV which the point resides on.
|
||||
//
|
||||
// What it actually does is use the above functions to trace a ray in from infinity,
|
||||
// crawling the closest point along the mesh surface until the ray reaches
|
||||
// the starting point to discover the final FEV.
|
||||
//
|
||||
// The actual collision prediction probably does a single test
|
||||
// and then immediately returns with 0 FEV transitions on average,
|
||||
// because of the strict time_limit constraint.
|
||||
//
|
||||
// Most of the calculation time is just calculating the starting point
|
||||
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
|
||||
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
|
||||
infinity_body.infinity_dir().and_then(|dir|{
|
||||
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
|
||||
//a line is simpler to solve than a parabola
|
||||
infinity_body.velocity=dir;
|
||||
infinity_body.acceleration=vec3::ZERO;
|
||||
//crawl in from negative infinity along a tangent line to get the closest fev
|
||||
// TODO: change crawl_fev args to delta time? Optional values?
|
||||
match crate::face_crawler::crawl_fev(infinity_fev,self,&infinity_body,integer::Time::MIN/4,infinity_body.time){
|
||||
crate::face_crawler::CrawlResult::Miss(fev)=>Some(fev),
|
||||
crate::face_crawler::CrawlResult::Hit(_,_)=>None,
|
||||
}
|
||||
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_in(&self,relative_body:&crate::physics::Body,time_limit:integer::Time)->Option<(MinkowskiFace,GigaTime)>{
|
||||
self.closest_fev_not_inside(relative_body.clone()).map_or(None,|fev|{
|
||||
pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{
|
||||
//continue forwards along the body parabola
|
||||
match crate::face_crawler::crawl_fev(fev,self,relative_body,relative_body.time,time_limit){
|
||||
crate::face_crawler::CrawlResult::Miss(_)=>None,
|
||||
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
|
||||
}
|
||||
fev.crawl(self,relative_body,start_time,time_limit).hit()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_out(&self,relative_body:&crate::physics::Body,time_limit:integer::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=crate::physics::Body::new(
|
||||
relative_body.extrapolated_position(time_limit),
|
||||
-relative_body.extrapolated_velocity(time_limit),
|
||||
relative_body.acceleration,
|
||||
-time_limit,
|
||||
);
|
||||
self.closest_fev_not_inside(infinity_body).map_or(None,|fev|{
|
||||
let infinity_body=-relative_body.clone();
|
||||
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
|
||||
//continue backwards along the body parabola
|
||||
match crate::face_crawler::crawl_fev(fev,self,&-relative_body.clone(),-time_limit,-relative_body.time){
|
||||
crate::face_crawler::CrawlResult::Miss(_)=>None,
|
||||
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,-time)),//no need to test -time<time_limit because of the first step
|
||||
}
|
||||
fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
|
||||
//no need to test -time<time_limit because of the first step
|
||||
.map(|(face,time)|(face,-time))
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_face_out(&self,relative_body:&crate::physics::Body,time_limit:integer::Time,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
|
||||
pub fn predict_collision_face_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
|
||||
//no algorithm needed, there is only one state and two cases (Edge,None)
|
||||
//determine when it passes an edge ("sliding off" case)
|
||||
let start_time={
|
||||
let r=(start_time-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num,r.den)
|
||||
};
|
||||
let mut best_time={
|
||||
let r=(time_limit-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
@ -757,7 +768,7 @@ impl MinkowskiMesh<'_>{
|
||||
//WARNING! d outside of *2
|
||||
//WARNING: truncated precision
|
||||
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){
|
||||
if Ratio::new(Planar64::ZERO,Planar64::EPSILON).le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_edge=Some(directed_edge_id);
|
||||
break;
|
||||
@ -766,15 +777,12 @@ impl MinkowskiMesh<'_>{
|
||||
}
|
||||
best_edge.map(|e|(e.as_undirected(),best_time))
|
||||
}
|
||||
fn infinity_in(&self,infinity_body:crate::physics::Body)->Option<(MinkowskiFace,GigaTime)>{
|
||||
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
|
||||
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
|
||||
match crate::face_crawler::crawl_fev(infinity_fev,self,&infinity_body,integer::Time::MIN/4,infinity_body.time){
|
||||
crate::face_crawler::CrawlResult::Miss(_)=>None,
|
||||
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
|
||||
}
|
||||
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
|
||||
}
|
||||
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
|
||||
let infinity_body=crate::physics::Body::new(point,vec3::Y,vec3::ZERO,integer::Time::ZERO);
|
||||
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
|
||||
//movement must escape the mesh forwards and backwards in time,
|
||||
//otherwise the point is not inside the mesh
|
||||
self.infinity_in(infinity_body)
|
||||
@ -784,9 +792,13 @@ impl MinkowskiMesh<'_>{
|
||||
)
|
||||
}
|
||||
}
|
||||
impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiMesh<'_>{
|
||||
impl MeshQuery for MinkowskiMesh<'_>{
|
||||
type Face=MinkowskiFace;
|
||||
type Edge=MinkowskiDirectedEdge;
|
||||
type Vert=MinkowskiVert;
|
||||
type Normal=Vector3<Fixed<3,96>>;
|
||||
type Offset=Fixed<4,128>;
|
||||
// TODO: relative d
|
||||
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
|
||||
match face_id{
|
||||
MinkowskiFace::VertFace(v0,f1)=>{
|
||||
@ -816,7 +828,7 @@ impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiM
|
||||
},
|
||||
}
|
||||
}
|
||||
fn face_edges(&self,face_id:MinkowskiFace)->Cow<Vec<MinkowskiDirectedEdge>>{
|
||||
fn face_edges(&self,face_id:MinkowskiFace)->Cow<[MinkowskiDirectedEdge]>{
|
||||
match face_id{
|
||||
MinkowskiFace::VertFace(v0,f1)=>{
|
||||
Cow::Owned(self.mesh1.face_edges(f1).iter().map(|&edge_id1|{
|
||||
@ -917,7 +929,7 @@ impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiM
|
||||
},
|
||||
}
|
||||
}
|
||||
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<Vec<MinkowskiDirectedEdge>>{
|
||||
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<[MinkowskiDirectedEdge]>{
|
||||
match vert_id{
|
||||
MinkowskiVert::VertVert(v0,v1)=>{
|
||||
let mut edges=Vec::new();
|
||||
@ -959,7 +971,7 @@ impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiM
|
||||
},
|
||||
}
|
||||
}
|
||||
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<Vec<MinkowskiFace>>{
|
||||
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<[MinkowskiFace]>{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@ -973,7 +985,7 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
|
||||
for k in 0..len{
|
||||
if k!=i&&k!=j{
|
||||
let d=n.dot(normals[k]).is_negative();
|
||||
if let Some(comp)=&d_comp{
|
||||
if let &Some(comp)=&d_comp{
|
||||
// This is testing if d_comp*d < 0
|
||||
if comp^d{
|
||||
return true;
|
||||
@ -993,9 +1005,3 @@ fn test_is_empty_volume(){
|
||||
assert!(!is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3()].to_vec()));
|
||||
assert!(is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3(),vec3::NEG_X.fix_3()].to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_me_a_cube(){
|
||||
let mesh=PhysicsMesh::unit_cube();
|
||||
//println!("mesh={:?}",mesh);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
349
engine/physics/src/push_solve.rs
Normal file
349
engine/physics/src/push_solve.rs
Normal file
@ -0,0 +1,349 @@
|
||||
use strafesnet_common::integer::{self,vec3::{self,Vector3},Fixed,Planar64,Planar64Vec3,Ratio};
|
||||
|
||||
// This algorithm is based on Lua code
|
||||
// written by Trey Reynolds in 2021
|
||||
|
||||
// EPSILON=1/2^10
|
||||
|
||||
// A stack-allocated variable-size list that holds up to 4 elements
|
||||
// Direct references are used instead of indices i0, i1, i2, i3
|
||||
type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
|
||||
|
||||
// hack to allow comparing ratios to zero
|
||||
const RATIO_ZERO:Ratio<Fixed<1,32>,Fixed<1,32>>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
|
||||
|
||||
struct Ray{
|
||||
origin:Planar64Vec3,
|
||||
direction:Planar64Vec3,
|
||||
}
|
||||
impl Ray{
|
||||
fn extrapolate<Num,Den,N1,T1>(&self,t:Ratio<Num,Den>)->Planar64Vec3
|
||||
where
|
||||
Num:Copy,
|
||||
Den:Copy,
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
Planar64:core::ops::Mul<Den,Output=N1>,
|
||||
N1:integer::Divide<Den,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
{
|
||||
self.origin+self.direction.map(|elem|(t*elem).divide().fix())
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a contact restriction
|
||||
pub struct Contact{
|
||||
pub position:Planar64Vec3,
|
||||
pub velocity:Planar64Vec3,
|
||||
pub normal:Planar64Vec3,
|
||||
}
|
||||
impl Contact{
|
||||
fn relative_to(&self,point:Planar64Vec3)->Self{
|
||||
Self{
|
||||
position:self.position-point,
|
||||
velocity:self.velocity,
|
||||
normal:self.normal,
|
||||
}
|
||||
}
|
||||
fn relative_dot(&self,direction:Planar64Vec3)->Fixed<2,64>{
|
||||
(direction-self.velocity).dot(self.normal)
|
||||
}
|
||||
/// Calculate the time of intersection. (previously get_touch_time)
|
||||
fn solve(&self,ray:&Ray)->Ratio<Fixed<2,64>,Fixed<2,64>>{
|
||||
(self.position-ray.origin).dot(self.normal)/(ray.direction-self.velocity).dot(self.normal)
|
||||
}
|
||||
}
|
||||
|
||||
//note that this is horrible with fixed point arithmetic
|
||||
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{
|
||||
const EPSILON:Fixed<2,64>=Fixed::from_bits(Fixed::<2,64>::ONE.to_bits().shr(10));
|
||||
let det=c0.normal.dot(c0.velocity);
|
||||
if det.abs()<EPSILON{
|
||||
return None;
|
||||
}
|
||||
let d0=c0.normal.dot(c0.position);
|
||||
Some(c0.normal*d0/det)
|
||||
}
|
||||
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{
|
||||
const EPSILON:Fixed<4,128>=Fixed::from_bits(Fixed::<4,128>::ONE.to_bits().shr(10));
|
||||
let u0_u1=c0.velocity.cross(c1.velocity);
|
||||
let n0_n1=c0.normal.cross(c1.normal);
|
||||
let det=u0_u1.dot(n0_n1);
|
||||
if det.abs()<EPSILON{
|
||||
return None;
|
||||
}
|
||||
let d0=c0.normal.dot(c0.position);
|
||||
let d1=c1.normal.dot(c1.position);
|
||||
Some((c1.normal.cross(u0_u1)*d0+u0_u1.cross(c0.normal)*d1)/det)
|
||||
}
|
||||
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128>>,Fixed<3,96>>>{
|
||||
const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10));
|
||||
let n0_n1=c0.normal.cross(c1.normal);
|
||||
let det=c2.normal.dot(n0_n1);
|
||||
if det.abs()<EPSILON{
|
||||
return None;
|
||||
}
|
||||
let d0=c0.normal.dot(c0.position);
|
||||
let d1=c1.normal.dot(c1.position);
|
||||
let d2=c2.normal.dot(c2.position);
|
||||
Some((c1.normal.cross(c2.normal)*d0+c2.normal.cross(c0.normal)*d1+c0.normal.cross(c1.normal)*d2)/det)
|
||||
}
|
||||
|
||||
fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<Fixed<2,64>,Fixed<2,64>>;1]>{
|
||||
let det=u0.dot(u0);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
}
|
||||
let s0=u0.dot(point)/det;
|
||||
Some([s0])
|
||||
}
|
||||
fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio<Fixed<4,128>,Fixed<4,128>>;2]>{
|
||||
let u0_u1=u0.cross(u1);
|
||||
let det=u0_u1.dot(u0_u1);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
}
|
||||
let s0=u0_u1.dot(point.cross(u1))/det;
|
||||
let s1=u0_u1.dot(u0.cross(point))/det;
|
||||
Some([s0,s1])
|
||||
}
|
||||
fn decompose3(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3,u2:Planar64Vec3)->Option<[Ratio<Fixed<3,96>,Fixed<3,96>>;3]>{
|
||||
let det=u0.cross(u1).dot(u2);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
}
|
||||
let s0=point.cross(u1).dot(u2)/det;
|
||||
let s1=u0.cross(point).dot(u2)/det;
|
||||
let s2=u0.cross(u1).dot(point)/det;
|
||||
Some([s0,s1,s2])
|
||||
}
|
||||
|
||||
fn is_space_enclosed_2(
|
||||
a:Planar64Vec3,
|
||||
b:Planar64Vec3,
|
||||
)->bool{
|
||||
a.cross(b)==Vector3::new([Fixed::ZERO;3])
|
||||
&&a.dot(b).is_negative()
|
||||
}
|
||||
fn is_space_enclosed_3(
|
||||
a:Planar64Vec3,
|
||||
b:Planar64Vec3,
|
||||
c:Planar64Vec3
|
||||
)->bool{
|
||||
a.cross(b).dot(c)==Fixed::ZERO
|
||||
&&{
|
||||
let det_abac=a.cross(b).dot(a.cross(c));
|
||||
let det_abbc=a.cross(b).dot(b.cross(c));
|
||||
let det_acbc=a.cross(c).dot(b.cross(c));
|
||||
return!( det_abac*det_abbc).is_positive()
|
||||
&&!( det_abbc*det_acbc).is_positive()
|
||||
&&!(-det_acbc*det_abac).is_positive()
|
||||
||is_space_enclosed_2(a,b)
|
||||
||is_space_enclosed_2(a,c)
|
||||
||is_space_enclosed_2(b,c)
|
||||
}
|
||||
}
|
||||
fn is_space_enclosed_4(
|
||||
a:Planar64Vec3,
|
||||
b:Planar64Vec3,
|
||||
c:Planar64Vec3,
|
||||
d:Planar64Vec3,
|
||||
)->bool{
|
||||
let det_abc=a.cross(b).dot(c);
|
||||
let det_abd=a.cross(b).dot(d);
|
||||
let det_acd=a.cross(c).dot(d);
|
||||
let det_bcd=b.cross(c).dot(d);
|
||||
return( det_abc*det_abd).is_negative()
|
||||
&&(-det_abc*det_acd).is_negative()
|
||||
&&( det_abd*det_acd).is_negative()
|
||||
&&( det_abc*det_bcd).is_negative()
|
||||
&&(-det_abd*det_bcd).is_negative()
|
||||
&&( det_acd*det_bcd).is_negative()
|
||||
||is_space_enclosed_3(a,b,c)
|
||||
||is_space_enclosed_3(a,b,d)
|
||||
||is_space_enclosed_3(a,c,d)
|
||||
||is_space_enclosed_3(b,c,d)
|
||||
}
|
||||
|
||||
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
|
||||
Ray{origin:point,direction:vec3::ZERO}
|
||||
}
|
||||
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
|
||||
let direction=solve1(c0)?.divide().fix_1();
|
||||
let [s0]=decompose1(direction,c0.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
}
|
||||
let origin=point+solve1(
|
||||
&c0.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
|
||||
let direction=solve2(c0,c1)?.divide().fix_1();
|
||||
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
}
|
||||
let origin=point+solve2(
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ray>{
|
||||
let direction=solve3(c0,c1,c2)?.divide().fix_1();
|
||||
let [s0,s1,s2]=decompose3(direction,c0.velocity,c1.velocity,c2.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO)||s2.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
}
|
||||
let origin=point+solve3(
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
&c2.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
|
||||
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
|
||||
(get_push_ray_0(point),Conts::new_const())
|
||||
}
|
||||
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts)>{
|
||||
get_push_ray_1(point,c0)
|
||||
.map(|ray|(ray,Conts::from_iter([c0])))
|
||||
}
|
||||
fn get_best_push_ray_and_conts_2<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact)->Option<(Ray,Conts<'a>)>{
|
||||
if is_space_enclosed_2(c0.normal,c1.normal){
|
||||
return None;
|
||||
}
|
||||
if let Some(ray)=get_push_ray_2(point,c0,c1){
|
||||
return Some((ray,Conts::from_iter([c0,c1])));
|
||||
}
|
||||
if let Some(ray)=get_push_ray_1(point,c0){
|
||||
if !c1.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0])));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
fn get_best_push_ray_and_conts_3<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact)->Option<(Ray,Conts<'a>)>{
|
||||
if is_space_enclosed_3(c0.normal,c1.normal,c2.normal){
|
||||
return None;
|
||||
}
|
||||
if let Some(ray)=get_push_ray_3(point,c0,c1,c2){
|
||||
return Some((ray,Conts::from_iter([c0,c1,c2])));
|
||||
}
|
||||
if let Some(ray)=get_push_ray_2(point,c0,c1){
|
||||
if !c2.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0,c1])));
|
||||
}
|
||||
}
|
||||
if let Some(ray)=get_push_ray_2(point,c0,c2){
|
||||
if !c1.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0,c2])));
|
||||
}
|
||||
}
|
||||
if let Some(ray)=get_push_ray_1(point,c0){
|
||||
if !c1.relative_dot(ray.direction).is_negative()
|
||||
&&!c2.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0])));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
fn get_best_push_ray_and_conts_4<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact,c3:&'a Contact)->Option<(Ray,Conts<'a>)>{
|
||||
if is_space_enclosed_4(c0.normal,c1.normal,c2.normal,c3.normal){
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ray012,conts012)=get_best_push_ray_and_conts_3(point,c0,c1,c2)?;
|
||||
let (ray013,conts013)=get_best_push_ray_and_conts_3(point,c0,c1,c3)?;
|
||||
let (ray023,conts023)=get_best_push_ray_and_conts_3(point,c0,c2,c3)?;
|
||||
|
||||
let err012=c3.relative_dot(ray012.direction);
|
||||
let err013=c2.relative_dot(ray013.direction);
|
||||
let err023=c1.relative_dot(ray023.direction);
|
||||
|
||||
let best_err=err012.max(err013).max(err023);
|
||||
|
||||
if best_err==err012{
|
||||
return Some((ray012,conts012))
|
||||
}else if best_err==err013{
|
||||
return Some((ray013,conts013))
|
||||
}else if best_err==err023{
|
||||
return Some((ray023,conts023))
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn get_best_push_ray_and_conts<'a>(
|
||||
point:Planar64Vec3,
|
||||
conts:&[&'a Contact],
|
||||
)->Option<(Ray,Conts<'a>)>{
|
||||
match conts{
|
||||
&[c0,c1,c2,c3]=>get_best_push_ray_and_conts_4(point,c0,c1,c2,c3),
|
||||
&[c0,c1,c2]=>get_best_push_ray_and_conts_3(point,c0,c1,c2),
|
||||
&[c0,c1]=>get_best_push_ray_and_conts_2(point,c0,c1),
|
||||
&[c0]=>get_best_push_ray_and_conts_1(point,c0),
|
||||
&[]=>Some(get_best_push_ray_and_conts_0(point)),
|
||||
_=>unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
|
||||
contacts.iter()
|
||||
.filter(|&contact|
|
||||
!conts.iter().any(|&c|std::ptr::eq(c,contact))
|
||||
&&contact.relative_dot(ray.direction).is_negative()
|
||||
)
|
||||
.map(|contact|(contact.solve(ray),contact))
|
||||
.min_by_key(|&(t,_)|t)
|
||||
}
|
||||
|
||||
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
|
||||
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point);
|
||||
loop{
|
||||
let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){
|
||||
Some((t,cont))=>(t,cont),
|
||||
None=>return ray.origin,
|
||||
};
|
||||
|
||||
if RATIO_ZERO.le_ratio(next_t){
|
||||
return ray.origin;
|
||||
}
|
||||
|
||||
//push_front
|
||||
if conts.len()==conts.capacity(){
|
||||
//this is a dead case, new_conts never has more than 3 elements
|
||||
conts.rotate_right(1);
|
||||
conts[0]=next_cont;
|
||||
}else{
|
||||
conts.push(next_cont);
|
||||
conts.rotate_right(1);
|
||||
}
|
||||
|
||||
let meet_point=ray.extrapolate(next_t);
|
||||
match get_best_push_ray_and_conts(meet_point,conts.as_slice()){
|
||||
Some((new_ray,new_conts))=>(ray,conts)=(new_ray,new_conts),
|
||||
None=>return meet_point,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests{
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_push_solve(){
|
||||
let contacts=vec![
|
||||
Contact{
|
||||
position:vec3::ZERO,
|
||||
velocity:vec3::Y,
|
||||
normal:vec3::Y,
|
||||
}
|
||||
];
|
||||
assert_eq!(
|
||||
vec3::ZERO,
|
||||
push_solve(&contacts,vec3::NEG_Y)
|
||||
);
|
||||
}
|
||||
}
|
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;
|
281
engine/session/src/mouse_interpolator.rs
Normal file
281
engine/session/src/mouse_interpolator.rs
Normal file
@ -0,0 +1,281 @@
|
||||
use strafesnet_common::mouse::MouseState;
|
||||
use strafesnet_common::physics::{
|
||||
MouseInstruction,SetControlInstruction,ModeInstruction,MiscInstruction,
|
||||
Instruction as PhysicsInstruction,
|
||||
TimeInner as PhysicsTimeInner,
|
||||
Time as PhysicsTime,
|
||||
};
|
||||
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
|
||||
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
|
||||
|
||||
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
|
||||
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTimeInner>;
|
||||
|
||||
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTimeInner>;
|
||||
|
||||
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
|
||||
|
||||
/// To be fed into MouseInterpolator
|
||||
#[derive(Clone,Debug)]
|
||||
pub(crate) enum Instruction{
|
||||
MoveMouse(glam::IVec2),
|
||||
SetControl(SetControlInstruction),
|
||||
Mode(ModeInstruction),
|
||||
Misc(MiscInstruction),
|
||||
Idle,
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
enum UnbufferedInstruction{
|
||||
MoveMouse(glam::IVec2),
|
||||
NonMouse(NonMouseInstruction),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
enum BufferedInstruction{
|
||||
Mouse(MouseInstruction),
|
||||
NonMouse(NonMouseInstruction),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub(crate) enum NonMouseInstruction{
|
||||
SetControl(SetControlInstruction),
|
||||
Mode(ModeInstruction),
|
||||
Misc(MiscInstruction),
|
||||
Idle,
|
||||
}
|
||||
impl From<Instruction> for UnbufferedInstruction{
|
||||
#[inline]
|
||||
fn from(value:Instruction)->Self{
|
||||
match value{
|
||||
Instruction::MoveMouse(mouse_instruction)=>UnbufferedInstruction::MoveMouse(mouse_instruction),
|
||||
Instruction::SetControl(set_control_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::SetControl(set_control_instruction)),
|
||||
Instruction::Mode(mode_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Mode(mode_instruction)),
|
||||
Instruction::Misc(misc_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Misc(misc_instruction)),
|
||||
Instruction::Idle=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Idle),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<BufferedInstruction> for PhysicsInstruction{
|
||||
#[inline]
|
||||
fn from(value:BufferedInstruction)->Self{
|
||||
match value{
|
||||
BufferedInstruction::Mouse(mouse_instruction)=>PhysicsInstruction::Mouse(mouse_instruction),
|
||||
BufferedInstruction::NonMouse(non_mouse_instruction)=>match non_mouse_instruction{
|
||||
NonMouseInstruction::SetControl(set_control_instruction)=>PhysicsInstruction::SetControl(set_control_instruction),
|
||||
NonMouseInstruction::Mode(mode_instruction)=>PhysicsInstruction::Mode(mode_instruction),
|
||||
NonMouseInstruction::Misc(misc_instruction)=>PhysicsInstruction::Misc(misc_instruction),
|
||||
NonMouseInstruction::Idle=>PhysicsInstruction::Idle,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum StepInstruction{
|
||||
Pop,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
enum BufferState{
|
||||
Unbuffered,
|
||||
Initializing(SessionTime,MouseState<PhysicsTimeInner>),
|
||||
Buffered(SessionTime,MouseState<PhysicsTimeInner>),
|
||||
}
|
||||
|
||||
pub struct MouseInterpolator{
|
||||
buffer_state:BufferState,
|
||||
// double timestamped timeline?
|
||||
buffer:std::collections::VecDeque<TimedPhysicsInstruction>,
|
||||
output:std::collections::VecDeque<TimedPhysicsInstruction>,
|
||||
}
|
||||
// Maybe MouseInterpolator manipulation is better expressed using impls
|
||||
// and called from Instruction trait impls in session
|
||||
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:DoubleTimedSelfInstruction){
|
||||
self.push_unbuffered_input(ins.time,ins.instruction.time,ins.instruction.instruction.into())
|
||||
}
|
||||
}
|
||||
impl InstructionEmitter<StepInstruction> for MouseInterpolator{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
|
||||
self.buffered_instruction_with_timeout(time_limit)
|
||||
}
|
||||
}
|
||||
impl MouseInterpolator{
|
||||
pub fn new()->MouseInterpolator{
|
||||
MouseInterpolator{
|
||||
buffer_state:BufferState::Unbuffered,
|
||||
buffer:std::collections::VecDeque::new(),
|
||||
output:std::collections::VecDeque::new(),
|
||||
}
|
||||
}
|
||||
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
|
||||
self.buffer.push_front(TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:BufferedInstruction::Mouse(ins.instruction).into(),
|
||||
});
|
||||
// flush buffer to output
|
||||
if self.output.len()==0{
|
||||
// swap buffers
|
||||
core::mem::swap(&mut self.buffer,&mut self.output);
|
||||
}else{
|
||||
// append buffer contents to output
|
||||
self.output.append(&mut self.buffer);
|
||||
}
|
||||
}
|
||||
fn get_mouse_timedout_at(&self,time_limit:SessionTime)->Option<SessionTime>{
|
||||
match &self.buffer_state{
|
||||
BufferState::Unbuffered=>None,
|
||||
BufferState::Initializing(time,_mouse_state)
|
||||
|BufferState::Buffered(time,_mouse_state)=>{
|
||||
let timeout=*time+MOUSE_TIMEOUT;
|
||||
(timeout<time_limit).then_some(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn timeout_mouse(&mut self,timeout_time:PhysicsTime){
|
||||
// the state always changes to unbuffered
|
||||
let buffer_state=core::mem::replace(&mut self.buffer_state,BufferState::Unbuffered);
|
||||
match buffer_state{
|
||||
BufferState::Unbuffered=>(),
|
||||
BufferState::Initializing(_time,mouse_state)=>{
|
||||
// only a single mouse move was sent in 10ms, this is very much an edge case!
|
||||
self.push_mouse_and_flush_buffer(TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::ReplaceMouse{
|
||||
m1:MouseState{pos:mouse_state.pos,time:timeout_time},
|
||||
m0:mouse_state,
|
||||
},
|
||||
});
|
||||
}
|
||||
BufferState::Buffered(_time,mouse_state)=>{
|
||||
// duplicate the currently buffered mouse state but at a later (future, from the physics perspective) time
|
||||
self.push_mouse_and_flush_buffer(TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::SetNextMouse(MouseState{pos:mouse_state.pos,time:timeout_time}),
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
fn push_unbuffered_input(&mut self,session_time:SessionTime,physics_time:PhysicsTime,ins:UnbufferedInstruction){
|
||||
// new input
|
||||
// if there is zero instruction buffered, it means the mouse is not moving
|
||||
// case 1: unbuffered
|
||||
// no mouse event is buffered
|
||||
// - ins is mouse event? change to buffered
|
||||
// - ins other -> write to timeline
|
||||
// case 2: buffered
|
||||
// a mouse event is buffered, and exists within the last 10ms
|
||||
// case 3: stop
|
||||
// a mouse event is buffered, but no mouse events have transpired within 10ms
|
||||
|
||||
// replace_with allows the enum variant to safely be replaced
|
||||
// from behind a mutable reference, but a panic in the closure means that
|
||||
// the entire program terminates rather than completing an unwind.
|
||||
let (ins_mouse,ins_other)=replace_with::replace_with_or_abort_and_return(&mut self.buffer_state,|buffer_state|{
|
||||
match ins{
|
||||
UnbufferedInstruction::MoveMouse(pos)=>{
|
||||
let next_mouse_state=MouseState{pos,time:physics_time};
|
||||
match buffer_state{
|
||||
BufferState::Unbuffered=>{
|
||||
((None,None),BufferState::Initializing(session_time,next_mouse_state))
|
||||
},
|
||||
BufferState::Initializing(_time,mouse_state)=>{
|
||||
let ins_mouse=TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::ReplaceMouse{
|
||||
m0:mouse_state,
|
||||
m1:next_mouse_state.clone(),
|
||||
},
|
||||
};
|
||||
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
|
||||
},
|
||||
BufferState::Buffered(_time,mouse_state)=>{
|
||||
let ins_mouse=TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::SetNextMouse(next_mouse_state.clone()),
|
||||
};
|
||||
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
|
||||
},
|
||||
}
|
||||
},
|
||||
UnbufferedInstruction::NonMouse(other_instruction)=>((None,Some(TimedInstruction{
|
||||
time:physics_time,
|
||||
instruction:other_instruction,
|
||||
})),buffer_state),
|
||||
}
|
||||
});
|
||||
if let Some(ins)=ins_mouse{
|
||||
self.push_mouse_and_flush_buffer(ins);
|
||||
}
|
||||
if let Some(ins)=ins_other{
|
||||
let instruction=TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:BufferedInstruction::NonMouse(ins.instruction).into(),
|
||||
};
|
||||
if matches!(self.buffer_state,BufferState::Unbuffered){
|
||||
self.output.push_back(instruction);
|
||||
}else{
|
||||
self.buffer.push_back(instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
|
||||
match self.get_mouse_timedout_at(time_limit){
|
||||
Some(timeout)=>Some(TimedInstruction{
|
||||
time:timeout,
|
||||
instruction:StepInstruction::Timeout,
|
||||
}),
|
||||
None=>(self.output.len()!=0).then_some(TimedInstruction{
|
||||
// this timestamp should not matter
|
||||
time:time_limit,
|
||||
instruction:StepInstruction::Pop,
|
||||
}),
|
||||
}
|
||||
}
|
||||
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedPhysicsInstruction>{
|
||||
match ins.instruction{
|
||||
StepInstruction::Pop=>(),
|
||||
StepInstruction::Timeout=>self.timeout_mouse(ins.time),
|
||||
}
|
||||
self.output.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test(){
|
||||
let mut interpolator=MouseInterpolator::new();
|
||||
|
||||
let timer=strafesnet_common::timer::Timer::<strafesnet_common::timer::Scaled<SessionTimeInner,PhysicsTimeInner>>::unpaused(SessionTime::ZERO,PhysicsTime::from_secs(1000));
|
||||
|
||||
macro_rules! push{
|
||||
($time:expr,$ins:expr)=>{
|
||||
println!("in={:?}",$ins);
|
||||
interpolator.push_unbuffered_input(
|
||||
$time,
|
||||
timer.time($time),
|
||||
$ins,
|
||||
);
|
||||
while let Some(ins)=interpolator.buffered_instruction_with_timeout($time){
|
||||
let ins_retimed=TimedInstruction{
|
||||
time:timer.time(ins.time),
|
||||
instruction:ins.instruction,
|
||||
};
|
||||
let out=interpolator.pop_buffered_instruction(ins_retimed);
|
||||
println!("out={out:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// test each buffer_state transition
|
||||
let mut t=SessionTime::ZERO;
|
||||
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
|
||||
t+=SessionTime::from_millis(5);
|
||||
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
|
||||
t+=SessionTime::from_millis(5);
|
||||
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
|
||||
t+=SessionTime::from_millis(1);
|
||||
}
|
||||
}
|
443
engine/session/src/session.rs
Normal file
443
engine/session/src/session.rs
Normal file
@ -0,0 +1,443 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use strafesnet_common::gameplay_modes::{ModeId,StageId};
|
||||
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
|
||||
// session represents the non-hardware state of the client.
|
||||
// Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
|
||||
use strafesnet_common::physics::{
|
||||
ModeInstruction,MiscInstruction,
|
||||
Instruction as PhysicsInputInstruction,
|
||||
TimeInner as PhysicsTimeInner,
|
||||
Time as PhysicsTime
|
||||
};
|
||||
use strafesnet_common::timer::{Scaled,Timer};
|
||||
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
|
||||
use strafesnet_settings::directories::Directories;
|
||||
|
||||
use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
|
||||
use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
|
||||
use strafesnet_settings::settings::UserSettings;
|
||||
|
||||
pub enum Instruction<'a>{
|
||||
Input(SessionInputInstruction),
|
||||
Control(SessionControlInstruction),
|
||||
Playback(SessionPlaybackInstruction),
|
||||
ChangeMap(&'a strafesnet_common::map::CompleteMap),
|
||||
LoadReplay(strafesnet_snf::bot::Segment),
|
||||
Idle,
|
||||
}
|
||||
|
||||
pub enum SessionInputInstruction{
|
||||
Mouse(glam::IVec2),
|
||||
SetControl(strafesnet_common::physics::SetControlInstruction),
|
||||
Mode(ImplicitModeInstruction),
|
||||
Misc(strafesnet_common::physics::MiscInstruction),
|
||||
}
|
||||
/// Implicit mode instruction are fed separately to session.
|
||||
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum ImplicitModeInstruction{
|
||||
ResetAndRestart,
|
||||
ResetAndSpawn(ModeId,StageId),
|
||||
}
|
||||
|
||||
pub enum SessionControlInstruction{
|
||||
SetPaused(bool),
|
||||
// copy the current session simulation recording into a replay and view it
|
||||
CopyRecordingIntoReplayAndSpectate,
|
||||
StopSpectate,
|
||||
SaveReplay,
|
||||
LoadIntoReplayState,
|
||||
}
|
||||
pub enum SessionPlaybackInstruction{
|
||||
SkipForward,
|
||||
SkipBack,
|
||||
TogglePaused,
|
||||
DecreaseTimescale,
|
||||
IncreaseTimescale,
|
||||
}
|
||||
|
||||
pub struct FrameState{
|
||||
pub body:physics::Body,
|
||||
pub camera:physics::PhysicsCamera,
|
||||
pub time:PhysicsTime,
|
||||
}
|
||||
|
||||
pub struct Simulation{
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:physics::PhysicsState,
|
||||
}
|
||||
impl Simulation{
|
||||
pub const fn new(
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:physics::PhysicsState,
|
||||
)->Self{
|
||||
Self{
|
||||
timer,
|
||||
physics,
|
||||
}
|
||||
}
|
||||
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
|
||||
FrameState{
|
||||
body:self.physics.camera_body(),
|
||||
camera:self.physics.camera(),
|
||||
time:self.timer.time(time),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Recording{
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
}
|
||||
impl Recording{
|
||||
pub fn new(
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
)->Self{
|
||||
Self{instructions}
|
||||
}
|
||||
fn clear(&mut self){
|
||||
self.instructions.clear();
|
||||
}
|
||||
}
|
||||
pub struct Replay{
|
||||
next_instruction_id:usize,
|
||||
recording:Recording,
|
||||
simulation:Simulation,
|
||||
}
|
||||
impl Replay{
|
||||
pub const fn new(
|
||||
recording:Recording,
|
||||
simulation:Simulation,
|
||||
)->Self{
|
||||
Self{
|
||||
next_instruction_id:0,
|
||||
recording,
|
||||
simulation,
|
||||
}
|
||||
}
|
||||
pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){
|
||||
let mut time=self.simulation.timer.time(time_limit);
|
||||
loop{
|
||||
if let Some(ins)=self.recording.instructions.get(self.next_instruction_id){
|
||||
if ins.time<time{
|
||||
PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone());
|
||||
self.next_instruction_id+=1;
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}else{
|
||||
// loop playback
|
||||
self.next_instruction_id=0;
|
||||
// No need to reset physics because the very first instruction is 'Reset'
|
||||
let new_time=self.recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
self.simulation.timer.set_time(time_limit,new_time);
|
||||
time=new_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Hash,PartialEq,Eq)]
|
||||
struct BotId(u32);
|
||||
//#[derive(Clone,Copy,Hash,PartialEq,Eq)]
|
||||
//struct PlayerId(u32);
|
||||
|
||||
enum ViewState{
|
||||
Play,
|
||||
//Spectate(PlayerId),
|
||||
Replay(BotId),
|
||||
}
|
||||
|
||||
pub struct Session{
|
||||
directories:Directories,
|
||||
user_settings:UserSettings,
|
||||
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
|
||||
view_state:ViewState,
|
||||
//gui:GuiState
|
||||
geometry_shared:physics::PhysicsData,
|
||||
simulation:Simulation,
|
||||
// below fields not included in lite session
|
||||
recording:Recording,
|
||||
//players:HashMap<PlayerId,Simulation>,
|
||||
replays:HashMap<BotId,Replay>,
|
||||
}
|
||||
impl Session{
|
||||
pub fn new(
|
||||
user_settings:UserSettings,
|
||||
directories:Directories,
|
||||
simulation:Simulation,
|
||||
)->Self{
|
||||
Self{
|
||||
user_settings,
|
||||
directories,
|
||||
mouse_interpolator:MouseInterpolator::new(),
|
||||
geometry_shared:Default::default(),
|
||||
simulation,
|
||||
view_state:ViewState::Play,
|
||||
recording:Default::default(),
|
||||
replays:HashMap::new(),
|
||||
}
|
||||
}
|
||||
fn clear_recording(&mut self){
|
||||
self.recording.clear();
|
||||
}
|
||||
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
|
||||
self.simulation.physics.clear();
|
||||
self.geometry_shared.generate_models(map);
|
||||
}
|
||||
pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>Some(self.simulation.get_frame_state(time)),
|
||||
ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
|
||||
replay.simulation.get_frame_state(time)
|
||||
),
|
||||
}
|
||||
}
|
||||
pub fn user_settings(&self)->&UserSettings{
|
||||
&self.user_settings
|
||||
}
|
||||
}
|
||||
|
||||
// mouseinterpolator consumes RawInputInstruction
|
||||
// mouseinterpolator emits PhysicsInputInstruction
|
||||
// mouseinterpolator consumes DoStep to move on to the next emitted instruction
|
||||
// Session comsumes SessionInstruction -> forwards RawInputInstruction to mouseinterpolator
|
||||
// Session consumes DoStep -> forwards DoStep to mouseinterpolator
|
||||
// Session emits DoStep
|
||||
|
||||
impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::TimeInner>){
|
||||
// repetitive procedure macro
|
||||
macro_rules! run_mouse_interpolator_instruction{
|
||||
($instruction:expr)=>{
|
||||
self.mouse_interpolator.process_instruction(TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:TimedInstruction{
|
||||
time:self.simulation.timer.time(ins.time),
|
||||
instruction:$instruction,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// process any timeouts that occured since the last instruction
|
||||
self.process_exhaustive(ins.time);
|
||||
|
||||
match ins.instruction{
|
||||
// send it down to MouseInterpolator with two timestamps, SessionTime and PhysicsTime
|
||||
Instruction::Input(SessionInputInstruction::Mouse(pos))=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::MoveMouse(pos));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::SetControl(set_control_instruction))=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::SetControl(set_control_instruction));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndRestart))=>{
|
||||
self.clear_recording();
|
||||
let mode_id=self.simulation.physics.mode();
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
|
||||
self.clear_recording();
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::SetPaused(paused))=>{
|
||||
// don't flush the buffered instructions in the mouse interpolator
|
||||
// until the mouse is confirmed to be not moving at a later time
|
||||
// what if they pause for 5ms lmao
|
||||
_=self.simulation.timer.set_paused(ins.time,paused);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
|
||||
// Bind: B
|
||||
|
||||
// pause simulation
|
||||
_=self.simulation.timer.set_paused(ins.time,true);
|
||||
|
||||
// create recording
|
||||
let mut recording=Recording::default();
|
||||
recording.instructions.extend(self.recording.instructions.iter().cloned());
|
||||
|
||||
// create timer starting at first instruction (or zero if the list is empty)
|
||||
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
let timer=Timer::unpaused(ins.time,new_time);
|
||||
|
||||
// create default physics state
|
||||
let simulation=Simulation::new(timer,Default::default());
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
let bot_id=BotId(self.replays.len() as u32);
|
||||
self.replays.insert(bot_id,Replay::new(
|
||||
recording,
|
||||
simulation,
|
||||
));
|
||||
|
||||
// begin spectate
|
||||
self.view_state=ViewState::Replay(bot_id);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::StopSpectate)=>{
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
// delete the bot, otherwise it's inaccessible and wastes CPU
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>{
|
||||
self.replays.remove(&bot_id);
|
||||
},
|
||||
}
|
||||
_=self.simulation.timer.set_paused(ins.time,false);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::SaveReplay)=>{
|
||||
// Bind: N
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
let mut replays_path=self.directories.replays.clone();
|
||||
let file_name=format!("{}.snfb",ins.time);
|
||||
std::thread::spawn(move ||{
|
||||
std::fs::create_dir_all(replays_path.as_path()).unwrap();
|
||||
replays_path.push(file_name);
|
||||
let file=std::fs::File::create(replays_path).unwrap();
|
||||
strafesnet_snf::bot::write_bot(
|
||||
std::io::BufWriter::new(file),
|
||||
strafesnet_physics::VERSION.get(),
|
||||
replay.recording.instructions
|
||||
).unwrap();
|
||||
println!("Finished writing bot file!");
|
||||
});
|
||||
},
|
||||
}
|
||||
_=self.simulation.timer.set_paused(ins.time,false);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::LoadIntoReplayState)=>{
|
||||
// Bind: J
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
self.recording.instructions=replay.recording.instructions.into_iter().take(replay.next_instruction_id).collect();
|
||||
self.simulation=replay.simulation;
|
||||
},
|
||||
}
|
||||
// don't unpause -- use the replay timer state whether it is pasued or unpaused
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>{
|
||||
// allow simulation timescale for fun
|
||||
let scale=self.simulation.timer.get_scale();
|
||||
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
|
||||
},
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let scale=replay.simulation.timer.get_scale();
|
||||
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::DecreaseTimescale)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>{
|
||||
// allow simulation timescale for fun
|
||||
let scale=self.simulation.timer.get_scale();
|
||||
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
|
||||
},
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let scale=replay.simulation.timer.get_scale();
|
||||
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::SkipForward)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
|
||||
replay.simulation.timer.set_time(ins.time,time);
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::SkipBack)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
|
||||
replay.simulation.timer.set_time(ins.time,time);
|
||||
// resimulate the entire playback lol
|
||||
replay.next_instruction_id=0;
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::TogglePaused)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
_=replay.simulation.timer.set_paused(ins.time,!replay.simulation.timer.is_paused());
|
||||
},
|
||||
}
|
||||
}
|
||||
Instruction::ChangeMap(complete_map)=>{
|
||||
self.clear_recording();
|
||||
self.change_map(complete_map);
|
||||
},
|
||||
Instruction::LoadReplay(bot)=>{
|
||||
// pause simulation
|
||||
_=self.simulation.timer.set_paused(ins.time,true);
|
||||
|
||||
// create recording
|
||||
let recording=Recording::new(bot.instructions);
|
||||
|
||||
// create timer starting at first instruction (or zero if the list is empty)
|
||||
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
let timer=Timer::unpaused(ins.time,new_time);
|
||||
|
||||
// create default physics state
|
||||
let simulation=Simulation::new(timer,Default::default());
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
let bot_id=BotId(self.replays.len() as u32);
|
||||
self.replays.insert(bot_id,Replay::new(
|
||||
recording,
|
||||
simulation,
|
||||
));
|
||||
|
||||
// begin spectate
|
||||
self.view_state=ViewState::Replay(bot_id);
|
||||
},
|
||||
Instruction::Idle=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle);
|
||||
// this just refreshes the replays
|
||||
for replay in self.replays.values_mut(){
|
||||
// TODO: filter idles from recording, inject new idles in real time
|
||||
replay.advance(&self.geometry_shared,ins.time);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// process all emitted output instructions
|
||||
self.process_exhaustive(ins.time);
|
||||
}
|
||||
}
|
||||
impl InstructionConsumer<StepInstruction> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::TimeInner>){
|
||||
let time=self.simulation.timer.time(ins.time);
|
||||
if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins.set_time(time)){
|
||||
//record
|
||||
self.recording.instructions.push(instruction.clone());
|
||||
PhysicsContext::run_input_instruction(&mut self.simulation.physics,&self.geometry_shared,instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InstructionEmitter<StepInstruction> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
|
||||
self.mouse_interpolator.next_instruction(time_limit)
|
||||
}
|
||||
}
|
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 {
|
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 = { path = "../lib/common", registry = "strafesnet" }
|
||||
strafesnet_physics = { 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(())
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_bsp_loader"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -11,6 +11,8 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
glam = "0.29.0"
|
||||
strafesnet_common = { path = "../common", registry = "strafesnet" }
|
||||
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
|
||||
vbsp = "0.6.0"
|
||||
vmdl = "0.2.0"
|
||||
vpk = "0.2.0"
|
||||
|
@ -1,32 +1,52 @@
|
||||
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()
|
||||
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;
|
||||
|
||||
fn ingest_vertex(
|
||||
mb:&mut model::MeshBuilder,
|
||||
world_position:vbsp::Vector,
|
||||
texture_transform_u:glam::Vec4,
|
||||
texture_transform_v:glam::Vec4,
|
||||
normal:model::NormalId,
|
||||
color:model::ColorId,
|
||||
)->model::VertexId{
|
||||
//world_model.origin seems to always be 0,0,0
|
||||
let vertex_xyz=world_position.into();
|
||||
let pos=mb.acquire_pos_id(valve_transform(vertex_xyz));
|
||||
|
||||
//calculate texture coordinates
|
||||
let pos_4d=glam::Vec3::from_array(vertex_xyz).extend(1.0);
|
||||
let tex=glam::vec2(texture_transform_u.dot(pos_4d),texture_transform_v.dot(pos_4d));
|
||||
let tex=mb.acquire_tex_id(tex);
|
||||
|
||||
mb.acquire_vertex_id(model::IndexedVertex{
|
||||
pos,
|
||||
tex,
|
||||
normal,
|
||||
color,
|
||||
})
|
||||
}
|
||||
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,
|
||||
{
|
||||
|
||||
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);
|
||||
const TEMP_TOUCH_ME_ATTRIBUTE:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(0);
|
||||
|
||||
let mut prop_mesh_count=0;
|
||||
//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());
|
||||
//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;
|
||||
};
|
||||
let mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model());
|
||||
let placement=prop.as_prop_placement();
|
||||
model::Model{
|
||||
mesh:mesh_id,
|
||||
@ -48,13 +68,11 @@ where
|
||||
//the generated MeshIds in here will collide with the Loader Mesh Ids
|
||||
//but I can't think of a good workaround other than just remapping one later.
|
||||
let world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
|
||||
//non-deduplicated
|
||||
let mut spam_pos=Vec::new();
|
||||
let mut spam_tex=Vec::new();
|
||||
let mut spam_normal=Vec::new();
|
||||
let mut spam_vertices=Vec::new();
|
||||
let mut mb=model::MeshBuilder::new();
|
||||
|
||||
let color=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
let mut graphics_groups=Vec::new();
|
||||
let mut physics_group=model::IndexedPhysicsGroup::default();
|
||||
let mut render_id_to_graphics_group_id=std::collections::HashMap::new();
|
||||
let polygon_groups=world_model.faces().enumerate().map(|(polygon_group_id,face)|{
|
||||
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
|
||||
let face_texture=face.texture();
|
||||
@ -63,62 +81,41 @@ where
|
||||
let texture_transform_u=glam::Vec4::from_array(face_texture.texture_transforms_u)/(face_texture_data.width as f32);
|
||||
let texture_transform_v=glam::Vec4::from_array(face_texture.texture_transforms_v)/(face_texture_data.height as f32);
|
||||
|
||||
//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()));
|
||||
|
||||
//normal
|
||||
let normal=face.normal();
|
||||
let normal_idx=spam_normal.len() as u32;
|
||||
spam_normal.push(valve_transform(normal.into()));
|
||||
let mut polygon_iter=face.vertex_positions().map(|vertex_position|{
|
||||
//world_model.origin seems to always be 0,0,0
|
||||
let vertex_xyz=(world_model.origin+vertex_position).into();
|
||||
let pos_idx=spam_pos.len();
|
||||
spam_pos.push(valve_transform(vertex_xyz));
|
||||
|
||||
//calculate texture coordinates
|
||||
let pos=glam::Vec3::from_array(vertex_xyz).extend(1.0);
|
||||
let tex=glam::vec2(texture_transform_u.dot(pos),texture_transform_v.dot(pos));
|
||||
let tex_idx=spam_tex.len() as u32;
|
||||
spam_tex.push(tex);
|
||||
|
||||
let vertex_id=model::VertexId::new(spam_vertices.len() as u32);
|
||||
spam_vertices.push(model::IndexedVertex{
|
||||
pos:model::PositionId::new(pos_idx as u32),
|
||||
tex:model::TextureCoordinateId::new(tex_idx as u32),
|
||||
normal:model::NormalId::new(normal_idx),
|
||||
color:model::ColorId::new(0),
|
||||
});
|
||||
vertex_id
|
||||
});
|
||||
let normal=mb.acquire_normal_id(valve_transform(face.normal().into()));
|
||||
let mut polygon_iter=face.vertex_positions().map(|vertex_position|
|
||||
world_model.origin+vertex_position
|
||||
);
|
||||
let polygon_list=std::iter::from_fn(move||{
|
||||
match (polygon_iter.next(),polygon_iter.next(),polygon_iter.next()){
|
||||
(Some(v1),Some(v2),Some(v3))=>Some(vec![v1,v2,v3]),
|
||||
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3]),
|
||||
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
|
||||
_=>None,
|
||||
}
|
||||
}).map(|triplet|{
|
||||
triplet.map(|world_position|
|
||||
ingest_vertex(&mut mb,world_position,texture_transform_u,texture_transform_v,normal,color)
|
||||
).to_vec()
|
||||
}).collect();
|
||||
if face.is_visible(){
|
||||
//TODO: deduplicate graphics groups by render id
|
||||
//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=render_config_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(face_texture_data.name())));
|
||||
//deduplicate graphics groups by render id
|
||||
let graphics_group_id=*render_id_to_graphics_group_id.entry(render_id).or_insert_with(||{
|
||||
let graphics_group_id=graphics_groups.len();
|
||||
graphics_groups.push(model::IndexedGraphicsGroup{
|
||||
render:render_id,
|
||||
groups:vec![polygon_group_id],
|
||||
})
|
||||
groups:vec![],
|
||||
});
|
||||
graphics_group_id
|
||||
});
|
||||
graphics_groups[graphics_group_id].groups.push(polygon_group_id);
|
||||
}
|
||||
physics_group.groups.push(polygon_group_id);
|
||||
model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))
|
||||
}).collect();
|
||||
model::Mesh{
|
||||
unique_pos:spam_pos,
|
||||
unique_tex:spam_tex,
|
||||
unique_normal:spam_normal,
|
||||
unique_color:vec![glam::Vec4::ONE],
|
||||
unique_vertices:spam_vertices,
|
||||
polygon_groups,
|
||||
graphics_groups,
|
||||
physics_groups:vec![physics_group],
|
||||
}
|
||||
|
||||
mb.build(polygon_groups,graphics_groups,vec![])
|
||||
}).collect();
|
||||
|
||||
let world_models:Vec<model::Model>=
|
||||
@ -176,26 +173,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 +198,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 +217,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 +241,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,13 @@
|
||||
mod bsp;
|
||||
pub mod data;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
pub use data::Bsp;
|
||||
mod bsp;
|
||||
mod mesh;
|
||||
pub mod loader;
|
||||
|
||||
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{
|
||||
@ -15,6 +21,38 @@ impl std::fmt::Display for ReadError{
|
||||
}
|
||||
impl std::error::Error for ReadError{}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError{
|
||||
Texture(loader::TextureError),
|
||||
Mesh(loader::MeshError),
|
||||
}
|
||||
impl std::fmt::Display for LoadError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LoadError{}
|
||||
impl From<loader::TextureError> for LoadError{
|
||||
fn from(value:loader::TextureError)->Self{
|
||||
Self::Texture(value)
|
||||
}
|
||||
}
|
||||
impl From<loader::MeshError> for LoadError{
|
||||
fn from(value:loader::MeshError)->Self{
|
||||
Self::Mesh(value)
|
||||
}
|
||||
}
|
||||
pub struct Bsp{
|
||||
bsp:vbsp::Bsp,
|
||||
case_folded_file_names:std::collections::HashMap<String,String>,
|
||||
}
|
||||
impl AsRef<vbsp::Bsp> for Bsp{
|
||||
fn as_ref(&self)->&vbsp::Bsp{
|
||||
&self.bsp
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{
|
||||
let mut s=Vec::new();
|
||||
|
||||
@ -23,15 +61,66 @@ 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)
|
||||
impl Bsp{
|
||||
pub fn new(bsp:vbsp::Bsp)->Self{
|
||||
let case_folded_file_names=bsp.pack.clone().into_zip().lock().unwrap().file_names().map(|s|{
|
||||
(s.to_lowercase(),s.to_owned())
|
||||
}).collect();
|
||||
Self{
|
||||
bsp,
|
||||
case_folded_file_names,
|
||||
}
|
||||
}
|
||||
pub fn pack_get(&self,name_lowercase:&str)->Result<Option<Vec<u8>>,vbsp::BspError>{
|
||||
match self.case_folded_file_names.get(name_lowercase){
|
||||
Some(name_folded)=>self.bsp.pack.get(name_folded),
|
||||
None=>Ok(None),
|
||||
}
|
||||
}
|
||||
pub fn to_snf(&self,failure_mode:LoadFailureMode,vpk_list:&[Vpk])->Result<strafesnet_common::map::CompleteMap,LoadError>{
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
|
||||
let map_step1=bsp::convert(
|
||||
self,
|
||||
&mut texture_deferred_loader,
|
||||
&mut mesh_deferred_loader,
|
||||
);
|
||||
|
||||
let mut mesh_loader=loader::MeshLoader::new(loader::BspFinder{bsp:self,vpks:vpk_list},&mut texture_deferred_loader);
|
||||
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
|
||||
|
||||
let map_step2=map_step1.add_prop_meshes(prop_meshes);
|
||||
|
||||
let mut texture_loader=loader::TextureLoader::new();
|
||||
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
|
||||
|
||||
let map=map_step2.add_render_configs_and_textures(render_configs);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
pub struct Vpk{
|
||||
vpk:vpk::VPK,
|
||||
case_folded_file_names:std::collections::HashMap<String,String>,
|
||||
}
|
||||
impl AsRef<vpk::VPK> for Vpk{
|
||||
fn as_ref(&self)->&vpk::VPK{
|
||||
&self.vpk
|
||||
}
|
||||
}
|
||||
impl Vpk{
|
||||
pub fn new(vpk:vpk::VPK)->Vpk{
|
||||
let case_folded_file_names=vpk.tree.keys().map(|s|{
|
||||
(s.to_lowercase(),s.to_owned())
|
||||
}).collect();
|
||||
Vpk{
|
||||
vpk,
|
||||
case_folded_file_names,
|
||||
}
|
||||
}
|
||||
pub fn tree_get(&self,name_lowercase:&str)->Option<&vpk::entry::VPKEntry>{
|
||||
let name_folded=self.case_folded_file_names.get(name_lowercase)?;
|
||||
self.vpk.tree.get(name_folded)
|
||||
}
|
||||
}
|
175
lib/bsp_loader/src/loader.rs
Normal file
175
lib/bsp_loader/src/loader.rs
Normal file
@ -0,0 +1,175 @@
|
||||
use std::{borrow::Cow, io::Read};
|
||||
|
||||
use strafesnet_common::model::Mesh;
|
||||
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
|
||||
|
||||
use crate::{Bsp,Vpk};
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct BspFinder<'bsp,'vpk>{
|
||||
pub bsp:&'bsp Bsp,
|
||||
pub vpks:&'vpk [Vpk],
|
||||
}
|
||||
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
|
||||
pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
// search bsp
|
||||
if let Some(data)=self.bsp.pack_get(path)?{
|
||||
return Ok(Some(Cow::Owned(data)));
|
||||
}
|
||||
|
||||
//search each vpk
|
||||
for vpk in self.vpks{
|
||||
if let Some(vpk_entry)=vpk.tree_get(path){
|
||||
return Ok(Some(vpk_entry.get()?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModelLoader<'bsp,'vpk,'a>{
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
life:core::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
impl ModelLoader<'_,'_,'_>{
|
||||
#[inline]
|
||||
pub const fn new<'bsp,'vpk,'a>(
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
)->ModelLoader<'bsp,'vpk,'a>{
|
||||
ModelLoader{
|
||||
finder,
|
||||
life:core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
type Error=MeshError;
|
||||
type Index=&'a str;
|
||||
type Resource=vmdl::Model;
|
||||
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 mdl=self.finder.find(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl)?;
|
||||
let vtx=self.finder.find(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?;
|
||||
let vvd=self.finder.find(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?;
|
||||
Ok(vmdl::Model::from_parts(
|
||||
vmdl::mdl::Mdl::read(mdl.as_ref())?,
|
||||
vmdl::vtx::Vtx::read(vtx.as_ref())?,
|
||||
vmdl::vvd::Vvd::read(vvd.as_ref())?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MeshLoader<'bsp,'vpk,'load,'a>{
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
|
||||
}
|
||||
impl MeshLoader<'_,'_,'_,'_>{
|
||||
#[inline]
|
||||
pub const fn new<'bsp,'vpk,'load,'a>(
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
|
||||
)->MeshLoader<'bsp,'vpk,'load,'a>{
|
||||
MeshLoader{
|
||||
finder,
|
||||
deferred_loader
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'bsp,'vpk,'load,'a> Loader for MeshLoader<'bsp,'vpk,'load,'a>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
type Error=MeshError;
|
||||
type Index=&'a str;
|
||||
type Resource=Mesh;
|
||||
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
|
||||
let model=ModelLoader::new(self.finder).load(index)?;
|
||||
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
|
||||
Ok(mesh)
|
||||
}
|
||||
}
|
78
lib/bsp_loader/src/mesh.rs
Normal file
78
lib/bsp_loader/src/mesh.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use strafesnet_common::model;
|
||||
use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader;
|
||||
|
||||
use crate::valve_transform;
|
||||
|
||||
fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:model::ColorId)->model::VertexId{
|
||||
let pos=mb.acquire_pos_id(valve_transform(vertex.position.into()));
|
||||
let normal=mb.acquire_normal_id(valve_transform(vertex.normal.into()));
|
||||
let tex=mb.acquire_tex_id(glam::Vec2::from_array(vertex.texture_coordinates));
|
||||
mb.acquire_vertex_id(model::IndexedVertex{
|
||||
pos,
|
||||
tex,
|
||||
normal,
|
||||
color,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->model::Mesh{
|
||||
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 mb=model::MeshBuilder::new();
|
||||
|
||||
let color=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
|
||||
let model_vertices=model.vertices();
|
||||
|
||||
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]),
|
||||
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
|
||||
_=>None,
|
||||
}
|
||||
})
|
||||
}).flat_map(|[v1,v2,v3]|{
|
||||
// this should probably be a fatal error :D
|
||||
let v1=model_vertices.get(v1)?;
|
||||
let v2=model_vertices.get(v2)?;
|
||||
let v3=model_vertices.get(v3)?;
|
||||
Some(vec![
|
||||
ingest_vertex(&mut mb,v1,color),
|
||||
ingest_vertex(&mut mb,v2,color),
|
||||
ingest_vertex(&mut mb,v3,color),
|
||||
])
|
||||
}).collect()
|
||||
))
|
||||
}).collect();
|
||||
mb.build(polygon_groups,graphics_groups,physics_groups)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_common"
|
||||
version = "0.5.2"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -12,8 +12,8 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
[dependencies]
|
||||
arrayvec = "0.7.4"
|
||||
bitflags = "2.6.0"
|
||||
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
|
||||
linear_ops = { path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
|
||||
ratio_ops = { path = "../ratio_ops", registry = "strafesnet" }
|
||||
fixed_wide = { version = "0.1.2", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
|
||||
linear_ops = { version = "0.1.0", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
|
||||
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
|
||||
glam = "0.29.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
|
@ -18,13 +18,8 @@ bitflags::bitflags!{
|
||||
const Use=1<<14;//Interact with object
|
||||
const PrimaryAction=1<<15;//LBM/Shoot/Melee
|
||||
const SecondaryAction=1<<16;//RMB/ADS/Block
|
||||
}
|
||||
}
|
||||
impl Controls{
|
||||
pub const fn wasd()->Self{
|
||||
Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight)
|
||||
}
|
||||
pub const fn wasdqe()->Self{
|
||||
Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).union(Self::MoveUp).union(Self::MoveDown)
|
||||
|
||||
const WASD=Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).bits();
|
||||
const WASDQE=Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).union(Self::MoveUp).union(Self::MoveDown).bits();
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
use crate::model;
|
||||
use crate::integer::{Time,Planar64,Planar64Vec3};
|
||||
use crate::integer::{AbsoluteTime,Planar64,Planar64Vec3};
|
||||
|
||||
//you have this effect while in contact
|
||||
#[derive(Clone,Hash,Eq,PartialEq)]
|
||||
@ -31,7 +31,7 @@ pub enum Booster{
|
||||
//Affine(crate::integer::Planar64Affine3),//capable of SetVelocity,DotVelocity,normal booster,bouncy part,redirect velocity, and much more
|
||||
Velocity(Planar64Vec3),//straight up boost velocity adds to your current velocity
|
||||
Energy{direction:Planar64Vec3,energy:Planar64},//increase energy in direction
|
||||
AirTime(Time),//increase airtime, invariant across mass and gravity changes
|
||||
AirTime(AbsoluteTime),//increase airtime, invariant across mass and gravity changes
|
||||
Height(Planar64),//increase height, invariant across mass and gravity changes
|
||||
}
|
||||
impl Booster{
|
||||
@ -57,13 +57,13 @@ pub enum TrajectoryChoice{
|
||||
#[derive(Clone,Hash,Eq,PartialEq)]
|
||||
pub enum SetTrajectory{
|
||||
//Speed-type SetTrajectory
|
||||
AirTime(Time),//air time (relative to gravity direction) is invariant across mass and gravity changes
|
||||
AirTime(AbsoluteTime),//air time (relative to gravity direction) is invariant across mass and gravity changes
|
||||
Height(Planar64),//boost height (relative to gravity direction) is invariant across mass and gravity changes
|
||||
DotVelocity{direction:Planar64Vec3,dot:Planar64},//set your velocity in a specific direction without touching other directions
|
||||
//Velocity-type SetTrajectory
|
||||
TargetPointTime{//launch on a trajectory that will land at a target point in a set amount of time
|
||||
target_point:Planar64Vec3,
|
||||
time:Time,//short time = fast and direct, long time = launch high in the air, negative time = wrong way
|
||||
time:AbsoluteTime,//short time = fast and direct, long time = launch high in the air, negative time = wrong way
|
||||
},
|
||||
TargetPointSpeed{//launch at a fixed speed and land at a target point
|
||||
target_point:Planar64Vec3,
|
||||
|
@ -110,6 +110,7 @@ impl Stage{
|
||||
pub fn into_inner(self)->(HashMap<CheckpointId,ModelId>,HashSet<ModelId>){
|
||||
(self.ordered_checkpoints,self.unordered_checkpoints)
|
||||
}
|
||||
/// Returns true if the stage has no checkpoints.
|
||||
#[inline]
|
||||
pub const fn is_empty(&self)->bool{
|
||||
self.is_complete(0,0)
|
||||
|
@ -1,7 +1,8 @@
|
||||
const VALVE_SCALE:Planar64=Planar64::raw(1<<28);// 1/16
|
||||
|
||||
use crate::integer::{int,vec3::int as int3,Time,Ratio64,Planar64,Planar64Vec3};
|
||||
use crate::integer::{int,vec3::int as int3,AbsoluteTime,Ratio64,Planar64,Planar64Vec3};
|
||||
use crate::controls_bitflag::Controls;
|
||||
use crate::physics::Time as PhysicsTime;
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct StyleModifiers{
|
||||
@ -48,7 +49,7 @@ pub enum JumpCalculation{
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum JumpImpulse{
|
||||
Time(Time),//jump time is invariant across mass and gravity changes
|
||||
Time(AbsoluteTime),//jump time is invariant across mass and gravity changes
|
||||
Height(Planar64),//jump height is invariant across mass and gravity changes
|
||||
Linear(Planar64),//jump velocity is invariant across mass and gravity changes
|
||||
Energy(Planar64),// :)
|
||||
@ -199,8 +200,8 @@ impl ControlsActivation{
|
||||
}
|
||||
pub const fn full_3d()->Self{
|
||||
Self{
|
||||
controls_mask:Controls::wasdqe(),
|
||||
controls_intersects:Controls::wasdqe(),
|
||||
controls_mask:Controls::WASDQE,
|
||||
controls_intersects:Controls::WASDQE,
|
||||
controls_contains:Controls::empty(),
|
||||
}
|
||||
}
|
||||
@ -208,8 +209,8 @@ impl ControlsActivation{
|
||||
//Normal
|
||||
pub const fn full_2d()->Self{
|
||||
Self{
|
||||
controls_mask:Controls::wasd(),
|
||||
controls_intersects:Controls::wasd(),
|
||||
controls_mask:Controls::WASD,
|
||||
controls_intersects:Controls::WASD,
|
||||
controls_contains:Controls::empty(),
|
||||
}
|
||||
}
|
||||
@ -272,8 +273,8 @@ impl StrafeSettings{
|
||||
false=>None,
|
||||
}
|
||||
}
|
||||
pub fn next_tick(&self,time:Time)->Time{
|
||||
Time::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
|
||||
pub fn next_tick(&self,time:PhysicsTime)->PhysicsTime{
|
||||
PhysicsTime::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
|
||||
}
|
||||
pub const fn activates(&self,controls:Controls)->bool{
|
||||
self.enable.activates(controls)
|
||||
@ -435,7 +436,7 @@ impl StyleModifiers{
|
||||
enable:ControlsActivation::full_2d(),
|
||||
air_accel_limit:None,
|
||||
mv:int(3),
|
||||
tick_rate:Ratio64::new(64,Time::ONE_SECOND.nanos() as u64).unwrap(),
|
||||
tick_rate:Ratio64::new(64,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Energy(int(512)),
|
||||
@ -477,10 +478,10 @@ impl StyleModifiers{
|
||||
enable:ControlsActivation::full_2d(),
|
||||
air_accel_limit:None,
|
||||
mv:int(27)/10,
|
||||
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
|
||||
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Time(Time::from_micros(715_588)),
|
||||
impulse:JumpImpulse::Time(AbsoluteTime::from_micros(715_588)),
|
||||
calculation:JumpCalculation::Max,
|
||||
limit_minimum:true,
|
||||
}),
|
||||
@ -534,7 +535,7 @@ impl StyleModifiers{
|
||||
enable:ControlsActivation::full_2d(),
|
||||
air_accel_limit:Some(Planar64::raw(150<<28)*100),
|
||||
mv:(Planar64::raw(30)*VALVE_SCALE).fix_1(),
|
||||
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
|
||||
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
|
||||
@ -575,7 +576,7 @@ impl StyleModifiers{
|
||||
enable:ControlsActivation::full_2d(),
|
||||
air_accel_limit:Some((int(150)*66*VALVE_SCALE).fix_1()),
|
||||
mv:(int(30)*VALVE_SCALE).fix_1(),
|
||||
tick_rate:Ratio64::new(66,Time::ONE_SECOND.nanos() as u64).unwrap(),
|
||||
tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
|
||||
|
@ -1,53 +1,82 @@
|
||||
use crate::integer::Time;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TimedInstruction<I>{
|
||||
pub time:Time,
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct TimedInstruction<I,T>{
|
||||
pub time:Time<T>,
|
||||
pub instruction:I,
|
||||
}
|
||||
impl<I,T> TimedInstruction<I,T>{
|
||||
#[inline]
|
||||
pub fn set_time<TimeInner>(self,new_time:Time<TimeInner>)->TimedInstruction<I,TimeInner>{
|
||||
TimedInstruction{
|
||||
time:new_time,
|
||||
instruction:self.instruction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure all emitted instructions are processed before consuming external instructions
|
||||
pub trait InstructionEmitter<I>{
|
||||
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<I>>;
|
||||
type TimeInner;
|
||||
fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<I,Self::TimeInner>>;
|
||||
}
|
||||
/// Apply an atomic state update
|
||||
pub trait InstructionConsumer<I>{
|
||||
fn process_instruction(&mut self, instruction:TimedInstruction<I>);
|
||||
type TimeInner;
|
||||
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::TimeInner>);
|
||||
}
|
||||
/// If the object produces its own instructions, allow exhaustively feeding them back in
|
||||
pub trait InstructionFeedback<I,T>:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>
|
||||
where
|
||||
Time<T>:Copy,
|
||||
{
|
||||
#[inline]
|
||||
fn process_exhaustive(&mut self,time_limit:Time<T>){
|
||||
while let Some(instruction)=self.next_instruction(time_limit){
|
||||
self.process_instruction(instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<I,T,X> InstructionFeedback<I,T> for X
|
||||
where
|
||||
Time<T>:Copy,
|
||||
X:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>,
|
||||
{}
|
||||
|
||||
//PROPER PRIVATE FIELDS!!!
|
||||
pub struct InstructionCollector<I>{
|
||||
time:Time,
|
||||
pub struct InstructionCollector<I,T>{
|
||||
time:Time<T>,
|
||||
instruction:Option<I>,
|
||||
}
|
||||
impl<I> InstructionCollector<I>{
|
||||
pub const fn new(time:Time)->Self{
|
||||
impl<I,T> InstructionCollector<I,T>
|
||||
where Time<T>:Copy+PartialOrd,
|
||||
{
|
||||
#[inline]
|
||||
pub const fn new(time:Time<T>)->Self{
|
||||
Self{
|
||||
time,
|
||||
instruction:None
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub const fn time(&self)->Time{
|
||||
pub const fn time(&self)->Time<T>{
|
||||
self.time
|
||||
}
|
||||
pub fn collect(&mut self,instruction:Option<TimedInstruction<I>>){
|
||||
match instruction{
|
||||
Some(unwrap_instruction)=>{
|
||||
if unwrap_instruction.time<self.time {
|
||||
self.time=unwrap_instruction.time;
|
||||
self.instruction=Some(unwrap_instruction.instruction);
|
||||
}
|
||||
},
|
||||
None=>(),
|
||||
#[inline]
|
||||
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
|
||||
if let Some(ins)=instruction{
|
||||
if ins.time<self.time{
|
||||
self.time=ins.time;
|
||||
self.instruction=Some(ins.instruction);
|
||||
}
|
||||
}
|
||||
pub fn instruction(self)->Option<TimedInstruction<I>>{
|
||||
}
|
||||
#[inline]
|
||||
pub fn take(self)->Option<TimedInstruction<I,T>>{
|
||||
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
|
||||
match self.instruction{
|
||||
Some(instruction)=>Some(TimedInstruction{
|
||||
self.instruction.map(|instruction|TimedInstruction{
|
||||
time:self.time,
|
||||
instruction
|
||||
}),
|
||||
None=>None,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,26 @@ pub use fixed_wide::fixed::{Fixed,Fix};
|
||||
pub use ratio_ops::ratio::{Ratio,Divide};
|
||||
|
||||
//integer units
|
||||
|
||||
/// specific example of a "default" time type
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
pub struct Time(i64);
|
||||
impl Time{
|
||||
pub const MIN:Self=Self(i64::MIN);
|
||||
pub const MAX:Self=Self(i64::MAX);
|
||||
pub const ZERO:Self=Self(0);
|
||||
pub const ONE_SECOND:Self=Self(1_000_000_000);
|
||||
pub const ONE_MILLISECOND:Self=Self(1_000_000);
|
||||
pub const ONE_MICROSECOND:Self=Self(1_000);
|
||||
pub const ONE_NANOSECOND:Self=Self(1);
|
||||
pub enum TimeInner{}
|
||||
pub type AbsoluteTime=Time<TimeInner>;
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
pub struct Time<T>(i64,core::marker::PhantomData<T>);
|
||||
impl<T> Time<T>{
|
||||
pub const MIN:Self=Self::raw(i64::MIN);
|
||||
pub const MAX:Self=Self::raw(i64::MAX);
|
||||
pub const ZERO:Self=Self::raw(0);
|
||||
pub const EPSILON:Self=Self::raw(1);
|
||||
pub const ONE_SECOND:Self=Self::raw(1_000_000_000);
|
||||
pub const ONE_MILLISECOND:Self=Self::raw(1_000_000);
|
||||
pub const ONE_MICROSECOND:Self=Self::raw(1_000);
|
||||
pub const ONE_NANOSECOND:Self=Self::raw(1);
|
||||
#[inline]
|
||||
pub const fn raw(num:i64)->Self{
|
||||
Self(num)
|
||||
Self(num,core::marker::PhantomData)
|
||||
}
|
||||
#[inline]
|
||||
pub const fn get(self)->i64{
|
||||
@ -22,19 +29,19 @@ impl Time{
|
||||
}
|
||||
#[inline]
|
||||
pub const fn from_secs(num:i64)->Self{
|
||||
Self(Self::ONE_SECOND.0*num)
|
||||
Self::raw(Self::ONE_SECOND.0*num)
|
||||
}
|
||||
#[inline]
|
||||
pub const fn from_millis(num:i64)->Self{
|
||||
Self(Self::ONE_MILLISECOND.0*num)
|
||||
Self::raw(Self::ONE_MILLISECOND.0*num)
|
||||
}
|
||||
#[inline]
|
||||
pub const fn from_micros(num:i64)->Self{
|
||||
Self(Self::ONE_MICROSECOND.0*num)
|
||||
Self::raw(Self::ONE_MICROSECOND.0*num)
|
||||
}
|
||||
#[inline]
|
||||
pub const fn from_nanos(num:i64)->Self{
|
||||
Self(Self::ONE_NANOSECOND.0*num)
|
||||
Self::raw(Self::ONE_NANOSECOND.0*num)
|
||||
}
|
||||
//should I have checked subtraction? force all time variables to be positive?
|
||||
#[inline]
|
||||
@ -45,14 +52,18 @@ impl Time{
|
||||
pub const fn to_ratio(self)->Ratio<Planar64,Planar64>{
|
||||
Ratio::new(Planar64::raw(self.0),Planar64::raw(1_000_000_000))
|
||||
}
|
||||
#[inline]
|
||||
pub const fn coerce<U>(self)->Time<U>{
|
||||
Time::raw(self.0)
|
||||
}
|
||||
impl From<Planar64> for Time{
|
||||
}
|
||||
impl<T> From<Planar64> for Time<T>{
|
||||
#[inline]
|
||||
fn from(value:Planar64)->Self{
|
||||
Time((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
|
||||
}
|
||||
}
|
||||
impl<Num,Den,N1,T1> From<Ratio<Num,Den>> for Time
|
||||
impl<T,Num,Den,N1,T1> From<Ratio<Num,Den>> for Time<T>
|
||||
where
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
N1:Divide<Den,Output=T1>,
|
||||
@ -60,34 +71,34 @@ impl<Num,Den,N1,T1> From<Ratio<Num,Den>> for Time
|
||||
{
|
||||
#[inline]
|
||||
fn from(value:Ratio<Num,Den>)->Self{
|
||||
Time((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Time{
|
||||
impl<T> std::fmt::Display for Time<T>{
|
||||
#[inline]
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{}s+{:09}ns",self.0/Self::ONE_SECOND.0,self.0%Self::ONE_SECOND.0)
|
||||
}
|
||||
}
|
||||
impl std::default::Default for Time{
|
||||
impl<T> std::default::Default for Time<T>{
|
||||
fn default()->Self{
|
||||
Self(0)
|
||||
Self::raw(0)
|
||||
}
|
||||
}
|
||||
impl std::ops::Neg for Time{
|
||||
type Output=Time;
|
||||
impl<T> std::ops::Neg for Time<T>{
|
||||
type Output=Self;
|
||||
#[inline]
|
||||
fn neg(self)->Self::Output {
|
||||
Time(-self.0)
|
||||
Self::raw(-self.0)
|
||||
}
|
||||
}
|
||||
macro_rules! impl_time_additive_operator {
|
||||
($trait:ty, $method:ident) => {
|
||||
impl $trait for Time{
|
||||
type Output=Time;
|
||||
impl<T> $trait for Time<T>{
|
||||
type Output=Self;
|
||||
#[inline]
|
||||
fn $method(self,rhs:Self)->Self::Output {
|
||||
Time(self.0.$method(rhs.0))
|
||||
Self::raw(self.0.$method(rhs.0))
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -97,7 +108,7 @@ impl_time_additive_operator!(core::ops::Sub,sub);
|
||||
impl_time_additive_operator!(core::ops::Rem,rem);
|
||||
macro_rules! impl_time_additive_assign_operator {
|
||||
($trait:ty, $method:ident) => {
|
||||
impl $trait for Time{
|
||||
impl<T> $trait for Time<T>{
|
||||
#[inline]
|
||||
fn $method(&mut self,rhs:Self){
|
||||
self.0.$method(rhs.0)
|
||||
@ -108,33 +119,37 @@ macro_rules! impl_time_additive_assign_operator {
|
||||
impl_time_additive_assign_operator!(core::ops::AddAssign,add_assign);
|
||||
impl_time_additive_assign_operator!(core::ops::SubAssign,sub_assign);
|
||||
impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
|
||||
impl std::ops::Mul for Time{
|
||||
impl<T> std::ops::Mul for Time<T>{
|
||||
type Output=Ratio<fixed_wide::fixed::Fixed<2,64>,fixed_wide::fixed::Fixed<2,64>>;
|
||||
#[inline]
|
||||
fn mul(self,rhs:Self)->Self::Output{
|
||||
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
|
||||
}
|
||||
}
|
||||
impl std::ops::Div<i64> for Time{
|
||||
type Output=Time;
|
||||
impl<T> std::ops::Div<i64> for Time<T>{
|
||||
type Output=Self;
|
||||
#[inline]
|
||||
fn div(self,rhs:i64)->Self::Output{
|
||||
Time(self.0/rhs)
|
||||
Self::raw(self.0/rhs)
|
||||
}
|
||||
}
|
||||
impl std::ops::Mul<i64> for Time{
|
||||
type Output=Time;
|
||||
impl<T> std::ops::Mul<i64> for Time<T>{
|
||||
type Output=Self;
|
||||
#[inline]
|
||||
fn mul(self,rhs:i64)->Self::Output{
|
||||
Time(self.0*rhs)
|
||||
Self::raw(self.0*rhs)
|
||||
}
|
||||
}
|
||||
impl core::ops::Mul<Time> for Planar64{
|
||||
impl<T> core::ops::Mul<Time<T>> for Planar64{
|
||||
type Output=Ratio<Fixed<2,64>,Planar64>;
|
||||
fn mul(self,rhs:Time)->Self::Output{
|
||||
fn mul(self,rhs:Time<T>)->Self::Output{
|
||||
Ratio::new(self*Fixed::raw(rhs.0),Planar64::raw(1_000_000_000))
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test_time{
|
||||
use super::*;
|
||||
type Time=super::AbsoluteTime;
|
||||
#[test]
|
||||
fn time_from_planar64(){
|
||||
let a:Time=Planar64::from(1).into();
|
||||
@ -156,6 +171,7 @@ fn time_times_planar64(){
|
||||
let b=Planar64::from(2);
|
||||
assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn gcd(mut a:u64,mut b:u64)->u64{
|
||||
|
@ -7,6 +7,7 @@ pub mod mouse;
|
||||
pub mod timer;
|
||||
pub mod integer;
|
||||
pub mod physics;
|
||||
pub mod session;
|
||||
pub mod updatable;
|
||||
pub mod instruction;
|
||||
pub mod gameplay_attributes;
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::integer::{Planar64Vec3,Planar64Affine3};
|
||||
use crate::gameplay_attributes;
|
||||
|
||||
@ -123,6 +125,87 @@ pub struct Mesh{
|
||||
pub physics_groups:Vec<IndexedPhysicsGroup>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MeshBuilder{
|
||||
unique_pos:Vec<Planar64Vec3>,//Unit32Vec3
|
||||
unique_normal:Vec<Planar64Vec3>,//Unit32Vec3
|
||||
unique_tex:Vec<TextureCoordinate>,
|
||||
unique_color:Vec<Color4>,
|
||||
unique_vertices:Vec<IndexedVertex>,
|
||||
pos_id_from:HashMap<Planar64Vec3,PositionId>,//Unit32Vec3
|
||||
normal_id_from:HashMap<Planar64Vec3,NormalId>,//Unit32Vec3
|
||||
tex_id_from:HashMap<[u32;2],TextureCoordinateId>,
|
||||
color_id_from:HashMap<[u32;4],ColorId>,
|
||||
vertex_id_from:HashMap<IndexedVertex,VertexId>,
|
||||
}
|
||||
impl MeshBuilder{
|
||||
pub fn new()->Self{
|
||||
Self::default()
|
||||
}
|
||||
pub fn build(
|
||||
self,
|
||||
polygon_groups:Vec<PolygonGroup>,
|
||||
graphics_groups:Vec<IndexedGraphicsGroup>,
|
||||
physics_groups:Vec<IndexedPhysicsGroup>,
|
||||
)->Mesh{
|
||||
let MeshBuilder{
|
||||
unique_pos,
|
||||
unique_normal,
|
||||
unique_tex,
|
||||
unique_color,
|
||||
unique_vertices,
|
||||
..
|
||||
}=self;
|
||||
Mesh{
|
||||
unique_pos,
|
||||
unique_normal,
|
||||
unique_tex,
|
||||
unique_color,
|
||||
unique_vertices,
|
||||
polygon_groups,
|
||||
graphics_groups,
|
||||
physics_groups,
|
||||
}
|
||||
}
|
||||
pub fn acquire_pos_id(&mut self,pos:Planar64Vec3)->PositionId{
|
||||
*self.pos_id_from.entry(pos).or_insert_with(||{
|
||||
let pos_id=PositionId::new(self.unique_pos.len() as u32);
|
||||
self.unique_pos.push(pos);
|
||||
pos_id
|
||||
})
|
||||
}
|
||||
pub fn acquire_normal_id(&mut self,normal:Planar64Vec3)->NormalId{
|
||||
*self.normal_id_from.entry(normal).or_insert_with(||{
|
||||
let normal_id=NormalId::new(self.unique_normal.len() as u32);
|
||||
self.unique_normal.push(normal);
|
||||
normal_id
|
||||
})
|
||||
}
|
||||
pub fn acquire_tex_id(&mut self,tex:TextureCoordinate)->TextureCoordinateId{
|
||||
let h=tex.to_array().map(f32::to_bits);
|
||||
*self.tex_id_from.entry(h).or_insert_with(||{
|
||||
let tex_id=TextureCoordinateId::new(self.unique_tex.len() as u32);
|
||||
self.unique_tex.push(tex);
|
||||
tex_id
|
||||
})
|
||||
}
|
||||
pub fn acquire_color_id(&mut self,color:Color4)->ColorId{
|
||||
let h=color.to_array().map(f32::to_bits);
|
||||
*self.color_id_from.entry(h).or_insert_with(||{
|
||||
let color_id=ColorId::new(self.unique_color.len() as u32);
|
||||
self.unique_color.push(color);
|
||||
color_id
|
||||
})
|
||||
}
|
||||
pub fn acquire_vertex_id(&mut self,vertex:IndexedVertex)->VertexId{
|
||||
*self.vertex_id_from.entry(vertex.clone()).or_insert_with(||{
|
||||
let vertex_id=VertexId::new(self.unique_vertices.len() as u32);
|
||||
self.unique_vertices.push(vertex);
|
||||
vertex_id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
|
||||
pub struct ModelId(u32);
|
||||
pub struct Model{
|
||||
|
@ -1,11 +1,11 @@
|
||||
use crate::integer::Time;
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct MouseState{
|
||||
pub struct MouseState<T>{
|
||||
pub pos:glam::IVec2,
|
||||
pub time:Time,
|
||||
pub time:Time<T>,
|
||||
}
|
||||
impl Default for MouseState{
|
||||
impl<T> Default for MouseState<T>{
|
||||
fn default()->Self{
|
||||
Self{
|
||||
time:Time::ZERO,
|
||||
@ -13,8 +13,10 @@ impl Default for MouseState{
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MouseState{
|
||||
pub fn lerp(&self,target:&MouseState,time:Time)->glam::IVec2{
|
||||
impl<T> MouseState<T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub fn lerp(&self,target:&MouseState<T>,time:Time<T>)->glam::IVec2{
|
||||
let m0=self.pos.as_i64vec2();
|
||||
let m1=target.pos.as_i64vec2();
|
||||
//these are deltas
|
||||
|
@ -1,7 +1,33 @@
|
||||
use crate::mouse::MouseState;
|
||||
use crate::gameplay_modes::{ModeId,StageId};
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
pub enum TimeInner{}
|
||||
pub type Time=crate::integer::Time<TimeInner>;
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum Instruction{
|
||||
ReplaceMouse(crate::mouse::MouseState,crate::mouse::MouseState),
|
||||
SetNextMouse(crate::mouse::MouseState),
|
||||
Mouse(MouseInstruction),
|
||||
SetControl(SetControlInstruction),
|
||||
Mode(ModeInstruction),
|
||||
Misc(MiscInstruction),
|
||||
/// Idle: there were no input events, but the simulation is safe to advance to this timestep
|
||||
Idle,
|
||||
}
|
||||
impl Instruction{
|
||||
pub const IDLE:Self=Self::Idle;
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum MouseInstruction{
|
||||
/// Replace the entire interpolation state to avoid dividing by zero when replacing twice
|
||||
ReplaceMouse{
|
||||
m0:MouseState<TimeInner>,
|
||||
m1:MouseState<TimeInner>,
|
||||
},
|
||||
SetNextMouse(MouseState<TimeInner>),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum SetControlInstruction{
|
||||
SetMoveRight(bool),
|
||||
SetMoveUp(bool),
|
||||
SetMoveBack(bool),
|
||||
@ -10,18 +36,21 @@ pub enum Instruction{
|
||||
SetMoveForward(bool),
|
||||
SetJump(bool),
|
||||
SetZoom(bool),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum ModeInstruction{
|
||||
/// Reset: fully replace the physics state.
|
||||
/// This forgets all inputs and settings which need to be reapplied.
|
||||
Reset,
|
||||
/// Restart: Teleport to the start zone.
|
||||
Restart,
|
||||
/// This runs when you press R or teleport to a bonus
|
||||
Restart(ModeId),
|
||||
/// Spawn: Teleport to a specific mode's spawn
|
||||
/// Sets current mode & spawn
|
||||
Spawn(crate::gameplay_modes::ModeId,crate::gameplay_modes::StageId),
|
||||
Idle,
|
||||
//Idle: there were no input events, but the simulation is safe to advance to this timestep
|
||||
//for interpolation / networking / playback reasons, most playback heads will always want
|
||||
//to be 1 instruction ahead to generate the next state for interpolation.
|
||||
/// This runs when the map loads to put you at the map lobby
|
||||
Spawn(ModeId,StageId),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum MiscInstruction{
|
||||
PracticeFly,
|
||||
SetSensitivity(crate::integer::Ratio64Vec2),
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
|
||||
use crate::integer::Time;
|
||||
|
||||
use crate::physics::{TimeInner as PhysicsTimeInner,Time as PhysicsTime};
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
pub enum TimeInner{}
|
||||
pub type Time=crate::integer::Time<TimeInner>;
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum FlagReason{
|
||||
@ -15,6 +20,11 @@ pub enum FlagReason{
|
||||
}
|
||||
impl ToString for FlagReason{
|
||||
fn to_string(&self)->String{
|
||||
self.as_ref().to_owned()
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for FlagReason{
|
||||
fn as_ref(&self)->&str{
|
||||
match self{
|
||||
FlagReason::Anticheat=>"Passed through anticheat zone.",
|
||||
FlagReason::StyleChange=>"Changed style.",
|
||||
@ -25,7 +35,7 @@ impl ToString for FlagReason{
|
||||
FlagReason::Timescale=>"Timescale is not allowed in this style.",
|
||||
FlagReason::TimeTravel=>"Time travel is not allowed in this style.",
|
||||
FlagReason::Teleport=>"Illegal teleport.",
|
||||
}.to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,8 +55,8 @@ impl std::error::Error for Error{}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
enum RunState{
|
||||
Created,
|
||||
Started{timer:TimerFixed<Realtime,Unpaused>},
|
||||
Finished{timer:TimerFixed<Realtime,Paused>},
|
||||
Started{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Unpaused>},
|
||||
Finished{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Paused>},
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
@ -62,14 +72,14 @@ impl Run{
|
||||
flagged:None,
|
||||
}
|
||||
}
|
||||
pub fn time(&self,time:Time)->Time{
|
||||
pub fn time(&self,time:PhysicsTime)->Time{
|
||||
match &self.state{
|
||||
RunState::Created=>Time::ZERO,
|
||||
RunState::Started{timer}=>timer.time(time),
|
||||
RunState::Finished{timer}=>timer.time(time),
|
||||
RunState::Finished{timer}=>timer.time(),
|
||||
}
|
||||
}
|
||||
pub fn start(&mut self,time:Time)->Result<(),Error>{
|
||||
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
|
||||
match &self.state{
|
||||
RunState::Created=>{
|
||||
self.state=RunState::Started{
|
||||
@ -81,7 +91,7 @@ impl Run{
|
||||
RunState::Finished{..}=>Err(Error::AlreadyFinished),
|
||||
}
|
||||
}
|
||||
pub fn finish(&mut self,time:Time)->Result<(),Error>{
|
||||
pub fn finish(&mut self,time:PhysicsTime)->Result<(),Error>{
|
||||
//this uses Copy
|
||||
match &self.state{
|
||||
RunState::Created=>Err(Error::NotStarted),
|
||||
@ -100,4 +110,10 @@ impl Run{
|
||||
self.flagged=Some(flag_reason);
|
||||
}
|
||||
}
|
||||
pub fn get_finish_time(&self)->Option<Time>{
|
||||
match &self.state{
|
||||
RunState::Finished{timer}=>Some(timer.time()),
|
||||
_=>None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
lib/common/src/session.rs
Normal file
3
lib/common/src/session.rs
Normal file
@ -0,0 +1,3 @@
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
pub enum TimeInner{}
|
||||
pub type Time=crate::integer::Time<TimeInner>;
|
@ -22,79 +22,106 @@ impl PauseState for Unpaused{
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
pub enum Inner{}
|
||||
type InnerTime=Time<Inner>;
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct Realtime{
|
||||
offset:Time,
|
||||
pub struct Realtime<In,Out>{
|
||||
offset:InnerTime,
|
||||
_in:core::marker::PhantomData<In>,
|
||||
_out:core::marker::PhantomData<Out>,
|
||||
}
|
||||
impl<In,Out> Realtime<In,Out>{
|
||||
pub const fn new(offset:InnerTime)->Self{
|
||||
Self{
|
||||
offset,
|
||||
_in:core::marker::PhantomData,
|
||||
_out:core::marker::PhantomData,
|
||||
}
|
||||
impl Realtime{
|
||||
pub const fn new(offset:Time)->Self{
|
||||
Self{offset}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct Scaled{
|
||||
pub struct Scaled<In,Out>{
|
||||
scale:Ratio64,
|
||||
offset:Time,
|
||||
offset:InnerTime,
|
||||
_in:core::marker::PhantomData<In>,
|
||||
_out:core::marker::PhantomData<Out>,
|
||||
}
|
||||
impl<In,Out> Scaled<In,Out>
|
||||
where Time<In>:Copy,
|
||||
{
|
||||
pub const fn new(scale:Ratio64,offset:InnerTime)->Self{
|
||||
Self{
|
||||
scale,
|
||||
offset,
|
||||
_in:core::marker::PhantomData,
|
||||
_out:core::marker::PhantomData,
|
||||
}
|
||||
impl Scaled{
|
||||
pub const fn new(scale:Ratio64,offset:Time)->Self{
|
||||
Self{scale,offset}
|
||||
}
|
||||
const fn with_scale(scale:Ratio64)->Self{
|
||||
Self{scale,offset:Time::ZERO}
|
||||
Self::new(scale,InnerTime::ZERO)
|
||||
}
|
||||
const fn scale(&self,time:Time)->Time{
|
||||
Time::raw(self.scale.mul_int(time.get()))
|
||||
const fn scale(&self,time:Time<In>)->InnerTime{
|
||||
InnerTime::raw(self.scale.mul_int(time.get()))
|
||||
}
|
||||
const fn get_scale(&self)->Ratio64{
|
||||
self.scale
|
||||
}
|
||||
fn set_scale(&mut self,time:Time,new_scale:Ratio64){
|
||||
fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
|
||||
let new_time=self.get_time(time);
|
||||
self.scale=new_scale;
|
||||
self.set_time(time,new_time);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TimerState:Copy+std::fmt::Debug{
|
||||
pub trait TimerState{
|
||||
type In;
|
||||
type Out;
|
||||
fn identity()->Self;
|
||||
fn get_time(&self,time:Time)->Time;
|
||||
fn set_time(&mut self,time:Time,new_time:Time);
|
||||
fn get_offset(&self)->Time;
|
||||
fn set_offset(&mut self,offset:Time);
|
||||
fn get_time(&self,time:Time<Self::In>)->Time<Self::Out>;
|
||||
fn set_time(&mut self,time:Time<Self::In>,new_time:Time<Self::Out>);
|
||||
fn get_offset(&self)->InnerTime;
|
||||
fn set_offset(&mut self,offset:InnerTime);
|
||||
}
|
||||
impl TimerState for Realtime{
|
||||
impl<In,Out> TimerState for Realtime<In,Out>{
|
||||
type In=In;
|
||||
type Out=Out;
|
||||
fn identity()->Self{
|
||||
Self{offset:Time::ZERO}
|
||||
Self::new(InnerTime::ZERO)
|
||||
}
|
||||
fn get_time(&self,time:Time)->Time{
|
||||
time+self.offset
|
||||
fn get_time(&self,time:Time<In>)->Time<Out>{
|
||||
time.coerce()+self.offset.coerce()
|
||||
}
|
||||
fn set_time(&mut self,time:Time,new_time:Time){
|
||||
self.offset=new_time-time;
|
||||
fn set_time(&mut self,time:Time<In>,new_time:Time<Out>){
|
||||
self.offset=new_time.coerce()-time.coerce();
|
||||
}
|
||||
fn get_offset(&self)->Time{
|
||||
fn get_offset(&self)->InnerTime{
|
||||
self.offset
|
||||
}
|
||||
fn set_offset(&mut self,offset:Time){
|
||||
fn set_offset(&mut self,offset:InnerTime){
|
||||
self.offset=offset;
|
||||
}
|
||||
}
|
||||
impl TimerState for Scaled{
|
||||
impl<In,Out> TimerState for Scaled<In,Out>
|
||||
where Time<In>:Copy,
|
||||
{
|
||||
type In=In;
|
||||
type Out=Out;
|
||||
fn identity()->Self{
|
||||
Self{scale:Ratio64::ONE,offset:Time::ZERO}
|
||||
Self::new(Ratio64::ONE,InnerTime::ZERO)
|
||||
}
|
||||
fn get_time(&self,time:Time)->Time{
|
||||
self.scale(time)+self.offset
|
||||
fn get_time(&self,time:Time<In>)->Time<Out>{
|
||||
(self.scale(time)+self.offset).coerce()
|
||||
}
|
||||
fn set_time(&mut self,time:Time,new_time:Time){
|
||||
self.offset=new_time-self.scale(time);
|
||||
fn set_time(&mut self,time:Time<In>,new_time:Time<Out>){
|
||||
self.offset=new_time.coerce()-self.scale(time);
|
||||
}
|
||||
fn get_offset(&self)->Time{
|
||||
fn get_offset(&self)->InnerTime{
|
||||
self.offset
|
||||
}
|
||||
fn set_offset(&mut self,offset:Time){
|
||||
fn set_offset(&mut self,offset:InnerTime){
|
||||
self.offset=offset;
|
||||
}
|
||||
}
|
||||
@ -106,8 +133,10 @@ pub struct TimerFixed<T:TimerState,P:PauseState>{
|
||||
}
|
||||
|
||||
//scaled timer methods are generic across PauseState
|
||||
impl<P:PauseState> TimerFixed<Scaled,P>{
|
||||
pub fn scaled(time:Time,new_time:Time,scale:Ratio64)->Self{
|
||||
impl<P:PauseState,In,Out> TimerFixed<Scaled<In,Out>,P>
|
||||
where Time<In>:Copy,
|
||||
{
|
||||
pub fn scaled(time:Time<In>,new_time:Time<Out>,scale:Ratio64)->Self{
|
||||
let mut timer=Self{
|
||||
state:Scaled::with_scale(scale),
|
||||
_paused:P::new(),
|
||||
@ -118,15 +147,17 @@ impl<P:PauseState> TimerFixed<Scaled,P>{
|
||||
pub const fn get_scale(&self)->Ratio64{
|
||||
self.state.get_scale()
|
||||
}
|
||||
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
|
||||
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
|
||||
self.state.set_scale(time,new_scale)
|
||||
}
|
||||
}
|
||||
|
||||
//pause and unpause is generic across TimerState
|
||||
impl<T:TimerState> TimerFixed<T,Paused>{
|
||||
pub fn into_unpaused(self,time:Time)->TimerFixed<T,Unpaused>{
|
||||
let new_time=self.time(time);
|
||||
impl<T:TimerState> TimerFixed<T,Paused>
|
||||
where Time<T::In>:Copy,
|
||||
{
|
||||
pub fn into_unpaused(self,time:Time<T::In>)->TimerFixed<T,Unpaused>{
|
||||
let new_time=self.time();
|
||||
let mut timer=TimerFixed{
|
||||
state:self.state,
|
||||
_paused:Unpaused,
|
||||
@ -134,9 +165,14 @@ impl<T:TimerState> TimerFixed<T,Paused>{
|
||||
timer.set_time(time,new_time);
|
||||
timer
|
||||
}
|
||||
pub fn time(&self)->Time<T::Out>{
|
||||
self.state.get_offset().coerce()
|
||||
}
|
||||
impl<T:TimerState> TimerFixed<T,Unpaused>{
|
||||
pub fn into_paused(self,time:Time)->TimerFixed<T,Paused>{
|
||||
}
|
||||
impl<T:TimerState> TimerFixed<T,Unpaused>
|
||||
where Time<T::In>:Copy,
|
||||
{
|
||||
pub fn into_paused(self,time:Time<T::In>)->TimerFixed<T,Paused>{
|
||||
let new_time=self.time(time);
|
||||
let mut timer=TimerFixed{
|
||||
state:self.state,
|
||||
@ -145,11 +181,14 @@ impl<T:TimerState> TimerFixed<T,Unpaused>{
|
||||
timer.set_time(time,new_time);
|
||||
timer
|
||||
}
|
||||
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
|
||||
self.state.get_time(time)
|
||||
}
|
||||
}
|
||||
|
||||
//the new constructor and time queries are generic across both
|
||||
impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
|
||||
pub fn new(time:Time,new_time:Time)->Self{
|
||||
pub fn new(time:Time<T::In>,new_time:Time<T::Out>)->Self{
|
||||
let mut timer=Self{
|
||||
state:T::identity(),
|
||||
_paused:P::new(),
|
||||
@ -166,15 +205,9 @@ impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
|
||||
pub fn into_state(self)->T{
|
||||
self.state
|
||||
}
|
||||
pub fn time(&self,time:Time)->Time{
|
||||
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
|
||||
match P::IS_PAUSED{
|
||||
true=>self.state.get_offset(),
|
||||
false=>self.state.get_time(time),
|
||||
}
|
||||
}
|
||||
pub fn set_time(&mut self,time:Time,new_time:Time){
|
||||
match P::IS_PAUSED{
|
||||
true=>self.state.set_offset(new_time),
|
||||
true=>self.state.set_offset(new_time.coerce()),
|
||||
false=>self.state.set_time(time,new_time),
|
||||
}
|
||||
}
|
||||
@ -198,7 +231,11 @@ pub enum Timer<T:TimerState>{
|
||||
Paused(TimerFixed<T,Paused>),
|
||||
Unpaused(TimerFixed<T,Unpaused>),
|
||||
}
|
||||
impl<T:TimerState> Timer<T>{
|
||||
impl<T:TimerState> Timer<T>
|
||||
where
|
||||
T:Copy,
|
||||
Time<T::In>:Copy,
|
||||
{
|
||||
pub fn from_state(state:T,paused:bool)->Self{
|
||||
match paused{
|
||||
true=>Self::Paused(TimerFixed::from_state(state)),
|
||||
@ -211,32 +248,32 @@ impl<T:TimerState> Timer<T>{
|
||||
Self::Unpaused(timer)=>(timer.into_state(),false),
|
||||
}
|
||||
}
|
||||
pub fn paused(time:Time,new_time:Time)->Self{
|
||||
pub fn paused(time:Time<T::In>,new_time:Time<T::Out>)->Self{
|
||||
Self::Paused(TimerFixed::new(time,new_time))
|
||||
}
|
||||
pub fn unpaused(time:Time,new_time:Time)->Self{
|
||||
pub fn unpaused(time:Time<T::In>,new_time:Time<T::Out>)->Self{
|
||||
Self::Unpaused(TimerFixed::new(time,new_time))
|
||||
}
|
||||
pub fn time(&self,time:Time)->Time{
|
||||
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
|
||||
match self{
|
||||
Self::Paused(timer)=>timer.time(time),
|
||||
Self::Paused(timer)=>timer.time(),
|
||||
Self::Unpaused(timer)=>timer.time(time),
|
||||
}
|
||||
}
|
||||
pub fn set_time(&mut self,time:Time,new_time:Time){
|
||||
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
|
||||
match self{
|
||||
Self::Paused(timer)=>timer.set_time(time,new_time),
|
||||
Self::Unpaused(timer)=>timer.set_time(time,new_time),
|
||||
}
|
||||
}
|
||||
pub fn pause(&mut self,time:Time)->Result<(),Error>{
|
||||
pub fn pause(&mut self,time:Time<T::In>)->Result<(),Error>{
|
||||
*self=match *self{
|
||||
Self::Paused(_)=>return Err(Error::AlreadyPaused),
|
||||
Self::Unpaused(timer)=>Self::Paused(timer.into_paused(time)),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub fn unpause(&mut self,time:Time)->Result<(),Error>{
|
||||
pub fn unpause(&mut self,time:Time<T::In>)->Result<(),Error>{
|
||||
*self=match *self{
|
||||
Self::Paused(timer)=>Self::Unpaused(timer.into_unpaused(time)),
|
||||
Self::Unpaused(_)=>return Err(Error::AlreadyUnpaused),
|
||||
@ -249,7 +286,7 @@ impl<T:TimerState> Timer<T>{
|
||||
Self::Unpaused(_)=>false,
|
||||
}
|
||||
}
|
||||
pub fn set_paused(&mut self,time:Time,paused:bool)->Result<(),Error>{
|
||||
pub fn set_paused(&mut self,time:Time<T::In>,paused:bool)->Result<(),Error>{
|
||||
match paused{
|
||||
true=>self.pause(time),
|
||||
false=>self.unpause(time),
|
||||
@ -257,14 +294,16 @@ impl<T:TimerState> Timer<T>{
|
||||
}
|
||||
}
|
||||
//scaled timer methods are generic across PauseState
|
||||
impl Timer<Scaled>{
|
||||
impl<In,Out> Timer<Scaled<In,Out>>
|
||||
where Time<In>:Copy,
|
||||
{
|
||||
pub const fn get_scale(&self)->Ratio64{
|
||||
match self{
|
||||
Self::Paused(timer)=>timer.get_scale(),
|
||||
Self::Unpaused(timer)=>timer.get_scale(),
|
||||
}
|
||||
}
|
||||
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
|
||||
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
|
||||
match self{
|
||||
Self::Paused(timer)=>timer.set_scale(time,new_scale),
|
||||
Self::Unpaused(timer)=>timer.set_scale(time,new_scale),
|
||||
@ -280,12 +319,17 @@ mod test{
|
||||
Time::from_secs($s)
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
enum Parent{}
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
|
||||
enum Calculated{}
|
||||
#[test]
|
||||
fn test_timerfixed_scaled(){
|
||||
//create a paused timer that reads 0s
|
||||
let timer=TimerFixed::<Scaled,Paused>::from_state(Scaled{scale:0.5f32.try_into().unwrap(),offset:sec!(0)});
|
||||
let timer=TimerFixed::<Scaled<Parent,Calculated>,Paused>::from_state(Scaled::new(0.5f32.try_into().unwrap(),sec!(0)));
|
||||
//the paused timer at 1 second should read 0s
|
||||
assert_eq!(timer.time(sec!(1)),sec!(0));
|
||||
assert_eq!(timer.time(),sec!(0));
|
||||
|
||||
//unpause it after one second
|
||||
let timer=timer.into_unpaused(sec!(1));
|
||||
@ -295,12 +339,12 @@ mod test{
|
||||
//pause the timer after 11 seconds
|
||||
let timer=timer.into_paused(sec!(11));
|
||||
//the paused timer at 20 seconds should read 5s
|
||||
assert_eq!(timer.time(sec!(20)),sec!(5));
|
||||
assert_eq!(timer.time(),sec!(5));
|
||||
}
|
||||
#[test]
|
||||
fn test_timer()->Result<(),Error>{
|
||||
//create a paused timer that reads 0s
|
||||
let mut timer=Timer::<Realtime>::paused(sec!(0),sec!(0));
|
||||
let mut timer=Timer::<Realtime<Parent,Calculated>>::paused(sec!(0),sec!(0));
|
||||
//the paused timer at 1 second should read 0s
|
||||
assert_eq!(timer.time(sec!(1)),sec!(0));
|
||||
|
||||
|
@ -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 }
|
||||
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
|
||||
|
116
lib/deferred_loader/src/deferred_loader.rs
Normal file
116
lib/deferred_loader/src/deferred_loader.rs
Normal file
@ -0,0 +1,116 @@
|
||||
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};
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
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_indices(self)->impl Iterator<Item=H>{
|
||||
self.render_config_id_from_asset_id.into_keys().flatten()
|
||||
}
|
||||
pub fn into_render_configs<L:Loader<Resource=Texture,Index=H>>(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 texture: {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_indices(self)->impl Iterator<Item=H>{
|
||||
self.mesh_id_from_asset_id.into_keys()
|
||||
}
|
||||
pub fn into_meshes<L:Loader<Resource=Mesh,Index=H>>(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 mesh: {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))
|
||||
)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fixed_wide"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -17,4 +17,4 @@ zeroes=["dep:arrayvec"]
|
||||
bnum = "0.12.0"
|
||||
arrayvec = { version = "0.7.6", optional = true }
|
||||
paste = "1.0.15"
|
||||
ratio_ops = { path = "../ratio_ops", registry = "strafesnet", optional = true }
|
||||
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
|
||||
|
@ -1,6 +1,6 @@
|
||||
use bnum::{BInt,cast::As};
|
||||
|
||||
#[derive(Clone,Copy,Debug,Default,Hash)]
|
||||
#[derive(Clone,Copy,Debug,Default,Hash,PartialEq,Eq,PartialOrd,Ord)]
|
||||
/// A Fixed point number for which multiply operations widen the bits in the output. (when the wide-mul feature is enabled)
|
||||
/// N is the number of u64s to use
|
||||
/// F is the number of fractional bits (always N*32 lol)
|
||||
@ -87,12 +87,6 @@ impl_from!(
|
||||
i8,i16,i32,i64,i128,isize
|
||||
);
|
||||
|
||||
impl<const N:usize,const F:usize> PartialEq for Fixed<N,F>{
|
||||
#[inline]
|
||||
fn eq(&self,other:&Self)->bool{
|
||||
self.bits.eq(&other.bits)
|
||||
}
|
||||
}
|
||||
impl<const N:usize,const F:usize,T> PartialEq<T> for Fixed<N,F>
|
||||
where
|
||||
T:Copy,
|
||||
@ -103,14 +97,7 @@ where
|
||||
self.bits.eq(&other.into())
|
||||
}
|
||||
}
|
||||
impl<const N:usize,const F:usize> Eq for Fixed<N,F>{}
|
||||
|
||||
impl<const N:usize,const F:usize> PartialOrd for Fixed<N,F>{
|
||||
#[inline]
|
||||
fn partial_cmp(&self,other:&Self)->Option<std::cmp::Ordering>{
|
||||
self.bits.partial_cmp(&other.bits)
|
||||
}
|
||||
}
|
||||
impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
|
||||
where
|
||||
T:Copy,
|
||||
@ -121,12 +108,6 @@ impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
|
||||
self.bits.partial_cmp(&other.into())
|
||||
}
|
||||
}
|
||||
impl<const N:usize,const F:usize> Ord for Fixed<N,F>{
|
||||
#[inline]
|
||||
fn cmp(&self,other:&Self)->std::cmp::Ordering{
|
||||
self.bits.cmp(&other.bits)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N:usize,const F:usize> std::ops::Neg for Fixed<N,F>{
|
||||
type Output=Self;
|
||||
|
@ -57,7 +57,7 @@ fn from_f32(){
|
||||
assert_eq!(b,Ok(a));
|
||||
//I32F32::MIN hits a special case since it's not representable as a positive signed integer
|
||||
//TODO: don't return an overflow because this is technically possible
|
||||
let a=I32F32::MIN;
|
||||
let _a=I32F32::MIN;
|
||||
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into();
|
||||
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
|
||||
//16 is within the 24 bits of float precision
|
||||
|
@ -14,8 +14,8 @@ fixed-wide=["dep:fixed_wide","dep:paste"]
|
||||
deferred-division=["dep:ratio_ops"]
|
||||
|
||||
[dependencies]
|
||||
ratio_ops = { path = "../ratio_ops", registry = "strafesnet", optional = true }
|
||||
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", optional = true }
|
||||
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
|
||||
fixed_wide = { version = "0.1.2", path = "../fixed_wide", registry = "strafesnet", optional = true }
|
||||
paste = { version = "1.0.15", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_rbx_loader"
|
||||
version = "0.5.2"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -15,8 +15,10 @@ glam = "0.29.0"
|
||||
lazy-regex = "3.1.0"
|
||||
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
|
||||
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
|
||||
rbx_mesh = "0.1.2"
|
||||
rbx_mesh = "0.3.1"
|
||||
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
|
||||
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
|
||||
roblox_emulator = { path = "../roblox_emulator", registry = "strafesnet" }
|
||||
strafesnet_common = { path = "../common", registry = "strafesnet" }
|
||||
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
|
||||
roblox_emulator = { version = "0.4.7", path = "../roblox_emulator", registry = "strafesnet" }
|
||||
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
|
||||
|
@ -1,8 +1,11 @@
|
||||
use std::io::Read;
|
||||
use rbx_dom_weak::WeakDom;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
mod rbx;
|
||||
mod mesh;
|
||||
mod union;
|
||||
pub mod loader;
|
||||
mod primitives;
|
||||
|
||||
pub mod data{
|
||||
@ -30,6 +33,9 @@ impl Model{
|
||||
let services=context.convert_into_place();
|
||||
Place{dom,services}
|
||||
}
|
||||
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
|
||||
to_snf(self,failure_mode)
|
||||
}
|
||||
}
|
||||
impl AsRef<WeakDom> for Model{
|
||||
fn as_ref(&self)->&WeakDom{
|
||||
@ -42,7 +48,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()?,
|
||||
@ -61,6 +67,9 @@ impl Place{
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
|
||||
to_snf(self,failure_mode)
|
||||
}
|
||||
}
|
||||
impl AsRef<WeakDom> for Place{
|
||||
fn as_ref(&self)->&WeakDom{
|
||||
@ -92,16 +101,49 @@ 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)
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError{
|
||||
Texture(loader::TextureError),
|
||||
Mesh(loader::MeshError),
|
||||
}
|
||||
impl std::fmt::Display for LoadError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LoadError{}
|
||||
impl From<loader::TextureError> for LoadError{
|
||||
fn from(value:loader::TextureError)->Self{
|
||||
Self::Texture(value)
|
||||
}
|
||||
}
|
||||
impl From<loader::MeshError> for LoadError{
|
||||
fn from(value:loader::MeshError)->Self{
|
||||
Self::Mesh(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
|
||||
let dom=dom.as_ref();
|
||||
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
|
||||
let map_step1=rbx::convert(
|
||||
dom,
|
||||
&mut texture_deferred_loader,
|
||||
&mut mesh_deferred_loader,
|
||||
);
|
||||
|
||||
let mut mesh_loader=loader::MeshLoader::new();
|
||||
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
|
||||
|
||||
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
|
||||
|
||||
let mut texture_loader=loader::TextureLoader::new();
|
||||
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
|
||||
|
||||
let map=map_step2.add_render_configs_and_textures(render_configs);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
191
lib/rbx_loader/src/loader.rs
Normal file
191
lib/rbx_loader/src/loader.rs
Normal file
@ -0,0 +1,191 @@
|
||||
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;
|
||||
use crate::rbx::RobloxFaceTextureDescription;
|
||||
|
||||
fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<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(data)
|
||||
}
|
||||
|
||||
#[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 data=read_entire_file(file_name)?;
|
||||
Ok(Texture::ImageDDS(data))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum MeshError{
|
||||
Io(std::io::Error),
|
||||
RobloxAssetIdParse(RobloxAssetIdParseErr),
|
||||
Mesh(crate::mesh::Error),
|
||||
Union(crate::union::Error),
|
||||
DecodeBinary(rbx_binary::DecodeError),
|
||||
OneChildPolicy,
|
||||
MissingInstance,
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
impl From<crate::union::Error> for MeshError{
|
||||
fn from(value:crate::union::Error)->Self{
|
||||
Self::Union(value)
|
||||
}
|
||||
}
|
||||
impl From<rbx_binary::DecodeError> for MeshError{
|
||||
fn from(value:rbx_binary::DecodeError)->Self{
|
||||
Self::DecodeBinary(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
pub enum MeshType<'a>{
|
||||
FileMesh,
|
||||
Union{
|
||||
mesh_data:&'a [u8],
|
||||
physics_data:&'a [u8],
|
||||
size_float_bits:[u32;3],
|
||||
part_texture_description:[Option<RobloxFaceTextureDescription>;6],
|
||||
},
|
||||
}
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
pub struct MeshIndex<'a>{
|
||||
mesh_type:MeshType<'a>,
|
||||
content:&'a str,
|
||||
}
|
||||
impl MeshIndex<'_>{
|
||||
pub fn file_mesh(content:&str)->MeshIndex{
|
||||
MeshIndex{
|
||||
mesh_type:MeshType::FileMesh,
|
||||
content,
|
||||
}
|
||||
}
|
||||
pub fn union<'a>(
|
||||
content:&'a str,
|
||||
mesh_data:&'a [u8],
|
||||
physics_data:&'a [u8],
|
||||
size:&rbx_dom_weak::types::Vector3,
|
||||
part_texture_description:crate::rbx::RobloxPartDescription,
|
||||
)->MeshIndex<'a>{
|
||||
MeshIndex{
|
||||
mesh_type:MeshType::Union{
|
||||
mesh_data,
|
||||
physics_data,
|
||||
size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
|
||||
part_texture_description,
|
||||
},
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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=MeshIndex<'a>;
|
||||
type Resource=Mesh;
|
||||
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
|
||||
let mesh=match index.mesh_type{
|
||||
MeshType::FileMesh=>{
|
||||
let RobloxAssetId(asset_id)=index.content.parse()?;
|
||||
let file_name=format!("meshes/{}",asset_id);
|
||||
let data=read_entire_file(file_name)?;
|
||||
crate::mesh::convert(RobloxMeshBytes::new(data))?
|
||||
},
|
||||
MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{
|
||||
// decode asset
|
||||
let size=glam::Vec3::from_array(size_float_bits.map(f32::from_bits));
|
||||
if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){
|
||||
let RobloxAssetId(asset_id)=index.content.parse()?;
|
||||
let file_name=format!("unions/{}",asset_id);
|
||||
let data=read_entire_file(file_name)?;
|
||||
let dom=rbx_binary::from_reader(std::io::Cursor::new(data))?;
|
||||
let &[referent]=dom.root().children()else{
|
||||
return Err(MeshError::OneChildPolicy);
|
||||
};
|
||||
let Some(instance)=dom.get_by_ref(referent)else{
|
||||
return Err(MeshError::MissingInstance);
|
||||
};
|
||||
if physics_data.is_empty(){
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("PhysicsData"){
|
||||
physics_data=data.as_ref();
|
||||
}
|
||||
}
|
||||
if mesh_data.is_empty(){
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("MeshData"){
|
||||
mesh_data=data.as_ref();
|
||||
}
|
||||
}
|
||||
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
}else{
|
||||
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(mesh)
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
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}};
|
||||
use strafesnet_common::{integer::vec3,model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId}};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
|
||||
@ -204,7 +205,13 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Me
|
||||
unique_vertices,
|
||||
polygon_groups,
|
||||
//these should probably be moved to the model...
|
||||
graphics_groups:Vec::new(),
|
||||
//but what if models want to use the same texture
|
||||
graphics_groups:vec![model::IndexedGraphicsGroup{
|
||||
render:RenderConfigId::new(0),
|
||||
//the lowest lod is highest quality
|
||||
groups:vec![model::PolygonGroupId::new(0)]
|
||||
}],
|
||||
//disable physics
|
||||
physics_groups:Vec::new(),
|
||||
})
|
||||
}
|
||||
|
@ -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)]
|
||||
@ -42,6 +42,121 @@ const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
|
||||
vec3::int( 0,-1, 0),//CubeFace::Bottom
|
||||
vec3::int( 0, 0,-1),//CubeFace::Front
|
||||
];
|
||||
|
||||
#[derive(Hash,PartialEq,Eq)]
|
||||
pub enum WedgeFace{
|
||||
Right,
|
||||
TopFront,
|
||||
Back,
|
||||
Left,
|
||||
Bottom,
|
||||
}
|
||||
const WEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
|
||||
vec3::int( 1, 0, 0),//Wedge::Right
|
||||
vec3::int( 0, 1,-1),//Wedge::TopFront
|
||||
vec3::int( 0, 0, 1),//Wedge::Back
|
||||
vec3::int(-1, 0, 0),//Wedge::Left
|
||||
vec3::int( 0,-1, 0),//Wedge::Bottom
|
||||
];
|
||||
/*
|
||||
local cornerWedgeVerticies = {
|
||||
Vector3.new(-1/2,-1/2,-1/2),7
|
||||
Vector3.new(-1/2,-1/2, 1/2),0
|
||||
Vector3.new( 1/2,-1/2,-1/2),6
|
||||
Vector3.new( 1/2,-1/2, 1/2),1
|
||||
Vector3.new( 1/2, 1/2,-1/2),5
|
||||
}
|
||||
*/
|
||||
#[derive(Hash,PartialEq,Eq)]
|
||||
pub enum CornerWedgeFace{
|
||||
Right,
|
||||
TopBack,
|
||||
TopLeft,
|
||||
Bottom,
|
||||
Front,
|
||||
}
|
||||
const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
|
||||
vec3::int( 1, 0, 0),//CornerWedge::Right
|
||||
vec3::int( 0, 1, 1),//CornerWedge::BackTop
|
||||
vec3::int(-1, 1, 0),//CornerWedge::LeftTop
|
||||
vec3::int( 0,-1, 0),//CornerWedge::Bottom
|
||||
vec3::int( 0, 0,-1),//CornerWedge::Front
|
||||
];
|
||||
#[derive(Default)]
|
||||
pub struct CubeFaceDescription([Option<FaceDescription>;6]);
|
||||
impl CubeFaceDescription{
|
||||
pub fn insert(&mut self,index:CubeFace,value:FaceDescription){
|
||||
self.0[index as usize]=Some(value);
|
||||
}
|
||||
pub fn pairs(self)->impl Iterator<Item=(usize,FaceDescription)>{
|
||||
self.0.into_iter().enumerate().filter_map(|(i,v)|v.map(|u|(i,u)))
|
||||
}
|
||||
}
|
||||
pub fn unit_cube(render:RenderConfigId)->Mesh{
|
||||
let mut t=CubeFaceDescription::default();
|
||||
t.insert(CubeFace::Right,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Top,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Back,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Left,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Bottom,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Front,FaceDescription::new_with_render_id(render));
|
||||
generate_partial_unit_cube(t)
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct WedgeFaceDescription([Option<FaceDescription>;5]);
|
||||
impl WedgeFaceDescription{
|
||||
pub fn insert(&mut self,index:WedgeFace,value:FaceDescription){
|
||||
self.0[index as usize]=Some(value);
|
||||
}
|
||||
pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
|
||||
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)
|
||||
// }
|
||||
#[derive(Default)]
|
||||
pub struct CornerWedgeFaceDescription([Option<FaceDescription>;5]);
|
||||
impl CornerWedgeFaceDescription{
|
||||
pub fn insert(&mut self,index:CornerWedgeFace,value:FaceDescription){
|
||||
self.0[index as usize]=Some(value);
|
||||
}
|
||||
pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
|
||||
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)
|
||||
// }
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FaceDescription{
|
||||
pub render:RenderConfigId,
|
||||
pub transform:glam::Affine2,
|
||||
pub color:Color4,
|
||||
}
|
||||
impl FaceDescription{
|
||||
pub fn new_with_render_id(render:RenderConfigId)->Self {
|
||||
Self{
|
||||
render,
|
||||
transform:glam::Affine2::IDENTITY,
|
||||
color:Color4::new(1.0,1.0,1.0,0.0),//zero alpha to hide the default texture
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[
|
||||
// right (1, 0, 0)
|
||||
[
|
||||
@ -86,128 +201,6 @@ const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[
|
||||
[7,2,5],
|
||||
],
|
||||
];
|
||||
|
||||
#[derive(Hash,PartialEq,Eq)]
|
||||
pub enum WedgeFace{
|
||||
Right,
|
||||
TopFront,
|
||||
Back,
|
||||
Left,
|
||||
Bottom,
|
||||
}
|
||||
const WEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
|
||||
vec3::int( 1, 0, 0),//Wedge::Right
|
||||
vec3::int( 0, 1,-1),//Wedge::TopFront
|
||||
vec3::int( 0, 0, 1),//Wedge::Back
|
||||
vec3::int(-1, 0, 0),//Wedge::Left
|
||||
vec3::int( 0,-1, 0),//Wedge::Bottom
|
||||
];
|
||||
/*
|
||||
local cornerWedgeVerticies = {
|
||||
Vector3.new(-1/2,-1/2,-1/2),7
|
||||
Vector3.new(-1/2,-1/2, 1/2),0
|
||||
Vector3.new( 1/2,-1/2,-1/2),6
|
||||
Vector3.new( 1/2,-1/2, 1/2),1
|
||||
Vector3.new( 1/2, 1/2,-1/2),5
|
||||
}
|
||||
*/
|
||||
#[derive(Hash,PartialEq,Eq)]
|
||||
pub enum CornerWedgeFace{
|
||||
Right,
|
||||
TopBack,
|
||||
TopLeft,
|
||||
Bottom,
|
||||
Front,
|
||||
}
|
||||
const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
|
||||
vec3::int( 1, 0, 0),//CornerWedge::Right
|
||||
vec3::int( 0, 1, 1),//CornerWedge::BackTop
|
||||
vec3::int(-1, 1, 0),//CornerWedge::LeftTop
|
||||
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{
|
||||
pub fn insert(&mut self,index:CubeFace,value:FaceDescription){
|
||||
self.0[index as usize]=Some(value);
|
||||
}
|
||||
pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,6>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
|
||||
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
|
||||
}
|
||||
}
|
||||
pub fn unit_cube(render:RenderConfigId)->Mesh{
|
||||
let mut t=CubeFaceDescription::default();
|
||||
t.insert(CubeFace::Right,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Top,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Back,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Left,FaceDescription::new_with_render_id(render));
|
||||
t.insert(CubeFace::Bottom,FaceDescription::new_with_render_id(render));
|
||||
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{
|
||||
pub fn insert(&mut self,index:WedgeFace,value:FaceDescription){
|
||||
self.0[index as usize]=Some(value);
|
||||
}
|
||||
pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
|
||||
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)
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct CornerWedgeFaceDescription([Option<FaceDescription>;5]);
|
||||
impl CornerWedgeFaceDescription{
|
||||
pub fn insert(&mut self,index:CornerWedgeFace,value:FaceDescription){
|
||||
self.0[index as usize]=Some(value);
|
||||
}
|
||||
pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
|
||||
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)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FaceDescription{
|
||||
pub render:RenderConfigId,
|
||||
pub transform:glam::Affine2,
|
||||
pub color:Color4,
|
||||
}
|
||||
impl FaceDescription{
|
||||
pub fn new_with_render_id(render:RenderConfigId)->Self {
|
||||
Self{
|
||||
render,
|
||||
transform:glam::Affine2::IDENTITY,
|
||||
color:Color4::new(1.0,1.0,1.0,0.0),//zero alpha to hide the default texture
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
let mut generated_pos=Vec::new();
|
||||
let mut generated_tex=Vec::new();
|
||||
let mut generated_normal=Vec::new();
|
||||
@ -286,35 +279,35 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
}
|
||||
//don't think too hard about the copy paste because this is all going into the map tool eventually...
|
||||
pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh{
|
||||
let wedge_default_polys=[
|
||||
const WEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
|
||||
// right (1, 0, 0)
|
||||
vec![
|
||||
&[
|
||||
[6,2,0],//[vertex,tex,norm]
|
||||
[2,0,0],
|
||||
[1,3,0],
|
||||
],
|
||||
// FrontTop (0, 1, -1)
|
||||
vec![
|
||||
&[
|
||||
[3,1,1],
|
||||
[2,0,1],
|
||||
[6,3,1],
|
||||
[7,2,1],
|
||||
],
|
||||
// back (0, 0, 1)
|
||||
vec![
|
||||
&[
|
||||
[0,3,2],
|
||||
[1,2,2],
|
||||
[2,1,2],
|
||||
[3,0,2],
|
||||
],
|
||||
// left (-1, 0, 0)
|
||||
vec![
|
||||
&[
|
||||
[0,2,3],
|
||||
[3,1,3],
|
||||
[7,3,3],
|
||||
],
|
||||
// bottom (0,-1, 0)
|
||||
vec![
|
||||
&[
|
||||
[1,1,4],
|
||||
[0,0,4],
|
||||
[7,3,4],
|
||||
@ -358,7 +351,7 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
|
||||
//push vertices as they are needed
|
||||
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
|
||||
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
|
||||
wedge_default_polys[face_id].iter().map(|tup|{
|
||||
WEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
|
||||
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
|
||||
pos_index
|
||||
@ -399,34 +392,34 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
|
||||
}
|
||||
|
||||
pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{
|
||||
let cornerwedge_default_polys=[
|
||||
const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
|
||||
// right (1, 0, 0)
|
||||
vec![
|
||||
&[
|
||||
[6,2,0],//[vertex,tex,norm]
|
||||
[5,1,0],
|
||||
[1,3,0],
|
||||
],
|
||||
// BackTop (0, 1, 1)
|
||||
vec![
|
||||
&[
|
||||
[5,3,1],
|
||||
[0,1,1],
|
||||
[1,0,1],
|
||||
],
|
||||
// LeftTop (-1, 1, 0)
|
||||
vec![
|
||||
&[
|
||||
[5,3,2],
|
||||
[7,2,2],
|
||||
[0,1,2],
|
||||
],
|
||||
// bottom (0,-1, 0)
|
||||
vec![
|
||||
&[
|
||||
[1,1,3],
|
||||
[0,0,3],
|
||||
[7,3,3],
|
||||
[6,2,3],
|
||||
],
|
||||
// front (0, 0,-1)
|
||||
vec![
|
||||
&[
|
||||
[5,0,4],
|
||||
[6,3,4],
|
||||
[7,2,4],
|
||||
@ -469,7 +462,7 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
|
||||
//push vertices as they are needed
|
||||
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
|
||||
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
|
||||
cornerwedge_default_polys[face_id].iter().map(|tup|{
|
||||
CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
|
||||
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
|
||||
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
|
||||
pos_index
|
||||
|
@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::loader::MeshIndex;
|
||||
use crate::primitives;
|
||||
use strafesnet_common::aabb::Aabb;
|
||||
use strafesnet_common::map;
|
||||
use strafesnet_common::model;
|
||||
use strafesnet_common::gameplay_modes;
|
||||
@ -8,6 +10,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 +135,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();
|
||||
@ -341,58 +346,103 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,PartialEq)]
|
||||
struct RobloxTextureTransform{
|
||||
offset_u:f32,
|
||||
offset_v:f32,
|
||||
scale_u:f32,
|
||||
scale_v:f32,
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct RobloxTextureTransform{
|
||||
offset_studs_u:f32,
|
||||
offset_studs_v:f32,
|
||||
studs_per_tile_u:f32,
|
||||
studs_per_tile_v:f32,
|
||||
size_u:f32,
|
||||
size_v:f32,
|
||||
}
|
||||
impl std::cmp::Eq for RobloxTextureTransform{}//????
|
||||
impl std::default::Default for RobloxTextureTransform{
|
||||
fn default()->Self{
|
||||
Self{offset_u:0.0,offset_v:0.0,scale_u:1.0,scale_v:1.0}
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
|
||||
pub struct RobloxTextureTransformBits{
|
||||
offset_studs_u:u32,
|
||||
offset_studs_v:u32,
|
||||
studs_per_tile_u:u32,
|
||||
studs_per_tile_v:u32,
|
||||
size_u:u32,
|
||||
size_v:u32,
|
||||
}
|
||||
impl RobloxTextureTransform{
|
||||
fn identity()->Self{
|
||||
Self{
|
||||
offset_studs_u:0.0,
|
||||
offset_studs_v:0.0,
|
||||
studs_per_tile_u:1.0,
|
||||
studs_per_tile_v:1.0,
|
||||
size_u:1.0,
|
||||
size_v:1.0,
|
||||
}
|
||||
}
|
||||
impl std::hash::Hash for RobloxTextureTransform{
|
||||
fn hash<H:std::hash::Hasher>(&self,state:&mut H) {
|
||||
self.offset_u.to_ne_bytes().hash(state);
|
||||
self.offset_v.to_ne_bytes().hash(state);
|
||||
self.scale_u.to_ne_bytes().hash(state);
|
||||
self.scale_v.to_ne_bytes().hash(state);
|
||||
pub fn to_bits(self)->RobloxTextureTransformBits{
|
||||
RobloxTextureTransformBits{
|
||||
offset_studs_u:self.offset_studs_u.to_bits(),
|
||||
offset_studs_v:self.offset_studs_v.to_bits(),
|
||||
studs_per_tile_u:self.studs_per_tile_u.to_bits(),
|
||||
studs_per_tile_v:self.studs_per_tile_v.to_bits(),
|
||||
size_u:self.size_u.to_bits(),
|
||||
size_v:self.size_v.to_bits(),
|
||||
}
|
||||
}
|
||||
#[derive(Clone,PartialEq)]
|
||||
struct RobloxFaceTextureDescription{
|
||||
pub fn affine(&self)->glam::Affine2{
|
||||
glam::Affine2::from_translation(
|
||||
glam::vec2(self.offset_studs_u/self.studs_per_tile_u,self.offset_studs_v/self.studs_per_tile_v)
|
||||
)
|
||||
*glam::Affine2::from_scale(
|
||||
glam::vec2(self.size_u/self.studs_per_tile_u,self.size_v/self.studs_per_tile_v)
|
||||
)
|
||||
}
|
||||
pub fn set_size(&mut self,size_u:f32,size_v:f32){
|
||||
self.size_u=size_u;
|
||||
self.size_v=size_v;
|
||||
}
|
||||
}
|
||||
impl core::hash::Hash for RobloxTextureTransform{
|
||||
fn hash<H:core::hash::Hasher>(&self,state:&mut H){
|
||||
self.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
|
||||
pub struct RobloxFaceTextureDescriptionBits{
|
||||
render:RenderConfigId,
|
||||
color:glam::Vec4,
|
||||
transform:RobloxTextureTransform,
|
||||
color:[u32;4],
|
||||
transform:RobloxTextureTransformBits,
|
||||
}
|
||||
impl std::cmp::Eq for RobloxFaceTextureDescription{}//????
|
||||
impl std::hash::Hash for RobloxFaceTextureDescription{
|
||||
fn hash<H:std::hash::Hasher>(&self,state:&mut H){
|
||||
self.render.hash(state);
|
||||
self.transform.hash(state);
|
||||
for &el in self.color.as_ref().iter(){
|
||||
el.to_ne_bytes().hash(state);
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct RobloxFaceTextureDescription{
|
||||
pub render:RenderConfigId,
|
||||
pub color:glam::Vec4,
|
||||
pub transform:RobloxTextureTransform,
|
||||
}
|
||||
impl core::cmp::PartialEq for RobloxFaceTextureDescription{
|
||||
fn eq(&self,other:&Self)->bool{
|
||||
self.to_bits().eq(&other.to_bits())
|
||||
}
|
||||
}
|
||||
impl core::cmp::Eq for RobloxFaceTextureDescription{}
|
||||
impl core::hash::Hash for RobloxFaceTextureDescription{
|
||||
fn hash<H:core::hash::Hasher>(&self,state:&mut H){
|
||||
self.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
impl RobloxFaceTextureDescription{
|
||||
fn to_face_description(&self)->primitives::FaceDescription{
|
||||
pub fn to_bits(self)->RobloxFaceTextureDescriptionBits{
|
||||
RobloxFaceTextureDescriptionBits{
|
||||
render:self.render,
|
||||
color:self.color.to_array().map(f32::to_bits),
|
||||
transform:self.transform.to_bits(),
|
||||
}
|
||||
}
|
||||
pub fn to_face_description(&self)->primitives::FaceDescription{
|
||||
primitives::FaceDescription{
|
||||
render:self.render,
|
||||
transform:glam::Affine2::from_translation(
|
||||
glam::vec2(self.transform.offset_u,self.transform.offset_v)
|
||||
)
|
||||
*glam::Affine2::from_scale(
|
||||
glam::vec2(self.transform.scale_u,self.transform.scale_v)
|
||||
),
|
||||
transform:self.transform.affine(),
|
||||
color:self.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6];
|
||||
pub type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6];
|
||||
type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
|
||||
type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
|
||||
#[derive(Clone,Eq,Hash,PartialEq)]
|
||||
@ -403,52 +453,134 @@ enum RobloxBasePartDescription{
|
||||
Wedge(RobloxWedgeDescription),
|
||||
CornerWedge(RobloxCornerWedgeDescription),
|
||||
}
|
||||
fn get_texture_description<'a>(
|
||||
temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
|
||||
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
|
||||
dom:&'a rbx_dom_weak::WeakDom,
|
||||
object:&rbx_dom_weak::Instance,
|
||||
size:&rbx_dom_weak::types::Vector3,
|
||||
)->RobloxPartDescription{
|
||||
//use the biggest one and cut it down later...
|
||||
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
|
||||
temp_objects.clear();
|
||||
recursive_collect_superclass(temp_objects,&dom,object,"Decal");
|
||||
for &mut decal_ref in temp_objects{
|
||||
if let Some(decal)=dom.get_by_ref(decal_ref){
|
||||
if let (
|
||||
Some(rbx_dom_weak::types::Variant::Content(content)),
|
||||
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
|
||||
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
|
||||
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
|
||||
) = (
|
||||
decal.properties.get("Texture"),
|
||||
decal.properties.get("Face"),
|
||||
decal.properties.get("Color3"),
|
||||
decal.properties.get("Transparency"),
|
||||
) {
|
||||
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"{
|
||||
//generate tranform
|
||||
if let (
|
||||
Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_u)),
|
||||
Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_v)),
|
||||
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)),
|
||||
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)),
|
||||
) = (
|
||||
decal.properties.get("OffsetStudsU"),
|
||||
decal.properties.get("OffsetStudsV"),
|
||||
decal.properties.get("StudsPerTileU"),
|
||||
decal.properties.get("StudsPerTileV"),
|
||||
)
|
||||
{
|
||||
let (size_u,size_v)=match normal_id{
|
||||
0=>(size.z,size.y),//right
|
||||
1=>(size.x,size.z),//top
|
||||
2=>(size.x,size.y),//back
|
||||
3=>(size.z,size.y),//left
|
||||
4=>(size.x,size.z),//bottom
|
||||
5=>(size.x,size.y),//front
|
||||
_=>unreachable!(),
|
||||
};
|
||||
(
|
||||
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
|
||||
RobloxTextureTransform{
|
||||
offset_studs_u,
|
||||
offset_studs_v,
|
||||
studs_per_tile_u,
|
||||
studs_per_tile_v,
|
||||
size_u,
|
||||
size_v,
|
||||
}
|
||||
)
|
||||
}else{
|
||||
(glam::Vec4::ONE,RobloxTextureTransform::identity())
|
||||
}
|
||||
}else{
|
||||
(glam::Vec4::ONE,RobloxTextureTransform::identity())
|
||||
};
|
||||
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
|
||||
render:render_id,
|
||||
color:roblox_texture_color,
|
||||
transform:roblox_texture_transform,
|
||||
});
|
||||
}else{
|
||||
println!("NormalId={} is invalid",normal_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
part_texture_description
|
||||
}
|
||||
enum Shape{
|
||||
Primitive(primitives::Primitives),
|
||||
MeshPart,
|
||||
PhysicsData,
|
||||
}
|
||||
enum MeshAvailability{
|
||||
Immediate,
|
||||
Deferred(RenderConfigId),
|
||||
DeferredMesh(RenderConfigId),
|
||||
DeferredUnion(RobloxPartDescription),
|
||||
}
|
||||
struct DeferredModelDeferredAttributes{
|
||||
struct DeferredModelDeferredAttributes<'a>{
|
||||
render:RenderConfigId,
|
||||
model:ModelDeferredAttributes,
|
||||
model:ModelDeferredAttributes<'a>,
|
||||
}
|
||||
struct ModelDeferredAttributes{
|
||||
struct ModelDeferredAttributes<'a>{
|
||||
mesh:model::MeshId,
|
||||
deferred_attributes:GetAttributesArgs,
|
||||
deferred_attributes:GetAttributesArgs<'a>,
|
||||
color:model::Color4,//transparency is in here
|
||||
transform:Planar64Affine3,
|
||||
}
|
||||
struct DeferredUnionDeferredAttributes<'a>{
|
||||
render:RobloxPartDescription,
|
||||
model:ModelDeferredAttributes<'a>,
|
||||
}
|
||||
struct ModelOwnedAttributes{
|
||||
mesh:model::MeshId,
|
||||
attributes:attr::CollisionAttributes,
|
||||
color:model::Color4,//transparency is in here
|
||||
transform:Planar64Affine3,
|
||||
}
|
||||
struct GetAttributesArgs{
|
||||
name:Box<str>,
|
||||
struct GetAttributesArgs<'a>{
|
||||
name:&'a str,
|
||||
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<MeshIndex<'a>>,
|
||||
)->PartialMap1<'a>{
|
||||
let mut deferred_models_deferred_attributes=Vec::new();
|
||||
let mut deferred_unions_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 +617,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"){
|
||||
@ -506,6 +635,7 @@ where
|
||||
"WedgePart"=>Shape::Primitive(primitives::Primitives::Wedge),
|
||||
"CornerWedgePart"=>Shape::Primitive(primitives::Primitives::CornerWedge),
|
||||
"MeshPart"=>Shape::MeshPart,
|
||||
"UnionOperation"=>Shape::PhysicsData,
|
||||
_=>{
|
||||
println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class);
|
||||
Shape::Primitive(primitives::Primitives::Cube)
|
||||
@ -515,73 +645,7 @@ where
|
||||
let (availability,mesh_id)=match shape{
|
||||
Shape::Primitive(primitive_shape)=>{
|
||||
//TODO: TAB TAB
|
||||
//use the biggest one and cut it down later...
|
||||
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
|
||||
temp_objects.clear();
|
||||
recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal");
|
||||
for &decal_ref in &temp_objects{
|
||||
if let Some(decal)=dom.get_by_ref(decal_ref){
|
||||
if let (
|
||||
Some(rbx_dom_weak::types::Variant::Content(content)),
|
||||
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
|
||||
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
|
||||
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
|
||||
) = (
|
||||
decal.properties.get("Texture"),
|
||||
decal.properties.get("Face"),
|
||||
decal.properties.get("Color3"),
|
||||
decal.properties.get("Transparency"),
|
||||
) {
|
||||
let render_id=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"{
|
||||
//generate tranform
|
||||
if let (
|
||||
Some(rbx_dom_weak::types::Variant::Float32(ox)),
|
||||
Some(rbx_dom_weak::types::Variant::Float32(oy)),
|
||||
Some(rbx_dom_weak::types::Variant::Float32(sx)),
|
||||
Some(rbx_dom_weak::types::Variant::Float32(sy)),
|
||||
) = (
|
||||
decal.properties.get("OffsetStudsU"),
|
||||
decal.properties.get("OffsetStudsV"),
|
||||
decal.properties.get("StudsPerTileU"),
|
||||
decal.properties.get("StudsPerTileV"),
|
||||
)
|
||||
{
|
||||
let (size_u,size_v)=match normal_id{
|
||||
0=>(size.z,size.y),//right
|
||||
1=>(size.x,size.z),//top
|
||||
2=>(size.x,size.y),//back
|
||||
3=>(size.z,size.y),//left
|
||||
4=>(size.x,size.z),//bottom
|
||||
5=>(size.x,size.y),//front
|
||||
_=>unreachable!(),
|
||||
};
|
||||
(
|
||||
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
|
||||
RobloxTextureTransform{
|
||||
offset_u:*ox/(*sx),offset_v:*oy/(*sy),
|
||||
scale_u:size_u/(*sx),scale_v:size_v/(*sy),
|
||||
}
|
||||
)
|
||||
}else{
|
||||
(glam::Vec4::ONE,RobloxTextureTransform::default())
|
||||
}
|
||||
}else{
|
||||
(glam::Vec4::ONE,RobloxTextureTransform::default())
|
||||
};
|
||||
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
|
||||
render:render_id,
|
||||
color:roblox_texture_color,
|
||||
transform:roblox_texture_transform,
|
||||
});
|
||||
}else{
|
||||
println!("NormalId={} unsupported for shape={:?}",normal_id,primitive_shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
|
||||
//obscure rust syntax "slice pattern"
|
||||
let [
|
||||
f0,//Cube::Right
|
||||
@ -598,7 +662,7 @@ where
|
||||
//use front face texture first and use top face texture as a fallback
|
||||
primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([
|
||||
f0,//Cube::Right->Wedge::Right
|
||||
if f5.is_some(){f5}else{f1},//Cube::Front|Cube::Top->Wedge::TopFront
|
||||
f5.or(f1),//Cube::Front|Cube::Top->Wedge::TopFront
|
||||
f2,//Cube::Back->Wedge::Back
|
||||
f3,//Cube::Left->Wedge::Left
|
||||
f4,//Cube::Bottom->Wedge::Bottom
|
||||
@ -606,8 +670,8 @@ where
|
||||
//TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top
|
||||
primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([
|
||||
f0,//Cube::Right->CornerWedge::Right
|
||||
if f2.is_some(){f2}else{f1.clone()},//Cube::Back|Cube::Top->CornerWedge::TopBack
|
||||
if f3.is_some(){f3}else{f1},//Cube::Left|Cube::Top->CornerWedge::TopLeft
|
||||
f2.or(f1.clone()),//Cube::Back|Cube::Top->CornerWedge::TopBack
|
||||
f3.or(f1),//Cube::Left|Cube::Top->CornerWedge::TopLeft
|
||||
f4,//Cube::Bottom->CornerWedge::Bottom
|
||||
f5,//Cube::Front->CornerWedge::Front
|
||||
]),
|
||||
@ -694,29 +758,51 @@ 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::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(Some(texture_asset_id.as_ref()))),
|
||||
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id.as_ref())),
|
||||
)
|
||||
}else{
|
||||
panic!("Mesh has no Mesh or Texture");
|
||||
},
|
||||
Shape::PhysicsData=>{
|
||||
let mut content="";
|
||||
let mut mesh_data:&[u8]=&[];
|
||||
let mut physics_data:&[u8]=&[];
|
||||
if let Some(rbx_dom_weak::types::Variant::Content(asset_id))=object.properties.get("AssetId"){
|
||||
content=asset_id.as_ref();
|
||||
}
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("MeshData"){
|
||||
mesh_data=data.as_ref();
|
||||
}
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("PhysicsData"){
|
||||
physics_data=data.as_ref();
|
||||
}
|
||||
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
|
||||
let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
|
||||
let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
|
||||
(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
|
||||
},
|
||||
};
|
||||
let model_deferred_attributes=ModelDeferredAttributes{
|
||||
mesh:mesh_id,
|
||||
transform:model_transform,
|
||||
color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency),
|
||||
deferred_attributes:GetAttributesArgs{
|
||||
name:object.name.as_str().into(),
|
||||
name:object.name.as_str(),
|
||||
can_collide:*can_collide,
|
||||
velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(),
|
||||
},
|
||||
};
|
||||
match availability{
|
||||
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
|
||||
MeshAvailability::Deferred(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
|
||||
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
|
||||
render,
|
||||
model:model_deferred_attributes
|
||||
}),
|
||||
MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
|
||||
render:part_texture_description,
|
||||
model:model_deferred_attributes,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -725,21 +811,71 @@ where
|
||||
primitive_meshes,
|
||||
primitive_models_deferred_attributes,
|
||||
deferred_models_deferred_attributes,
|
||||
deferred_unions_deferred_attributes,
|
||||
}
|
||||
}
|
||||
struct MeshWithAabb{
|
||||
mesh:model::Mesh,
|
||||
aabb:strafesnet_common::aabb::Aabb,
|
||||
aabb:Aabb,
|
||||
}
|
||||
pub struct PartialMap1{
|
||||
fn acquire_mesh_id_from_render_config_id<'a>(
|
||||
primitive_meshes:&mut Vec<model::Mesh>,
|
||||
mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
|
||||
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
|
||||
old_mesh_id:model::MeshId,
|
||||
render:RenderConfigId,
|
||||
)->Option<(model::MeshId,&'a Aabb)>{
|
||||
//ignore meshes that fail to load completely for now
|
||||
loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|(
|
||||
*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
|
||||
.entry(render).or_insert_with(||{
|
||||
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
|
||||
let mut mesh_clone=mesh_with_aabb.mesh.clone();
|
||||
//set the render group lool
|
||||
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
|
||||
graphics_group.render=render;
|
||||
}
|
||||
primitive_meshes.push(mesh_clone);
|
||||
mesh_id
|
||||
}),
|
||||
&mesh_with_aabb.aabb,
|
||||
))
|
||||
}
|
||||
fn acquire_union_id_from_render_config_id<'a>(
|
||||
primitive_meshes:&mut Vec<model::Mesh>,
|
||||
union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
|
||||
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
|
||||
old_union_id:model::MeshId,
|
||||
part_texture_description:RobloxPartDescription,
|
||||
)->Option<(model::MeshId,&'a Aabb)>{
|
||||
//ignore uniones that fail to load completely for now
|
||||
loaded_meshes.get(&old_union_id).map(|union_with_aabb|(
|
||||
*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
|
||||
.entry(part_texture_description.clone()).or_insert_with(||{
|
||||
let union_id=model::MeshId::new(primitive_meshes.len() as u32);
|
||||
let mut union_clone=union_with_aabb.mesh.clone();
|
||||
//set the render groups
|
||||
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description){
|
||||
if let Some(face_texture_description)=maybe_face_texture_description{
|
||||
graphics_group.render=face_texture_description.render;
|
||||
}
|
||||
}
|
||||
primitive_meshes.push(union_clone);
|
||||
union_id
|
||||
}),
|
||||
&union_with_aabb.aabb,
|
||||
))
|
||||
}
|
||||
pub struct PartialMap1<'a>{
|
||||
primitive_meshes:Vec<model::Mesh>,
|
||||
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes>,
|
||||
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes>,
|
||||
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>,
|
||||
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>,
|
||||
deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>,
|
||||
}
|
||||
impl 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,51 +888,32 @@ 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)=>{
|
||||
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);
|
||||
}
|
||||
Some((old_mesh_id,MeshWithAabb{
|
||||
(old_mesh_id,MeshWithAabb{
|
||||
mesh,
|
||||
aabb,
|
||||
}))
|
||||
},
|
||||
Err(e)=>{
|
||||
println!("Error converting mesh: {e:?}");
|
||||
None
|
||||
},
|
||||
}
|
||||
).collect();
|
||||
})
|
||||
}).collect();
|
||||
|
||||
// SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way
|
||||
// I just want to chain iterators together man
|
||||
let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes);
|
||||
|
||||
let mut mesh_id_from_render_config_id=HashMap::new();
|
||||
//ignore meshes that fail to load completely for now
|
||||
let mut acquire_mesh_id_from_render_config_id=|old_mesh_id,render|{
|
||||
loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|(
|
||||
*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
|
||||
.entry(render).or_insert_with(||{
|
||||
let mesh_id=model::MeshId::new(self.primitive_meshes.len() as u32);
|
||||
let mut mesh_clone=mesh_with_aabb.mesh.clone();
|
||||
//add a render group lool
|
||||
mesh_clone.graphics_groups.push(model::IndexedGraphicsGroup{
|
||||
render,
|
||||
//the lowest lod is highest quality
|
||||
groups:vec![model::PolygonGroupId::new(0)]
|
||||
});
|
||||
self.primitive_meshes.push(mesh_clone);
|
||||
mesh_id
|
||||
}),
|
||||
&mesh_with_aabb.aabb,
|
||||
))
|
||||
};
|
||||
let mut union_id_from_render_config_id=HashMap::new();
|
||||
//now that the meshes are loaded, these models can be generated
|
||||
let models_owned_attributes:Vec<ModelOwnedAttributes>=
|
||||
self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{
|
||||
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
|
||||
//insert into primitive_meshes
|
||||
let (mesh,aabb)=acquire_mesh_id_from_render_config_id(
|
||||
unsafe{*aint_no_way.get()},
|
||||
&mut mesh_id_from_render_config_id,
|
||||
&loaded_meshes,
|
||||
deferred_model_deferred_attributes.model.mesh,
|
||||
deferred_model_deferred_attributes.render
|
||||
)?;
|
||||
@ -814,7 +931,32 @@ impl PartialMap1{
|
||||
deferred_model_deferred_attributes.model.transform.translation
|
||||
),
|
||||
})
|
||||
}).chain(self.primitive_models_deferred_attributes.into_iter())
|
||||
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
|
||||
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
|
||||
//insert into primitive_meshes
|
||||
let (mesh,aabb)=acquire_union_id_from_render_config_id(
|
||||
unsafe{*aint_no_way.get()},
|
||||
&mut union_id_from_render_config_id,
|
||||
&loaded_meshes,
|
||||
deferred_union_deferred_attributes.model.mesh,
|
||||
deferred_union_deferred_attributes.render
|
||||
)?;
|
||||
let size=aabb.size();
|
||||
Some(ModelDeferredAttributes{
|
||||
mesh,
|
||||
deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,
|
||||
color:deferred_union_deferred_attributes.model.color,
|
||||
transform:Planar64Affine3::new(
|
||||
Planar64Mat3::from_cols([
|
||||
(deferred_union_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().fix_1(),
|
||||
(deferred_union_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().fix_1(),
|
||||
(deferred_union_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().fix_1()
|
||||
]),
|
||||
deferred_union_deferred_attributes.model.transform.translation
|
||||
),
|
||||
})
|
||||
}))
|
||||
.chain(self.primitive_models_deferred_attributes.into_iter())
|
||||
.enumerate().map(|(model_id,model_deferred_attributes)|{
|
||||
let model_id=model::ModelId::new(model_id as u32);
|
||||
ModelOwnedAttributes{
|
||||
@ -882,15 +1024,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()
|
||||
);
|
||||
|
177
lib/rbx_loader/src/union.rs
Normal file
177
lib/rbx_loader/src/union.rs
Normal file
@ -0,0 +1,177 @@
|
||||
use rbx_mesh::mesh_data::NormalId2 as MeshDataNormalId2;
|
||||
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
|
||||
use strafesnet_common::integer::vec3;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Block,
|
||||
MissingVertexId(u32),
|
||||
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
|
||||
RobloxPhysicsData(rbx_mesh::physics_data::Error),
|
||||
RobloxMeshData(rbx_mesh::mesh_data::Error),
|
||||
}
|
||||
impl std::fmt::Display for Error{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
// wacky state machine to make sure all vertices in a face agree upon what NormalId to use.
|
||||
// Roblox duplicates this information per vertex when it should only exist per-face.
|
||||
enum MeshDataNormalStatus{
|
||||
Agree(MeshDataNormalId2),
|
||||
Conflicting,
|
||||
}
|
||||
struct MeshDataNormalChecker{
|
||||
status:Option<MeshDataNormalStatus>,
|
||||
}
|
||||
impl MeshDataNormalChecker{
|
||||
fn new()->Self{
|
||||
Self{status:None}
|
||||
}
|
||||
fn check(&mut self,normal:MeshDataNormalId2){
|
||||
self.status=match self.status.take(){
|
||||
None=>Some(MeshDataNormalStatus::Agree(normal)),
|
||||
Some(MeshDataNormalStatus::Agree(old_normal))=>{
|
||||
if old_normal==normal{
|
||||
Some(MeshDataNormalStatus::Agree(old_normal))
|
||||
}else{
|
||||
Some(MeshDataNormalStatus::Conflicting)
|
||||
}
|
||||
},
|
||||
Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting),
|
||||
};
|
||||
}
|
||||
fn into_agreed_normal(self)->Option<MeshDataNormalId2>{
|
||||
self.status.and_then(|status|match status{
|
||||
MeshDataNormalStatus::Agree(normal)=>Some(normal),
|
||||
MeshDataNormalStatus::Conflicting=>None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error{}
|
||||
pub fn convert(
|
||||
roblox_physics_data:&[u8],
|
||||
roblox_mesh_data:&[u8],
|
||||
size:glam::Vec3,
|
||||
part_texture_description:crate::rbx::RobloxPartDescription,
|
||||
)->Result<model::Mesh,Error>{
|
||||
const NORMAL_FACES:usize=6;
|
||||
let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];
|
||||
|
||||
// build graphics and physics meshes
|
||||
let mut mb=strafesnet_common::model::MeshBuilder::new();
|
||||
// graphics
|
||||
let graphics_groups=if !roblox_mesh_data.is_empty(){
|
||||
// create per-face texture coordinate affine transforms
|
||||
let cube_face_description=part_texture_description.map(|opt|opt.map(|mut t|{
|
||||
t.transform.set_size(1.0,1.0);
|
||||
t.to_face_description()
|
||||
}));
|
||||
|
||||
let mesh_data=rbx_mesh::read_mesh_data_versioned(
|
||||
std::io::Cursor::new(roblox_mesh_data)
|
||||
).map_err(Error::RobloxMeshData)?;
|
||||
let graphics_mesh=match mesh_data{
|
||||
rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block),
|
||||
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL2(mesh_data2))=>mesh_data2.mesh,
|
||||
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL4(mesh_data4))=>mesh_data4.mesh,
|
||||
};
|
||||
for [vertex_id0,vertex_id1,vertex_id2] in graphics_mesh.faces{
|
||||
let face=[
|
||||
graphics_mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
|
||||
graphics_mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
|
||||
graphics_mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
|
||||
];
|
||||
let mut normal_agreement_checker=MeshDataNormalChecker::new();
|
||||
let face=face.into_iter().map(|vertex|{
|
||||
normal_agreement_checker.check(vertex.normal_id);
|
||||
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?);
|
||||
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?);
|
||||
let tex_coord=glam::Vec2::from_array(vertex.tex);
|
||||
let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1];
|
||||
let (tex,color)=match maybe_face_description{
|
||||
Some(face_description)=>{
|
||||
// transform texture coordinates and set decal color
|
||||
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
|
||||
let color=mb.acquire_color_id(face_description.color);
|
||||
(tex,color)
|
||||
},
|
||||
None=>{
|
||||
// texture coordinates don't matter and pass through mesh vertex color
|
||||
let tex=mb.acquire_tex_id(tex_coord);
|
||||
let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
|
||||
(tex,color)
|
||||
},
|
||||
};
|
||||
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
|
||||
}).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?;
|
||||
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
|
||||
polygon_groups_normal_id[normal_id as usize-1].push(face);
|
||||
}else{
|
||||
panic!("Empty face!");
|
||||
}
|
||||
}
|
||||
(0..NORMAL_FACES).map(|polygon_group_id|{
|
||||
model::IndexedGraphicsGroup{
|
||||
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
|
||||
groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
|
||||
}
|
||||
}).collect()
|
||||
}else{
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
//physics
|
||||
let physics_convex_meshes=if !roblox_physics_data.is_empty(){
|
||||
let physics_data=rbx_mesh::read_physics_data_versioned(
|
||||
std::io::Cursor::new(roblox_physics_data)
|
||||
).map_err(Error::RobloxPhysicsData)?;
|
||||
let physics_convex_meshes=match physics_data{
|
||||
rbx_mesh::physics_data::PhysicsData::CSGK(_)
|
||||
// have not seen this format in practice
|
||||
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
|
||||
=>return Err(Error::Block),
|
||||
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes(meshes))
|
||||
=>meshes.meshes,
|
||||
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
|
||||
=>vec![pim.mesh],
|
||||
};
|
||||
physics_convex_meshes
|
||||
}else{
|
||||
Vec::new()
|
||||
};
|
||||
let polygon_groups:Vec<PolygonGroup>=polygon_groups_normal_id.into_iter().map(|faces|
|
||||
// graphics polygon groups (to be rendered)
|
||||
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
|
||||
).chain(physics_convex_meshes.into_iter().map(|mesh|{
|
||||
// this can be factored out of the loop but I am lazy
|
||||
let color=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
|
||||
// physics polygon groups (to do physics)
|
||||
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[vertex_id0,vertex_id1,vertex_id2]|{
|
||||
let face=[
|
||||
mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
|
||||
mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
|
||||
mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
|
||||
].map(|v|glam::Vec3::from_slice(v)/size);
|
||||
let vertex_norm=(face[1]-face[0])
|
||||
.cross(face[2]-face[0]);
|
||||
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
|
||||
face.into_iter().map(|vertex_pos|{
|
||||
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
|
||||
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
|
||||
}).collect()
|
||||
}).collect::<Result<_,_>>()?)))
|
||||
})).collect::<Result<_,_>>()?;
|
||||
let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
|
||||
groups:vec![PolygonGroupId::new(id as u32)]
|
||||
}).collect();
|
||||
Ok(mb.build(
|
||||
polygon_groups,
|
||||
graphics_groups,
|
||||
physics_groups,
|
||||
))
|
||||
}
|
11
lib/rbxassetid/Cargo.toml
Normal file
11
lib/rbxassetid/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "rbxassetid"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Parse Roblox asset id from 'Content' urls."
|
||||
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
url = "2.5.4"
|
176
lib/rbxassetid/LICENSE-APACHE
Normal file
176
lib/rbxassetid/LICENSE-APACHE
Normal file
@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
23
lib/rbxassetid/LICENSE-MIT
Normal file
23
lib/rbxassetid/LICENSE-MIT
Normal file
@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
26
lib/rbxassetid/README.md
Normal file
26
lib/rbxassetid/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
Roblox Asset Id
|
||||
===============
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use rbxassetid::RobloxAssetId;
|
||||
|
||||
let content="rbxassetid://255299419";
|
||||
let RobloxAssetId(asset_id)=content.parse()?;
|
||||
```
|
||||
|
||||
#### License
|
||||
|
||||
<sup>
|
||||
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
|
||||
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
|
||||
</sup>
|
||||
|
||||
<br>
|
||||
|
||||
<sub>
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
|
||||
be dual licensed as above, without any additional terms or conditions.
|
||||
</sub>
|
41
lib/rbxassetid/src/lib.rs
Normal file
41
lib/rbxassetid/src/lib.rs
Normal file
@ -0,0 +1,41 @@
|
||||
#[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)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rbxassetid(){
|
||||
let content="rbxassetid://255299419";
|
||||
let RobloxAssetId(_asset_id)=content.parse().unwrap();
|
||||
}
|
@ -33,7 +33,7 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
// LMAO look at this function!
|
||||
pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{
|
||||
let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
|
||||
f(&mut *dom)
|
||||
f(*dom)
|
||||
}
|
||||
|
||||
fn coerce_float32(value:&mlua::Value)->Option<f32>{
|
||||
|
@ -1,22 +1,22 @@
|
||||
macro_rules! type_from_lua_userdata{
|
||||
($asd:ident)=>{
|
||||
impl mlua::FromLua for $asd{
|
||||
($ty:ident)=>{
|
||||
impl mlua::FromLua for $ty{
|
||||
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
|
||||
match value{
|
||||
mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! type_from_lua_userdata_lua_lifetime{
|
||||
($asd:ident)=>{
|
||||
impl mlua::FromLua for $asd<'static>{
|
||||
($ty:ident)=>{
|
||||
impl mlua::FromLua for $ty<'static>{
|
||||
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
|
||||
match value{
|
||||
mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))),
|
||||
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,8 +59,8 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
|
||||
match delay.classify(){
|
||||
std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?,
|
||||
// cases where the number is too large to schedule
|
||||
std::num::FpCategory::Infinite=>return Ok(()),
|
||||
std::num::FpCategory::Normal=>if (u64::MAX as f64)<delay{
|
||||
std::num::FpCategory::Infinite
|
||||
|std::num::FpCategory::Normal if (u64::MAX as f64)<delay=>{
|
||||
return Ok(());
|
||||
},
|
||||
_=>(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_snf"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@ -8,4 +8,4 @@ edition = "2021"
|
||||
[dependencies]
|
||||
binrw = "0.14.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_common = { path = "../common", registry = "strafesnet" }
|
||||
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
|
||||
|
@ -1,98 +1,347 @@
|
||||
use binrw::{BinReaderExt, binrw};
|
||||
use binrw::{binrw,BinReaderExt,BinWrite,BinWriterExt};
|
||||
|
||||
use crate::newtypes;
|
||||
use crate::file::BlockId;
|
||||
use strafesnet_common::physics::Time;
|
||||
|
||||
const VERSION:u32=0;
|
||||
|
||||
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
InvalidHeader,
|
||||
InvalidHeader(binrw::Error),
|
||||
InvalidSegment(binrw::Error),
|
||||
SegmentConvert(newtypes::integer::RatioError),
|
||||
InstructionConvert(newtypes::physics::InstructionConvert),
|
||||
InstructionWrite(binrw::Error),
|
||||
InvalidSegmentId(SegmentId),
|
||||
InvalidData(binrw::Error),
|
||||
IO(std::io::Error),
|
||||
File(crate::file::Error),
|
||||
}
|
||||
|
||||
// Bot files are simply the sequence of instructions that the physics received during the run.
|
||||
// The instructions are partitioned into timestamped blocks for ease of streaming.
|
||||
//
|
||||
// Keyframe information required for efficient seeking
|
||||
// is part of a different file, and is generated from this file.
|
||||
|
||||
/* block types
|
||||
|
||||
BLOCK_BOT_HEADER:
|
||||
u128 map_resource_uuid //which map is this bot running
|
||||
//don't include style info in bot header because it's in the simulation state
|
||||
//blocks are laid out in chronological order, but indices may jump around.
|
||||
u64 num_segments
|
||||
// Segments are laid out in chronological order,
|
||||
// but block_id is not necessarily in ascending order.
|
||||
//
|
||||
// This is to place the final segment close to the start of the file,
|
||||
// which allows the duration of the bot to be conveniently calculated
|
||||
// from the first and last instruction timestamps.
|
||||
//
|
||||
// Use exact physics version for replay playback
|
||||
// Use highest compatible physics version for verification
|
||||
u32 physics_version
|
||||
u32 num_segments
|
||||
for _ in 0..num_segments{
|
||||
i64 time //simulation_state timestamp
|
||||
u64 block_id
|
||||
i64 time
|
||||
u32 instruction_count
|
||||
u32 block_id
|
||||
}
|
||||
|
||||
BLOCK_BOT_SEGMENT:
|
||||
//format version indicates what version of these structures to use
|
||||
SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about.
|
||||
//to read, greedily decode instructions until eof
|
||||
loop{
|
||||
//delta encode as much as possible (time,mousepos)
|
||||
//strafe ticks are implied
|
||||
//physics can be implied in an input-only bot file
|
||||
TimedInstruction<SimulationInstruction> instruction
|
||||
// segments can potentially be losslessly compressed!
|
||||
for _ in 0..instruction_count{
|
||||
// TODO: delta encode as much as possible (time,mousepos)
|
||||
i64 time
|
||||
physics::Instruction instruction
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
//error hiding mock code
|
||||
mod simulation{
|
||||
#[super::binrw]
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
pub struct State{}
|
||||
#[super::binrw]
|
||||
#[brw(little)]
|
||||
pub struct Instruction{}
|
||||
struct SegmentHeader{
|
||||
time:i64,
|
||||
instruction_count:u32,
|
||||
block_id:BlockId,
|
||||
}
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
struct Header{
|
||||
physics_version:u32,
|
||||
num_segments:u32,
|
||||
#[br(count=num_segments)]
|
||||
segments:Vec<SegmentHeader>,
|
||||
}
|
||||
// mod instruction{
|
||||
// #[super::binrw]
|
||||
// #[brw(little)]
|
||||
// pub struct TimedInstruction<Instruction:binrw::BinRead+binrw::BinWrite>{
|
||||
// time:u64,
|
||||
// instruction:Instruction
|
||||
// }
|
||||
// }
|
||||
// mod timeline{
|
||||
// #[super::binrw]
|
||||
// #[brw(little)]
|
||||
// pub struct Timeline<Instruction:binrw::BinRead+binrw::BinWrite>{
|
||||
// #[bw(try_calc(u32::try_from(instructions.len())))]
|
||||
// instruction_count:u32,
|
||||
// #[br(count=instruction_count)]
|
||||
// instructions:Vec<super::instruction::TimedInstruction<Instruction>>
|
||||
// }
|
||||
// }
|
||||
|
||||
//serious code
|
||||
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
#[derive(Clone,Copy,Debug,id::Id)]
|
||||
pub struct SegmentId(u32);
|
||||
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
pub struct Segment{
|
||||
state:simulation::State,
|
||||
//#[bw(try_calc(u32::try_from(instructions.len())))]
|
||||
//instruction_count:u32,
|
||||
//#[br(count=instruction_count)]
|
||||
//instructions:Vec<instruction::TimedInstruction<simulation::Instruction>>
|
||||
pub instructions:Vec<TimedPhysicsInstruction>
|
||||
}
|
||||
|
||||
//please remember that strafe ticks are implicit! 33% smaller bot files
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct SegmentInfo{
|
||||
/// time of the first instruction in this segment.
|
||||
time:Time,
|
||||
instruction_count:u32,
|
||||
/// How many total instructions in segments up to and including this segment
|
||||
/// Alternatively, the id of the first instruction be in the _next_ segment
|
||||
instructions_subtotal:u64,
|
||||
block_id:BlockId,
|
||||
}
|
||||
|
||||
pub struct StreamableBot<R:BinReaderExt>{
|
||||
file:crate::file::File<R>,
|
||||
//timeline:timeline::Timeline<SegmentId>,
|
||||
segment_id_to_block_id:Vec<crate::file::BlockId>,
|
||||
segment_map:Vec<SegmentInfo>,
|
||||
}
|
||||
impl<R:BinReaderExt> StreamableBot<R>{
|
||||
pub(crate) fn new(file:crate::file::File<R>)->Result<Self,Error>{
|
||||
Err(Error::InvalidHeader)
|
||||
pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{
|
||||
//assume the file seek is in the right place to start reading header
|
||||
let header:Header=file.data_mut().read_le().map_err(Error::InvalidHeader)?;
|
||||
let mut instructions_subtotal=0;
|
||||
let segment_map=header.segments.into_iter().map(|SegmentHeader{time,instruction_count,block_id}|{
|
||||
instructions_subtotal+=instruction_count as u64;
|
||||
SegmentInfo{
|
||||
time:Time::raw(time),
|
||||
instruction_count,
|
||||
instructions_subtotal,
|
||||
block_id,
|
||||
}
|
||||
}).collect();
|
||||
Ok(Self{
|
||||
file,
|
||||
segment_map,
|
||||
})
|
||||
}
|
||||
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
|
||||
Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
|
||||
}
|
||||
pub fn find_segments_instruction_range(&self,start_instruction:u64,end_instruction:u64)->&[SegmentInfo]{
|
||||
let start=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<start_instruction);
|
||||
let end=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<end_instruction);
|
||||
&self.segment_map[start..=end]
|
||||
}
|
||||
// pub fn find_segments_time_range(&self,start_time:Time,end_time:Time)->&[SegmentInfo]{
|
||||
// // TODO: This is off by one, both should be one less
|
||||
// let start=self.segment_map.partition_point(|segment_info|segment_info.time<start_time);
|
||||
// let end=self.segment_map.partition_point(|segment_info|segment_info.time<end_time);
|
||||
// &self.segment_map[start..=end]
|
||||
// }
|
||||
fn append_to_segment(&mut self,segment_info:SegmentInfo,segment:&mut Segment)->Result<(),Error>{
|
||||
let mut block=self.file.block_reader(segment_info.block_id).map_err(Error::File)?;
|
||||
for _ in 0..segment_info.instruction_count{
|
||||
let instruction:newtypes::physics::TimedInstruction=block.read_le().map_err(Error::InvalidSegment)?;
|
||||
segment.instructions.push(instruction.try_into().map_err(Error::SegmentConvert)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn load_segment(&mut self,segment_info:SegmentInfo)->Result<Segment,Error>{
|
||||
let mut segment=Segment{
|
||||
instructions:Vec::with_capacity(segment_info.instruction_count as usize),
|
||||
};
|
||||
self.append_to_segment(segment_info,&mut segment)?;
|
||||
Ok(segment)
|
||||
}
|
||||
pub fn read_all(&mut self)->Result<Segment,Error>{
|
||||
let mut segment=Segment{
|
||||
instructions:Vec::new(),
|
||||
};
|
||||
for i in 0..self.segment_map.len(){
|
||||
let segment_info=self.segment_map[i];
|
||||
self.append_to_segment(segment_info,&mut segment)?;
|
||||
}
|
||||
pub fn load_segment(&mut self,segment_id:SegmentId)->Result<Segment,Error>{
|
||||
let block_id=*self.segment_id_to_block_id.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?;
|
||||
let mut block=self.file.block_reader(block_id).map_err(Error::File)?;
|
||||
let segment=block.read_le().map_err(Error::InvalidSegment)?;
|
||||
Ok(segment)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
|
||||
pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
|
||||
// decide which instructions to put in which segment
|
||||
// write segment 1 to block 1
|
||||
// write segment N to block 2
|
||||
// write rest of segments
|
||||
// 1 2 3 4 5
|
||||
// becomes
|
||||
// [1 5] 2 3 4
|
||||
struct SegmentHeaderInfo{
|
||||
time:Time,
|
||||
instruction_count:u32,
|
||||
range:core::ops::Range<usize>
|
||||
}
|
||||
|
||||
let mut segment_header_infos=Vec::new();
|
||||
let mut raw_segments=std::io::Cursor::new(Vec::new());
|
||||
|
||||
// block info
|
||||
let mut start_time=Time::ZERO;
|
||||
let mut start_position=raw_segments.position() as usize;
|
||||
let mut instruction_count=0;
|
||||
|
||||
let mut last_position=start_position;
|
||||
|
||||
let mut iter=instructions.into_iter();
|
||||
|
||||
macro_rules! collect_instruction{
|
||||
($instruction:expr)=>{
|
||||
let time=$instruction.time;
|
||||
let instruction_writable:newtypes::physics::TimedInstruction=$instruction.try_into().map_err(Error::InstructionConvert)?;
|
||||
instruction_writable.write_le(&mut raw_segments).map_err(Error::InstructionWrite)?;
|
||||
instruction_count+=1;
|
||||
let position=raw_segments.position() as usize;
|
||||
// exceeds max block size
|
||||
if MAX_BLOCK_SIZE<position-last_position{
|
||||
segment_header_infos.push(SegmentHeaderInfo{
|
||||
time:start_time,
|
||||
instruction_count,
|
||||
range:start_position..last_position,
|
||||
});
|
||||
start_position=last_position;
|
||||
instruction_count=0;
|
||||
start_time=time;
|
||||
}
|
||||
last_position=position;
|
||||
}
|
||||
}
|
||||
|
||||
// unroll one loop iteration to grab the starting time
|
||||
if let Some(instruction)=iter.next(){
|
||||
start_time=instruction.time;
|
||||
collect_instruction!(instruction);
|
||||
}
|
||||
|
||||
for instruction in iter{
|
||||
collect_instruction!(instruction);
|
||||
}
|
||||
//last block, whatever size it happens to be
|
||||
{
|
||||
let final_position=raw_segments.position() as usize;
|
||||
segment_header_infos.push(SegmentHeaderInfo{
|
||||
time:start_time,
|
||||
instruction_count,
|
||||
range:start_position..final_position,
|
||||
});
|
||||
}
|
||||
// drop cursor
|
||||
let raw_segments=raw_segments.into_inner();
|
||||
|
||||
let num_segments=segment_header_infos.len();
|
||||
|
||||
// segments list is in chronological order
|
||||
let make_segment_header=|block_id,&SegmentHeaderInfo{time,instruction_count,range:ref _range}|SegmentHeader{
|
||||
time:time.get(),
|
||||
instruction_count,
|
||||
block_id,
|
||||
};
|
||||
let segments=if 2<num_segments{
|
||||
let mut segments=Vec::with_capacity(num_segments);
|
||||
// segment 1 is second block
|
||||
if let Some(seg)=segment_header_infos.first(){
|
||||
segments.push(make_segment_header(BlockId::new(1),seg));
|
||||
}
|
||||
// rest of segments start at fourth block
|
||||
for (i,seg) in segment_header_infos[1..num_segments-1].iter().enumerate(){
|
||||
make_segment_header(BlockId::new(3+i as u32),seg);
|
||||
}
|
||||
// last segment is third block
|
||||
if let Some(seg)=segment_header_infos.last(){
|
||||
segments.push(make_segment_header(BlockId::new(2),seg));
|
||||
}
|
||||
segments
|
||||
}else{
|
||||
// all segments in order
|
||||
segment_header_infos.iter().enumerate().map(|(i,seg)|
|
||||
make_segment_header(BlockId::new(1+i as u32),seg)
|
||||
).collect()
|
||||
};
|
||||
|
||||
let header=Header{
|
||||
physics_version,
|
||||
num_segments:num_segments as u32,
|
||||
segments,
|
||||
};
|
||||
|
||||
// map header is +1
|
||||
let block_count=1+num_segments as u32;
|
||||
|
||||
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
|
||||
// block_location is one longer than block_count
|
||||
let mut block_location=Vec::with_capacity(1+block_count as usize);
|
||||
|
||||
//probe header length
|
||||
let mut bot_header_data=Vec::new();
|
||||
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
|
||||
|
||||
// the first block location is the map header
|
||||
block_location.push(offset);
|
||||
offset+=bot_header_data.len() as u64;
|
||||
block_location.push(offset);
|
||||
|
||||
// priming includes file header + first 3 blocks [bot header, first segment, last segment]
|
||||
let priming=if 2<num_segments{
|
||||
// segment 1 is block 2
|
||||
if let Some(seg)=segment_header_infos.first(){
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
// last segment is block 3
|
||||
if let Some(seg)=segment_header_infos.last(){
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
|
||||
let priming=offset;
|
||||
|
||||
// rest of segments
|
||||
for seg in &segment_header_infos[1..num_segments-1]{
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
priming
|
||||
}else{
|
||||
// all segments in order
|
||||
for seg in &segment_header_infos{
|
||||
offset+=seg.range.len() as u64;
|
||||
block_location.push(offset);
|
||||
}
|
||||
offset
|
||||
};
|
||||
|
||||
let file_header=crate::file::Header{
|
||||
fourcc:crate::file::FourCC::Bot,
|
||||
version:VERSION,
|
||||
priming,
|
||||
resource:0,
|
||||
block_count,
|
||||
block_location,
|
||||
};
|
||||
|
||||
// write file header
|
||||
writer.write_le(&file_header).map_err(Error::InvalidData)?;
|
||||
// write bot header
|
||||
writer.write(&bot_header_data).map_err(Error::IO)?;
|
||||
|
||||
// write blocks
|
||||
if 2<num_segments{
|
||||
// segment 1 is block 2
|
||||
if let Some(seg)=segment_header_infos.first(){
|
||||
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
|
||||
}
|
||||
// last segment is block 3
|
||||
if let Some(seg)=segment_header_infos.last(){
|
||||
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
|
||||
}
|
||||
// rest of segments
|
||||
for seg in &segment_header_infos[1..num_segments-1]{
|
||||
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
|
||||
}
|
||||
}else{
|
||||
// all segments in order
|
||||
for seg in segment_header_infos{
|
||||
writer.write(&raw_segments[seg.range]).map_err(Error::IO)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -7,21 +7,29 @@ pub enum Error{
|
||||
|
||||
/*
|
||||
BLOCK_DEMO_HEADER:
|
||||
u32 num_maps
|
||||
for map_id in 0..num_maps{
|
||||
i64 simulation_time
|
||||
u128 map_resource_id
|
||||
u64 map_header_block_id
|
||||
}
|
||||
u32 num_bots
|
||||
for bot_id in 0..num_bots{
|
||||
i64 simulation_time
|
||||
u128 bot_resource_id
|
||||
u64 bot_header_block_id
|
||||
}
|
||||
|
||||
//map loading timeline
|
||||
|
||||
//bot loading timeline
|
||||
how to do worldstate for deathrun!?
|
||||
- this is done in the client, there is no worldstate in the demo file
|
||||
|
||||
*/
|
||||
|
||||
pub struct StreamableDemo<R:BinReaderExt>{
|
||||
map:Box<crate::map::StreamableMap<R>>,
|
||||
map:Vec<crate::map::StreamableMap<R>>,
|
||||
bots:Vec<crate::bot::StreamableBot<R>>,
|
||||
}
|
||||
impl<R:BinReaderExt> StreamableDemo<R>{
|
||||
|
@ -73,6 +73,16 @@ pub struct Header{
|
||||
#[br(count=block_count+1)]
|
||||
pub block_location:Vec<u64>,
|
||||
}
|
||||
impl Header{
|
||||
pub const fn calculate_size(block_count:u32)->usize{
|
||||
4 // fourcc
|
||||
+4 // version
|
||||
+8 // priming
|
||||
+16 // resource
|
||||
+4 // block_count
|
||||
+(block_count as usize+1)*8 // block_location
|
||||
}
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
|
@ -86,6 +86,7 @@ for model_id in 0..num_models{
|
||||
//if you hash the resource itself and set the first 8 bits to this, that's the resource uuid
|
||||
#[binrw]
|
||||
#[brw(little,repr=u8)]
|
||||
#[repr(u8)]
|
||||
enum ResourceType{
|
||||
Mesh,
|
||||
Texture,
|
||||
@ -94,21 +95,6 @@ enum ResourceType{
|
||||
//Video,
|
||||
//Animation,
|
||||
}
|
||||
const RESOURCE_TYPE_VARIANT_COUNT:u8=2;
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
struct ResourceId(u128);
|
||||
impl ResourceId{
|
||||
fn resource_type(&self)->Option<ResourceType>{
|
||||
let discriminant=self.0 as u8;
|
||||
//TODO: use this when it is stabilized https://github.com/rust-lang/rust/issues/73662
|
||||
//if (discriminant as usize)<std::mem::variant_count::<ResourceType>(){
|
||||
match discriminant<RESOURCE_TYPE_VARIANT_COUNT{
|
||||
true=>Some(unsafe{std::mem::transmute::<u8,ResourceType>(discriminant)}),
|
||||
false=>None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResourceMap<T>{
|
||||
meshes:HashMap<strafesnet_common::model::MeshId,T>,
|
||||
@ -135,11 +121,6 @@ struct ResourceBlockHeader{
|
||||
resource:ResourceType,
|
||||
id:BlockId,
|
||||
}
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
struct ResourceExternalHeader{
|
||||
resource_uuid:ResourceId,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[brw(little)]
|
||||
@ -411,28 +392,26 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
|
||||
attributes:map.attributes.into_iter().map(Into::into).collect(),
|
||||
render_configs:map.render_configs.into_iter().map(Into::into).collect(),
|
||||
};
|
||||
let mut file_header=crate::file::Header{
|
||||
//probe header length
|
||||
let mut map_header_data=Vec::new();
|
||||
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
|
||||
|
||||
// calculate final file header
|
||||
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
|
||||
offset+=map_header_data.len() as u64;
|
||||
// priming includes map header
|
||||
let priming=offset;
|
||||
for position in &mut block_location{
|
||||
*position+=offset;
|
||||
}
|
||||
let file_header=crate::file::Header{
|
||||
fourcc:crate::file::FourCC::Map,
|
||||
version:0,
|
||||
priming:0,
|
||||
priming,
|
||||
resource:0,
|
||||
block_count,
|
||||
block_location,
|
||||
};
|
||||
//probe header length
|
||||
let mut file_header_data=Vec::new();
|
||||
binrw::BinWrite::write_le(&file_header,&mut std::io::Cursor::new(&mut file_header_data)).map_err(Error::InvalidData)?;
|
||||
let mut map_header_data=Vec::new();
|
||||
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
|
||||
|
||||
//update file header according to probe data
|
||||
let mut offset=file_header_data.len() as u64;
|
||||
file_header.priming=offset;
|
||||
file_header.block_location[0]=offset;
|
||||
offset+=map_header_data.len() as u64;
|
||||
for position in &mut file_header.block_location[1..]{
|
||||
*position+=offset;
|
||||
}
|
||||
|
||||
//write (updated) file header
|
||||
writer.write_le(&file_header).map_err(Error::InvalidData)?;
|
||||
|
@ -1,3 +1,9 @@
|
||||
pub const fn flag(b:bool,mask:u8)->u8{
|
||||
(-(b as i8) as u8)&mask
|
||||
}
|
||||
pub fn bool_from_u8(value:u8)->bool{
|
||||
value!=0
|
||||
}
|
||||
pub fn bool_into_u8(value:&bool)->u8{
|
||||
*value as u8
|
||||
}
|
||||
|
@ -38,6 +38,23 @@ pub struct Ratio64Vec2{
|
||||
pub x:Ratio64,
|
||||
pub y:Ratio64,
|
||||
}
|
||||
impl TryInto<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
|
||||
type Error=RatioError;
|
||||
fn try_into(self)->Result<strafesnet_common::integer::Ratio64Vec2,Self::Error>{
|
||||
Ok(strafesnet_common::integer::Ratio64Vec2{
|
||||
x:self.x.try_into()?,
|
||||
y:self.y.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl From<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
|
||||
fn from(value:strafesnet_common::integer::Ratio64Vec2)->Self{
|
||||
Self{
|
||||
x:value.x.into(),
|
||||
y:value.y.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Angle32=i32;
|
||||
pub type Planar64=i64;
|
||||
|
@ -1,7 +1,9 @@
|
||||
mod common;
|
||||
pub mod aabb;
|
||||
pub mod model;
|
||||
pub mod mouse;
|
||||
pub mod integer;
|
||||
pub mod physics;
|
||||
pub mod gameplay_modes;
|
||||
pub mod gameplay_style;
|
||||
pub mod gameplay_attributes;
|
25
lib/snf/src/newtypes/mouse.rs
Normal file
25
lib/snf/src/newtypes/mouse.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use super::integer::Time;
|
||||
|
||||
#[binrw::binrw]
|
||||
#[brw(little)]
|
||||
pub struct MouseState{
|
||||
pub pos:[i32;2],
|
||||
pub time:Time,
|
||||
}
|
||||
|
||||
impl<T> Into<strafesnet_common::mouse::MouseState<T>> for MouseState{
|
||||
fn into(self)->strafesnet_common::mouse::MouseState<T>{
|
||||
strafesnet_common::mouse::MouseState{
|
||||
pos:self.pos.into(),
|
||||
time:strafesnet_common::integer::Time::raw(self.time),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> From<strafesnet_common::mouse::MouseState<T>> for MouseState{
|
||||
fn from(value:strafesnet_common::mouse::MouseState<T>)->Self{
|
||||
Self{
|
||||
pos:value.pos.to_array(),
|
||||
time:value.time.get(),
|
||||
}
|
||||
}
|
||||
}
|
156
lib/snf/src/newtypes/physics.rs
Normal file
156
lib/snf/src/newtypes/physics.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use super::integer::Time;
|
||||
use super::common::{bool_from_u8,bool_into_u8};
|
||||
|
||||
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
|
||||
|
||||
#[binrw::binrw]
|
||||
#[brw(little)]
|
||||
pub struct TimedInstruction{
|
||||
pub time:Time,
|
||||
pub instruction:Instruction,
|
||||
}
|
||||
|
||||
impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
|
||||
type Error=super::integer::RatioError;
|
||||
fn try_into(self)->Result<TimedPhysicsInstruction,Self::Error>{
|
||||
Ok(strafesnet_common::instruction::TimedInstruction{
|
||||
time:strafesnet_common::integer::Time::raw(self.time),
|
||||
instruction:self.instruction.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
|
||||
type Error=super::physics::InstructionConvert;
|
||||
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
|
||||
Ok(Self{
|
||||
time:value.time.get(),
|
||||
instruction:value.instruction.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[binrw::binrw]
|
||||
#[brw(little)]
|
||||
pub enum Instruction{
|
||||
#[brw(magic=0u8)]
|
||||
ReplaceMouse{
|
||||
m0:super::mouse::MouseState,
|
||||
m1:super::mouse::MouseState
|
||||
},
|
||||
#[brw(magic=1u8)]
|
||||
SetNextMouse(super::mouse::MouseState),
|
||||
#[brw(magic=2u8)]
|
||||
SetMoveRight(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=3u8)]
|
||||
SetMoveUp(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=4u8)]
|
||||
SetMoveBack(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=5u8)]
|
||||
SetMoveLeft(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=6u8)]
|
||||
SetMoveDown(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=7u8)]
|
||||
SetMoveForward(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=8u8)]
|
||||
SetJump(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=9u8)]
|
||||
SetZoom(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=10u8)]
|
||||
Reset,
|
||||
#[brw(magic=11u8)]
|
||||
Restart(super::gameplay_modes::ModeId),
|
||||
#[brw(magic=12u8)]
|
||||
Spawn(super::gameplay_modes::ModeId,super::gameplay_modes::StageId),
|
||||
#[brw(magic=13u8)]
|
||||
PracticeFly,
|
||||
#[brw(magic=14u8)]
|
||||
SetSensitivity(super::integer::Ratio64Vec2),
|
||||
#[brw(magic=255u8)]
|
||||
Idle,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum InstructionConvert{
|
||||
/// This is an instruction that can be dropped when serializing
|
||||
DropInstruction,
|
||||
}
|
||||
impl std::fmt::Display for InstructionConvert{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for InstructionConvert{}
|
||||
impl TryInto<strafesnet_common::physics::Instruction> for Instruction{
|
||||
type Error=super::integer::RatioError;
|
||||
fn try_into(self)->Result<strafesnet_common::physics::Instruction,Self::Error>{
|
||||
Ok(match self{
|
||||
Instruction::ReplaceMouse{m0,m1}=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
|
||||
Instruction::SetNextMouse(m)=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m.into())),
|
||||
Instruction::SetMoveRight(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state.into())),
|
||||
Instruction::SetMoveUp(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state.into())),
|
||||
Instruction::SetMoveBack(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state.into())),
|
||||
Instruction::SetMoveLeft(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state.into())),
|
||||
Instruction::SetMoveDown(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state.into())),
|
||||
Instruction::SetMoveForward(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state.into())),
|
||||
Instruction::SetJump(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state.into())),
|
||||
Instruction::SetZoom(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state.into())),
|
||||
Instruction::Reset=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset),
|
||||
Instruction::Restart(mode_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::new(mode_id))),
|
||||
Instruction::Spawn(mode_id,stage_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(
|
||||
strafesnet_common::gameplay_modes::ModeId::new(mode_id),
|
||||
strafesnet_common::gameplay_modes::StageId::new(stage_id),
|
||||
)),
|
||||
Instruction::PracticeFly=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly),
|
||||
Instruction::SetSensitivity(sensitivity)=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity.try_into()?)),
|
||||
Instruction::Idle=>strafesnet_common::physics::Instruction::Idle,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl TryFrom<strafesnet_common::physics::Instruction> for Instruction{
|
||||
type Error=InstructionConvert;
|
||||
fn try_from(value:strafesnet_common::physics::Instruction)->Result<Self,Self::Error>{
|
||||
match value{
|
||||
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0,m1})=>Ok(Instruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
|
||||
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m))=>Ok(Instruction::SetNextMouse(m.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state))=>Ok(Instruction::SetMoveRight(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state))=>Ok(Instruction::SetMoveUp(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state))=>Ok(Instruction::SetMoveBack(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state))=>Ok(Instruction::SetMoveLeft(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state))=>Ok(Instruction::SetMoveDown(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state))=>Ok(Instruction::SetMoveForward(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state))=>Ok(Instruction::SetJump(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state))=>Ok(Instruction::SetZoom(state.into())),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset)=>Ok(Instruction::Reset),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(mode_id))=>Ok(Instruction::Restart(mode_id.get())),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(mode_id,stage_id))=>Ok(Instruction::Spawn(
|
||||
mode_id.get(),
|
||||
stage_id.get(),
|
||||
)),
|
||||
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly)=>Ok(Instruction::PracticeFly),
|
||||
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity))=>Ok(Instruction::SetSensitivity(sensitivity.into())),
|
||||
strafesnet_common::physics::Instruction::Idle=>Ok(Instruction::Idle),
|
||||
}
|
||||
}
|
||||
}
|
2
lib/snf/src/session.rs
Normal file
2
lib/snf/src/session.rs
Normal file
@ -0,0 +1,2 @@
|
||||
// a session is a recording of the client's inputs
|
||||
// which should deterministically recreate a bot or whatever the client did
|
37
map-tool/Cargo.toml
Normal file
37
map-tool/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "map-tool"
|
||||
version = "1.7.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.2", features = ["derive"] }
|
||||
flate2 = "1.0.27"
|
||||
futures = "0.3.31"
|
||||
image = "0.25.2"
|
||||
image_dds = "0.7.1"
|
||||
lazy-regex = "3.1.0"
|
||||
rbx_asset = { version = "0.2.5", registry = "strafesnet" }
|
||||
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
|
||||
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
|
||||
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
|
||||
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
|
||||
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_bsp_loader = { version = "0.3.0", path = "../lib/bsp_loader", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.5.0", path = "../lib/deferred_loader", registry = "strafesnet" }
|
||||
strafesnet_rbx_loader = { version = "0.6.0", path = "../lib/rbx_loader", registry = "strafesnet" }
|
||||
strafesnet_snf = { version = "0.3.0", path = "../lib/snf", registry = "strafesnet" }
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
|
||||
vbsp = "0.6.0"
|
||||
vmdl = "0.2.0"
|
||||
vmt-parser = "0.2.0"
|
||||
vpk = "0.2.0"
|
||||
vtf = "0.3.0"
|
||||
|
||||
#[profile.release]
|
||||
#lto = true
|
||||
#strip = true
|
||||
#codegen-units = 1
|
23
map-tool/LICENSE
Normal file
23
map-tool/LICENSE
Normal file
@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
2
map-tool/README.md
Normal file
2
map-tool/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# map-tool
|
||||
|
30
map-tool/src/main.rs
Normal file
30
map-tool/src/main.rs
Normal file
@ -0,0 +1,30 @@
|
||||
mod roblox;
|
||||
mod source;
|
||||
|
||||
use clap::{Parser,Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands{
|
||||
#[command(flatten)]
|
||||
Roblox(roblox::Commands),
|
||||
#[command(flatten)]
|
||||
Source(source::Commands),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main()->AResult<()>{
|
||||
let cli=Cli::parse();
|
||||
match cli.command{
|
||||
Commands::Roblox(commands)=>commands.run().await,
|
||||
Commands::Source(commands)=>commands.run().await,
|
||||
}
|
||||
}
|
431
map-tool/src/roblox.rs
Normal file
431
map-tool/src/roblox.rs
Normal file
@ -0,0 +1,431 @@
|
||||
use std::path::{Path,PathBuf};
|
||||
use std::io::{Cursor,Read,Seek};
|
||||
use std::collections::HashSet;
|
||||
use clap::{Args,Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
use rbx_dom_weak::Instance;
|
||||
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
|
||||
use rbxassetid::RobloxAssetId;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
const DOWNLOAD_LIMIT:usize=16;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands{
|
||||
RobloxToSNF(RobloxToSNFSubcommand),
|
||||
DownloadAssets(DownloadAssetsSubcommand),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct RobloxToSNFSubcommand {
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(required=true)]
|
||||
input_files:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct DownloadAssetsSubcommand{
|
||||
#[arg(required=true)]
|
||||
roblox_files:Vec<PathBuf>,
|
||||
// #[arg(long)]
|
||||
// cookie_file:Option<String>,
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub async fn run(self)->AResult<()>{
|
||||
match self{
|
||||
Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await,
|
||||
Commands::DownloadAssets(subcommand)=>download_assets(
|
||||
subcommand.roblox_files,
|
||||
rbx_asset::cookie::Cookie::new("".to_string()),
|
||||
).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum LoadDomError{
|
||||
IO(std::io::Error),
|
||||
Binary(rbx_binary::DecodeError),
|
||||
Xml(rbx_xml::DecodeError),
|
||||
UnknownFormat,
|
||||
}
|
||||
fn load_dom<R:Read+Seek>(mut input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
|
||||
let mut first_8=[0u8;8];
|
||||
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
|
||||
input.rewind().map_err(LoadDomError::IO)?;
|
||||
match &first_8{
|
||||
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
|
||||
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
|
||||
_=>Err(LoadDomError::UnknownFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/* The ones I'm interested in:
|
||||
Beam.Texture
|
||||
Decal.Texture
|
||||
FileMesh.MeshId
|
||||
FileMesh.TextureId
|
||||
MaterialVariant.ColorMap
|
||||
MaterialVariant.MetalnessMap
|
||||
MaterialVariant.NormalMap
|
||||
MaterialVariant.RoughnessMap
|
||||
MeshPart.MeshId
|
||||
MeshPart.TextureID
|
||||
ParticleEmitter.Texture
|
||||
Sky.MoonTextureId
|
||||
Sky.SkyboxBk
|
||||
Sky.SkyboxDn
|
||||
Sky.SkyboxFt
|
||||
Sky.SkyboxLf
|
||||
Sky.SkyboxRt
|
||||
Sky.SkyboxUp
|
||||
Sky.SunTextureId
|
||||
SurfaceAppearance.ColorMap
|
||||
SurfaceAppearance.MetalnessMap
|
||||
SurfaceAppearance.NormalMap
|
||||
SurfaceAppearance.RoughnessMap
|
||||
SurfaceAppearance.TexturePack
|
||||
*/
|
||||
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
|
||||
if let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(property){
|
||||
let url:&str=content.as_ref();
|
||||
if let Ok(asset_id)=url.parse(){
|
||||
content_list.insert(asset_id);
|
||||
}else{
|
||||
println!("Content failed to parse into AssetID: {:?}",content);
|
||||
}
|
||||
}else{
|
||||
println!("property={} does not exist for class={}",property,object.class.as_str());
|
||||
}
|
||||
}
|
||||
async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
|
||||
let mut file=tokio::fs::File::open(path).await?;
|
||||
let mut data=Vec::new();
|
||||
file.read_to_end(&mut data).await?;
|
||||
Ok(Cursor::new(data))
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct UniqueAssets{
|
||||
meshes:HashSet<RobloxAssetId>,
|
||||
unions:HashSet<RobloxAssetId>,
|
||||
textures:HashSet<RobloxAssetId>,
|
||||
}
|
||||
impl UniqueAssets{
|
||||
fn collect(&mut self,object:&Instance){
|
||||
match object.class.as_str(){
|
||||
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Decal"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Texture"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
|
||||
"MeshPart"=>{
|
||||
accumulate_content_id(&mut self.textures,object,"TextureID");
|
||||
accumulate_content_id(&mut self.meshes,object,"MeshId");
|
||||
},
|
||||
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
|
||||
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Sky"=>{
|
||||
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
|
||||
accumulate_content_id(&mut self.textures,object,"SunTextureId");
|
||||
},
|
||||
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
|
||||
_=>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum UniqueAssetError{
|
||||
IO(std::io::Error),
|
||||
LoadDom(LoadDomError),
|
||||
}
|
||||
async fn unique_assets(path:&Path)->Result<UniqueAssets,UniqueAssetError>{
|
||||
// read entire file
|
||||
let mut assets=UniqueAssets::default();
|
||||
let data=read_entire_file(path).await.map_err(UniqueAssetError::IO)?;
|
||||
let dom=load_dom(data).map_err(UniqueAssetError::LoadDom)?;
|
||||
for object in dom.into_raw().1.into_values(){
|
||||
assets.collect(&object);
|
||||
}
|
||||
Ok(assets)
|
||||
}
|
||||
enum DownloadType{
|
||||
Texture(RobloxAssetId),
|
||||
Mesh(RobloxAssetId),
|
||||
Union(RobloxAssetId),
|
||||
}
|
||||
impl DownloadType{
|
||||
fn path(&self)->PathBuf{
|
||||
match self{
|
||||
DownloadType::Texture(asset_id)=>format!("downloaded_textures/{}",asset_id.0.to_string()).into(),
|
||||
DownloadType::Mesh(asset_id)=>format!("meshes/{}",asset_id.0.to_string()).into(),
|
||||
DownloadType::Union(asset_id)=>format!("unions/{}",asset_id.0.to_string()).into(),
|
||||
}
|
||||
}
|
||||
fn asset_id(&self)->u64{
|
||||
match self{
|
||||
DownloadType::Texture(asset_id)=>asset_id.0,
|
||||
DownloadType::Mesh(asset_id)=>asset_id.0,
|
||||
DownloadType::Union(asset_id)=>asset_id.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
enum DownloadResult{
|
||||
Cached(PathBuf),
|
||||
Data(Vec<u8>),
|
||||
Failed,
|
||||
}
|
||||
#[derive(Default,Debug)]
|
||||
struct Stats{
|
||||
total_assets:u32,
|
||||
cached_assets:u32,
|
||||
downloaded_assets:u32,
|
||||
failed_downloads:u32,
|
||||
timed_out_downloads:u32,
|
||||
}
|
||||
async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieContext,download_instruction:DownloadType)->Result<DownloadResult,std::io::Error>{
|
||||
stats.total_assets+=1;
|
||||
let download_instruction=download_instruction;
|
||||
// check if file exists on disk
|
||||
let path=download_instruction.path();
|
||||
if tokio::fs::try_exists(path.as_path()).await?{
|
||||
stats.cached_assets+=1;
|
||||
return Ok(DownloadResult::Cached(path));
|
||||
}
|
||||
let asset_id=download_instruction.asset_id();
|
||||
// if not, download file
|
||||
let mut retry=0;
|
||||
const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
|
||||
let mut backoff=1000f32;
|
||||
loop{
|
||||
let asset_result=context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
asset_id,
|
||||
version:None,
|
||||
}).await;
|
||||
match asset_result{
|
||||
Ok(asset_result)=>{
|
||||
stats.downloaded_assets+=1;
|
||||
tokio::fs::write(path,&asset_result).await?;
|
||||
break Ok(DownloadResult::Data(asset_result));
|
||||
},
|
||||
Err(rbx_asset::cookie::GetError::Response(rbx_asset::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{
|
||||
if scwuab.status_code.as_u16()==429{
|
||||
if retry==12{
|
||||
println!("Giving up asset download {asset_id}");
|
||||
stats.timed_out_downloads+=1;
|
||||
break Ok(DownloadResult::Failed);
|
||||
}
|
||||
println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
|
||||
backoff*=BACKOFF_MUL;
|
||||
retry+=1;
|
||||
}else{
|
||||
stats.failed_downloads+=1;
|
||||
println!("weird scuwab error: {scwuab:?}");
|
||||
break Ok(DownloadResult::Failed);
|
||||
}
|
||||
},
|
||||
Err(e)=>{
|
||||
stats.failed_downloads+=1;
|
||||
println!("sadly error: {e}");
|
||||
break Ok(DownloadResult::Failed);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum ConvertTextureError{
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
#[error("Image error {0:?}")]
|
||||
Image(#[from]image::ImageError),
|
||||
#[error("DDS create error {0:?}")]
|
||||
DDS(#[from]image_dds::CreateDdsError),
|
||||
#[error("DDS write error {0:?}")]
|
||||
DDSWrite(#[from]image_dds::ddsfile::Error),
|
||||
}
|
||||
async fn convert_texture(asset_id:RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{
|
||||
let data=match download_result{
|
||||
DownloadResult::Cached(path)=>tokio::fs::read(path).await?,
|
||||
DownloadResult::Data(data)=>data,
|
||||
DownloadResult::Failed=>return Ok(()),
|
||||
};
|
||||
// image::ImageFormat::Png
|
||||
// image::ImageFormat::Jpeg
|
||||
let image=image::load_from_memory(&data)?.to_rgba8();
|
||||
|
||||
// pick format
|
||||
let format=if image.width()%4!=0||image.height()%4!=0{
|
||||
image_dds::ImageFormat::Rgba8UnormSrgb
|
||||
}else{
|
||||
image_dds::ImageFormat::BC7RgbaUnormSrgb
|
||||
};
|
||||
|
||||
//this fails if the image dimensions are not a multiple of 4
|
||||
let dds=image_dds::dds_from_image(
|
||||
&image,
|
||||
format,
|
||||
image_dds::Quality::Slow,
|
||||
image_dds::Mipmaps::GeneratedAutomatic,
|
||||
)?;
|
||||
|
||||
let file_name=format!("textures/{}.dds",asset_id.0);
|
||||
let mut file=std::fs::File::create(file_name)?;
|
||||
dds.write(&mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{
|
||||
tokio::try_join!(
|
||||
tokio::fs::create_dir_all("downloaded_textures"),
|
||||
tokio::fs::create_dir_all("textures"),
|
||||
tokio::fs::create_dir_all("meshes"),
|
||||
tokio::fs::create_dir_all("unions"),
|
||||
)?;
|
||||
// use mpsc
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
let (send_assets,mut recv_assets)=tokio::sync::mpsc::channel(DOWNLOAD_LIMIT);
|
||||
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
|
||||
// map decode dispatcher
|
||||
// read files multithreaded
|
||||
// produce UniqueAssetsResult per file
|
||||
tokio::spawn(async move{
|
||||
// move send so it gets dropped when all maps have been decoded
|
||||
// closing the channel
|
||||
let mut it=paths.into_iter();
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let send=send_assets.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=unique_assets(path.as_path()).await;
|
||||
_=send.send(result).await;
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
});
|
||||
// download manager
|
||||
// insert into global unique assets guy
|
||||
// add to download queue if the asset is globally unique and does not already exist on disk
|
||||
let mut stats=Stats::default();
|
||||
let context=rbx_asset::cookie::CookieContext::new(cookie);
|
||||
let mut globally_unique_assets=UniqueAssets::default();
|
||||
// pop a job = retry_queue.pop_front() or ingest(recv.recv().await)
|
||||
// SLOW MODE:
|
||||
// acquire all permits
|
||||
// drop all permits
|
||||
// pop one job
|
||||
// if it succeeds go into fast mode
|
||||
// FAST MODE:
|
||||
// acquire one permit
|
||||
// pop a job
|
||||
let download_thread=tokio::spawn(async move{
|
||||
while let Some(result)=recv_assets.recv().await{
|
||||
let unique_assets=match result{
|
||||
Ok(unique_assets)=>unique_assets,
|
||||
Err(e)=>{
|
||||
println!("error: {e:?}");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
for texture_id in unique_assets.textures{
|
||||
if globally_unique_assets.textures.insert(texture_id){
|
||||
let data=download_retry(&mut stats,&context,DownloadType::Texture(texture_id)).await?;
|
||||
send_texture.send((texture_id,data)).await?;
|
||||
}
|
||||
}
|
||||
for mesh_id in unique_assets.meshes{
|
||||
if globally_unique_assets.meshes.insert(mesh_id){
|
||||
download_retry(&mut stats,&context,DownloadType::Mesh(mesh_id)).await?;
|
||||
}
|
||||
}
|
||||
for union_id in unique_assets.unions{
|
||||
if globally_unique_assets.unions.insert(union_id){
|
||||
download_retry(&mut stats,&context,DownloadType::Union(union_id)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
dbg!(stats);
|
||||
Ok::<(),anyhow::Error>(())
|
||||
});
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some((asset_id,download_result)))=(SEM.acquire().await,recv_texture.recv().await){
|
||||
tokio::spawn(async move{
|
||||
let result=convert_texture(asset_id,download_result).await;
|
||||
drop(permit);
|
||||
result.unwrap();
|
||||
});
|
||||
}
|
||||
download_thread.await??;
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
RobloxRead(strafesnet_rbx_loader::ReadError),
|
||||
RobloxLoad(strafesnet_rbx_loader::LoadError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
|
||||
let entire_file=tokio::fs::read(path).await?;
|
||||
|
||||
let model=strafesnet_rbx_loader::read(
|
||||
std::io::Cursor::new(entire_file)
|
||||
).map_err(ConvertError::RobloxRead)?;
|
||||
|
||||
let mut place=model.into_place();
|
||||
place.run_scripts();
|
||||
|
||||
let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
|
||||
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
|
||||
|
||||
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
|
||||
let start=std::time::Instant::now();
|
||||
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
let mut it=paths.into_iter();
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let output_folder=output_folder.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=convert_to_snf(path.as_path(),output_folder).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
|
||||
println!("elapsed={:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
464
map-tool/src/source.rs
Normal file
464
map-tool/src/source.rs
Normal file
@ -0,0 +1,464 @@
|
||||
use std::path::{Path,PathBuf};
|
||||
use std::borrow::Cow;
|
||||
use clap::{Args,Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
use futures::StreamExt;
|
||||
use strafesnet_bsp_loader::loader::BspFinder;
|
||||
use strafesnet_deferred_loader::loader::Loader;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands{
|
||||
SourceToSNF(SourceToSNFSubcommand),
|
||||
ExtractTextures(ExtractTexturesSubcommand),
|
||||
VPKContents(VPKContentsSubcommand),
|
||||
BSPContents(BSPContentsSubcommand),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct SourceToSNFSubcommand {
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(required=true)]
|
||||
input_files:Vec<PathBuf>,
|
||||
#[arg(long)]
|
||||
vpks:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct ExtractTexturesSubcommand{
|
||||
#[arg(required=true)]
|
||||
bsp_files:Vec<PathBuf>,
|
||||
#[arg(long)]
|
||||
vpks:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct VPKContentsSubcommand {
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct BSPContentsSubcommand {
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub async fn run(self)->AResult<()>{
|
||||
match self{
|
||||
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder,subcommand.vpks).await,
|
||||
Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpks).await,
|
||||
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
|
||||
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum VMTContent{
|
||||
VMT(String),
|
||||
VTF(String),
|
||||
Patch(vmt_parser::material::PatchMaterial),
|
||||
Unsupported,//don't want to deal with whatever vmt variant
|
||||
Unresolved,//could not locate a texture because of vmt content
|
||||
}
|
||||
impl VMTContent{
|
||||
fn vtf(opt:Option<String>)->Self{
|
||||
match opt{
|
||||
Some(s)=>Self::VTF(s),
|
||||
None=>Self::Unresolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
|
||||
//just grab some texture from somewhere for now
|
||||
match material{
|
||||
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
|
||||
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
|
||||
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
|
||||
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
|
||||
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
|
||||
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
|
||||
_=>unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum GetVMTError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("Utf8 error {0:?}")]
|
||||
Utf8(#[from]std::str::Utf8Error),
|
||||
#[error("Vdf error {0:?}")]
|
||||
Vdf(#[from]vmt_parser::VdfError),
|
||||
#[error("Vmt not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
|
||||
let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
|
||||
//decode vmt and then write
|
||||
let vmt_str=core::str::from_utf8(&vmt_data)?;
|
||||
let material=vmt_parser::from_str(vmt_str)?;
|
||||
//println!("vmt material={:?}",material);
|
||||
Ok(material)
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum LoadVMTError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("GetVMT error {0:?}")]
|
||||
GetVMT(#[from]GetVMTError),
|
||||
#[error("FromUtf8 error {0:?}")]
|
||||
FromUtf8(#[from]std::string::FromUtf8Error),
|
||||
#[error("Vdf error {0:?}")]
|
||||
Vdf(#[from]vmt_parser::VdfError),
|
||||
#[error("Vmt unsupported")]
|
||||
Unsupported,
|
||||
#[error("Vmt unresolved")]
|
||||
Unresolved,
|
||||
#[error("Vmt not found")]
|
||||
NotFound,
|
||||
}
|
||||
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
match get_some_texture(material){
|
||||
VMTContent::VMT(mut s)=>{
|
||||
s.make_ascii_lowercase();
|
||||
recursive_vmt_loader(finder,get_vmt(finder,&s)?)
|
||||
},
|
||||
VMTContent::VTF(s)=>{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
texture_file_name.push(s);
|
||||
texture_file_name.set_extension("vtf");
|
||||
texture_file_name.as_mut_os_str().make_ascii_lowercase();
|
||||
Ok(finder.find(texture_file_name.to_str().unwrap())?)
|
||||
},
|
||||
VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
|
||||
mat.resolve(|search_name|{
|
||||
let name_lowercase=search_name.to_lowercase();
|
||||
match finder.find(&name_lowercase)?{
|
||||
Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
|
||||
None=>Err(LoadVMTError::NotFound),
|
||||
}
|
||||
})?
|
||||
),
|
||||
VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
|
||||
VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
|
||||
}
|
||||
}
|
||||
fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
//lower case
|
||||
texture_file_name.push(texture_name);
|
||||
texture_file_name.as_mut_os_str().make_ascii_lowercase();
|
||||
//remove stem and search for both vtf and vmt files
|
||||
let stem=texture_file_name.file_stem().unwrap().to_owned();
|
||||
texture_file_name.pop();
|
||||
texture_file_name.push(stem);
|
||||
if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
|
||||
return Ok(Some(stuff));
|
||||
}
|
||||
|
||||
// search for both vmt,vtf
|
||||
let mut texture_file_name_vmt=texture_file_name.clone();
|
||||
texture_file_name_vmt.set_extension("vmt");
|
||||
|
||||
let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
|
||||
match get_vmt_result{
|
||||
Ok(material)=>{
|
||||
let vmt_result=recursive_vmt_loader(finder,material);
|
||||
match vmt_result{
|
||||
Ok(Some(stuff))=>return Ok(Some(stuff)),
|
||||
Ok(None)
|
||||
|Err(LoadVMTError::NotFound)=>(),
|
||||
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
|
||||
Err(e)=>return Err(e),
|
||||
}
|
||||
}
|
||||
|Err(GetVMTError::NotFound)=>(),
|
||||
Err(e)=>Err(e)?,
|
||||
}
|
||||
|
||||
// try looking for vtf
|
||||
let mut texture_file_name_vtf=texture_file_name.clone();
|
||||
texture_file_name_vtf.set_extension("vtf");
|
||||
|
||||
let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
|
||||
match get_vtf_result{
|
||||
Ok(material)=>{
|
||||
let vtf_result=recursive_vmt_loader(finder,material);
|
||||
match vtf_result{
|
||||
Ok(Some(stuff))=>return Ok(Some(stuff)),
|
||||
Ok(None)
|
||||
|Err(LoadVMTError::NotFound)=>(),
|
||||
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
|
||||
Err(e)=>return Err(e),
|
||||
}
|
||||
}
|
||||
|Err(GetVMTError::NotFound)=>(),
|
||||
Err(e)=>Err(e)?,
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum ExtractTextureError{
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("MeshLoad error {0:?}")]
|
||||
MeshLoad(#[from]strafesnet_bsp_loader::loader::MeshError),
|
||||
#[error("Load VMT error {0:?}")]
|
||||
LoadVMT(#[from]LoadVMTError),
|
||||
}
|
||||
async fn gimme_them_textures(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],send_texture:tokio::sync::mpsc::Sender<(Vec<u8>,String)>)->Result<(),ExtractTextureError>{
|
||||
let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
|
||||
let loader_bsp=strafesnet_bsp_loader::Bsp::new(bsp);
|
||||
let bsp=loader_bsp.as_ref();
|
||||
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
for texture in bsp.textures(){
|
||||
texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
|
||||
}
|
||||
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
for prop in bsp.static_props(){
|
||||
mesh_deferred_loader.acquire_mesh_id(prop.model());
|
||||
}
|
||||
|
||||
let finder=BspFinder{
|
||||
bsp:&loader_bsp,
|
||||
vpks:vpk_list
|
||||
};
|
||||
|
||||
let mut mesh_loader=strafesnet_bsp_loader::loader::ModelLoader::new(finder);
|
||||
// load models and collect requested textures
|
||||
for model_path in mesh_deferred_loader.into_indices(){
|
||||
let model:vmdl::Model=match mesh_loader.load(model_path){
|
||||
Ok(model)=>model,
|
||||
Err(e)=>{
|
||||
println!("Model={model_path} Load model error: {e}");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
for texture in model.textures(){
|
||||
for search_path in &texture.search_paths{
|
||||
let mut path=PathBuf::from(search_path.as_str());
|
||||
path.push(texture.name.as_str());
|
||||
let path=path.to_str().unwrap().to_owned();
|
||||
texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for texture_path in texture_deferred_loader.into_indices(){
|
||||
match load_texture(finder,&texture_path){
|
||||
Ok(Some(texture))=>send_texture.send(
|
||||
(texture.into_owned(),texture_path.into_owned())
|
||||
).await.unwrap(),
|
||||
Ok(None)=>(),
|
||||
Err(e)=>println!("Texture={texture_path} Load error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum ConvertTextureError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("Vtf error {0:?}")]
|
||||
Vtf(#[from]vtf::Error),
|
||||
#[error("DDS create error {0:?}")]
|
||||
DDS(#[from]image_dds::CreateDdsError),
|
||||
#[error("DDS write error {0:?}")]
|
||||
DDSWrite(#[from]image_dds::ddsfile::Error),
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
}
|
||||
|
||||
async fn convert_texture(texture:Vec<u8>,write_file_name:impl AsRef<Path>)->Result<(),ConvertTextureError>{
|
||||
let image=vtf::from_bytes(&texture)?.highres_image.decode(0)?.to_rgba8();
|
||||
|
||||
let format=if image.width()%4!=0||image.height()%4!=0{
|
||||
image_dds::ImageFormat::Rgba8UnormSrgb
|
||||
}else{
|
||||
image_dds::ImageFormat::BC7RgbaUnormSrgb
|
||||
};
|
||||
//this fails if the image dimensions are not a multiple of 4
|
||||
let dds = image_dds::dds_from_image(
|
||||
&image,
|
||||
format,
|
||||
image_dds::Quality::Slow,
|
||||
image_dds::Mipmaps::GeneratedAutomatic,
|
||||
)?;
|
||||
|
||||
//write dds
|
||||
let mut dest=PathBuf::from("textures");
|
||||
dest.push(write_file_name);
|
||||
dest.set_extension("dds");
|
||||
std::fs::create_dir_all(dest.parent().unwrap())?;
|
||||
let mut writer=std::io::BufWriter::new(std::fs::File::create(dest)?);
|
||||
dds.write(&mut writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_vpks(vpk_paths:Vec<PathBuf>,thread_limit:usize)->Vec<strafesnet_bsp_loader::Vpk>{
|
||||
futures::stream::iter(vpk_paths).map(|vpk_path|async{
|
||||
// idk why it doesn't want to pass out the errors but this is fatal anyways
|
||||
tokio::task::spawn_blocking(move||Ok::<_,vpk::Error>(strafesnet_bsp_loader::Vpk::new(vpk::VPK::read(&vpk_path)?))).await.unwrap().unwrap()
|
||||
})
|
||||
.buffer_unordered(thread_limit)
|
||||
.collect().await
|
||||
}
|
||||
|
||||
async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
|
||||
tokio::try_join!(
|
||||
tokio::fs::create_dir_all("extracted_textures"),
|
||||
tokio::fs::create_dir_all("textures"),
|
||||
tokio::fs::create_dir_all("meshes"),
|
||||
)?;
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
|
||||
// load vpk list and leak for static lifetime
|
||||
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
|
||||
|
||||
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
|
||||
let mut it=paths.into_iter();
|
||||
let extract_thread=tokio::spawn(async move{
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let send=send_texture.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=gimme_them_textures(&path,vpk_list,send).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Map={path:?} Decode error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// convert images
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some((data,dest)))=(SEM.acquire().await,recv_texture.recv().await){
|
||||
// TODO: dedup dest?
|
||||
tokio::spawn(async move{
|
||||
let result=convert_texture(data,dest).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
extract_thread.await?;
|
||||
_=SEM.acquire_many(thread_limit as u32).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
|
||||
let vpk_index=vpk::VPK::read(&vpk_path)?;
|
||||
for (label,entry) in vpk_index.tree.into_iter(){
|
||||
println!("vpk label={} entry={:?}",label,entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bsp_contents(path:PathBuf)->AResult<()>{
|
||||
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
|
||||
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
|
||||
println!("file_name={:?}",file_name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
BspRead(strafesnet_bsp_loader::ReadError),
|
||||
BspLoad(strafesnet_bsp_loader::LoadError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
|
||||
async fn convert_to_snf(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],output_folder:PathBuf)->AResult<()>{
|
||||
let entire_file=tokio::fs::read(path).await?;
|
||||
|
||||
let bsp=strafesnet_bsp_loader::read(
|
||||
std::io::Cursor::new(entire_file)
|
||||
).map_err(ConvertError::BspRead)?;
|
||||
|
||||
let map=bsp.to_snf(LoadFailureMode::DefaultToNone,vpk_list).map_err(ConvertError::BspLoad)?;
|
||||
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
|
||||
|
||||
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn source_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
|
||||
let start=std::time::Instant::now();
|
||||
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
|
||||
// load vpk list and leak for static lifetime
|
||||
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
|
||||
|
||||
let mut it=paths.into_iter();
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let output_folder=output_folder.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=convert_to_snf(path.as_path(),vpk_list,output_folder).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
|
||||
println!("elapsed={:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafe-client"
|
||||
version = "0.10.5"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "Custom"
|
||||
@ -9,24 +9,24 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
user-install=[] # as opposed to portable install
|
||||
default = ["snf"]
|
||||
snf = ["dep:strafesnet_snf"]
|
||||
source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
|
||||
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.13.1", features = ["derive"] }
|
||||
configparser = "3.0.2"
|
||||
ddsfile = "0.5.1"
|
||||
glam = "0.29.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
parking_lot = "0.12.1"
|
||||
pollster = "0.4.0"
|
||||
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
|
||||
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { path = "../lib/deferred_loader", features = ["legacy"], registry = "strafesnet", optional = true }
|
||||
strafesnet_deferred_loader = { path = "../lib/deferred_loader", registry = "strafesnet", optional = true }
|
||||
strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
|
||||
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
|
||||
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
|
||||
strafesnet_roblox_bot_file = { version = "0.2.0", registry = "strafesnet" }
|
||||
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
|
||||
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
|
||||
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
|
||||
wgpu = "23.0.1"
|
||||
wgpu = "24.0.0"
|
||||
winit = "0.30.7"
|
||||
|
68
strafe-client/src/app.rs
Normal file
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,9 @@
|
||||
use std::io::Read;
|
||||
|
||||
#[cfg(any(feature="roblox",feature="source"))]
|
||||
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ReadError{
|
||||
#[cfg(feature="roblox")]
|
||||
@ -10,6 +14,8 @@ pub enum ReadError{
|
||||
StrafesNET(strafesnet_snf::Error),
|
||||
#[cfg(feature="snf")]
|
||||
StrafesNETMap(strafesnet_snf::map::Error),
|
||||
#[cfg(feature="snf")]
|
||||
StrafesNETBot(strafesnet_snf::bot::Error),
|
||||
Io(std::io::Error),
|
||||
UnknownFileFormat,
|
||||
}
|
||||
@ -20,37 +26,52 @@ impl std::fmt::Display for ReadError{
|
||||
}
|
||||
impl std::error::Error for ReadError{}
|
||||
|
||||
pub enum DataStructure{
|
||||
pub enum ReadFormat{
|
||||
#[cfg(feature="roblox")]
|
||||
Roblox(strafesnet_rbx_loader::Model),
|
||||
#[cfg(feature="source")]
|
||||
Source(strafesnet_bsp_loader::Bsp),
|
||||
#[cfg(feature="snf")]
|
||||
StrafesNET(strafesnet_common::map::CompleteMap),
|
||||
SNFM(strafesnet_common::map::CompleteMap),
|
||||
#[cfg(feature="snf")]
|
||||
SNFB(strafesnet_snf::bot::Segment),
|
||||
}
|
||||
|
||||
pub fn read<R:Read+std::io::Seek>(input:R)->Result<DataStructure,ReadError>{
|
||||
pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
|
||||
let mut buf=std::io::BufReader::new(input);
|
||||
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
|
||||
match &peek[0..4]{
|
||||
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned();
|
||||
// reading the entire file is way faster than round tripping the disk constantly
|
||||
let mut entire_file=Vec::new();
|
||||
buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?;
|
||||
let cursor=std::io::Cursor::new(entire_file);
|
||||
match peek.as_slice(){
|
||||
#[cfg(feature="roblox")]
|
||||
b"<rob"=>Ok(DataStructure::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)),
|
||||
b"<rob"=>Ok(ReadFormat::Roblox(strafesnet_rbx_loader::read(cursor).map_err(ReadError::Roblox)?)),
|
||||
#[cfg(feature="source")]
|
||||
b"VBSP"=>Ok(DataStructure::Source(strafesnet_bsp_loader::read(buf).map_err(ReadError::Source)?)),
|
||||
b"VBSP"=>Ok(ReadFormat::Source(strafesnet_bsp_loader::read(cursor).map_err(ReadError::Source)?)),
|
||||
#[cfg(feature="snf")]
|
||||
b"SNFM"=>Ok(DataStructure::StrafesNET(
|
||||
strafesnet_snf::read_map(buf).map_err(ReadError::StrafesNET)?
|
||||
b"SNFM"=>Ok(ReadFormat::SNFM(
|
||||
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
|
||||
.into_complete_map().map_err(ReadError::StrafesNETMap)?
|
||||
)),
|
||||
#[cfg(feature="snf")]
|
||||
b"SNFB"=>Ok(ReadFormat::SNFB(
|
||||
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
|
||||
.read_all().map_err(ReadError::StrafesNETBot)?
|
||||
)),
|
||||
_=>Err(ReadError::UnknownFileFormat),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError{
|
||||
ReadError(ReadError),
|
||||
File(std::io::Error),
|
||||
Io(std::io::Error),
|
||||
#[cfg(feature="roblox")]
|
||||
LoadRoblox(strafesnet_rbx_loader::LoadError),
|
||||
#[cfg(feature="source")]
|
||||
LoadSource(strafesnet_bsp_loader::LoadError),
|
||||
}
|
||||
impl std::fmt::Display for LoadError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
@ -59,86 +80,32 @@ impl std::fmt::Display for LoadError{
|
||||
}
|
||||
impl std::error::Error for LoadError{}
|
||||
|
||||
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::CompleteMap,LoadError>{
|
||||
pub enum LoadFormat{
|
||||
#[cfg(any(feature="snf",feature="roblox",feature="source"))]
|
||||
Map(strafesnet_common::map::CompleteMap),
|
||||
#[cfg(feature="snf")]
|
||||
Bot(strafesnet_snf::bot::Segment),
|
||||
}
|
||||
|
||||
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
|
||||
//blocking because it's simpler...
|
||||
let file=std::fs::File::open(path).map_err(LoadError::File)?;
|
||||
match read(file).map_err(LoadError::ReadError)?{
|
||||
#[cfg(feature="snf")]
|
||||
DataStructure::StrafesNET(map)=>Ok(map),
|
||||
ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
|
||||
#[cfg(feature="snf")]
|
||||
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
|
||||
#[cfg(feature="roblox")]
|
||||
DataStructure::Roblox(model)=>{
|
||||
ReadFormat::Roblox(model)=>{
|
||||
let mut place=model.into_place();
|
||||
place.run_scripts();
|
||||
|
||||
let mut loader=strafesnet_deferred_loader::roblox_legacy();
|
||||
|
||||
let (texture_loader,mesh_loader)=loader.get_inner_mut();
|
||||
|
||||
let map_step1=strafesnet_rbx_loader::convert(
|
||||
&place,
|
||||
|name|texture_loader.acquire_render_config_id(name),
|
||||
|name|mesh_loader.acquire_mesh_id(name),
|
||||
);
|
||||
|
||||
let meshpart_meshes=mesh_loader.load_meshes().map_err(LoadError::Io)?;
|
||||
|
||||
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 (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
|
||||
|
||||
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,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
Ok(map)
|
||||
Ok(LoadFormat::Map(
|
||||
place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?
|
||||
))
|
||||
},
|
||||
#[cfg(feature="source")]
|
||||
DataStructure::Source(bsp)=>{
|
||||
let mut loader=strafesnet_deferred_loader::source_legacy();
|
||||
|
||||
let (texture_loader,mesh_loader)=loader.get_inner_mut();
|
||||
|
||||
let map_step1=strafesnet_bsp_loader::convert(
|
||||
&bsp,
|
||||
|name|texture_loader.acquire_render_config_id(name),
|
||||
|name|mesh_loader.acquire_mesh_id(name),
|
||||
);
|
||||
|
||||
let prop_meshes=mesh_loader.load_meshes(bsp.as_ref());
|
||||
|
||||
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 (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
|
||||
|
||||
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,
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
Ok(map)
|
||||
},
|
||||
ReadFormat::Source(bsp)=>Ok(LoadFormat::Map(
|
||||
bsp.to_snf(LoadFailureMode::DefaultToNone,&[]).map_err(LoadError::LoadSource)?
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
use strafesnet_graphics::graphics;
|
||||
use strafesnet_session::session;
|
||||
use strafesnet_settings::settings;
|
||||
|
||||
pub enum Instruction{
|
||||
Render(crate::graphics::FrameState),
|
||||
//UpdateModel(crate::graphics::GraphicsModelUpdate),
|
||||
Resize(winit::dpi::PhysicalSize<u32>,crate::settings::UserSettings),
|
||||
Render(session::FrameState),
|
||||
//UpdateModel(graphics::GraphicsModelUpdate),
|
||||
Resize(winit::dpi::PhysicalSize<u32>,settings::UserSettings),
|
||||
ChangeMap(strafesnet_common::map::CompleteMap),
|
||||
}
|
||||
|
||||
@ -14,14 +18,13 @@ WorkerDescription{
|
||||
*/
|
||||
//up to three frames in flight, dropping new frame requests when all three are busy, and dropping output frames when one renders out of order
|
||||
|
||||
pub fn new<'a>(
|
||||
mut graphics:crate::graphics::GraphicsState,
|
||||
pub fn new(
|
||||
mut graphics:graphics::GraphicsState,
|
||||
mut config:wgpu::SurfaceConfiguration,
|
||||
surface:wgpu::Surface<'a>,
|
||||
surface:wgpu::Surface,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
)->crate::compat_worker::INWorker<'a,Instruction>{
|
||||
let mut resize=None;
|
||||
)->crate::compat_worker::INWorker<'_,Instruction>{
|
||||
crate::compat_worker::INWorker::new(move |ins:Instruction|{
|
||||
match ins{
|
||||
Instruction::ChangeMap(map)=>{
|
||||
@ -29,10 +32,6 @@ pub fn new<'a>(
|
||||
graphics.generate_models(&device,&queue,&map);
|
||||
},
|
||||
Instruction::Resize(size,user_settings)=>{
|
||||
resize=Some((size,user_settings));
|
||||
}
|
||||
Instruction::Render(frame_state)=>{
|
||||
if let Some((size,user_settings))=resize.take(){
|
||||
println!("Resizing to {:?}",size);
|
||||
let t0=std::time::Instant::now();
|
||||
config.width=size.width.max(1);
|
||||
@ -41,6 +40,7 @@ pub fn new<'a>(
|
||||
graphics.resize(&device,&config,&user_settings);
|
||||
println!("Resize took {:?}",t0.elapsed());
|
||||
}
|
||||
Instruction::Render(frame_state)=>{
|
||||
//this has to go deeper somehow
|
||||
let frame=match surface.get_current_texture(){
|
||||
Ok(frame)=>frame,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user